Context
Claude Code is the highest-fanout runtime we support — memory + observed data confirm sessions with 20+ Agent/Task calls in this repo alone (top-level pendingWorkflowCount and pendingBackgroundAgentCount counters prove the harness tracks concurrent spawns).
Today the transcript viewer shows the parent turn with a single collapsed "Task" tool chip — the child's actual work (thinking, tools, turns) is never inlined. Users have to jump to the Subagents modal and open a second transcript. Nested delegation collapses entirely.
Substrate
- Parent JSONL:
~/.claude/projects/<cwd-slug>/<session-uuid>.jsonl
- Sub-agent JSONLs:
<parent-dir>/<parent-uuid>/subagents/agent-*.jsonl, one file per child (../clawmetry-pro/clawmetry_pro/adapters/claude_code.py:78, :199)
- Meta:
<parent-dir>/<parent-uuid>/subagents/agent-<uuid>.meta.json — has description, agentType
Sub-agent model
Invoked via the Task tool (legacy) or the newer Agent tool. Context passed = the caller's input.prompt string. Reply comes back as a tool_result whose text is the child's final message. Child linkage back to parent via parentUuid, logicalParentUuid, sourceToolAssistantUUID, sourceToolUseID, isSidechain: true.
subagent_type: "fork" inherits the parent's full context (verified in Anthropic Agent tool docs).
Workflow model
The Workflow tool persists a workflow script; each agent it fans out shows up as a normal Task/Agent tool_use in the parent, plus its own sidechain file. No separate DAG table. Reconstruct DAG by grouping sidechains by sourceToolAssistantUUID.
Mode + approvals
- Mode:
{"type":"permission-mode","permissionMode":"default|acceptEdits|plan|bypassPermissions"} on every session, plus a per-turn permissionMode field on turns. bypassPermissions == YOLO. Separate mode events (normal/plan) reflect Plan Mode toggles mid-session.
- Approvals: no discrete
permission_request record. A tool_use followed by a tool_result = approved. A tool_use followed by a user message with toolDenialKind and no tool_result = denied. Denial reasons appear as top-level toolDenialKind, refusedUserMessageUuid. Model refusals appear as apiRefusalCategory / apiRefusalExplanation.
- Edit-approve: capture as
edit_diff when a second tool_use appears with different args after the same sourceToolUseID (see OSS-02 for schema).
Deliverable
Implement iter_replay_events(session_id) → Iterator[ReplayEvent] for the Claude family adapter (the family adapter is in OSS — Pro's claude_code.py extends it).
Output events per the canonical schema (OSS-01):
mode.changed on every permission-mode / mode event
llm.call / llm.response / thinking / tool.call / tool.result for the normal event stream
agent.spawn on every Task/Agent tool_use — payload includes description, subagent_type, prompt, child_session_id (from enumerating subagents/agent-*.jsonl and matching sourceToolAssistantUUID)
agent.return when the child's tool_result lands, with token_count, duration_ms, final_message_summary
workflow.start / workflow.stage / workflow.end when the Workflow tool is invoked — payload includes the workflow name + phases from the script's meta block
approval.requested / approval.decided per the OSS-02 mapping rules
Acceptance
- 3 recorded fixtures replay in the new UI:
- A session with ≥1
Task spawn (child renders inline, expandable)
- A session with a
Workflow fanout ≥5 agents (workflow swimlane renders above the transcript)
- A session with nested delegation depth ≥2 (child spawns grandchild — indent tree renders correctly)
pendingBackgroundAgentCount from the parent JSONL is surfaced as a workflow badge
- Mode chip shows the correct value for a
bypassPermissions fixture (red YOLO)
- Approvals rail shows denied approvals with
toolDenialKind as decision_reason
Depends on
- OSS-01 (schema + endpoint + renderer skeleton)
- OSS-02 (mode + approvals ingest + UI wiring)
Gotchas
- Sidechain 40:1 fanout — enumerate lazily on the endpoint side; do not eagerly parse all sidechain JSONLs at ingest time. Store a
child_session_ids: [] reference on the parent; parse-on-expand.
sourceToolAssistantUUID linking — the ONLY reliable way to tie a sidechain to the exact tool_use that spawned it. parentUuid chain within one file is not enough.
fork subagents inherit context — the child's first message is not the entire history; the UI should NOT re-render the parent context when opening a fork child.
References
- Substrate + linking:
../clawmetry-pro/clawmetry_pro/adapters/claude_code.py:78-90, :199-210, :642-644
- Existing
Task cost-split logic: routes/sessions.py:1308 (query_cost_split_with_subagents), :4486 (query_events_with_subagents)
run_ledger.parent_task_id: clawmetry/local_store.py:545
- Depth-1 grouping bug (nested spawns collapse):
routes/sessions.py:2984 (api_delegation_tree)
Context
Claude Code is the highest-fanout runtime we support — memory + observed data confirm sessions with 20+
Agent/Taskcalls in this repo alone (top-levelpendingWorkflowCountandpendingBackgroundAgentCountcounters prove the harness tracks concurrent spawns).Today the transcript viewer shows the parent turn with a single collapsed "Task" tool chip — the child's actual work (thinking, tools, turns) is never inlined. Users have to jump to the Subagents modal and open a second transcript. Nested delegation collapses entirely.
Substrate
~/.claude/projects/<cwd-slug>/<session-uuid>.jsonl<parent-dir>/<parent-uuid>/subagents/agent-*.jsonl, one file per child (../clawmetry-pro/clawmetry_pro/adapters/claude_code.py:78,:199)<parent-dir>/<parent-uuid>/subagents/agent-<uuid>.meta.json— hasdescription,agentTypeSub-agent model
Invoked via the
Tasktool (legacy) or the newerAgenttool. Context passed = the caller'sinput.promptstring. Reply comes back as atool_resultwhose text is the child's final message. Child linkage back to parent viaparentUuid,logicalParentUuid,sourceToolAssistantUUID,sourceToolUseID,isSidechain: true.subagent_type: "fork"inherits the parent's full context (verified in Anthropic Agent tool docs).Workflow model
The
Workflowtool persists a workflow script; each agent it fans out shows up as a normal Task/Agent tool_use in the parent, plus its own sidechain file. No separate DAG table. Reconstruct DAG by grouping sidechains bysourceToolAssistantUUID.Mode + approvals
{"type":"permission-mode","permissionMode":"default|acceptEdits|plan|bypassPermissions"}on every session, plus a per-turnpermissionModefield on turns.bypassPermissions== YOLO. Separatemodeevents (normal/plan) reflect Plan Mode toggles mid-session.permission_requestrecord. Atool_usefollowed by atool_result= approved. Atool_usefollowed by ausermessage withtoolDenialKindand notool_result= denied. Denial reasons appear as top-leveltoolDenialKind,refusedUserMessageUuid. Model refusals appear asapiRefusalCategory/apiRefusalExplanation.edit_diffwhen a secondtool_useappears with different args after the samesourceToolUseID(see OSS-02 for schema).Deliverable
Implement
iter_replay_events(session_id) → Iterator[ReplayEvent]for the Claude family adapter (the family adapter is in OSS — Pro'sclaude_code.pyextends it).Output events per the canonical schema (OSS-01):
mode.changedon everypermission-mode/modeeventllm.call/llm.response/thinking/tool.call/tool.resultfor the normal event streamagent.spawnon everyTask/Agenttool_use — payload includesdescription,subagent_type,prompt,child_session_id(from enumeratingsubagents/agent-*.jsonland matchingsourceToolAssistantUUID)agent.returnwhen the child'stool_resultlands, withtoken_count,duration_ms,final_message_summaryworkflow.start/workflow.stage/workflow.endwhen theWorkflowtool is invoked — payload includes the workflow name + phases from the script'smetablockapproval.requested/approval.decidedper the OSS-02 mapping rulesAcceptance
Taskspawn (child renders inline, expandable)Workflowfanout ≥5 agents (workflow swimlane renders above the transcript)pendingBackgroundAgentCountfrom the parent JSONL is surfaced as a workflow badgebypassPermissionsfixture (red YOLO)toolDenialKindas decision_reasonDepends on
Gotchas
child_session_ids: []reference on the parent; parse-on-expand.sourceToolAssistantUUIDlinking — the ONLY reliable way to tie a sidechain to the exact tool_use that spawned it.parentUuidchain within one file is not enough.forksubagents inherit context — the child's first message is not the entire history; the UI should NOT re-render the parent context when opening a fork child.References
../clawmetry-pro/clawmetry_pro/adapters/claude_code.py:78-90,:199-210,:642-644Taskcost-split logic:routes/sessions.py:1308(query_cost_split_with_subagents),:4486(query_events_with_subagents)run_ledger.parent_task_id:clawmetry/local_store.py:545routes/sessions.py:2984(api_delegation_tree)