Skip to content

fix(desktop): reconnect the IPC push relay with backoff - #1773

Merged
chuks-qua merged 6 commits into
mainfrom
feat/ipc-relay-reconnect
Sep 29, 2026
Merged

chuks-qua merged 6 commits into
mainfrom
feat/ipc-relay-reconnect

Conversation

@chuks-qua

@chuks-qua chuks-qua commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

What

The Electron main process owns a net.Socket to the server's IPC push endpoint and relays frames to the renderer. Previously it opened one socket and never recovered: a dropped connection sent ipc-push-disconnect once, the renderer un-suppressed channels and fell back to WebSocket, and the fast path stayed dead for the rest of the session.

The relay now reconnects in the main process with exponential backoff (500 ms doubling to a 15 s ceiling), resets the delay on a successful connect, and stops reconnecting when the window is destroyed or the relay is torn down. No reconnect handshake is needed: the renderer re-suppresses push channels as frames resume, and the WebSocket path covers the gap.

Also removes isThreadRunning from the thread store. The selector has no callers; consumers read runningThreadIds directly.

Verified with the ipc-relay suite (19 tests), including new coverage for backoff doubling, reset on connect, and no reconnect after cleanup or window destruction. tsc --noEmit is clean in apps/desktop and apps/web.

Why

Part of the stale-streaming reliability program in docs/plans/canonical-runtime-state-migration.md. A dead IPC relay silently degraded desktop push delivery; combined with the reactive-only WebSocket recovery ladder, silent transport death was the class of failure that left threads stuck showing streaming.

UI Changes

None.

Config Changes

None.

Review Notes

Stacked on #1772. The reconnect backoff bounds (500 ms, 15 s ceiling) are new constants in ipc-relay.ts.


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

A half-open socket answers nothing and never fires close, so every
recovery mechanism (reconnect, hydration, subscription replay) waited
forever while the UI showed stale streaming state. A 30s interval now
issues the existing app.version RPC with a 10s deadline; an unanswered
probe closes the socket locally and the established onclose ladder owns
recovery. agent.send gains a 20s timeout so a composer submit cannot
park forever on a dead socket.

The wedged-socket test now answers the liveness probe, since a socket
that ignores it is exactly what the watchdog closes.
The reducer now writes routing.executionId onto the stored turn so
clients can correlate a canonical turn to the local runtime without
threading the event envelope through state. A payload-supplied
executionId that disagrees with routing is rejected as a routing
conflict.
Composer, project tree, and running-thread indicators now follow the
canonical replica once a turn correlates by execution identity, so a
dropped legacy terminal event can no longer strand runningThreadIds.
The optimistic window before turnStarted and the finalizing phase stay
on the legacy path until a canonical turn proves correlation. The
narrative projection keeps its existing child-only lifecycle gate.
The relay opened one socket and never recovered: a dropped connection
silently degraded the renderer to WebSocket-only delivery for the rest
of the session. Reconnect in the main process with bounded exponential
backoff, reset on successful connect, and stop when the window dies or
the relay is torn down. The renderer re-suppresses push channels as
frames resume, so no reconnect handshake is needed.
No caller remains: consumers read runningThreadIds directly.
Base automatically changed from feat/canonical-runtime-phase to main September 29, 2026 20:37
…hase

The runtime reconcile stamps runtimePhase from a correlated canonical turn
but never correlated the record's execution identity, so a canonically owned
"running" phase made getCanonicalLifecycleTurn treat the provider-owned child
turn as a local execution and suppress its projection. A later terminal
canonical event could then no longer clear the stamped phase either.

reconcileCanonicalRuntime now stamps turnExecutionId from the claiming turn's
executionId on live claims, and the lifecycle gate only treats runtimePhase
as locally owned when the canonical runtime turn is not the latest turn.
@chuks-qua
chuks-qua merged commit dcc3ffd into main Sep 29, 2026
6 checks passed
@chuks-qua
chuks-qua deleted the feat/ipc-relay-reconnect branch September 29, 2026 20:47
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.

1 participant