Distinguish an empty turn from a failed turn end to end #406

Merged
toasterson merged 1 commit from claude/wi-019fa59c-distinguish-an-empty-turn-from-a-failed into main 2026-07-28 20:31:11 +00:00
Owner

Anima work item 019fa59c-941f-7942-961c-5950e1405669 (WI-387).

Salvaged work. The implementer completed this turn at 18:31 UTC on 2026-07-28 — ~1000 events, reporting "everything compiles and all 323 unit tests pass" — and the runner discarded it. The content gate added in #396 reads the staged diff, but the forcing prompt asks the agent to commit and opencode does, so git add -A found nothing to stage, the turn was classified scaffold-only, and no branch was pushed. The commit survived only in the runner's worktree volume; I recovered it via git bundle and rebased it onto current main. The fix for the discard itself is in a separate PR.

The commit is the agent's own, unmodified — author and message preserved.

What it does

Most of the failed-turn branching landed in #402 (WI 019fa7ac). This closes the four remaining gaps the acceptance criteria name:

  1. Explicit TurnOutcome (Completed/Empty/Failed) on the witness, folded into the witness hash so a failed turn and an empty turn can never share a hash. The detector reads the classification rather than inferring it from diff emptiness.
  2. Migration 082 adds a nullable outcome column; legacy rows (NULL) hash differently from any classified row, so the cutover is a clean break.
  3. Activity event + summarizeracp.turn_completed carries outcome; summaries read "Turn completed" / "Turn empty (no progress)" / "Turn failed — <detail>".
  4. Session-closed-failed witness — a 429 that kills the session before any TurnDone (the "healthy-looking session with very few events and no turn_completed" shape) now records a Failed witness, so K consecutive identical-cause session deaths park.

Verification

cargo check --workspace --all-targets clean against current main after rebase. Migration 082 does not collide (main is at 081).

🤖 Generated with Claude Code

Anima work item `019fa59c-941f-7942-961c-5950e1405669` (WI-387). **Salvaged work.** The implementer completed this turn at 18:31 UTC on 2026-07-28 — ~1000 events, reporting "everything compiles and all 323 unit tests pass" — and the runner discarded it. The content gate added in #396 reads the *staged* diff, but the forcing prompt asks the agent to commit and opencode does, so `git add -A` found nothing to stage, the turn was classified scaffold-only, and no branch was pushed. The commit survived only in the runner's worktree volume; I recovered it via `git bundle` and rebased it onto current `main`. The fix for the discard itself is in a separate PR. The commit is the agent's own, unmodified — author and message preserved. ## What it does Most of the failed-turn branching landed in #402 (WI 019fa7ac). This closes the four remaining gaps the acceptance criteria name: 1. **Explicit `TurnOutcome`** (`Completed`/`Empty`/`Failed`) on the witness, folded into the witness hash so a failed turn and an empty turn can never share a hash. The detector reads the classification rather than inferring it from diff emptiness. 2. **Migration 082** adds a nullable `outcome` column; legacy rows (NULL) hash differently from any classified row, so the cutover is a clean break. 3. **Activity event + summarizer** — `acp.turn_completed` carries `outcome`; summaries read "Turn completed" / "Turn empty (no progress)" / "Turn failed — &lt;detail&gt;". 4. **Session-closed-failed witness** — a 429 that kills the session before any `TurnDone` (the "healthy-looking session with very few events and no `turn_completed`" shape) now records a `Failed` witness, so K consecutive identical-cause session deaths park. ## Verification `cargo check --workspace --all-targets` clean against current `main` after rebase. Migration 082 does not collide (main is at 081). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
The 38-hour outage's root cause was the server ignoring
TurnDone.stop_reason, collapsing failed turns (401/EMFILE/timeout/429)
into empty turns. Most of the failed-turn branching landed in the #402
squash merge (WI 019fa7ac); this slice closes the remaining gaps the
acceptance criteria name.

- Explicit TurnOutcome (Completed/Empty/Failed) on the Witness, folded
  into the witness hash so a failed turn and an empty turn can never
  share a hash. The detector reads the classification rather than
  inferring it from diff emptiness.
- Migration 082 adds a nullable outcome column; legacy rows (NULL) hash
  differently from any classified row so the cutover is a clean break.
- The acp.turn_completed activity event carries outcome; the summarizer
  surfaces 'Turn completed' / 'Turn empty (no progress)' / 'Turn failed'.
- A session that dies without a TurnDone (a 429 that killed the first
  turn, EMFILE on session/new, a transport drop) now records a Failed
  witness via record_witness_for_session_failure, so K consecutive
  identical-cause session deaths park — the 'healthy-looking session
  with very few events and no turn_completed' shape from the pushed
  context.

Tests: 19 progress_witness unit tests (8 new), 53 runner unit tests
(7 new), 5 activity summarizer unit tests (new module), 6 turn_failure
integration tests (2 new). All 403 workspace unit tests pass.
toasterson force-pushed claude/wi-019fa59c-distinguish-an-empty-turn-from-a-failed from d2f8a5e1aa to 8d26e1907d 2026-07-28 19:13:02 +00:00 Compare
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
toasterson/Anima!406
No description provided.