Skip to content

refactor: State Socket — mux session-state + host-metrics SSE into one WebSocket#375

Merged
sahil-noon merged 4 commits into
sockets-v2from
260716-qf3j-state-socket
Jul 17, 2026
Merged

refactor: State Socket — mux session-state + host-metrics SSE into one WebSocket#375
sahil-noon merged 4 commits into
sockets-v2from
260716-qf3j-state-socket

Conversation

@sahil-noon

Copy link
Copy Markdown
Collaborator

Meta

Change ID Type Confidence Plan Review
qf3j refactor 4.8/5.0 15/15 tasks, 23/23 acceptance ✓ ✓ 3 cycles
Impact +/− Net
raw +3393 / −1540 +1853
true +3013 / −1492 +1521
└ impl +2007 / −1166 +841
└ tests +1006 / −326 +680

excludes fab/, docs/ · generated by fab-kit v2.15.5

Pipeline: intake ✓ → apply ✓ → review ✓ → hydrate ✓ → ship → review-pr

Summary

Every attached tmux server currently costs one session-state EventSource plus one host-metrics EventSource, and SSE holds an HTTP/1.1 pool slot in every browser engine (cap 6 per origin) — on plaintext origins a 5-server host saturates the pool from SSE alone, and on Firefox/WebKit a saturated pool blocks new WebSocket handshakes entirely, so no terminal relay can ever connect. This change muxes session-state + host-metrics onto a single /ws/state WebSocket (established WS holds no pool slot in any engine), with in-band subscribe/ack ordering that also structurally fixes the long-standing attachServer→SSE race (multi-server-sidebar:70 now passes deterministically). Change 1 of 3 in the socket-unification plan (fab/plans/sahil/socket-unification.md); terminal relays and chat SSE are untouched (Changes 2 and 3).

Changes

  • Backend — new /ws/state endpoint (gorilla/websocket): JSON-envelope protocol — hello/subscribe/unsubscribe/preview-scope ops, ack+snapshot / event / gone / error frames; server-key validation (ValidateServerName) on every op; cached global slots replayed once after hello.
  • Backend — sseHub refactor (api/sse.go): sseClient generalized to a per-connection subscription set with per-subscription routing; producers unchanged; SSE event names/payloads move verbatim into the envelope (byte-equality unit-tested); dead-server reap now emits an in-band gone event; GET /api/sessions/stream + handleSSE retired; POST /api/preview-scope stays.
  • Frontend — session-context.tsx transport swap: one StateSocket per tab (src/lib/state-socket.ts — hello, ref-counted subscribe, 1s→15s backoff, resubscribe-on-reconnect); attachServer and all subscribe* seams keep exact signatures; connection dots = socket-connected AND subscription-acked; StrictMode-safe across remounts; /ws added to the Vite dev proxy.
  • Tests: hub payload byte-equality suite; new connection-budget e2e guard (zero EventSources, exactly one live /ws/state WS on host/server/terminal/board routes); 12 e2e specs migrated to a shared WS mock with .spec.md companions updated.

sahil87 added 2 commits July 16, 2026 20:46
Replace the per-server EventSource pool and host-metrics EventSource with
a single /ws/state connection (hello/subscribe/ack protocol), eliminating
steady-state h1 pool usage that was blocking WebSocket handshakes on
Firefox/WebKit once 6 SSE connections saturated the pool. Retires
GET /api/sessions/stream; SSE event names/payloads move verbatim into the
new envelope, and attachServer/subscribe* consumer seams are unchanged.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Refactors the “state” transport to eliminate per-server SSE fan-out by multiplexing session-state and host-metrics over a single /ws/state WebSocket, updating both backend hub routing and frontend consumers, and migrating tests/docs accordingly.

