feat(coding-agent): roster subscription push consumed by the agents view and subagents bar - #1900
feat(coding-agent): roster subscription push consumed by the agents view and subagents bar#1900snimu wants to merge 34 commits into
Conversation
a1870b6 to
d7e0973
Compare
db18389 to
32f3abc
Compare
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
… 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
…erseded-fetch race
…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).
32f3abc to
4a961d6
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
❌ 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)}`; | ||
| } |
There was a problem hiding this comment.
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)
Reviewed by Cursor Bugbot for commit 4a961d6. Configure here.
| (parentFile !== undefined && | ||
| child.parentSessionPath !== undefined && | ||
| canonicalSessionPath(child.parentSessionPath) === parentFile); | ||
| if (!direct) continue; |
There was a problem hiding this comment.
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)
Reviewed by Cursor Bugbot for commit 4a961d6. Configure here.


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
roster_subscribe/roster_unsubscribecommands and coalescedroster_updatepushes (schema 24), gated on a newagent_rostercapability advertised only by the supervisor hello — a TUI can never subscribe against a daemon that cannot serve itRosterStoreacross scope transitions: subscribe once, re-attach is a no-op, rows render ledger truth (classify-once statuses, queued/recovering/failed labels,last heard Xs agostaleness); queued subagents appear under their parent before any session exists, and row identity is stable across queued -> bound -> passivated (parent-qualified agent alias)agent_rostercapability 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 capabilityeverBoundmarker immune to evidence laundering across repeated terminal projections; bar and view counts come from one shared population and formulaSessionSummarygains additiverosterStatus/statusLabel/lastHeardFromAtso list consumers read the same classify-once verdictsValidation
npm run checkgreen; schema digest test greenNote
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
listpolling with a daemon-pushed agent roster (protocol schema 24):roster_subscribeseeds a snapshot androster_updatedelivers coalesced deltas; backpressured clients get a full resync on drain. The supervisor advertisesagent_rosteronly on its hello and fans out ledger mutations (status labels, staleness stamps, visibility for client-owned workers).The agents view keeps a persistent
AgentsViewRosterStoreand 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 viasubscribeAgentRosterand 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.requestaddsrecoverable: falsesoroster_subscribefails fast on disconnect.AgentSessiondrops 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_updatesubscriptionroster_subscribe/roster_unsubscribeand outboundroster_updatemessages, bumping schema revision to 24 and advertising theagent_rostercapability in daemon-protocol.ts.DaemonSupervisornow coalesces roster mutations and pushes changed/removed entries to subscribed clients; backpressured clients receive a fullresyncafter socket drain instead of incremental updates.AgentsViewRosterStorein roster-store.ts replaces 1s polling in the agents view, maintaining a local mirror fed by server pushes with capability checks and reconnect safety.InteractiveModesubscribes to the roster for subagent bar counts viasubscribeToRosterBar, falling back to snapshot-based counting when the daemon lacks support or subscription fails.AgentSession.getRlmChildSnapshotshides terminal runs that never bound a session, and errored-before-bind child runs now surface ascancelledto observers.DaemonClient.requestgains arecoverableoption (default true); non-recoverable requests are rejected immediately on disconnect instead of parked for reconnect.agent_rostercapability cause the agents view to fail early withSTALE_ROSTER_DAEMON_MESSAGE;refreshSessionsno 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 forroster_subscribe/roster_updatewill not interoperate on these commands.Macroscope summarized 4a961d6.