roboticus_db/lib.rs
1//! # roboticus-db
2//!
3//! SQLite persistence layer for the Roboticus agent runtime. All state --
4//! sessions, memories, tool calls, policy decisions, cron jobs, embeddings,
5//! skills, and semantic cache -- lives in a single SQLite database with WAL
6//! mode enabled.
7//!
8//! ## Key Types
9//!
10//! - [`Database`] -- Thread-safe handle wrapping `Arc<Mutex<Connection>>`
11//!
12//! ## Modules
13//!
14//! - `schema` -- Table definitions, `initialize_db()`, migration runner
15//! - `sessions` -- Session CRUD, message append/list, turn persistence
16//! - `memory` -- 5-tier memory CRUD (working, episodic, semantic, procedural, relationship) + FTS5
17//! - `embeddings` -- BLOB embedding storage / lookup with JSON fallback
18//! - `ann` -- HNSW approximate nearest-neighbor index (instant-distance)
19//! - `hippocampus` -- Long-term memory consolidation and decay
20//! - `learned_skills` -- Learned skill CRUD, reinforcement (success/failure), priority
21//! - `checkpoint` -- Session checkpoint / restore via `context_snapshots` table
22//! - `efficiency` -- Efficiency metrics tracking and queries
23//! - `agents` -- Sub-agent registry and enabled-agent CRUD
24//! - `backend` -- Storage backend abstraction trait
25//! - `cache` -- Semantic cache persistence (loaded on boot, flushed every 5 min)
26//! - `cron` -- Cron job state, lease acquisition, run history
27//! - `skills` -- Skill definition CRUD and trigger lookup
28//! - `tools` -- Tool call records
29//! - `policy` -- Policy decision records
30//! - `metrics` -- Inference cost tracking, proxy snapshots, transactions, turn feedback
31//! - `routing_dataset` -- Historical routing decision + cost outcome JOIN for ML training
32//! - `shadow_routing` -- Counterfactual ML predictions stored alongside production decisions
33//! - `revenue_introspection` -- Unified introspection surface: strategy health, profitability, audit trail
34
35pub mod abuse;
36pub mod agents;
37pub mod ann;
38pub mod approvals;
39pub mod backend;
40pub mod cache;
41pub mod checkpoint;
42pub mod cron;
43pub mod delegation;
44pub mod delivery_queue;
45pub mod efficiency;
46pub mod embeddings;
47mod ext;
48pub use ext::*;
49pub mod hippocampus;
50pub mod hygiene_log;
51pub mod learned_skills;
52pub mod memory;
53pub mod metrics;
54pub mod model_selection;
55pub mod policy;
56pub mod revenue_accounting;
57pub mod revenue_feedback;
58pub mod revenue_introspection;
59pub mod revenue_opportunity_queries;
60pub mod revenue_scoring;
61pub mod revenue_strategy_summary;
62pub mod revenue_swap_tasks;
63pub mod revenue_tax_tasks;
64pub mod routing_dataset;
65pub mod schema;
66pub mod service_revenue;
67pub mod sessions;
68pub mod shadow_routing;
69pub mod skills;
70pub mod tasks;
71pub mod tools;
72pub mod traces;
73pub mod typestate;
74
75use std::sync::{Arc, Mutex};
76
77use rusqlite::Connection;
78pub use rusqlite::params_from_iter;
79
80use roboticus_core::Result;
81
82#[derive(Clone)]
83pub struct Database {
84 conn: Arc<Mutex<Connection>>,
85}
86
87impl Database {
88 /// Opens a new database at the given path (or in-memory if `":memory:"`).
89 ///
90 /// # Examples
91 ///
92 /// ```
93 /// use roboticus_db::Database;
94 ///
95 /// let db = Database::new(":memory:").unwrap();
96 /// // database is now ready for use
97 /// ```
98 pub fn new(path: &str) -> Result<Self> {
99 let conn = if path == ":memory:" {
100 Connection::open_in_memory()
101 } else {
102 Connection::open(path)
103 }
104 .db_err()?;
105
106 // WAL mode + foreign keys + synchronous=NORMAL (safe under WAL, ~2x
107 // write throughput vs FULL which adds an unnecessary extra fsync on
108 // every checkpoint). auto_vacuum=INCREMENTAL lets us reclaim space
109 // on demand via `PRAGMA incremental_vacuum` without full-db rewrites.
110 conn.execute_batch(
111 "PRAGMA journal_mode=WAL; \
112 PRAGMA foreign_keys=ON; \
113 PRAGMA synchronous=NORMAL; \
114 PRAGMA auto_vacuum=INCREMENTAL;",
115 )
116 .db_err()?;
117
118 // For existing databases that were created with auto_vacuum=NONE,
119 // switching to INCREMENTAL requires a one-time full VACUUM. Check
120 // current mode and upgrade if needed. This is a no-op on new DBs.
121 let current_auto_vacuum: i64 = conn
122 .query_row("PRAGMA auto_vacuum", [], |row| row.get(0))
123 .unwrap_or(0);
124 if current_auto_vacuum == 0 {
125 // 0 = NONE, 2 = INCREMENTAL. VACUUM rewrites the DB file once.
126 let _ = conn.execute_batch("PRAGMA auto_vacuum=INCREMENTAL; VACUUM;");
127 }
128
129 let db = Self {
130 conn: Arc::new(Mutex::new(conn)),
131 };
132 schema::initialize_db(&db)?;
133 Ok(db)
134 }
135
136 /// Acquires the database connection mutex, recovering from poison if a
137 /// prior holder panicked.
138 ///
139 /// Rationale: the `Connection` is a handle to a WAL-mode SQLite database.
140 /// If a thread panics mid-transaction, SQLite's journal automatically rolls
141 /// back uncommitted changes on the next access, so the database file is
142 /// never left in a corrupt state. Propagating the poison would make the
143 /// entire database permanently unavailable for all threads, which is worse
144 /// than allowing the next caller to proceed with a connection that SQLite
145 /// has already self-healed.
146 pub fn conn(&self) -> std::sync::MutexGuard<'_, Connection> {
147 self.conn.lock().unwrap_or_else(|e| e.into_inner())
148 }
149}
150
151impl std::fmt::Debug for Database {
152 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
153 f.debug_struct("Database").finish()
154 }
155}
156
157#[cfg(test)]
158mod tests {
159 use super::*;
160
161 #[test]
162 fn database_debug_impl() {
163 let db = Database::new(":memory:").expect("in-memory db");
164 let s = format!("{:?}", db);
165 assert_eq!(s, "Database");
166 }
167
168 #[test]
169 fn database_new_in_memory() {
170 let db = Database::new(":memory:").expect("in-memory db");
171 let _guard = db.conn();
172 }
173
174 #[test]
175 fn database_new_invalid_path_returns_error() {
176 let result = Database::new("/");
177 assert!(result.is_err(), "opening \"/\" as database should fail");
178 }
179}