Changes:

  • Backend: add /ws/state WebSocket endpoint + adapt the existing hub to route events via per-connection subscription sets; retire GET /api/sessions/stream.
  • Frontend: introduce a per-tab StateSocket client and swap session-context wiring from EventSource to WebSocket; add Vite dev proxy for /ws.
  • Tests/docs: migrate e2e/unit tests from SSE to state-socket mocks, and add an e2e “connection budget” guard.

Reviewed changes

Copilot reviewed 49 out of 49 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
fab/changes/260716-qf3j-state-socket/plan.md Change plan and acceptance criteria for the state-socket refactor.
fab/changes/260716-qf3j-state-socket/.status.yaml Updates change tracking/status metadata for the rollout.
fab/changes/260716-qf3j-state-socket/.history.jsonl Records stage-transition history for the change.
docs/memory/run-kit/tmux-sessions.md Updates tmux/SSE lifecycle documentation to reference state-socket + gone.
docs/memory/run-kit/index.md Updates memory index/architecture references to the new state socket.
app/frontend/vite.config.ts Adds /ws WebSocket proxy support for dev/test-e2e.
app/frontend/tests/e2e/top-bar-refresh.spec.ts Migrates mocked terminal-route spec from SSE to state-socket mock.
app/frontend/tests/e2e/top-bar-refresh.spec.md Updates companion doc to describe state-socket mocking.
app/frontend/tests/e2e/top-bar-persistence.spec.ts Migrates mocked persistence spec from SSE to state-socket mock.
app/frontend/tests/e2e/top-bar-persistence.spec.md Updates companion doc to describe state-socket mocking.
app/frontend/tests/e2e/status-dot-tip.spec.ts Migrates status-dot-tip spec from SSE to state-socket mock.
app/frontend/tests/e2e/status-dot-tip.spec.md Updates companion doc to describe state-socket mocking.
app/frontend/tests/e2e/spawn-agent.spec.ts Migrates spawn-agent spec from SSE to state-socket mock.
app/frontend/tests/e2e/spawn-agent.spec.md Updates companion doc to describe state-socket mocking.
app/frontend/tests/e2e/server-reorder.spec.ts Updates live-backend reorder spec to observe state-socket global events.
app/frontend/tests/e2e/server-reorder.spec.md Updates companion doc for state-socket-based broadcast verification.
app/frontend/tests/e2e/row-minimalism.spec.ts Migrates row-minimalism spec from SSE to state-socket mock.
app/frontend/tests/e2e/row-minimalism.spec.md Updates companion doc to describe state-socket mocking.
app/frontend/tests/e2e/pr-status-sidebar.spec.ts Migrates PR-status sidebar spec from SSE to state-socket mock.
app/frontend/tests/e2e/pr-status-sidebar.spec.md Updates companion doc to describe state-socket mocking.
app/frontend/tests/e2e/pane-register-panel.spec.ts Migrates pane-register-panel spec from SSE to state-socket mock.
app/frontend/tests/e2e/pane-register-panel.spec.md Updates companion doc to describe state-socket mocking.
app/frontend/tests/e2e/connection-budget.spec.ts Adds guard spec ensuring exactly one /ws/state WS and zero SSE responses.
app/frontend/tests/e2e/connection-budget.spec.md Companion doc describing the connection-budget guard’s purpose and steps.
app/frontend/tests/e2e/chat-view.spec.ts Migrates sessions stream mocking to state-socket (chat SSE remains separate).
app/frontend/tests/e2e/chat-view.spec.md Updates companion doc to describe state-socket mocking.
app/frontend/tests/e2e/board-reorder.spec.ts Updates board reorder spec to observe per-server board-changed via WS.
app/frontend/tests/e2e/board-reorder.spec.md Updates companion doc for state-socket-based echo verification.
app/frontend/tests/e2e/board-list-reorder.spec.ts Updates board list reorder spec to observe board-order via WS.
app/frontend/tests/e2e/board-list-reorder.spec.md Updates companion doc for state-socket-based echo verification.
app/frontend/tests/e2e/agent-next-waiting.spec.ts Migrates agent-next-waiting spec from SSE to state-socket mock.
app/frontend/tests/e2e/agent-next-waiting.spec.md Updates companion doc to describe state-socket mocking.
app/frontend/tests/e2e/_state-socket-mock.ts Adds shared Playwright WS mock helper for /ws/state.
app/frontend/src/lib/state-socket.ts Introduces the StateSocket client (single WS per tab, ref-counted subs, reconnect).
app/frontend/src/hooks/use-boards.test.tsx Updates hook tests to use a stub WebSocket transport instead of EventSource.
app/backend/api/status_refresh_test.go Adjusts tests to consume hub events rather than SSE bytes.
app/backend/api/state_ws.go Adds the /ws/state handler and envelope rendering types.
app/backend/api/state_ws_test.go Adds hub- and handler-level tests for hello/subscribe/ack/events/gone/errors.
app/backend/api/sse.go Refactors hub/client model to support state-socket subscriptions; removes SSE edge.
app/backend/api/sse_test.go Updates hub tests for new hubEvent-based channels and global fan-out semantics.
app/backend/api/sse_subscriber_test.go Updates subscriber-driven poll tests to consume hubEvent frames.
app/backend/api/sessions_test.go Updates sessions tests to consume hubEvent frames.
app/backend/api/router.go Registers /ws/state and removes /api/sessions/stream.
app/backend/api/preview_test.go Updates preview tests to consume hubEvent frames.
app/backend/api/boards_test.go Updates board tests to consume hubEvent frames / test client wiring.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +52 to +59
export type StateSocketControl = {
/** Push a host-global event (e.g. board-order, status-refresh, server-order). */
emitGlobal: (type: string, data: unknown) => void;
/** Push a per-server event (e.g. sessions, session-order, board-changed, preview). */
emitServer: (key: string, type: string, data: unknown) => void;
/** Push a `gone` frame for a server. */
emitGone: (key: string) => void;
};

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Skipped — StateSocketControl is the documented return type of the shared helper's live-frame-control entry point (routeStateSocketWithControl), which specs are directed to by the module doc comments. Keeping it exported is intentional test-helper surface for specs that need live frame control, so de-scoping it now would break the helper's stated contract.

