forge_workspace/store.rs
1//! Machine-local embedded database (redb).
2//!
3//! One redb file per config dir at
4//! `<app-support>/stores/<stable-key>.redb` holds that config dir's durable
5//! per-machine state. The key is a SHA-256 of the canonicalised config-dir
6//! path, so a Rust release cannot move it; the single-instance lock keeps its
7//! own digest and is unaffected. redb takes an exclusive file lock on open,
8//! so a second opener on the same path fails here rather than corrupting.
9//!
10//! This wrapper is deliberately general - open plus the raw handle.
11//! Table logic lives per-tenant in the submodules: biometric enrolments
12//! ([`biometric`]), Gotify subscriptions ([`gotify`]), Slack subscriptions
13//! ([`slack`]), durable crons ([`cron`]) and the schedules their fired
14//! prompts came from ([`cron_names`]), session identities ([`sessions`]),
15//! review threads ([`review`]), forge state
16//! ([`state`], the account-usage cache), the
17//! usage scan's per-file token summaries ([`token_usage`]), cached
18//! model pricing ([`pricing`]), plugin update history ([`plugins`]),
19//! and the catalog's per-file worker-tag scans ([`session_tags`]).
20//!
21//! [`dynamic_workers`] is the retired worker table: nothing writes it,
22//! and it holds only until the boot sweep has moved every row onto
23//! [`sessions`].
24//!
25//! [`model_catalog`] is retired rather than live: it exists only to drop
26//! the table the deleted catalog machinery left behind in older stores.
27//!
28//! [`settings`] is the same, for the table the `/spinner` override lived in.
29
30use std::path::Path;
31
32use anyhow::Context;
33
34pub mod bench_results;
35pub mod biometric;
36pub mod cron;
37pub mod cron_names;
38pub mod dictate_models;
39pub mod dynamic_workers;
40pub mod gotify;
41pub mod handoffs;
42pub mod model_catalog;
43pub mod plugins;
44pub mod pricing;
45pub mod review;
46pub mod session_tags;
47pub mod sessions;
48pub mod settings;
49pub mod slack;
50pub mod speaking_models;
51pub mod speaking_results;
52pub mod state;
53pub mod task_archive;
54pub mod task_chases;
55pub mod task_history;
56pub mod tasks;
57pub mod token_usage;
58
59/// This config dir's store file: `<app_support>/stores/<stable-key>.redb`.
60///
61/// The key is stable across Rust releases; the single-instance lock keeps its
62/// own [`forge_sdk::config_dir_hash`], so a new binary never misses a running
63/// old process's lock.
64pub fn path_for(app_support: &Path, config_dir: &Path) -> std::path::PathBuf {
65 app_support.join("stores").join(format!("{}.redb", forge_sdk::config_dir_store_key(config_dir)))
66}
67
68/// Handle to the machine-local redb database.
69pub struct Db {
70 inner: redb::Database,
71}
72
73impl Db {
74 /// Open (or create) the database at `path`. Fails if redb cannot
75 /// take its exclusive file lock (e.g. a second config dir on the
76 /// same machine already holds it).
77 pub fn open(path: &Path) -> anyhow::Result<Self> {
78 let inner = redb::Database::create(path)
79 .with_context(|| format!("open redb database at {}", path.display()))?;
80 Ok(Self { inner })
81 }
82
83 pub(crate) fn database(&self) -> &redb::Database {
84 &self.inner
85 }
86
87 /// Compact the file in place, reclaiming what a dropped table left
88 /// behind. Returns whether it did anything. redb takes `&mut self`,
89 /// so the caller needs the handle owned - the boot compacts before it
90 /// is wrapped for the rest of the process.
91 pub fn compact(&mut self) -> anyhow::Result<bool> {
92 self.inner.compact().context("compact redb database")
93 }
94}