Skip to main content

vv_agent/memory/session/
storage.rs

1use std::path::{Path, PathBuf};
2
3use super::SessionMemory;
4
5const SESSION_MEMORY_FILENAME: &str = "session_memory.json";
6
7impl SessionMemory {
8    pub fn storage_path(&self) -> Option<PathBuf> {
9        let workspace = self.workspace.as_ref()?;
10        let workspace_root = workspace.canonicalize().ok()?;
11        let storage_dir = if self.config.storage_dir.is_absolute() {
12            self.config.storage_dir.clone()
13        } else {
14            workspace.join(&self.config.storage_dir)
15        };
16        let scoped_dir = match self
17            .storage_scope
18            .as_deref()
19            .filter(|scope| !scope.is_empty())
20        {
21            Some(scope) => storage_dir.join(sanitize_storage_scope(scope)?),
22            None => storage_dir,
23        };
24        let resolved = scoped_dir.join(SESSION_MEMORY_FILENAME);
25        let parent = resolved.parent()?;
26        let canonical_parent = if parent.exists() {
27            parent.canonicalize().ok()?
28        } else {
29            canonicalize_existing_ancestor(parent)?
30        };
31        if !canonical_parent.starts_with(&workspace_root) {
32            return None;
33        }
34        Some(resolved)
35    }
36}
37
38fn sanitize_storage_scope(raw: &str) -> Option<String> {
39    let safe = raw
40        .trim()
41        .chars()
42        .map(|ch| {
43            if ch.is_ascii_alphanumeric() || matches!(ch, '.' | '_' | '-') {
44                ch
45            } else {
46                '_'
47            }
48        })
49        .collect::<String>()
50        .trim_matches(['.', '_', '-'])
51        .to_string();
52    (!safe.is_empty()).then_some(safe)
53}
54
55fn canonicalize_existing_ancestor(path: &Path) -> Option<PathBuf> {
56    let mut current = path;
57    while !current.exists() {
58        current = current.parent()?;
59    }
60    current.canonicalize().ok()
61}