Comment on lines +63 to +66
export async function routeStateSocketWithControl(
page: Page,
opts: StateSocketMockOptions = {},
): Promise<StateSocketControl> {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Skipped — routeStateSocketWithControl is the module's public live-frame-control entry point (mockStateSocket delegates to it, and the doc comments direct specs here for post-load frame control). It's an intentional shared-helper API, so keeping it exported is correct even though no spec drives live frames yet.

Comment on lines +105 to +108
ws.onclose = () => {
this.handlers.onConnectionChange?.(false);
this.scheduleReconnect();
};

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — reqToKey is now cleared in ws.onclose, so pending subscribe reqs from a dead connection can't accumulate across repeated drop-while-pending reconnect cycles (reconnect resubscribes with fresh req numbers anyway). (07caa3d)

Comment thread app/backend/api/state_ws.go Outdated
Comment on lines +120 to +123
// String renders a hubEvent as an SSE-style frame. Used ONLY by tests that
// assert on the legacy frame shape; production rendering is renderEnvelope. For
// a gone marker it mirrors the retired `event: server-gone\ndata: {}` frame.
func (e hubEvent) String() string {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — moved the test-only String() method (and its fmt import) into state_ws_test.go (same package), so it no longer ships in the production binary; the hubEvent doc comment now points there. Backend tests still green. (07caa3d)

@sahil-noon
sahil-noon marked this pull request as ready for review July 17, 2026 05:52
@sahil-noon
sahil-noon merged commit d05b47b into sockets-v2 Jul 17, 2026
5 checks passed
@sahil-noon
sahil-noon deleted the 260716-qf3j-state-socket branch July 17, 2026 05:52
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.

3 participants