Context
Today the session UI shows nothing about how a session ran:
- Auto vs interactive vs plan vs
--dangerously-skip-permissions — Claude Code emits {"type":"permission-mode","permissionMode":"default"} on every session (verified in ../clawmetry-pro/tests/fixtures/runtimes/claude_code/…jsonl:1), but _parse_v3_event at clawmetry/sync.py:3186 never consumes it. No sessions column stores it. grep permissionMode|acceptEdits|bypassPermissions|autoAccept across clawmetry/, routes/, app.js returns zero hits.
- Approvals asked count — the
approvals DuckDB table (clawmetry/local_store.py:610) IS populated by the sync daemon (sync.py:7346). /api/session-governance/<sid> (routes/sessions.py:2887) returns {approvals[], decision_count, denied_count}. /api/session-insight/<sid> folds it into governance = {decision_count, denied_count}. The transcript viewer never fetches either endpoint.
- User replies to approvals —
approvals.decision + decision_reason + resolver are stored, rendered only in the NemoClaw tab (app.js:8177).
- "Edit" approvals — where the user modifies the tool args before approving (a real Claude Code flow) — has no column and no ingest path.
Deliverables
1. Ingest mode + policy signals
clawmetry/sync.py _parse_v3_event — consume permission-mode, mode, permissionMode (Claude Code); turn_context.approval_policy + sandbox_policy + permission_profile (Codex); session.auto_mode_resolved (Copilot); exec_approvals_config (OpenClaw); steps.permissions per step (Antigravity).
- New
sessions columns: mode_permission, mode_sandbox, mode_collaboration, approval_policy_default, resolved_at (latest resolved timestamp — modes can change per turn in Codex + Claude Code Plan Mode).
- Per-turn mode changes stored as
kind=mode.changed events in the new replay_events table (see OSS-01).
2. Extend approvals capture
- Add
edit_diff JSON column to approvals table — {before: tool_args, after: tool_args} captured whenever the user modifies the tool_use payload before approving. Claude Code's toolDenialKind maps to decision=denied; a subsequent tool_use with different args after the same sourceToolUseID maps to decision=edited with edit_diff.
- Capture
apiRefusalCategory / apiRefusalExplanation for model-side refusals as resolver=model.
- Capture
hookInfos / hookErrors / hookAdditionalContext for hook-driven auto-decisions as resolver=hook.
3. Wire the transcript viewer
/api/replay-tree/<id> (from OSS-01) includes approvals[] per turn.
- New UI in
clawmetry/static/js/app.js:
- Session-level mode chip in the transcript header — auto / plan / interactive / YOLO — with a tooltip listing sandbox + collaboration.
- Per-turn mode marker — small badge on turn chapters where mode changed (Codex per-turn, Claude Code Plan Mode toggles).
- Approvals rail running down the right side of the transcript, one row per approval, click to jump to the tool_use it gated. Colors: green approved, yellow edited, red denied, gray timeout.
- Per-turn approvals count on the TURNS TOC (next to the existing
🔧 N / ✕ N).
- Inline approval bubble rendered above the gated
tool_use chip, showing prompt (if captured — see hook-lifecycle issue), decision, decision_reason, resolver, edit_diff.
Acceptance
- Every ingested session has
mode_permission populated (or explicitly unknown for runtimes without a signal — see per-runtime issues)
/api/replay-tree/<id> returns approvals[] matching what /api/session-governance/<id> returns today
- Opening a session with ≥1 approval renders the approvals rail; clicking scrolls to the gated tool
- Fixture session with an edited approval shows the diff (before/after tool args)
- Sessions with
bypassPermissions/--dangerously-skip-permissions show a red YOLO chip
Depends on
Blocks
- Every per-runtime mapper issue (they need the columns + endpoint contract to write into)
Gotchas
- Approval outcome ≠ approval prompt. Almost every runtime persists only the decision, not the prompt text the user saw. Rebuilding the prompt for replay needs a runtime hook (see hook-lifecycle issue).
- Auto mode is per-turn (Codex, Claude Code), per-session (Copilot, OpenClaw), or per-project (opencode
permission table). A single session-level "auto" chip is misleading — badge at the turn level where the runtime supports it.
- Do not conflate
mode.changed events across runtimes with different vocabularies — keep the raw runtime value in payload, only normalize the top-level enum.
References
clawmetry/local_store.py:610 (approvals schema)
clawmetry/sync.py:7346 (approvals ingest today)
clawmetry/sync.py:3186 (_parse_v3_event — the seam to extend)
routes/sessions.py:2887 (/api/session-governance/<sid> — already returns most of what we need)
routes/sessions.py:2905 (/api/session-insight/<sid> — folds governance into waste_flags)
clawmetry/static/js/app.js:8177 (NemoClaw approvals renderer — reference implementation for the bubble)
Context
Today the session UI shows nothing about how a session ran:
--dangerously-skip-permissions— Claude Code emits{"type":"permission-mode","permissionMode":"default"}on every session (verified in../clawmetry-pro/tests/fixtures/runtimes/claude_code/…jsonl:1), but_parse_v3_eventatclawmetry/sync.py:3186never consumes it. Nosessionscolumn stores it.grep permissionMode|acceptEdits|bypassPermissions|autoAcceptacrossclawmetry/,routes/,app.jsreturns zero hits.approvalsDuckDB table (clawmetry/local_store.py:610) IS populated by the sync daemon (sync.py:7346)./api/session-governance/<sid>(routes/sessions.py:2887) returns{approvals[], decision_count, denied_count}./api/session-insight/<sid>folds it intogovernance = {decision_count, denied_count}. The transcript viewer never fetches either endpoint.approvals.decision+decision_reason+resolverare stored, rendered only in the NemoClaw tab (app.js:8177).Deliverables
1. Ingest mode + policy signals
clawmetry/sync.py_parse_v3_event— consumepermission-mode,mode,permissionMode(Claude Code);turn_context.approval_policy+sandbox_policy+permission_profile(Codex);session.auto_mode_resolved(Copilot);exec_approvals_config(OpenClaw);steps.permissionsper step (Antigravity).sessionscolumns:mode_permission,mode_sandbox,mode_collaboration,approval_policy_default,resolved_at(latest resolved timestamp — modes can change per turn in Codex + Claude Code Plan Mode).kind=mode.changedevents in the newreplay_eventstable (see OSS-01).2. Extend approvals capture
edit_diffJSON column toapprovalstable —{before: tool_args, after: tool_args}captured whenever the user modifies the tool_use payload before approving. Claude Code'stoolDenialKindmaps todecision=denied; a subsequenttool_usewith different args after the samesourceToolUseIDmaps todecision=editedwithedit_diff.apiRefusalCategory/apiRefusalExplanationfor model-side refusals asresolver=model.hookInfos/hookErrors/hookAdditionalContextfor hook-driven auto-decisions asresolver=hook.3. Wire the transcript viewer
/api/replay-tree/<id>(from OSS-01) includesapprovals[]per turn.clawmetry/static/js/app.js:🔧 N/✕ N).tool_usechip, showing prompt (if captured — see hook-lifecycle issue), decision, decision_reason, resolver, edit_diff.Acceptance
mode_permissionpopulated (or explicitlyunknownfor runtimes without a signal — see per-runtime issues)/api/replay-tree/<id>returnsapprovals[]matching what/api/session-governance/<id>returns todaybypassPermissions/--dangerously-skip-permissionsshow a red YOLO chipDepends on
Blocks
Gotchas
permissiontable). A single session-level "auto" chip is misleading — badge at the turn level where the runtime supports it.mode.changedevents across runtimes with different vocabularies — keep the raw runtime value in payload, only normalize the top-level enum.References
clawmetry/local_store.py:610(approvals schema)clawmetry/sync.py:7346(approvals ingest today)clawmetry/sync.py:3186(_parse_v3_event— the seam to extend)routes/sessions.py:2887(/api/session-governance/<sid>— already returns most of what we need)routes/sessions.py:2905(/api/session-insight/<sid>— folds governance intowaste_flags)clawmetry/static/js/app.js:8177(NemoClaw approvals renderer — reference implementation for the bubble)