Skip to content

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

Open
snimu wants to merge 43 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 43 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
  • against a daemon without the capability the entire legacy poll path runs unchanged (explicit compatibility fallback)
  • 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 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_roster capability). 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; refreshSessions reapplies 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.subscribeAgentRoster feeds 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_update subscription in daemon and agents view

  • Adds roster_subscribe/roster_unsubscribe commands and roster_update push messages to the daemon protocol (schema revision 24, minProtocol 7, gated by agent_roster capability)
  • DaemonSupervisor now maintains a buffered, coalescing push pipeline (onRosterMutation, scheduleRosterPush, flushRosterUpdates) that batches changed/removed rows and writes a single roster_update per microtask to all subscribed clients; backpressured clients get a full resync on next drain
  • New AgentsViewRosterStore mirrors the daemon roster with snapshot+delta semantics; AgentsViewMode sources live rows from the store and removes the 1s pollSessions loop; saved catalog loads lazily on first non-empty query
  • InteractiveMode subagent bar now prefers pushed roster summaries via countRosterSubagentStatuses when available, with fallback to snapshot-based counting
  • Adds roster-soak.ts soak script to stress-test convergence, resync bounds, and heap usage under heavy churn
  • Risk: daemons below protocol 7 or without agent_roster capability will cause subscribeAgentRoster and AgentsViewRosterStore.attach to throw; reconnect now forces rosterStore.attach with force: true and changes the reconnect error message. InteractiveMode.updateSubagentSummary now drops terminal runs that never had session evidence (everBound false), which may change which rows persist in the bar

Macroscope summarized 087d895.

snimu added 9 commits August 29, 2026 16:01
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
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/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
Comment thread packages/coding-agent/src/modes/agents-view/roster-store.ts
snimu added 8 commits August 29, 2026 19:12
- 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
Comment thread packages/coding-agent/src/modes/agents-view/agents-view-mode.ts
Comment thread packages/coding-agent/scripts/roster-soak.ts
snimu added 7 commits August 29, 2026 22:18
- 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
snimu added 5 commits August 30, 2026 01:24
…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");
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit b6d91fa. Configure here.

snimu added 12 commits August 30, 2026 02:10
…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");
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit bf85952. Configure here.

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

There are 4 total unresolved issues (including 2 from previous reviews).

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 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 });
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 087d895. Configure here.

}
pendingUpdates = undefined;
this.subscribed = true;
return true;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 087d895. 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