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 pack;
38pub mod progress;
39pub mod query;
40pub mod retention;
41
42pub use error::SessionStoreError;
43pub use event_log::{
44 DEFAULT_MAX_EVENTS, EvictionSummaryHook, SessionEventLog, SessionManifest, TurnIndex, TurnIndexEntry,
45};
46pub use migration::{MigrationReport, migrate_legacy};
47pub use pack::{
48 AUDIT_PACK_SCHEMA_VERSION, AuditPackEntry, AuditVerification, SessionAuditPack, audit_pack_path, create_audit_pack,
49 read_audit_pack, verify_audit_pack, write_audit_pack,
50};
51pub use progress::{
52 GoalClassifierVerdict, GoalEvent, GoalHistoryEntry, GoalOrchestration, GoalPauseReason, GoalPhase, GoalStatus,
53 GoalTracker, Milestone, MilestoneStatus, ProgressLedger, load_progress, progress_path, save_progress,
54};
55pub use query::{
56 FactRecord, MemorySearchResult, SessionMemoryView, SessionSummary, query_facts, recent_sessions, search_memory,
57 session_memory_facts, write_session_memory_view,
58};
59pub use retention::{
60 RETENTION_PIN_FILE, RetentionPolicy, apply_retention, apply_retention_preserving, gc_legacy, pin_session_retention,
61 session_retention_pinned, unpin_session_retention,
62};
63
64use std::path::{Path, PathBuf};
65
66const SESSIONS_DIR: &str = ".vtcode/sessions";
68
69const DERIVED_DIR: &str = "derived";
71
72const SESSION_STORE_SCHEMA_VERSION: u32 = 1;
74
75#[must_use]
77pub(crate) fn sessions_root(workspace: &Path) -> PathBuf {
78 workspace.join(SESSIONS_DIR)
79}
80
81#[must_use]
83pub(crate) fn session_dir(workspace: &Path, session_id: &str) -> PathBuf {
84 sessions_root(workspace).join(sanitize_id(session_id))
85}
86
87#[must_use]
93pub fn session_directory(workspace: &Path, session_id: &str) -> PathBuf {
94 session_dir(workspace, session_id)
95}
96
97pub fn open(workspace: &Path, session_id: &str, max_events: usize) -> Result<SessionEventLog, SessionStoreError> {
104 SessionEventLog::open(workspace, session_id, max_events)
105}
106
107pub fn open_with_eviction_summary(
110 workspace: &Path,
111 session_id: &str,
112 max_events: usize,
113 eviction_summary_hook: EvictionSummaryHook,
114) -> Result<SessionEventLog, SessionStoreError> {
115 SessionEventLog::open_with_eviction_summary(workspace, session_id, max_events, eviction_summary_hook)
116}
117
118pub(crate) fn ensure_private_directory(path: &Path) -> Result<(), SessionStoreError> {
120 vtcode_commons::VtCodePaths::ensure_user_dir(path).map_err(|error| SessionStoreError::CreateDir {
121 path: path.to_path_buf(),
122 source: std::io::Error::other(error),
123 })?;
124
125 #[cfg(unix)]
126 {
127 use std::os::unix::fs::PermissionsExt;
128
129 std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o700))
130 .map_err(|error| SessionStoreError::io(path.to_path_buf(), error))?;
131 }
132
133 Ok(())
134}
135
136fn sanitize_id(id: &str) -> String {
138 let mut out = String::with_capacity(id.len());
139 for c in id.chars() {
140 if c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == '.' {
141 out.push(c);
142 } else {
143 out.push('_');
144 }
145 }
146 let out = out.trim_start_matches('.').to_string();
148 if out.is_empty() { "session".to_string() } else { out }
149}
150
151#[cfg(test)]
152mod tests;