Skip to content

Commit 204013e

Browse files
committed
feat(session): prune session files past the retention window at startup
Wires the existing cleanup_period_days config to a real implementation: SessionHistory::cleanup_older_than removes session files (.json / .jsonl and quarantined .corrupt-*) whose mtime is older than the configured number of days, bounding unbounded growth of the sessions directory. Run best-effort at CLI startup, skipped when persistence is disabled or the period is unset/zero.
1 parent eb7c795 commit 204013e

2 files changed

Lines changed: 79 additions & 0 deletions

File tree

crates/cli/src/agent.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,22 @@ pub async fn run(cli: &Cli, resume_session_id: Option<String>) -> anyhow::Result
372372
Some(sessions_dir.clone())
373373
};
374374

375+
// Best-effort retention: prune session files older than the configured
376+
// window so the sessions directory does not grow unbounded. Skipped when
377+
// persistence is off or retention is unset/zero.
378+
if let Some(ref dir) = effective_sessions_dir
379+
&& let Some(days) = settings.cleanup_period_days
380+
&& days > 0
381+
{
382+
match crab_session::SessionHistory::new(dir.clone()).cleanup_older_than(days) {
383+
Ok(n) if n > 0 => {
384+
tracing::info!("pruned {n} session file(s) older than {days} days");
385+
}
386+
Ok(_) => {}
387+
Err(e) => tracing::warn!(error = %e, "session cleanup failed"),
388+
}
389+
}
390+
375391
// Resolve resume ID: explicit --resume > -c (continue latest) > None
376392
let effective_resume_id = if resume_session_id.is_some() {
377393
resume_session_id

crates/session/src/history.rs

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,37 @@ impl SessionHistory {
322322
self.read_session_file(session_id).ok().flatten()?.name
323323
}
324324

325+
/// Delete session files (`.json` / `.jsonl` and quarantined `.corrupt-*`)
326+
/// whose modification time is older than `days`, bounding the growth of the
327+
/// sessions directory. `days == 0` is a no-op (retention disabled). Returns
328+
/// the number of files removed.
329+
pub fn cleanup_older_than(&self, days: u32) -> crab_core::Result<u32> {
330+
if days == 0 || !self.base_dir.exists() {
331+
return Ok(0);
332+
}
333+
let Some(cutoff) = std::time::SystemTime::now()
334+
.checked_sub(std::time::Duration::from_secs(u64::from(days) * 86_400))
335+
else {
336+
return Ok(0);
337+
};
338+
339+
let mut removed = 0u32;
340+
for entry in std::fs::read_dir(&self.base_dir)?.flatten() {
341+
let path = entry.path();
342+
if !path.is_file() {
343+
continue;
344+
}
345+
if let Ok(meta) = entry.metadata()
346+
&& let Ok(modified) = meta.modified()
347+
&& modified < cutoff
348+
&& std::fs::remove_file(&path).is_ok()
349+
{
350+
removed += 1;
351+
}
352+
}
353+
Ok(removed)
354+
}
355+
325356
/// Find the most recent session to resume for `--continue`.
326357
///
327358
/// Prefers the newest session whose recorded working directory matches
@@ -1584,6 +1615,38 @@ mod tests {
15841615
assert!(history.load_jsonl("s1").unwrap().is_none());
15851616
}
15861617

1618+
#[test]
1619+
fn cleanup_older_than_removes_stale_and_keeps_recent() {
1620+
let dir = tempfile::tempdir().unwrap();
1621+
let history = SessionHistory::new(dir.path().to_path_buf());
1622+
history.save("recent", &[Message::user("new")]).unwrap();
1623+
1624+
// Backdate one session file well past the retention window.
1625+
let old_path = dir.path().join("old.json");
1626+
std::fs::write(&old_path, r#"{"session_id":"old","messages":[]}"#).unwrap();
1627+
let stale = std::time::SystemTime::now() - std::time::Duration::from_secs(40 * 86_400);
1628+
let f = std::fs::OpenOptions::new()
1629+
.write(true)
1630+
.open(&old_path)
1631+
.unwrap();
1632+
f.set_modified(stale).unwrap();
1633+
drop(f);
1634+
1635+
let removed = history.cleanup_older_than(30).unwrap();
1636+
assert_eq!(removed, 1);
1637+
assert!(!old_path.exists());
1638+
assert!(dir.path().join("recent.json").exists());
1639+
}
1640+
1641+
#[test]
1642+
fn cleanup_older_than_zero_is_disabled() {
1643+
let dir = tempfile::tempdir().unwrap();
1644+
let history = SessionHistory::new(dir.path().to_path_buf());
1645+
history.save("s1", &[Message::user("hi")]).unwrap();
1646+
assert_eq!(history.cleanup_older_than(0).unwrap(), 0);
1647+
assert!(dir.path().join("s1.json").exists());
1648+
}
1649+
15871650
#[test]
15881651
fn jsonl_skips_malformed_lines() {
15891652
let dir = tempfile::tempdir().unwrap();

0 commit comments

Comments
 (0)