11#include <nlohmann/json.hpp>
19using json = nlohmann::json;
42 if (
b < 0x80) {
n = 0; }
43 else if (
b >= 0xC2 &&
b < 0xE0) {
n = 1; }
44 else if (
b >= 0xE0 &&
b < 0xF0) {
n = 2; }
45 else if (
b >= 0xF0 &&
b < 0xF5) {
n = 3; }
57 else if (
lead == 0xED) {
ok =
ok &&
c1 <= 0x9F; }
58 else if (
lead == 0xF0) {
ok =
ok &&
c1 >= 0x90; }
59 else if (
lead == 0xF4) {
ok =
ok &&
c1 <= 0x8F; }
73 return ok ?
static_cast<size_t>(
n + 1) : 0;
84 static constexpr const char*
kRepl =
"\xEF\xBF\xBD";
85 const auto*
p =
reinterpret_cast<const uint8_t*
>(
s.data());
86 const auto*
end =
p +
s.size();
88 out.reserve(
s.size());
92 out.append(
reinterpret_cast<const char*
>(
p),
len);
116 return p ?
reinterpret_cast<const char*
>(
p) :
"";
129 if (!
p)
return std::nullopt;
130 return std::string(
reinterpret_cast<const char*
>(
p));
155 const std::optional<std::string>&
val) {
172 const std::optional<int>&
val) {
189 const std::filesystem::path& db_path)
223 const std::string& title,
224 const std::optional<std::string>& project_path,
225 const std::optional<std::string>& model_id) {
229 "INSERT INTO conversations "
230 "(id, title, created_at, updated_at, project_path, model_id, metadata) "
231 "VALUES (?, ?, ?, ?, ?, ?, ?)",
246 logger->error(
"Failed to create conversation '{}' — INSERT did not "
247 "land, returning empty id",
rec.id);
248 return std::string{};
250 logger->info(
"Created conversation: {}",
rec.id);
261 std::string tool_calls;
262 std::string tool_results;
263 long long token_count;
265 std::optional<std::string> tier;
278 r.role =
m.value(
"role",
"");
279 r.content =
m.value(
"content",
"");
280 r.tool_calls =
m.contains(
"tool_calls") ?
m[
"tool_calls"].dump() :
"[]";
282 m.contains(
"tool_results") ?
m[
"tool_results"].dump() :
"[]";
283 r.token_count =
m.value(
"token_count", 0);
284 r.is_compacted =
m.value(
"is_compacted",
false);
285 if (
m.contains(
"identity_tier") && !
m[
"identity_tier"].is_null()) {
286 r.tier =
m[
"identity_tier"].get<std::string>();
301 const std::string& now,
const MessageRow&
r) {
325 const std::string& conversation_id,
326 const std::string& messages_json) {
329 "UPDATE conversations SET updated_at = ? WHERE id = ?",
335 auto msgs = json::parse(messages_json,
nullptr,
false);
336 if (!
msgs.is_array()) {
337 logger->error(
"save_messages: invalid JSON array");
342 "INSERT INTO messages "
343 "(id, conversation_id, role, content, tool_calls, tool_results, "
344 "token_count, created_at, is_compacted, identity_tier) "
345 "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
350 for (
const auto&
m :
msgs) {
358 logger->error(
"save_messages: one or more rows failed to insert for "
359 "conversation '{}' ({} message(s) attempted)",
360 conversation_id,
msgs.size());
382 o[
"metadata"] = json::parse(
col_text(
s, 6),
nullptr,
false);
398 m[
"tool_calls"] = json::parse(
col_text(
s, 4),
nullptr,
false);
399 m[
"tool_results"] = json::parse(
col_text(
s, 5),
nullptr,
false);
418 const std::string& conversation_id,
422 "SELECT * FROM conversations WHERE id = ?",
428 if (!
found)
return false;
430 json messages = json::array();
432 "SELECT * FROM messages WHERE conversation_id = ? "
433 "ORDER BY created_at ASC",
442 result[
"conversation"] = std::move(
conv_obj);
443 result[
"messages"] = std::move(messages);
459 json arr = json::array();
461 "SELECT c.id, c.title, c.updated_at, c.project_path, "
462 "COUNT(m.id) as message_count "
463 "FROM conversations c "
464 "LEFT JOIN messages m ON c.id = m.conversation_id "
466 "ORDER BY c.updated_at DESC "
479 arr.push_back(std::move(entry));
495 const std::string& conversation_id) {
497 "DELETE FROM conversations WHERE id = ?",
501 if (
ok) logger->info(
"Deleted conversation: {}", conversation_id);
514 const std::string& conversation_id,
515 const std::string& title) {
517 "UPDATE conversations SET title = ? WHERE id = ?",
538 json arr = json::array();
540 "SELECT DISTINCT c.id, c.title, c.updated_at, "
541 "snippet(messages_fts, 0, '>>>', '<<<', '...', 32) as snippet "
543 "JOIN messages m ON messages_fts.rowid = m.rowid "
544 "JOIN conversations c ON m.conversation_id = c.id "
545 "WHERE messages_fts MATCH ? "
558 arr.push_back(std::move(entry));
620 const std::string& parent_conversation_id,
621 const std::string& delegating_tier,
622 const std::string& target_tier,
623 std::string& delegation_id,
624 std::string& child_conversation_id) {
625 if (!parent_conversation_id.empty()) {
return true; }
626 logger->error(
"create_delegation refused: parent_conversation_id "
627 "is empty (delegating_tier={}, target_tier={}). "
628 "Root conversation must be created before "
629 "delegating (gh#48).",
630 delegating_tier, target_tier);
631 delegation_id.clear();
632 child_conversation_id.clear();
650 const std::string& parent_conversation_id,
651 const std::string& delegating_tier,
652 const std::string& target_tier,
653 const std::string& task,
655 std::string& delegation_id,
656 std::string& child_conversation_id) {
658 delegating_tier, target_tier,
660 child_conversation_id)) {
664 auto child_title =
"Delegation: " + target_tier +
" — " +
670 parent_conversation_id, child_conversation_id,
671 delegating_tier, target_tier, task);
672 if (max_turns > 0)
rec.max_turns = max_turns;
673 rec.status =
"running";
676 "INSERT INTO delegations "
677 "(id, parent_conversation_id, child_conversation_id, "
678 "delegating_tier, target_tier, task, max_turns, "
679 "status, result_summary, created_at, completed_at) "
680 "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
683 delegation_id =
rec.id;
690 logger->info(
"Created delegation {}: {} -> {}",
691 rec.id, delegating_tier, target_tier);
693 logger->error(
"Failed to insert delegation {} ({} -> {}): "
694 "parent_conversation_id='{}' — check the SQL "
695 "execute error logged immediately above",
696 rec.id, delegating_tier, target_tier,
697 parent_conversation_id);
712 const std::string& delegation_id,
713 const std::string& status,
714 const std::optional<std::string>& result_summary) {
717 "UPDATE delegations "
718 "SET status = ?, result_summary = ?, completed_at = ? "
740 entry[
"parent_conversation_id"] =
col_text(
s, 1);
741 entry[
"child_conversation_id"] =
col_text(
s, 2);
742 entry[
"delegating_tier"] =
col_text(
s, 3);
767 entry[
"parent_conversation_id"] =
col_text(
s, 1);
768 entry[
"child_conversation_id"] =
col_text(
s, 2);
769 entry[
"delegating_tier"] =
col_text(
s, 3);
790 const std::string& conversation_id,
792 json arr = json::array();
794 "SELECT * FROM delegations "
795 "WHERE parent_conversation_id = ? "
796 "ORDER BY created_at ASC",
818 const std::string& delegation_id,
823 "SELECT * FROM delegations WHERE id = ? LIMIT 1",
849 const std::string& target_tier, std::string& delegation_id) {
852 "SELECT id FROM delegations "
853 "WHERE target_tier = ? AND status = 'completed' "
854 "ORDER BY completed_at DESC LIMIT 1",
861 if (text !=
nullptr) {
862 delegation_id =
reinterpret_cast<const char*
>(text);
887 json arr = json::array();
890 "SELECT * FROM delegations "
891 "WHERE result_summary LIKE ? AND status = 'completed' "
892 "ORDER BY completed_at DESC LIMIT ?",
915 const std::string& conversation_id,
916 const std::string& messages_json) {
921 auto msgs = json::parse(messages_json,
nullptr,
false);
925 "INSERT INTO compaction_snapshots "
926 "(id, conversation_id, messages_json, message_count, "
927 "token_count_estimate, created_at) "
928 "VALUES (?, ?, ?, ?, NULL, ?)",
952 db_.
fetch_one(
"SELECT COUNT(*) FROM conversations",
956 db_.
fetch_one(
"SELECT COUNT(*) FROM messages",
960 db_.
fetch_one(
"SELECT COALESCE(SUM(token_count), 0) FROM messages",
981 static thread_local std::mt19937
gen(std::random_device{}());
982 std::uniform_int_distribution<uint32_t>
dist(0, 15);
983 std::uniform_int_distribution<uint32_t>
dist2(8, 11);
985 const char hex[] =
"0123456789abcdef";
986 std::string
uuid(36,
'-');
990 0,1,2,3,4,5,6,7, 9,10,11,12, 14,15,16,17,
991 19,20,21,22, 24,25,26,27,28,29,30,31,32,33,34,35
1010 auto now = std::chrono::system_clock::now();
1011 auto time = std::chrono::system_clock::to_time_t(now);
1016 std::strftime(
buf,
sizeof(
buf),
"%Y-%m-%dT%H:%M:%S", &
utc);
1030 const std::string& title,
1031 const std::optional<std::string>& project_path,
1032 const std::optional<std::string>& model_id) {
1034 return {
generate_uuid(), title, now, now, project_path, model_id,
"{}"};
1050 const std::string& parent_conversation_id,
1051 const std::string& child_conversation_id,
1052 const std::string& delegating_tier,
1053 const std::string& target_tier,
1054 const std::string& task) {
1056 child_conversation_id, delegating_tier, target_tier,
1057 task, std::nullopt,
"pending", std::nullopt,
bool execute(std::string_view sql, std::function< void(sqlite3_stmt *)> binder=nullptr)
Execute a write statement (INSERT, UPDATE, DELETE).
void close()
Close database connection.
size_t fetch_all(std::string_view sql, std::function< void(sqlite3_stmt *)> binder, std::function< void(sqlite3_stmt *)> row_handler)
Fetch all matching rows.
bool fetch_one(std::string_view sql, std::function< void(sqlite3_stmt *)> binder, std::function< void(sqlite3_stmt *)> extractor)
Fetch a single row.
bool initialize()
Initialize database and run pending migrations.
bool save_messages(const std::string &conversation_id, const std::string &messages_json)
Save messages to a conversation.
bool get_delegation_by_id(const std::string &delegation_id, std::string &result_json)
Look up a single delegation record by id (gh#32, v2.1.6).
bool complete_delegation(const std::string &delegation_id, const std::string &status, const std::optional< std::string > &result_summary=std::nullopt)
Mark a delegation as completed or failed.
bool latest_delegation_for_target(const std::string &target_tier, std::string &delegation_id)
Search delegations across all conversations (gh#32, v2.1.6).
bool update_title(const std::string &conversation_id, const std::string &title)
Update a conversation's title.
SqliteStorageBackend(const std::filesystem::path &db_path)
Construct with database file path.
bool create_delegation(const std::string &parent_conversation_id, const std::string &delegating_tier, const std::string &target_tier, const std::string &task, int max_turns, std::string &delegation_id, std::string &child_conversation_id)
Create a delegation record with a child conversation.
bool search_conversations(const std::string &query, int limit, std::string &result_json)
Full-text search across conversations.
bool get_delegations(const std::string &conversation_id, std::string &result_json)
Get delegations for a parent conversation.
bool save_snapshot(const std::string &conversation_id, const std::string &messages_json)
Save a pre-compaction snapshot of full conversation history.
std::string create_conversation(const std::string &title="New Conversation", const std::optional< std::string > &project_path=std::nullopt, const std::optional< std::string > &model_id=std::nullopt)
Create a new conversation.
void close()
Close storage and database connection.
bool load_conversation(const std::string &conversation_id, std::string &result_json)
Load a conversation with messages.
bool get_stats(std::string &result_json)
Get storage statistics.
bool delete_conversation(const std::string &conversation_id)
Delete a conversation and all associated records.
bool search_delegations(const std::string &query, int max_results, std::string &result_json)
Substring-match delegations across all conversations.
bool list_conversations(int limit, int offset, std::string &result_json)
List conversations with pagination.
bool initialize()
Initialize storage (open database, run migrations).
long limit
Results to keep.
long offset
Results to skip (from a previous next_offset)
spdlog initialization and logger access.
auto now()
Get current time for timing measurements.
ENTROPIC_EXPORT std::shared_ptr< spdlog::logger > get(const std::string &name)
Get or create a named logger.
Activate model on GPU (WARM → ACTIVE).
static std::string col_text(sqlite3_stmt *stmt, int col)
Get text from a sqlite3 column, returning empty string for NULL.
std::string utc_timestamp()
Get current UTC time as ISO 8601 string.
@ ok
Tool dispatched, returned non-empty content.
@ optional
optional: every escape asks the host's path approver
static void bind_opt_int(sqlite3_stmt *stmt, int idx, const std::optional< int > &val)
Bind optional int to a parameter position (NULL if unset).
static void bind_opt_text(sqlite3_stmt *stmt, int idx, const std::optional< std::string > &val)
Bind optional text to a parameter position.
static bool guard_parent_conversation(const std::string &parent_conversation_id, const std::string &delegating_tier, const std::string &target_tier, std::string &delegation_id, std::string &child_conversation_id)
Reject an empty parent_conversation_id with a clear error and reset the out params (gh#48 defense-in-...
std::string generate_uuid()
Generate a UUID v4 string.
static void bind_delegation_insert(sqlite3_stmt *s, const DelegationRecord &rec)
Bind a Delegation record onto the delegations-INSERT statement's 11 placeholders.
ConversationRecord make_conversation(const std::string &title="New Conversation", const std::optional< std::string > &project_path=std::nullopt, const std::optional< std::string > &model_id=std::nullopt)
Create a new ConversationRecord with generated UUID and timestamps.
static json col_nullable_int(sqlite3_stmt *stmt, int col)
Get a nullable integer column as a JSON value.
static std::optional< std::string > col_opt_text(sqlite3_stmt *stmt, int col)
Get optional text from a sqlite3 column.
DelegationRecord make_delegation(const std::string &parent_conversation_id, const std::string &child_conversation_id, const std::string &delegating_tier, const std::string &target_tier, const std::string &task)
Create a new DelegationRecord with generated UUID and timestamp.
SqliteStorageBackend — conversation persistence via SQLite.
Database record for a conversation.
Database record for a delegation.