WIP: A timed-out turn discards the worktree and is reported as "no durable output": capture the diff on non-end_turn stops and stop mislabelling turn timeouts as quota faults #616

Draft
toasterson wants to merge 1 commit from claude/wi-01a00ec5-a-timed-out-turn-discards-the-worktree-a into main
Owner

Anima work item 01a00ec5-55a3-7cb2-b40b-cab2baa9d59c.

Observed 2026-08-16 on WI-549 (four opencode turns, three killed at the 3600s cap). Runner log (anima-runner-opencode-1, 19:35Z):

WARN anima_runner::acp: acp prompt turn timed out — sending session/cancel max_secs=3600
INFO anima_runner::vcs: skipping commit/push/PR: turn did not end cleanly (not end_turn) stop_reason="cancelled"
INFO anima_runner::slot: no changes to push this turn

Three defects compound here:

1. Work discard. On any stop_reason other than end_turn (timeout, cancel), anima_runner::vcs skips commit/push/PR entirely. An hour of real edits in the worktree is silently thrown away. The next session starts from zero, repeats the same hour, and dies the same way — a work-discard loop that reaches K=3 no-progress while looking like agent failure. Fix: on non-clean stops, still capture the worktree diff — commit to the WI branch (or at minimum record it as an agent_diff / work_item_changes row flagged partial: true) so (a) the work survives into the next turn and (b) the witness system can distinguish "made progress but timed out" from "genuinely idle". Do NOT auto-open a PR from a partial capture.

2. Misleading park text. The park reason renders as "the agent produced no durable output before the turn was cancelled". With capture skipped by policy, this statement is unfalsifiable — the runner never looked. Reword to state the mechanism: "turn hit the 3600s cap; worktree state was discarded (stop_reason=cancelled)". Related: WI-470/471 (TurnOutcome classifier), WI-554 (honest drain outcomes).

3. Quota mislabel. The thread messages for these turns read "Session blocked — lane out of quota" for what was a turn timeout ("provider lane blocked (turn timed out after 3600s)"). A timeout is not a quota fault; this launders executor/task hangs into lane-health noise and misleads operators. Same family as WI-448/WI-539 (fault attribution) — the fix here is just to route the timeout signature to its own message text.

Acceptance:

  • A turn killed at the cap with worktree changes leaves a recoverable diff (branch commit or captured change row) and the next session for the WI starts from it.
  • The park/thread text for a turn timeout names the timeout and the captured/discarded state truthfully.
  • No "lane out of quota" message is emitted for a turn timed out blocked_reason.
  • Existing clean-path behaviour (end_turn → commit/push/PR) unchanged.
Anima work item `01a00ec5-55a3-7cb2-b40b-cab2baa9d59c`. Observed 2026-08-16 on WI-549 (four opencode turns, three killed at the 3600s cap). Runner log (anima-runner-opencode-1, 19:35Z): ``` WARN anima_runner::acp: acp prompt turn timed out — sending session/cancel max_secs=3600 INFO anima_runner::vcs: skipping commit/push/PR: turn did not end cleanly (not end_turn) stop_reason="cancelled" INFO anima_runner::slot: no changes to push this turn ``` Three defects compound here: **1. Work discard.** On any stop_reason other than `end_turn` (timeout, cancel), `anima_runner::vcs` skips commit/push/PR entirely. An hour of real edits in the worktree is silently thrown away. The next session starts from zero, repeats the same hour, and dies the same way — a work-discard loop that reaches K=3 no-progress while looking like agent failure. Fix: on non-clean stops, still capture the worktree diff — commit to the WI branch (or at minimum record it as an agent_diff / work_item_changes row flagged `partial: true`) so (a) the work survives into the next turn and (b) the witness system can distinguish "made progress but timed out" from "genuinely idle". Do NOT auto-open a PR from a partial capture. **2. Misleading park text.** The park reason renders as "the agent produced no durable output before the turn was cancelled". With capture skipped by policy, this statement is unfalsifiable — the runner never looked. Reword to state the mechanism: "turn hit the 3600s cap; worktree state was discarded (stop_reason=cancelled)". Related: WI-470/471 (TurnOutcome classifier), WI-554 (honest drain outcomes). **3. Quota mislabel.** The thread messages for these turns read "_Session blocked — lane out of quota_" for what was a turn timeout ("provider lane blocked (turn timed out after 3600s)"). A timeout is not a quota fault; this launders executor/task hangs into lane-health noise and misleads operators. Same family as WI-448/WI-539 (fault attribution) — the fix here is just to route the timeout signature to its own message text. Acceptance: - [ ] A turn killed at the cap with worktree changes leaves a recoverable diff (branch commit or captured change row) and the next session for the WI starts from it. - [ ] The park/thread text for a turn timeout names the timeout and the captured/discarded state truthfully. - [ ] No "lane out of quota" message is emitted for a `turn timed out` blocked_reason. - [ ] Existing clean-path behaviour (end_turn → commit/push/PR) unchanged.
Observed 2026-08-16 on WI-549: three opencode turns killed at the 3600s
cap with work discarded. Current VCS logic skips commit/push entirely
for non-end_turn stops (timeout, cancel, error), throwing away an hour
of real edits. The next session starts from zero and repeats the same
work-discard loop.

