Skip to content

fix: stop dropping streamed chat events and cross-session poll writes#80

Merged
yuga-hashimoto merged 1 commit into
mainfrom
claude/chat-ai-response-ui-bugs-yhrkm0
Jul 24, 2026
Merged

fix: stop dropping streamed chat events and cross-session poll writes#80
yuga-hashimoto merged 1 commit into
mainfrom
claude/chat-ai-response-ui-bugs-yhrkm0

Conversation

@yuga-hashimoto

@yuga-hashimoto yuga-hashimoto commented Jul 24, 2026

Copy link
Copy Markdown
Owner

Why

Reported symptoms:

  1. AIの返事がリアルタイムに返ってこない — replies trickle in or lag badly.
  2. 応答を全くしない時もある(別チャットでは反応する) — some chats never show an answer.
  3. 考え中でもないのに送信ボタンが停止ボタンになる — the composer sticks on the stop button.

This branch was rebuilt from scratch on top of current main (the earlier version of this PR was
cut before ~40 commits of chat/drawer work landed and was no longer applicable). Everything below
was verified against the code as it stands on main today; the unread/working/read drawer markers
and the tool-summary card already exist there and are untouched.

Root causes and fixes

1. Dropped SSE frames — OpenCodeApiClient

singleEventStream() pushed each parsed frame with trySend, which fails silently once the
channel 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 send on an explicitly
buffered channelFlow, and a frame left in the buffer when the stream ends mid-message is now
flushed instead of lost.

2. The event stream was torn down on every health recheck

RemoteRuntimeTarget.connect() / LocalRuntimeTarget.connect() set Connecting even when the
runtime was already Connected, and RuntimeActivityRepository restarted the SSE stream on
any 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.
  • The repository keeps one stream alive across transient churn, treating Connecting as "hold" and
    only cancelling when the runtime actually leaves the connected state.

3. Cross-session corruption from the response poll

sendMessage()'s fallback poll wrote into _uiState unconditionally, targeting whatever chat was
on 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 the
failure path) is now guarded on the polled session still being the open one.

4. Stale running state on session switch

openSession() left isRunning / isThinking untouched, so opening another chat while the
previous one was mid-turn showed the new chat already stuck on the stop button.

5. Delta before its part

A message.part.delta arriving before the message.part.updated that introduces the part was
dropped outright (streamedParts[...] ?: return). It now starts the part with the delta text.

Testing

./gradlew testDebugUnitTest lintDebug221 tests pass, lint clean.

New regression tests:

  • switching sessions mid-poll does not corrupt the newly opened session — parks session A's poll
    mid-flight, opens session B, and asserts B's transcript survives intact.
  • opening a different session while the previous one is running clears the stop button
  • a brief Connecting blip does not restart the event stream
  • going disconnected then reconnecting does reopen the stream

Not verified on a device — no emulator in this environment.

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
yuga-hashimoto force-pushed the claude/chat-ai-response-ui-bugs-yhrkm0 branch from ea7d814 to 45c0d99 Compare July 24, 2026 19:18
@yuga-hashimoto yuga-hashimoto changed the title fix: deliver chat replies reliably and surface real chat state fix: stop dropping streamed chat events and cross-session poll writes Jul 24, 2026
@yuga-hashimoto
yuga-hashimoto marked this pull request as ready for review July 24, 2026 19:18
@yuga-hashimoto
yuga-hashimoto merged commit c6e5ed9 into main Jul 24, 2026
1 of 2 checks passed
@github-actions

Copy link
Copy Markdown
Contributor

API 34 emulator screenshot failure

Diagnostic logs were uploaded as the emulator-ui-diagnostics-30120258731 workflow artifact.

