Context
The transcript viewer is a single flat renderer driven by one endpoint (/api/transcript/<id>) that assumes a linear message list. It's runtime-blind — every runtime (Claude Code with its 40+ sidechain Task/Agent/Workflow fanouts, OpenClaw with its subagent_runs + flow_runs tree, Codex with per-turn approval_policy, Antigravity with cascade_id sub-trajectories, n8n as a workflow-first DAG…) is coerced into the same flat shape. Sub-agent trees collapse. Workflow fanouts collapse. Mode changes vanish. Approvals never render.
This meta issue lands the neutral foundation everything else builds on.
Deliverables
1. Canonical replay-event schema — new module clawmetry/replay_schema.py. Every adapter maps its native events into this shape:
ReplayEvent = {
"ts": float, # unix seconds
"kind": str, # see enum below
"span_id": str, # stable id for this event
"parent_span_id": str|None, # tree edge (delegation parent, NOT transcript-chain parent)
"session_id": str,
"runtime": str, # "claude_code" | "openclaw" | ...
"payload": dict, # kind-specific
"mode": { # None if unchanged
"permission": str|None, # "default"|"acceptEdits"|"plan"|"bypassPermissions"|"yolo"|None
"sandbox": str|None, # "read-only"|"workspace-write"|"danger-full-access"|None
"collaboration": str|None,
} | None,
"approval": { # only on kind=approval.*
"status": str, # "requested"|"approved"|"denied"|"edited"|"timeout"
"decision_reason": str|None,
"resolver": str|None, # "user"|"hook"|"policy"
"edit_diff": dict|None, # {"before": args, "after": args} — new field
} | None,
}
kind enum: llm.call | llm.response | tool.call | tool.result | agent.spawn | agent.return | workflow.start | workflow.stage | workflow.end | approval.requested | approval.decided | mode.changed | thinking | compaction.
2. DuckDB table replay_events in clawmetry/local_store.py. Indices on (session_id, ts), (parent_span_id), (runtime, kind). Written by the sync daemon (writer lock owned by daemon per feedback_local_store_fetch_takes_writelock.md).
3. Endpoint /api/replay-tree/<session_id> in routes/sessions.py — returns a nested structure:
{
"session_id": ...,
"runtime": ...,
"mode": {...}, # latest resolved
"turns": [
{
"turn_id": ...,
"events": [...], # kind=llm.*/tool.*/thinking
"delegations": [ # kind=agent.spawn, nested
{"span_id": ..., "child_session_id": ..., "events": [...], "delegations": [...]}
],
"approvals": [...] # kind=approval.*, aligned to the turn
}
],
"workflows": [ ... ] # top-level workflow fanouts (Claude Code Workflow tool, OpenClaw flow_runs)
}
Reads from DuckDB via routes/local_query.py — per feedback_daemon_proxy_pattern.md, no direct writer-lock grab.
4. Renderer skeleton in clawmetry/static/js/app.js — new _renderReplayTree seam. Extends _renderTurnChapter (currently at app.js:15600) with a delegations prop and a new _renderSubagentBlock that renders nested children inline with indent + collapse. Runtime dispatcher in the same pattern as the Harness tab template system at app.js:16729.
Out of scope
- Runtime-specific
iter_replay_events() mappers — those are per-runtime follow-ups (Claude Code, OpenClaw, and 13 more in clawmetry-pro).
- Mode + approvals ingest — separate OSS issue.
- Hook lifecycle contract — separate OSS issue.
Acceptance
Gotchas
- "parent" is overloaded — transcript-chain parent (OpenClaw v3
parentId is a chain not a tree — reference_openclaw_v3_parentid_is_chain.md), delegation parent, and branch parent (Pi). Schema uses parent_span_id for the delegation edge only.
- Sidechain files can outnumber the parent 40:1 for Claude Code. Endpoint MUST support lazy child expansion — do not fetch all sidechains eagerly.
- SQLite runtimes (Hermes, opencode, Goose, Antigravity, OpenClaw, deepagents) actively write while we read — every read path uses
mode=ro.
References
- Audit: session-trace redesign extension points at
clawmetry/static/js/app.js:15420, :15600, :15676
- Endpoint patterns:
routes/sessions.py:4338 (api_transcript), :4575 (api_transcript_events)
- DuckDB conventions:
clawmetry/local_store.py:534 (run_ledger), :888 (spans)
- CLAUDE.md →
routes/local_query.py bridge rules
Context
The transcript viewer is a single flat renderer driven by one endpoint (
/api/transcript/<id>) that assumes a linear message list. It's runtime-blind — every runtime (Claude Code with its 40+ sidechainTask/Agent/Workflowfanouts, OpenClaw with itssubagent_runs+flow_runstree, Codex with per-turnapproval_policy, Antigravity withcascade_idsub-trajectories, n8n as a workflow-first DAG…) is coerced into the same flat shape. Sub-agent trees collapse. Workflow fanouts collapse. Mode changes vanish. Approvals never render.This meta issue lands the neutral foundation everything else builds on.
Deliverables
1. Canonical replay-event schema — new module
clawmetry/replay_schema.py. Every adapter maps its native events into this shape:kindenum:llm.call | llm.response | tool.call | tool.result | agent.spawn | agent.return | workflow.start | workflow.stage | workflow.end | approval.requested | approval.decided | mode.changed | thinking | compaction.2. DuckDB table
replay_eventsinclawmetry/local_store.py. Indices on(session_id, ts),(parent_span_id),(runtime, kind). Written by the sync daemon (writer lock owned by daemon perfeedback_local_store_fetch_takes_writelock.md).3. Endpoint
/api/replay-tree/<session_id>inroutes/sessions.py— returns a nested structure:Reads from DuckDB via
routes/local_query.py— perfeedback_daemon_proxy_pattern.md, no direct writer-lock grab.4. Renderer skeleton in
clawmetry/static/js/app.js— new_renderReplayTreeseam. Extends_renderTurnChapter(currently atapp.js:15600) with adelegationsprop and a new_renderSubagentBlockthat renders nested children inline with indent + collapse. Runtime dispatcher in the same pattern as the Harness tab template system atapp.js:16729.Out of scope
iter_replay_events()mappers — those are per-runtime follow-ups (Claude Code, OpenClaw, and 13 more inclawmetry-pro).Acceptance
replay_eventstable lands in DuckDB migrations/api/replay-tree/<id>returns the nested shape (emptydelegations/workflowsOK until mappers land)runtime_kindon its eventsGotchas
parentIdis a chain not a tree —reference_openclaw_v3_parentid_is_chain.md), delegation parent, and branch parent (Pi). Schema usesparent_span_idfor the delegation edge only.mode=ro.References
clawmetry/static/js/app.js:15420,:15600,:15676routes/sessions.py:4338(api_transcript),:4575(api_transcript_events)clawmetry/local_store.py:534(run_ledger),:888(spans)routes/local_query.pybridge rules