fix: deliver chat events, end runs on time, and show real chat state#83
Merged
Merged
Conversation
The previous round of fixes shipped in v0.3.7/v0.3.8 and the symptoms stayed. Digging into what actually happens at runtime turned up four different causes, none of which the earlier work touched. Events never reached the app. The stream only opened once a runtime reported Connected, but every REST call — sending a prompt, loading a transcript — works regardless of that flag. A runtime whose state never reached Connected therefore looked completely functional while silently delivering no events at all: no live reply, no drawer activity, no idle notification. The stream now follows the selected runtime and relies on its own retry, which is both simpler and strictly more robust. Runs never ended. The response poll asked the session object for a completion time, but sessions do not carry one — only assistant messages do — so completion was never detected and the composer sat on the stop button, still showing "thinking", until the two minute timeout expired. Completion is now read off the transcript. Drawer state was event-only. Active chats came purely from stream events, so with no events every chat sat on the grey idle dot even while plainly working. A chat now reports its own run, and unread markers are persisted so a chat that finished while the app was closed is still unread on the next launch. Markers did not match what was asked for: working now shows a spinner rather than a static green dot, and finished-but-unread shows a filled blue dot rather than a hollow ring. Also fixed, both pre-existing on main and both blocking CI: - ChatViewModel started an unbounded 30s connection probe from init. Under a virtual test clock that loop never ends, so every chat test hung and the suite could not run — which is a large part of why chat regressions kept shipping. The probe is now opt-in and enabled by the app; the suite runs in seconds again. - The quick input widget declared an EditText, which RemoteViews cannot inflate, so the widget could not render. It is a tap target that opens the input activity, so it is now a TextView. - Six locales were missing three strings. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RLvEkDwiWT8R4S23gdPsSB
spotlessCheck flagged kotlinx.coroutines.Job as unused after the stream lifecycle stopped tracking a job handle. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RLvEkDwiWT8R4S23gdPsSB
Contributor
API 34 emulator screenshot failureDiagnostic logs were uploaded as the |
…ures The androidTest source set is only compiled by the emulator capture job, so it silently rotted when AppDrawerContent gained project selection and VoiceSettingsScreen gained the wake-word toggle. The capture job has been failing to compile on main since; this restores it and exercises the new running and unread drawer markers while it is there. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RLvEkDwiWT8R4S23gdPsSB
The jacoco plugin only attaches a report task to the JVM `test` task, which an Android module does not have, so `:app:jacocoTestReport` never existed and CI's coverage step failed on every run. Registers a report over the debug unit tests, writing where the workflow expects it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RLvEkDwiWT8R4S23gdPsSB
Scanning the whole build directory for exec files made Gradle treat every other task's output as an undeclared input, so the report failed as soon as a release variant was assembled in the same invocation — which is exactly what CI does. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RLvEkDwiWT8R4S23gdPsSB
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
The previous round (#80, shipped in v0.3.7/v0.3.8) did not fix the reported symptoms. This time the
diagnosis started from the running app rather than from reading the code.
In a screenshot of a finished turn, the reply had arrived, yet "処理中…", "考え中…" and the stop
button were all still showing — and every chat in the drawer was on the grey idle dot, including the
one that was running.
isThinkingis only ever cleared byupdateStreamingMessage, which only runs on amessage.part.updatedevent. It was still set, so no stream event had reached the app at all;the visible reply had come from the 3-second fallback poll. That pinned the real problem, and
pulling on it turned up four separate causes.
Root causes
1. The event stream never opened
RuntimeActivityRepositoryonly opened the stream once the runtime reportedRuntimeState.Connected. But every REST call — sending a prompt, loading a transcript — worksregardless of that flag. A runtime whose state never reached
Connectedtherefore lookedcompletely functional while silently delivering no events at all: no live reply, no drawer
activity, no idle notification.
The stream now follows the selected runtime and leans on its own retry/backoff for reconnection,
which is simpler and strictly more robust than gating on a second, separately-maintained flag.
2. Runs never ended
The response poll asked
session(id).time.completedwhether the turn was done. Sessions do notcarry a completion time — only assistant messages do — so it was always null, completion was
never detected, and the composer sat on the stop button showing "考え中" until the 120s timeout
expired. That is exactly the screenshot.
Completion is now read off the transcript: the newest message being a finished assistant turn,
requiring a reply that did not exist before this prompt so the previous turn's completion cannot
end the new one instantly.
3. Drawer state was event-only
activeSessionIdswas populated purely from stream events, so with no events every chat stayed onthe idle dot. A chat now reports its own run state, and unread markers are persisted, so a chat
that finished while the app was closed is still unread on the next launch.
4. The markers were not what was asked for
Also fixed — pre-existing on main, both blocking CI
ChatViewModel.initstarted an unbounded 30-secondconnection probe (
while (isActive) { probe(); delay(30s) }, added in212afdf). Under a virtualtest clock that loop never ends, so
advanceUntilIdle()spun forever and every chat test hung— verified by running
ChatViewModelQuestionTeston cleanmain, where it is killed at a 420stimeout. This is a large part of why chat regressions kept shipping unnoticed. The probe is now
opt-in and enabled by the app; the suite completes in ~15s again.
EditText, whichRemoteViewscannot inflate (RemoteViewLayoutlint error onmain). It is a tap target thatopens the input activity, so it is now a
TextView.MissingTranslationlint error onmain).Testing
./gradlew :app:testDebugUnitTest :app:lintDebug— 245 tests pass, lint clean.Before this branch, that same command fails on
main: the test task hangs, and lint reports the twoerrors above.
New coverage: the stream opens without a connected state; runtime-state churn never restarts it;
losing the runtime clears activity but keeps unread markers; a chat's own run drives the drawer;
unread markers survive a restart; and the wire-format decoding the live path depends on
(
sessionID/messageID/partID, tool state, and assistanttime.completed) is pinned against amock server.
Not verified on a device — no emulator in this environment.