> Task :app:writeDebugAppMetadata
> Task :app:writeDebugSigningConfigVersions
> Task :app:preDebugAndroidTestBuild SKIPPED
> Task :app:processDebugAndroidTestManifest
> Task :app:generateDebugAndroidTestBuildConfig
> Task :app:checkDebugAndroidTestAarMetadata
> Task :app:generateDebugAndroidTestResValues
> Task :app:mapDebugAndroidTestSourceSetPaths
> Task :app:generateDebugAndroidTestResources
> Task :app:javaPreCompileDebugAndroidTest
> Task :app:mergeDebugAndroidTestShaders
> Task :app:compileDebugAndroidTestShaders NO-SOURCE
> Task :app:generateDebugAndroidTestAssets UP-TO-DATE
> Task :app:mergeDebugAndroidTestAssets
> Task :app:compressDebugAndroidTestAssets
> Task :app:desugarDebugAndroidTestFileDependencies
> Task :app:mergeDebugAndroidTestResources
> Task :app:checkDebugAndroidTestDuplicateClasses
> Task :app:processDebugAndroidTestResources
> Task :app:mergeLibDexDebugAndroidTest
> Task :app:mergeDebugAndroidTestJniLibFolders
> Task :app:mergeExtDexDebug
> Task :app:mergeExtDexDebugAndroidTest
> Task :app:mergeDebugAndroidTestNativeLibs NO-SOURCE
> Task :app:stripDebugAndroidTestDebugSymbols NO-SOURCE
> Task :app:validateSigningDebugAndroidTest
> Task :app:writeDebugAndroidTestSigningConfigVersions
> Task :app:compileDebugKotlin
w: file:///home/runner/work/opencode-android/opencode-android/app/src/main/java/com/opencode/android/feature/chat/LiveTranscriptOverlay.kt:101:44 'Send: ImageVector' is deprecated. Use the AutoMirrored version at Icons.AutoMirrored.Filled.Send
w: file:///home/runner/work/opencode-android/opencode-android/app/src/main/java/com/opencode/android/feature/search/CommandPalette.kt:98:46 'Chat: ImageVector' is deprecated. Use the AutoMirrored version at Icons.AutoMirrored.Filled.Chat
w: file:///home/runner/work/opencode-android/opencode-android/app/src/main/java/com/opencode/android/feature/settings/SettingsScreenV2.kt:188:42 'Chat: ImageVector' is deprecated. Use the AutoMirrored version at Icons.AutoMirrored.Filled.Chat
w: file:///home/runner/work/opencode-android/opencode-android/app/src/main/java/com/opencode/android/feature/settings/SettingsScreenV2.kt:203:42 'Chat: ImageVector' is deprecated. Use the AutoMirrored version at Icons.AutoMirrored.Filled.Chat
w: file:///home/runner/work/opencode-android/opencode-android/app/src/main/java/com/opencode/android/feature/settings/SettingsScreenV2.kt:210:42 'Chat: ImageVector' is deprecated. Use the AutoMirrored version at Icons.AutoMirrored.Filled.Chat
w: file:///home/runner/work/opencode-android/opencode-android/app/src/main/java/com/opencode/android/ui/AppDrawerContent.kt:88:5 Parameter 'onDeleteSession' is never used
w: file:///home/runner/work/opencode-android/opencode-android/app/src/main/java/com/opencode/android/ui/AppDrawerContent.kt:89:5 Parameter 'onArchiveSession' is never used
w: file:///home/runner/work/opencode-android/opencode-android/app/src/main/java/com/opencode/android/ui/AppDrawerContent.kt:542:5 Parameter 'isActive' is never used
w: file:///home/runner/work/opencode-android/opencode-android/app/src/main/java/com/opencode/android/ui/AppDrawerContent.kt:543:5 Parameter 'hasUnread' is never used
w: file:///home/runner/work/opencode-android/opencode-android/app/src/main/java/com/opencode/android/ui/OpenCodeApp.kt:148:5 Parameter 'appTheme' is never used
w: file:///home/runner/work/opencode-android/opencode-android/app/src/main/java/com/opencode/android/ui/OpenCodeApp.kt:149:5 Parameter 'uiFontSize' is never used
w: file:///home/runner/work/opencode-android/opencode-android/app/src/main/java/com/opencode/android/ui/components/FileTypeIcon.kt:80:48 'InsertDriveFile: ImageVector' is deprecated. Use the AutoMirrored version at Icons.AutoMirrored.Filled.InsertDriveFile
w: file:///home/runner/work/opencode-android/opencode-android/app/src/main/java/com/opencode/android/ui/theme/Theme.kt:98:43 Parameter 'codeFontSize' is never used

> Task :app:compileDebugJavaWithJavac
> Task :app:dexBuilderDebug
> Task :app:mergeDebugGlobalSynthetics
> Task :app:processDebugJavaRes
> Task :app:bundleDebugClassesToCompileJar
> Task :app:mergeProjectDexDebug
> Task :app:mergeDebugJavaResource

> Task :app:compileDebugAndroidTestKotlin FAILED
e: file:///home/runner/work/opencode-android/opencode-android/app/src/androidTest/java/com/opencode/android/ui/UiScreenshotInstrumentedTest.kt:132:29 No value passed for parameter 'workspaces'
e: file:///home/runner/work/opencode-android/opencode-android/app/src/androidTest/java/com/opencode/android/ui/UiScreenshotInstrumentedTest.kt:132:29 No value passed for parameter 'selectedWorkspacePath'
e: file:///home/runner/work/opencode-android/opencode-android/app/src/androidTest/java/com/opencode/android/ui/UiScreenshotInstrumentedTest.kt:132:29 No value passed for parameter 'onSelectProject'
e: file:///home/runner/work/opencode-android/opencode-android/app/src/androidTest/java/com/opencode/android/ui/UiScreenshotInstrumentedTest.kt:267:17 No value passed for parameter 'wakeWordEnabled'
e: file:///home/runner/work/opencode-android/opencode-android/app/src/androidTest/java/com/opencode/android/ui/UiScreenshotInstrumentedTest.kt:267:17 No value passed for parameter 'onWakeWordChange'

> Task :app:packageDebug
> Task :app:createDebugApkListingFileRedirect
gradle/actions: Writing build results to /home/runner/work/_temp/.gradle-actions/build-results/emulator-1784920851895.json

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':app:compileDebugAndroidTestKotlin'.
> A failure occurred while executing org.jetbrains.kotlin.compilerRunner.GradleCompilerRunnerWithWorkers$GradleKotlinCompilerWorkAction
   > Compilation error. See log for more details

* Try:
> Run with --stacktrace option to get the stack trace.
> Run with --info or --debug option to get more log output.
> Run with --scan to get full insights.
> Get more help at https://help.gradle.org.

BUILD FAILED in 14m 30s
59 actionable tasks: 59 executed
OK: killing emulator, bye bye

OK

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants