Skip to main content

claw_core/
engine.rs

1//! Main engine entry point for claw-core.
2//!
3//! [`ClawEngine`] is the primary handle through which callers interact with the
4//! embedded SQLite database. It owns the SQLx connection pool, an internal LRU
5//! cache for [`MemoryRecord`]s, applies migrations on startup (when
6//! `auto_migrate` is enabled), and exposes methods for interacting with the
7//! various store modules.
8
9use std::path::Path;
10use std::sync::Arc;
11
12use sqlx::SqlitePool;
13use tokio::sync::Mutex;
14use uuid::Uuid;
15
16use crate::cache::{CacheStats, ClawCache};
17use crate::config::ClawConfig;
18use crate::error::{ClawError, ClawResult};
19use crate::snapshot::{SnapshotManifest, SnapshotMeta, Snapshotter};
20use crate::store::memory::{ListOptions, MemoryRecord, MemoryStore, MemoryType};
21use crate::store::session_lifecycle::{Session, SessionLifecycleStore};
22use crate::store::tool_output::{ToolOutputRecord, ToolOutputStore};
23
24/// Database-level statistics for a [`ClawEngine`] instance.
25///
26/// Retrieve via [`ClawEngine::db_stats`].
27///
28/// # Example
29///
30/// ```rust,no_run
31/// # use claw_core::ClawEngine;
32/// # async fn example() -> claw_core::ClawResult<()> {
33/// # let engine = ClawEngine::open_default().await?;
34/// let stats = engine.db_stats().await?;
35/// println!("memories: {}", stats.memory_count);
36/// # Ok(())
37/// # }
38/// ```
39#[derive(Debug, Clone)]
40pub struct DbStats {
41    /// Total number of memory records in the database.
42    pub memory_count: u64,
43    /// Total number of sessions in the database.
44    pub session_count: u64,
45    /// Total number of tool-output records in the database.
46    pub tool_output_count: u64,
47}
48
49/// Comprehensive runtime statistics for a [`ClawEngine`] instance.
50///
51/// Retrieve via [`ClawEngine::stats`].
52///
53/// # Example
54///
55/// ```rust,no_run
56/// # use claw_core::ClawEngine;
57/// # async fn example() -> claw_core::ClawResult<()> {
58/// # let engine = ClawEngine::open_default().await?;
59/// let s = engine.stats().await?;
60/// println!("hit rate (last 1 000 ops): {:.1}%", s.cache_hit_rate * 100.0);
61/// # Ok(())
62/// # }
63/// ```
64#[derive(Debug, Clone)]
65pub struct ClawStats {
66    /// Total number of memory records currently stored.
67    pub total_memories: u64,
68    /// Cache hit rate over the most recent 1 000 lookup operations (0.0 – 1.0).
69    pub cache_hit_rate: f64,
70    /// Timestamp of the most recently created snapshot, or `None` if no
71    /// snapshot has been taken during this engine session.
72    pub last_snapshot_at: Option<chrono::DateTime<chrono::Utc>>,
73    /// Size of the main database file in bytes.
74    pub db_size_bytes: u64,
75    /// Size of the WAL file in bytes (0 if WAL is not enabled or not present).
76    pub wal_size_bytes: u64,
77}
78
79/// The main entry point for claw-core.
80///
81/// Construct a [`ClawEngine`] via [`ClawEngine::open`] or
82/// [`ClawEngine::open_default`], passing a validated [`ClawConfig`]. The engine
83/// holds the underlying SQLx [`SqlitePool`], an internal LRU memory cache, and
84/// serves as the root accessor for all store, transaction, and snapshot APIs.
85///
86/// # Example
87///
88/// ```rust,no_run
89/// use claw_core::{ClawEngine, ClawConfig};
90///
91/// # async fn example() -> claw_core::ClawResult<()> {
92/// let config = ClawConfig::builder()
93///     .db_path("/tmp/my_agent.db")
94///     .build()?;
95/// let engine = ClawEngine::open(config).await?;
96/// engine.close().await;
97/// # Ok(())
98/// # }
99/// ```
100#[derive(Debug)]
101pub struct ClawEngine {
102    /// Validated runtime configuration.
103    pub(crate) config: ClawConfig,
104    /// SQLx connection pool backed by SQLite.
105    pub(crate) pool: SqlitePool,
106    /// In-memory LRU cache for [`MemoryRecord`]s.
107    cache: Arc<Mutex<ClawCache<Uuid, MemoryRecord>>>,
108    /// Running cache statistics (includes rolling hit-rate window).
109    stats: Arc<Mutex<CacheStats>>,
110    /// Timestamp of the last successful snapshot taken during this session.
111    last_snapshot_at: Arc<Mutex<Option<chrono::DateTime<chrono::Utc>>>>,
112}
113
114impl ClawEngine {
115    /// Open (or create) the database at the path specified in `config`.
116    ///
117    /// When `config.auto_migrate` is `true`, any pending embedded migrations
118    /// are applied before the engine is returned.
119    ///
120    /// # Errors
121    ///
122    /// Returns a [`ClawError`] if the pool cannot be created or migrations fail.
123    ///
124    /// # Example
125    ///
126    /// ```rust,no_run
127    /// # use claw_core::{ClawEngine, ClawConfig};
128    /// # async fn example() -> claw_core::ClawResult<()> {
129    /// let config = ClawConfig::builder()
130    ///     .db_path("/tmp/claw.db")
131    ///     .build()?;
132    /// let engine = ClawEngine::open(config).await?;
133    /// engine.close().await;
134    /// # Ok(())
135    /// # }
136    /// ```
137    pub async fn open(config: ClawConfig) -> ClawResult<Self> {
138        use sqlx::sqlite::{SqliteConnectOptions, SqliteJournalMode, SqlitePoolOptions};
139        use std::str::FromStr;
140
141        let db_url = format!("sqlite:{}", config.db_path.display());
142
143        let journal_mode = match config.journal_mode {
144            crate::config::JournalMode::WAL => SqliteJournalMode::Wal,
145            crate::config::JournalMode::Delete => SqliteJournalMode::Delete,
146            crate::config::JournalMode::Truncate => SqliteJournalMode::Truncate,
147        };
148
149        let connect_options = SqliteConnectOptions::from_str(&db_url)
150            .map_err(|e| ClawError::Config(format!("invalid database URL: {e}")))?
151            .create_if_missing(true)
152            .journal_mode(journal_mode);
153
154        let pool = SqlitePoolOptions::new()
155            .max_connections(config.max_connections)
156            .connect_with(connect_options)
157            .await?;
158
159        // When the "encryption" feature is enabled and a key is provided,
160        // apply `PRAGMA key` immediately after connecting.  This requires a
161        // SQLCipher build of SQLite (not the default bundled libsqlite3).
162        #[cfg(feature = "encryption")]
163        if let Some(key) = &config.encryption_key {
164            let hex: String = key.iter().map(|b| format!("{b:02x}")).collect();
165            sqlx::query(&format!("PRAGMA key = \"x'{hex}'\""))
166                .execute(&pool)
167                .await?;
168        }
169
170        // Estimate ~512 bytes per MemoryRecord; minimum 64 entries.
171        let cache_cap = ((config.cache_size_mb * 1024 * 1024) / 512).max(64);
172        let cache = Arc::new(Mutex::new(ClawCache::new(cache_cap)?));
173        let stats = Arc::new(Mutex::new(CacheStats::new()));
174
175        let engine = ClawEngine {
176            config,
177            pool,
178            cache,
179            stats,
180            last_snapshot_at: Arc::new(Mutex::new(None)),
181        };
182
183        if engine.config.auto_migrate {
184            engine.migrate().await?;
185        }
186
187        Ok(engine)
188    }
189
190    /// Open the database using the default [`ClawConfig`].
191    ///
192    /// The database is created at `$XDG_DATA_HOME/clawdb/claw.db` (or
193    /// platform equivalent). All migrations are applied automatically.
194    ///
195    /// # Errors
196    ///
197    /// Returns a [`ClawError`] if the pool cannot be created or migrations fail.
198    ///
199    /// # Example
200    ///
201    /// ```rust,no_run
202    /// use claw_core::ClawEngine;
203    ///
204    /// # async fn example() -> claw_core::ClawResult<()> {
205    /// let engine = ClawEngine::open_default().await?;
206    /// engine.close().await;
207    /// # Ok(())
208    /// # }
209    /// ```
210    pub async fn open_default() -> ClawResult<Self> {
211        ClawEngine::open(ClawConfig::default()).await
212    }
213
214    /// Apply any pending embedded SQL migrations.
215    ///
216    /// # Errors
217    ///
218    /// Returns [`ClawError::Migration`] if a migration step fails.
219    pub async fn migrate(&self) -> ClawResult<()> {
220        crate::schema::migrations::run_migrations(&self.pool).await
221    }
222
223    /// Return a reference to the underlying [`SqlitePool`].
224    pub fn pool(&self) -> &SqlitePool {
225        &self.pool
226    }
227
228    /// Return a reference to the engine's [`ClawConfig`].
229    pub fn config(&self) -> &ClawConfig {
230        &self.config
231    }
232
233    /// Close the connection pool, waiting for in-flight queries to complete.
234    pub async fn close(self) {
235        self.pool.close().await;
236    }
237
238    // ── Memory API ────────────────────────────────────────────────────────────
239
240    /// Insert a new [`MemoryRecord`] into the database.
241    ///
242    /// The record is also inserted into the in-memory LRU cache.
243    ///
244    /// # Errors
245    ///
246    /// Returns a [`ClawError`] if the SQL execution fails.
247    ///
248    /// # Example
249    ///
250    /// ```rust,no_run
251    /// # use claw_core::{ClawEngine, MemoryRecord, MemoryType};
252    /// # async fn example() -> claw_core::ClawResult<()> {
253    /// # let engine = ClawEngine::open_default().await?;
254    /// let record = MemoryRecord::new("hello world", MemoryType::Semantic, vec![], None);
255    /// let id = engine.insert_memory(&record).await?;
256    /// # Ok(())
257    /// # }
258    /// ```
259    #[tracing::instrument(skip(self, record), fields(memory_id = %record.id))]
260    pub async fn insert_memory(&self, record: &MemoryRecord) -> ClawResult<Uuid> {
261        MemoryStore::new(&self.pool).insert(record).await?;
262        let mut cache = self.cache.lock().await;
263        let mut stats = self.stats.lock().await;
264        cache.insert(record.id, record.clone());
265        stats.insert_count += 1;
266        Ok(record.id)
267    }
268
269    /// Retrieve a [`MemoryRecord`] by its UUID.
270    ///
271    /// Results are served from the in-memory LRU cache when available.
272    ///
273    /// # Errors
274    ///
275    /// Returns [`ClawError::NotFound`] if no record with the given `id` exists.
276    ///
277    /// # Example
278    ///
279    /// ```rust,no_run
280    /// # use claw_core::{ClawEngine, MemoryRecord, MemoryType};
281    /// # async fn example() -> claw_core::ClawResult<()> {
282    /// # let engine = ClawEngine::open_default().await?;
283    /// let record = MemoryRecord::new("hello", MemoryType::Semantic, vec![], None);
284    /// let id = engine.insert_memory(&record).await?;
285    /// let fetched = engine.get_memory(id).await?;
286    /// assert_eq!(fetched.content, "hello");
287    /// # Ok(())
288    /// # }
289    /// ```
290    #[tracing::instrument(skip(self), fields(memory_id = %id))]
291    pub async fn get_memory(&self, id: Uuid) -> ClawResult<MemoryRecord> {
292        {
293            let mut cache = self.cache.lock().await;
294            let mut stats = self.stats.lock().await;
295            if let Some(record) = cache.get(&id) {
296                stats.record_hit();
297                return Ok(record.clone());
298            }
299            stats.record_miss();
300        }
301
302        let record = MemoryStore::new(&self.pool).get(id).await?;
303        let mut cache = self.cache.lock().await;
304        cache.insert(record.id, record.clone());
305        Ok(record)
306    }
307
308    /// Update the content of an existing [`MemoryRecord`].
309    ///
310    /// The `updated_at` timestamp is set to the current UTC time. The cache
311    /// entry is invalidated so the next read fetches fresh data.
312    ///
313    /// # Errors
314    ///
315    /// Returns [`ClawError::NotFound`] if no record with the given `id` exists.
316    ///
317    /// # Example
318    ///
319    /// ```rust,no_run
320    /// # use claw_core::{ClawEngine, MemoryRecord, MemoryType};
321    /// # async fn example() -> claw_core::ClawResult<()> {
322    /// # let engine = ClawEngine::open_default().await?;
323    /// let record = MemoryRecord::new("old content", MemoryType::Semantic, vec![], None);
324    /// let id = engine.insert_memory(&record).await?;
325    /// engine.update_memory(id, "new content").await?;
326    /// # Ok(())
327    /// # }
328    /// ```
329    #[tracing::instrument(skip(self), fields(memory_id = %id))]
330    pub async fn update_memory(&self, id: Uuid, content: &str) -> ClawResult<()> {
331        let updated_at = chrono::Utc::now();
332        MemoryStore::new(&self.pool)
333            .update_content(id, content, updated_at)
334            .await?;
335        let mut cache = self.cache.lock().await;
336        cache.invalidate(&id);
337        Ok(())
338    }
339
340    /// Delete a [`MemoryRecord`] from the database by its UUID.
341    ///
342    /// The cache entry is also invalidated.
343    ///
344    /// # Errors
345    ///
346    /// Returns [`ClawError::NotFound`] if no record with the given `id` exists.
347    ///
348    /// # Example
349    ///
350    /// ```rust,no_run
351    /// # use claw_core::{ClawEngine, MemoryRecord, MemoryType};
352    /// # async fn example() -> claw_core::ClawResult<()> {
353    /// # let engine = ClawEngine::open_default().await?;
354    /// let record = MemoryRecord::new("to delete", MemoryType::Episodic, vec![], None);
355    /// let id = engine.insert_memory(&record).await?;
356    /// engine.delete_memory(id).await?;
357    /// # Ok(())
358    /// # }
359    /// ```
360    #[tracing::instrument(skip(self), fields(memory_id = %id))]
361    pub async fn delete_memory(&self, id: Uuid) -> ClawResult<()> {
362        MemoryStore::new(&self.pool).delete(id).await?;
363        let mut cache = self.cache.lock().await;
364        cache.invalidate(&id);
365        Ok(())
366    }
367
368    /// List all [`MemoryRecord`]s, optionally filtered by [`MemoryType`].
369    ///
370    /// Results are ordered by `created_at` ascending.
371    ///
372    /// # Errors
373    ///
374    /// Returns a [`ClawError`] if the query fails.
375    ///
376    /// # Example
377    ///
378    /// ```rust,no_run
379    /// # use claw_core::{ClawEngine, MemoryType};
380    /// # async fn example() -> claw_core::ClawResult<()> {
381    /// # let engine = ClawEngine::open_default().await?;
382    /// let all = engine.list_memories(None).await?;
383    /// let semantic = engine.list_memories(Some(MemoryType::Semantic)).await?;
384    /// # Ok(())
385    /// # }
386    /// ```
387    #[tracing::instrument(skip(self))]
388    pub async fn list_memories(
389        &self,
390        type_filter: Option<MemoryType>,
391    ) -> ClawResult<Vec<MemoryRecord>> {
392        MemoryStore::new(&self.pool)
393            .list(type_filter.as_ref())
394            .await
395    }
396
397    /// List memories with keyset pagination.
398    ///
399    /// # Errors
400    ///
401    /// Returns a [`ClawError`] if the query fails.
402    ///
403    /// # Example
404    ///
405    /// ```rust,no_run
406    /// # use claw_core::{ClawEngine, ListOptions, MemoryType};
407    /// # async fn example() -> claw_core::ClawResult<()> {
408    /// # let engine = ClawEngine::open_default().await?;
409    /// let (page, cursor) = engine.list_memories_paginated(
410    ///     Some(MemoryType::Semantic),
411    ///     ListOptions { limit: 50, cursor: None },
412    /// ).await?;
413    /// # Ok(())
414    /// # }
415    /// ```
416    #[tracing::instrument(skip(self))]
417    pub async fn list_memories_paginated(
418        &self,
419        type_filter: Option<MemoryType>,
420        opts: ListOptions,
421    ) -> ClawResult<(Vec<MemoryRecord>, Option<String>)> {
422        MemoryStore::new(&self.pool)
423            .list_paginated(type_filter.as_ref(), &opts)
424            .await
425    }
426
427    /// Search for [`MemoryRecord`]s whose tag list contains `tag`.
428    ///
429    /// # Errors
430    ///
431    /// Returns a [`ClawError`] if the query fails.
432    ///
433    /// # Example
434    ///
435    /// ```rust,no_run
436    /// # use claw_core::ClawEngine;
437    /// # async fn example() -> claw_core::ClawResult<()> {
438    /// # let engine = ClawEngine::open_default().await?;
439    /// let results = engine.search_by_tag("important").await?;
440    /// # Ok(())
441    /// # }
442    /// ```
443    #[tracing::instrument(skip(self))]
444    pub async fn search_by_tag(&self, tag: &str) -> ClawResult<Vec<MemoryRecord>> {
445        MemoryStore::new(&self.pool).search_by_tag(tag).await
446    }
447
448    /// Full-text search over all [`MemoryRecord`] contents using SQLite FTS5.
449    ///
450    /// The `query` string follows FTS5 query syntax (e.g. `"hello world"` for
451    /// phrase search, `hello AND world` for AND search).
452    ///
453    /// # Errors
454    ///
455    /// Returns a [`ClawError`] if the query fails.
456    ///
457    /// # Example
458    ///
459    /// ```rust,no_run
460    /// # use claw_core::ClawEngine;
461    /// # async fn example() -> claw_core::ClawResult<()> {
462    /// # let engine = ClawEngine::open_default().await?;
463    /// let results = engine.fts_search("hello world").await?;
464    /// # Ok(())
465    /// # }
466    /// ```
467    #[tracing::instrument(skip(self))]
468    pub async fn fts_search(&self, query: &str) -> ClawResult<Vec<MemoryRecord>> {
469        MemoryStore::new(&self.pool).fts_search(query).await
470    }
471
472    /// Expire all [`MemoryRecord`]s whose TTL has elapsed.
473    ///
474    /// Returns the number of records deleted. The cache is cleared if any
475    /// records were deleted, since we do not know which IDs were affected.
476    ///
477    /// # Errors
478    ///
479    /// Returns a [`ClawError`] if the underlying deletion fails.
480    ///
481    /// # Example
482    ///
483    /// ```rust,no_run
484    /// # use claw_core::ClawEngine;
485    /// # async fn example() -> claw_core::ClawResult<()> {
486    /// # let engine = ClawEngine::open_default().await?;
487    /// let expired = engine.expire_ttl_memories().await?;
488    /// println!("deleted {expired} expired records");
489    /// # Ok(())
490    /// # }
491    /// ```
492    pub async fn expire_ttl_memories(&self) -> ClawResult<u64> {
493        let deleted = MemoryStore::new(&self.pool).expire_ttl().await?;
494        if deleted > 0 {
495            let mut cache = self.cache.lock().await;
496            cache.clear();
497        }
498        Ok(deleted)
499    }
500
501    // ── Session API ───────────────────────────────────────────────────────────
502
503    /// Start a new session and return its unique ID.
504    ///
505    /// # Errors
506    ///
507    /// Returns a [`ClawError`] if the SQL execution fails.
508    ///
509    /// # Example
510    ///
511    /// ```rust,no_run
512    /// # use claw_core::ClawEngine;
513    /// # async fn example() -> claw_core::ClawResult<()> {
514    /// # let engine = ClawEngine::open_default().await?;
515    /// let session_id = engine.start_session().await?;
516    /// # Ok(())
517    /// # }
518    /// ```
519    pub async fn start_session(&self) -> ClawResult<String> {
520        SessionLifecycleStore::new(&self.pool).start().await
521    }
522
523    /// Mark the session identified by `session_id` as ended.
524    ///
525    /// # Errors
526    ///
527    /// Returns [`ClawError::NotFound`] if the session does not exist.
528    ///
529    /// # Example
530    ///
531    /// ```rust,no_run
532    /// # use claw_core::ClawEngine;
533    /// # async fn example() -> claw_core::ClawResult<()> {
534    /// # let engine = ClawEngine::open_default().await?;
535    /// let sid = engine.start_session().await?;
536    /// engine.end_session(&sid).await?;
537    /// # Ok(())
538    /// # }
539    /// ```
540    pub async fn end_session(&self, session_id: &str) -> ClawResult<()> {
541        SessionLifecycleStore::new(&self.pool).end(session_id).await
542    }
543
544    /// Retrieve the [`Session`] record for `session_id`.
545    ///
546    /// # Errors
547    ///
548    /// Returns [`ClawError::NotFound`] if the session does not exist.
549    ///
550    /// # Example
551    ///
552    /// ```rust,no_run
553    /// # use claw_core::ClawEngine;
554    /// # async fn example() -> claw_core::ClawResult<()> {
555    /// # let engine = ClawEngine::open_default().await?;
556    /// let sid = engine.start_session().await?;
557    /// let session = engine.get_session(&sid).await?;
558    /// assert!(session.ended_at.is_none());
559    /// # Ok(())
560    /// # }
561    /// ```
562    pub async fn get_session(&self, session_id: &str) -> ClawResult<Session> {
563        SessionLifecycleStore::new(&self.pool).get(session_id).await
564    }
565
566    /// List all sessions, ordered by `started_at` descending.
567    ///
568    /// # Errors
569    ///
570    /// Returns a [`ClawError`] if the query fails.
571    ///
572    /// # Example
573    ///
574    /// ```rust,no_run
575    /// # use claw_core::ClawEngine;
576    /// # async fn example() -> claw_core::ClawResult<()> {
577    /// # let engine = ClawEngine::open_default().await?;
578    /// let sessions = engine.list_sessions().await?;
579    /// # Ok(())
580    /// # }
581    /// ```
582    pub async fn list_sessions(&self) -> ClawResult<Vec<Session>> {
583        SessionLifecycleStore::new(&self.pool).list().await
584    }
585
586    // ── Tool Output API ───────────────────────────────────────────────────────
587
588    /// Record a tool-output entry.
589    ///
590    /// # Errors
591    ///
592    /// Returns a [`ClawError`] if the SQL execution fails.
593    ///
594    /// # Example
595    ///
596    /// ```rust,no_run
597    /// # use claw_core::{ClawEngine, ToolOutput};
598    /// # use uuid::Uuid;
599    /// # async fn example() -> claw_core::ClawResult<()> {
600    /// # let engine = ClawEngine::open_default().await?;
601    /// let output = ToolOutput {
602    ///     id: Uuid::new_v4(),
603    ///     session_id: "sess-1".to_string(),
604    ///     tool_name: "my_tool".to_string(),
605    ///     output: serde_json::json!({"result": 42}),
606    ///     success: true,
607    ///     created_at: chrono::Utc::now(),
608    /// };
609    /// engine.record_tool_output(&output).await?;
610    /// # Ok(())
611    /// # }
612    /// ```
613    pub async fn record_tool_output(&self, output: &ToolOutputRecord) -> ClawResult<()> {
614        ToolOutputStore::new(&self.pool).insert(output).await
615    }
616
617    /// List all tool-output records for a given `session_id`.
618    ///
619    /// # Errors
620    ///
621    /// Returns a [`ClawError`] if the query fails.
622    ///
623    /// # Example
624    ///
625    /// ```rust,no_run
626    /// # use claw_core::ClawEngine;
627    /// # async fn example() -> claw_core::ClawResult<()> {
628    /// # let engine = ClawEngine::open_default().await?;
629    /// let outputs = engine.list_tool_outputs("sess-1").await?;
630    /// # Ok(())
631    /// # }
632    /// ```
633    pub async fn list_tool_outputs(&self, session_id: &str) -> ClawResult<Vec<ToolOutputRecord>> {
634        ToolOutputStore::new(&self.pool)
635            .get_by_session(session_id)
636            .await
637    }
638
639    // ── Transaction API ───────────────────────────────────────────────────────
640
641    /// Begin a new [`crate::transaction::ClawTransaction`] against this engine.
642    ///
643    /// # Errors
644    ///
645    /// Returns [`ClawError::Transaction`] if the pool cannot start a transaction.
646    ///
647    /// # Example
648    ///
649    /// ```rust,no_run
650    /// # use claw_core::{ClawEngine, MemoryRecord, MemoryType};
651    /// # async fn example() -> claw_core::ClawResult<()> {
652    /// # let engine = ClawEngine::open_default().await?;
653    /// let mut tx = engine.transaction().await?;
654    /// let r = MemoryRecord::new("hello", MemoryType::Semantic, vec![], None);
655    /// tx.insert_memory(&r).await?;
656    /// tx.commit().await?;
657    /// # Ok(())
658    /// # }
659    /// ```
660    pub async fn transaction(&self) -> ClawResult<crate::transaction::ClawTransaction<'_>> {
661        crate::transaction::ClawTransaction::begin(self).await
662    }
663
664    /// Begin a new [`crate::transaction::ClawTransaction`] against this engine.
665    ///
666    /// This is an alias for [`ClawEngine::transaction`].
667    ///
668    /// # Errors
669    ///
670    /// Returns [`ClawError::Transaction`] if the pool cannot start a transaction.
671    pub async fn begin_transaction(&self) -> ClawResult<crate::transaction::ClawTransaction<'_>> {
672        crate::transaction::ClawTransaction::begin(self).await
673    }
674
675    // ── Snapshot API ──────────────────────────────────────────────────────────
676
677    /// Create a snapshot of the current database state.
678    ///
679    /// Requires `config.snapshot_dir` to be set. A WAL checkpoint is performed
680    /// before the file copy to ensure all committed data is in the main DB file.
681    ///
682    /// # Errors
683    ///
684    /// Returns [`ClawError::Config`] if no snapshot directory is configured, or
685    /// [`ClawError::Snapshot`] if the snapshot file cannot be written.
686    ///
687    /// # Example
688    ///
689    /// ```rust,no_run
690    /// # use claw_core::{ClawEngine, ClawConfig};
691    /// # async fn example() -> claw_core::ClawResult<()> {
692    /// # let config = ClawConfig::builder()
693    /// #     .db_path("/tmp/claw.db")
694    /// #     .snapshot_dir("/tmp/snaps")
695    /// #     .build()?;
696    /// # let engine = ClawEngine::open(config).await?;
697    /// let meta = engine.snapshot_create().await?;
698    /// println!("snapshot at {}", meta.path.display());
699    /// # Ok(())
700    /// # }
701    /// ```
702    #[tracing::instrument(skip(self))]
703    pub async fn snapshot_create(&self) -> ClawResult<SnapshotMeta> {
704        let snap_dir = self.config.snapshot_dir.as_ref().ok_or_else(|| {
705            ClawError::Config("snapshot_dir must be set to use snapshot_create".to_string())
706        })?;
707        // Flush WAL pages into the main DB file before copying.
708        sqlx::query("PRAGMA wal_checkpoint(FULL)")
709            .execute(&self.pool)
710            .await?;
711        let snapshotter = Snapshotter::new(snap_dir)?;
712        let meta = snapshotter.take(&self.config.db_path)?;
713        *self.last_snapshot_at.lock().await = Some(meta.created_at);
714        Ok(meta)
715    }
716
717    /// Restore the database from a snapshot file.
718    ///
719    /// This method validates the snapshot is a genuine SQLite 3 file, closes
720    /// the connection pool, replaces the live database, removes stale WAL/SHM
721    /// sidecars, re-opens the pool, re-runs migrations, and clears the LRU
722    /// cache.
723    ///
724    /// # Errors
725    ///
726    /// Returns [`ClawError::Snapshot`] if the snapshot is invalid or the copy
727    /// fails, or a database error if the pool cannot be re-opened.
728    ///
729    /// # Example
730    ///
731    /// ```rust,no_run
732    /// # use claw_core::{ClawEngine, ClawConfig};
733    /// # use std::path::Path;
734    /// # async fn example() -> claw_core::ClawResult<()> {
735    /// # let config = ClawConfig::builder()
736    /// #     .db_path("/tmp/claw.db")
737    /// #     .snapshot_dir("/tmp/snaps")
738    /// #     .build()?;
739    /// # let mut engine = ClawEngine::open(config).await?;
740    /// engine.restore(Path::new("/tmp/snaps/snapshot.db")).await?;
741    /// # Ok(())
742    /// # }
743    /// ```
744    #[tracing::instrument(skip(self), fields(snapshot = %snapshot_path.display()))]
745    pub async fn restore(&mut self, snapshot_path: &Path) -> ClawResult<()> {
746        use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions};
747        use std::str::FromStr;
748
749        // 1. Validate SQLite magic bytes.
750        Self::validate_sqlite_magic(snapshot_path)?;
751
752        // 2. Close the existing connection pool.
753        self.pool.close().await;
754
755        // 3. Overwrite the live database with the snapshot.
756        std::fs::copy(snapshot_path, &self.config.db_path)
757            .map_err(|e| ClawError::Snapshot(format!("failed to restore snapshot: {e}")))?;
758
759        // 4. Remove stale WAL / SHM sidecar files.
760        let db_name = self
761            .config
762            .db_path
763            .file_name()
764            .unwrap_or_default()
765            .to_string_lossy()
766            .into_owned();
767        let db_parent = self
768            .config
769            .db_path
770            .parent()
771            .unwrap_or(std::path::Path::new("."));
772        for suffix in &["-wal", "-shm"] {
773            let sidecar = db_parent.join(format!("{db_name}{suffix}"));
774            if sidecar.exists() {
775                let _ = std::fs::remove_file(&sidecar);
776            }
777        }
778
779        // 5. Re-open the connection pool.
780        let db_url = format!("sqlite:{}", self.config.db_path.display());
781        let connect_options = SqliteConnectOptions::from_str(&db_url)
782            .map_err(|e| ClawError::Config(format!("invalid database URL: {e}")))?
783            .create_if_missing(false);
784        self.pool = SqlitePoolOptions::new()
785            .max_connections(self.config.max_connections)
786            .connect_with(connect_options)
787            .await?;
788
789        // Re-apply encryption key if required.
790        #[cfg(feature = "encryption")]
791        if let Some(key) = &self.config.encryption_key {
792            let hex: String = key.iter().map(|b| format!("{b:02x}")).collect();
793            sqlx::query(&format!("PRAGMA key = \"x'{hex}'\""))
794                .execute(&self.pool)
795                .await?;
796        }
797
798        // 6. Apply pending migrations.
799        self.migrate().await?;
800
801        // 7. Clear stale LRU cache entries.
802        self.cache.lock().await.clear();
803
804        tracing::info!(
805            snapshot = %snapshot_path.display(),
806            db = %self.config.db_path.display(),
807            "database restored from snapshot"
808        );
809        Ok(())
810    }
811
812    /// Load the [`SnapshotManifest`] from the configured snapshot directory.
813    ///
814    /// Returns an empty manifest if no snapshot has been taken yet.
815    ///
816    /// # Errors
817    ///
818    /// Returns [`ClawError::Config`] if no `snapshot_dir` is configured.
819    pub fn snapshot_manifest(&self) -> ClawResult<SnapshotManifest> {
820        let snap_dir = self
821            .config
822            .snapshot_dir
823            .as_ref()
824            .ok_or_else(|| ClawError::Config("snapshot_dir must be set".to_string()))?;
825        Snapshotter::new(snap_dir)?.load_manifest()
826    }
827
828    /// Rotate the SQLCipher encryption key using `PRAGMA rekey`.
829    ///
830    /// Only available with the `encryption` Cargo feature.
831    ///
832    /// # Errors
833    ///
834    /// Returns a [`ClawError`] if the PRAGMA execution fails.
835    #[cfg(feature = "encryption")]
836    pub async fn rotate_key(&self, _old_key: [u8; 32], new_key: [u8; 32]) -> ClawResult<()> {
837        let hex: String = new_key.iter().map(|b| format!("{b:02x}")).collect();
838        sqlx::query(&format!("PRAGMA rekey = \"x'{hex}'\""))
839            .execute(&self.pool)
840            .await?;
841        Ok(())
842    }
843
844    // ── Cache & Stats API ─────────────────────────────────────────────────────
845
846    /// Return a snapshot of the current in-memory cache statistics.
847    ///
848    /// # Example
849    ///
850    /// ```rust,no_run
851    /// # use claw_core::ClawEngine;
852    /// # async fn example() -> claw_core::ClawResult<()> {
853    /// # let engine = ClawEngine::open_default().await?;
854    /// let stats = engine.cache_stats().await;
855    /// println!("hits: {}, misses: {}", stats.hit_count, stats.miss_count);
856    /// # Ok(())
857    /// # }
858    /// ```
859    pub async fn cache_stats(&self) -> CacheStats {
860        self.stats.lock().await.clone()
861    }
862
863    /// Return comprehensive runtime statistics.
864    ///
865    /// Reports the total memory count, rolling cache hit rate (last 1000 ops),
866    /// last snapshot timestamp, and on-disk database/WAL sizes.
867    ///
868    /// # Errors
869    ///
870    /// Returns a [`ClawError`] if the memory count query fails.
871    ///
872    /// # Example
873    ///
874    /// ```rust,no_run
875    /// # use claw_core::ClawEngine;
876    /// # async fn example() -> claw_core::ClawResult<()> {
877    /// # let engine = ClawEngine::open_default().await?;
878    /// let s = engine.stats().await?;
879    /// println!("hit rate: {:.1}%", s.cache_hit_rate * 100.0);
880    /// # Ok(())
881    /// # }
882    /// ```
883    #[tracing::instrument(skip(self))]
884    pub async fn stats(&self) -> ClawResult<ClawStats> {
885        let (total_memories,): (i64,) = sqlx::query_as("SELECT COUNT(*) FROM memories")
886            .fetch_one(&self.pool)
887            .await?;
888        let cache_hit_rate = self.stats.lock().await.rolling_hit_rate();
889        let last_snapshot_at = *self.last_snapshot_at.lock().await;
890        let db_size_bytes = std::fs::metadata(&self.config.db_path)
891            .map(|m| m.len())
892            .unwrap_or(0);
893        let wal_path = {
894            let p = self.config.db_path.to_string_lossy();
895            std::path::PathBuf::from(format!("{p}-wal"))
896        };
897        let wal_size_bytes = std::fs::metadata(&wal_path).map(|m| m.len()).unwrap_or(0);
898        Ok(ClawStats {
899            total_memories: total_memories as u64,
900            cache_hit_rate,
901            last_snapshot_at,
902            db_size_bytes,
903            wal_size_bytes,
904        })
905    }
906
907    /// Return database-level statistics.
908    ///
909    /// # Errors
910    ///
911    /// Returns a [`ClawError`] if any of the count queries fail.
912    ///
913    /// # Example
914    ///
915    /// ```rust,no_run
916    /// # use claw_core::ClawEngine;
917    /// # async fn example() -> claw_core::ClawResult<()> {
918    /// # let engine = ClawEngine::open_default().await?;
919    /// let stats = engine.db_stats().await?;
920    /// println!("memories: {}", stats.memory_count);
921    /// # Ok(())
922    /// # }
923    /// ```
924    pub async fn db_stats(&self) -> ClawResult<DbStats> {
925        let (mc,): (i64,) = sqlx::query_as("SELECT COUNT(*) FROM memories")
926            .fetch_one(&self.pool)
927            .await?;
928        let (sc,): (i64,) = sqlx::query_as("SELECT COUNT(*) FROM sessions")
929            .fetch_one(&self.pool)
930            .await?;
931        let (tc,): (i64,) = sqlx::query_as("SELECT COUNT(*) FROM tool_output")
932            .fetch_one(&self.pool)
933            .await?;
934        Ok(DbStats {
935            memory_count: mc as u64,
936            session_count: sc as u64,
937            tool_output_count: tc as u64,
938        })
939    }
940
941    // ── Helpers ───────────────────────────────────────────────────────────────
942
943    fn validate_sqlite_magic(path: &Path) -> ClawResult<()> {
944        use std::io::Read;
945        const SQLITE_MAGIC: &[u8; 16] = b"SQLite format 3\0";
946        let mut header = [0u8; 16];
947        let mut file = std::fs::File::open(path)
948            .map_err(|e| ClawError::Snapshot(format!("cannot open snapshot: {e}")))?;
949        file.read_exact(&mut header)
950            .map_err(|e| ClawError::Snapshot(format!("cannot read snapshot header: {e}")))?;
951        if &header != SQLITE_MAGIC {
952            return Err(ClawError::Snapshot(
953                "file does not have a valid SQLite 3 header".to_string(),
954            ));
955        }
956        Ok(())
957    }
958}