Skip to content

Session replay: Claude Code mapper — Task/Agent/Workflow fanout, inline sidechains, nested delegation #4815

Description

@vivekchand

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:
    1. A session with ≥1 Task spawn (child renders inline, expandable)
    2. A session with a Workflow fanout ≥5 agents (workflow swimlane renders above the transcript)
    3. 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)

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions