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 43 commits into
Conversation
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
# Conflicts: # packages/coding-agent/test/daemon-agent-roster.test.ts
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
# Conflicts: # packages/coding-agent/test/daemon-supervisor-monitor.test.ts
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
| const store = this.rosterStore; | ||
| if (!(await store.attach(this.client))) { | ||
| throw new Error("Daemon is stale: it does not advertise the agent_roster capability; restart the daemon"); | ||
| } |
There was a problem hiding this comment.
Chat hard-fails without roster capability
High Severity
subscribeAgentRoster throws when attach returns false, and subscribeToRosterBar does not catch it, so rebindCurrentSession aborts. agent_roster is advertised only on the supervisor hello, so a current non-supervisor daemon never gains it. The snapshot bar path is then unreachable, and the error tells the user to restart a daemon that still cannot serve the subscription.
Additional Locations (2)
Reviewed by Cursor Bugbot for commit b6d91fa. Configure here.
…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
| this.rosterStore = this.persistentState.rosterStore; | ||
| if (!(await this.rosterStore.attach(client))) { | ||
| throw new Error("Daemon is stale: it does not advertise the agent_roster capability; restart the daemon"); | ||
| } |
There was a problem hiding this comment.
Subscribe handshake abort skips reconnect
High Severity
A drop during roster_subscribe (or a waitForHello timeout) makes attach throw instead of returning false. run already registered onClose, so reconnect starts, then the throw exits the view before ui.start. The outer finally disposes the client while reconnectClient can still call recoverDaemon and reconnect on that closed client, so a transient blip aborts Agents instead of reconnecting.
Additional Locations (2)
Reviewed by Cursor Bugbot for commit bf85952. Configure here.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
There are 4 total unresolved issues (including 2 from previous reviews).
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 087d895. Configure here.
| // refreshSavedSessions re-arms this latch itself when the current fetch fails without a catalog. | ||
| this.savedSearchFetchStarted = true; | ||
| void this.refreshSavedSessions({ preserveStatusOnError: true }); | ||
| } |
There was a problem hiding this comment.
Restored query skips catalog fetch
Medium Severity
The saved-catalog fetch is armed only from queryChanged, which runs after a keystroke. Remount restores persistentState.query via setText and then marks both catalogs ready, so a query that is already in the editor never starts the fetch.
Additional Locations (2)
Reviewed by Cursor Bugbot for commit 087d895. Configure here.
| } | ||
| pendingUpdates = undefined; | ||
| this.subscribed = true; | ||
| return true; |
There was a problem hiding this comment.
Overlapping attach drops subscription
Medium Severity
attach is not serialized. A failing in-flight subscribe calls detachFromClient, which removes whatever listener a newer attach just installed, and run then treats any failed subscribe as a missing agent_roster capability.
Additional Locations (2)
Reviewed by Cursor Bugbot for commit 087d895. 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)everBoundmarker 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 revision, supervisor push/backpressure, and live UI catalog sourcing; agents view now requires
agent_roster(hard error if missing) instead of polling.Overview
Replaces the agents view’s 1-second live session polling with a supervisor push subscription (
roster_subscribe/roster_update, schema 24,agent_rostercapability). The supervisor coalesces ledger mutations into batched pushes and sends a full resync when a subscriber was backpressured.Clients mirror pushes in
AgentsViewRosterStore, shared across scope transitions on one daemon connection;refreshSessionsreapplies the local mirror with no list RPCs. Rows use ledger fields (rosterStatus, queued/recovering/failed labels, last heard staleness) and stable subagent row identity across queued → bound → passivated.The saved-session catalog loads once per view, only when the user types a search query (deep message search); mutations refresh it only if that catalog was already loaded.
DaemonAgentConnection.subscribeAgentRosterfeeds the interactive subagents bar from the same roster; terminal runs that never bound a session are dropped (everBound), aligned with roster removal rules. Adds a roster soak script and broad tests for store, push, and lazy catalog behavior.Reviewed by Cursor Bugbot for commit 087d895. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Replace 1s roster polling with push-based
roster_updatesubscription in daemon and agents viewroster_subscribe/roster_unsubscribecommands androster_updatepush messages to the daemon protocol (schema revision 24, minProtocol 7, gated byagent_rostercapability)DaemonSupervisornow maintains a buffered, coalescing push pipeline (onRosterMutation,scheduleRosterPush,flushRosterUpdates) that batches changed/removed rows and writes a singleroster_updateper microtask to all subscribed clients; backpressured clients get a full resync on next drainAgentsViewRosterStoremirrors the daemon roster with snapshot+delta semantics;AgentsViewModesources live rows from the store and removes the 1spollSessionsloop; saved catalog loads lazily on first non-empty queryInteractiveModesubagent bar now prefers pushed roster summaries viacountRosterSubagentStatuseswhen available, with fallback to snapshot-based countingroster-soak.tssoak script to stress-test convergence, resync bounds, and heap usage under heavy churnagent_rostercapability will causesubscribeAgentRosterandAgentsViewRosterStore.attachto throw; reconnect now forcesrosterStore.attachwithforce: trueand changes the reconnect error message.InteractiveMode.updateSubagentSummarynow drops terminal runs that never had session evidence (everBoundfalse), which may change which rows persist in the barMacroscope summarized 087d895.