a turn this tab did not start is still a turn in progress
"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
This commit is contained in:
@@ -409,10 +409,26 @@ export function useChat(initialSessionId?: string, initialModel?: string | null,
|
||||
break;
|
||||
|
||||
case 'session:claude':
|
||||
// The turn has only just begun and the conversation is already addressable. Everything that makes
|
||||
// a refresh survivable hangs off this: the URL is what the reattach handshake sends, so writing it
|
||||
// here rather than at `result` is the difference between "refresh mid-turn and lose the turn" and
|
||||
// "refresh mid-turn and watch it carry on".
|
||||
/*
|
||||
A turn is starting, and THIS TAB MAY NOT BE THE ONE THAT STARTED IT.
|
||||
|
||||
`isGenerating` used to be set in exactly three places — `sendPrompt`, and the two reconnect
|
||||
handshakes — so it answered "am I waiting for a reply I asked for", not "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 or another device driving the same session.
|
||||
|
||||
The tab still rendered the work, because the events arrive either way. But `buildRows` reads
|
||||
`isGenerating` to decide whether the last turn is finished, so with it false a live turn is
|
||||
`complete`, and the moment it sat on a completed tool call with nothing said after it the reader
|
||||
got "Turn finished · no closing message" — over an agent that carried on a second later. It also
|
||||
left the composer enabled mid-turn, and a message typed then was not marked `injected`, which
|
||||
splits the live turn: the precise failure `injected` exists to prevent (see types.ts).
|
||||
|
||||
Marked here rather than on the first tool call because this is the harness's own turn-start
|
||||
signal, and it is the only one that arrives before any work does.
|
||||
*/
|
||||
setIsGenerating(true);
|
||||
setHasStarted(true);
|
||||
claudeSessionIdRef.current = msg.claudeSessionId;
|
||||
setClaudeSessionId(msg.claudeSessionId);
|
||||
if (replaceUrl) writePermalink(msg.claudeSessionId);
|
||||
@@ -449,6 +465,9 @@ export function useChat(initialSessionId?: string, initialModel?: string | null,
|
||||
}
|
||||
|
||||
case 'tool:start': {
|
||||
// Belt and braces for a turn whose `session:claude` this tab missed — an event that is NOT durable,
|
||||
// so a socket that connects mid-turn never receives one. A tool cannot start outside a turn.
|
||||
setIsGenerating(true);
|
||||
toolCallsInTurnRef.current = true;
|
||||
const call: ChatMessage = {
|
||||
role: 'tool',
|
||||
|
||||
Reference in New Issue
Block a user