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 explanation;
34pub mod mailbox;
36pub mod manifest;
38pub mod matrix;
39pub mod migration;
40pub mod pack;
42pub mod progress;
43pub mod query;
44pub mod retention;
45pub mod snapshot;
46
47pub use error::SessionStoreError;
48pub use event_log::{
49 DEFAULT_MAX_EVENTS, EvictionSummaryHook, SessionEventLog, SessionManifest, TurnIndex, TurnIndexEntry,
50};
51pub use mailbox::{
52 DEFAULT_TTL_SECS, InboxDrain, MAILBOX_DIR, MAILBOX_SCHEMA_VERSION, MAX_INBOX_DEPTH, MAX_MAIL_BYTES,
53 MAX_OPTION_DESCRIPTION_CHARS, MAX_OPTION_LABEL_CHARS, MAX_OPTIONS, MAX_QUESTION_CHARS, MAX_QUESTION_ID_CHARS,
54 MAX_QUESTIONS, MAX_TEXT_CHARS, MAX_TTL_SECS, MIN_TTL_SECS, MailKind, MailMessage, MailOption, MailQuestion,
55 MailReceipt, NewMail, ack_mail, drain_inbox, find_mail, read_mail, send_mail,
56};
57pub use migration::{MigrationReport, migrate_legacy};
58pub use pack::{
59 AUDIT_PACK_SCHEMA_VERSION, AuditPackEntry, AuditVerification, SessionAuditPack, audit_pack_path, create_audit_pack,
60 read_audit_pack, verify_audit_pack, write_audit_pack,
61};
62pub use progress::{
63 GoalClassifierVerdict, GoalEvent, GoalHistoryEntry, GoalOrchestration, GoalPauseReason, GoalPhase, GoalStatus,
64 GoalTracker, Milestone, MilestoneStatus, ProgressLedger, load_progress, progress_path, save_progress,
65};
66pub use query::{
67 FactRecord, MemorySearchResult, SessionMemoryView, SessionSummary, live_sessions, query_facts, recent_sessions,
68 search_memory, session_memory_facts, write_session_memory_view,
69};
70pub use retention::{
71 RETENTION_PIN_FILE, RetentionPolicy, apply_retention, apply_retention_preserving, evict_zero_turn_completed_store,
72 gc_legacy, mark_abandoned_active_sessions, pin_session_retention, retention_pinned_session_ids,
73 session_retention_pinned, unpin_session_retention,
74};
75
76use std::path::{Path, PathBuf};
77
78const SESSIONS_DIR: &str = ".vtcode/sessions";
80
81const DERIVED_DIR: &str = "derived";
83
84const SESSION_STORE_SCHEMA_VERSION: u32 = 1;
86
87#[must_use]
89pub(crate) fn sessions_root(workspace: &Path) -> PathBuf {
90 workspace.join(SESSIONS_DIR)
91}
92
93#[must_use]
95pub(crate) fn session_dir(workspace: &Path, session_id: &str) -> PathBuf {
96 sessions_root(workspace).join(sanitize_id(session_id))
97}
98
99#[must_use]
105pub fn session_directory(workspace: &Path, session_id: &str) -> PathBuf {
106 session_dir(workspace, session_id)
107}
108
109pub fn open(workspace: &Path, session_id: &str, max_events: usize) -> Result<SessionEventLog, SessionStoreError> {
116 SessionEventLog::open(workspace, session_id, max_events)
117}
118
119pub fn open_exclusive(
126 workspace: &Path,
127 session_id: &str,
128 max_events: usize,
129) -> Result<SessionEventLog, SessionStoreError> {
130 SessionEventLog::open_exclusive(workspace, session_id, max_events)
131}
132
133pub fn open_with_eviction_summary(
136 workspace: &Path,
137 session_id: &str,
138 max_events: usize,
139 eviction_summary_hook: EvictionSummaryHook,
140) -> Result<SessionEventLog, SessionStoreError> {
141 SessionEventLog::open_with_eviction_summary(workspace, session_id, max_events, eviction_summary_hook)
142}
143
144pub(crate) fn ensure_private_directory(path: &Path) -> Result<(), SessionStoreError> {
146 vtcode_commons::VtCodePaths::ensure_user_dir(path).map_err(|error| SessionStoreError::CreateDir {
147 path: path.to_path_buf(),
148 source: std::io::Error::other(error),
149 })?;
150
151 #[cfg(unix)]
152 {
153 use std::os::unix::fs::PermissionsExt;
154
155 std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o700))
156 .map_err(|error| SessionStoreError::io(path.to_path_buf(), error))?;
157 }
158
159 Ok(())
160}
161
162fn sanitize_id(id: &str) -> String {
164 let mut out = String::with_capacity(id.len());
165 for c in id.chars() {
166 if c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == '.' {
167 out.push(c);
168 } else {
169 out.push('_');
170 }
171 }
172 let out = out.trim_start_matches('.').to_string();
174 if out.is_empty() { "session".to_string() } else { out }
175}
176
177#[cfg(test)]
178mod tests;