fix(web): SSE resilience + specs Dialog.Portal wrapper #247
No reviewers
Labels
No labels
area:agents
area:dashboard
area:database
area:design
area:design-review
area:flows
area:infra
area:meta
area:security
area:sessions
area:webhook
area:workdir
security
type:bug
type:chore
type:meta
type:user-story
No milestone
No project
No assignees
3 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
charles/claude-hooks!247
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "fix/sse-reliability-and-dialog-portal"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Three in-session fixes landed live earlier today via hot-edits but were never committed — consolidating so a cold restart picks them up from source.
apps/server/src/main.ts— disable Bun's default 10 sidleTimeouton/eventsand/architect/stream/*viasrv.timeout(req, 0). Without it the server silently killed every SSE stream at ~10 s (heartbeat is a server-side write; Bun's idle timer only resets on client bytes, which EventSource never sends). Root cause of the "reconnecting for 122s" banner seen on 2026-04-21.apps/web/src/lib/sse.ts— add an explicit 3 s reconnect timer in theuseSSEhook. Firefox's native EventSource auto-retry is unreliable; on a graceful TCP close (rather than a network error) it treats the stream as finished and never reconnects. Our timer closes the old stream and opens a new one deterministically.apps/web/src/routes/specs.tsx— wrapDialog.Backdrop+Dialog.PopupinDialog.Portal. Base UI's Dialog composition requires the Portal wrapper; without it the/app/specsroute threw "Base UI error #26" on first render. Matches the pattern already used inissue-card.tsx.Test plan
bun run --cwd apps/server typecheckcleanbun run --cwd apps/web typecheckcleancurl -sN --max-time 60 https://claude.jacquin.app/eventsholds the full 60 s (confirmed 2026-04-21)/app/specsrenders without Base UI error #26 (confirmed 2026-04-21)🤖 Generated with Claude Code
Review — Round 1
CI: ✅ green (run #1907, 5m30s)
Three focused bug fixes — all correct.
apps/server/src/main.ts—srv.timeout(req, 0)The root cause analysis is right: Bun's idle timer resets on client-side bytes only, and EventSource/
curlnever sends bytes back. Callingsrv.timeout(req, 0)on the originalRequestobject (beforehandleRequestreconstructs it) is the only place this works correctly per Bun's API contract. The/architect/stream/*path is included too — good catch, same problem would apply there.apps/web/src/lib/sse.ts— explicit reconnect timerThe 3 s timer correctly handles the Firefox graceful-close case without interfering with the existing debounce/hard-fail logic:
reconnectTimerRef.currentbefore callingconnect(), so a freshonerrorfrom the new stream arms a new 3 s timer — the retry loop is perpetual and self-resetting.if (offlineSinceRef.current !== null)guards the timer callback from firing afteronopenalready cleared the offline state — no spurious reconnects.clearTimers()inonopenis idempotent whether the timer already fired or not.One minor observation (not a blocker): after
connect()is called from the reconnect timer, if the old stream's.close()races with anonerrorcallback, theonerrorwould seereconnectTimerRef.current === null(already cleared) and arm another 3 s timer in parallel. In practice,EventSource.close()does not fireonerrorin any major browser, so this race is theoretical. The code is safe as-is.apps/web/src/routes/specs.tsx—Dialog.PortalwrapperWrapping
Dialog.Backdrop+Dialog.PopupinDialog.Portalis exactly what Base UI requires for its Dialog composition (error #26 is the missing-portal error). Pattern matchesissue-card.tsx. Nothing else changed in the component logic.