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::{
27 DEFAULT_MAX_EVENTS, SessionEventLog, SessionManifest, TurnIndex, TurnIndexEntry,
28};
29pub use migration::{MigrationReport, migrate_legacy};
30pub use progress::{
31 Milestone, MilestoneStatus, ProgressLedger, load_progress, progress_path, save_progress,
32};
33pub use query::{FactRecord, SessionSummary, query_facts, recent_sessions};
34pub use retention::{RetentionPolicy, apply_retention, gc_legacy};
35
36use std::path::{Path, PathBuf};
37
38/// Directory (relative to the workspace) holding all per-session stores.
39pub const SESSIONS_DIR: &str = ".vtcode/sessions";
40
41/// Sub-directory inside a session holding regenerated views.
42pub const DERIVED_DIR: &str = "derived";
43
44/// Schema version for the on-disk session store layout.
45pub const SESSION_STORE_SCHEMA_VERSION: u32 = 1;
46
47/// Resolve the sessions root directory for a workspace.
48#[must_use]
49pub fn sessions_root(workspace: &Path) -> PathBuf {
50 workspace.join(SESSIONS_DIR)
51}
52
53/// Resolve the directory for a single session.
54#[must_use]
55pub fn session_dir(workspace: &Path, session_id: &str) -> PathBuf {
56 sessions_root(workspace).join(sanitize_id(session_id))
57}
58
59/// Open (creating if necessary) the event log for a session.
60///
61/// This is the canonical entry point for recording a session's events. The
62/// returned [`SessionEventLog`] is cheap to clone (internally `Arc`-free but the
63/// file handle is shared via an internal mutex) and supports concurrent
64/// `append` calls from the runloop's event sink.
65pub fn open(
66 workspace: &Path,
67 session_id: &str,
68 max_events: usize,
69) -> Result<SessionEventLog, SessionStoreError> {
70 SessionEventLog::open(workspace, session_id, max_events)
71}
72
73/// Sanitize a session id so it is safe to use as a directory name.
74fn sanitize_id(id: &str) -> String {
75 let mut out = String::with_capacity(id.len());
76 for c in id.chars() {
77 if c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == '.' {
78 out.push(c);
79 } else {
80 out.push('_');
81 }
82 }
83 // Strip leading dots to avoid creating hidden directories.
84 let out = out.trim_start_matches('.').to_string();
85 if out.is_empty() {
86 "session".to_string()
87 } else {
88 out
89 }
90}
91
92#[cfg(test)]
93mod tests;