Skip to main content

vtcode_memory/
manifest.rs

1use std::fs;
2use std::path::Path;
3
4use serde::{Deserialize, Serialize};
5
6use crate::SessionManifest;
7use crate::TurnIndex;
8use crate::error::SessionStoreError;
9
10/// Durable intent record for an event-file cap rewrite.
11///
12/// The event file is published before the derived manifest and turn index. If
13/// the process stops in that interval, this marker preserves the retained turn
14/// ordinal even when the stale index cannot be trusted during recovery.
15#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
16pub(crate) struct PendingCapRewrite {
17    pub(crate) previous_file_len: u64,
18    pub(crate) new_file_len: u64,
19    pub(crate) retained_turn_base: u64,
20}
21
22/// Manifest persistence helpers.
23///
24/// Separated from `event_log` so the hot append path does not carry
25/// serialization concerns, and so `open` can cheaply probe the manifest
26/// before deciding whether to run the O(n) scan.
27pub struct ManifestStore {
28    session_dir: std::path::PathBuf,
29}
30
31impl ManifestStore {
32    /// Create a new manifest store for the given session directory.
33    pub(crate) fn new(session_dir: std::path::PathBuf) -> Self {
34        Self { session_dir }
35    }
36
37    /// Path to `manifest.json` inside the session directory.
38    fn manifest_path(&self) -> std::path::PathBuf {
39        self.session_dir.join("manifest.json")
40    }
41
42    /// Path to `index/turns.json` inside the session directory.
43    fn turns_path(&self) -> std::path::PathBuf {
44        self.session_dir.join("index").join("turns.json")
45    }
46
47    /// Path to the cap-rewrite intent marker.
48    fn pending_cap_rewrite_path(&self) -> std::path::PathBuf {
49        self.session_dir.join("index").join("pending-cap-rewrite.json")
50    }
51
52    /// Load the manifest if it exists and is parseable.
53    ///
54    /// Returns `Ok(None)` when the file is missing (fresh session) or
55    /// malformed, rather than erroring — the caller can fall back to scanning
56    /// the event log.
57    pub(crate) fn load_manifest(&self) -> Result<Option<SessionManifest>, SessionStoreError> {
58        let path = self.manifest_path();
59        let Some(bytes) = read_optional_private_file(&path)? else {
60            return Ok(None);
61        };
62        Ok(serde_json::from_slice(&bytes).ok())
63    }
64
65    /// Load the turn index if it exists and is parseable.
66    ///
67    /// Returns `Ok(None)` when the file is missing or malformed.
68    pub(crate) fn load_turn_index(&self) -> Result<Option<TurnIndex>, SessionStoreError> {
69        let path = self.turns_path();
70        let Some(bytes) = read_optional_private_file(&path)? else {
71            return Ok(None);
72        };
73        Ok(serde_json::from_slice(&bytes).ok())
74    }
75
76    /// Load a pending cap-rewrite marker when it is present and valid.
77    pub(crate) fn load_pending_cap_rewrite(&self) -> Result<Option<PendingCapRewrite>, SessionStoreError> {
78        let path = self.pending_cap_rewrite_path();
79        let Some(bytes) = read_optional_private_file(&path)? else {
80            return Ok(None);
81        };
82        Ok(serde_json::from_slice(&bytes).ok())
83    }
84
85    /// Persist a cap-rewrite marker before replacing the canonical event file.
86    pub(crate) fn write_pending_cap_rewrite(&self, pending: &PendingCapRewrite) -> Result<(), SessionStoreError> {
87        let path = self.pending_cap_rewrite_path();
88        let bytes = serde_json::to_vec(pending)?;
89        vtcode_commons::VtCodePaths::write_private_file_atomic(&path, &bytes)
90            .map_err(|error| SessionStoreError::io(path, std::io::Error::other(error)))
91    }
92
93    /// Remove a completed cap-rewrite marker.
94    pub(crate) fn clear_pending_cap_rewrite(&self) -> Result<(), SessionStoreError> {
95        let path = self.pending_cap_rewrite_path();
96        match fs::remove_file(&path) {
97            Ok(()) => Ok(()),
98            Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
99            Err(error) => Err(SessionStoreError::io(path, error)),
100        }
101    }
102    /// Atomically write the manifest. Parent directories must already exist.
103    pub(crate) fn write_manifest(&self, manifest: &SessionManifest) -> Result<(), SessionStoreError> {
104        let path = self.manifest_path();
105        let bytes = serde_json::to_vec(manifest)?;
106        vtcode_commons::VtCodePaths::write_private_file_atomic(&path, &bytes)
107            .map_err(|error| SessionStoreError::io(path.clone(), std::io::Error::other(error)))?;
108        crate::query::invalidate_manifest_cache(&path);
109        Ok(())
110    }
111
112    /// Atomically write the turn index. Parent directories must already exist.
113    pub(crate) fn write_turn_index(&self, index: &TurnIndex) -> Result<(), SessionStoreError> {
114        let path = self.turns_path();
115        let bytes = serde_json::to_vec(index)?;
116        vtcode_commons::VtCodePaths::write_private_file_atomic(&path, &bytes)
117            .map_err(|error| SessionStoreError::io(path, std::io::Error::other(error)))
118    }
119}
120
121fn read_optional_private_file(path: &Path) -> Result<Option<Vec<u8>>, SessionStoreError> {
122    match fs::symlink_metadata(path) {
123        Ok(_) => vtcode_commons::VtCodePaths::read_file_no_follow(path)
124            .map(Some)
125            .map_err(|error| SessionStoreError::io(path.to_path_buf(), std::io::Error::other(error))),
126        Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(None),
127        Err(error) => Err(SessionStoreError::io(path.to_path_buf(), error)),
128    }
129}