Skip to main content

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