Skip to main content

vtcode_memory/
lib.rs

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