Context
Claude Code's Task tool spawns subagents whose transcripts land in sidecar .jsonl files at:
~/.claude/projects/<slug>/<sessionId>/subagents/agent-<agentId>.jsonl
The parent session's tool_result row for the Task tool_use carries a toolUseResult.agentId that references the sidecar's filename. Burn's current ingest may drop these or attribute them imprecisely; we need a structural pairing pass plus a first-class "orphan" bucket for sidecars that don't pair (e.g., crash mid-dispatch, slash-command synthetic dispatches).
Prior art: agent-profiler handles both cases. Source:
lib/claude-code/sessions.js — discovery walks <sessionId>/subagents/agent-*.jsonl.
lib/claude-code/traces.js — toolUseResult.agentId linkage and UnattachedGroup synthesis for orphans.
lib/traces/types.d.ts — UnattachedGroup type: "skill-dispatched subagent subtree with no paired tool_use in the main transcript".
Proposal
- Discovery: extend the Claude reader to enumerate
<sessionId>/subagents/agent-*.jsonl alongside the main <sessionId>.jsonl.
- Pairing: for each subagent transcript, find the parent session's
tool_result row whose toolUseResult.agentId matches. Attach the subagent's records under that tool_use's span subtree.
- Orphan bucket: any subagent transcript that doesn't pair becomes an
UnattachedGroup — a top-level span with no parent ToolUse. Surface it in span tree output and in the inference-flow DAG with an unattached edge style.
- Subagent metadata: read the companion
agent-<id>.meta.json if present for agentType and other harness-emitted attributes.
Implementation sketch
// crates/relayburn-sdk/src/reader/claude/subagents.rs (new)
pub struct SubagentTranscript {
pub agent_id: String,
pub agent_type: Option<String>, // from .meta.json
pub records: Vec<Record>,
pub paired_tool_use_id: Option<String>, // None => UnattachedGroup
}
pub fn discover_subagents(session_dir: &Path, session_id: &str) -> Vec<SubagentTranscript>;
pub fn pair_to_main(main: &[Record], subs: Vec<SubagentTranscript>) -> Vec<SubagentTranscript>;
Wire into the span-tree builder so subagent records become a nested Subagent subtree at the correct ToolUse node, or a top-level UnattachedGroup when unpaired.
Open questions
agentId extraction. Is it always on toolUseResult.agentId of the Task tool_result, or also embedded elsewhere (assistant tool_use input)? Audit a few real sessions.
- Slash-command synthetic dispatches. Some subagents are spawned by slash commands, not Task. These are expected to be unpaired in the main transcript. Don't treat as errors — treat as
UnattachedGroup and let the slash-command triad pass (separate issue) optionally nest them under a synthesized Skill span.
- Performance. Subagent sidecar count per session can reach hundreds in agent-heavy workflows. Discovery should be lazy — only walk the subagents directory when something asks for it.
- Schema. Should subagent transcripts ingest into the same
turns table with a subagent_id column, or a separate subagent_turns table? Recommend same table with subagent_id nullable — keeps queries uniform.
Acceptance
References
- agent-profiler:
lib/claude-code/sessions.js, lib/claude-code/traces.js subagent pairing logic, lib/traces/types.d.ts UnattachedGroup.
- Related: span-tree foundation (provides the Subagent node level), inference-flow DAG (renders the dispatch/return/unattached edges).
Context
Claude Code's Task tool spawns subagents whose transcripts land in sidecar
.jsonlfiles at:The parent session's
tool_resultrow for the Task tool_use carries atoolUseResult.agentIdthat references the sidecar's filename. Burn's current ingest may drop these or attribute them imprecisely; we need a structural pairing pass plus a first-class "orphan" bucket for sidecars that don't pair (e.g., crash mid-dispatch, slash-command synthetic dispatches).Prior art: agent-profiler handles both cases. Source:
lib/claude-code/sessions.js— discovery walks<sessionId>/subagents/agent-*.jsonl.lib/claude-code/traces.js—toolUseResult.agentIdlinkage andUnattachedGroupsynthesis for orphans.lib/traces/types.d.ts—UnattachedGrouptype: "skill-dispatched subagent subtree with no paired tool_use in the main transcript".Proposal
<sessionId>/subagents/agent-*.jsonlalongside the main<sessionId>.jsonl.tool_resultrow whosetoolUseResult.agentIdmatches. Attach the subagent's records under that tool_use's span subtree.UnattachedGroup— a top-level span with no parentToolUse. Surface it in span tree output and in the inference-flow DAG with anunattachededge style.agent-<id>.meta.jsonif present foragentTypeand other harness-emitted attributes.Implementation sketch
Wire into the span-tree builder so subagent records become a nested
Subagentsubtree at the correctToolUsenode, or a top-levelUnattachedGroupwhen unpaired.Open questions
agentIdextraction. Is it always ontoolUseResult.agentIdof the Task tool_result, or also embedded elsewhere (assistant tool_use input)? Audit a few real sessions.UnattachedGroupand let the slash-command triad pass (separate issue) optionally nest them under a synthesized Skill span.turnstable with asubagent_idcolumn, or a separatesubagent_turnstable? Recommend same table withsubagent_idnullable — keeps queries uniform.Acceptance
<sessionId>/subagents/agent-*.jsonlautomatically.tool_resultrows withtoolUseResult.agentIdpair to the matching sidecar.UnattachedGroupin the span tree output.agent-<id>.meta.jsonis read when present andagentTypeis exposed as a span attribute.burn summary(or newburn subagents) counts pairings and orphans.References
lib/claude-code/sessions.js,lib/claude-code/traces.jssubagent pairing logic,lib/traces/types.d.tsUnattachedGroup.