Skip to content

fix: typing while a tool runs re-armed the stall watchdog and reset the turn - #298

Merged
saucam merged 1 commit into
mainfrom
fix/midturn-push-keeps-tool-running
Aug 19, 2026
Merged

fix: typing while a tool runs re-armed the stall watchdog and reset the turn#298
saucam merged 1 commit into
mainfrom
fix/midturn-push-keeps-tool-running

Conversation

@saucam

@saucam saucam commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator

Diagnoses the reported symptom: send a message while Claude is working and the session goes quiet, then needs a "please continue" to come back.

Root cause

The mid-turn push path carved out waiting_approval when re-asserting status, but not tool_running:

if (this.#status !== "waiting_approval") this.#setStatus("thinking");

Both are real states the push does not end, and both are load-bearing twice over:

  • Frontends key off them — the approval bar off waiting_approval, the running-tool card off tool_running. The flip made the session header say "thinking" while the tool card still said "executing", which is exactly what the reported transcript shows.
  • #watchdogPaused() keys off them. A genuinely-executing tool is a legitimately silent run — that is precisely why the stall watchdog pauses there.

So typing during a long tool silently re-armed the watchdog against it. Once the stall window lapsed, the run was force-recovered:

⚠️ Turn timed out — no activity for 0s. The session was reset;
   send your message again to continue.

killing a healthy tool and the message the user had just queued into it. The longer the tool — a warehouse sweep, a big build — the more certain the kill. Typing while it ran is what shortened its leash.

Fix

Keep tool_running for the same reason waiting_approval was already kept. Anything else still becomes thinking — the push does start model work there. When the tool finishes, the existing tool-completion path moves it to thinking and the watchdog re-arms normally.

Tests

Two, both verified to fail without the fix (I reverted the one-line change and re-ran to confirm they aren't vacuous):

  1. Status is preserved across a mid-turn push during a running tool.
  2. The damage that actually mattered — sitting past the stall window with the tool still running yields no timeout message, no provider teardown, no second turn. Without the fix this one fails with the exact ⚠️ Turn timed out message above.

The existing waiting_approval mid-turn test and the "long tool on a backend without mid-turn injection" test both still pass, so this closes the gap between them rather than widening either.

Full suite: 2313 pass, 0 fail; lint + typecheck clean.

Scope note

This fixes one confirmed mechanism with this symptom. If quiet sessions persist after it lands, the next thing I'd look at is #pendingMidTurnCount: it is incremented per querying push and only decremented on a turn_done, so if a push ever fails to produce the extra turn boundary it expects, the turn's real terminal turn_done gets absorbed as an intermediate one and the consumer waits forever. That path is currently backstopped only by the stall watchdog — the same watchdog this PR stops mis-firing. Worth a look if the symptom recurs, but I did not want to speculatively change reconciliation logic in the same PR as a proven one-line state fix.

…he turn

Reported symptom: send a message while Claude is working and the session goes
quiet, then needs a "please continue" to come back.

A mid-turn push carved out `waiting_approval` when re-asserting status, but not
`tool_running`:

    if (this.#status !== "waiting_approval") this.#setStatus("thinking");

Both are real states the push does not end, and both are load-bearing twice
over. Frontends key off them — the approval bar off waiting_approval, the
running-tool card off tool_running — so the flip made the session header
disagree with a tool card still showing "executing". Worse, `#watchdogPaused()`
keys off them: a genuinely-executing tool is a LEGITIMATELY silent run, which is
exactly why the stall watchdog pauses there.

So typing during a long tool silently re-armed the watchdog against it. Once the
stall window lapsed the run was force-recovered:

    ⚠️ Turn timed out — no activity for 0s. The session was reset;
       send your message again to continue.

killing a healthy tool AND the message the user had just queued into it. The
longer the tool (a warehouse sweep, a big build), the more certain the kill —
and typing while it ran is what shortened its leash.

Keep tool_running for the same reason waiting_approval was already kept.
Anything else still becomes "thinking": the push does start model work there.
When the tool finishes, the existing tool-completion path moves it to thinking
and the watchdog re-arms normally.

Two tests, both verified to fail without the fix: the status is preserved
across a mid-turn push during a running tool, and — the damage that actually
mattered — sitting past the stall window with the tool still running produces no
timeout message, no provider teardown, and no second turn.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@saucam
saucam merged commit 6623c2f into main Aug 19, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants