openclaw

Identity

OpenClaw is a personal assistant that talks to people on messaging apps, runs scheduled and event-driven work, and keeps memory in files. It can run a conversation through another harness (the Codex app-server, the Claude Code CLI).
  • Native format: openclaw-agent-sqlite. One database per configured agent (~/.openclaw/agents/<agentId>/agent/openclaw-agent.sqlite, or PostgreSQL) with sessions, windows, append-only transcript events, conversations, deliveries and participants; a shared state database (state/openclaw.sqlite) with approvals, audit events, cron jobs, tasks, the delivery queue and skills. Legacy JSONL transcripts: openclaw-session-jsonl.
  • Session ID: a window’s session_id (a UUID). No namespace. The logical conversation’s session_key (for example agent:main:whatsapp:group:<id>) → header conversationKey. agentId → header _meta ai.openclaw/agentId.
  • Entity keys: messageId is the transcript entry id. A rewritten entry (one with supersedesEntryId) keeps the id at the root of its supersedesEntryId chain as its messageId, with supersedes naming the record it replaces and assembly {mode: rewrite, sourceIds: [<new id>]} (ASR-352, ASR-522). callId is toolCall.id; older transcripts that repeat IDs leave results unpaired with linkage: ambiguous and no callId, keeping the native ID in tool_result.nativeCallId (ASR-262). inferenceId is the assistant row’s responseId, else its turnId, else derived from the entry ID: OpenClaw’s turnId names one model response, not a turn. turnId is __openclaw.runId: OpenClaw’s run is the ASR turn.
  • Runs by other harnesses: agentHarnessId names the runtime that ran the window. For the Codex app-server runtime, OpenClaw documents its transcript as a mirror of the Codex thread (“Primary thread state: Codex thread, plus OpenClaw transcript mirror”): authority: mirror and executedBy [{session: "codex:<thread id>", evidence: native_field}]. Claude Code CLI bindings (cliSessionBindings, claudeCliSessionId) and Muse bindings → executedBy without mirror, because OpenClaw records its own events for them.
  • Assistant rows that are not model output: rows with provider: "openclaw" (delivery-mirror, gateway-injected, automation-result) get no inference. A delivery mirror copies a reply of the same stream: copiedFrom {record: "messageId=<sourceMessageId>", proof: native_field} and author: harness. A cron or subagent result written into the conversation → author: harness (cron) or agent (subagent), origin: injected or subagent_report.
  • Who wrote a message: provenance.kind: external_user → origin: prompt, with author: human only when the sender’s identity says it is a person (senderKind: human, or a Gateway profile); inter_session → author: agent, origin: inter_session, from: {session: <originSessionId>}; internal_system → author: harness, origin: injected. custom_message entries → author: harness; their role is decided per provider at request time and is not stored: role absent with capture.role: absent. operatorMessage rows likewise have no stored role. __openclaw.modelPromptProjection → content, with the stored text in displayContent (displayCorrespondence: native). Talk rows (provenance.kind: realtime_voice) and media with transcribed: true → text parts with derivation {method: transcription}.
  • Triggers: provenance.sourceTool → trigger.native. Mapping: cron or cron_execution → schedule (id from jobId, runId from runId). Use heartbeat only when evidence from the time the job ran, or a saved receipt from the job’s setup, establishes the job’s kind for that run. A current job name or uncertain conversion time does not establish the job’s kind for that run. Without that evidence, keep schedule and native provenance. Gmail and webhook hooks (hook_external_content_source) → event or webhook; main_session_restart_recovery → recovery; exec and agent_harness_completion → task_completion. A session created for a cron run → header trigger.
  • Senders and conversations: __openclaw.senderId, senderName, senderUsername and senderIdentity → a participant (displayName; ids [{scheme: <channel>, value: <id>, basis: <senderIdentity.type>}]; kind from senderKind) and message.from {participant}; senderIsOwner → role: owner. conversations rows → header conversations (channel, kind, nativeId); transport.messageId and replyToId → platform; whether the agent was mentioned → addressed; transport.clients → via (Control UI → web, TUI → tui, CLI → cli, mobile → mobile, channels → channel).
  • Approvals: operator_approvals → permission with requestId from the approval ID and subject.callId from source_tool_call_id. status: pending → pending, allowed and denied → resolved, expired → expired, cancelled → resolved with decision: cancelled; a terminal_reason other than user or timeout → no normalized status unless this registry defines its meaning; preserve the literal in ai.openclaw/terminalReason. decision: allow-once → allow_once, allow-always → allow_always, deny → reject_once. resolver_kind and resolver_id → decider (device → {device: resolver_id}, channel → {address: resolver_id}); no resolver ID means no actor reference. approvalReviews[] → reviews (by: {address: <label>}, verdict from status, rationale); when the automatic reviewer itself resolved the request without a person, decidedBy: classifier.
  • Questions: Gateway question records are kept in memory only. The ask_user call and its result are stored → input_request with mode: questions, callId, questions and options from the input, status from the result (answered, or no_answer → timed_out) and answers.
  • Tools: MCP identity is on the result: toolResult.details.mcpServer and mcpTool → tool_result.provider {kind: mcp, server, tool, identity: exact} (ASR-276); the call’s composite name stays as recorded. Code Mode nested calls (openclaw.nested-tool.v1) → tool_call with requestedBy: code and parentCallId from parentToolCallId, and its tool_result. A user-run shell command (bashExecution) → tool_call with requestedBy: user. A background command (status: running with a process ID) and the process tool → terminal with kind: shell.
  • Memory: a file-tool write to the agent workspace’s MEMORY.md or memory/YYYY-MM-DD*.md (scope: agent), USER.md (scope: user) or DREAMS.md → memory_change {store: "workspace", op: add | update | delete, target.path, cause.callId}. Dreaming → memory_change with op: consolidate and cause: {actor: harness}, and a subagent with capture.childSession: absent. openclaw memory forget and memory_session_tombstones → memory_change with op: forget and target.key the forgotten session ID; status stays absent until the source records the deletion. Loaded workspace files (systemPromptReport) → instructions with kind: memory, user or project, a digest, and truncated when the file was cut before injection.
  • Notes: compaction {summary, firstKeptEntryId, tokensBefore, tokensAfter} → compaction with replaces.throughId (the compaction’s native predecessor, from parentId), keptFromId and insertAt.beforeId (firstKeptEntryId), and tokens. branch_summary → kind: branch_summary. A Codex-runtime compaction without a summary → kind: provider_native with capture.summary: absent. Usage: input excludes cached tokens → input.uncached; cacheWrite1h and cacheWrite5m → cacheWriteByTtl 1h and 5m; cost.total → cost.amount in USD with source: provider_reported when totalOrigin is provider-billed, otherwise harness_estimated; a call whose usageReport.state is unavailable has no usage (not zeros); responseModel (or model when absent) → model.id, model → model.requested. model_change and thinking_level_change → config_change. Session settings (permissionMode, sandboxMode, queue mode) are current values only: write one config_change at the end of the capture, with snapshot: true and assembly: {mode: snapshot}, never backdated (ASR-034, ASR-187). SessionEntry.goal → plan with planType: goal (assembly: snapshot); the progress_card tool → plan with planType: items and markdown content. skill_workshop_changes → config_change.capability {kind: skill, name, op} (create → install, remove_file and archive → remove, other actions retained at ai.openclaw/skillAction with normalized op absent). OpenClaw records hook effects but not hook runs: no hook records. Provider compaction checkpoints are compactions, not checkpoint records; cloud workspace snapshots are checkpoint with kind: snapshot.

