Skip to main content

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}