Skip to content

board-stream

The board's push channel — an SSE that fires on session-store change so the dashboard reloads on real transitions, not on a tight poll.

raw source

The dashboard kept its status/grouping fresh by re-fetching the whole board on a 4s timer, while the live terminal rode a WebSocket. So a session's status change felt laggy (up to the poll interval) even though the backend already knew it — two different freshness models on one screen. Give the board the terminal's model: the backend pushes a "something changed" signal and the dashboard reloads on it, so status flips as fast as the characters do.

expanded spec

board-stream is the board's live-delivery channel: GET /api/board/stream, a server-sent-events stream a dashboard opens once. It is server→client only — no request body, no client frames — and carries no board payload. It emits a bare board-changed signal; the client refetches /api/board on it, reusing that route's conditional-request (ETag/304) path. The board stays rebuilt on demand and this stream stays tiny.

Two event sources, all subscribers. The session-side of the board has two kinds of change, and only one lands as a file, so the channel watches both. (1) A recursive fs.watch on the per-user session store (runtime): every lifecycle status transition lands as a write to sessions/<id>/session.json (the harness hooks author it), so a record write is the board-changed signal for status/grouping. (2) A subscriber-gated poll (~2s) of the CHEAP session signature (sessions) — two tmux calls, no git — for the signals that are tmux-derived, not file writes: liveness (a worker crashing / going offline) and activity (a worker's live self-summary headline). Neither writes a session file, so the fs-watch is blind to them; before this poll they only reached the board on the slow fallback, which is why a finished-but-crashed worker or a moving headline lagged. Both sources fire the same debounced board-changed, fanned to every open stream; the poll runs only while someone is subscribed (a closed dashboard costs nothing) and stops when the last stream drops.

Only the cold path polls now. What neither source sees — a spec edit or merge that reshapes the tree, a forge issue update — is the genuinely cold path: it rides the dashboard's slow fallback poll (dashboard-shell). The session signals (status, liveness, activity) are all push; the rare, already-visible-to-its-author tree changes stay on a relaxed timer. The stream also sends a periodic keep-alive ping so an idle proxy never times the connection out, and it never throws: if a source can't start, subscribers simply fall back to the poll.

Reconnect is free. A backend hot-reload replaces the child and drops the stream; EventSource auto-reconnects to the fresh child — the same drop-and-reconnect the live pty bridges already do (spec-cli), so a reload self-heals with no client logic. An old backend without this route, or a proxy that strips SSE, degrades to the fallback poll — never to a stale board.

Still a full-snapshot refetch. The push cuts latency and idle traffic (an untouched board now fetches nothing, versus a full rebuild every 4s), but each board-changed still triggers a whole /api/board refetch — a ~1 MB snapshot on the dogfood board (it scales with the node count), rebuilt from git each time. The ETag/304 saves the wire but not that rebuild: even an unchanged reload pays the full server-side git read, so what the push channel really saves is that repeated work, by only fetching on a real change. Shrinking the payload itself (an incremental/diff board, or folding the changed slice into the event) is a separate, larger concern tracked as GitHub issue #26, not part of this channel.