feat(web): live fleet board state over fleet.subscribe - #324
Merged
Conversation
Groundwork for P5.2 (docs/conductor-frontends-design.md §12). The board contract shipped in P5.0 — snapshot then streamed deltas — and no client has consumed it since. This adds the state slice the docked conductor surface will render, and routes `fleet.update` into it. Reducers are exported as PURE functions holding the whole ordering and bounding contract, so the part worth testing needs no reactive root — the same split lib/fleet.ts uses for grouping. The signal layer is a thin shell over them. Four rules earn their place: **Replace in place, insert by key.** A task's createdAt never changes, so a running→done transition must not make the row jump while you read it. New rows are positioned by createdAt rather than unshifted, because "deltas arrive newest-last" is an assumption about the network, not a guarantee — a reconnect replays and a burst interleaves. **Events order by autoincrement id, not timestamp.** One dispatcher tick routinely settles several events in the same millisecond; ordering those by createdAt shuffles them arbitrarily between renders. **The board is capped client-side** at the daemon's own page sizes. The snapshot is bounded but the delta stream is not, so a board left open all day would grow without limit. Capping to the window the daemon would have sent also means a reconnect cannot silently change how much history is on screen. The snapshot is re-sliced too, so a future daemon raising its limit does not raise this client's memory ceiling. **`agg` is replaced wholesale, never derived.** It counts tasks that may have aged off this capped board, so a locally recomputed count would drift low exactly on the long-lived sessions where it matters. Unsubscribe deliberately KEEPS the board: leaving the pane should not blank what you just read, and the next subscribe re-snapshots anyway, so clearing would only add a flash of empty state on every visit. A generation counter drops replies from a subscription already left, the same guard blackboard.ts uses. 15 tests covering ordering, capping, idempotent redelivery, a missing conductor as a valid state, and the task→session join. Typecheck, lint and build clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
rsharath
approved these changes
Sep 5, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Groundwork for P5.2 (conductor-frontends-design.md §12). Independent of #323 — no shared files.
Why
The board contract shipped in P5.0 (snapshot, then streamed deltas) and no client has consumed it since. This adds the state slice the docked conductor surface renders, and routes
fleet.updateinto it.I verified the contract works end-to-end while testing #319 — a real dispatch produced
queued → running → donedeltas plus atask_doneevent — so this is built against observed behaviour, not just the type.Shape
Reducers are exported as pure functions holding the whole ordering and bounding contract, so the part worth testing needs no reactive root — the same split
lib/fleet.tsuses. The signal layer is a thin shell.Four rules that earn their place
Replace in place, insert by key. A task's
createdAtnever changes, so arunning → donetransition must not make the row jump while you're reading it. New rows are positioned bycreatedAtrather than unshifted, because "deltas arrive newest-last" is an assumption about the network, not a guarantee — a reconnect replays, a burst interleaves.Events order by autoincrement
id, not timestamp. One dispatcher tick routinely settles several events in the same millisecond; ordering those bycreatedAtshuffles them arbitrarily between renders.The board is capped client-side at the daemon's own page sizes (
FLEET_TASK_LIMIT100 /FLEET_EVENT_LIMIT50). The snapshot is bounded but the delta stream is not, so a board left open all day would grow without limit. Capping to the window the daemon would have sent also means a reconnect can't silently change how much history is on screen. The snapshot is re-sliced too, so a future daemon raising its limit doesn't raise this client's memory ceiling.aggis replaced wholesale, never derived. It counts tasks that may have aged off this capped board, so a locally recomputed count would drift low exactly on the long-lived sessions where it matters most.One deliberate non-obvious choice
unsubscribeFleetkeeps the board. Leaving the conductor pane shouldn't blank what you just read, and the next subscribe re-snapshots anyway — clearing would only add a flash of empty state on every visit. A generation counter drops replies from a subscription already left (the same guardblackboard.tsuses for its index fetch).Verification
15 tests: in-place replace, out-of-order insertion, both caps, idempotent redelivery, a missing conductor treated as a valid state rather than an error, error-clearing on recovery, and the task→session join for both spawn workers and send targets.
Typecheck, lint, full suite (178 tests) and production build clean.
Next
The docked surface itself —
CenterPanebranching onrole === "conductor"to render the chat plus a state-grouped rail over this slice.🤖 Generated with Claude Code