cmux NEXT: show and open a Cloud VM chat's child chats
When an agent chat runs on a cmux Cloud VM and starts child chats there (cmux-subsessions start_session, or acpmux new inside the session, which sets spawnedBy), those children now show on the Mac: in the parent chat's Subagents summary and in the sidebar's session tree under the parent, and a Subagents row opens the child's chat.
Design
- The Mac reaches a VM chat only through the VM daemon's
agent-session-attach-v1on one store tab (RemoteAcpmuxWireover the Cloud link). That link served only the tab's own session, so the VM's other sessions, the children included, never reached the app. - New scoped verb
agent-session-children {surface}(capabilityagent-session-children-v1) on an attached or observed tab. The daemon reads acpmux's_acpmux/sessionsitself and answers only the attached session's descendants: chats whosestartedBy(sub-session link, any depth) orspawnedBynames the session or another descendant. It never returns the session itself, another chat, an unrelated chat's children, or a peer daemon's session. The verb takes no session param (anything else isbad_request) and is refused on the remote relay, on WebSocket clients and before attach, the same as every other attach verb. - The attachment follows those descendants. Their
_acpmux/session_changednotices, and those of a new chat whose summary names the session or a followed chat as its parent, go out asagent-session-changedon the tab. A child that appears, runs, waits or finishes reaches the app live, with no polling. - Opening a child: every descendant the daemon listed or followed is remembered (from acpmux's answer, never a client's word, bounded at 1024).
new-conversation-tabmay file one under this store's ownregistry:host, the same exception that already exists for landed (moved) chats. Any other session in aregistry:tab is stillconversation_tab.machine_mismatch(cx-k21v holds). - Swift:
RemoteAcpmuxWire's_acpmux/watchnow returns the chat plus its listed children, so the page's existingspawnedSessions/startedSessionscode draws them in Summary > Subagents. Their changes arrive as_acpmux/session_changed. The children's parents go intoRemoteAgentSessionLinks, which feedsSidebarSessionTreeFeed(remote tabs nest under their parent's workspace) andchat.sideChat/chat.openTab(a remote chat may open a listed child beside it, in aregistry:tab of that machine, attached through the same link). - The app calls the verb only on a daemon that advertises it. An older VM daemon lists no children, which is today's behavior.
Changes
cmux-tui-core/src/server/agent_session_children.rs(new): the descendant walk, the verb, the follow set and the known-children list.agent_session_attach.rs/agent_session_stream.rs: the verb's routing. The attach seeds the follow set from the watch answer it already made and streams descendants' changes. A child change on a tab that no longer shows the session ends the attachment (detached).conversation_tabs_wire.rs: the known-children exception forregistry:tabs.capabilities.rs,spec/commands.md: the capability and the spec.- Swift:
RemoteAcpmuxWire,AgentSessionRemoteClient.children(),AgentSessionAttachClient.children(),RemoteAgentSessionLinks(new),SidebarSessionTreeFeed,AgentPaneSideChat.holds,AgentTabStore.open(a listed remote child opens on its machine), and theDaemonCapabilities.agentSessionChildrenconstant.
Tests
- Rust unit tests (
agent_session_children.rs): both links at any depth, list order, no root or peers, a link loop ends, the 256 cap, the known-children bound. - Rust daemon tests (
agent_session_children_tests.rs, fake acpmux): binds a tab to a session and checks thatagent-session-childrenlists exactly the grandchild, sub-session andspawnedBychild, never the unrelated chat, its child or a peer child. Also checked: refused before attach, refused with session/extra params, refused on the relay gate and on WebSocket; children'ssession_changedare streamed and others are not, including a new child and its own child; a listed or followed child opens in aregistry:tab of this store, while unrelated, peer and root sessions and another store's host are refused. - End to end (
cmux-tui/tests/vm_child_sessions_journey.rs, real daemon and acpmux with the test agent): a parent chat's tab attaches,cmux acp newruns withACPMUX_SESSION_ID= parent, the attachment hears the child appear,agent-session-childrenlists exactly that child withspawnedBy= parent (never the machine's other chat), and the child opens in aregistry:tab that attaches to it, while the other chat is refused. - Swift (
RemoteChildSessionsTests, Mac CI): the wire's watch lists the chat plus its children and notes their parents; the links' loop and one-parent rules; the sidebar tree nests a remote child's workspace under its parent's; a remote chat opens a listed child beside it, and an unlisted one is refused.
Follow-ups (not in this PR)
- Notifications held while an attach connects (
ReaderSlot::hold) are filtered to the attached session once it resolves, so a child's change in that window is dropped; the app's next_acpmux/watch(agent-session-children) lists it, but the held-notice path should keep followed descendants too. - App-side machine tie:
RemoteAgentSessionLinksis keyed by session id only, not by machine; a child opens in the pane's machine and that machine's daemon checks it against its own list, but the app should key links by machine. - Trust: the links come from the machine's acpmux list (peers filtered), not proof of who started what: the root is the tab the trusted client attached, and
spawnedByis set fromACPMUX_SESSION_IDwith no caller token.
Changelog
Added: A cmux Cloud VM chat's child chats now show under it in its Subagents summary and the sidebar's session tree, and open from there.
No capture: the new rows only appear when a cmux Cloud VM's acpmux has child sessions, and a Mac tour can't start one. The Swift side is covered by RemoteChildSessionsTests, the daemon side by the session_children journey test.
🤖 Generated with Claude Code
Agent session: cosmic-gray