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 delivery_queue;
44pub mod efficiency;
45pub mod embeddings;
46mod ext;
47pub use ext::*;
48pub mod hippocampus;
49pub mod hygiene_log;
50pub mod learned_skills;
51pub mod memory;
52pub mod metrics;
53pub mod model_selection;
54pub mod policy;
55pub mod revenue_accounting;
56pub mod revenue_feedback;
57pub mod revenue_introspection;
58pub mod revenue_opportunity_queries;
59pub mod revenue_scoring;
60pub mod revenue_strategy_summary;
61pub mod revenue_swap_tasks;
62pub mod revenue_tax_tasks;
63pub mod routing_dataset;
64pub mod schema;
65pub mod service_revenue;
66pub mod sessions;
67pub mod shadow_routing;
68pub mod skills;
69pub mod tasks;
70pub mod tools;
71
72use std::sync::{Arc, Mutex};
73
74use rusqlite::Connection;
75pub use rusqlite::params_from_iter;
76
77use roboticus_core::Result;
78
79#[derive(Clone)]
80pub struct Database {
81 conn: Arc<Mutex<Connection>>,
82}
83
84impl Database {
85 /// Opens a new database at the given path (or in-memory if `":memory:"`).
86 ///
87 /// # Examples
88 ///
89 /// ```
90 /// use roboticus_db::Database;
91 ///
92 /// let db = Database::new(":memory:").unwrap();
93 /// // database is now ready for use
94 /// ```
95 pub fn new(path: &str) -> Result<Self> {
96 let conn = if path == ":memory:" {
97 Connection::open_in_memory()
98 } else {
99 Connection::open(path)
100 }
101 .db_err()?;
102
103 // WAL mode + foreign keys + synchronous=NORMAL (safe under WAL, ~2x
104 // write throughput vs FULL which adds an unnecessary extra fsync on
105 // every checkpoint). auto_vacuum=INCREMENTAL lets us reclaim space
106 // on demand via `PRAGMA incremental_vacuum` without full-db rewrites.
107 conn.execute_batch(
108 "PRAGMA journal_mode=WAL; \
109 PRAGMA foreign_keys=ON; \
110 PRAGMA synchronous=NORMAL; \
111 PRAGMA auto_vacuum=INCREMENTAL;",
112 )
113 .db_err()?;
114
115 // For existing databases that were created with auto_vacuum=NONE,
116 // switching to INCREMENTAL requires a one-time full VACUUM. Check
117 // current mode and upgrade if needed. This is a no-op on new DBs.
118 let current_auto_vacuum: i64 = conn
119 .query_row("PRAGMA auto_vacuum", [], |row| row.get(0))
120 .unwrap_or(0);
121 if current_auto_vacuum == 0 {
122 // 0 = NONE, 2 = INCREMENTAL. VACUUM rewrites the DB file once.
123 let _ = conn.execute_batch("PRAGMA auto_vacuum=INCREMENTAL; VACUUM;");
124 }
125
126 let db = Self {
127 conn: Arc::new(Mutex::new(conn)),
128 };
129 schema::initialize_db(&db)?;
130 Ok(db)
131 }
132
133 /// Acquires the database connection mutex, recovering from poison if a
134 /// prior holder panicked.
135 ///
136 /// Rationale: the `Connection` is a handle to a WAL-mode SQLite database.
137 /// If a thread panics mid-transaction, SQLite's journal automatically rolls
138 /// back uncommitted changes on the next access, so the database file is
139 /// never left in a corrupt state. Propagating the poison would make the
140 /// entire database permanently unavailable for all threads, which is worse
141 /// than allowing the next caller to proceed with a connection that SQLite
142 /// has already self-healed.
143 pub fn conn(&self) -> std::sync::MutexGuard<'_, Connection> {
144 self.conn.lock().unwrap_or_else(|e| e.into_inner())
145 }
146}
147
148impl std::fmt::Debug for Database {
149 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
150 f.debug_struct("Database").finish()
151 }
152}
153
154#[cfg(test)]
155mod tests {
156 use super::*;
157
158 #[test]
159 fn database_debug_impl() {
160 let db = Database::new(":memory:").expect("in-memory db");
161 let s = format!("{:?}", db);
162 assert_eq!(s, "Database");
163 }
164
165 #[test]
166 fn database_new_in_memory() {
167 let db = Database::new(":memory:").expect("in-memory db");
168 let _guard = db.conn();
169 }
170
171 #[test]
172 fn database_new_invalid_path_returns_error() {
173 let result = Database::new("/");
174 assert!(result.is_err(), "opening \"/\" as database should fail");
175 }
176}