fix: stop dropping streamed chat events and cross-session poll writes#80
Merged
Merged
Conversation
Four independent defects behind "replies arrive late / not at all / the stop button sticks": - The SSE reader pushed parsed frames with trySend, which fails silently once the channel is full. During a fast reply the collector falls behind and whole chunks of the answer were discarded. It now suspends on a buffered channel, and flushes a frame left over when the stream ends mid-message. - connect() dropped an already-connected runtime back to Connecting on every health recheck, and the activity repository restarted the SSE stream on any runtime-state emission. Each catalog refresh therefore tore down the live stream mid-reply. Health rechecks now keep a connected runtime connected, and the stream survives transient churn, stopping only when the runtime actually goes away. - The send-side response poll wrote into whatever chat happened to be on screen. Switching sessions while a turn was in flight let the old session's poll overwrite the new session's transcript and clear its running state. Every write is now guarded on the target session still being the open one. - openSession kept the previous chat's running state, so opening another chat mid-turn showed the composer already stuck on the stop button. Also: a text delta arriving before the part event that introduces it was dropped; it now starts the part instead. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RLvEkDwiWT8R4S23gdPsSB
yuga-hashimoto
force-pushed
the
claude/chat-ai-response-ui-bugs-yhrkm0
branch
from
July 24, 2026 19:18
ea7d814 to
45c0d99
Compare
yuga-hashimoto
marked this pull request as ready for review
July 24, 2026 19:18
Contributor
API 34 emulator screenshot failureDiagnostic logs were uploaded as the |
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.
Why
Reported symptoms:
This branch was rebuilt from scratch on top of current
main(the earlier version of this PR wascut before ~40 commits of chat/drawer work landed and was no longer applicable). Everything below
was verified against the code as it stands on
maintoday; the unread/working/read drawer markersand the tool-summary card already exist there and are untouched.
Root causes and fixes
1. Dropped SSE frames —
OpenCodeApiClientsingleEventStream()pushed each parsed frame withtrySend, which fails silently once thechannel buffer is full. During a fast reply the collector falls behind and whole chunks of the
answer are discarded with no error anywhere. Switched to a suspending
sendon an explicitlybuffered
channelFlow, and a frame left in the buffer when the stream ends mid-message is nowflushed instead of lost.
2. The event stream was torn down on every health recheck
RemoteRuntimeTarget.connect()/LocalRuntimeTarget.connect()setConnectingeven when theruntime was already
Connected, andRuntimeActivityRepositoryrestarted the SSE stream onany runtime-state emission. Every catalog refresh therefore killed the live stream mid-reply and
reopened it, losing everything in between.
connect()no longer downgrades a connected runtime while re-checking health.Connectingas "hold" andonly cancelling when the runtime actually leaves the connected state.
3. Cross-session corruption from the response poll
sendMessage()'s fallback poll wrote into_uiStateunconditionally, targeting whatever chat wason screen at the time the write landed — not the chat it was polling for. Switching sessions while
a turn was in flight let the old session's poll overwrite the new session's transcript and clear
its running state. Every write in the poll (message list,
isRunning, the final reconcile, and thefailure path) is now guarded on the polled session still being the open one.
4. Stale running state on session switch
openSession()leftisRunning/isThinkinguntouched, so opening another chat while theprevious one was mid-turn showed the new chat already stuck on the stop button.
5. Delta before its part
A
message.part.deltaarriving before themessage.part.updatedthat introduces the part wasdropped outright (
streamedParts[...] ?: return). It now starts the part with the delta text.Testing
./gradlew testDebugUnitTest lintDebug— 221 tests pass, lint clean.New regression tests:
switching sessions mid-poll does not corrupt the newly opened session— parks session A's pollmid-flight, opens session B, and asserts B's transcript survives intact.
opening a different session while the previous one is running clears the stop buttona brief Connecting blip does not restart the event streamgoing disconnected then reconnecting does reopen the streamNot verified on a device — no emulator in this environment.