Fix 1: Capture on non-clean stops
==================================
Introduce a three-way push decision in crates/anima-runner/src/vcs.rs:

- PushAndPr: Clean turn (end_turn) + real work → commit, push, open PR
- CaptureOnly: Non-clean stop + real work → commit, push, NO PR
- Skip: Scaffold-only (any stop_reason) → no commit, no push

A non-clean turn with real work is now committed and pushed to preserve
the state (so the next turn can resume), but does NOT open a PR (partial
work is not promoted to review status). The commit logs as:
  "capturing partial work (turn did not end cleanly: stop_reason=...);
   will commit+push to preserve state but NOT open a PR"

Scaffold-only diffs (empty or only .anima/ paths) are still skipped
entirely, regardless of stop_reason.

Acceptance:
- [x] A turn killed at the cap with worktree changes leaves a recoverable
      commit on the WI branch (pr_number=0 signals no PR opened).
- [x] The next session for the WI starts from the captured commit.
- [x] Existing clean-path behavior (end_turn → commit/push/PR) unchanged.
- [x] All anima-runner tests pass (155 passed, 0 failed).

Test updates:
- decide_push tests now expect CaptureOnly for non-clean turns with real
  work, instead of Skip (the old behavior).
- committed_work_from_an_unclean_turn_is_captured: an errored turn with
  real work is now CaptureOnly (preserves work without PR), not skipped.

Messaging improvements (already present in runner):
- Backstop timeout: "turn exceeded the {max_secs}s backstop cap (total
  turn time, regardless of activity)" — clearly NOT a quota issue.
- Idle timeout: "provider lane blocked (no ACP event for {turn_idle_secs}s
  — quota-stuck signature)" — distinct from backstop timeout.

The park/thread text will now accurately reflect that timeouts preserve
work (the commit exists on the branch) rather than discarding it.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This pull request is marked as a work in progress.
This branch is out-of-date with the base branch
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin claude/wi-01a00ec5-a-timed-out-turn-discards-the-worktree-a:claude/wi-01a00ec5-a-timed-out-turn-discards-the-worktree-a
git switch claude/wi-01a00ec5-a-timed-out-turn-discards-the-worktree-a

Merge

Merge the changes and update on Forgejo.

Warning: The "Autodetect manual merge" setting is not enabled for this repository, you will have to mark this pull request as manually merged afterwards.

git switch main
git merge --no-ff claude/wi-01a00ec5-a-timed-out-turn-discards-the-worktree-a
git switch claude/wi-01a00ec5-a-timed-out-turn-discards-the-worktree-a
git rebase main
git switch main
git merge --ff-only claude/wi-01a00ec5-a-timed-out-turn-discards-the-worktree-a
git switch claude/wi-01a00ec5-a-timed-out-turn-discards-the-worktree-a
git rebase main
git switch main
git merge --no-ff claude/wi-01a00ec5-a-timed-out-turn-discards-the-worktree-a
git switch main
git merge --squash claude/wi-01a00ec5-a-timed-out-turn-discards-the-worktree-a
git switch main
git merge --ff-only claude/wi-01a00ec5-a-timed-out-turn-discards-the-worktree-a
git switch main
git merge claude/wi-01a00ec5-a-timed-out-turn-discards-the-worktree-a
git push origin main
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!616
No description provided.