* Add provider-owned subagent stream contract * OpenCode provider: detect child sessions, attach adopted children to the hosting server, synthesize external turns - Emit internal provider_child_session_detected / provider_session_deleted stream events from the global event stream (before the no-active-turn gate), including BFS hydration of existing descendants on subscribe - Track child sessions in a registry mapping child session id -> hosting server url; resumeSession attaches via acquireExisting() so adopted children share the parent's server generation and keep receiving live events - Synthesize external turns for adopted (externally driven) sessions so timeline, tool calls, and permission requests stream without a Paseo-initiated prompt - Honor launchContext.env in resumeSession (acquireDedicated parity with createSession) - Stop folding child tool parts into the parent's sub_agent action log; the parent row stays as a pointer * Render provider subagents in workspace timelines * fix provider subagent timeline reliability * fix provider subagent lifecycle edge cases * fix provider subagent event fidelity * Fix provider subagent history fidelity * Fix restored provider subagent state * Fix provider subagent visibility edges * Restore provider subagent timelines * Page provider subagent history * Forward subagent questions reliably * fix(subagents): preserve OpenCode child context * fix(subagents): harden provider timeline routing * fix(subagents): preserve provider page continuity * fix(subagents): retain live child prompts --------- Co-authored-by: Omer <639682+omercnet@users.noreply.github.com>
10 KiB
Agent lifecycle
How an agent is created, runs, becomes a subagent, gets archived, and disappears from the UI. The model spans the daemon (lifecycle, archive) and the client (tabs, the subagents track).
States
initializing → idle → running → idle (or error → closed)
↑ │
└────────┘ (agent completes a turn, awaits next prompt)
Each agent in AgentManager carries a lastStatus of initializing, idle, running, error, or closed. State transitions persist to disk and stream to subscribed clients via WebSocket.
Relationships
Agents can launch other agents via the agent-scoped create_agent MCP tool. Agent-scoped creation is always asynchronous. relationship and workspace are separate decisions:
relationshipdecides whether the new agent belongs under the caller.workspacedecides where the new agent lives and whether a new workspace/worktree is created.
relationship: { kind: "subagent" } stamps the created agent with paseo.parent-agent-id, pointing back at the creating agent. The client surfaces that as agent.parentAgentId. This requires an agent-scoped MCP session.
relationship: { kind: "detached" } creates a sibling/root agent (e.g. handoffs, fire-and-forget delegations). The daemon may still use the creating agent for cwd/config inheritance, but it does not write paseo.parent-agent-id.
- Subagents — exist as part of the creating agent's work, appear in that agent's subagent track, and are archived with it.
- Detached agents — stand on their own, do not appear in the creating agent's subagent track, and are not archived with it.
workspace: { kind: "current" } uses the caller's workspace and can optionally override the runtime cwd. It requires an agent-scoped MCP session. workspace: { kind: "create", source: { kind: "directory" | "worktree", ... } } creates a new workspace for the new agent; worktree creation goes through the Paseo worktree workflow and stamps the agent with that fresh workspace id.
Users can also detach an existing subagent from the subagents track. Detach removes the paseo.parent-agent-id label only: it does not stop, archive, move, or restart the agent. The agent keeps its current cwd and workspaceId, leaves the former parent's track, and behaves like a root agent for tab close, workspace activity, and future parent archive.
notifyOnFinish defaults to true for agent-scoped creation and background prompt follow-ups because most delegated work needs to report back to the creating agent. Set it to false only for truly fire-and-forget agents or prompts.
Archive
Archive is a soft delete: the agent record stays on disk with archivedAt set, the runtime is closed, and the agent disappears from active lists. Archive is global — it lives on the server and propagates to every connected client.
create_agent_request can opt an agent into autoArchive. In that mode the daemon archives the agent after the first terminal turn event (turn_completed, turn_failed, or turn_canceled). If the same request created a Paseo worktree through its worktree field, auto-archive archives that worktree too, which removes the agent records inside the worktree.
Archiving runs through AgentManager.archiveAgent (packages/server/src/server/agent/agent-manager.ts):
- Snapshot the current session into the registry
- Set
archivedAtand normalizelastStatusaway fromrunning/initializing - Notify subscribers
- Close the runtime (kills the process if still running)
- Cascade-archive children — any agent whose
paseo.parent-agent-idlabel matches the archived agent gets archived too, recursively
Cascade is what keeps subagent fleets from outliving their orchestrator.
Tabs vs archive
These are two distinct concepts that used to be conflated:
| Concept | Scope | Triggers |
|---|---|---|
| Tab (workspace layout) | Per-client | User opens/closes a view |
| Archive (lifecycle) | Global | Explicit lifecycle gesture |
Closing a tab on a root agent still archives — the tab is the agent's home, so closing it means "I'm done with this agent." A confirm dialog protects against archiving a running agent by accident.
Closing a tab on a subagent (any agent with parentAgentId) is layout-only. The agent stays unarchived and stays in its parent's track. The user can re-open the tab from the track at any time. This is implemented in handleCloseAgentTab (packages/app/src/screens/workspace/workspace-screen.tsx).
The asymmetry is intentional: a subagent's home is the parent's track, not the tab. Tabs are ephemeral viewing slots; the track is the persistent record of the parent's children.
Workspace activity
Agent lifecycle status stays literal: a parent agent is idle when its own turn is idle, even if a child is running.
Workspace status is an aggregate activity signal computed per workspaceId: a workspace's status reflects only records whose workspaceId === workspace.id. Ownership is never derived from cwd — many workspaces may share one directory, and same-cwd siblings do not clump under one status. A root agent contributes its normal state bucket to its owning workspace only. Running subagents contribute running to their root parent's owning workspace (by the parent agent's workspaceId), not to the subagent's current cwd or worktree. Non-running subagent attention, permission, and error states stay in the parent's subagents track and do not escalate the workspace bucket.
The subagents track
The collapsible track above the composer in an agent's pane (packages/app/src/subagents/track.tsx) combines two kinds of children:
- Paseo subagents are full managed agents. Their membership rule (
packages/app/src/subagents/select.ts) is:
parentAgentId === thisAgent.id AND !archivedAt
- Provider subagents are child executions owned by Claude, Codex, or OpenCode. They are not inserted into
AgentManageras managed agents. Providers emit a separate descriptor and timeline stream throughagent.provider_subagents.*; the client keeps that state outside the normal agent store and merges only the presentation rows into the track.
Clicking either kind opens a workspace tab. A Paseo subagent tab is a normal interactive agent pane. A provider subagent tab is a read-only timeline pane with no composer, archive, detach, rewind, or fork actions. Both panes use AgentStreamView, so message, reasoning, tool-call, and layout rendering stay identical.
Provider timelines use the same structural timeline item format but deliberately have a separate lifecycle and transport. A provider thread/session identifier is not a Paseo agent identifier, and closing its tab is always layout-only.
Archived Paseo subagents disappear from the track, by design. To remove one from the track without closing its tab, use the archive button (X) on the row — it opens a confirm dialog and archives the subagent on confirm. Provider-owned rows have no Paseo lifecycle controls and disappear only when the provider removes them or the parent session is discarded.
To keep the agent alive but remove it from the parent's track, use detach. The daemon clears the parent label, emits the normal agent update, and every client reclassifies the agent from subagent to root/sibling from that updated snapshot.
Why this shape
The decision was to decouple "close tab" from "archive" only for subagents, rather than universally:
- Closing a tab on a root agent still archives — preserves the existing UX users are trained on
- Closing a tab on a subagent is layout-only — fixes the lossy "click to read, close to dismiss view, lose the row" flow
- Archive button on track rows — gives subagents an explicit lifecycle gesture in their home surface
- Detach button on track rows — lets a subagent continue independently without killing its work
- Cascade archive on parent — keeps subagents from leaking when the parent is archived
We considered universal decoupling (no tab close ever archives, archive is always explicit) but rejected it: it changes a behavior root-agent users rely on.
Limitations
Subagent accumulation under long-lived parents
A parent that spawns many subagents will see the track grow. There's no automatic cleanup for completed subagents — the user prunes via the archive button on each row. A bulk gesture (e.g. "archive all idle children") could land later if this becomes a real problem.
Cross-client tab dismissal
Closing a subagent's tab on one client doesn't affect other clients' layouts. This is the expected behavior of decoupled tabs and is consistent with how layouts have always worked. Archive remains the global gesture for cross-client cleanup.
Storage
$PASEO_HOME/agents/{cwd-with-dashes}/{agent-id}.json
{cwd-with-dashes} is derived from the agent's filesystem cwd. It is not the workspace id; agent storage stays cwd-keyed while workspace identity is the opaque workspace id.
Each agent is a single JSON file. Fields relevant to this doc:
| Field | Type | Meaning |
|---|---|---|
id |
string |
Stable identifier |
archivedAt |
string? |
Soft-delete timestamp (ISO 8601) |
labels["paseo.parent-agent-id"] |
string? |
Parent agent ID, set automatically by create_agent when relationship.kind === "subagent" |
lastStatus |
AgentStatus |
initializing / idle / running / error / closed |
See docs/data-model.md for the full agent record.