Entropic 2.3.8
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 <cstring>
16#include <random>
17
18using json = nlohmann::json;
19
20namespace entropic {
21
22namespace {
23auto logger = entropic::log::get("storage.backend");
24} // anonymous namespace
25
26// ── Helpers ───────────────────────────────────────────────
27
36static std::string col_text(sqlite3_stmt* stmt, int col) {
37 auto* p = sqlite3_column_text(stmt, col);
38 return p ? reinterpret_cast<const char*>(p) : "";
39}
40
49static std::optional<std::string> col_opt_text(sqlite3_stmt* stmt, int col) {
50 auto* p = sqlite3_column_text(stmt, col);
51 if (!p) return std::nullopt;
52 return std::string(reinterpret_cast<const char*>(p));
53}
54
63static void bind_opt_text(sqlite3_stmt* stmt, int idx,
64 const std::optional<std::string>& val) {
65 if (val) {
66 sqlite3_bind_text(stmt, idx, val->c_str(), -1, SQLITE_TRANSIENT);
67 } else {
68 sqlite3_bind_null(stmt, idx);
69 }
70}
71
80static void bind_opt_int(sqlite3_stmt* stmt, int idx,
81 const std::optional<int>& val) {
82 if (val) {
83 sqlite3_bind_int(stmt, idx, *val);
84 } else {
85 sqlite3_bind_null(stmt, idx);
86 }
87}
88
89// ── SqliteStorageBackend ──────────────────────────────────
90
98 const std::filesystem::path& db_path)
99 : db_(db_path) {}
100
108 return db_.initialize();
109}
110
117 db_.close();
118}
119
120// ── Conversation CRUD ─────────────────────────────────────
121
132 const std::string& title,
133 const std::optional<std::string>& project_path,
134 const std::optional<std::string>& model_id) {
135 auto rec = make_conversation(title, project_path, model_id);
136
137 db_.execute(
138 "INSERT INTO conversations "
139 "(id, title, created_at, updated_at, project_path, model_id, metadata) "
140 "VALUES (?, ?, ?, ?, ?, ?, ?)",
141 [&](sqlite3_stmt* s) {
142 sqlite3_bind_text(s, 1, rec.id.c_str(), -1, SQLITE_TRANSIENT);
143 sqlite3_bind_text(s, 2, rec.title.c_str(), -1, SQLITE_TRANSIENT);
144 sqlite3_bind_text(s, 3, rec.created_at.c_str(), -1, SQLITE_TRANSIENT);
145 sqlite3_bind_text(s, 4, rec.updated_at.c_str(), -1, SQLITE_TRANSIENT);
146 bind_opt_text(s, 5, rec.project_path);
147 bind_opt_text(s, 6, rec.model_id);
148 sqlite3_bind_text(s, 7, rec.metadata.c_str(), -1, SQLITE_TRANSIENT);
149 });
150
151 logger->info("Created conversation: {}", rec.id);
152 return rec.id;
153}
154
155namespace {
156
158struct MessageRow {
159 std::string id;
160 std::string role;
161 std::string content;
162 std::string tool_calls;
163 std::string tool_results;
164 long long token_count;
165 bool is_compacted;
166 std::optional<std::string> tier;
167};
168
176MessageRow build_message_row(const json& m) {
177 MessageRow r;
178 r.id = generate_uuid();
179 r.role = m.value("role", "");
180 r.content = m.value("content", "");
181 r.tool_calls = m.contains("tool_calls") ? m["tool_calls"].dump() : "[]";
182 r.tool_results =
183 m.contains("tool_results") ? m["tool_results"].dump() : "[]";
184 r.token_count = m.value("token_count", 0);
185 r.is_compacted = m.value("is_compacted", false);
186 if (m.contains("identity_tier") && !m["identity_tier"].is_null()) {
187 r.tier = m["identity_tier"].get<std::string>();
188 }
189 return r;
190}
191
201void bind_message_insert(sqlite3_stmt* s, const std::string& conversation_id,
202 const std::string& now, const MessageRow& r) {
203 sqlite3_bind_text(s, 1, r.id.c_str(), -1, SQLITE_TRANSIENT);
204 sqlite3_bind_text(s, 2, conversation_id.c_str(), -1, SQLITE_TRANSIENT);
205 sqlite3_bind_text(s, 3, r.role.c_str(), -1, SQLITE_TRANSIENT);
206 sqlite3_bind_text(s, 4, r.content.c_str(), -1, SQLITE_TRANSIENT);
207 sqlite3_bind_text(s, 5, r.tool_calls.c_str(), -1, SQLITE_TRANSIENT);
208 sqlite3_bind_text(s, 6, r.tool_results.c_str(), -1, SQLITE_TRANSIENT);
209 sqlite3_bind_int64(s, 7, r.token_count);
210 sqlite3_bind_text(s, 8, now.c_str(), -1, SQLITE_TRANSIENT);
211 sqlite3_bind_int(s, 9, r.is_compacted ? 1 : 0);
212 bind_opt_text(s, 10, r.tier);
213}
214
215} // namespace
216
226 const std::string& conversation_id,
227 const std::string& messages_json) {
228 auto now = utc_timestamp();
229 db_.execute(
230 "UPDATE conversations SET updated_at = ? WHERE id = ?",
231 [&](sqlite3_stmt* s) {
232 sqlite3_bind_text(s, 1, now.c_str(), -1, SQLITE_TRANSIENT);
233 sqlite3_bind_text(s, 2, conversation_id.c_str(), -1, SQLITE_TRANSIENT);
234 });
235
236 auto msgs = json::parse(messages_json, nullptr, false);
237 if (!msgs.is_array()) {
238 logger->error("save_messages: invalid JSON array");
239 return false;
240 }
241
242 static constexpr const char* kInsertSql =
243 "INSERT INTO messages "
244 "(id, conversation_id, role, content, tool_calls, tool_results, "
245 "token_count, created_at, is_compacted, identity_tier) "
246 "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
247 for (const auto& m : msgs) {
248 auto row = build_message_row(m);
249 db_.execute(kInsertSql, [&](sqlite3_stmt* s) {
250 bind_message_insert(s, conversation_id, now, row);
251 });
252 }
253
254 return true;
255}
256
257namespace {
258
266json conversation_row_to_json(sqlite3_stmt* s) {
267 json o;
268 o["id"] = col_text(s, 0);
269 o["title"] = col_text(s, 1);
270 o["created_at"] = col_text(s, 2);
271 o["updated_at"] = col_text(s, 3);
272 o["project_path"] = col_opt_text(s, 4).value_or("");
273 o["model_id"] = col_opt_text(s, 5).value_or("");
274 o["metadata"] = json::parse(col_text(s, 6), nullptr, false);
275 return o;
276}
277
285json message_row_to_json(sqlite3_stmt* s) {
286 json m;
287 m["id"] = col_text(s, 0);
288 m["role"] = col_text(s, 2);
289 m["content"] = col_text(s, 3);
290 m["tool_calls"] = json::parse(col_text(s, 4), nullptr, false);
291 m["tool_results"] = json::parse(col_text(s, 5), nullptr, false);
292 m["token_count"] = sqlite3_column_int64(s, 6);
293 m["created_at"] = col_text(s, 7);
294 m["is_compacted"] = sqlite3_column_int(s, 8) != 0;
295 m["identity_tier"] = col_opt_text(s, 9).value_or("");
296 return m;
297}
298
299} // namespace
300
310 const std::string& conversation_id,
311 std::string& result_json) {
312 json conv_obj;
313 bool found = db_.fetch_one(
314 "SELECT * FROM conversations WHERE id = ?",
315 [&](sqlite3_stmt* s) {
316 sqlite3_bind_text(s, 1, conversation_id.c_str(), -1, SQLITE_TRANSIENT);
317 },
318 [&](sqlite3_stmt* s) { conv_obj = conversation_row_to_json(s); });
319
320 if (!found) return false;
321
322 json messages = json::array();
323 db_.fetch_all(
324 "SELECT * FROM messages WHERE conversation_id = ? "
325 "ORDER BY created_at ASC",
326 [&](sqlite3_stmt* s) {
327 sqlite3_bind_text(s, 1, conversation_id.c_str(), -1, SQLITE_TRANSIENT);
328 },
329 [&](sqlite3_stmt* s) {
330 messages.push_back(message_row_to_json(s));
331 });
332
333 json result;
334 result["conversation"] = std::move(conv_obj);
335 result["messages"] = std::move(messages);
336 result_json = result.dump();
337 return true;
338}
339
350 int limit, int offset, std::string& result_json) {
351 json arr = json::array();
352 db_.fetch_all(
353 "SELECT c.id, c.title, c.updated_at, c.project_path, "
354 "COUNT(m.id) as message_count "
355 "FROM conversations c "
356 "LEFT JOIN messages m ON c.id = m.conversation_id "
357 "GROUP BY c.id "
358 "ORDER BY c.updated_at DESC "
359 "LIMIT ? OFFSET ?",
360 [&](sqlite3_stmt* s) {
361 sqlite3_bind_int(s, 1, limit);
362 sqlite3_bind_int(s, 2, offset);
363 },
364 [&](sqlite3_stmt* s) {
365 json entry;
366 entry["id"] = col_text(s, 0);
367 entry["title"] = col_text(s, 1);
368 entry["updated_at"] = col_text(s, 2);
369 entry["project_path"] = col_opt_text(s, 3).value_or("");
370 entry["message_count"] = sqlite3_column_int(s, 4);
371 arr.push_back(std::move(entry));
372 });
373
374 result_json = arr.dump();
375 return true;
376}
377
386 const std::string& conversation_id) {
387 bool ok = db_.execute(
388 "DELETE FROM conversations WHERE id = ?",
389 [&](sqlite3_stmt* s) {
390 sqlite3_bind_text(s, 1, conversation_id.c_str(), -1, SQLITE_TRANSIENT);
391 });
392 if (ok) logger->info("Deleted conversation: {}", conversation_id);
393 return ok;
394}
395
405 const std::string& conversation_id,
406 const std::string& title) {
407 return db_.execute(
408 "UPDATE conversations SET title = ? WHERE id = ?",
409 [&](sqlite3_stmt* s) {
410 sqlite3_bind_text(s, 1, title.c_str(), -1, SQLITE_TRANSIENT);
411 sqlite3_bind_text(s, 2, conversation_id.c_str(), -1, SQLITE_TRANSIENT);
412 });
413}
414
415// ── Search ────────────────────────────────────────────────
416
427 const std::string& query, int limit,
428 std::string& result_json) {
429 json arr = json::array();
430 db_.fetch_all(
431 "SELECT DISTINCT c.id, c.title, c.updated_at, "
432 "snippet(messages_fts, 0, '>>>', '<<<', '...', 32) as snippet "
433 "FROM messages_fts "
434 "JOIN messages m ON messages_fts.rowid = m.rowid "
435 "JOIN conversations c ON m.conversation_id = c.id "
436 "WHERE messages_fts MATCH ? "
437 "ORDER BY rank "
438 "LIMIT ?",
439 [&](sqlite3_stmt* s) {
440 sqlite3_bind_text(s, 1, query.c_str(), -1, SQLITE_TRANSIENT);
441 sqlite3_bind_int(s, 2, limit);
442 },
443 [&](sqlite3_stmt* s) {
444 json entry;
445 entry["id"] = col_text(s, 0);
446 entry["title"] = col_text(s, 1);
447 entry["updated_at"] = col_text(s, 2);
448 entry["snippet"] = col_text(s, 3);
449 arr.push_back(std::move(entry));
450 });
451
452 result_json = arr.dump();
453 return true;
454}
455
456// ── Delegation storage ────────────────────────────────────
457
471static void bind_delegation_insert(sqlite3_stmt* s,
472 const DelegationRecord& rec) {
473 sqlite3_bind_text(s, 1, rec.id.c_str(), -1, SQLITE_TRANSIENT);
474 sqlite3_bind_text(s, 2, rec.parent_conversation_id.c_str(),
475 -1, SQLITE_TRANSIENT);
476 sqlite3_bind_text(s, 3, rec.child_conversation_id.c_str(),
477 -1, SQLITE_TRANSIENT);
478 sqlite3_bind_text(s, 4, rec.delegating_tier.c_str(),
479 -1, SQLITE_TRANSIENT);
480 sqlite3_bind_text(s, 5, rec.target_tier.c_str(),
481 -1, SQLITE_TRANSIENT);
482 sqlite3_bind_text(s, 6, rec.task.c_str(), -1, SQLITE_TRANSIENT);
483 bind_opt_int(s, 7, rec.max_turns);
484 sqlite3_bind_text(s, 8, rec.status.c_str(), -1, SQLITE_TRANSIENT);
485 bind_opt_text(s, 9, rec.result_summary);
486 sqlite3_bind_text(s, 10, rec.created_at.c_str(),
487 -1, SQLITE_TRANSIENT);
488 bind_opt_text(s, 11, rec.completed_at);
489}
490
511 const std::string& parent_conversation_id,
512 const std::string& delegating_tier,
513 const std::string& target_tier,
514 std::string& delegation_id,
515 std::string& child_conversation_id) {
516 if (!parent_conversation_id.empty()) { return true; }
517 logger->error("create_delegation refused: parent_conversation_id "
518 "is empty (delegating_tier={}, target_tier={}). "
519 "Root conversation must be created before "
520 "delegating (gh#48).",
521 delegating_tier, target_tier);
522 delegation_id.clear();
523 child_conversation_id.clear();
524 return false;
525}
526
541 const std::string& parent_conversation_id,
542 const std::string& delegating_tier,
543 const std::string& target_tier,
544 const std::string& task,
545 int max_turns,
546 std::string& delegation_id,
547 std::string& child_conversation_id) {
548 if (!guard_parent_conversation(parent_conversation_id,
549 delegating_tier, target_tier,
550 delegation_id,
551 child_conversation_id)) {
552 return false;
553 }
554
555 auto child_title = "Delegation: " + target_tier + " — " +
556 task.substr(0, 60);
557 child_conversation_id = create_conversation(
558 child_title, std::nullopt, target_tier);
559
560 auto rec = make_delegation(
561 parent_conversation_id, child_conversation_id,
562 delegating_tier, target_tier, task);
563 if (max_turns > 0) rec.max_turns = max_turns;
564 rec.status = "running";
565
566 bool ok = db_.execute(
567 "INSERT INTO delegations "
568 "(id, parent_conversation_id, child_conversation_id, "
569 "delegating_tier, target_tier, task, max_turns, "
570 "status, result_summary, created_at, completed_at) "
571 "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
572 [&](sqlite3_stmt* s) { bind_delegation_insert(s, rec); });
573
574 delegation_id = rec.id;
575 // gh#48 defense-in-depth (v2.1.12): log success only when the
576 // INSERT actually succeeded. Pre-v2.1.12 this fired
577 // unconditionally and masked the FK failure logged one line up
578 // by `database.cpp`'s `execute()` from anyone scanning for
579 // "Created delegation" in the session log.
580 if (ok) {
581 logger->info("Created delegation {}: {} -> {}",
582 rec.id, delegating_tier, target_tier);
583 } else {
584 logger->error("Failed to insert delegation {} ({} -> {}): "
585 "parent_conversation_id='{}' — check the SQL "
586 "execute error logged immediately above",
587 rec.id, delegating_tier, target_tier,
588 parent_conversation_id);
589 }
590 return ok;
591}
592
603 const std::string& delegation_id,
604 const std::string& status,
605 const std::optional<std::string>& result_summary) {
606 auto now = utc_timestamp();
607 return db_.execute(
608 "UPDATE delegations "
609 "SET status = ?, result_summary = ?, completed_at = ? "
610 "WHERE id = ?",
611 [&](sqlite3_stmt* s) {
612 sqlite3_bind_text(s, 1, status.c_str(), -1, SQLITE_TRANSIENT);
613 bind_opt_text(s, 2, result_summary);
614 sqlite3_bind_text(s, 3, now.c_str(), -1, SQLITE_TRANSIENT);
615 sqlite3_bind_text(s, 4, delegation_id.c_str(), -1, SQLITE_TRANSIENT);
616 });
617}
618
619namespace {
620
628json delegation_row_to_json(sqlite3_stmt* s) {
629 json entry;
630 entry["id"] = col_text(s, 0);
631 entry["parent_conversation_id"] = col_text(s, 1);
632 entry["child_conversation_id"] = col_text(s, 2);
633 entry["delegating_tier"] = col_text(s, 3);
634 entry["target_tier"] = col_text(s, 4);
635 entry["task"] = col_text(s, 5);
636 auto mt = sqlite3_column_int(s, 6);
637 entry["max_turns"] = (sqlite3_column_type(s, 6) == SQLITE_NULL)
638 ? json(nullptr) : json(mt);
639 entry["status"] = col_text(s, 7);
640 entry["result_summary"] = col_opt_text(s, 8).value_or("");
641 entry["created_at"] = col_text(s, 9);
642 entry["completed_at"] = col_opt_text(s, 10).value_or("");
643 return entry;
644}
645
657json delegation_summary_to_json(sqlite3_stmt* s) {
658 json entry;
659 entry["id"] = col_text(s, 0);
660 entry["parent_conversation_id"] = col_text(s, 1);
661 entry["child_conversation_id"] = col_text(s, 2);
662 entry["delegating_tier"] = col_text(s, 3);
663 entry["target_tier"] = col_text(s, 4);
664 entry["task"] = col_text(s, 5);
665 entry["status"] = col_text(s, 7);
666 entry["result_summary"] = col_opt_text(s, 8).value_or("");
667 entry["completed_at"] = col_opt_text(s, 10).value_or("");
668 return entry;
669}
670
671} // namespace
672
682 const std::string& conversation_id,
683 std::string& result_json) {
684 json arr = json::array();
685 db_.fetch_all(
686 "SELECT * FROM delegations "
687 "WHERE parent_conversation_id = ? "
688 "ORDER BY created_at ASC",
689 [&](sqlite3_stmt* s) {
690 sqlite3_bind_text(s, 1, conversation_id.c_str(), -1, SQLITE_TRANSIENT);
691 },
692 [&](sqlite3_stmt* s) {
693 arr.push_back(delegation_row_to_json(s));
694 });
695
696 result_json = arr.dump();
697 return true;
698}
699
709 const std::string& delegation_id,
710 std::string& result_json) {
711 bool found = false;
712 json entry;
713 db_.fetch_all(
714 "SELECT * FROM delegations WHERE id = ? LIMIT 1",
715 [&](sqlite3_stmt* s) {
716 sqlite3_bind_text(s, 1, delegation_id.c_str(), -1,
717 SQLITE_TRANSIENT);
718 },
719 [&](sqlite3_stmt* s) {
720 found = true;
721 entry = delegation_row_to_json(s);
722 });
723 if (!found) {
724 return false;
725 }
726 result_json = entry.dump();
727 return true;
728}
729
745 const std::string& query, int max_results,
746 std::string& result_json) {
747 json arr = json::array();
748 std::string like = "%" + query + "%";
749 db_.fetch_all(
750 "SELECT * FROM delegations "
751 "WHERE result_summary LIKE ? AND status = 'completed' "
752 "ORDER BY completed_at DESC LIMIT ?",
753 [&](sqlite3_stmt* s) {
754 sqlite3_bind_text(s, 1, like.c_str(), -1, SQLITE_TRANSIENT);
755 sqlite3_bind_int(s, 2, max_results);
756 },
757 [&](sqlite3_stmt* s) {
758 arr.push_back(delegation_summary_to_json(s));
759 });
760 result_json = arr.dump();
761 return true;
762}
763
764// ── Compaction snapshots ──────────────────────────────────
765
775 const std::string& conversation_id,
776 const std::string& messages_json) {
777 auto snap_id = generate_uuid();
778 auto now = utc_timestamp();
779
780 // Count messages
781 auto msgs = json::parse(messages_json, nullptr, false);
782 int msg_count = msgs.is_array() ? static_cast<int>(msgs.size()) : 0;
783
784 return db_.execute(
785 "INSERT INTO compaction_snapshots "
786 "(id, conversation_id, messages_json, message_count, "
787 "token_count_estimate, created_at) "
788 "VALUES (?, ?, ?, ?, NULL, ?)",
789 [&](sqlite3_stmt* s) {
790 sqlite3_bind_text(s, 1, snap_id.c_str(), -1, SQLITE_TRANSIENT);
791 sqlite3_bind_text(s, 2, conversation_id.c_str(), -1, SQLITE_TRANSIENT);
792 sqlite3_bind_text(s, 3, messages_json.c_str(), -1, SQLITE_TRANSIENT);
793 sqlite3_bind_int(s, 4, msg_count);
794 sqlite3_bind_text(s, 5, now.c_str(), -1, SQLITE_TRANSIENT);
795 });
796}
797
798// ── Statistics ────────────────────────────────────────────
799
807bool SqliteStorageBackend::get_stats(std::string& result_json) {
808 int64_t total_convs = 0;
809 int64_t total_msgs = 0;
810 int64_t total_tokens = 0;
811
812 db_.fetch_one("SELECT COUNT(*) FROM conversations",
813 nullptr,
814 [&](sqlite3_stmt* s) { total_convs = sqlite3_column_int64(s, 0); });
815
816 db_.fetch_one("SELECT COUNT(*) FROM messages",
817 nullptr,
818 [&](sqlite3_stmt* s) { total_msgs = sqlite3_column_int64(s, 0); });
819
820 db_.fetch_one("SELECT COALESCE(SUM(token_count), 0) FROM messages",
821 nullptr,
822 [&](sqlite3_stmt* s) { total_tokens = sqlite3_column_int64(s, 0); });
823
824 json stats;
825 stats["total_conversations"] = total_convs;
826 stats["total_messages"] = total_msgs;
827 stats["total_tokens"] = total_tokens;
828 result_json = stats.dump();
829 return true;
830}
831
832// ── Record factory implementations ────────────────────────
833
840std::string generate_uuid() {
841 static thread_local std::mt19937 gen(std::random_device{}());
842 std::uniform_int_distribution<uint32_t> dist(0, 15);
843 std::uniform_int_distribution<uint32_t> dist2(8, 11);
844
845 const char hex[] = "0123456789abcdef";
846 std::string uuid(36, '-');
847
848 // 8-4-4-4-12 format
849 static constexpr int positions[] = {
850 0,1,2,3,4,5,6,7, 9,10,11,12, 14,15,16,17,
851 19,20,21,22, 24,25,26,27,28,29,30,31,32,33,34,35
852 };
853
854 for (int pos : positions) {
855 uuid[pos] = hex[dist(gen)];
856 }
857 uuid[14] = '4'; // version 4
858 uuid[19] = hex[dist2(gen)]; // variant 1
859
860 return uuid;
861}
862
869std::string utc_timestamp() {
870 auto now = std::chrono::system_clock::now();
871 auto time = std::chrono::system_clock::to_time_t(now);
872 struct tm utc{};
873 gmtime_r(&time, &utc);
874
875 char buf[32];
876 std::strftime(buf, sizeof(buf), "%Y-%m-%dT%H:%M:%S", &utc);
877 return buf;
878}
879
890 const std::string& title,
891 const std::optional<std::string>& project_path,
892 const std::optional<std::string>& model_id) {
893 auto now = utc_timestamp();
894 return {generate_uuid(), title, now, now, project_path, model_id, "{}"};
895}
896
909 const std::string& parent_conversation_id,
910 const std::string& child_conversation_id,
911 const std::string& delegating_tier,
912 const std::string& target_tier,
913 const std::string& task) {
914 return {generate_uuid(), parent_conversation_id,
915 child_conversation_id, delegating_tier, target_tier,
916 task, std::nullopt, "pending", std::nullopt,
917 utc_timestamp(), std::nullopt};
918}
919
920} // 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:225
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:708
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:602
bool update_title(const std::string &conversation_id, const std::string &title)
Update a conversation's title.
Definition backend.cpp:404
SqliteStorageBackend(const std::filesystem::path &db_path)
Construct with database file path.
Definition backend.cpp:97
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:540
bool search_conversations(const std::string &query, int limit, std::string &result_json)
Full-text search across conversations.
Definition backend.cpp:426
bool get_delegations(const std::string &conversation_id, std::string &result_json)
Get delegations for a parent conversation.
Definition backend.cpp:681
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:774
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:131
void close()
Close storage and database connection.
Definition backend.cpp:116
bool load_conversation(const std::string &conversation_id, std::string &result_json)
Load a conversation with messages.
Definition backend.cpp:309
bool get_stats(std::string &result_json)
Get storage statistics.
Definition backend.cpp:807
bool delete_conversation(const std::string &conversation_id)
Delete a conversation and all associated records.
Definition backend.cpp:385
bool search_delegations(const std::string &query, int max_results, std::string &result_json)
Search delegations across all conversations (gh#32, v2.1.6).
Definition backend.cpp:744
bool list_conversations(int limit, int offset, std::string &result_json)
List conversations with pagination.
Definition backend.cpp:349
bool initialize()
Initialize storage (open database, run migrations).
Definition backend.cpp:107
spdlog initialization and logger access.
auto now()
Get current time for timing measurements.
Definition logging.h:193
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:36
std::string utc_timestamp()
Get current UTC time as ISO 8601 string.
Definition backend.cpp:869
@ ok
Tool dispatched, returned non-empty content.
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:80
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:63
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:510
std::string generate_uuid()
Generate a UUID v4 string.
Definition backend.cpp:840
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:471
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:889
static std::optional< std::string > col_opt_text(sqlite3_stmt *stmt, int col)
Get optional text from a sqlite3 column.
Definition backend.cpp:49
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:908
SqliteStorageBackend — conversation persistence via SQLite.
Database record for a conversation.
Definition records.h:25
Database record for a delegation.
Definition records.h:56
std::string target_tier
Target tier for child loop.
Definition records.h:61
std::string created_at
ISO 8601 timestamp.
Definition records.h:66
std::string delegating_tier
Tier that initiated delegation.
Definition records.h:60
std::string status
pending/running/completed/failed
Definition records.h:64
std::optional< std::string > completed_at
Completion timestamp (nullable)
Definition records.h:67
std::optional< int > max_turns
Turn limit (nullable)
Definition records.h:63
std::string parent_conversation_id
Parent conversation FK.
Definition records.h:58
std::optional< std::string > result_summary
Result summary (nullable)
Definition records.h:65
std::string task
Task description.
Definition records.h:62
std::string id
UUID primary key.
Definition records.h:57
std::string child_conversation_id
Child conversation FK.
Definition records.h:59