36CREATE TABLE IF NOT EXISTS conversations (
39 created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
40 updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
43 metadata TEXT DEFAULT '{}'
46CREATE TABLE IF NOT EXISTS messages (
48 conversation_id TEXT NOT NULL,
49 role TEXT NOT NULL CHECK(role IN ('user', 'assistant', 'system', 'tool')),
50 content TEXT NOT NULL,
51 tool_calls TEXT DEFAULT '[]',
52 tool_results TEXT DEFAULT '[]',
53 token_count INTEGER DEFAULT 0,
54 created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
55 is_compacted BOOLEAN DEFAULT FALSE,
57 FOREIGN KEY (conversation_id) REFERENCES conversations(id) ON DELETE CASCADE
60CREATE TABLE IF NOT EXISTS tool_executions (
63 server_name TEXT NOT NULL,
64 tool_name TEXT NOT NULL,
65 arguments TEXT DEFAULT '{}',
67 duration_ms INTEGER DEFAULT 0,
68 status TEXT CHECK(status IN ('success', 'error', 'timeout')),
69 created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
70 FOREIGN KEY (message_id) REFERENCES messages(id) ON DELETE CASCADE
73CREATE INDEX IF NOT EXISTS idx_messages_conversation
74 ON messages(conversation_id);
75CREATE INDEX IF NOT EXISTS idx_messages_created
76 ON messages(created_at);
77CREATE INDEX IF NOT EXISTS idx_conversations_updated
78 ON conversations(updated_at);
83CREATE VIRTUAL TABLE IF NOT EXISTS messages_fts USING fts5(
89CREATE TRIGGER IF NOT EXISTS messages_ai AFTER INSERT ON messages BEGIN
90 INSERT INTO messages_fts(rowid, content) VALUES (NEW.rowid, NEW.content);
93CREATE TRIGGER IF NOT EXISTS messages_ad AFTER DELETE ON messages BEGIN
94 INSERT INTO messages_fts(messages_fts, rowid, content)
95 VALUES('delete', OLD.rowid, OLD.content);
98CREATE TRIGGER IF NOT EXISTS messages_au AFTER UPDATE ON messages BEGIN
99 INSERT INTO messages_fts(messages_fts, rowid, content)
100 VALUES('delete', OLD.rowid, OLD.content);
101 INSERT INTO messages_fts(rowid, content) VALUES (NEW.rowid, NEW.content);
107CREATE TABLE IF NOT EXISTS delegations (
109 parent_conversation_id TEXT NOT NULL,
110 child_conversation_id TEXT NOT NULL,
111 delegating_tier TEXT NOT NULL,
112 target_tier TEXT NOT NULL,
115 status TEXT NOT NULL DEFAULT 'pending'
116 CHECK(status IN ('pending', 'running', 'completed', 'failed')),
118 created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
119 completed_at TIMESTAMP,
120 FOREIGN KEY (parent_conversation_id)
121 REFERENCES conversations(id) ON DELETE CASCADE,
122 FOREIGN KEY (child_conversation_id)
123 REFERENCES conversations(id) ON DELETE CASCADE
126CREATE INDEX IF NOT EXISTS idx_delegations_parent
127 ON delegations(parent_conversation_id);
128CREATE INDEX IF NOT EXISTS idx_delegations_child
129 ON delegations(child_conversation_id);
134CREATE TABLE IF NOT EXISTS compaction_snapshots (
136 conversation_id TEXT NOT NULL,
137 messages_json TEXT NOT NULL,
138 message_count INTEGER NOT NULL,
139 token_count_estimate INTEGER,
140 created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
141 FOREIGN KEY (conversation_id)
142 REFERENCES conversations(id) ON DELETE CASCADE
145CREATE INDEX IF NOT EXISTS idx_snapshots_conversation
146 ON compaction_snapshots(conversation_id);
166 : db_path_(db_path) {}
184 std::lock_guard lock(mutex_);
187 auto parent = db_path_.parent_path();
189 std::filesystem::create_directories(
parent);
193 db_path_.c_str(), &db_,
198 logger->error(
"Failed to open database {}: {}",
204 sqlite3_exec(db_,
"PRAGMA foreign_keys = ON",
nullptr,
nullptr,
nullptr);
205 sqlite3_exec(db_,
"PRAGMA journal_mode = WAL",
nullptr,
nullptr,
nullptr);
207 if (!run_migrations()) {
208 logger->error(
"Migration failed for {}", db_path_.string());
212 logger->info(
"Database initialized: {}", db_path_.string());
222 std::lock_guard lock(mutex_);
236 std::lock_guard lock(mutex_);
237 return db_ !=
nullptr;
247sqlite3_stmt* SqliteDatabase::prepare(std::string_view sql) {
250 db_, sql.data(),
static_cast<int>(sql.size()),
254 logger->error(
"SQL prepare failed: {} — {}",
271 std::lock_guard lock(mutex_);
272 if (!db_)
return false;
274 auto*
stmt = prepare(sql);
275 if (!
stmt)
return false;
296 std::lock_guard lock(mutex_);
297 if (!db_)
return false;
301 db_, std::string(sql).
c_str(),
305 logger->error(
"SQL exec failed: {}",
323 std::string_view sql,
326 std::lock_guard lock(mutex_);
327 if (!db_)
return false;
329 auto*
stmt = prepare(sql);
330 if (!
stmt)
return false;
354 std::string_view sql,
357 std::lock_guard lock(mutex_);
360 auto*
stmt = prepare(sql);
393 std::vector<std::string>
applied;
396 "SELECT name FROM migrations", -1, &
stmt,
nullptr);
402 applied.emplace_back(
reinterpret_cast<const char*
>(text));
418 logger->info(
"Running migration: {}",
mig.name);
423 logger->error(
"Migration {} failed: {}",
431 "INSERT INTO migrations (name) VALUES (?)",
452 if (
n == name)
return true;
469bool SqliteDatabase::run_migrations() {
472 "CREATE TABLE IF NOT EXISTS migrations ("
473 " id INTEGER PRIMARY KEY,"
475 " applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP"
480 logger->error(
"Failed to create migrations table: {}",
bool execute(std::string_view sql, std::function< void(sqlite3_stmt *)> binder=nullptr)
Execute a write statement (INSERT, UPDATE, DELETE).
bool execute_raw(std::string_view sql)
Execute raw SQL (multiple statements, no binding).
bool is_open() const
Check if database is open.
void close()
Close database connection.
~SqliteDatabase()
Destructor — closes connection if open.
sqlite3 * raw_handle() const
Get the underlying sqlite3 handle (for advanced use).
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.
SqliteDatabase(const std::filesystem::path &db_path)
Construct with database file path.
Thread-safe SQLite database wrapper with migration support.
spdlog initialization and logger access.
ENTROPIC_EXPORT std::shared_ptr< spdlog::logger > get(const std::string &name)
Get or create a named logger.
Activate model on GPU (WARM → ACTIVE).
@ ok
Tool dispatched, returned non-empty content.
@ optional
optional: every escape asks the host's path approver
static constexpr std::array< Migration, 4 > MIGRATIONS
@dg_internal
static bool apply_migration(sqlite3 *db, const Migration &mig)
Apply a single migration and record it.
@ count
Sentinel — MUST remain last.
static constexpr const char * MIGRATION_003_DELEGATIONS
@dg_internal
static constexpr const char * MIGRATION_002_FTS
@dg_internal
static constexpr const char * MIGRATION_004_COMPACTION_SNAPSHOTS
@dg_internal
static constexpr const char * MIGRATION_001_INITIAL
@dg_internal
static std::vector< std::string > get_applied_migrations(sqlite3 *db)
Get names of already-applied migrations.
static bool is_applied(const std::vector< std::string > &applied, const char *name)
Check if a migration name is in the applied list.
A single named migration.
const char * name
Migration name (e.g., "001_initial")
const char * sql
SQL to execute.