Repository navigation
Conversation
… resync, and reconnect backoff The event stream client only reconnected when the streaming fetch rejected. A half-open connection (suspended tab, proxy/NAT drop without RST) delivers no events and no error, so the UI stayed stale until a hard refresh — the failure class reported across anomalyco#39030, anomalyco#47258, anomalyco#45860 and anomalyco#51857. - Add a stall watchdog armed per event: the server emits a heartbeat every 10s, so if nothing arrives within 45s the connection is dead in practice — abort and resync instead of waiting forever. - Force a resync on visibilitychange->visible and window online events, matching the foreground-recovery behavior that anomalyco#47571 had before the v2 client refactor removed it. - Always restart the stream on pageshow: an open streaming fetch makes the page ineligible for bfcache, so the previous persisted-only check left the stream stopped after ordinary tab returns. - Back off reconnects exponentially (250ms -> 10s + jitter) when attempts keep dropping fast, instead of a fixed 250ms loop that aborted each re-bootstrap with the next reconnect (anomalyco#48014). Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
|
The following comment was made by an LLM, it may be inaccurate: |
|
cc @thdxr @adamdotdevin @kitlangton — requesting review when you have a moment. Why this PR: the web UI's SSE client had no liveness detection after the v2 refactor removed the stall watchdog that #47571 introduced — a half-open connection leaves the app stale until a hard refresh (daily pain behind reverse proxies; tracked in #39030, #47258, #45860, #51857). This restores that watchdog in Scope is deliberately narrow: one file's recovery loop + its test file; no server changes, no contract changes — it consumes the existing Verified locally: 14/14 unit tests pass, tsgo typecheck clean, no new lint warnings. Happy to split the backoff into a separate PR if you'd rather land #48015 first — the two changes touch the same loop, so whichever merges second will need a small rebase either way. |
Why this fix mattersThis is currently the single largest source of "dead" web UI reports. The failure class this PR closes — an SSE stream that dies silently and never recovers — accounts for a long chain of user-visible reports, and it keeps recurring because every previous fix addressed only one symptom instead of stream liveness itself:
Impact it removes:
Cost/risk of the change is minimal: ~75 lines in one file, no server or API contract changes, driven entirely by events already on the wire. The unit tests cover the new invariants ( If it helps review: the watchdog + backoff + resync are separable — happy to split if that gets parts merged faster. |
|
cc @thdxr @adamdotdevin — adding implementation detail for reviewers. Exact behavior changes in
Why heartbeat-based detection is safe: the server emits 14/14 unit tests pass (new: delay growth 250→500→1000→2000, 10s cap, jitter bounds, stall>3 heartbeats); typecheck and lint clean. |
|
Reviewed the diff in detail — the recovery semantics are right and the scope discipline (one file, no contract changes, reuses the existing 10s Consider — let lastEventAt = 0
// inside the for-await loop, next to armStallWatchdog():
lastEventAt = Date.now()
makeEventListener(document, "visibilitychange", () => {
if (document.visibilityState !== "visible") return
if (Date.now() - lastEventAt < STREAM_STALL_TIMEOUT_MS) return
resync("Page returned to the foreground")
})
Nit — only the pure FYI — Verdict: approve modulo the resync gating — that's the only behavioral surprise I'd want addressed or explicitly justified. cc @Hona @Brendonovich — could you review and approve when you get a chance? This closes the largest remaining "dead UI" failure class (SSE half-open after tab suspend / proxy drop), restoring the watchdog semantics #47571 had before the v2 refactor. |
Issue for this PR
Type of change
What does this PR do?
The web UI's event stream client (
packages/app/src/context/server-sdk.tsx) only recovered when the streaming fetch rejected. When the underlying TCP connection dies silently — tab suspended by the OS/browser, proxy or NAT drop without RST, tunnel restart —for awaitnever resolves and never throws, so the UI goes stale untilCtrl+F5. The server already emitsserver.heartbeatevery 10s (packages/opencode/src/server/routes/instance/httpapi/handlers/event.ts); the client just never used it.This restores the liveness semantics that PR #47571 had in
packages/client/src/solid/connection.ts(deleted in the v2 client refactor) inside the current event path:STREAM_STALL_TIMEOUT_MS(45s = >3× the 10s heartbeat), the attempt is aborted and the loop resyncs. A stalled stream logs"event stream stalled"once instead of silently hanging.visibilitychange→document.visibilityState === "visible"andwindow.onlinenow forceresync(), which aborts the current attempt so the loop resubscribes immediately; the server'sserver.connectedthen drives the existing full re-bootstrap inserver-sync.tsx.pageshowalways restarts —pagehidecallsstop(), butresumeStreamAfterPageShowonly restarted whenevent.persistedwas true. An open streaming fetch makes the page ineligible for bfcache, sopersistedis almost alwaysfalseand the stream stayed dead after ordinary tab returns.resumeStreamAfterPageShowis removed;pageshownow callsstart()directly (idempotent).STABLE_CONNECTION_MS(3s) back off exponentially from 250ms to 10s plus jitter, instead of a fixed 250ms reconnect loop. Each reconnect emitsserver.connected→ full re-bootstrap; without backoff, a flapping connection self-inflicted aborted bootstraps (visible as repeated HTTP 499s).How did you verify your code works?
bun test --conditions=solid --preload ./happydom.ts ./src/context/server-sdk.test.ts— 14 pass / 0 fail (new tests coverreconnectDelayexponential growth, the 10s cap, bounded jitter, and the stall-timeout-vs-heartbeat invariant).bun typecheckinpackages/app(tsgo) — clean.bunx oxlinton touched files — 0 errors, no new warnings (15 warnings vs 17 at baseline; the removedresumeStreamAfterPageShowcode carried two).Screenshots / screen recording
N/A — recovery logic, no visual change. (A connection-health surface to show these states is proposed separately in #51860.)
Checklist
reconnectDelay, stall timeout invariant)coalesceServerEvents/enqueueServerEvent/adaptServerEventtests still pass — event ordering and delta coalescing unchangedserver.connected/server.heartbeatevents only