fold the twin agent rosters into one - #2153
Merged
Merged
Conversation
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
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ 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.
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.
sethkarten
approved these changes
Sep 10, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

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.sendalready uses to resolvereceiver_roleandreceiver_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:
activeSessionIdis present only for a member with a live session in this daemon.statuskeeps 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 asendcall.The current agent is no longer duplicated: it stays in
currentand is not repeated inagents.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 markedinactive.createAgentObserveListResultwalkedlistTargetableSessionStatesplus passive subagents only, so saved inactive roots and peers in other workers were never observable. That is the scope gap the unified roster closes.buildAgentFamilyRosteris replaced byselectAgentFamily, which returns{relationship, entry}members over the same catalog entries. The observe list maps those members to summaries;sendresolution filters the same members. One backend, one truth.Messaging without observation
Checked, and no such configuration ships.
agentMessageControllerandagentObserveControllerare constructed only by daemon-mode, always as a pair (three construction sites).sdk.tsandmain.tsonly 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_agentshost request still exists and now always fails with: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.mdnow point atagent_observe.list_agents().sendguidance is unchanged.LOC
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_agentskernel 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 checkat the repo root: clean.npx tsgo --noEmit -p tsconfig.json: clean.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
parentActiveSessionIdorrlmParentNodeId. That is deliberate:relationshipalready names the link to the current agent, andrlm.list_subagents()owns the child handles. No in-tree consumer reads those fields off observe rows.sendresolution.get_agentandrecent_messageskeep 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 whatsendcan address, which is the invariant worth keeping; the header-only case is rare (in-daemon spawns and passive hydration both write metadata).repliedSinceTaskor status. It self-heals on the next call, so no mechanism was added for it.activeSessionIdmeans "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 makingagent_observe.list_agents()the one roster for parent, siblings, and children—including inactive, on-disk, and remote peers—with arelationshipfield aligned withagent_message.sendaddressing.Backend consolidation:
roster()/buildAgentFamilyRosterbecomefamily()/selectAgentFamilyover an expanded family catalog; observe listing maps those members to live or persisted summaries (optionalactiveSessionId, previews capped atAGENT_OBSERVE_PREVIEW_MAX_CHARS). Broadcast and role-basedagent_message.sendresolution use the samefamily()list. Stale callers hittingagent_message.list_agentsget 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_agentsagent_message.list_agentsand makingagent_observe.list_agentsthe single source for family discovery.agent_observe.list_agentsnow returns the full nuclear family, including inactive, persisted, and remote members with relationship data.agent_message.list_agentsnow rejects with a migration error directing callers toagent_observe.list_agents. The controller contract now requiresfamily().Macroscope summarized 415e713.