Skip to content

feat(coding-agent): roster subscription push consumed by the agents view and subagents bar - #1900

Open
snimu wants to merge 34 commits into
feat/agent-roster-ledgerfrom
feat/agent-roster-push
Open

feat(coding-agent): roster subscription push consumed by the agents view and subagents bar#1900
snimu wants to merge 34 commits into
feat/agent-roster-ledgerfrom
feat/agent-roster-push

Conversation

@snimu

@snimu snimu commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

Part 3 of 3 for ENG-5794 (https://linear.app/primeintellect/issue/ENG-5794). Stacked on #1897; base is feat/agent-roster-ledger, so this diff is PR3-only.

Summary

  • add roster_subscribe/roster_unsubscribe commands and coalesced roster_update pushes (schema 24), gated on a new agent_roster capability advertised only by the supervisor hello — a TUI can never subscribe against a daemon that cannot serve it
  • every ledger mutation funnels into one setImmediate-coalesced push per macrotask; backpressured subscribers are skipped and receive one full resync on drain (refused resync writes re-arm the retry); client-owned visibility transitions push removals/writes
  • the agents view holds a persistent RosterStore across scope transitions: subscribe once, re-attach is a no-op, rows render ledger truth (classify-once statuses, queued/recovering/failed labels, last heard Xs ago staleness); queued subagents appear under their parent before any session exists, and row identity is stable across queued -> bound -> passivated (parent-qualified agent alias)
  • on roster-capable daemons the 1s poll never starts, navigation and refreshes issue zero daemon requests, and the saved catalog loads at most once per view instance, only when the user types a search query — the per-transition reconcile storm (previously up to ~GB-scale transient allocations with 200 saved sessions) is gone from the nav path
  • the 1s poll path is deleted outright, not kept as a fallback: the agents view requires the agent_roster capability and fails fast against a daemon that lacks it (unreachable in practice — the wired entry paths enforce an exact schema match and replace stale daemons); the chat subagents bar degrades to snapshot-driven counts, so interactive startup never depends on the capability
  • the subagents bar drops terminal runs that never bound (matching the roster's removal rule) via a sticky everBound marker immune to evidence laundering across repeated terminal projections; bar and view counts come from one shared population and formula
  • SessionSummary gains additive rosterStatus/statusLabel/lastHeardFromAt so list consumers read the same classify-once verdicts

Validation

  • three adversarial review rounds to APPROVE; production-fed lifecycle matrix (unbound-error, queued, bound, heartbeat-only, passivated, recovering) drives the real handler; header==bar equality pinned from production data paths
  • mutants killed by named tests: staleness-stamp push drop, drain-resync deletion, re-attach re-subscribe, capability-gate strip, per-write flush (real-socket coalescing), terminal-rule removal, incoming-only evidence (laundering), plus the PR2 suite intact
  • sandbox: 28-suite battery 892+ passed; full coding-agent suite zero regressions vs base; root npm run check green; schema digest test green

Note

Medium Risk
Touches daemon protocol, supervisor push/backpressure, and agents-view live catalog semantics; behavior changes are broad but gated on capability with extensive tests and a soak script.

Overview
Replaces the agents view’s 1-second list polling with a daemon-pushed agent roster (protocol schema 24): roster_subscribe seeds a snapshot and roster_update delivers coalesced deltas; backpressured clients get a full resync on drain. The supervisor advertises agent_roster only on its hello and fans out ledger mutations (status labels, staleness stamps, visibility for client-owned workers).

The agents view keeps a persistent AgentsViewRosterStore and shared daemon client across scope transitions—refreshes are local reapplies with zero list RPCs. It requires the capability and errors on stale daemons; the chat subagents bar optionally uses the same store via subscribeAgentRoster and falls back to RLM child snapshots when subscribe is unavailable. Rows use ledger fields (rosterStatus, queued/recovering/failed labels, last-heard-from) and stable subagent row identity across queued → bound → passivated.

Saved-session catalog loads once per view, only when search text is non-empty (deep message search), not on every navigation. DaemonClient.request adds recoverable: false so roster_subscribe fails fast on disconnect. AgentSession drops terminal child runs that never bound a session from roster snapshots and emits cancelled as the removal signal on pre-bind failures.

Reviewed by Cursor Bugbot for commit 4a961d6. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Replace agents-view polling with pushed roster_update subscription

  • Adds daemon protocol commands roster_subscribe/roster_unsubscribe and outbound roster_update messages, bumping schema revision to 24 and advertising the agent_roster capability in daemon-protocol.ts.
  • DaemonSupervisor now coalesces roster mutations and pushes changed/removed entries to subscribed clients; backpressured clients receive a full resync after socket drain instead of incremental updates.
  • New AgentsViewRosterStore in roster-store.ts replaces 1s polling in the agents view, maintaining a local mirror fed by server pushes with capability checks and reconnect safety.
  • InteractiveMode subscribes to the roster for subagent bar counts via subscribeToRosterBar, falling back to snapshot-based counting when the daemon lacks support or subscription fails.
  • AgentSession.getRlmChildSnapshots hides terminal runs that never bound a session, and errored-before-bind child runs now surface as cancelled to observers.
  • DaemonClient.request gains a recoverable option (default true); non-recoverable requests are rejected immediately on disconnect instead of parked for reconnect.
  • Behavioral Change: daemons without agent_roster capability cause the agents view to fail early with STALE_ROSTER_DAEMON_MESSAGE; refreshSessions no longer accepts { preserveStatusOnError } and saved-session loading is now lazy (deferred until first search). Schema revision bump to 24 means older clients/servers without compatibility entries for roster_subscribe/roster_update will not interoperate on these commands.

Macroscope summarized 4a961d6.

Comment thread packages/coding-agent/src/modes/agents-view/roster-store.ts
Comment thread packages/coding-agent/src/modes/agents-view/agents-view-mode.ts Outdated
Comment thread packages/coding-agent/src/modes/agents-view/agents-view-mode.ts Outdated
Comment thread packages/coding-agent/src/modes/agents-view/agents-view-mode.ts
Comment thread packages/coding-agent/src/modes/agents-view/agents-view-mode.ts Outdated
Comment thread packages/coding-agent/src/modes/daemon/daemon-protocol.ts
Comment thread packages/coding-agent/src/modes/agents-view/roster-store.ts
Comment thread packages/coding-agent/src/modes/agents-view/agents-view-mode.ts Outdated
Comment thread packages/coding-agent/src/modes/interactive/interactive-mode.ts
Comment thread packages/coding-agent/src/modes/agents-view/agents-view-mode.ts Outdated
Comment thread packages/coding-agent/src/modes/agents-view/roster-store.ts Outdated
Comment thread packages/coding-agent/src/modes/agents-view/roster-store.ts
Comment thread packages/coding-agent/src/modes/agents-view/agents-view-mode.ts
Comment thread packages/coding-agent/scripts/roster-soak.ts
Comment thread packages/coding-agent/src/modes/agents-view/agents-view-mode.ts
Comment thread packages/coding-agent/src/modes/agents-view/agents-view-mode.ts Outdated
Comment thread packages/coding-agent/src/modes/agents-view/roster-store.ts
Comment thread packages/coding-agent/src/modes/agent-connection/daemon-agent-connection.ts Outdated
Comment thread packages/coding-agent/src/modes/agents-view/agents-view-mode.ts
Comment thread packages/coding-agent/src/modes/agents-view/agents-view-mode.ts
Comment thread packages/coding-agent/src/modes/daemon/daemon-supervisor.ts
Comment thread packages/coding-agent/src/modes/interactive/interactive-mode.ts
Comment thread packages/coding-agent/src/modes/agents-view/agents-view-mode.ts
@snimu
snimu force-pushed the feat/agent-roster-push branch 2 times, most recently from db18389 to 32f3abc Compare August 31, 2026 16:08
Comment thread packages/coding-agent/src/modes/interactive/interactive-mode.ts Outdated
Comment thread packages/coding-agent/src/modes/agents-view/agents-view-mode.ts
Comment thread packages/coding-agent/src/modes/agents-view/roster-store.ts
Comment thread packages/coding-agent/src/modes/daemon/agent-roster.ts
snimu added 10 commits August 31, 2026 18:52
Adds roster_subscribe/roster_unsubscribe and capability-gated roster_update
pushes (agent_roster, schema revision 24); the agents view holds a shared
DaemonClient and roster store across scope transitions, renders ledger
statuses and labels (queued/recovering/failed, staleness), falls back to
the legacy poll path only against daemons without the capability, and
fetches the saved catalog once per view instance when a search query needs
deep text. The supervisor coalesces pushes per macrotask and resyncs
backpressured subscribers on drain.

ENG-5794
Navigation issues no daemon requests (pinned), the lazy saved-catalog fetch
happens once per view instance only when a query is typed, and rows
synthesized from the ledger carry rosterStatus so sections, labels, and
staleness render the classify-once verdicts. Adds the changelog fragment.

ENG-5794
- The subagents bar follows the roster's terminal rule: a done/error run
  with no session evidence across its history (daemon session id, live
  activity, or session token accounting) is dropped like a cancelled one,
  on both connection kinds; children with transcripts keep their rows.
  The bar/view equality test now drives the real update handler through a
  lifecycle matrix (unbound-error, queued, bound, heartbeat-only,
  passivated, recovering) against roster-derived sections.
- Visibility transitions are roster pushes: a row claimed by a
  client-owned worker reaches subscribers as a removal, and promotion
  re-enqueues the worker's rows.
- A refused drain resync re-arms rosterResyncPending so the next drain
  retries instead of stranding the subscriber; pinned through the real
  connection drain listener.
- The watchdog staleness stamp and clear are pinned end-to-end to a
  subscriber push.
- Saved-sibling name validation prefers the ledger's rosterStatus like the
  other fallbacks.
- Queued and bound child rows share one stable identity (the qualified
  roster agent id) in row identities and reconciliation aliases, so
  selection survives the bind push without duplicate rows.

ENG-5794
…ojections

Terminal merges clear the session-evidence display fields, so a repeated
terminal projection saw an evidence-free snapshot and removed a
transcript-bearing child. Bound-ness is now a sticky everBound snapshot
field set the first time evidence (daemon session id, live activity, or
session token accounting) is observed, and the terminal drop rule reads
it, keeping repeated terminal projections idempotent while a never-bound
run still cannot fabricate evidence. Saved-sibling name validation now
reads the ledger row's status through the session-file index instead of a
rosterStatus field that inactive summaries never carry.

ENG-5794
- The agents view drops its poll fallback as dead code: exact-version
  forced restart already ships, so a daemon without the agent_roster
  capability is a hard error naming the stale daemon, refreshes reapply
  the pushed store locally, and reconnects re-attach the subscription.
- The daemon-mode subagents bar consumes the pushed roster through a
  store shared per connection (subscribeAgentRoster on AgentConnection),
  counting direct children with the same ledger statuses the view renders;
  the in-process connection keeps the sanctioned snapshot-to-classifier
  path. The lifecycle equality matrix now pins push-fed bar == view.
- A drain-time roster resync clears its pending flag even when the write
  reports backpressure, since socket.write queues the payload either way:
  one resync per loss gap, never one per drain.
- scripts/roster-soak.ts drives a real supervisor socket with thousands
  of churning sessions, depth-40 chains, and a deliberately slow
  subscriber, asserting convergence, coalesced resyncs, bounded heap, and
  answered commands.

ENG-5794
Fixture fallout from removing the poll fallback and the legacy refresh
shim: query-changed and reply fixtures stub the saved-catalog fetch, the
handoff-scope pins feed the pushed store instead of a failing list
request, the rename pin asserts one local reapply, and the monitor seed
helper writes delta-shaped rows directly. Biome formatting rides along.

ENG-5794
- The daemon-mode bar fails hard on a stale daemon: subscribing to the
  roster is awaited during session (re)binding, the in-flight forced
  reattach after reconnect throws instead of ignoring a refusal, and
  the snapshot->classifier path survives only on in-process connections.
  A production-path pin (real supervisor socket, real DaemonAgentConnection
  and store) proves the bar counts pushed rows, not stale snapshots.
- The staleness watchdog stamps rows only on the transition into stale;
  repeat sweeps of an already-stale worker emit zero mutations.
- roster_unsubscribe clears any pending resync and drains re-check the
  subscription before resyncing.
- A snapshot apply never surfaces a live spawn-ledger edge as a transient
  removal, pinned over the push surface.
- The soak asserts exact payload equality for both subscribers, requires
  the induced loss gap to resolve through coalesced resyncs, bounds list
  latencies, and names the worker-frame integration pin it leaves to
  vitest. refreshBothCatalogs and the poll-era comments go away; fixtures
  drop the last poll-model stubs.

ENG-5794
- buffer roster_update pushes racing the subscribe reply and replay them
  after the snapshot resync (AgentsViewRosterStore.attach)
- await the parsed daemon_hello inside attach so a fresh connection is
  never misread as missing the agent_roster capability
- re-arm the lazy saved-catalog load when its fetch fails, and refresh
  the loaded catalog after renames and deactivations
- classify roster_subscribe/roster_unsubscribe as read-only so command
  journal replays cannot skip re-subscribing a new socket
- roster-soak: try/finally lifecycle; any rejection cleans up and exits
  nonzero instead of hanging
…l classification

- pushes racing the roster_subscribe reply replay after the snapshot resync
- roster_subscribe/roster_unsubscribe stay out of the mutation journal
snimu added 24 commits August 31, 2026 18:52
… re-arm

- persistentState.savedCatalogLoaded survives view remounts and gates the
  rename/deactivate/delete catalog refreshes
- the per-instance search fetch re-arms only while no catalog exists, so
  a superseded fetch's false return cannot force refetches or clear data
- reconnect-timeout status tells the truth: reconnect stopped
…e current fetch

- refreshSavedSessions owns the re-arm: it fires on the current
  generation's failure (or a skipped start) while no catalog exists, so a
  superseded settle can never disarm the latch under a pending fetch
- remount coverage moves to a production-constructor test; the hand-built
  harness variant is deleted
…ving pins

- store apply/removal/resync test also pins one listener emission per tick
- one row-label test covers queued, recovering, and stale ledger states
- the zero-request refresh test also drives row navigation
- drop the hand-wired reconcile-batch adapter test and the cross-surface
  bar matrix; the two unique history branches move next to the other
  updateSubagentSummary pins
- the labels test also pins the stable queued-to-bound row identity
- the zero-request refresh pin sheds its navigation half
…, drop dead soak/view fixture fields

- condense the anchor-identity and soak-header comments to one line each
- present-tense fix in the soak convergence check label
- delete the dead rosterCapable fixture fields
…, restored-query fetch, serialized attach

- the subagents bar degrades to child snapshots when the roster subscribe
  fails; a session rebind never hard-fails on it (the agents view keeps
  the hard error)
- the view arms its close-driven reconnect only after a successful roster
  attach, so a handshake failure exits cleanly instead of racing a
  background reconnect against client disposal
- one armSavedSearchFetch latch serves typed and restored queries; run()
  arms it for a restored non-empty query
- AgentsViewRosterStore serializes attaches, so a stale attempt settling
  late can never detach a newer subscription
…plit on the roster attach seam

- armSavedSearchFetch honors the persistent savedCatalogLoaded gate: a
  remounted view with a restored query never refetches a loaded catalog
- AgentsViewRosterStore.attach returns false only for a missing
  capability; transport and subscribe failures detach their own listener
  and throw, so the connection reconnect loop retries the rebind instead
  of resyncing with a dead subscription; the chat bar keeps catching both
…iption identity

- roster_subscribe opts out of request-recovery parking (new per-request
  recoverable option): a close mid-subscribe rejects into the callers own
  bounded retry loop instead of deadlocking the connection reconnect,
  whose parked request could only be revived by the hello that same loop
  was stuck producing
- the store keys its live subscription to the connection hello: a
  reconnected transport re-subscribes naturally and the force flag is
  deleted from attach and every call site
AgentSession now owns the roster rule end to end: getRlmChildSnapshots skips
terminal runs that never bound a session (covering seed/replace/state paths),
and a pre-bind failure emits its terminal update as cancelled - the wire's
existing removal signal - so event consumers need no second predicate. Deletes
the everBound sticky marker, its snapshot field, and hasSubagentSessionEvidence.
The failure still reaches the parent as rlm_child_failure and stays listed with
its true error status in listRlmSubagents.
…one store channel

AgentRoster.amend patches statusLabel/lastHeardFromAt in place and notifies, so
markWorkerRosterEntries, the staleness sweep, and the promotion re-publish stop
bypassing the store with manual onRosterMutation calls; onMutation now has one
caller channel.
An unsubscribed socket on a live supervisor never receives roster_update, and
the pre-roster list validator stays open to the additive rosterStatus/
statusLabel/lastHeardFromAt summary fields; both directions of the schema-24
wire change are now pinned.
…fetch after outages

The 15s heartbeat poll now restarts the reconnect loop over a dead socket
instead of leaving the view permanently offline after one 120s window (the 1s
poll used to do this), and a connected poll failure no longer overwrites a
sticky notice. A successful reconnect re-arms the lazy saved-catalog fetch
through the one arm predicate so a query that outlived the outage regains its
deep-search matches.
… disarm, hour ages

Each roster push settles a missing restored selection anchor (rebuilds re-arm
it and no poll clears it anymore), a successful /name disarms the composer like
/kill, the delete-confirm list RPC documents itself as a deliberate
authoritative liveness check, last-heard ages gain an hour unit, and
refreshSessions loses its dead boolean (the 'refresh failed' rename status was
unreachable).
…ned-only rows

flushRosterUpdates now gates removed ids on a published-ids set: rows born to
client-owned workers emit nothing (no repeated no-op reconciles in every
subscriber, no leak of private roster ids that embed transcript paths), a
published row claimed by an owned worker leaves the surface exactly once, and
promotion re-publishes through the existing empty amend. Seeds and resyncs
register their ids so later disappearances stay removable.
…guard stay push-clean

The subagents-bar roster callback now requests a render, so a push with no
accompanying session event paints immediately. Agents-view exit closes the
socket before store disposal (the supervisor drops the subscription with the
client) and dispose serializes behind attach with a fire-and-forget
unsubscribe, so a wedged daemon cannot block exit and an in-flight attach
cannot leave a dangling listener. The pre-delete liveness probe keeps its
narrower plain-list verdict local instead of overwriting the pushed catalog,
so a failed delete no longer hides queued/passivated rows on the next
heartbeat reconcile.
…-daemon string

liveCatalogReady could never be false once the view runs: the field, its dead
savedCatalogReady initializer, and the liveCatalog* fixture leftovers are gone,
and shouldApplyScopeResolution takes only the saved-catalog readiness. The
stale-daemon capability message now has one owner
(STALE_ROSTER_DAEMON_MESSAGE in roster-store).
@snimu
snimu force-pushed the feat/agent-roster-push branch from 32f3abc to 4a961d6 Compare August 31, 2026 16:53

@cursor cursor Bot 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.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 4a961d6. Configure here.

}
if (summary.lastHeardFromAt !== undefined) {
return `last heard ${formatAgeLabel(summary.lastHeardFromAt)}`;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale last-heard labels stop ticking

Medium Severity

last heard Xs ago is computed once when rows are rebuilt from lastHeardFromAt. The watchdog stamps that time only on the transition into stale, and the 1s poll that used to rebuild every second is gone. After the first push, idle stale rows keep a frozen age until some other reconcile happens (a later push or a successful 15s heartbeat refresh).

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 4a961d6. Configure here.

(parentFile !== undefined &&
child.parentSessionPath !== undefined &&
canonicalSessionPath(child.parentSessionPath) === parentFile);
if (!direct) continue;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Bar omits parentSessionId children

Medium Severity

countRosterSubagentStatuses treats a child as direct only via parentActiveSessionId or parentSessionPath. The agents view also parents through parentSessionId, and queued ledger rows in this PR’s own tests often carry only that field. Those children render under the parent in the view but never enter the chat bar totals.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 4a961d6. Configure here.

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