OpenClaw SQLite converter supplement

  • Identity: one stream per session_windows.session_id; session_key remains conversationKey. Legacy JSONL uses header session.id. No namespace. Duplicate native entry identities are errors, not permission to merge them.
  • Copies: require the recorded ancestor window and transcript_event_identities membership under the pinned window-copy contract. Matching IDs outside that contract do not prove copies. Only a contiguous prefix consisting entirely of copied records can supply inherits.localThroughId; wrapper-owned audit facts cannot be included in that prefix. Copied run IDs never establish which run or window owns facts in a side store.
  • Limits: retain unknown side rows and compressed trajectory evidence without claiming their normalized semantics. Cold archives need not_retained gaps when history was lost. Unavailable context, terminal lifecycle, memory origins/tombstones, lane resolution, standing grants, boards and progress history remain declared limits where the converter cannot map them. A converter’s partial coverage does not remove the broader destinations above.

Position basis

  • Capture order: transcript sequence, audit sequence, skill cursor_id, trajectory sequence, then mutable state. Independent database transactions are not one atomic capture. When state that spans several stores has changed, use a new capture (ASR-033/034). Stable combinations of table, primary key and projection identify records; positions based only on rowid are scoped to the capture.
Use only positions established by the stated format and mapping evidence. Current-state rows without transcript positions form the snapshot tail. A protocol commit boundary takes precedence over physical EOF; uncommitted bytes are not a gap.

Destinations

OpenClaw: native facts without a standard field

ai.openclaw/reaction records a reaction that the source observed: a reaction state in the Control UI, or a channel’s reaction notice. The agent’s own react action is a tool call; it gets a reaction record only when the source also records the reaction itself.

OpenClaw supplement: native facts without a standard field

