Entropic 2.14.3
Local-first agentic inference engine
Loading...
Searching...
No Matches
backend.cpp
Go to the documentation of this file.
1// SPDX-License-Identifier: Apache-2.0
9
11#include <nlohmann/json.hpp>
12#include <sqlite3.h>
13
14#include <chrono>
15#include <cstdint>
16#include <cstring>
17#include <random>
18
19using json = nlohmann::json;
20
21namespace entropic {
22
23namespace {
24
25auto logger = entropic::log::get("storage.backend");
26
27// ── UTF-8 sanitizer (gh#112/gh#113) ──────────────────────────────────────────
28// Mirrors mcp::sanitize_utf8 but lives here so entropic-storage stays
29// self-contained (no dependency on entropic-mcp or core).
30// Called at the delegation read boundary to prevent type_error.316.
31
33inline bool su8_is_cont(uint8_t b) { return (b & 0xC0) == 0x80; }
34
40inline int su8_follow(uint8_t b) {
41 int n = -1;
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; }
46 return n;
47}
48
55 bool ok = su8_is_cont(c1);
56 if (lead == 0xE0) { ok = ok && c1 >= 0xA0; }
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; }
60 return ok;
61}
62
68size_t su8_seq_len(const uint8_t* p, const uint8_t* end) {
69 int n = su8_follow(*p);
70 bool ok = (n >= 0)
71 && (n == 0 || (p + n < end && su8_first_cont_ok(*p, p[1])));
72 for (int i = 2; ok && i <= n; ++i) { ok = su8_is_cont(p[i]); }
73 return ok ? static_cast<size_t>(n + 1) : 0;
74}
75
83std::string sanitize_storage_utf8(const std::string& s) {
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();
87 std::string out;
88 out.reserve(s.size());
89 while (p < end) {
90 size_t len = su8_seq_len(p, end);
91 if (len > 0) {
92 out.append(reinterpret_cast<const char*>(p), len);
93 p += len;
94 } else {
95 out.append(kRepl, 3);
96 ++p;
97 }
98 }
99 return out;
100}
101
102} // anonymous namespace
103
104// ── Helpers ───────────────────────────────────────────────
105
114static std::string col_text(sqlite3_stmt* stmt, int col) {
115 auto* p = sqlite3_column_text(stmt, col);
116 return p ? reinterpret_cast<const char*>(p) : "";
117}
118
127static std::optional<std::string> col_opt_text(sqlite3_stmt* stmt, int col) {
128 auto* p = sqlite3_column_text(stmt, col);
129 if (!p) return std::nullopt;
130 return std::string(reinterpret_cast<const char*>(p));
131}
132
142 if (sqlite3_column_type(stmt, col) == SQLITE_NULL) { return json(nullptr); }
143 return json(sqlite3_column_int(stmt, col));
144}
145
155 const std::optional<std::string>& val) {
156 if (val) {
158 } else {
160 }
161}
162
172 const std::optional<int>& val) {
173 if (val) {
175 } else {
177 }
178}
179
180// ── SqliteStorageBackend ──────────────────────────────────
181
189 const std::filesystem::path& db_path)
190 : db_(db_path) {}
191
199 return db_.initialize();
200}
201
208 db_.close();
209}
210
211// ── Conversation CRUD ─────────────────────────────────────
212
223 const std::string& title,
224 const std::optional<std::string>& project_path,
225 const std::optional<std::string>& model_id) {
226 auto rec = make_conversation(title, project_path, model_id);
227
228 const bool ok = db_.execute(
229 "INSERT INTO conversations "
230 "(id, title, created_at, updated_at, project_path, model_id, metadata) "
231 "VALUES (?, ?, ?, ?, ?, ?, ?)",
232 [&](sqlite3_stmt* s) {
233 sqlite3_bind_text(s, 1, rec.id.c_str(), -1, SQLITE_TRANSIENT);
234 sqlite3_bind_text(s, 2, rec.title.c_str(), -1, SQLITE_TRANSIENT);
235 sqlite3_bind_text(s, 3, rec.created_at.c_str(), -1, SQLITE_TRANSIENT);
236 sqlite3_bind_text(s, 4, rec.updated_at.c_str(), -1, SQLITE_TRANSIENT);
237 bind_opt_text(s, 5, rec.project_path);
238 bind_opt_text(s, 6, rec.model_id);
239 sqlite3_bind_text(s, 7, rec.metadata.c_str(), -1, SQLITE_TRANSIENT);
240 });
241
242 // gh: fail loud. Returning the generated id after a failed INSERT told the
243 // caller a conversation existed when it did not — every later reference to
244 // that id then failed for a reason with no connection to the real cause.
245 if (!ok) {
246 logger->error("Failed to create conversation '{}' — INSERT did not "
247 "land, returning empty id", rec.id);
248 return std::string{};
249 }
250 logger->info("Created conversation: {}", rec.id);
251 return rec.id;
252}
253
254namespace {
255
257struct MessageRow {
258 std::string id;
259 std::string role;
260 std::string content;
261 std::string tool_calls;
262 std::string tool_results;
263 long long token_count;
264 bool is_compacted;
265 std::optional<std::string> tier;
266};
267
275MessageRow build_message_row(const json& m) {
276 MessageRow r;
277 r.id = generate_uuid();
278 r.role = m.value("role", "");
279 r.content = m.value("content", "");
280 r.tool_calls = m.contains("tool_calls") ? m["tool_calls"].dump() : "[]";
281 r.tool_results =
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>();
287 }
288 return r;
289}
290
300void bind_message_insert(sqlite3_stmt* s, const std::string& conversation_id,
301 const std::string& now, const MessageRow& r) {
302 sqlite3_bind_text(s, 1, r.id.c_str(), -1, SQLITE_TRANSIENT);
303 sqlite3_bind_text(s, 2, conversation_id.c_str(), -1, SQLITE_TRANSIENT);
304 sqlite3_bind_text(s, 3, r.role.c_str(), -1, SQLITE_TRANSIENT);
305 sqlite3_bind_text(s, 4, r.content.c_str(), -1, SQLITE_TRANSIENT);
306 sqlite3_bind_text(s, 5, r.tool_calls.c_str(), -1, SQLITE_TRANSIENT);
307 sqlite3_bind_text(s, 6, r.tool_results.c_str(), -1, SQLITE_TRANSIENT);
308 sqlite3_bind_int64(s, 7, r.token_count);
309 sqlite3_bind_text(s, 8, now.c_str(), -1, SQLITE_TRANSIENT);
310 sqlite3_bind_int(s, 9, r.is_compacted ? 1 : 0);
311 bind_opt_text(s, 10, r.tier);
312}
313
314} // namespace
315
325 const std::string& conversation_id,
326 const std::string& messages_json) {
327 auto now = utc_timestamp();
328 db_.execute(
329 "UPDATE conversations SET updated_at = ? WHERE id = ?",
330 [&](sqlite3_stmt* s) {
331 sqlite3_bind_text(s, 1, now.c_str(), -1, SQLITE_TRANSIENT);
332 sqlite3_bind_text(s, 2, conversation_id.c_str(), -1, SQLITE_TRANSIENT);
333 });
334
335 auto msgs = json::parse(messages_json, nullptr, false);
336 if (!msgs.is_array()) {
337 logger->error("save_messages: invalid JSON array");
338 return false;
339 }
340
341 static constexpr const char* kInsertSql =
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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
346 // gh: every row result was previously discarded and the function returned
347 // true unconditionally, so a caller could not tell a persisted turn from a
348 // lost one. Insert all rows, then report whether all of them landed.
349 bool all_ok = true;
350 for (const auto& m : msgs) {
351 auto row = build_message_row(m);
352 all_ok &= db_.execute(kInsertSql, [&](sqlite3_stmt* s) {
353 bind_message_insert(s, conversation_id, now, row);
354 });
355 }
356
357 if (!all_ok) {
358 logger->error("save_messages: one or more rows failed to insert for "
359 "conversation '{}' ({} message(s) attempted)",
360 conversation_id, msgs.size());
361 }
362 return all_ok;
363}
364
365namespace {
366
375 json o;
376 o["id"] = col_text(s, 0);
377 o["title"] = col_text(s, 1);
378 o["created_at"] = col_text(s, 2);
379 o["updated_at"] = col_text(s, 3);
380 o["project_path"] = col_opt_text(s, 4).value_or("");
381 o["model_id"] = col_opt_text(s, 5).value_or("");
382 o["metadata"] = json::parse(col_text(s, 6), nullptr, false);
383 return o;
384}
385
394 json m;
395 m["id"] = col_text(s, 0);
396 m["role"] = col_text(s, 2);
397 m["content"] = col_text(s, 3);
398 m["tool_calls"] = json::parse(col_text(s, 4), nullptr, false);
399 m["tool_results"] = json::parse(col_text(s, 5), nullptr, false);
400 m["token_count"] = sqlite3_column_int64(s, 6);
401 m["created_at"] = col_text(s, 7);
402 m["is_compacted"] = sqlite3_column_int(s, 8) != 0;
403 m["identity_tier"] = col_opt_text(s, 9).value_or("");
404 return m;
405}
406
407} // namespace
408
418 const std::string& conversation_id,
419 std::string& result_json) {
420 json conv_obj;
421 bool found = db_.fetch_one(
422 "SELECT * FROM conversations WHERE id = ?",
423 [&](sqlite3_stmt* s) {
424 sqlite3_bind_text(s, 1, conversation_id.c_str(), -1, SQLITE_TRANSIENT);
425 },
427
428 if (!found) return false;
429
430 json messages = json::array();
431 db_.fetch_all(
432 "SELECT * FROM messages WHERE conversation_id = ? "
433 "ORDER BY created_at ASC",
434 [&](sqlite3_stmt* s) {
435 sqlite3_bind_text(s, 1, conversation_id.c_str(), -1, SQLITE_TRANSIENT);
436 },
437 [&](sqlite3_stmt* s) {
438 messages.push_back(message_row_to_json(s));
439 });
440
441 json result;
442 result["conversation"] = std::move(conv_obj);
443 result["messages"] = std::move(messages);
444 result_json = result.dump();
445 return true;
446}
447
458 int limit, int offset, std::string& result_json) {
459 json arr = json::array();
460 db_.fetch_all(
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 "
465 "GROUP BY c.id "
466 "ORDER BY c.updated_at DESC "
467 "LIMIT ? OFFSET ?",
468 [&](sqlite3_stmt* s) {
471 },
472 [&](sqlite3_stmt* s) {
473 json entry;
474 entry["id"] = col_text(s, 0);
475 entry["title"] = col_text(s, 1);
476 entry["updated_at"] = col_text(s, 2);
477 entry["project_path"] = col_opt_text(s, 3).value_or("");
478 entry["message_count"] = sqlite3_column_int(s, 4);
479 arr.push_back(std::move(entry));
480 });
481
482 result_json = arr.dump();
483 return true;
484}
485
495 const std::string& conversation_id) {
496 bool ok = db_.execute(
497 "DELETE FROM conversations WHERE id = ?",
498 [&](sqlite3_stmt* s) {
499 sqlite3_bind_text(s, 1, conversation_id.c_str(), -1, SQLITE_TRANSIENT);
500 });
501 if (ok) logger->info("Deleted conversation: {}", conversation_id);
502 return ok;
503}
504
514 const std::string& conversation_id,
515 const std::string& title) {
516 return db_.execute(
517 "UPDATE conversations SET title = ? WHERE id = ?",
518 [&](sqlite3_stmt* s) {
519 sqlite3_bind_text(s, 1, title.c_str(), -1, SQLITE_TRANSIENT);
520 sqlite3_bind_text(s, 2, conversation_id.c_str(), -1, SQLITE_TRANSIENT);
521 });
522}
523
524// ── Search ────────────────────────────────────────────────
525
536 const std::string& query, int limit,
537 std::string& result_json) {
538 json arr = json::array();
539 db_.fetch_all(
540 "SELECT DISTINCT c.id, c.title, c.updated_at, "
541 "snippet(messages_fts, 0, '>>>', '<<<', '...', 32) as snippet "
542 "FROM messages_fts "
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 ? "
546 "ORDER BY rank "
547 "LIMIT ?",
548 [&](sqlite3_stmt* s) {
549 sqlite3_bind_text(s, 1, query.c_str(), -1, SQLITE_TRANSIENT);
551 },
552 [&](sqlite3_stmt* s) {
553 json entry;
554 entry["id"] = col_text(s, 0);
555 entry["title"] = col_text(s, 1);
556 entry["updated_at"] = col_text(s, 2);
557 entry["snippet"] = col_text(s, 3);
558 arr.push_back(std::move(entry));
559 });
560
561 result_json = arr.dump();
562 return true;
563}
564
565// ── Delegation storage ────────────────────────────────────
566
581 const DelegationRecord& rec) {
582 sqlite3_bind_text(s, 1, rec.id.c_str(), -1, SQLITE_TRANSIENT);
583 sqlite3_bind_text(s, 2, rec.parent_conversation_id.c_str(),
584 -1, SQLITE_TRANSIENT);
585 sqlite3_bind_text(s, 3, rec.child_conversation_id.c_str(),
586 -1, SQLITE_TRANSIENT);
587 sqlite3_bind_text(s, 4, rec.delegating_tier.c_str(),
588 -1, SQLITE_TRANSIENT);
589 sqlite3_bind_text(s, 5, rec.target_tier.c_str(),
590 -1, SQLITE_TRANSIENT);
591 sqlite3_bind_text(s, 6, rec.task.c_str(), -1, SQLITE_TRANSIENT);
592 bind_opt_int(s, 7, rec.max_turns);
593 sqlite3_bind_text(s, 8, rec.status.c_str(), -1, SQLITE_TRANSIENT);
594 bind_opt_text(s, 9, rec.result_summary);
595 sqlite3_bind_text(s, 10, rec.created_at.c_str(),
596 -1, SQLITE_TRANSIENT);
597 bind_opt_text(s, 11, rec.completed_at);
598}
599
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();
633 return false;
634}
635
650 const std::string& parent_conversation_id,
651 const std::string& delegating_tier,
652 const std::string& target_tier,
653 const std::string& task,
654 int max_turns,
655 std::string& delegation_id,
656 std::string& child_conversation_id) {
657 if (!guard_parent_conversation(parent_conversation_id,
658 delegating_tier, target_tier,
659 delegation_id,
660 child_conversation_id)) {
661 return false;
662 }
663
664 auto child_title = "Delegation: " + target_tier + " — " +
665 task.substr(0, 60);
666 child_conversation_id = create_conversation(
667 child_title, std::nullopt, target_tier);
668
669 auto rec = make_delegation(
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";
674
675 bool ok = db_.execute(
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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
682
683 delegation_id = rec.id;
684 // gh#48 defense-in-depth (v2.1.12): log success only when the
685 // INSERT actually succeeded. Pre-v2.1.12 this fired
686 // unconditionally and masked the FK failure logged one line up
687 // by `database.cpp`'s `execute()` from anyone scanning for
688 // "Created delegation" in the session log.
689 if (ok) {
690 logger->info("Created delegation {}: {} -> {}",
691 rec.id, delegating_tier, target_tier);
692 } else {
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);
698 }
699 return ok;
700}
701
712 const std::string& delegation_id,
713 const std::string& status,
714 const std::optional<std::string>& result_summary) {
715 auto now = utc_timestamp();
716 return db_.execute(
717 "UPDATE delegations "
718 "SET status = ?, result_summary = ?, completed_at = ? "
719 "WHERE id = ?",
720 [&](sqlite3_stmt* s) {
721 sqlite3_bind_text(s, 1, status.c_str(), -1, SQLITE_TRANSIENT);
722 bind_opt_text(s, 2, result_summary);
723 sqlite3_bind_text(s, 3, now.c_str(), -1, SQLITE_TRANSIENT);
724 sqlite3_bind_text(s, 4, delegation_id.c_str(), -1, SQLITE_TRANSIENT);
725 });
726}
727
728namespace {
729
738 json entry;
739 entry["id"] = col_text(s, 0);
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);
743 entry["target_tier"] = col_text(s, 4);
744 entry["task"] = sanitize_storage_utf8(col_text(s, 5));
745 entry["max_turns"] = col_nullable_int(s, 6);
746 entry["status"] = col_text(s, 7);
747 entry["result_summary"] = sanitize_storage_utf8(col_opt_text(s, 8).value_or(""));
748 entry["created_at"] = col_text(s, 9);
749 entry["completed_at"] = col_opt_text(s, 10).value_or("");
750 return entry;
751}
752
765 json entry;
766 entry["id"] = col_text(s, 0);
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);
770 entry["target_tier"] = col_text(s, 4);
771 entry["task"] = sanitize_storage_utf8(col_text(s, 5));
772 entry["status"] = col_text(s, 7);
773 entry["result_summary"] = sanitize_storage_utf8(col_opt_text(s, 8).value_or(""));
774 entry["completed_at"] = col_opt_text(s, 10).value_or("");
775 return entry;
776}
777
778} // namespace
779
790 const std::string& conversation_id,
791 std::string& result_json) {
792 json arr = json::array();
793 db_.fetch_all(
794 "SELECT * FROM delegations "
795 "WHERE parent_conversation_id = ? "
796 "ORDER BY created_at ASC",
797 [&](sqlite3_stmt* s) {
798 sqlite3_bind_text(s, 1, conversation_id.c_str(), -1, SQLITE_TRANSIENT);
799 },
800 [&](sqlite3_stmt* s) {
801 arr.push_back(delegation_row_to_json(s));
802 });
803
804 result_json = arr.dump();
805 return true;
806}
807
818 const std::string& delegation_id,
819 std::string& result_json) {
820 bool found = false;
821 json entry;
822 db_.fetch_all(
823 "SELECT * FROM delegations WHERE id = ? LIMIT 1",
824 [&](sqlite3_stmt* s) {
825 sqlite3_bind_text(s, 1, delegation_id.c_str(), -1,
827 },
828 [&](sqlite3_stmt* s) {
829 found = true;
830 entry = delegation_row_to_json(s);
831 });
832 if (!found) {
833 return false;
834 }
835 result_json = entry.dump();
836 return true;
837}
838
849 const std::string& target_tier, std::string& delegation_id) {
850 bool found = false;
851 db_.fetch_all(
852 "SELECT id FROM delegations "
853 "WHERE target_tier = ? AND status = 'completed' "
854 "ORDER BY completed_at DESC LIMIT 1",
855 [&](sqlite3_stmt* s) {
856 sqlite3_bind_text(s, 1, target_tier.c_str(), -1,
858 },
859 [&](sqlite3_stmt* s) {
860 const auto* text = sqlite3_column_text(s, 0);
861 if (text != nullptr) {
862 delegation_id = reinterpret_cast<const char*>(text);
863 found = true;
864 }
865 });
866 return found;
867}
868
885 const std::string& query, int max_results,
886 std::string& result_json) {
887 json arr = json::array();
888 std::string like = "%" + query + "%";
889 db_.fetch_all(
890 "SELECT * FROM delegations "
891 "WHERE result_summary LIKE ? AND status = 'completed' "
892 "ORDER BY completed_at DESC LIMIT ?",
893 [&](sqlite3_stmt* s) {
894 sqlite3_bind_text(s, 1, like.c_str(), -1, SQLITE_TRANSIENT);
896 },
897 [&](sqlite3_stmt* s) {
898 arr.push_back(delegation_summary_to_json(s));
899 });
900 result_json = arr.dump();
901 return true;
902}
903
904// ── Compaction snapshots ──────────────────────────────────
905
915 const std::string& conversation_id,
916 const std::string& messages_json) {
917 auto snap_id = generate_uuid();
918 auto now = utc_timestamp();
919
920 // Count messages
921 auto msgs = json::parse(messages_json, nullptr, false);
922 int msg_count = msgs.is_array() ? static_cast<int>(msgs.size()) : 0;
923
924 return db_.execute(
925 "INSERT INTO compaction_snapshots "
926 "(id, conversation_id, messages_json, message_count, "
927 "token_count_estimate, created_at) "
928 "VALUES (?, ?, ?, ?, NULL, ?)",
929 [&](sqlite3_stmt* s) {
931 sqlite3_bind_text(s, 2, conversation_id.c_str(), -1, SQLITE_TRANSIENT);
932 sqlite3_bind_text(s, 3, messages_json.c_str(), -1, SQLITE_TRANSIENT);
934 sqlite3_bind_text(s, 5, now.c_str(), -1, SQLITE_TRANSIENT);
935 });
936}
937
938// ── Statistics ────────────────────────────────────────────
939
951
952 db_.fetch_one("SELECT COUNT(*) FROM conversations",
953 nullptr,
955
956 db_.fetch_one("SELECT COUNT(*) FROM messages",
957 nullptr,
959
960 db_.fetch_one("SELECT COALESCE(SUM(token_count), 0) FROM messages",
961 nullptr,
963
964 json stats;
965 stats["total_conversations"] = total_convs;
966 stats["total_messages"] = total_msgs;
967 stats["total_tokens"] = total_tokens;
968 result_json = stats.dump();
969 return true;
970}
971
972// ── Record factory implementations ────────────────────────
973
980std::string generate_uuid() {
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);
984
985 const char hex[] = "0123456789abcdef";
986 std::string uuid(36, '-');
987
988 // 8-4-4-4-12 format
989 static constexpr int positions[] = {
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
992 };
993
994 for (int pos : positions) {
995 uuid[pos] = hex[dist(gen)];
996 }
997 uuid[14] = '4'; // version 4
998 uuid[19] = hex[dist2(gen)]; // variant 1
999
1000 return uuid;
1001}
1002
1009std::string utc_timestamp() {
1010 auto now = std::chrono::system_clock::now();
1011 auto time = std::chrono::system_clock::to_time_t(now);
1012 struct tm utc{};
1013 gmtime_r(&time, &utc);
1014
1015 char buf[32];
1016 std::strftime(buf, sizeof(buf), "%Y-%m-%dT%H:%M:%S", &utc);
1017 return buf;
1018}
1019
1030 const std::string& title,
1031 const std::optional<std::string>& project_path,
1032 const std::optional<std::string>& model_id) {
1033 auto now = utc_timestamp();
1034 return {generate_uuid(), title, now, now, project_path, model_id, "{}"};
1035}
1036
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) {
1055 return {generate_uuid(), parent_conversation_id,
1056 child_conversation_id, delegating_tier, target_tier,
1057 task, std::nullopt, "pending", std::nullopt,
1058 utc_timestamp(), std::nullopt};
1059}
1060
1061} // namespace entropic
bool execute(std::string_view sql, std::function< void(sqlite3_stmt *)> binder=nullptr)
Execute a write statement (INSERT, UPDATE, DELETE).
Definition database.cpp:269
void close()
Close database connection.
Definition database.cpp:221
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.
Definition database.cpp:353
bool fetch_one(std::string_view sql, std::function< void(sqlite3_stmt *)> binder, std::function< void(sqlite3_stmt *)> extractor)
Fetch a single row.
Definition database.cpp:322
bool initialize()
Initialize database and run pending migrations.
Definition database.cpp:183
bool save_messages(const std::string &conversation_id, const std::string &messages_json)
Save messages to a conversation.
Definition backend.cpp:324
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).
Definition backend.cpp:817
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.
Definition backend.cpp:711
bool latest_delegation_for_target(const std::string &target_tier, std::string &delegation_id)
Search delegations across all conversations (gh#32, v2.1.6).
Definition backend.cpp:848
bool update_title(const std::string &conversation_id, const std::string &title)
Update a conversation's title.
Definition backend.cpp:513
SqliteStorageBackend(const std::filesystem::path &db_path)
Construct with database file path.
Definition backend.cpp:188
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.
Definition backend.cpp:649
bool search_conversations(const std::string &query, int limit, std::string &result_json)
Full-text search across conversations.
Definition backend.cpp:535
bool get_delegations(const std::string &conversation_id, std::string &result_json)
Get delegations for a parent conversation.
Definition backend.cpp:789
bool save_snapshot(const std::string &conversation_id, const std::string &messages_json)
Save a pre-compaction snapshot of full conversation history.
Definition backend.cpp:914
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.
Definition backend.cpp:222
void close()
Close storage and database connection.
Definition backend.cpp:207
bool load_conversation(const std::string &conversation_id, std::string &result_json)
Load a conversation with messages.
Definition backend.cpp:417
bool get_stats(std::string &result_json)
Get storage statistics.
Definition backend.cpp:947
bool delete_conversation(const std::string &conversation_id)
Delete a conversation and all associated records.
Definition backend.cpp:494
bool search_delegations(const std::string &query, int max_results, std::string &result_json)
Substring-match delegations across all conversations.
Definition backend.cpp:884
bool list_conversations(int limit, int offset, std::string &result_json)
List conversations with pagination.
Definition backend.cpp:457
bool initialize()
Initialize storage (open database, run migrations).
Definition backend.cpp:198
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.
Definition logging.h:200
ENTROPIC_EXPORT std::shared_ptr< spdlog::logger > get(const std::string &name)
Get or create a named logger.
Definition logging.cpp:211
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.
Definition backend.cpp:114
std::string utc_timestamp()
Get current UTC time as ISO 8601 string.
Definition backend.cpp:1009
@ 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).
Definition backend.cpp:171
static void bind_opt_text(sqlite3_stmt *stmt, int idx, const std::optional< std::string > &val)
Bind optional text to a parameter position.
Definition backend.cpp:154
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-...
Definition backend.cpp:619
std::string generate_uuid()
Generate a UUID v4 string.
Definition backend.cpp:980
static void bind_delegation_insert(sqlite3_stmt *s, const DelegationRecord &rec)
Bind a Delegation record onto the delegations-INSERT statement's 11 placeholders.
Definition backend.cpp:580
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.
Definition backend.cpp:1029
static json col_nullable_int(sqlite3_stmt *stmt, int col)
Get a nullable integer column as a JSON value.
Definition backend.cpp:141
static std::optional< std::string > col_opt_text(sqlite3_stmt *stmt, int col)
Get optional text from a sqlite3 column.
Definition backend.cpp:127
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.
Definition backend.cpp:1049
SqliteStorageBackend — conversation persistence via SQLite.
Database record for a conversation.
Definition records.h:25
Database record for a delegation.
Definition records.h:56