#[allow(unused_imports)]
use crate::hash::fnv64;
use crate::sync_util::LockExt;
use indexmap::IndexMap;
use std::path::{Path, PathBuf};
use std::sync::{Arc, LazyLock, Mutex};
const MAX_SNAPSHOT_BYTES: u64 = 8 * 1024 * 1024;
const MAX_TURNS: usize = 200;
#[derive(Clone)]
enum Capture {
Absent,
Content(Arc<Vec<u8>>),
}
struct TurnBucket {
turn_id: String,
captures: IndexMap<PathBuf, Capture>,
}
struct Store {
turns: Vec<TurnBucket>,
pool: std::collections::HashMap<u64, Arc<Vec<u8>>>,
}
static STORE: LazyLock<Mutex<Store>> = LazyLock::new(|| {
Mutex::new(Store {
turns: Vec::new(),
pool: std::collections::HashMap::new(),
})
});
pub fn begin_turn(turn_id: &str) {
let mut s = STORE.lock_ignore_poison();
s.turns.push(TurnBucket {
turn_id: turn_id.to_string(),
captures: IndexMap::new(),
});
while s.turns.len() > MAX_TURNS {
s.turns.remove(0);
}
}
fn intern(store: &mut Store, bytes: Vec<u8>) -> Arc<Vec<u8>> {
let key = fnv64(&bytes);
if let Some(existing) = store.pool.get(&key) {
if **existing == bytes {
return existing.clone();
}
return Arc::new(bytes);
}
let arc = Arc::new(bytes);
store.pool.insert(key, arc.clone());
arc
}
pub fn capture(path: &Path) {
let canonical = std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf());
let capture = match std::fs::metadata(&canonical) {
Ok(meta) if meta.is_file() => {
if meta.len() > MAX_SNAPSHOT_BYTES {
return; }
match std::fs::read(&canonical) {
Ok(bytes) => Some(bytes),
Err(_) => return, }
}
_ => None,
};
let mut s = STORE.lock_ignore_poison();
if s.turns.is_empty() {
s.turns.push(TurnBucket {
turn_id: String::new(),
captures: IndexMap::new(),
});
}
let entry = match capture {
Some(bytes) => Capture::Content(intern(&mut s, bytes)),
None => Capture::Absent,
};
let last = s.turns.last_mut().expect("just ensured non-empty");
if !last.captures.contains_key(&canonical) {
last.captures.insert(canonical, entry);
}
}
pub fn capture_bytes(path: &Path, content: &[u8]) {
let canonical = std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf());
if content.len() as u64 > MAX_SNAPSHOT_BYTES {
return;
}
let mut s = STORE.lock_ignore_poison();
if s.turns.is_empty() {
s.turns.push(TurnBucket {
turn_id: String::new(),
captures: IndexMap::new(),
});
}
let interned = intern(&mut s, content.to_vec());
let last = s.turns.last_mut().expect("just ensured non-empty");
if !last.captures.contains_key(&canonical) {
last.captures.insert(canonical, Capture::Content(interned));
}
}
pub fn restore_from(turn_id: &str) -> Vec<PathBuf> {
let mut s = STORE.lock_ignore_poison();
let idx = match s.turns.iter().position(|t| t.turn_id == turn_id) {
Some(i) => i,
None => return Vec::new(),
};
let mut targets: IndexMap<PathBuf, Capture> = IndexMap::new();
for bucket in &s.turns[idx..] {
for (path, cap) in &bucket.captures {
targets.entry(path.clone()).or_insert_with(|| cap.clone());
}
}
let mut restored = Vec::new();
for (path, cap) in &targets {
let ok = match cap {
Capture::Content(bytes) => std::fs::write(path, bytes.as_slice()).is_ok(),
Capture::Absent => match std::fs::remove_file(path) {
Ok(_) => true,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => true,
Err(_) => false,
},
};
if ok {
restored.push(path.clone());
}
}
s.turns.truncate(idx);
restored
}
pub fn clear() {
let mut s = STORE.lock_ignore_poison();
s.turns.clear();
s.pool.clear();
}
#[cfg(test)]
pub(crate) static TEST_GATE: Mutex<()> = Mutex::new(());
#[cfg(test)]
mod tests {
use super::*;
fn isolated<R>(f: impl FnOnce(&Path) -> R) -> R {
let _g = TEST_GATE.lock_ignore_poison();
clear();
let dir = std::env::temp_dir().join(format!("dirge-snap-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let r = f(&dir);
clear();
let _ = std::fs::remove_dir_all(&dir);
r
}
#[test]
fn restore_reverts_edit_to_pre_state() {
isolated(|dir| {
let p = dir.join("a.txt");
std::fs::write(&p, "original").unwrap();
begin_turn("u1");
capture(&p); std::fs::write(&p, "mutated").unwrap();
let restored = restore_from("u1");
assert_eq!(restored.len(), 1);
assert_eq!(std::fs::read_to_string(&p).unwrap(), "original");
});
}
#[test]
fn earliest_pre_state_within_turn_wins() {
isolated(|dir| {
let p = dir.join("a.txt");
std::fs::write(&p, "v0").unwrap();
begin_turn("u1");
capture(&p); std::fs::write(&p, "v1").unwrap();
capture(&p); std::fs::write(&p, "v2").unwrap();
restore_from("u1");
assert_eq!(std::fs::read_to_string(&p).unwrap(), "v0");
});
}
#[test]
fn restore_spans_multiple_turns_taking_earliest() {
isolated(|dir| {
let p = dir.join("a.txt");
std::fs::write(&p, "t1pre").unwrap();
begin_turn("u1");
capture(&p);
std::fs::write(&p, "after-t1").unwrap();
begin_turn("u2");
capture(&p); std::fs::write(&p, "after-t2").unwrap();
restore_from("u1");
assert_eq!(std::fs::read_to_string(&p).unwrap(), "t1pre");
});
}
#[test]
fn newly_created_file_is_deleted_on_restore() {
isolated(|dir| {
let p = dir.join("new.txt");
begin_turn("u1");
capture(&p); std::fs::write(&p, "created this turn").unwrap();
let restored = restore_from("u1");
assert_eq!(restored.len(), 1);
assert!(!p.exists(), "file created in the turn must be removed");
});
}
#[test]
fn rewinding_to_unknown_turn_restores_nothing() {
isolated(|dir| {
let p = dir.join("a.txt");
std::fs::write(&p, "x").unwrap();
begin_turn("u1");
capture(&p);
std::fs::write(&p, "y").unwrap();
let restored = restore_from("nope");
assert!(restored.is_empty());
assert_eq!(std::fs::read_to_string(&p).unwrap(), "y");
});
}
#[test]
fn restore_truncates_rewound_turns() {
isolated(|dir| {
let p = dir.join("a.txt");
std::fs::write(&p, "v0").unwrap();
begin_turn("u1");
capture(&p);
std::fs::write(&p, "v1").unwrap();
restore_from("u1");
std::fs::write(&p, "v2").unwrap();
let restored = restore_from("u1");
assert!(restored.is_empty());
assert_eq!(std::fs::read_to_string(&p).unwrap(), "v2");
});
}
#[test]
fn capture_bytes_records_pre_state_without_reading_disk() {
isolated(|dir| {
let p = dir.join("a.txt");
std::fs::write(&p, "disk").unwrap();
begin_turn("u1");
capture_bytes(&p, b"inhand");
std::fs::write(&p, "mutated").unwrap();
restore_from("u1");
assert_eq!(std::fs::read_to_string(&p).unwrap(), "inhand");
});
}
#[test]
fn subagent_edits_fold_into_parent_turn_and_are_parent_rewindable() {
isolated(|dir| {
let parent_file = dir.join("parent.txt");
let sub_file = dir.join("sub.txt");
std::fs::write(&parent_file, "p0").unwrap();
std::fs::write(&sub_file, "s0").unwrap();
begin_turn("u1");
capture(&parent_file);
std::fs::write(&parent_file, "p1").unwrap();
capture(&sub_file);
std::fs::write(&sub_file, "s1").unwrap();
let restored = restore_from("u1");
assert_eq!(restored.len(), 2, "both parent and subagent edits restore");
assert_eq!(std::fs::read_to_string(&parent_file).unwrap(), "p0");
assert_eq!(std::fs::read_to_string(&sub_file).unwrap(), "s0");
});
}
#[test]
fn dedup_pool_reuses_identical_content() {
isolated(|dir| {
let a = dir.join("a.txt");
let b = dir.join("b.txt");
std::fs::write(&a, "same").unwrap();
std::fs::write(&b, "same").unwrap();
begin_turn("u1");
capture(&a);
capture(&b);
let s = STORE.lock_ignore_poison();
assert_eq!(
s.pool.len(),
1,
"identical content must dedup to one object"
);
});
}
}