Skip to content

fold the twin agent rosters into one - #2153

Merged
snimu merged 5 commits into
mainfrom
res-1335-one-roster
Sep 10, 2026
Merged

snimu merged 5 commits into
mainfrom
res-1335-one-roster

Conversation

@snimu

@snimu snimu commented Sep 9, 2026 •

Copy link
Copy Markdown
Contributor

Purpose

Family discovery had two entry points that disagreed. agent_message.list_agents() returned the nuclear family from the family catalog, including members that exist only on disk. agent_observe.list_agents() returned live sessions in this worker plus passive RLM subagents. Neither list was a superset of the other, so an agent had to call both to learn who it could reach, and the two answers could differ for the same family.

agent_observe.list_agents() is now the one roster. agent_message.list_agents() is removed.

What the roster returns

The roster is built from the family catalog that agent_message.send already uses to resolve receiver_role and receiver_name, so membership and relationships have a single definition. Every reachable member appears: parent, siblings, and children, active or not.

The distinction between a live member and a persisted one is carried by fields, not by a flag or a second function:

  • activeSessionId is present only for a member with a live session in this daemon.
  • status keeps the live vocabulary (model, tool, busy, user, idle, compacting) for live members and the catalog vocabulary (running, idle, inactive) for the rest.
  • relationship (parent, sibling, child) is on every entry, so a roster row maps directly onto a send call.
  • Live members keep their runtime detail (streaming, queued count, latest message preview); persisted members report only what the catalog knows (cwd, message count, first message, child id).

The current agent is no longer duplicated: it stays in current and is not repeated in agents.

I did not add a boolean mode flag. The two old lists differed in scope, not in mode: the message roster added saved root siblings and supervisor peers, the observe list added live detail. Both are expressible as per-entry fields on one union, which is what this does.

Source archaeology

  • createAgentFamilyCatalog (daemon-mode) is the fuller family: active local sessions, supervisor peers from other workers, passive RLM subagents from the ledger, and saved root sessions marked inactive.
  • The old createAgentObserveListResult walked listTargetableSessionStates plus passive subagents only, so saved inactive roots and peers in other workers were never observable. That is the scope gap the unified roster closes.
  • buildAgentFamilyRoster is replaced by selectAgentFamily, which returns {relationship, entry} members over the same catalog entries. The observe list maps those members to summaries; send resolution filters the same members. One backend, one truth.

Messaging without observation

Checked, and no such configuration ships. agentMessageController and agentObserveController are constructed only by daemon-mode, always as a pair (three construction sites). sdk.ts and main.ts only forward the options. So removing discovery from the messaging skill cannot leave a shipped configuration able to send but unable to discover, and no gating change was needed. Some unit tests install only a message controller; those are fixtures, not a runtime mode.

Mixed versions

The agent_message.list_agents host request still exists and now always fails with:

agent_message.list_agents was removed; the family roster now lives in agent_observe.list_agents(). Restart the Python kernel to load the current skills, then call await agent_observe.list_agents().

An old kernel, or a new kernel imitating an old transcript, gets that guidance instead of an opaque unknown-request error.

Prose

System prompt family-discovery guidance, both SKILL.md files, the skill docstrings, and docs/long-running-agents.md now point at agent_observe.list_agents(). send guidance is unchanged.

LOC

area + - net
src 128 131 -3
skills (python + SKILL.md) 22 18 +4
docs + changelog 4 2 +2
tests 180 168 +12

One new test (inactive family member appears in the observe roster with persisted-only fields). Every other test change is an update of an existing roster test; the removed agent_message.list_agents kernel assertions were merged into the existing send tests, and the removed-call guidance is pinned in the existing host-boundary test.

Validation

Prime sandbox, node:24:

  • npm run check at the repo root: clean.
  • npx tsgo --noEmit -p tsconfig.json: clean.
  • vitest: daemon-mode, agent-session-bus, agent-session-recursion, system-prompt, agent-session-services, suite/agent-session-observe, kernel-agent-message-skill, kernel-agent-observe-skill, acp-kernel-features, suite/regressions/617-subagent-terminal-agent-message - 388 passed, 5 skipped, 0 failed.

Wake-on-send for a non-resident target is already pinned by the existing daemon passivation and supervisor-wake tests, which still pass.

