Skip to main content

vtcode_memory/
manifest.rs

1use std::fs;
2use std::path::Path;
3
4use crate::SessionManifest;
5use crate::TurnIndex;
6use crate::error::SessionStoreError;
7
8/// Manifest persistence helpers.
9///
10/// Separated from `event_log` so the hot append path does not carry
11/// serialization concerns, and so `open` can cheaply probe the manifest
12/// before deciding whether to run the O(n) scan.
13pub struct ManifestStore {
14    session_dir: std::path::PathBuf,
15}
16
17impl ManifestStore {
18    /// Create a new manifest store for the given session directory.
19    pub(crate) fn new(session_dir: std::path::PathBuf) -> Self {
20        Self { session_dir }
21    }
22
23    /// Path to `manifest.json` inside the session directory.
24    fn manifest_path(&self) -> std::path::PathBuf {
25        self.session_dir.join("manifest.json")
26    }
27
28    /// Path to `index/turns.json` inside the session directory.
29    fn turns_path(&self) -> std::path::PathBuf {
30        self.session_dir.join("index").join("turns.json")
31    }
32
33    /// Load the manifest if it exists and is parseable.
34    ///
35    /// Returns `Ok(None)` when the file is missing (fresh session) or
36    /// malformed, rather than erroring — the caller can fall back to scanning
37    /// the event log.
38    pub(crate) fn load_manifest(&self) -> Result<Option<SessionManifest>, SessionStoreError> {
39        let path = self.manifest_path();
40        let Some(bytes) = read_optional_private_file(&path)? else {
41            return Ok(None);
42        };
43        Ok(serde_json::from_slice(&bytes).ok())
44    }
45
46    /// Load the turn index if it exists and is parseable.
47    ///
48    /// Returns `Ok(None)` when the file is missing or malformed.
49    pub(crate) fn load_turn_index(&self) -> Result<Option<TurnIndex>, SessionStoreError> {
50        let path = self.turns_path();
51        let Some(bytes) = read_optional_private_file(&path)? else {
52            return Ok(None);
53        };
54        Ok(serde_json::from_slice(&bytes).ok())
55    }
56    /// Atomically write the manifest. Parent directories must already exist.
57    pub(crate) fn write_manifest(&self, manifest: &SessionManifest) -> Result<(), SessionStoreError> {
58        let path = self.manifest_path();
59        let bytes = serde_json::to_vec(manifest)?;
60        vtcode_commons::VtCodePaths::write_private_file_atomic(&path, &bytes)
61            .map_err(|error| SessionStoreError::io(path, std::io::Error::other(error)))
62    }
63
64    /// Atomically write the turn index. Parent directories must already exist.
65    pub(crate) fn write_turn_index(&self, index: &TurnIndex) -> Result<(), SessionStoreError> {
66        let path = self.turns_path();
67        let bytes = serde_json::to_vec(index)?;
68        vtcode_commons::VtCodePaths::write_private_file_atomic(&path, &bytes)
69            .map_err(|error| SessionStoreError::io(path, std::io::Error::other(error)))
70    }
71}
72
73fn read_optional_private_file(path: &Path) -> Result<Option<Vec<u8>>, SessionStoreError> {
74    match fs::symlink_metadata(path) {
75        Ok(_) => vtcode_commons::VtCodePaths::read_file_no_follow(path)
76            .map(Some)
77            .map_err(|error| SessionStoreError::io(path.to_path_buf(), std::io::Error::other(error))),
78        Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(None),
79        Err(error) => Err(SessionStoreError::io(path.to_path_buf(), error)),
80    }
81}