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 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}