Skip to main content

vtcode_memory/
lib.rs

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