fix: detect early init failures in attach and roll back partial worktree state

Two pre-existing issues fixed:

1. attach: while waiting for progress.jsonl, check for terminal
   status.json and engine child death. A detached run that dies during
   early init (before any event fires) now surfaces the real failure
   instead of timing out after 10s.

2. worktree: when `git worktree add` fails after branch creation,
   roll back the branch with `git branch -D` to avoid leaking partial
   git state.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Bryan Helmkamp 2026-03-26 23:28:08 -04:00
parent 0f738c1aa2
commit b1fb172dde
No known key found for this signature in database
2 changed files with 34 additions and 1 deletions

View file

@ -56,11 +56,36 @@ pub async fn attach_run(
});
}
// Wait for progress.jsonl to appear
// Wait for progress.jsonl to appear.
// If the engine dies during early init (before any event is emitted),
// progress.jsonl may never be created. Check for terminal status or
// engine death so we surface the real failure instead of timing out.
let mut wait_count = 0;
while !progress_path.exists() {
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
wait_count += 1;
// Check if engine died before writing any progress
if let Some(record) = read_status_record(&status_path) {
if record.status.is_terminal() {
progress_ui.finish();
return Ok(determine_exit_code(&conclusion_path, Some(record)));
}
}
if let Some(guard) = engine_guard.as_mut() {
if let Some(child) = guard.inner() {
if matches!(child.try_wait(), Ok(Some(_))) {
// Engine exited without writing progress.jsonl
progress_ui.finish();
return Ok(determine_exit_code(
&conclusion_path,
read_status_record(&status_path),
));
}
}
}
if wait_count > 100 {
// Guard's Drop kills+waits on the engine child
drop(engine_guard.take());

View file

@ -148,6 +148,14 @@ impl Sandbox for WorktreeSandbox {
.exec_command(&add_cmd, 30_000, None, None, None)
.await?;
if result.exit_code != 0 {
// Roll back the branch created above so we don't leak partial state.
if !self.config.skip_branch_creation {
let rollback_cmd = format!("{GIT} branch -D {branch}");
let _ = self
.inner
.exec_command(&rollback_cmd, 30_000, None, None, None)
.await;
}
return Err(format!(
"git worktree add failed (exit {}): {}",
result.exit_code,