A failed turn is indistinguishable from an empty one: server ignores stop_reason, WI re-dispatches forever in working #366
No reviewers
Labels
No labels
Compat/Breaking
Kind/Bug
Kind/Documentation
Kind/Enhancement
Kind/Feature
Kind/Security
Kind/Testing
Priority
Critical
Priority
High
Priority
Low
Priority
Medium
Reviewed
Confirmed
Reviewed
Duplicate
Reviewed
Invalid
Reviewed
Won't Fix
Status
Abandoned
Status
Blocked
Status
Need More Info
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
toasterson/Anima!366
Loading…
Reference in a new issue
No description provided.
Delete branch "claude/wi-019f98b5-a-failed-turn-is-indistinguishable-from"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Anima work item
019f98b5-2ccd-7d80-9842-ec3e395b0d12.Problem
The runner reports why a turn ended; the server throws it away. A turn that died — expired auth, EMFILE, a blown turn budget — lands on the server as the same nothing as a turn where the agent genuinely had no changes. The work item stays in
working, the scheduler re-dispatches it on the next tick, the turn dies the same way, and this repeats indefinitely with no failure recorded anywhere a human or the loop guard can see.This is why the fleet can burn a day and a half at zero throughput while every surface reads "working".
Observed (2026-07-24 → 07-25)
Nothing has merged to
mainsince 2026-07-23 21:22. In that window all three lanes failed silently:anima-runner-1):CLAUDE_CODE_OAUTH_TOKENexpired. Every turn since at least 03:19Z:acp prompt turn failed error=… API Error: 401 {"type":"authentication_error","message":"OAuth access token has expired"}immediately followed byno changes to push this turn. A fresh session id every ~33 min, ~20 consecutive dead turns.anima-runner-opencode): every turn hangs and is killed at the 3600s cap —acp prompt turn timed out — sending session/cancel, thencancelled turn did not settle within the grace window, thenno changes to push this turn. One WI-hour burned per turn, ~1 turn/90 min, zero output.EMFILE: too many open files, watch …/.claudethrown out ofSettingsManager.setupWatchersduringsession/new— the session came up crippled and the turn failed, again reported as nothing.Root cause (code)
crates/anima-runner/src/acp.rs(~L927) already classifies the outcome correctly:crates/anima-server/src/services/runner.rs(~L1076–1120), handlingTurnDone, does exactly one thing with it: writespayload: {"stop_reason": td.stop_reason}onto anacp.turn_completedactivity event. There is no branch on the value. The only phase-advancing path below it is gated on!td.pr_ref.is_empty()— and a failed turn has nopr_ref, so it falls through to the same no-op as a legitimately empty turn.Note the runner does distinguish a dead session (
SessionClosed.reason = "failed: …", hardened by #337). The gap is one level down: a turn that fails inside a session that stays alive.Proposed fix
td.stop_reasonin theTurnDonehandler.errorandcancelled(budget blown) are failures;end_turn/max_tokensare not.failed_dispatch_countagainst the existing park cap, and (b) post a visible note on the WI's anchored thread carrying the runner's error text — the 401 string above is self-diagnosing and a human seeing it once would have fixed the fleet in a minute.needs-humanwith the cause as the park reason, distinct from a code-failure park.Acceptance criteria
stop_reason=errorno longer reads as an empty turn: the WI's failure counter moves and the error text appears on the thread.TurnDone{stop_reason:"error", pr_ref:""}→ counter incremented + thread note emitted;TurnDone{stop_reason:"end_turn", pr_ref:""}→ unchanged.Files
crates/anima-server/src/services/runner.rs(TurnDonehandling, ~L1076)crates/anima-server/src/scheduler.rs(back-off, lane degradation, park reason)crates/anima-runner/src/acp.rs(carry the error text on TurnDone, not just the"error"label)proto/anima/v1/runner.proto(TurnDoneneeds an error field)Related
019f8602-ed6b-7421-8a12-a4303f6b745areopen WIs when orphaned sessions fail — same family, session level rather than turn level.019f9698-6f38-70d3-a1f8-468e966dabc2CI-red loop can't tell infra failure from job failure — the same "we threw away the reason" mistake on the CI side.019f903c-eeb1-7e71-928b-e248db024384fleet observability metrics + alerts — would have alarmed on this.working49536b834549536b8345to4aef3b1f324aef3b1f32to09a83243d409a83243d4to815c9daff6815c9daff6to3e34c8df003e34c8df00toc914c46149c914c46149to971c7d59a1WIP: A failed turn is indistinguishable from an empty one: server ignores stop_reason, WI re-dispatches forever into A failed turn is indistinguishable from an empty one: server ignores stop_reason, WI re-dispatches forever inworkingworking