Skip to content

Session replay: canonical schema + replay_events table + /api/replay-tree endpoint + renderer skeleton #4813

Description

@vivekchand

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

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions