Fix spurious 'didn't finish' double-response on completed turns#219
Merged
Conversation
The second/unprompted 'Looks like that turn didn't finish…' bubble was a client-side continuation nudge (CONTINUE_PROMPTS), not a real second agent turn. reconcileTaskMessages inferred 'interrupted' from weak heuristics — a stripped _streaming row (hadStreaming) or a trailing user message — which transiently hold right as a turn completes. The in-flight poll's finished-refetch made it worse by reconciling with both guards off (false,false), so a freshly-completed turn got nudged → the double response, on new tasks especially (a new conversation enters and leaves the in-flight set exactly at completion). This contradicts the detached-buffer design: a turn that finishes writes a terminal record (response.completed/failed) and a returning client just re-tails the buffer — a disconnect or normal completion is not an interruption. So key the nudge off that terminal signal instead: - reduceServerEvents exposes status; hydrateMessagesFromServerEvents tags finished turns _turnComplete (events carried response.completed/failed). - reconcileTaskMessages nudges ONLY when the last turn is genuinely incomplete (trailing user with no reply, or an assistant turn with no terminal AND no content). Drops the hadStreaming-alone trigger. - The in-flight finished-refetch passes allowContinuation=false — a producer leaving the in-flight list means it finished. Auto-resume for a still-running producer is unchanged (reconnectInFlight already re-tails the buffer on open). Harness-agnostic, so this fixes both Anton and Hermes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
pnewsam
approved these changes
Jun 23, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Symptom
On new tasks (both Anton and Hermes), a second, unprompted assistant bubble sometimes appears right after a normal reply: "Looks like that turn didn't finish. Want me to take another swing at it?"
Diagnosis
That bubble is not a real second turn — it's a client-side continuation nudge (
CONTINUE_PROMPTS,App.jsx), meant for when you return to a conversation whose turn died while the app/server was down.reconcileTaskMessageswas inferring "interrupted" from weak heuristics:_streamingrow (hadStreaming), orboth of which transiently hold right as a turn completes. The in-flight poll's finished-refetch made it worse by reconciling with both guards off (
reconcileTaskMessages(enriched, false, false)), so a freshly-completed turn got nudged. New tasks hit it most: a new conversation enters and leaves the in-flight set exactly at completion.Why this contradicts the architecture
Streaming is detached — the producer runs as a background task writing to a per-turn
StreamBufferthat survives client disconnect/reopen and always ends with exactly one terminal record (completed/cancelled/error/interrupted/restart). A client disconnect or a normal completion is therefore not an interruption; a returning client just re-tails the buffer. The nudge heuristic ignored that terminal signal.Fix — key off the terminal signal, not heuristics
reduceServerEventsexposesstatus;hydrateMessagesFromServerEventstags finished turns_turnComplete(their persisted events carriedresponse.completed/failed).reconcileTaskMessagesnudges only when the last turn is genuinely incomplete — a trailing user message with no reply, or an assistant turn with no terminal and no content. ThehadStreaming-alone trigger is removed.allowContinuation=false— a producer leaving the in-flight list means it finished.Auto-resume is unchanged: a still-running producer is already re-tailed on open via
reconnectInFlight, so a genuinely running task resumes streaming with no nudge. Genuine interruptions (producer died, nothing persisted) still get the nudge.Frontend-only; harness-agnostic (fixes both Anton and Hermes).
tscclean.🤖 Generated with Claude Code