openclaw¶
OpenClaw (catalog agent openclaw, previously named Clawdbot and then Moltbot) is a self-hosted
agent whose Gateway (the long-running openclaw gateway server process that owns every session)
takes messages from chat channels (Telegram, WhatsApp, Slack, Signal, iMessage and others under
extensions/) and runs agents with shell, browser and file tools, keeping per-agent sessions; it
ships as a TypeScript CLI and gateway with a Swift macOS app, and can run in Docker. The Control UI
(its web front end), the TUI and the mobile and desktop apps are clients of that Gateway, not
copies of its store, and OpenClaw has no vendor account. Its transcript record shape (type:
"session" header, then message, model_change, compaction and other
entries linked by id/parentId) is the tree-structured session format of
its agent core; since mid-2026 the records live as rows in a per-agent SQLite
database rather than in .jsonl files, which remain only as legacy import
sources and archives. Parsed: the openclaw parser reads the agent database, the Gateway's state
database and the legacy and archived session files.
Source and evidence¶
openclaw/openclaw, commit
3b16db73c0b5209aad2ee7c3fb5ddfb12fb208f9
(2026-10-04, package version 2026.9.8, agent schema version 24 per
package.json:5-9),
MIT, open source, TypeScript with a Swift macOS app. Every claim is source-level unless it cites
docs/ or CHANGELOG/ in the same checkout. Session sharing, backup upload and remote execution
were checked at the same commit on 2026-10-17.
Observed¶
- No real install of OpenClaw has been surveyed; every review was of source only.
- The bad-line rows of issue 177 (the
legacy
sessions.jsonindex and JSON lines that are not objects) are not observed on any real install, source-derived (openclaw/openclaw at146284a). - The public-share and backup-upload rows are fixture only (Sync, relay and origin).
Source reviews¶
- 2026-10-21: openclaw/openclaw at
146284a(version2026.10.5): the relay, permission, model-override, past-use, deletion, PostgreSQL-anchor and GitHub publication records became parser rows, marked source-derived in each Parser plan subsection; the Credentials and Exclusions sections gained the new secret globs, the GitHub identity and the clone exclusion, and Source host rows states the GitHub integration. - 2026-10-19: openclaw/openclaw at
146284a(2026-10-19, version2026.10.5,package.json:3; shared-state schema 28 and agent schema 31,package.json:17-20), against3b16db7, which the rest of this document cites (state 20, agent 24). It added the permission, model override, past-use, relay and deletion records of Record schema, Joins and timestamps and Other stores, the PostgreSQL engine, Logbook, Docker andprojects/paths of Per-user storage and Exclusions, and the drift notes of Format versions.
Collection¶
Per-user storage¶
Home is $OPENCLAW_HOME, else HOME, else USERPROFILE
(normalization-core/src/home-dir.ts:30-31,50).
The state directory is $OPENCLAW_STATE_DIR, else ~/.openclaw if it exists, else a legacy
~/.clawdbot if that exists, else ~/.openclaw
(src/config/state-dir.ts:8-43).
A named profile (OPENCLAW_PROFILE=work) uses ~/.openclaw-<profile>
(src/cli/profile-utils.ts:26-37);
remote worker machines get ~/.openclaw-worker
(gateway/worker-environments/project-setup-script.ts:142).
There is no platform branch: the same tree is used on Linux, macOS and Windows
(%USERPROFILE%\.openclaw). No XDG directories except one env file (Credentials).
Scratch directories¶
OpenClaw works in directories of its own: the default agent workspace workspace/ and
workspace-<agentId>/ (the transcript header cwd defaults to it), managed git worktrees in
worktrees/, repositories the Gateway cloned for a project in projects/<16 hex>/<name>
(src/projects/project-clone.ts:276-295),
sandbox workspaces in sandboxes/
(agents/sandbox/constants.ts:8),
and a cloud worker's remote_workspace_dir on another machine. All but the last are under the state
directory, so the catalog's .openclaw line collects them (less the worktrees exclusion) and the
analyzer classes a path under them as a scratch directory; state/openclaw.sqlite records which is
which (projects.source registered|cloned, worktrees.path and owner_kind,
Where the project path is recorded). The temp root /tmp/openclaw (else a per-user
os.tmpdir() directory,
infra/tmp-openclaw-dir.ts:4)
is outside the home and holds the rolling logs and Logbook's capture directory on the capturing
device; no catalog line can collect it.
Contents of the state directory¶
From
docs/openclaw-agent-runtime.md:49-60:
openclaw.jsonconfig, JSON5 (config/paths.ts:36-37;io.read-helpers.ts:262), with.bakrotation,.clobbered.<ts>and.last-goodsiblings (backup-rotation.ts:167,io.clobber-snapshot.ts:178,io.observe-recovery.ts:554). Legacy nameclawdbot.json.state/openclaw.sqlite: shared gateway database, 133 tables (src/state/openclaw-state-schema.sql), includingaudit_events(actor, agent, session, tool name per action,:149-171),channel_ingress_events(raw inbound channel messages,:1301-1323),exec_approvals_configandoperator_approvals(:401-437),cron_jobs,subagent_runs,projectsandworktrees.agents/<agentId>/agent/openclaw-agent.sqlite: per-agent database holding sessions, transcripts, memory index and auth profiles (src/state/openclaw-agent-schema.sql);agentDiris configurable per agent (agents/agent-scope-config.ts:577-588). Alsomodels.json(agents/models-config.ts:259).agents/<agentId>/sessions/(config/sessions/paths.ts:44-54): legacysessions.jsonand<sessionId>.jsonltranscripts awaitingdoctor --fiximport, and the reset and delete archives, checkpoints, trajectories and rollback copies named in Transcript stores (config/sessions/artifacts.ts:8-17,101-147).workspace/(default agent) andworkspace-<agentId>/(others), unlessOPENCLAW_WORKSPACE_DIRor config moves them (agents/workspace-default-path.ts:14-31,agent-scope-config.ts:461-485). A workspace holdsAGENTS.md,SOUL.md,TOOLS.md,IDENTITY.md,USER.md,BOOTSTRAP.md,MEMORY.md(agents/workspace-bootstrap-policy.ts:10-36), legacymemory.md(memory/root-memory-files.ts:6-8),memory/andskills/<name>/SKILL.md(agents/workspace.ts:208,234), and is a git repository (workspace-git.ts:33). These are the agent's instructions and memory and are prime evidence of prompt injection or persistence.credentials/($OPENCLAW_OAUTH_DIRoverrides,config/paths.ts:372-381).skills/managed skills from the marketplace (skills/runtime/refresh-source-roots.ts:49);~/.agents/skillsis also read (:52).extensions/installed plugins (docs/cli/backup.md:649).logs/:commands.log(hooks/bundled/command-logger/handler.ts:19-22),config-audit.jsonl(infra/state-migrations.audit-checkpoints.ts:121), and debug traces that hold full prompts:raw-stream.jsonl,anthropic-payload.jsonl,cache-trace.jsonl(embedded-agent-subscribe.raw-stream.ts:22,anthropic-payload-log.ts:88,cache-trace.ts:101).browser/<profile>/user-data/: Chromium profile driven by the agent (extensions/browser/src/browser/chrome.ts:720).worktrees/managed git worktrees (infra/state-migrations.doctor-discovery.ts:53),delivery-queue-media/(config/paths.ts:367-368),cron/runs/,delivery-queue/,sandbox/(docs/cli/backup.md:587-594),service-env/(Credentials).control/services.sqlite: daemon runtime pins, kept locally on either database engine (docs/reference/database-engines.md:20-21, at146284a).- With the experimental PostgreSQL engine,
state/openclaw.sqliteand eachopenclaw-agent.sqliteare small SQLite anchors and the stores they name are on a database server (PostgreSQL engine).
Outside the state directory¶
- Rolling logs
openclaw[-<profile>]-YYYY-MM-DD.login/tmp/openclawor a per-useros.tmpdir()fallback (logging/log-file-path.ts:43-56;infra/tmp-openclaw-dir.ts:4). On Windows that is underAppData/Local/Temp. - macOS app:
~/Library/Application Support/OpenClaw/databases/chat transcript cache andOpenClaw/canvas(apps/macos/Sources/OpenClaw/ChatTranscriptCacheSupport.swift:121-126,CanvasManager.swift:22-25). - Persistence:
~/Library/LaunchAgents/ai.openclaw.gateway.plistandai.openclaw.node.plist, systemd user units~/.config/systemd/user/openclaw-gateway.service(legacyclawdbot-gateway), Windows scheduled taskOpenClaw Gatewayrunning<state>/gateway.cmd(daemon/constants.ts:5-7,29-35;launchd-service-files.ts:48;systemd-service-files.ts:52;daemon/paths.ts:52-63).
Logbook¶
Logbook, a plugin off by default that takes periodic screen
snapshots from a paired device and summarises them into a timeline, keeps logbook/logbook.sqlite
and the image files logbook/frames/
(extensions/logbook/openclaw.plugin.json:2-10);
present at both
3b16db7
and 146284a,
collected under .openclaw. The frames are screenshots of the user's
screen and can be large; they are not excluded.
Docker¶
At 146284a
the compose file bind-mounts ~/.openclaw and a second host
directory, ~/.openclaw-auth-profile-secrets, at /home/node/.config/openclaw
(docker-compose.yml:46-49),
which is where gateway.env is read
(infra/dotenv-global-core.ts:206).
The catalog line openclaw|.openclaw-* collects it as if it were a profile directory; its
contents are credentials (Credentials).
Legacy names¶
~/.clawdbot is still read and doctor --fix renames it to ~/.openclaw, leaving ~/.clawdbot as
a symlink (Windows junction) to the new directory
(infra/state-migrations.state-dir.ts:186,206,447-459).
~/.moltbot and moltbot.json detection was removed
(CHANGELOG/2026.2.13.md:119,
CHANGELOG/2026.3.22.md:13),
so a ~/.moltbot on an old host is orphaned state that the current tool ignores. CLAWDBOT_* and
MOLTBOT_* environment variables are ignored
(gateway/env-deprecation.ts:7).
Credentials¶
agents/*/agent/openclaw-agent.sqliteholds provider API keys and OAuth tokens in two tables named in Secrets. The same file holds every transcript, so collect it unflagged and redact those tables.state/openclaw.sqlitehas 18 credential-bearing tables, among themdevice_auth_tokens,mcp_oauth_stores,secret_store_entries,worker_environment_credentials(secret-state-tables.ts:2-21). Collect unflagged.- Legacy files that
doctor --fiximports:agents/*/agent/auth-profiles.json(security/audit-extra.async.ts:441) andcredentials/oauth.json(agents/auth-profiles/legacy-source-files.ts:19).models.jsonprovider entries can carry keys that doctor migrates (commands/doctor-model-catalog-credentials.ts:247-255). credentials/subtrees written by channel plugins:whatsapp/<accountId>/session keys (extensions/whatsapp/src/accounts.ts:81),matrix/(extensions/matrix/src/storage-paths.ts:60),zalouser/(extensions/zalouser/src/session-state.ts:41), browser relay secret (extensions/browser/src/browser/extension-relay/relay-auth.ts:29).openclaw.jsonembedsgateway.auth.token/password(config/zod-schema.gateway.ts:307-309), channel bot tokens and providerapiKeyvalues, plaintext or as SecretRefs (references to a value held in the environment or a secret store). With the PostgreSQL engine,database.postgres.connectionmay be the DSN itself, password included, rather than a SecretRef (docs/reference/database-engines.md:68-73, at146284a). The.bak/.clobbered/.last-goodcopies carry the same. A secret glob flagsopenclaw.json..envin the state directory, and~/.config/openclaw/gateway.envwhen the state directory is the default (infra/dotenv-global-core.ts:190-204).service-env/<label>.env(mode 0600) beside the macOS LaunchAgent wrapper holds the service secrets (daemon/launchd-service-files.ts:35-60).- OpenClaw keeps its own secrets in files and SQLite, but reads Claude Code's macOS keychain item
Claude Code-credentialsto reuse that login (plugin-sdk/provider-auth-claude-compat.ts:13-15), so a Claude Code token can appear in its auth profiles. - The legacy exec approvals file
exec-approvals.jsonin the state directory, whosesocket.tokenauthenticates to the approvals socket (infra/exec-approvals-config.ts:95-101,infra/exec-approvals-core.ts:226-234, at146284a). It is migrated intostate/openclaw.sqlite, which keeps the token (Secrets), so a host that has not migrated still has the file. The secret glob.openclaw*/exec-approvals.jsonflags it, andexec-approvals.jsonat a Docker volume's root. ~/.openclaw-auth-profile-secrets/, the Docker installs' host directory for the container's~/.config/openclaw(gateway.env) (Docker); the secret glob.openclaw-auth-profile-secrets/*flags every file in it. The Docker guide states that OAuth access, refresh and ID tokens are plaintext in SQLite under the state directory (docs/install/docker-vm-runtime.md:46-50).- The managed GitHub identity, the account the Gateway's GitHub tools and publications act as
(Source host rows): its OAuth token record is a
secret_store_entriesrow ofstate/openclaw.sqlitenamedgithub-oauth-<32 hex>(secret-store-github-names.ts:9;openclaw-state-schema.sql:2737-2747), and a GitHub App installation'sprivateKeysits inopenclaw.jsonundertools.githubor an agent entry'stools.github, plaintext or as a SecretRef (zod-schema.github-identity.ts:16-43;zod-schema.agent-runtime.ts:614,:659).
Exclusions¶
The backup command's own skip list is a good guide
(docs/cli/backup.md:649-653,
659-663):
dev/, git/, npm/, npm-runtime/, tmp/ and tools/ (managed checkouts, packages, downloaded
runtimes such as signal-cli and sherpa-onnx TTS models), extensions/*/node_modules,
plugin-skills/ (a symlink index), sandbox/skills-workspaces/, and agents/*/agent/**/tmp. Add
worktrees/ (git worktrees of user repositories, the equivalent of .claude/worktrees) and the
Chromium cache subtrees under browser/*/user-data/. Do not exclude browser/*/user-data as a
whole: its History and Cookies show what the agent browsed.
At 146284a
the full git clones the Gateway makes when a project is added from a remote URL,
projects/<16 hex>/<name>, are excluded too; the clone directory's name must be 16 lower-case
hex characters
(src/projects/project-clone.ts:276-295),
so the pattern names it, .openclaw*/projects/ followed by sixteen [0-9a-f] classes, and
nothing else under projects/ is pruned. They are the same kind of tree as worktrees/; which
repository each one is stays in state/openclaw.sqlite projects (origin_url, repo_root).
The volume-relative forms are projects/ with the same sixteen classes, plugin-skills,
agents/*/agent/tmp and agents/*/agent/.tmp; dev, git and tmp cannot be added bare
without matching other tools' volume layouts.
Project-local files¶
The agent workspace is the main "project". Inside user repositories OpenClaw reads
.openclaw/worktree-setup.sh when it creates a managed worktree
(agents/worktrees/service-preparation.ts:329)
and the workspace bootstrap files listed in Per-user storage when a repository
is configured as a workspace. A crashed memory migration leaves .openclaw-repair/root-memory/
(root-memory-files.ts:9).
No per-project database.
Where the project path is recorded¶
openclaw.json:agents.defaults.workspaceandagents.entries.<id>.workspace/agentDir(config/zod-schema.agents.ts:30-39;zod-schema.agent-defaults-base.ts:72;zod-schema.agent-entry-base.ts:98-100). JSON5, so the key may be unquoted.state/openclaw.sqlitetablesprojects.repo_rootandworktrees.repo_root/path(openclaw-state-schema.sql:1854-1870,1901-1909): SQLite only.- The transcript header record
{"type":"session",...,"cwd":...}intranscript_eventsand in legacy.jsonlfiles (config/sessions/transcript-header.ts:20-29). Headers are short, so they are stored uncompressed and a byte grep of the database finds them. - At
146284a,projects.source(registeredfor a repository the user pointed the Gateway at,clonedfor one it cloned intoprojects/) andorigin_url, andworktrees.owner_kind(manual|workboard|session),owner_id,removed_at(openclaw-state-schema.sql:1854-1870,1901-1909), separate the user's projects from the agent's scratch checkouts. A cloud worker session's workspace isworker_session_placements.remote_workspace_diron the worker, not on this host. All SQLite only.
Parsing¶
Transcript stores¶
Home-relative, with .openclaw replaceable by .openclaw-<profile> or the
legacy .clawdbot:
.openclaw/agents/<agentId>/agent/openclaw-agent.sqlitewith-wal/-shm: the live store. One database per agent; onesession_windowsrow per transcript generation; onetranscript_eventsrow per record (src/state/openclaw-agent-schema.sql:159-188,566-604). Its directory,agentDir, can be moved by config (Contents of the state directory)..openclaw/agents/<agentId>/sessions/<sessionId>.jsonl: legacy one-file-per-session transcripts, imported into SQLite byopenclaw doctor --fixand then removed (docs/cli/doctor/state-migrations.md:60); a host that never ran doctor after upgrading still has them.sessions/at the state root is the older single-agent equivalent (docs/openclaw-agent-runtime.md:58-59).- Same directory, archives of reset or deleted sessions:
<sessionId>.jsonl.reset.<YYYY-MM-DDTHH-MM-SS[.mmm]Z>[.<32 hex generation>][.zst]and.jsonl.deleted.<...>(config/sessions/artifacts.ts:10-32; namingsession-accessor.sqlite-archive-artifact.ts:47,70)..zstfiles are a single zstd frame of the JSONL (config/sessions/archive-compression.ts:11-30). The canonical copy of each archive is also a blob insession_transcript_archives(openclaw-agent-schema.sql:606-624). When a delete or reset keeps one is in What survives a delete. - Also there:
<id>.checkpoint.<uuid>.jsonlcompaction checkpoints,<id>.trajectory.jsonlruntime trajectories,*.migrated[.N]and*.pre-doctor-*-repair-*.bakrollback copies (which of them are copies of a transcript is in Past use and secondary sources) (artifacts.ts:13-17,96-116), and a legacysessions.jsonindex.
The agent database uses a WAL journal
(infra/sqlite-wal.ts:169),
read through sqlite_util.py with sidecars. Payloads of 1 KiB to 4 MiB that shrink by at least 10%
are stored as event_zstd (one zstd frame, level 1, checksum, no dictionary) with event_json NULL
and the original length in event_utf8_bytes; others stay in event_json
(config/sessions/transcript-payload.ts:22-24,215-272;
infra/zstd-codec.ts:22-37).
The parser needs zstandard. Cold storage moves old generations into
session_transcript_cold_archives (blob, or a file in the sessions directory when storage='file',
:633-648);
archive_blob in both archive tables is JSONL, zstd when encoding='zstd'.
What survives a delete¶
openclaw sessions archive keeps the transcript and only sets archivedAt, archivedBy and
archiveReason (manual|active-session-cap|age-retention|stale-dashboard|restart-recovery) in
entry_json, mirrored in session_nodes.archived_at
(docs/cli/sessions.md:181-196;
src/config/sessions/types.ts:300-305;
gateway-protocol/src/schema/sessions-row.ts:48-54).
openclaw sessions delete removes the session row, its generations and bindings and keeps a
.jsonl.deleted.<ts> archive with its session_transcript_archives blob (reason deleted)
unless the session was incognito, as a reset keeps a .jsonl.reset.<ts> one
(docs/cli/sessions.md:222-233;
at 3b16db7,
docs/cli/sessions.md:212-217).
The memory index keeps the session's memory_index_chunks until openclaw memory forget, which
writes one memory_session_tombstones row (session_id, agent_id, reason, default forgotten,
created_at) per forgotten session
(openclaw-agent-schema.sql:699-704;
extensions/memory-core/src/memory-session-tombstones.ts:89-106);
inbound channel messages in channel_ingress_events are pruned by age and count, not by deletion,
conversation_deliveries rows go only with their conversation, and an uploaded backup keeps the
session. Pending inputs cascade away with the session, whether the full-text index rows go is not
determined, and a public share grant is lost with the session row
(Other records of past use,
Sync, relay and origin). There is no prompt-history file.
The analyzer reads the archive blobs as primary and the published .jsonl.deleted.* and
.jsonl.reset.* files as secondary. A deleted archive gives an archive=deleted.<ts>
session-start row and a session deleted: row, a tombstone a session forgotten from memory:
row, an archived session a session archived: row; the full-text index gives the entries the
transcript no longer holds, the pending inputs the prompts that never reached it, the channel
tables the inbound messages and deliveries, and the backups backup uploaded: rows. The memory
index chunks are not read (see
Secondary sources and Deletion).
Record schema¶
Transcript entries¶
Each transcript_events.event_json (or decompressed event_zstd) and each legacy JSONL line is one
entry
(agents/sessions/session-manager-types.ts:9-119):
- Header:
{type:"session", version, id, timestamp, cwd, parentSession?}, always the first record (config/sessions/transcript-header.ts:20-29;session-accessor.sqlite-transcript-header.ts:7-35).cwddefaults to the process cwd when the caller gives none. - Every other entry:
type,id,parentId(null at the root),timestamp(ISO 8601 string, UTC fromtoISOString()), optionalappendMode:"side". Types:message,thinking_level_change {thinkingLevel},model_change {provider, modelId},compaction {summary, firstKeptEntryId, tokensBefore, tokensAfter?, details?, fromHook?},reset {reason: new|reset|idle|daily|cron-stale},branch_summary {fromId, summary},custom {customType, data},custom_message {customType, content, display},label {targetId, label},session_info {name}. Unknown records withid/parentId(for exampleleaf) are preserved opaquely (config/sessions/session-entry-codec.ts:181-221). message.messageis anAgentMessage(packages/agent-core/src/types.ts:467-479), discriminated byrole(packages/llm-core/src/types.ts:369-390,516-561):user:contentstring or blocks,timestamp(epoch ms).runtimeContextmarks injected runtime context, not human input.assistant:contentblocks,api,provider,model,responseModel?,responseId?,turnId?,usage {input, output, cacheRead, cacheWrite, totalTokens, cost{...}},stopReason(stop|length|toolUse|error|aborted,:357),errorMessage?,errorBody?,providerReplay?,timestampms.toolResult:toolCallId,toolName,content(text/image blocks),details?,isError,timestampms.bashExecution:command,output,exitCode,cancelled,truncated,fullOutputPath?,timestampms (user-run!commands,agent-core/src/types.ts:397-416).custom {customType, content, display, details?},branchSummary {summary, fromId},compactionSummary {summary, tokensBefore}(itstimestampmay be a string,:418-461).- Content blocks
(
llm-core/src/types.ts:264-313):text {text},thinking {thinking, thinkingSignature?, redacted?},image {data (base64), mimeType},toolCall {id, name, arguments (object)}.
Row metadata¶
transcript_events.created_at is epoch ms, taken from the record's timestamp or Date.now()
(session-accessor.sqlite-transcript-store.ts:173);
seq orders a session from 0. session_windows: session_key, previous_session_id, reason
(initial|reset|rollover|fork|rewind|switch|recovery|compaction), channel, account_id,
chat_type, model_provider, model, started_at, ended_at, status, parent_session_key,
spawned_by, display_name. session_nodes
(:16-49):
session_key, current_session_id, entry_json (session metadata), created_via
(operator|spawn|channel|cron|...), created_actor_type/id, label.
Permission records¶
All in state/openclaw.sqlite; the tables are the same at
3b16db7
and 146284a,
and the line numbers are those of the later commit:
operator_approvals, one row per approval request the Gateway raised for anexec(shell) call, a plugin action or a system agent:kind(exec|plugin|system-agent),status(pending|allowed|denied|expired|cancelled),decision(allow-once|allow-always|deny),terminal_reason(user|timeout|malformed-verdict|no-route|run-aborted|gateway-restart|storage-corrupt),resolver_kind(device|channel|runtime|system: the paired device, the chat channel, the runtime's automatic reviewer or the system),resolver_id,presentation_json(what the reviewer was shown),requested_by_device_id,source_agent_id,source_session_key,source_session_id,source_run_id,source_tool_call_id,source_tool_name,created_at_ms,resolved_at_ms,consumed_at_ms(openclaw-state-schema.sql:370-475).source_session_idandsource_tool_call_idjoin it to the transcript'stoolCall.id.exec_approvals_config, the standing exec policy, migrated from the legacy~/.openclaw/exec-approvals.json(infra/exec-approvals-config.ts:95-101):raw_json(the whole file:defaultsand per-agentagents.<id>withsecuritydeny|allowlist|full,askoff|on-miss|always,askFallback,autoAllowSkills, anallowlistandmcpToolsgrants, andsocket.token, a credential), plusdefault_security,default_ask,default_ask_fallback,auto_allow_skills,allowlist_count,updated_at_ms(openclaw-state-schema.sql:356-368;infra/exec-approvals-core.ts:12-15,:226-234). The exec modeautoisallowlistwithon-missand the automatic reviewer on (:96-115).operator_approval_standing_grants: anallow-alwaysdecision on a scheduled (cron) job mints a grant,grant_id,minted_by_approval_id,agent_id,cron_job_id,operation_binding,created_at_ms,expires_at_ms,revoked_at_ms,last_used_at_ms,use_count(:512-529).channel_pairing_allow_entries(channel_key,account_id,entry,updated_at): the chat senders allowed to talk to the agent per channel account (:794-801).audit_eventscarriestool_call_id,tool_name,action,status,reason_codeandactor_type/actor_idper action (:100-132); itskindandactionvalues were not enumerated.- Per session, in
session_nodes.entry_json:elevatedLevel,execHost,execNode,execCwd,toolOverrides(mcpServerson or off,mcpToolsDeny) andsendPolicy(allow|deny) (src/config/sessions/types.ts:481-500,:532).
Model override¶
session_nodes.entry_json keeps the session's chosen model apart from
the transcript, providerOverride, modelOverride, modelOverrideSource (auto for a temporary
runtime fallback, user for /model or sessions.patch, default), and
modelOverrideFallbackOriginProvider/Model (the model the user had picked when a fallback
replaced it)
(src/config/sessions/types.ts:502-516).
The assistant message's responseModel beside model is the resolved id beside the request
(packages/llm-core/src/types.ts:535).
Joins and timestamps¶
- Session:
transcript_events.session_id = session_windows.session_id; the headeridequals it. Generations of one conversation sharesession_keyand chain throughprevious_session_id. - Session key encodes the origin:
agent:<agentId>:<channel>:<accountId>:direct:<peerId>,agent:<agentId>:<channel>:<peerKind>:<peerId>, oragent:<agentId>:main-style keys (routing/session-key.ts:206-261). This is how a responder ties a transcript to a Telegram chat or WhatsApp number. - Relay (the same at
3b16db7and146284a; line numbers of the later commit). A session carried through a messaging platform is recorded three ways besides its key, each naming the platform's copy of the conversation:session_windowschannel,account_id,chat_type(direct|group|channel),primary_conversation_idandhook_external_content_source(gmail|webhook, a session started by a Gmail or webhook hook) (openclaw-agent-schema.sql:140-171);conversations(conversation_id,channel,account_id,kind,peer_id,delivery_target,thread_id,native_channel_id,native_direct_user_id,label,created_at;:192-207), joined to a generation bysession_conversations(session_id,conversation_id,roleprimary|participant|related,first_seen_at,last_seen_at;:257-267); andsession_nodes.entry_jsondelivery,{kind:"none"},{kind:"internal"}or{kind:"external", route, context, origin}, whoseoriginholdsprovider,surface,chatType,from,to,accountId,threadId,nativeChannelIdandnativeDirectUserId(src/config/sessions/types.ts:60-83,:606), withsession_nodes.created_viachannelfor a session a channel message created (openclaw-agent-schema.sql:28). The channelwebchatis the Control UI itself, not a platform (utils/message-channel-constants.ts:3). - Tool call to result:
toolCall.idin an assistant message equalstoolResult.toolCallIdin a latermessageentry. - Tree:
parentId → id;transcript_event_identitiesindexes it (openclaw-agent-schema.sql:684-694). - Subagents:
session_nodes.parent_session_key/spawned_by, andfork_source_session_id/fork_source_entry_idfor forks; headerparentSession. - Project: header
cwd. No git branch is recorded. - Times: entry
timestampis an ISO 8601 UTC string, messagetimestampandtranscript_events.created_atare epoch ms (Record schema); how copies join the database rows is under Past use and secondary sources.
Other stores¶
Audit, ingress and debug logs¶
Not transcripts, but they tie actions to sessions: state/openclaw.sqlite audit_events, one row
per action (columns in Permission records), the raw inbound chat messages of
channel_ingress_events.payload_json (Other records of past use) and
the debug traces under logs/ that hold full prompts. The files and tables are named in
Contents of the state directory.
Other records of past use¶
Line numbers at 146284a; every source is present at
3b16db7 too, and the parser reads all but the memory index
(Past use and secondary sources):
| Source | Holds | Survives | Joins on | Plan |
|---|---|---|---|---|
openclaw-agent.sqlite session_transcript_fts (FTS5, the SQLite full-text index; content in its _content shadow table) and session_transcript_fts_rows (openclaw-agent-schema.sql:809-823) |
user and assistant text with session_id, message_id, role, timestamp, the index behind the sessions_search tool (docs/concepts/session-search.md:10-13) |
rows are deleted explicitly when a generation moves to cold storage (session-cold-storage-worker.ts:519-521); session_transcript_fts_rows has no foreign key to session_windows, so whether a session delete removes them was not determined |
session_id, entry id (message_id), text, time |
secondary |
openclaw-agent.sqlite session_pending_inputs (:848-864) and session_input_completions |
each accepted prompt as message_json with state queued, interrupted or cancelled and accepted_at, kept "outside the active transcript until its exact turn owns execution" |
a cancelled or interrupted prompt never reaches the transcript; the rows cascade away with the session | session_id, session_key, consumed_event_id (the transcript entry once consumed) |
primary for rows never consumed, secondary otherwise |
openclaw-agent.sqlite memory_index_chunks with source sessions (:658-670; the two sources are memory and sessions, memory-host-sdk/src/host/types.ts:5) |
chunks of transcript text with path, line range and updated_at |
a session delete: "retained deleted-session archives can remain eligible for memory search" until openclaw memory forget (docs/cli/sessions.md:222-233) |
path and text; no session id column |
secondary; the chunk path form was not determined |
state/openclaw.sqlite channel_ingress_events (openclaw-state-schema.sql:1273-1294) |
each inbound channel message as payload_json, with channel_id, account_id, status, received_at |
pruned by age and count per status (pending, completed, failed), not by session deletion (channels/message/ingress-queue.kernel.ts:494-512) |
text and time; the payload shape per channel was not determined | secondary |
openclaw-agent.sqlite conversation_deliveries (:225-248) |
each outbound send or turn to a platform conversation: status, platform_message_id, reply_text, reply_timestamp, source_session_key |
cascades with the conversations row, not with the session |
source_session_key, time |
secondary |
No prompt-history file exists: not found in openclaw/openclaw at
146284a
(grepped history.jsonl, input-history, prompt-history and
historyFile in src/).
Sharing and upload state¶
All in SQLite:
session_nodes.entry_jsonkeypublicShare, an object{id, sessionId, createdAt}:id48 lower-case hex characters,sessionIdthe transcript generation it publishes,createdAtepoch ms (src/config/sessions/types.ts:627-628,src/config/sessions/session-public-share.ts:7-24;entry_jsonis the session-metadata column,src/state/openclaw-agent-schema.sql:16-19). It is written when the session's creator or a Gateway admin turns on Public access in the Control UI (thesession.publicShare.setGateway method, a fresh randomidandDate.now()), and removed when it is turned off (src/gateway/server-methods/sessions-sharing.ts:351-373). The key is absent on a session never published, and is dropped when the entry moves to a newsessionId(a reset or fork) or is incognito (src/config/sessions/session-accessor.sqlite-entry-store.ts:518-533). The public URL is<control UI base>/share/session?token=<opaque>, served by the Gateway itself; the token is an encrypted form of the grant and is not stored (docs/web/urls.md:223-268).state/openclaw.sqlitebackup_runs(id,created_atepoch ms,archive_path,statusok|failed,manifest_json) (src/state/openclaw-state-schema.sql:1843-1849): one row peropenclaw backupattempt.manifest_jsonholdskind(archive|sqlite-snapshot|git|external),target(the storage location name for an offsite archive, the commit sha for a Git backup),namespace,error,pushFailed,bytes, and for an uploadlocation {name, provider, locationId, key, namespace, plaintextBytes, storedBytes}andretention {kept, deleted}(src/state/backup-run-records.contract.ts:98-124; writerssrc/commands/backup.ts:57-70,src/commands/backup-git.ts:140-151). The table is pruned to the newest 200 rows plus the newest attempt and newest success per target (src/state/backup-run-records.kernel.ts:22-48).state/openclaw.sqliteworker_session_placements, one row per session whose turns run on a paired device or a cloud worker:session_id,session_key,execution_mode(worker-turn|remote-exec),state(localtofailed),environment_id,remote_workspace_dir,last_transcript_ack_cursor,created_at_ms,updated_at_ms(src/state/openclaw-state-schema.sql:2142-2170). The row is deleted when the placement is retired (src/gateway/worker-environments/placement-store.ts:261-279).
PostgreSQL engine¶
Experimental at 146284a and absent at 3b16db7, which has no PostgreSQL source file.
With database.engine: "postgres" in openclaw.json the shared state, the agent databases
(transcripts included), and the Workboard, Logbook and Team Reports stores live on a PostgreSQL
server named by database.postgres.connection (a DSN string or a SecretRef);
database.families.agent.engine moves the agent databases, transcripts included
(docs/reference/database-engines.md:68-73,
:96-116).
state/openclaw.sqlite and each local openclaw-agent.sqlite then are "a small SQLite anchor"
holding one table, openclaw_engine_anchor (store_id, engine, schema) with engine postgres and
schema the server namespace <schemaPrefix>_<store>_<uuid-prefix>
(:591-595;
src/infra/database-engine/open.ts:149).
The transcript is then on the database server, wherever the operator runs it, and no local file
holds it; transcript payloads are plain JSON there, never zstd. A responder who finds an anchor
needs the DSN (in openclaw.json or the environment, Credentials) and the schema
value to request the server's copy. Daemon runtime pins stay local in control/services.sqlite on
either engine (Contents of the state directory). The parser
yields one whole-file row for an anchor, naming the store and the schema
(Sync, relay and origin).
Format versions¶
Header version: current 4, minimum readable 3; versions 1-2 need doctor import and may lack
id/parentId; the old hookMessage role becomes custom with customType:"hook"
(config/sessions/version.ts:1-4;
session-entry-codec.ts:154-179).
Database schema version is in schema_meta.schema_version
(openclaw-agent-schema.sql:6-14).
Test fixtures for schema v1, v14, v15, v19, v21 and v22 are in test/fixtures/sqlite/.
At 146284a
(2026-10-19) the agent schema is 31; the history lists 25 to 31 as unreleased changes to
session-writer validation, transcript navigation columns, the context-engine outbox, index sizes,
Brotli-compressed trajectory payloads and memory-index revisions
(docs/reference/database-schemas/agent-schema-history.md:43-50).
transcript_events gained navigation_* and message_role columns but keeps session_id, seq,
event_json, event_zstd, event_utf8_bytes and created_at with the same zstd rule
(openclaw-agent-schema.sql:437-482);
the archive tables and session_windows keep every column the parser reads; the header version is
still 4
(config/sessions/version.ts:2).
The archive and checkpoint file-name patterns moved, unchanged, to
config/sessions/transcript-file-names.ts:12-16;
the exec tool name cited in Parser plan now comes from
agents/lazy-exec-tool.ts:63,
core-tool-factory-descriptors.ts being gone, and skills/runtime/refresh-source-roots.ts,
cited in Contents of the state directory, has moved. No other
drift changes what the parser reads; the one change that defeats the parser is the PostgreSQL
engine (PostgreSQL engine).
Secrets¶
In the same database as the transcripts, openclaw-agent.sqlite: auth_profile_store.store_json
and auth_profile_state.state_json (provider API keys and OAuth tokens), and session_suggestions
(state/secret-state-tables.ts:31-35;
openclaw-agent-schema.sql:742-752);
do not emit them. In state/openclaw.sqlite, exec_approvals_config.raw_json keeps the approvals
socket's socket.token after the legacy exec-approvals.json is migrated
(Permission records). In records: thinkingSignature and
providerReplay.data are opaque provider state; image.data is base64 media;
toolResult.content and bashExecution.output are raw tool output and may contain anything the
agent read, including .env files.
The key that encrypts public share tokens is derived from the Gateway's device identity, the
device_identities row of state/openclaw.sqlite
(src/gateway/control-ui-public-session-token.ts:196-217);
whoever holds that database can mint working share URLs for every publicShare grant. Offsite
backup credentials (accessKeyId, secretAccessKey) and the encryption passphrase are configured
under storage.locations in the config file as SecretRefs (references to a value held in the
environment or a secret store, not the value itself)
(docs/concepts/storage-locations.md:79-91).
Parser plan¶
Selection: */agent/openclaw-agent.sqlite under an openclaw artifact
(plus -wal); */sessions/*.jsonl, *.jsonl.reset.*, *.jsonl.deleted.*
(decompress .zst), *.checkpoint.<uuid>.jsonl and
*.jsonl.pre-doctor-*-repair-*.bak. Skip *.trajectory.jsonl and
*.migrated* (see Past use and secondary sources). SQL:
SELECT e.rowid, e.session_id, e.seq,
e.event_json, e.event_zstd, e.created_at, w.model, w.channel, w.session_key
FROM transcript_events e LEFT JOIN session_windows w USING (session_id) ORDER
BY e.session_id, e.seq. Then archive blobs.
| Column | Source |
|---|---|
| timestamp_utc | entry timestamp; else message.timestamp (ms); else created_at (ms) |
| session_id | session_id / header id |
| project_path | header cwd of the session |
| git_branch | empty |
| turn_type | role user → user; assistant text → assistant, toolCall block → tool_use, thinking block → thinking; toolResult → tool_result; bashExecution → tool_use (tool bash); other entry types and roles → system |
| model | assistant responseModel or model; model_change.modelId |
| tool_name | toolCall.name / toolResult.toolName |
| tool_use_id | toolCall.id / toolResult.toolCallId |
| text | text blocks; toolCall.arguments.command/path/url else JSON; tool result text; summary for compaction; reason for reset |
| source_line | transcript_events.rowid, or the JSONL line number |
Put session_key (channel and peer) into the system row for each session. The shell tool is named
exec
(agents/core-tool-factory-descriptors.ts:20);
api values include anthropic-messages
(llm-core/src/types.ts:23).
The rows added at 146284a rest on source alone and each
subsection below marks them; the per-step real-install round
AGENTS.md requires is still owed.
Permission rows¶
No transcript entry records a per-call permission decision; bashExecution.cancelled stays on the
bash tool_result row. The approvals and the standing policy are rows of state/openclaw.sqlite
and of each session's entry_json, whose columns are in Permission records.
The parser yields, all source-derived; not observed on any real install:
- One row per
operator_approvalsrow, atresolved_at_ms(created_at_mswhile pending), withsession_idsource_session_id,tool_namesource_tool_nameandtool_use_idsource_tool_call_id, so it sits beside the call it governed:permission decision: <decision, else status> status=<status> kind=<kind> resolver=<resolver_kind> reason=<terminal_reason> approval <approval_id> (from state/openclaw.sqlite operator_approvals, <resolved_at_ms or created_at_ms>). - One
permission setting: <scope>.<setting> (from state/openclaw.sqlite exec_approvals_config, updated_at_ms)row persecurity,ask,askFallbackandautoAllowSkillsvalue (<key>=<value>), per allowlist entry (allowlist=<pattern>[ argPattern=<argPattern>] source=<source>) and per MCP tool grant (mcpTools=<server>/<tool> source=<source>) ofraw_json'sdefaultsand of eachagents.<id>,<scope>beingdefaultsoragents.<id>(packages/gateway-protocol/src/schema/exec-approvals.ts:15-75).socketis never read (Secrets); araw_jsonthat does not decode is oneparser:row that withholds the text, since the text holds the token. permission setting: standing grant <grant_id> allow-always cron job <cron_job_id> agent <agent_id> binding <operation_binding> approval <minted_by_approval_id> expires=<expires_at_ms> revoked=<revoked_at_ms> uses=<use_count> (from state/openclaw.sqlite operator_approval_standing_grants, created_at_ms)per standing grant.permission setting: channel <channel_key> account <account_id> allowFrom=<entry> (from state/openclaw.sqlite channel_pairing_allow_entries, updated_at)per allowed sender.- One
permission setting: <key>=<value> (from openclaw-agent.sqlite session_nodes.entry_json, updated_at)row perelevatedLevel,execHost,execNode,execCwd,toolOverrides(as JSON) andsendPolicyof a session'sentry_json, after the start row of its current generation, atsession_nodes.updated_at.
audit_events is not read: its kind and action values were not enumerated.
Model rows¶
system row model: <id> provider=<provider> at the first model a session names and
model: <old> -> <new> provider=<provider> on every later entry whose model differs, with the
model column set to the new id. Sources, in order: the session_windows model and
model_provider (the row follows the session-start row, at its time), model_change modelId and
provider, and the assistant responseModel, else model, with provider; when responseModel
differs from model the row ends requested=<model>. responseModel is the concrete model the
stream reported "when different from the requested model"
(llm-core/src/types.ts:533).
A model_change entry naming the model already in force still yields the plain
model: <id> provider=<provider> row, since it records a switch; an assistant message that repeats
the current model yields none.
The session's model override (Model override) gives one
model: <modelOverride> provider=<providerOverride> source=<modelOverrideSource> (from openclaw-agent.sqlite session_nodes.entry_json modelOverride, updated_at)
row, with fallback_from=<provider>/<model> before the parenthesis when
modelOverrideFallbackOriginProvider or Model is set, after the start row of the session's
current generation, at session_nodes.updated_at, the model column the override. It records a
/model the user typed even when no turn followed. The entry keeps only the current override, so
the row names no previous model. Source-derived; not observed on any real install.
Past-use rows and secondary sources¶
Every source is parsed on
every run, and a row of a secondary source is dropped when a primary
store's row records the same event
(issue 122; see
Secondary sources in analyzer/docs/output.md).
- Primary:
openclaw-agent.sqlite, all of it:transcript_events, the cold archives and everysession_transcript_archivesblob, whether or not itspublished_atis set (the time the blob was also written to the sessions directory as a file). The schema calls that table the "canonical cold-tier owner for reclaimed transcript generations", from which "the derived .deleted/.reset file may be recreated" (openclaw-agent-schema.sql:606-607); the file is published from the blob's exact bytes, hash-checked (session-accessor.sqlite-archive-artifact.ts:177-216).openclaw doctor(the CLI's repair and migration command) "repairs canonical blobs before their reconstructible files", rewriting a published file from its row (state-migrations.transcript-directives-archives.ts:327-379), so the table, not the file, is the store of record. The legacy plain<sessionId>.jsonlis primary too: before doctor imports it, it is the only copy. - Secondary, the published
.jsonl.reset.*/.jsonl.deleted.*file: the same entries as the blob. It survives the database being lost or excluded; otherwise the blob outlives it, since retention deletes a published row only once its file is gone (session-history-archive-pruning.worker.ts:105-127). - Secondary,
<name>.checkpoint.<uuid>.jsonl: a pre-compaction snapshot, a byte copy of the session file taken before compaction (src/gateway/session-compaction-checkpoints.ts:383-389at6814525, the parent of the commit that stopped copying,c59635a). At3b16db7no runtime writes new ones; the session store keeps only the references (legacy-compaction-history.ts:3-7), so the files exist on hosts that ran an older release. A checkpoint holds the entries a compaction later summarised, which survive in the legacy file or, after import, intranscript_events; it survives a compaction of the live transcript but is a copy of nothing the primary store lost unless that store is gone.<name>is the session file's name,<sessionId>or, for a session forked from a checkpoint,<ts>_<sessionId>(session-compaction-checkpoints.ts:320-332at6814525). - Secondary,
<sessionId>.jsonl.pre-doctor-(branch|openai-codex)-repair-<ts>.bak: a byte copy of a legacy transcript taken beforeopenclaw doctorrewrote it (src/commands/doctor-session-transcripts.ts:249-292at6559e3f, the parent of4273ca9, which removed the file-era runtime; the name pattern is still classified atartifacts.ts:13-14). A branch repair keeps only the active branch, so the backup also holds the abandoned branches, which no other store has, and its rows for them are kept. - Not read:
<id>.trajectory.jsonl, the trajectory flight recorder (a per-run log of runtime events: prompt submitted, tool call, model completed), a different schema markedopenclaw-trajectory(docs/tools/trajectory.md:76-102), not a transcript copy;*.migrated[.N](artifacts.ts:15), for which no writer of a session transcript was found at3b16db7or6559e3f(the suffix is used there for migrated cron logs, pairing stores and audit logs); and<id>.jsonl.bak.<ts>, the legacy compact backups (artifacts.ts:43-50), whose content is not recorded.
Joins: the copies are byte copies, so their entries carry the same id, timestamp and header
cwd as the primary store's rows, and the parser takes the session id of every JSONL file from its
header id (the name before the suffix only when there is no header), the id
transcript_events.session_id and the archive table carry. An archive whose session id is over
96 bytes is named session-<sha256>
(session-accessor.sqlite-archive-artifact.ts:24-29),
which the header id overrides. Every entry row of a copy therefore pairs with its primary twin at
the same time (a 2-second tolerance is not needed but is the default). The session-start row does
not: a copy's names the file (archive=<reason>.<ts> or copy=<suffix>) where the database's
carries the session_windows and archive columns, so each copy keeps one row showing it exists.
Covered: the archive blobs and their files, checkpoints and pre-doctor copies, and, source-derived;
not observed on any real install, these tables of
Other records of past use. The full-text index and the pending inputs
repeat transcript entries inside the primary database, where the file-level secondary flag cannot
mark them, so the parser joins them on the entry id itself and marks what it keeps recovered:
session_pending_inputs, each accepted prompt,message_jsona user message (roleuser,content;src/config/sessions/session-pending-input-value.ts:8-15): a row with noconsumed_event_idis primary and yields auserrow andpending input: <state>, not consumed by the transcript (input <input_id> run <run_id>; from openclaw-agent.sqlite session_pending_inputs, accepted_at), ataccepted_at; a consumed row yields itsuserrow, recovered, only when no transcript entry of the database has theconsumed_event_id. Amessage_jsonthat does not decode is its ownparser:row.session_transcript_fts, read from its FTS5 content tablesession_transcript_fts_contentas a plain table, its columnsc0toc4beingtext,session_id,message_id,roleandtimestampin the declared order (openclaw-agent-schema.sql:809-816): auserorassistantrow (elsesystem,<role>: <text>) per indexed entry whose(session_id, message_id)no transcript row carries, recovered.conversation_deliveriesjoined toconversations, withsession_idthe current generation ofsource_session_key:channel delivery: <operation_kind> <status> channel <channel> conversation <conversation_id> platform message <platform_message_id> (from openclaw-agent.sqlite conversation_deliveries, created_at), witherror=<rejection_error>before the parenthesis for a rejected send, and, when a platform message replied to the delivery (reply_message_id,reply_text,reply_timestamp;src/config/sessions/conversation-delivery-store.types.ts:24-30),channel delivery reply: <reply_text> (reply message <reply_message_id>; from openclaw-agent.sqlite conversation_deliveries, reply_timestamp). The outbound text itself is stored only asmessage_hash, so the rows repeat no transcript text.channel_ingress_eventsofstate/openclaw.sqlite:channel ingress: <channel_id> account <account_id> status <status>: <payload_json> (from state/openclaw.sqlite channel_ingress_events, received_at), the payload whole since its shape per channel was not determined, with no session id; it cannot be matched to the transcript.
memory_index_chunks waits until the form of its path for a session is known.
Sync, relay and origin rows¶
Sync: OpenClaw keeps no copy of a session on a vendor service. Session
content leaves the Gateway host in three ways, all operator actions; the
first two leave a local record (Sharing and upload state).
Both are covered (session synced: public share, backup uploaded:).
- Public link:
session_nodes.entry_jsonpublicSharemarks a session whose user messages and final answers anyone with the URL can read, served by the Gateway (opt-in per session, off by default,docs/concepts/multi-user.md:21-36). The parser yields onesystemrow after the generation's session-start row:session synced: public share <publicShare.id> generation <publicShare.sessionId> (from openclaw-agent.sqlite session_nodes.entry_json, publicShare.createdAt), timestamped bycreatedAt, withsession_idthe generation, and setsrecords_sync; a grant whose generation has no transcript rows in the database still yields its row. Fixture only: no real install has shown the key. Turning access off removes the key, and deleting the session deletes itssession_nodesrow (src/config/sessions/session-accessor.sqlite-entry-store.ts:271-274); the.jsonl.deleted.*archive and itssession_transcript_archivesblob keep the transcript but have no column for the grant (src/state/openclaw-agent-schema.sql:608-624), so no local file says a deleted or unpublished session was ever public. - Backup upload:
openclaw backup create --to <location>uploads the whole state directory, every agent database included, to a configured storage location (a mounted disk, or Cloudflare R2, the Cloudflare object store, through a plugin) underbackups/<namespace>/<UTC ts>-<8 hex>.tar.gz, andopenclaw backup git create --pushpushes per-table JSONL dumps to a Git remote; both are opt-in and can be scheduled withopenclaw backup enable(docs/cli/backup.md:86-96,docs/cli/backup.md:318-321,docs/cli/backup.md:364). The marker is host-wide, not per session: eachbackup_runsrow says every session in the uploaded databases atcreated_atleft the host. The parser yields onesystemrow perbackup_runsrow withstatusokand either alocationor a GittargetwithoutpushFailed, with an emptysession_id, in the agent's own term rather thansession synced:because it marks no one session:backup uploaded: <kind> to <location.name or archive_path> key <location.key or target> namespace <location.namespace or namespace or -> (from state/openclaw.sqlite backup_runs, created_at), timestamped bycreated_at. A Git backup run without--pushrecords the same manifest as a pushed one,targetthe commit and nopushFailed(src/commands/backup-git.ts:140-151), so a Git row says a commit was made and may have been pushed. Fixture only. Deleting a session does not touchbackup_runs, and the uploaded archives keep the session; retention prunes the table (docs/cli/backup.md:486-499). - Remote execution: a session placed on a paired device or a Crabbox cloud worker (a machine
OpenClaw rents from AWS, Hetzner or another backend for the session) sends its workspace and turn
context to that machine while the Gateway keeps the canonical transcript
(
docs/gateway/cloud-sessions.md:10-20). This is execution, not a copy kept elsewhere, and no row is proposed;worker_session_placementsnames the environment while the placement lives.
Not sync: the daily update check and the opt-in anonymous statistics carry no prompt or reply text,
only a session-creation count
(docs/gateway/telemetry.md:11-24);
OpenTelemetry export sends message text only when diagnostics.otel.captureContent is true, to
the operator's own collector
(docs/gateway/opentelemetry/privacy-and-trace-context.md:47-56).
Searched share, publicShare, sync, upload, cloud, remote, backup, telemetry,
export and publish in src/ and docs/.
Relay (records_relay; source-derived; not observed on any real install): the records are under
Joins and timestamps. After a generation's start row the parser yields one
session relayed: <channel> account <account_id> <kind> peer <peer_id> thread <thread_id> role <role> (from openclaw-agent.sqlite session_conversations and conversations, first_seen_at)
row per session_conversations link, at first_seen_at; for a generation with no such link,
session relayed: <channel> account <account_id> <chat_type> (from openclaw-agent.sqlite session_windows, started_at)
from its window; and for a session with neither, from an entry_json.delivery of kind external,
session relayed: <route.channel> account <account> <origin.chatType> from <origin.from> to <origin.to> (from openclaw-agent.sqlite session_nodes.entry_json delivery, updated_at)
(route is a channel route whose channel is the lower-case channel id,
src/plugin-sdk/channel-route.ts:18-25).
A window whose hook_external_content_source is gmail, a session a Gmail hook started, is an
email relay:
session relayed: gmail hook (from openclaw-agent.sqlite session_windows hook_external_content_source, started_at).
The Control UI channel webchat and the internal wake sources heartbeat, cron, webhook,
voice and sessions_send are not platforms and give no row
(src/utils/message-channel-constants.ts:3,
:14-20).
The start row still carries channel, account_id and chat_type.
Origin: the agent records none (no session is created on a vendor server; grepped origin and
remote in src/config/sessions/). A PostgreSQL anchor (PostgreSQL engine)
yields one whole-file row, source-derived; not observed on any real install:
parser: agent database is a PostgreSQL anchor (store <store_id>, schema <schema>): transcripts are on the database server,
or for state/openclaw.sqlite
parser: state database is a PostgreSQL anchor (store <store_id>, schema <schema>): the Gateway state is on the database server,
so the gap is visible.
Scratch directory rows¶
The scratch directories are under the state
directory, which the analyzer's catalog rule classes as scratch; proposed, a scratch directory:
row per state/openclaw.sqlite projects row whose source is cloned
and per worktrees row, so a state directory moved by
OPENCLAW_STATE_DIR is classed too.
Deletion rows¶
A deleted archive shows as an archive=deleted.<ts> session-start row
(What survives a delete) and, source-derived; not observed on any real
install, three rows mark a delete, an archive and a forget:
session deleted: <session_id> archive <archive_name> (from openclaw-agent.sqlite session_transcript_archives reason deleted, created_at)after the rows of eachsession_transcript_archivesblob whosereasonisdeleted, atcreated_at, even when the blob does not decompress.session archived: <archiveReason> by <archivedBy.type>:<archivedBy.id> (from openclaw-agent.sqlite session_nodes.entry_json, archivedAt)after the start row of the session's current generation, atarchivedAt;archivedByis a{type, id, label}actor (src/config/sessions/session-entry-provenance.ts:16-18).session forgotten from memory: <session_id> agent <agent_id> reason=<reason> (from openclaw-agent.sqlite memory_session_tombstones, created_at)per tombstone, with thecwdof the session's transcript when the database still holds one.
Bad-line rows¶
Issue 177: a legacy sessions.json that
cannot be read, does not decode or is not a JSON object yields one whole-file system
row in each transcript beside it, after that transcript's line rows,
with source_line 0 and the transcript's session_id and cwd:
parser: sessions.json unreadable (<reason>), parser: sessions.json
unparseable (<reason>) or parser: sessions.json is not a JSON object;
the transcript's own rows are still yielded, without the session key.
A line that is valid JSON but not an object (an array, a string, a
number, null), in a JSONL file or an archive blob, is its own row like
any other bad line, parser: unparseable line (not a JSON object):
<line> (prefixed archive <name> line <n>: inside a blob), and the
lines around it are still yielded.
Fixtures¶
JSONL form; the SQLite form stores the same strings in event_json:
{"type":"session","version":4,"id":"7d0c2a8e-1111-4000-8000-000000000001","timestamp":"2026-10-01T09:00:00.000Z","cwd":"/home/alice/.openclaw/workspace"}
{"type":"message","id":"a1b2c3d4","parentId":null,"timestamp":"2026-10-01T09:00:01.000Z","message":{"role":"user","content":[{"type":"text","text":"check disk space on the server"}],"timestamp":1759309201000}}
{"type":"message","id":"b2c3d4e5","parentId":"a1b2c3d4","timestamp":"2026-10-01T09:00:04.000Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"Use exec with df."},{"type":"text","text":"Checking."},{"type":"toolCall","id":"call_01","name":"exec","arguments":{"command":"df -h"}}],"api":"anthropic-messages","provider":"anthropic","model":"claude-sonnet-4-5","usage":{"input":900,"output":40,"cacheRead":0,"cacheWrite":0,"totalTokens":940,"cost":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"total":0}},"stopReason":"toolUse","timestamp":1759309204000}}
{"type":"message","id":"c3d4e5f6","parentId":"b2c3d4e5","timestamp":"2026-10-01T09:00:05.000Z","message":{"role":"toolResult","toolCallId":"call_01","toolName":"exec","content":[{"type":"text","text":"/dev/sda1 50G 20G 30G 40% /"}],"isError":false,"timestamp":1759309205000}}
{"type":"model_change","id":"d4e5f6a7","parentId":"c3d4e5f6","timestamp":"2026-10-01T09:01:00.000Z","provider":"openai","modelId":"gpt-5"}
A session_nodes.entry_json with the delivery route, model override and permission keys the
parser reads (src/config/sessions/types.ts:60-83,
:481-516,
:532),
and the raw_json of an exec_approvals_config row, whose socket.token the parser never reads:
{"sessionId":"7d0c2a8e-1111-4000-8000-000000000001","delivery":{"kind":"external","route":{"channel":"telegram","accountId":"default"},"context":{},"origin":{"provider":"telegram","chatType":"direct","from":"123456789"}},"providerOverride":"openai","modelOverride":"gpt-5","modelOverrideSource":"user","elevatedLevel":"on","execHost":"gateway","toolOverrides":{"mcpServers":"off"},"sendPolicy":"allow"}
{"version":1,"socket":{"path":"/home/alice/.openclaw/exec.sock","token":"<redacted>"},"defaults":{"security":"allowlist","ask":"on-miss"},"agents":{"main":{"autoAllowSkills":true,"allowlist":[{"pattern":"df","source":"allow-always"}],"mcpTools":[{"server":"github","tool":"search","source":"allow-always","addedAt":1}]}}}
Source host rows¶
OpenClaw connects a GitHub account as a managed identity per Gateway or per agent, an OAuth
sign-in or a GitHub App installation, which its bundled GitHub plugin (reader, github_read and
github_search tools) and its publication flow use
(extensions/github/README.md:57-59;
src/config/zod-schema.github-identity.ts:6-43).
The token is local: the OAuth record is a secret_store_entries row and an App's private key is
in openclaw.json (Credentials). A GitHub App installation lists the repositories
it may reach as repositories {id, fullName} in that config entry (same lines). The
repository-bound records are the GitHub publication requests of state/openclaw.sqlite,
github_publication_requests, github_personal_publication_requests and
github_repository_publication_requests, one row per push or pull request the Gateway made for a
session, with identity_source (system-detected|system-configured|agent-override|personal),
identity_account_id, identity_login, status, repository, branch, pull_request_url and
created_at_ms
(openclaw-state-schema.sql:2093-2140,
:2410-2482,
:2488-2531).
Each yields, source-derived; not observed on any real install,
source host: github account <identity_login> id <identity_account_id> identity <identity_source>; publication <status> repository <repository> branch <branch> pull request <pull_request_url> (from state/openclaw.sqlite <table>, created_at_ms)
at created_at_ms, session_id the session. The App installation's repository list is not read,
since the parser reads no openclaw.json (JSON5), so it gives no source host repository: rows.
Git remote rows¶
Each row of the state/openclaw.sqlite projects table
(Where the project path is recorded) whose origin_url is
set yields, source-derived; not observed on any real install, git remote: <origin_url>
(system) at the row's created_at_ms, with project_path its repo_root and an empty
session_id: the table names a repository the Gateway registered or cloned, not a session
(issue 253). Its other columns are not
read.
Confidence¶
- High: state directory resolution, profiles, the legacy
.clawdbotmigration, database file names, table and column names and schemas, workspace andagentDirdefaults, archive file naming, credential tables, service names; entry and message field names, the compression rule and the version constants. - Medium: the exclusion list comes from the backup documentation rather than each writer;
models.jsonholding literal keys (inferred from the doctor migration);browser/*/user-data/*/Cacheassumes Chromium's usualDefault/profile layout; the.moltbotlayout is assumed identical to.clawdbot(Legacy names); that legacy.jsonlfiles are removed after import on every path;cwdmeaning the workspace. - Not determined: the Windows log location beyond
os.tmpdir(); what the macOS transcript cache stores; whether the iOS and Android apps keep transcripts on the phone; the shapes oftrajectory_runtime_events,acp_parent_stream_eventsandcustom.datapercustomType; whether cold-archived generations keep anytranscript_eventsrows.