Skip to content

One event stops costing eight round-trips: scoped, coalesced room fetches - #45

Merged
udaychandra merged 2 commits into
mainfrom
agent/mast-refetch-diet
Aug 18, 2026
Merged

One event stops costing eight round-trips: scoped, coalesced room fetches#45
udaychandra merged 2 commits into
mainfrom
agent/mast-refetch-diet

Conversation

@udaychandra

Copy link
Copy Markdown
Contributor

Why

The room's SSE handling was resilient but coarse: any spec-scoped event that wasn't a message triggered specReviews + listRuns + one reviewDetail per review, and a new message re-fetched a full page. Since presence landed, agent_tool_* and agent_presence events stream during live runs — so an open room fired the 2+N fetch storm on every tool call the agent made, each fetch its own SSH round-trip.

What

Route by event type instead of reloading on any match (spec mast-refetch-diet):

  • Telemetry and presence events (agent_tool_*, agent_log_chunk, agent_presence, heartbeat) → zero fetches, zero timeline merges, zero renders in the room. Presence chips keep updating from the app-wide presence store, which was already wired.
  • spec_message_posted → one messages fetch with the server's exclusive after cursor, now exposed through Gateway.listSpecMessages ({before, after, limit} options; the endpoint already supported it). If the announced message_id doesn't land in the merge — a cross-node message can sync in with an id older than the cursor — one full latest-page fetch recovers it. Echo suppression for your own posts is unchanged.
  • review_* → that review's detail only, when the event carries the review id and the detail is loaded; the spec's review list otherwise. Verified in sail source: stage events carry no review_id today (only review_approved/finding_dismissed do), so the fallback is the common live path until sail adds it. review_iteration_started/review_completed are list-shape boundaries and refresh the list.
  • Run-lifecycle events (spec_dispatched, agent_session_*, agent_failed, agent_cancelled, …) → listRuns for that spec only.
  • spec_status_changed / snapshot / guardrail rows → render straight from the event payload, no fetch. The board's own reload path is untouched.
  • Unrecognized spec-scoped events → the old conservative refresh, so a new server event type is never silently dropped.

Bursts coalesce per (kind, spec): microtask batching plus an in-flight dirty flag (the useBoard pattern extended to cover the in-flight window), so five mixed events cost one fetch per kind and an event landing mid-flight re-runs exactly once.

The reconnect gap-fill (#44) keeps its full reconcile — routing applies only to live events.

Tests

Behavior tests count gateway calls, per the spec's acceptance list:

  • a remote spec_message_posted → exactly one messages call carrying after, zero review/run calls
  • review_stage_passed with a review id → one review-detail call, nothing else; without the id → one list call
  • 20 tool events + 2 presence transitions in an open room → zero fetches, zero new rows
  • a burst of 5 mixed events → one coalesced fetch per kind
  • unknown event type → conservative refresh retained

bun test (512 pass) and bun run typecheck green; no new dependencies; no sail changes.

… world

One SSE event used to ring a doorbell that reloaded everything: any
spec-scoped non-message event triggered specReviews + listRuns + a
reviewDetail per review, and a new message re-fetched a full page. Since
presence landed, agent_tool_* and agent_presence stream during live runs,
so an open room fired that 2+N storm on every tool call the agent made.

Route by event type instead:
- Telemetry and presence events (agent_tool_*, agent_log_chunk,
  agent_presence, heartbeat) cost zero fetches, zero merges, zero renders
  in the room; the presence store keeps the chips live.
- spec_message_posted fetches with the server's exclusive after cursor
  (now exposed through Gateway.listSpecMessages) and falls back to one
  full page if the announced message_id doesn't land in the merge.
- review_* refreshes the one review detail when the event carries the
  review id and it's loaded; the list otherwise (sail's stage events
  carry no review_id today) and on list-shape boundaries.
- Run-lifecycle events refresh listRuns only; spec_status_changed and
  snapshot rows render straight from the payload with no fetch.
- Unrecognized spec-scoped events keep the conservative refresh so a new
  server event type is never silently dropped.

Bursts coalesce per (kind, spec): microtask batching plus an in-flight
dirty flag, so five mixed events cost one fetch per kind. The reconnect
gap-fill's full reconcile is unchanged. Behavior tests count gateway
calls per the acceptance list.
The live catch-up's after-cursor skips a message that synced in out of order
(a cross-node id older than the newest confirmed). Its fallback then refetched
the latest page — which, in a room with more than a page of history, need not
reach back far enough to include that message, so it stayed lost until a
reconnect.

Recover by anchoring on the missing id's position instead: fetch the page ending
at its loaded successor (before=<successor>), which contains it. Falls back to
the latest page only when no successor exists (impossible while a newer confirmed
message is loaded). Convergence now genuinely comes from the fetch, as the spec's
edge promised. Proven red-first with a >PAGE_SIZE room and a late out-of-order
announcement.
@udaychandra
udaychandra merged commit 7b0c07e into main Aug 18, 2026
2 checks passed
@udaychandra
udaychandra deleted the agent/mast-refetch-diet branch August 18, 2026 01:30
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