cmux NEXT: show and open a Cloud VM chat's child chats

Merged
TE
15d ago
+1017-41

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

  1. The Mac reaches a VM chat only through the VM daemon's agent-session-attach-v1 on one store tab (RemoteAcpmuxWire over 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.
  2. New scoped verb agent-session-children {surface} (capability agent-session-children-v1) on an attached or observed tab. The daemon reads acpmux's _acpmux/sessions itself and answers only the attached session's descendants: chats whose startedBy (sub-session link, any depth) or spawnedBy names 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 is bad_request) and is refused on the remote relay, on WebSocket clients and before attach, the same as every other attach verb.
  3. The attachment follows those descendants. Their _acpmux/session_changed notices, and those of a new chat whose summary names the session or a followed chat as its parent, go out as agent-session-changed on the tab. A child that appears, runs, waits or finishes reaches the app live, with no polling.
  4. 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-tab may file one under this store's own registry: host, the same exception that already exists for landed (moved) chats. Any other session in a registry: tab is still conversation_tab.machine_mismatch (cx-k21v holds).
  5. Swift: RemoteAcpmuxWire's _acpmux/watch now returns the chat plus its listed children, so the page's existing spawnedSessions/startedSessions code draws them in Summary > Subagents. Their changes arrive as _acpmux/session_changed. The children's parents go into RemoteAgentSessionLinks, which feeds SidebarSessionTreeFeed (remote tabs nest under their parent's workspace) and chat.sideChat/chat.openTab (a remote chat may open a listed child beside it, in a registry: tab of that machine, attached through the same link).
  6. 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 for registry: 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 the DaemonCapabilities.agentSessionChildren constant.

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 that agent-session-children lists exactly the grandchild, sub-session and spawnedBy child, 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's session_changed are streamed and others are not, including a new child and its own child; a listed or followed child opens in a registry: 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 new runs with ACPMUX_SESSION_ID = parent, the attachment hears the child appear, agent-session-children lists exactly that child with spawnedBy = parent (never the machine's other chat), and the child opens in a registry: 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: RemoteAgentSessionLinks is 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 spawnedBy is set from ACPMUX_SESSION_ID with 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