From 6b8cf848fffdd41c281e91cc8400cb7ea971f3c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Tue, 25 Aug 2026 15:27:49 +0000 Subject: [PATCH] a turn this tab did not start is still a turn in progress MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "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 Officer-Session: f7a62308-0977-4cf4-aa29-c020853fd420 --- TODO.md | 70 +++++++++---------- .../officerdev/src/hooks/useChat.ts | 27 +++++-- ...his-tab-did-not-start-reads-as-finished.md | 50 +++++++++++++ ...h-mid-tool-call-renders-the-turn-folded.md | 14 ++++ 4 files changed, 122 insertions(+), 39 deletions(-) create mode 100644 todo/known-bugs/a-turn-this-tab-did-not-start-reads-as-finished.md diff --git a/TODO.md b/TODO.md index 7e6d0e91..dfa49c71 100644 --- a/TODO.md +++ b/TODO.md @@ -21,42 +21,42 @@ the owner's OS user and can never be granted. Indirection there really is accide ## Themes -**97 open, 30 closed**, across 32 themes. +**97 open, 31 closed**, across 32 themes. -| Theme | Open | Closed | -| ------------------------------------------------------------------------------------------ | ---- | ------ | -| [Affordances](todo/affordances/) | 2 | 0 | -| [Agents coordinating agents](todo/agent-coordination/) | 3 | 0 | -| [Speaking into a session from the backend](todo/agent-delivery/) | 1 | 1 | -| [Backburner](todo/backburner/) | 3 | 0 | -| [Background jobs](todo/background-jobs/) | 2 | 1 | -| [Chat interface](todo/chat/) | 1 | 0 | -| [Code editor](todo/code-editor/) | 1 | 0 | -| [Components — the shared UI layer](todo/components/) | 1 | 0 | -| [File watchers](todo/file-watchers/) | 1 | 0 | -| [Fleet — one command across every server](todo/fleet/) | 5 | 2 | -| [Headscale](todo/headscale/) | 0 | 5 | -| [Infra (alpha)](todo/infra/) | 7 | 0 | -| [Known bugs](todo/known-bugs/) | 5 | 3 | -| [Memory — one store instead of per-harness files](todo/memory/) | 1 | 0 | -| [Mobile apps](todo/mobile-apps/) | 4 | 0 | -| [Multi-user](todo/multi-user/) | 17 | 5 | -| [Notifications](todo/notifications/) | 1 | 0 | -| [Plugin ecosystem — before it opens to third parties (2026-08-18)](todo/plugin-ecosystem/) | 3 | 0 | -| [Plugin infrastructure](todo/plugin-infrastructure/) | 2 | 0 | -| [Plugin repos — what writing their READMEs turned up](todo/plugin-repos/) | 10 | 9 | -| [Plugin system — gaps the email extraction found (2026-08-18)](todo/plugin-system/) | 3 | 0 | -| [Prompt library](todo/prompt-library/) | 1 | 0 | -| [SendOff](todo/sendoff/) | 1 | 0 | -| [Settings — portable, and readable by an agent](todo/settings/) | 2 | 0 | -| [Shell — spotlight and the dock](todo/shell/) | 4 | 1 | -| [Sub-agents — agents spawning agents](todo/sub-agents/) | 0 | 2 | -| [Sudo terminal modal](todo/sudo-terminal-modal/) | 1 | 0 | -| [Tooling — the list about the list](todo/tooling/) | 0 | 1 | -| [Transmission](todo/transmission/) | 6 | 0 | -| [URL state — routing, query strings and selection](todo/url-state/) | 4 | 0 | -| [Waiting on the owner to run it](todo/waiting-on-owner/) | 3 | 0 | -| [Workspaces & Panels](todo/workspaces-panels/) | 2 | 0 | +| Theme | Open | Closed | +| --- | --- | --- | +| [Affordances](todo/affordances/) | 2 | 0 | +| [Agents coordinating agents](todo/agent-coordination/) | 3 | 0 | +| [Speaking into a session from the backend](todo/agent-delivery/) | 1 | 1 | +| [Backburner](todo/backburner/) | 3 | 0 | +| [Background jobs](todo/background-jobs/) | 2 | 1 | +| [Chat interface](todo/chat/) | 1 | 0 | +| [Code editor](todo/code-editor/) | 1 | 0 | +| [Components — the shared UI layer](todo/components/) | 1 | 0 | +| [File watchers](todo/file-watchers/) | 1 | 0 | +| [Fleet — one command across every server](todo/fleet/) | 5 | 2 | +| [Headscale](todo/headscale/) | 0 | 5 | +| [Infra (alpha)](todo/infra/) | 7 | 0 | +| [Known bugs](todo/known-bugs/) | 5 | 4 | +| [Memory — one store instead of per-harness files](todo/memory/) | 1 | 0 | +| [Mobile apps](todo/mobile-apps/) | 4 | 0 | +| [Multi-user](todo/multi-user/) | 17 | 5 | +| [Notifications](todo/notifications/) | 1 | 0 | +| [Plugin ecosystem — before it opens to third parties (2026-08-18)](todo/plugin-ecosystem/) | 3 | 0 | +| [Plugin infrastructure](todo/plugin-infrastructure/) | 2 | 0 | +| [Plugin repos — what writing their READMEs turned up](todo/plugin-repos/) | 10 | 9 | +| [Plugin system — gaps the email extraction found (2026-08-18)](todo/plugin-system/) | 3 | 0 | +| [Prompt library](todo/prompt-library/) | 1 | 0 | +| [SendOff](todo/sendoff/) | 1 | 0 | +| [Settings — portable, and readable by an agent](todo/settings/) | 2 | 0 | +| [Shell — spotlight and the dock](todo/shell/) | 4 | 1 | +| [Sub-agents — agents spawning agents](todo/sub-agents/) | 0 | 2 | +| [Sudo terminal modal](todo/sudo-terminal-modal/) | 1 | 0 | +| [Tooling — the list about the list](todo/tooling/) | 0 | 1 | +| [Transmission](todo/transmission/) | 6 | 0 | +| [URL state — routing, query strings and selection](todo/url-state/) | 4 | 0 | +| [Waiting on the owner to run it](todo/waiting-on-owner/) | 3 | 0 | +| [Workspaces & Panels](todo/workspaces-panels/) | 2 | 0 | ## Records — no items, kept for the reasoning diff --git a/src/workspaces/officerdev/src/hooks/useChat.ts b/src/workspaces/officerdev/src/hooks/useChat.ts index 340f6aa8..cab049b1 100644 --- a/src/workspaces/officerdev/src/hooks/useChat.ts +++ b/src/workspaces/officerdev/src/hooks/useChat.ts @@ -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', diff --git a/todo/known-bugs/a-turn-this-tab-did-not-start-reads-as-finished.md b/todo/known-bugs/a-turn-this-tab-did-not-start-reads-as-finished.md new file mode 100644 index 00000000..e73a1ca3 --- /dev/null +++ b/todo/known-bugs/a-turn-this-tab-did-not-start-reads-as-finished.md @@ -0,0 +1,50 @@ +--- +id: known-bugs/a-turn-this-tab-did-not-start-reads-as-finished +title: 'A turn this tab did not start reads as finished — "no closing message" over a working agent' +theme: known-bugs +status: done +created: 2026-08-25 +date_source: stated +order: 6 +--- + +**"Turn finished · no closing message" appears while the agent is still working, and output resumes a +moment later.** Reported by the owner 2026-08-25 with a screenshot: the session list shows Platform Harry +`generating`, and Platform Harry's own panel shows the banner under two completed Bash calls. He reports it +happens often. + +**Not the same bug as the two below it, and not a race.** `isGenerating` was set in exactly three places: +`sendPrompt`, and the two reconnect handshakes (`sync:live`, `sync:messages`). 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 or another device driving the same session, or simply a turn that started before this tab was looking. + +The tab still renders the work, because the events arrive regardless. But `buildRows` reads `isGenerating` +to decide whether the last turn is finished (`generatingNow = isLast && isGenerating`), so with it false a +live turn is `complete` — and the moment that turn sits on a completed tool call with nothing said after +it, the banner fires. Then the next event arrives and the conversation carries on, which is exactly what +"still outputting in a bit" is. + +Two further consequences of the same gap, both visible in the screenshot: the composer stays **enabled** +during someone else's turn, and a message typed then is not marked `injected` — which splits the live turn +and prints the banner a second way, the precise failure `injected` was added to prevent (`types.ts`). + +**Ruled out on the way, with evidence rather than reading.** A silent turn ending would earn the banner +honestly, so that was measured first: across every durable event ever recorded, **667 of 673 turns end with +`assistant:text` immediately before `result`**. Genuine silent endings are ~1% and cannot account for "a lot +of times". A stale `sync:live` was the other candidate and is real but rare — correlating every attach in +the officer log against the agent's own subsequent output, 15 of 549 "not generating" attaches were +contradicted, and only **one** provably so (the first event after it was a `tool:result`, so a tool was +already in flight when officer answered). + +--- + +**Fixed 2026-08-25.** `session:claude` — the harness's own turn-start signal, and the only one that arrives +before any work does — now sets `isGenerating`. `tool:start` sets it too, as a fallback: `session:claude` is +not durable, so a socket that connects mid-turn never receives one, and a tool cannot start outside a turn. + +Residual, and left alone deliberately: `sync:live` can still report `isGenerating: false` for a turn that is +running (the one proven case above, `sidecar.isClaudeGenerating` answering for an adopted session). The +`tool:start` fallback now corrects that on the next event rather than leaving the turn wrongly settled, so +the symptom is covered even where the answer is not. Fixing the answer itself belongs with +[[known-bugs/refresh-mid-tool-call-renders-the-turn-folded]], which is the same seam. diff --git a/todo/known-bugs/refresh-mid-tool-call-renders-the-turn-folded.md b/todo/known-bugs/refresh-mid-tool-call-renders-the-turn-folded.md index bc127da5..76fe3d29 100644 --- a/todo/known-bugs/refresh-mid-tool-call-renders-the-turn-folded.md +++ b/todo/known-bugs/refresh-mid-tool-call-renders-the-turn-folded.md @@ -43,3 +43,17 @@ Possibly related, same neighbourhood (refresh + a live turn + folding), not conf 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.