vtcode_session_store/lib.rs
1//! Unified per-session state store for VT Code.
2//!
3//! This crate is the single source of truth for an agent session's state,
4//! context, and history. Each session is persisted under
5//! `.vtcode/sessions/<session_id>/` as:
6//!
7//! - `events.jsonl` — the canonical append-only [`ThreadEvent`](vtcode_exec_events::ThreadEvent)
8//! log (schema-versioned). Everything else is derived from this.
9//! - `manifest.json` — session metadata and counters.
10//! - `index/turns.json` — byte-offset index enabling O(1) turn reconstruction.
11//! - `derived/` — regenerated views (`trajectory.jsonl`, `memory.json`, …).
12//!
13//! The store is intentionally append-only and off the agent's hot path: the
14//! live conversation stays in memory and is never reloaded from disk into
15//! context. Reads happen only for revert, compaction, analytics, and
16//! long-term-learning queries.
17
18pub mod error;
19pub mod event_log;
20pub mod migration;
21pub mod progress;
22pub mod query;
23pub mod retention;
24
25pub use error::SessionStoreError;
26pub use event_log::{SessionEventLog, SessionManifest, TurnIndex, TurnIndexEntry};
27pub use migration::{MigrationReport, migrate_legacy};
28pub use progress::{
29 Milestone, MilestoneStatus, ProgressLedger, load_progress, progress_path, save_progress,
30};
31pub use query::{FactRecord, SessionSummary, query_facts, recent_sessions};
32pub use retention::{RetentionPolicy, apply_retention, gc_legacy};
33
34use std::path::{Path, PathBuf};
35
36/// Directory (relative to the workspace) holding all per-session stores.
37pub const SESSIONS_DIR: &str = ".vtcode/sessions";
38
39/// Sub-directory inside a session holding regenerated views.
40pub const DERIVED_DIR: &str = "derived";
41
42/// Schema version for the on-disk session store layout.
43pub const SESSION_STORE_SCHEMA_VERSION: u32 = 1;
44
45/// Resolve the sessions root directory for a workspace.
46#[must_use]
47pub fn sessions_root(workspace: &Path) -> PathBuf {
48 workspace.join(SESSIONS_DIR)
49}
50
51/// Resolve the directory for a single session.
52#[must_use]
53pub fn session_dir(workspace: &Path, session_id: &str) -> PathBuf {
54 sessions_root(workspace).join(sanitize_id(session_id))
55}
56
57/// Open (creating if necessary) the event log for a session.
58///
59/// This is the canonical entry point for recording a session's events. The
60/// returned [`SessionEventLog`] is cheap to clone (internally `Arc`-free but the
61/// file handle is shared via an internal mutex) and supports concurrent
62/// `append` calls from the runloop's event sink.
63pub fn open(workspace: &Path, session_id: &str) -> Result<SessionEventLog, SessionStoreError> {
64 SessionEventLog::open(workspace, session_id)
65}
66
67/// Sanitize a session id so it is safe to use as a directory name.
68fn sanitize_id(id: &str) -> String {
69 let mut out = String::with_capacity(id.len());
70 for c in id.chars() {
71 if c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == '.' {
72 out.push(c);
73 } else {
74 out.push('_');
75 }
76 }
77 // Strip leading dots to avoid creating hidden directories.
78 let out = out.trim_start_matches('.').to_string();
79 if out.is_empty() {
80 "session".to_string()
81 } else {
82 out
83 }
84}
85
86#[cfg(test)]
87mod tests;