"Turn finished · no closing message" kept appearing over an agent that was plainly still working, with output resuming a moment later. isGenerating was set in exactly three places: sendPrompt, and the two reconnect handshakes. So it answered "am I waiting for a reply I asked for", never "is this conversation working". Every other way a turn begins left it false — a commander waking on its own schedule, a queued prompt the server delivered, another tab driving the same session, or a turn that started before this tab was looking. The events still render, but buildRows reads isGenerating to decide whether the last turn is finished, so a live turn was `complete` and the moment it sat on a completed tool call with nothing said after it, the banner fired. Same gap left the composer enabled mid-turn, and a message typed then was not marked injected — which splits the live turn and prints the banner a second way, the exact failure injected exists to prevent. session:claude now sets it: the harness's own turn-start signal, and the only one that arrives before any work does. tool:start sets it too, because session:claude is not durable and a socket connecting mid-turn never receives one, while a tool cannot start outside a turn. Two candidates were measured rather than argued. A genuinely silent turn ending earns the banner honestly, but 667 of 673 recorded turns end with assistant:text immediately before result, so that is ~1% and cannot be it. And a stale sync:live is real but rare: correlating every attach in the officer log against the agent's own later output, 15 of 549 "not generating" attaches were contradicted and one provably so. That residue is left alone — the tool:start fallback now corrects it on the next event — and noted on the refresh card, which is the same seam. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Officer-Session: f7a62308-0977-4cf4-aa29-c020853fd420
60 lines
4.1 KiB
Markdown
60 lines
4.1 KiB
Markdown
---
|
|
id: known-bugs/refresh-mid-tool-call-renders-the-turn-folded
|
|
title: 'Refresh mid-turn folds the live tool call — it reads as finished while still running'
|
|
theme: known-bugs
|
|
status: open
|
|
created: 2026-08-25
|
|
date_source: stated
|
|
order: 5
|
|
---
|
|
|
|
**Refresh mid-turn folds the live tool call.** Reported 2026-08-25: refreshing while an agent is mid
|
|
tool-run collapses that turn's tool calls instead of leaving them expanded, so the chat reads as
|
|
finished when the agent is still working. Requested fix: the last, still-running turn must always
|
|
render EXPANDED across a refresh or a back-navigation, never folded.
|
|
|
|
**This is not unbuilt — there is already code for exactly this case, and it is being defeated
|
|
somewhere between the transcript load and the live-state confirmation.** `turn-rows.ts`'s `buildRows`
|
|
computes `generatingNow = isLast && isGenerating` and refuses to fold the last turn when it is true — a
|
|
comment there names this precise failure mode: _"for those few frames every unfinished turn looks
|
|
finished."_ So the intended behaviour is real; the bug is that the frames it warns about are not always
|
|
brief.
|
|
|
|
Where the race most likely lives, traced but not proven — `hooks/useChat.ts`:
|
|
|
|
- **`historicalCount` starts at `preloadedMessages?.length ?? 0`** (line ~110) — the whole HTTP-loaded
|
|
transcript, tool call included, counted as history from the first render.
|
|
- **`isGenerating` starts `false`** and is only set true when the socket confirms it — via
|
|
`sync:live` (rebinding to a turn that kept running) or `sync:messages` (a full resume).
|
|
- **`sync:live`'s own comment says messages are "deliberately absent — the transcript already loaded
|
|
over HTTP"** — it sets `isGenerating` but does not touch `historicalCount`. `sync:messages`, by
|
|
contrast, sets both together in one call (`setHistoricalCount(transcript.length)` immediately before
|
|
`setIsGenerating(msg.isGenerating)`), which is the version that should self-correct.
|
|
|
|
So the two socket messages that confirm "still generating" do not treat `historicalCount` the same way,
|
|
and a refresh's actual sequence — HTTP transcript first, socket confirmation after — is exactly the
|
|
gap the `generatingNow` guard was written to close but may not fully reach depending on which message
|
|
arrives. Worth checking first: whether `sync:live` reliably fires before the user notices, and whether
|
|
server-side `session.isGenerating` (true from turn-start to turn-end in `claude-manager.ts`, tool calls
|
|
included) is actually what reaches the client in that message — or whether it's a timing gap rather
|
|
than a wrong value.
|
|
|
|
Possibly related, same neighbourhood (refresh + a live turn + folding), not confirmed as the same
|
|
cause: `known-bugs/refreshing-a-finished-chat-turn-loses-output` — a different symptom (a turn that DID
|
|
finish loses its output entirely and misreports `0 turns · 0 tok`) but the same refresh-time seam
|
|
between what the transcript says and what the live socket confirms.
|
|
|
|
**2026-08-25 — partly overtaken, and the measurement is worth having.** A separate cause was found and
|
|
fixed for the same family of symptom: `isGenerating` was never set by anything except `sendPrompt` and the
|
|
two handshakes, so a turn this tab did not start rendered as finished from the first event onwards. See
|
|
[[known-bugs/a-turn-this-tab-did-not-start-reads-as-finished]]. `session:claude` and `tool:start` now set
|
|
it, which means the last turn stops folding as soon as any work arrives — so the window this card is about
|
|
is now bounded by the next event rather than open-ended.
|
|
|
|
What is left here is the question this card actually asks, and it is now measured rather than suspected:
|
|
correlating every attach in the officer log against the agent's own subsequent durable output, **15 of 549
|
|
attaches that told a client "not generating" were contradicted, one of them provably** — the first event
|
|
after it was a `tool:result`, so a tool was already in flight when `sidecar.isClaudeGenerating` answered.
|
|
So `sync:live` does sometimes carry a wrong value, it is rare, and it is a wrong ANSWER rather than the
|
|
timing gap this card assumed. `historicalCount` was not implicated.
|