Deliberate drops and known corners

  • Passive subagent rows no longer carry parentActiveSessionId or rlmParentNodeId. That is deliberate: relationship already names the link to the current agent, and rlm.list_subagents() owns the child handles. No in-tree consumer reads those fields off observe rows.
  • Roster membership is catalog-based, like send resolution. get_agent and recent_messages keep their header-aware family check, so a live child whose parent link exists only in its session header can be readable while absent from the roster. The roster now matches what send can address, which is the invariant worth keeping; the header-only case is rare (in-daemon spawns and passive hydration both write metadata).
  • One roster call is not a single instant: the family catalog resolves first (supervisor call, session scan, passive walk), then live rows are read. A member that replies or closes during that window can show a slightly stale repliedSinceTask or status. It self-heals on the next call, so no mechanism was added for it.
  • A saved root's first user message is no longer copied onto sibling rows, and every preview a row carries is capped at the same length as the live latest-message preview.
  • A peer working in another worker keeps the active session id its peer summary reports, so activeSessionId means "a live session exists" rather than "a live session exists here". Transcript reads still need a session in this worker.

Fixes RES-1335


Note

Medium Risk
Touches daemon family catalog, observe listing, and message target resolution—core orchestration paths—with a breaking API removal mitigated by a guided host error.

Overview
Removes duplicate family discovery by deleting agent_message.list_agents() (Python skill and session host path) and making agent_observe.list_agents() the one roster for parent, siblings, and children—including inactive, on-disk, and remote peers—with a relationship field aligned with agent_message.send addressing.

Backend consolidation: roster() / buildAgentFamilyRoster become family() / selectAgentFamily over an expanded family catalog; observe listing maps those members to live or persisted summaries (optional activeSessionId, previews capped at AGENT_OBSERVE_PREVIEW_MAX_CHARS). Broadcast and role-based agent_message.send resolution use the same family() list. Stale callers hitting agent_message.list_agents get an explicit migration error.

Docs, RLM prompts, and skill text now point discovery at agent_observe.list_agents(); messaging behavior is unchanged aside from roster source.

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

Note

Unify twin agent rosters into agent_observe.list_agents

  • Consolidates the two agent rosters by removing agent_message.list_agents and making agent_observe.list_agents the single source for family discovery.
  • agent_observe.list_agents now returns the full nuclear family, including inactive, persisted, and remote members with relationship data.
  • Message handlers and daemon mode update to resolve broadcast and role-based sends through the shared family list.
  • Behavioral Change: agent_message.list_agents now rejects with a migration error directing callers to agent_observe.list_agents. The controller contract now requires family().

Macroscope summarized 415e713.

Family discovery had two entry points: agent_message.list_agents listed the
nuclear family including members that were only on disk, while
agent_observe.list_agents listed live sessions plus passive subagents. Neither
was a superset, so an agent had to call both to know who it could reach.

agent_observe.list_agents is now the single roster. It is built from the family
catalog that send() already uses to resolve receiver_role and receiver_name, so
membership has one definition. Entries carry the distinction as data: a member
with no live session here has no activeSessionId, keeps status "inactive", and
reports only persisted facts, while live members keep their runtime detail.
Every entry carries its relationship, so a roster row maps directly onto a send.

agent_message.list_agents is gone from the skill module and the docs; its host
request now fails with a message naming agent_observe.list_agents, so an old
kernel or a copied old transcript self-corrects.

Fixes RES-1335
Comment thread packages/coding-agent/skills/agent-observe/SKILL.md Outdated

@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 1 potential issue.

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 b470168. Configure here.

Comment thread packages/coding-agent/src/modes/daemon/daemon-mode.ts
The observe skill doc claimed transcript reads never work on inactive members;
the daemon hydrates a passive child on demand, so only root siblings in another
worker are unreadable.

Build the resident-session map after the family catalog resolves, so a session
that becomes resident during the catalog's disk and supervisor IO is still
reported with its live fields.
A saved root's first user message could be any size, so copying it onto every
sibling row made one roster reply carry the opening prompts of every saved
session. Saved roots no longer carry a first message at all, and the previews a
row can carry share one cap with the live latest-message preview.

A peer working in another worker is live, so its row now keeps the active
session id the peer summary reports instead of claiming an active session with
no id.
Comment thread packages/coding-agent/src/modes/daemon/daemon-mode.ts
@snimu
snimu requested a review from sethkarten September 10, 2026 09:28
@snimu
snimu merged commit 4f4d51c into main Sep 10, 2026
23 checks passed
@snimu
snimu deleted the res-1335-one-roster branch September 10, 2026 17:15
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.

2 participants