fix(resume): validate checkpoint before cleanup and clear progress.jsonl

Two issues in the resume cleanup logic:

1. progress.jsonl was not in the stale artifact list, so attach and
   logs would replay the previous attempt's events before the new run.
   Added it to the cleanup list.

2. Cleanup ran before validating the checkpoint was parseable. A
   crash during the original run can leave a truncated checkpoint.json
   that passes exists() but fails to parse. We now load and parse the
   checkpoint first; if it's corrupt we bail with the old conclusion
   and failure evidence intact.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Bryan Helmkamp 2026-03-26 13:08:36 -04:00
parent 25b9d24d9e
commit 4c6b846fb9
No known key found for this signature in database

View file

@ -1,7 +1,7 @@
use anyhow::bail;
use clap::Args;
use fabro_util::terminal::Styles;
use fabro_workflows::records::RunRecord;
use fabro_workflows::records::{Checkpoint, RunRecord};
use fabro_workflows::run_status::RunStatus;
#[derive(Debug, Args)]
@ -29,9 +29,18 @@ pub async fn resume_command(args: ResumeArgs, styles: &'static Styles) -> anyhow
}
let run_id = RunRecord::load(&run_dir)?.run_id;
if !run_dir.join("checkpoint.json").exists() {
bail!("no checkpoint found — nothing to resume");
}
// Validate checkpoint is parseable before touching any state.
// A crash during the original run can leave a truncated file;
// we must not destroy the old conclusion/failure evidence and
// only then discover the checkpoint is corrupt.
let cp_path = run_dir.join("checkpoint.json");
Checkpoint::load(&cp_path).map_err(|e| {
if cp_path.exists() {
anyhow::anyhow!("checkpoint.json is corrupt — cannot resume: {e}")
} else {
anyhow::anyhow!("no checkpoint found — nothing to resume")
}
})?;
// Guard against resuming a live run
if is_pid_alive(&run_dir.join("run.pid")) {
@ -48,6 +57,7 @@ pub async fn resume_command(args: ResumeArgs, styles: &'static Styles) -> anyhow
"interview_request.claim",
"detach.log",
"run.pid",
"progress.jsonl",
] {
let _ = std::fs::remove_file(run_dir.join(name));
}