Context
OpenClaw has the richest on-disk trace of any runtime we support — a purpose-built acp_replay_events stream (ACP = Agent Client Protocol), plus normalized subagent_runs + flow_runs + task_runs + command_log_entries tables. We ignore all of it and render the flat JSONL.
Substrate
- State DB (orchestration truth):
~/.openclaw/state/openclaw.sqlite
- Transcripts:
~/.openclaw/agents/<alias>/sessions/*.jsonl — v3 envelope {"type":"message","message":{...},"id","parentId","timestamp"} — parentId is a chain, not a tree (reference_openclaw_v3_parentid_is_chain.md)
Sub-agent model
First-class. Table subagent_runs with run_id, child_session_key, controller_session_key, requester_session_key, task, task_name, model, workspace_dir, spawn_mode, outcome_json. This is a REAL parent→child graph — the chain-not-tree warning only applies to the transcript, not to spawn relationships.
Workflow model
Table flow_runs — flow_id, shape, sync_mode, controller_id, status, goal, current_step, blocked_task_id, state_json, wait_json. Steps land in task_runs with parent_flow_id, parent_task_id, agent_id, requester_agent_id, delivery_status, notify_policy. A genuine DAG with a controller.
Replay-native stream: acp_replay_events(session_id, seq, at, update_json) + acp_replay_sessions — literally designed for step-by-step replay. Use it directly instead of reconstructing from the JSONL.
Mode + approvals
- Mode config:
exec_approvals_config(default_security, default_ask, default_ask_fallback, auto_allow_skills). default_ask=never + auto_allow_skills=1 = auto. Per-agent overrides in agent_count / allowlist_count.
- Approval audit trail:
command_log_entries(action, session_key, sender_id, entry_json) — the exec-side decisions. Plugin capability approvals in plugin_binding_approvals (per-plugin per-channel).
- User reply: in
command_log_entries.entry_json and in the ACP replay update stream. NeMo Guardrails approval queue writes its own rows (project_runtime_scoped_alerts_approvals.md).
Deliverable
Implement iter_replay_events(session_id) → Iterator[ReplayEvent] for OpenClaw. Read the SQLite in mode=ro immutable=0 (daemon may be writing).
Output per the canonical schema (OSS-01):
- Primary source:
acp_replay_events.update_json — this is the pre-normalized event stream. Map its update_type values to canonical kind.
- Enrich with joins to
subagent_runs (for agent.spawn / agent.return), flow_runs + task_runs (for workflow.*), command_log_entries (for approval.decided), exec_approvals_config (for mode.changed — session-scoped, one at start).
- Fall back to JSONL parse if
acp_replay_sessions has no row for a session_id (older sessions).
Acceptance
- 3 fixtures replay:
- A session with a
subagent_runs fanout ≥3 children (delegations render inline)
- A
flow_runs session with ≥5 task_runs steps (workflow swimlane renders with controller + step status)
- A session with an
exec_approvals_config that denied ≥1 tool call (approvals rail shows the denied entries with command_log_entries.entry_json as decision_reason)
- Mode chip shows correct value from
exec_approvals_config + per-agent override count in tooltip
plugin_binding_approvals rows render as approvals with resolver=policy
Depends on
- OSS-01 (schema)
- OSS-02 (mode + approvals ingest + UI wiring)
Gotchas
- Do not treat transcript
parentId as a tree. Use subagent_runs for delegation, parentId only for message ordering within a session.
- Two approvals systems (
reference_approvals_architecture.md) — exec (command_log_entries) and plugin capability (plugin_binding_approvals). Both must land in the approvals rail with different resolver values so the UI can distinguish.
- Daemon writes to
openclaw.sqlite continuously — every read opens mode=ro, never grab a writer lock.
References
- OSS substrate:
clawmetry/sync.py OpenClaw ingest branch (grep ~/.openclaw in sync.py)
- ACP replay is currently unused —
grep acp_replay in this repo returns zero — this issue lights it up
- Approvals arch memory:
reference_approvals_architecture.md
Context
OpenClaw has the richest on-disk trace of any runtime we support — a purpose-built
acp_replay_eventsstream (ACP = Agent Client Protocol), plus normalizedsubagent_runs+flow_runs+task_runs+command_log_entriestables. We ignore all of it and render the flat JSONL.Substrate
~/.openclaw/state/openclaw.sqlite~/.openclaw/agents/<alias>/sessions/*.jsonl— v3 envelope{"type":"message","message":{...},"id","parentId","timestamp"}—parentIdis a chain, not a tree (reference_openclaw_v3_parentid_is_chain.md)Sub-agent model
First-class. Table
subagent_runswithrun_id, child_session_key, controller_session_key, requester_session_key, task, task_name, model, workspace_dir, spawn_mode, outcome_json. This is a REAL parent→child graph — the chain-not-tree warning only applies to the transcript, not to spawn relationships.Workflow model
Table
flow_runs—flow_id, shape, sync_mode, controller_id, status, goal, current_step, blocked_task_id, state_json, wait_json. Steps land intask_runswithparent_flow_id,parent_task_id,agent_id,requester_agent_id,delivery_status,notify_policy. A genuine DAG with a controller.Replay-native stream:
acp_replay_events(session_id, seq, at, update_json)+acp_replay_sessions— literally designed for step-by-step replay. Use it directly instead of reconstructing from the JSONL.Mode + approvals
exec_approvals_config(default_security, default_ask, default_ask_fallback, auto_allow_skills).default_ask=never+auto_allow_skills=1= auto. Per-agent overrides inagent_count/allowlist_count.command_log_entries(action, session_key, sender_id, entry_json)— the exec-side decisions. Plugin capability approvals inplugin_binding_approvals(per-plugin per-channel).command_log_entries.entry_jsonand in the ACP replay update stream. NeMo Guardrails approval queue writes its own rows (project_runtime_scoped_alerts_approvals.md).Deliverable
Implement
iter_replay_events(session_id) → Iterator[ReplayEvent]for OpenClaw. Read the SQLite inmode=ro immutable=0(daemon may be writing).Output per the canonical schema (OSS-01):
acp_replay_events.update_json— this is the pre-normalized event stream. Map itsupdate_typevalues to canonicalkind.subagent_runs(foragent.spawn/agent.return),flow_runs+task_runs(forworkflow.*),command_log_entries(forapproval.decided),exec_approvals_config(formode.changed— session-scoped, one at start).acp_replay_sessionshas no row for a session_id (older sessions).Acceptance
subagent_runsfanout ≥3 children (delegations render inline)flow_runssession with ≥5task_runssteps (workflow swimlane renders with controller + step status)exec_approvals_configthat denied ≥1 tool call (approvals rail shows the denied entries withcommand_log_entries.entry_jsonas decision_reason)exec_approvals_config+ per-agent override count in tooltipplugin_binding_approvalsrows render as approvals withresolver=policyDepends on
Gotchas
parentIdas a tree. Usesubagent_runsfor delegation,parentIdonly for message ordering within a session.reference_approvals_architecture.md) — exec (command_log_entries) and plugin capability (plugin_binding_approvals). Both must land in the approvals rail with differentresolvervalues so the UI can distinguish.openclaw.sqlitecontinuously — every read opensmode=ro, never grab a writer lock.References
clawmetry/sync.pyOpenClaw ingest branch (grep~/.openclawin sync.py)grep acp_replayin this repo returns zero — this issue lights it upreference_approvals_architecture.md