These rows add destinations. Existing ai.openclaw/node and ai.openclaw/goal keep their grouped shapes. Unassigned rows use the common array shape, even for one fact. Do not put a whole goal or window into those narrower grouped objects.
  • Windows and lineage: OpenClaw moves a conversation to a new window for many reasons; each window is a session. session_windows.previous_session_id and reason → lineage.continues {session, reason} with the same reason names (reset, rollover, rewind, switch, recovery, compaction, and daily or idle when the window reason records them). A window with reason fork → forkedFrom, from SessionEntry.forkSource {sessionKey, sessionId, entryId} with record: "messageId=<entryId>". A new window begins with entries copied from the old one with the same IDs: inherits {mode: copied, from, localThroughId, evidence: native_structure} under the source’s copied-prefix contract. Individual copies can use proof: prefix_contract; that is not a lineage evidence value. A reset entry inside one window (reason: new | reset | idle | daily | cron-stale) → compaction with kind: reset and reason. Subagents: spawnedBy from SessionEntry.spawnedBy and subagent_runs. root may be claimed for a window with reason initial and no parent, fork or spawn field.
  • Tree: native parent parentId; response key responseId, else turnId; active leaf: the last leaf.targetId, falling back to session_transcript_index_state.leaf_event_id. Injected: custom_message entries, and user rows with synthetic, runtimeContext, or a provenance.kind other than external_user. A leaf entry for a rewind → rewind {reason: user_rewind}; for a branch switch → rewind {reason: other}.
These contracts apply to 2038c84, package 2026.10.7, agent schema 37 and its shared state schema 34.

Usage

Select one report per native accounting identity only when this entry establishes that identity. Keep unjoinable or overlapping reports as native evidence. Declare incomplete semantic coverage. Schema defaults and placeholder zeros are not reported usage. Rules for what a backend includes in its reported usage require evidence; do not infer them from a model name.

Lane and unassigned facts

  • Delivery: Assign conversation_deliveries and queue facts to a window only by an exact native window/message link or an independently owned run or queue generation. A conversationKey alone does not identify which window a fact belongs to. Unassigned facts go once in manifest _meta ai.openclaw/unassigned, with the lane’s identity (its conversation key) and provenance; declare a vendor loss only when facts are omitted, not merely unassigned. A standalone stream needs a bundle to retain these facts. When emitted, delivery.message independently requires a message reference that can be resolved (ASR-235). Map native outcomes created/queued to queued, sent/replied to sent, suppressed to suppressed, rejected to failed, and unknown to unknown; keep platform IDs, retry_count and recorded reasons. A NO_REPLY message alone is not a delivery attempt.
  • Side facts and execution: copied __openclaw.runId values never establish which run or window owns facts stored outside the transcript. Without an independent original-run/window link, retain the fact as unassigned. A native null exitCode means the exit code is unavailable; omit it. Keep a numeric exitSignal as a number in raw, never in signal.
Facts without established session ownership use manifest _meta["ai.openclaw/unassigned"] with the common shape. Selection may retain these facts with any session subset, including an empty subset, but must not imply session ownership. State the selection policy in the export report.

Losses

Declare every applicable loss under ASR-157. Missing model rendering, accounting joins, lineage or exact context boundaries remain losses when this entry does not qualify them. Unknown ownership is not itself lost data. Declare omitted unassigned facts with a documented vendor loss or report, not a new core category.

Evidence

Basis: source (openclaw/openclaw at 2038c84, package 2026.10.7, agent database schema 37). The SQLite converter supplement below is pinned to a different source revision, and its evidence comes from synthetic captures made by the native writer. Basis: source-verified at base 15e4d849d5f9a3559132454aeff90fe54781dee9, with native ASR patch 3dcc7856a4bccc8bed7a32f2a3b530511e1a1691, agent schema 37. The pinned persistence writers produced synthetic captures made without model calls (no-model captures). This evidence applies to the supplement; it does not replace the 2038c84 rules above. Compatibility with schemas 17 and 19 has authored tests only. The evidence for this converter does not cover PostgreSQL. Evidence applies only to the named formats and explicit mappings above. A real capture covers only exercised behavior. A no-model writer probe covers persistence, not model or tool execution. Authored tests do not qualify an agent version. Unlisted formats and link mappings remain unqualified.

Observations from 2026-10-27

observed, version 2026.10.8 (dc2523e): OpenClaw 2026.10.8 (dc2523e) wrote agent schema 25 and shared schema 20. Qualify storage schemas separately from release numbers. For shared skill/cron tables without cursor_id, the observed order is timestamp then rowid. Edit, shell-test, resume and review/question runs retained a native input_request. Preserve that request without inventing an answer or approval. The ctx conversion produced 2 streams and 132 records, with 0 errors, 0 notices, 0 gaps and 0 unidentified inputs under validator-v0.1.2. These counts describe the selected output; an empty bundle qualifies no source format. No sanitized native fixture was retained for this observation. Whole native records use native.record or native.blob, with JSON text columns preserved as strings. The exporter’s own bookkeeping for serialization, partitioning and message keys belongs in the exporter namespace (rs.ctx for ctx), not to a provider metadata destination. These runs add no evidence for approvals, lineage links, compaction boundaries or exact request context that they did not exercise.