Skip to content

fix(app): reconnect the event stream after the server restarts - #227

Closed
kateebonner wants to merge 1 commit into
local/amicodefrom
kate/sse-reconnect-after-restart
Closed

fix(app): reconnect the event stream after the server restarts#227
kateebonner wants to merge 1 commit into
local/amicodefrom
kate/sse-reconnect-after-restart

Conversation

@kateebonner

@kateebonner kateebonner commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

The bug

After the opencode server restarts, the app never reconnects. It stays disconnected from a server that is already back, indefinitely — only a page reload recovers it.

Traced in a real browser against a real restart:

+ 1.9s  loop iteration start
+ 1.9s  stream obtained -> CONNECTED
+10.8s  === server killed ===
+18.2s  === server listening again ===
+18.2s  onSseError: TypeError: network error
+18.2s  onSseError: TypeError: Failed to fetch
+53.2s  === end ===

No catch. No loop tail. No second loop iteration start. The retry never runs.

Why

The v1 event stream reports failures through its onSseError callback and then simply stops yielding — the async iterator neither throws nor completes. The reconnect loop only comes round when that iterator ends, so it parks inside for await forever. RECONNECT_DELAY_MS is 250ms and it never gets to use it.

onSseError set streamStatus = "disconnected" and returned, which is why the symptom is a permanently disconnected client rather than a crash.

The fix

Abort the attempt from onSseError. That ends the iterator, control reaches the catch and the loop tail, and the next iteration reconnects.

+17.6s  onSseError -> abort
+17.6s  loop iteration start          ← now happens
+17.6s  stream obtained -> CONNECTED
+17.9s  loop iteration start          ← one flap while the server finishes booting
+17.9s  stream obtained -> CONNECTED  ← settles; still connected 35s later

The abort is extracted into applySseError() and covered by tests. On its own it reads as redundant next to the disconnect right above it — which is exactly how it would get tidied away again. The early return for an already-closed stream is what keeps our own abort from recursing.

Why this matters more than it looks

This is not a rare edge. #221 made the solver toggle restart the opencode server by design, so every solver switch has been leaving the webview dead until reload.

It is also the same shape as #132's stuck reconnecting banner. I suspect it is the real reason ConnectionBanner was unmounted in f696388/a03aa04 rather than fixed — the banner was reporting this bug accurately and looked broken for it.

Found while smoke-testing #225 (the solver-switch banner), which could reach restarting but never ready. With this fix the full sequence lands:

t= 0s  requested   "Switching to Piccolo…"
t= 1s  restarting  "Restarting session server…"
t= 7s  server up
t=10s  ready       "Piccolo ready"
t=13s  cleared

Verification

  • bun turbo typecheck — 30/30
  • oxlint on the changed files — 0 errors
  • packages/app unit — 917 pass / 1 fail; the failure is i18n parity, pre-existing and unrelated
  • Browser-verified against a real server restart, traces above

Refs #132. Unblocks #225.

Summary by CodeRabbit

  • Bug Fixes
    • Improved handling of server-sent event connection errors.
    • Active streams now disconnect and abort cleanly when failures occur.
    • Closed streams continue to be ignored, preventing unnecessary actions.
    • Repeated connection failures are handled consistently.

The v1 event stream reports failures through its `onSseError` callback and
then simply stops yielding — the async iterator neither throws nor completes.
The reconnect loop only comes round when that iterator ends, so it parked
inside `for await` forever: the catch, the loop tail and the 250ms retry were
never reached, and the client stayed disconnected from a server that was
already back. Only a page reload recovered it.

Traced in the browser against a real restart. Before:

  +1.9s  loop iteration start
  +1.9s  stream obtained -> CONNECTED
  +18.2s onSseError: TypeError: network error
  +53.2s (end — no catch, no loop tail, no retry, still disconnected 35s
          after the server was listening again)

After:

  +17.6s onSseError -> abort
  +17.6s loop iteration start
  +17.6s stream obtained -> CONNECTED

This is not a rare edge. #221 made the solver toggle restart the opencode
server by design, so every solver switch left the webview dead until reload —
and it is the same shape as opencode#132's stuck reconnecting banner, which is
very likely why ConnectionBanner was unmounted in f696388 rather than fixed.

The abort is extracted into applySseError() and covered by tests: on its own it
reads as redundant next to the disconnect, which is exactly how it would get
tidied away again. The early return for an already-closed stream is what stops
our own abort from recursing.

Refs #132.
@coderabbitai

coderabbitai Bot commented Aug 21, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d8403f79-737a-414b-89d0-ed2ef910208d

📥 Commits

Reviewing files that changed from the base of the PR and between 97db956 and c00d466.

📒 Files selected for processing (2)
  • packages/app/src/context/server-sdk.test.ts
  • packages/app/src/context/server-sdk.tsx

Included review availability: Your plan provides up to 10 included reviews per hour; 9 remain after this review.


📝 Walkthrough

Walkthrough

The change adds applySseError for active SSE failures, updates onSseError to use it, and tests stream disconnection, abortion, closed-stream filtering, and repeated failures.

Changes

SSE error handling

Layer / File(s) Summary
SSE error helper
packages/app/src/context/server-sdk.tsx
applySseError ignores closed streams. It disconnects and aborts active streams and returns whether the error was real.
Error integration and validation
packages/app/src/context/server-sdk.tsx, packages/app/src/context/server-sdk.test.ts
onSseError delegates to applySseError and preserves non-real error filtering and logging. Tests cover active, closed, and repeated failures.

Estimated code review effort: 2 (Simple) | ~10 minutes

Merge Risk: ⚪ Minimal · up to c00d4

The reconnect fix is localized and tested, and no actionable merge-blocking risk remains at the current head.

Suggested reviewers: brendonovich, hona

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 3 functions across 2 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the primary change: restoring event-stream reconnection after the server restarts.
Description check ✅ Passed The description clearly explains the bug, root cause, fix, impact, and verification, but omits the template checklist and change-type section.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch kate/sse-reconnect-after-restart

Comment @coderabbitai help to get the list of available commands.

@kateebonner

Copy link
Copy Markdown
Contributor Author

Folded into #228 — same two commits, one PR. Closing this to keep review in one place.

@kateebonner
kateebonner deleted the kate/sse-reconnect-after-restart branch August 21, 2026 20:03
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