Skip to main content

vtcode_session_store/
migration.rs

1//! One-off migration of the legacy, overlapping history stores into the
2//! unified per-session store.
3//!
4//! Legacy inputs:
5//! - `.vtcode/history/session-*.memory.json` → `<session>/derived/memory.json`
6//! - `.vtcode/logs/trajectory-*.jsonl`  → `<session>/derived/trajectory.jsonl`
7//!
8//! Legacy `checkpoints/` are intentionally *not* migrated here: they require a
9//! lossy `Message` → `ThreadEvent` mapping and `/revert` must be rewired first.
10
11use std::path::Path;
12
13use chrono::Utc;
14
15use crate::error::SessionStoreError;
16use crate::{SessionManifest, session_dir};
17
18/// Outcome of a legacy migration run.
19#[derive(Debug, Clone, Default, PartialEq, Eq)]
20pub struct MigrationReport {
21    /// Number of session directories created.
22    pub sessions_created: usize,
23    /// Number of session memory envelopes imported.
24    pub memory_imported: usize,
25    /// Number of trajectory logs imported.
26    pub trajectory_imported: usize,
27    /// Total bytes copied into the unified store.
28    pub bytes_migrated: u64,
29}
30
31/// Migrate legacy history/trajectory stores into the unified session store.
32///
33/// When `remove_legacy` is true, the now-imported `history/` and `logs/`
34/// directories are deleted (the `checkpoints/` directory is preserved).
35pub fn migrate_legacy(
36    workspace: &Path,
37    remove_legacy: bool,
38) -> Result<MigrationReport, SessionStoreError> {
39    let mut report = MigrationReport::default();
40    let vt = workspace.join(".vtcode");
41
42    let history_dir = vt.join("history");
43    if history_dir.is_dir() {
44        for entry in std::fs::read_dir(&history_dir)
45            .map_err(|e| SessionStoreError::io(history_dir.clone(), e))?
46            .filter_map(Result::ok)
47        {
48            let path = entry.path();
49            if path.extension().and_then(|e| e.to_str()) != Some("json") {
50                continue;
51            }
52            let Some(name) = path.file_stem().and_then(|s| s.to_str()) else {
53                continue;
54            };
55            let session_id = name.to_string();
56            let bytes = std::fs::read(&path).map_err(|e| SessionStoreError::io(path.clone(), e))?;
57            let dir = session_dir(workspace, &session_id);
58            std::fs::create_dir_all(dir.join(crate::DERIVED_DIR)).map_err(|e| {
59                SessionStoreError::CreateDir {
60                    path: dir.clone(),
61                    source: e,
62                }
63            })?;
64            let dest = dir.join(crate::DERIVED_DIR).join("memory.json");
65            std::fs::write(&dest, &bytes).map_err(|e| SessionStoreError::io(dest, e))?;
66            write_manifest(&dir, &session_id, &path, "completed")?;
67            report.sessions_created += 1;
68            report.memory_imported += 1;
69            report.bytes_migrated += bytes.len() as u64;
70        }
71    }
72
73    let logs_dir = vt.join("logs");
74    if logs_dir.is_dir() {
75        for entry in std::fs::read_dir(&logs_dir)
76            .map_err(|e| SessionStoreError::io(logs_dir.clone(), e))?
77            .filter_map(Result::ok)
78        {
79            let path = entry.path();
80            if path.extension().and_then(|e| e.to_str()) != Some("jsonl") {
81                continue;
82            }
83            let Some(name) = path.file_stem().and_then(|s| s.to_str()) else {
84                continue;
85            };
86            // `trajectory-<ts>` → session id `traj-<ts>` to avoid colliding with
87            // history session ids.
88            let session_id = format!("traj-{}", name.trim_start_matches("trajectory-"));
89            let bytes = std::fs::read(&path).map_err(|e| SessionStoreError::io(path.clone(), e))?;
90            let dir = session_dir(workspace, &session_id);
91            std::fs::create_dir_all(dir.join(crate::DERIVED_DIR)).map_err(|e| {
92                SessionStoreError::CreateDir {
93                    path: dir.clone(),
94                    source: e,
95                }
96            })?;
97            let dest = dir.join(crate::DERIVED_DIR).join("trajectory.jsonl");
98            std::fs::write(&dest, &bytes).map_err(|e| SessionStoreError::io(dest, e))?;
99            write_manifest(&dir, &session_id, &path, "completed")?;
100            report.sessions_created += 1;
101            report.trajectory_imported += 1;
102            report.bytes_migrated += bytes.len() as u64;
103        }
104    }
105
106    if remove_legacy {
107        let freed = crate::retention::gc_legacy(workspace)?;
108        let _ = freed;
109    }
110
111    Ok(report)
112}
113
114fn write_manifest(
115    dir: &Path,
116    session_id: &str,
117    source: &Path,
118    status: &str,
119) -> Result<(), SessionStoreError> {
120    let ts = source
121        .metadata()
122        .ok()
123        .and_then(|m| m.modified().ok())
124        .map(|t| chrono::DateTime::<Utc>::from(t).to_rfc3339())
125        .unwrap_or_else(|| Utc::now().to_rfc3339());
126    let mut manifest = SessionManifest::new(session_id);
127    manifest.created_at = ts.clone();
128    manifest.updated_at = ts;
129    manifest.status = status.to_string();
130    let path = dir.join("manifest.json");
131    let bytes = serde_json::to_string_pretty(&manifest)?;
132    std::fs::write(&path, bytes).map_err(|e| SessionStoreError::io(path, e))?;
133    Ok(())
134}