acpmux: lineage is a field (parent, root, depth, spawn tool call)
What
PR 1 of the session-lineage brief: acpmux records lineage as a field, once, at session creation, from the proven caller.
session/newtakes its parent from the session the connection proved it runs in atinitialize(an agent's shell runningacpmux new/acpmux run)._meta.acpmux.parentmay name the caller itself or one of its lineage descendants; anything else is refused (lineage.parent_not_owned), so a session cannot claim a parent it does not own.parent: null, an ephemeral run, and an adopted chat make a root.- Persisted in
SessionMeta.lineage {parent, root, depth, toolCallId?}(skip_serializing_ifnone; old metas load with none).SubsessionLinkkeeps its sub-session-only fields; a sub-session saved before lineage existed derives its lineage from its link (Hub::lineage_of), so old stores need no migration. start_sessionfillstoolCallIdfrom the caller's in-flightstart_sessiontool call (tracked fromtool_callupdates; a prompt match, else the only running call, else none).acpmux newsendsACPMUX_TOOL_CALL_IDwhen the harness exports it.- Summary gains
parent,root,depth(lineage depth) andspawnToolCallId.spawnedBystays as the one-level alias (now from lineage, else the legacy tag), so the app and webviews keep working unchanged. The CLI only sends the legacyspawnedBytag when the daemon recorded no parent (older daemon, peer session). - Depth limits (
agents.subsessions.maxDepth) apply toacpmux newas they do tostart_session: one depth for every way in. - A caller whose session is gone gets a root, never an error. Forks and moved-in sessions are roots. Peer-forwarded
session/newstripsparent/spawnToolCallId(a family lives on one daemon). - Protocol spec and generated web types regenerated.
acpmux ls --tree (+ --json) is split into a follow-up PR to keep this one under ~800 lines.
Why
Leo: "LIKE E.G. AGENT SESSION CREATION LINEAGES... CLAUDE CODE MOSTLY JUST HAS ONE LAYER... WHAT IF WE DID MORE? OR DIFFERENT?"
Lineage is the data the sidebar nesting (later PRs) is derived from; today acpmux new only sets a best-effort one-level tag after creation.
Leo-ask: ask-19882abbc3
Tests
tests/session_lineage.rs: app-started session is a root; CLI child (proven caller) records parent/root/depth 1/toolCallId with no tag request; child of a child gets depth 2 and the same root; maxDepth refuses a third level for bothsession/newandstart_session;parent: nulland ephemeral make roots; spoofed parents refused (from the app and from a child naming its ancestor); a sub-session gets the same lineage; a gone caller leaves a root; lineage survives a daemon restart.- Unit tests:
hub/lineage.rs(parent ask parsing, tool call id bounds, start_session call tracking, store round trip and old meta),client.rs(env tool call, parentless, tag only when no parent recorded).
No capture: acpmux daemon/CLI only, no UI
Agent session: cosmic-gray
![View with [code]smith](https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-dark-v3-3x.png)
![Autofix with [code]smith](https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-dark-3x.png)
Need help on this PR? Tag @codesmith-bot with what you need.
Summary by cubic
Records the ancestry of agent-started sessions as first-class lineage fields instead of the best-effort one-level spawnedBy tag, giving the UI real data for nesting.
- Session summaries now expose
parent,root,spawnToolCallId, and lineage-baseddepth;spawnedBystays as a one-level alias limited to roots, so existing clients keep working. - A named parent is accepted only when it is the proven caller itself or a descendant, checked even on requests that record no lineage; other refusals carry
lineage.parent_not_owned. - Tool call IDs come from the caller's in-flight
start_sessioncall orACPMUX_TOOL_CALL_ID. - Old sessions derive lineage from their
SubsessionLink; no migration needed. agents.subsessions.maxDepthapplies toacpmux newlike it does tostart_session, and forks plus moved-in sessions are roots.- The CLI sends the legacy tag only to daemons that recorded no lineage (an older daemon, a peer); a lineage-recording daemon's root is never tagged, and peer-forwarded requests strip lineage fields.