Fix SSE live updates for open conversations (#14) - #36
Open
Melloss wants to merge 1 commit into
Open
Conversation
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.
Summary
Fixes #14 — "the app isn't refreshing / i have to get out of the
conversation and enter again to see it."
Root cause: leaving and re-entering a chat screen "fixed" it only
because it constructs a new
ChatViewModel, which does a one-shot RESTrefetch. The actual SSE live-update pipeline was never being repaired —
it stayed broken, so the conversation went stale again shortly after.
Three independent, compounding defects in the SSE/session layer:
Unguarded exception silently kills live updates.
SessionRepositoryProvider.collectWorkspaceEventscalledrepository.acceptEvent(event)inside acollect {}with notry/catch. Since the collector runs under a
SupervisorJob, onethrowing event permanently and silently terminated only that
workspace's event job — no log, no crash, no UI signal, and it was
never recreated by chat-screen navigation (only by closing the tab).
Zombie SSE sockets go undetected.
OpenCodeEventSourceusedreadTimeout(0), so a socket that died silently (NAT/proxy idle-kill,Doze-related network changes) never fired
onError/onClosed.connectionStatestayedConnectedindefinitely with zero eventsflowing, and nothing checked for it.
Reconnect self-heal was dead code, and incomplete even when live.
The synthetic
OpenCodeEvent.Connectednever reached theper-workspace fan-out (it has no
directory, so it never enteredConnectionManager.scopedEvents), meaningSessionRepositoryImpl.acceptEvent's reconnect → re-hydrate path(
hydrateAfterReconnect()) never fired in production. Even when itdid fire it only rebuilt the session/project snapshot — it never
refreshed a conversation's messages.
Changes
SessionRepositoryProvider.kt— wrapacceptEventin try/catch(rethrows
CancellationException, logs everything else) so one badevent can't kill the collector. Also broadcasts a synthetic
Connectedevent to every live workspace repository (filtered byconnection generation) on each non-Connected → Connected transition,
since the original per-directory routing could never reach it.
OpenCodeEventSource.kt— liveness watchdog: tracks the last receivedframe (message or heartbeat) and forces
reconnect()if the socketclaims
Connectedbut has been silent > 60s. Also fixes a pre-existingleak where
shutdown()didn't cancel the event-pump scope.SessionRepositoryImpl.kt— on everyConnectedevent, re-fetchesmessages (via the existing
loadMessagesREST path) for everyactively-observed session, independent of the one-shot snapshot-hydrate
guard (
inFlight), so it runs on every reconnect, not just the first.docs/design-locks/B-sse-hydrate-race.md— extends item 11 with a newitem 12 + worked examples documenting that reconnect self-heal now
covers message state, not just the session/project snapshot.
Test plan
./gradlew :app:testDebugUnitTest— full suite passes, includingnew regression tests:
events (
SessionRepositoryProviderTest)(
SessionRepositoryProviderTest— this one initially caught a bugin my own first attempt, where the refresh was accidentally gated
behind the one-shot hydrate guard)
OpenCodeEventSourceTest)./gradlew :app:detekt— clean./scripts/check_theme_violations.sh— cleanwas open, sent a message from the desktop TUI, restarted the server —
the open conversation on the phone caught up without navigating away
and back.