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)]
14pub mod error;
32pub mod event_log;
33pub 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, apply_retention_preserving, gc_legacy};
49
50use std::path::{Path, PathBuf};
51
52const SESSIONS_DIR: &str = ".vtcode/sessions";
54
55const DERIVED_DIR: &str = "derived";
57
58const SESSION_STORE_SCHEMA_VERSION: u32 = 1;
60
61#[must_use]
63pub(crate) fn sessions_root(workspace: &Path) -> PathBuf {
64 workspace.join(SESSIONS_DIR)
65}
66
67#[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#[must_use]
79pub fn session_directory(workspace: &Path, session_id: &str) -> PathBuf {
80 session_dir(workspace, session_id)
81}
82
83pub fn open(workspace: &Path, session_id: &str, max_events: usize) -> Result<SessionEventLog, SessionStoreError> {
90 SessionEventLog::open(workspace, session_id, max_events)
91}
92
93fn sanitize_id(id: &str) -> String {
95 let mut out = String::with_capacity(id.len());
96 for c in id.chars() {
97 if c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == '.' {
98 out.push(c);
99 } else {
100 out.push('_');
101 }
102 }
103 let out = out.trim_start_matches('.').to_string();
105 if out.is_empty() { "session".to_string() } else { out }
106}
107
108#[cfg(test)]
109mod tests;