import { rmSync, statSync } from "node:fs"; import { listRunDirs, tryReadRunState } from "./artifacts"; const DAY_MS = 24 * 60 * 60 * 1000; const COMPLETED_RUN_TTL_MS = 7 * DAY_MS; const INTERRUPTED_RUN_TTL_MS = 30 * DAY_MS; export interface CleanupResult { scanned: number; removed: number; errors: number; } /** * Sweep state/subagents/ and remove run directories past their TTL. * * - `phase === "done"` runs expire after COMPLETED_RUN_TTL_MS. * - All other phases expire after INTERRUPTED_RUN_TTL_MS. * - Malformed run.json or unreadable directories are skipped (not removed). * - Errors during rm are counted, not thrown — TTL sweep must never block startup. * * `now` is injectable for tests. */ export async function cleanupOldRuns(now = Date.now()): Promise { const result: CleanupResult = { scanned: 0, removed: 0, errors: 0 }; let dirs: string[]; try { dirs = listRunDirs(); } catch { return result; } for (const dir of dirs) { result.scanned += 1; const persisted = tryReadRunState(dir); if (!persisted) continue; let mtime: number; try { mtime = statSync(dir).mtimeMs; } catch { continue; } const ttl = persisted.phase === "done" ? COMPLETED_RUN_TTL_MS : INTERRUPTED_RUN_TTL_MS; if (now - mtime < ttl) continue; try { rmSync(dir, { recursive: true, force: true }); result.removed += 1; } catch { result.errors += 1; } } return result; } // Exposed for tests; module constants stay private otherwise. export const __testables = { COMPLETED_RUN_TTL_MS, INTERRUPTED_RUN_TTL_MS, DAY_MS, };