Database-First State Refactor: SQLite as Primary Durable State
This page details the migration plan for making SQLite the primary durable state and cache layer while keeping configuration file-backed. It is intended for developers and architects planning the state management overhaul.
Read this when
- Moving OpenClaw runtime data, cache, transcripts, task state, or scratch files into SQLite
- Designing doctor migrations from legacy JSON or JSONL files
- Changing backup, restore, VFS, or worker storage behavior
- Removing session locks, pruning, truncation, or JSON compatibility paths
Database-First State Refactor
Decision
Use a two-level SQLite layout:
- Global database:
~/.openclaw/state/openclaw.sqlite - Agent database: one SQLite database per agent for agent-owned workspace, transcript, VFS, artifact, and large per-agent runtime state
- Configuration stays file-backed:
openclaw.jsonremains outside the database. Runtime auth profiles move to SQLite; external provider or CLI credential files remain owner-managed outside OpenClaw's database.
The global database is the control-plane database. It owns agent discovery, shared gateway state, pairing, device/node state, task and flow ledgers, plugin state, scheduler runtime state, backup metadata, and migration state.
The agent database is the data-plane database. It owns the agent's session metadata, transcript event stream, VFS workspace or scratch namespace, tool artifacts, run artifacts, and searchable/indexable agent-local cache data.
This gives one durable global view without forcing large agent workspaces, transcripts, and binary scratch data into the shared gateway write lane.
Hard Contract
This migration has one canonical runtime shape:
- Session rows persist session metadata only. They must not persist
transcriptLocator, transcript file paths, sibling JSONL paths, lock paths, pruning metadata, or file-era compatibility pointers. - Transcript identity is always SQLite identity:
{agentId, sessionId}plus optional topic metadata where the protocol needs it. sqlite-transcript://...is not a runtime or protocol identity. New code must not derive, persist, pass, parse, or migrate transcript locators. Runtime and tests should not contain pseudo-locators at all; docs may mention the string only to ban it.- Legacy
sessions.json, transcript JSONL,.jsonl.lock, pruning, truncation, and old session-path logic belong only to the doctor migration/import path. - Legacy session config aliases belong only to doctor migration. Runtime does not interpret
session.idleMinutes,session.resetByType.dm, or cross-agentagent:main:*main-session aliases for another configured agent. - Session routing identity is typed relational state. Hot runtime and UI paths should read
sessions.session_scope,sessions.account_id,sessions.primary_conversation_id,conversations, andsession_conversations; they must not parsesession_keyor minesession_entries.entry_jsonfor provider identity except as a compatibility shadow while old call sites are being deleted. - Channel-level direct-message markers such as
dmversusdirectare routing vocabulary, not transcript locators or file-store compatibility handles. - Legacy hook handler config belongs only to doctor warning/migration surfaces. Runtime must not load
hooks.internal.handlers; hooks run through discovered hook directories andHOOK.mdmetadata only. - Runtime startup, hot reply paths, compaction, reset, recovery, diagnostics, TTS, memory hooks, subagents, plugin command routing, protocol boundaries, and hooks must pass
{agentId, sessionId}through the runtime. - Tests should seed and assert SQLite transcript rows through
{agentId, sessionId}. Tests that only prove JSONL path forwarding, caller-supplied locator preservation, or transcript-file compatibility should be deleted unless they cover doctor import, non-session support/debug materialization, or protocol shape. runEmbeddedPiAgent(...), prepared worker runs, and the inner embedded attempt must not accept transcript locators. They open the SQLite transcript manager by{agentId, sessionId}and pass that manager to the internalized PI-compatible agent session, so stale callers cannot make the runner write JSON/JSONL transcripts.- Runner diagnostics must store runtime/cache/payload trace records in SQLite. Runtime diagnostics must not expose JSONL file override knobs or generic transcript JSONL export helpers; user-facing exports can materialize explicit artifacts from database rows without feeding file names back into runtime.
- Raw stream logging uses
OPENCLAW_RAW_STREAM=1plus SQLite diagnostics rows. The old pi-monoPI_RAW_STREAM,PI_RAW_STREAM_PATH, andraw-openai-completions.jsonlfile logger contract is not part of OpenClaw runtime or tests. - QMD memory indexing must not export SQLite transcripts to markdown files. QMD indexes configured memory files only; session transcript search stays SQLite-backed.
- The QMD SDK subpath is QMD-only for new code. SQLite session transcript indexing helpers live on
memory-core-host-engine-session-transcripts; any QMD re-export is compatibility only and must not be used by runtime code. - Built-in memory indexes live in the owning agent database. Runtime config and resolved runtime contracts must not expose
memorySearch.store.path; doctor deletes that legacy config key and current code passes the agentdatabasePathinternally.
Implementation work should keep deleting code until these statements are true without exceptions outside doctor/import/export/debug boundaries.
Goal state and progress
Hard goal
- One global SQLite database owns control-plane state:
state/openclaw.sqlite. - One per-agent SQLite database owns data-plane state:
agents/<agentId>/agent/openclaw-agent.sqlite. - Config remains file-backed.
openclaw.jsonis not part of this database refactor. - Legacy files are doctor migration inputs only.
- Runtime never writes or reads session or transcript JSONL as active state.
Goal states
not-started: file-era runtime code still writes active state.migrating: doctor/import code can move file data into SQLite.dual-read: temporary bridge reads both SQLite and legacy files. This state is forbidden for this refactor unless it is explicitly documented as doctor-only.sqlite-runtime: runtime reads and writes SQLite only.clean: legacy runtime APIs and tests are removed, and the guard prevents regressions.done: docs, tests, backup, doctor migration, and changed checks prove the clean state.
Current state
- Sessions:
cleanfor runtime. Session rows are stored in the per-agent database, runtime APIs use{agentId, sessionId}or{agentId, sessionKey}, andsessions.jsonis a doctor-only legacy input. - Transcripts:
cleanfor runtime. Transcript events, identities, snapshots, and trajectory runtime events reside in the per-agent database. Runtime no longer accepts transcript locators or JSONL transcript paths. - PI embedded runner:
clean. Embedded PI runs, prepared workers, compaction, and retry loops use SQLite session scope and reject stale transcript handles. - Cron:
cleanfor runtime. Runtime usescron_jobsand cron-ownedtask_runs; runtime tests use SQLitestoreKeynaming, and file-era cron paths remain only in doctor legacy migration tests. - Task registry:
clean. Task and Task Flow runtime rows live instate/openclaw.sqlite; unshipped sidecar SQLite importers are removed. - Plugin state:
clean. Plugin state and blob rows live in the shared global database; old plugin-state sidecar SQLite helpers are guarded against. - Memory:
sqlite-runtimefor built-in memory and session transcript indexing. Memory index tables live in the per-agent database, plugin memory state uses shared plugin-state rows, and legacy memory files are doctor migration inputs or user workspace content. - Backup:
sqlite-runtime. Backup stages compact SQLite snapshots, omits live WAL or SHM sidecars, verifies SQLite integrity, and records backup runs in the global database. - Workspace setup:
sqlite-runtime. Setup completion, workspace attestations, and generated bootstrap hashes live in typed shared SQLite tables. Runtime does not read or write the retired workspace JSON and.attestedsidecars; Doctor owns their validated import and verified removal. - Doctor migration:
migrating, intentionally. Doctor imports legacy JSON, JSONL, and retired sidecar stores into SQLite, records migration runs and sources, and removes successful sources. - Exec approvals:
file-runtime. TypeScript and macOS still read and write the active state directory'sexec-approvals.json; the reservedexec_approvals_configschema has no runtime owner yet. A future cutover must add same-state doctor import and move both runtimes together. - E2E scripts:
cleanfor runtime coverage. Docker MCP seeding writes SQLite rows. The runtime-context Docker script creates legacy JSONL only inside the doctor migration seed and names the legacy session index path explicitly.
Remaining work
- Rename cron runtime-test store variables away from
storePathunless they are doctor legacy inputs. Files:src/cron/service.test-harness.ts,src/cron/service.runs-one-shot-main-job-disables-it.test.ts,src/cron/service/timer.regression.test.ts,src/cron/service/ops.test.ts,src/cron/service/store.test.ts,src/cron/service.heartbeat-ok-summary-suppressed.test.ts,src/cron/service.main-job-passes-heartbeat-target-last.test.ts,src/cron/store.test.ts. Proof:pnpm check:database-first-legacy-stores;rg -n 'storePath' src/cron --glob '!**/commands/doctor/**'. - Remove or rename obsolete file-era export test mocks.
File:
src/auto-reply/reply/commands-export-test-mocks.ts. Proof:rg -n 'resolveSessionFilePath|sessionFile|storePath|transcriptLocator' src/auto-reply/reply. - Make the Docker runtime-context legacy JSONL seed obviously doctor-only.
File:
scripts/e2e/session-runtime-context-docker-client.ts. Proof:rg -n 'sessions\\.json|sessionFile|\\.jsonl' scripts/e2e/session-runtime-context-docker-client.tsshows onlyseedBrokenLegacySessionForDoctorMigration. - Keep Kysely generated types aligned after any schema change.
Files:
src/state/openclaw-state-schema.sql,src/state/openclaw-agent-schema.sql,src/state/*generated*. Proof: no schema change in this pass;pnpm db:kysely:check;pnpm lint:kysely. - Re-run focused tests for touched stores, commands, and scripts.
Proof:
pnpm test src/cron/service/store.test.ts src/cron/store.test.ts src/cron/service.heartbeat-ok-summary-suppressed.test.ts src/cron/service.main-job-passes-heartbeat-target-last.test.ts src/cron/service.every-jobs-fire.test.ts src/cron/service.persists-delivered-status.test.ts src/cron/service.runs-one-shot-main-job-disables-it.test.ts src/cron/service/ops.test.ts src/cron/service/timer.regression.test.ts src/auto-reply/reply/commands-export-session.test.ts extensions/telegram/src/thread-bindings.test.ts extensions/slack/src/monitor/message-handler/prepare.test.ts src/acp/translator.session-lineage-meta.test.ts;git diff --check. - Before declaring
done, run the changed gate or remote broad proof. Proof:pnpm check:changed --timed -- <changed extension paths>passed on Hetzner Crabbox runrun_3f1cabf6b25cafter temporary Node 24 and pnpm setup and explicit path routing for the synced no-.gitworkspace.
Do not regress
- No transcript locators.
- No active session files.
- No fake JSONL test fixtures except doctor legacy migration tests.
- No raw SQLite access where Kysely is expected.
- No new file-era DB migrations. The global schema remains at version
1. The shipped per-agent version1schema has one bounded runtime migration to version2for stable memory-source identities.
Code-Read Assumptions
No follow-up product decisions are blocking this plan. The implementation should proceed with these assumptions:
- Use
node:sqlitedirectly and require a WAL-reset-safe Node runtime (22.22.3+, 24.15+, or 25.9+) for this storage path. - Keep exactly one normal configuration file. Do not move config, plugin manifests, or Git workspaces into SQLite in this refactor.
- Runtime compatibility files are not required. Legacy JSON and JSONL files are migration inputs only. The branch-local SQLite sidecars never shipped and are deleted instead of imported.
openclaw doctor --fixowns legacy file-to-database migration. Runtime startup owns only bounded upgrades between shipped SQLite schema versions; it must not import file-era state.- Credential compatibility follows the same rule: runtime credentials live in SQLite. Old
auth-profiles.json, per-agentauth.json, and sharedcredentials/oauth.jsonfiles are doctor migration inputs, then removed after import. - Generated model catalog state is database-backed. Runtime code must not write
agents/<agentId>/agent/models.json; existingmodels.jsonfiles are legacy doctor inputs and are removed after import intoagent_model_catalogs. - Runtime must not migrate, normalize, or bridge transcript locators. Active transcript identity is
{agentId, sessionId}in SQLite. File paths are legacy doctor inputs only, andsqlite-transcript://...must disappear from runtime, protocol, hook, and plugin surfaces instead of being treated as a boundary handle. - Runtime SQLite transcript reads do not run old JSONL entry-shape migrations or rewrite whole transcripts for compatibility. Legacy entry normalization stays in explicit doctor/import utilities. Doctor normalizes legacy JSONL transcript files before inserting SQLite rows; current runtime rows are already written in the current transcript schema. Trajectory/session export reads those rows as-is and must not perform export-time legacy migrations.
- Legacy transcript JSONL parse/migration helpers are doctor-only. Runtime transcript format code builds current SQLite transcript context only; doctor owns old JSONL entry upgrades before inserting rows.
- The old runtime-owned JSONL transcript streaming helper was deleted. Doctor import code owns explicit legacy file reads; runtime session history reads SQLite rows.
- Codex app-server bindings use the OpenClaw
sessionIdas the canonical key in the Codex plugin-state namespace.sessionKeyis metadata for routing/display and must not replace the durable session id or resurrect transcript-file identity. - Context engines receive the current runtime contract directly. The registry must not wrap engines with retry shims that delete
sessionKey,transcriptScope, orprompt; engines that cannot accept the current database-first params should fail loudly instead of being bridged. - Backup output should remain one archive file. Database contents should enter that archive as compact SQLite snapshots, not raw live WAL sidecars.
- Transcript search is useful but not required for the first database-first cut. Design the schema so FTS can be added later.
- Worker execution should stay experimental behind settings while the database boundary settles.
Code-Read Findings
The current branch is already past the proof-of-concept stage. The shared database exists, Node node:sqlite is wired through a small runtime helper, and former stores now write to state/openclaw.sqlite or the owning openclaw-agent.sqlite database.
The remaining work is not choosing SQLite; it is keeping the new boundary clean and deleting any compatibility-shaped interfaces that still look like the old file world:
- Session
storePathis no longer a runtime identity, test fixture shape, or status payload field. Runtime and bridge tests no longer contain thestorePathcontract name; doctor/migration code owns that legacy vocabulary. - Session writes no longer pass through the old in-process
store-writer.tsqueue. SQLite patch writes prepare outside the transaction, then use a short synchronous validate/apply transaction with explicit conflict detection. - Legacy path discovery still has valid migration uses, but runtime code should stop treating
sessions.jsonand transcript JSONL files as possible write targets. - Agent-owned tables live in per-agent SQLite databases. The global DB keeps registry/control-plane rows; transcript identity is
{agentId, sessionId}in the per-agent transcript rows. Runtime code must not persist transcript file paths or migrate transcript locators. - Doctor already imports several legacy files. The cleanup is to make that a single explicit migration implementation that doctor calls, with a durable migration report.
No additional product questions are blocking implementation.
Current Code Shape
The branch already has a real shared SQLite base:
-
The runtime floor now requires a WAL-reset-safe Node build: 22.22.3+, 24.15+, or 25.9+.
package.json, the CLI runtime guard, installer defaults, macOS runtime locator, CI, and public install docs all agree. -
src/state/openclaw-state-db.tsopensopenclaw.sqlite, sets WAL,synchronous=NORMAL,busy_timeout=30000,foreign_keys=ON, and applies the generated schema module derived fromsrc/state/openclaw-state-schema.sql. -
Kysely table types and runtime schema modules are generated from disposable SQLite databases created from the committed
.sqlfiles; runtime code no longer keeps copy-pasted schema strings for global, per-agent, or proxy capture databases. -
Runtime stores derive selected and inserted row types from those generated Kysely
DBinterfaces instead of shadowing SQLite row shapes by hand. Raw SQL remains limited to schema application, pragmas, and migration-only DDL. -
The global SQLite schema remains at
user_version = 1. The per-agent schema is at version2; its opener atomically migrates the shipped version1memory-source key to a stable integer identity. File-to-database import remains in doctor code. -
Relational ownership is enforced where the ownership boundary is canonical: source migration rows cascade from
migration_runs, task delivery state cascades fromtask_runs, and transcript identity rows cascade from transcript events. -
Current shared tables include
agent_databases,auth_profile_stores,auth_profile_state,plugin_state_entries,plugin_blob_entries,media_blobs,skill_uploads,capture_sessions,capture_events,capture_blobs,sandbox_registry_entries,cron_jobs,commitments,delivery_queue_entries,model_capability_cache,workspace_setup_state,workspace_path_aliases,workspace_attestations,workspace_generated_bootstrap_hashes,native_hook_relay_bridges,current_conversation_bindings,plugin_binding_approvals,tui_last_sessions,acp_sessions,acp_replay_sessions,acp_replay_events,task_runs,task_delivery_state,flow_runs,subagent_runs,migration_runs, andbackup_runs. -
Arbitrary plugin-owned state does not get host-owned typed tables. Installed plugins use
plugin_state_entriesfor versioned JSON payloads andplugin_blob_entriesfor bytes, with namespace/key ownership, TTL cleanup, backup, and plugin migration records. Host-owned plugin orchestration state can still have typed tables when the host owns the query contract, such asplugin_binding_approvals. -
Plugin migrations are data migrations over plugin-owned namespaces, not host schema migrations. A plugin can migrate its own versioned state/blob entries through a migration provider, and the host records source/run status in the normal migration ledger. New plugin installs do not require changing
openclaw-state-schema.sqlunless the host itself is taking ownership of a new cross-plugin contract. -
src/state/openclaw-agent-db.tsopensagents/<agentId>/agent/openclaw-agent.sqlite, registers the database in the global DB, and owns agent-local session, transcript, VFS, artifact, cache, and memory-index tables. Shared runtime discovery now reads the generated-typedagent_databasesregistry instead of reimplementing that query at each call site. -
Global and per-agent databases record a
schema_metarow with database role, schema version, timestamps, and agent id for agent databases. The global DB remains atuser_version = 1; per-agent DBs use version2after the bounded memory-source identity migration. -
Per-agent session identity now has a canonical
sessionsroot table keyed bysession_id, together withsession_key,session_scope,account_id,primary_conversation_id, timestamps, display fields, model metadata, a harness id, and parent/spawn linkage, serve as queryable columns.session_routesrepresents the unique active route index fromsession_keyto the currentsession_id, allowing a route key to shift to a new durable session without forcing hot reads to choose between duplicatesessions.session_keyrows. The oldsession_entries.entry_jsoncompatibility-shaped payload attaches to the durablesession_idroot via a foreign key; it is no longer the sole schema-level representation of a session. -
Per-agent external conversation identity is now relational:
conversationsstores normalized provider, account, and conversation identity, andsession_conversationsconnects one OpenClaw session to one or more external conversations. This handles shared-main DM sessions where multiple peers can intentionally map to one session without misrepresenting themselves insession_key. SQLite also enforces uniqueness on the natural provider identity, so the same channel, account, kind, peer, and thread tuple cannot diverge across conversation ids. Shared-main direct peers are linked with aparticipantrole, so one OpenClaw session can represent multiple external DM peers without demoting older peers into vague related rows.sessions.primary_conversation_idstill points at the current typed delivery target. Closed routing and status columns are enforced with SQLiteCHECKconstraints rather than relying solely on TypeScript unions. Runtime session projection clears compatibility routing shadows fromsession_entries.entry_jsonbefore applying typed session and conversation columns, so stale JSON payloads cannot revive delivery targets. Subagent announce routing also requires the typed SQLite delivery context; it no longer falls back to compatibilitySessionEntryroute fields. Gatewaychat.sendexplicit delivery inheritance reads the typed SQLite delivery context instead oforiginandlast*compatibility fields.tools.effectivelikewise derives provider, account, and thread context from typed SQLite delivery and routing rows, not stalelast*session-entry shadows. System-event prompt context rebuilds channel, to, account, and thread fields from typed delivery fields instead oforiginshadows. The shareddeliveryContextFromSessionhelper and session-to-conversation mapper now ignoreSessionEntry.originentirely; only typed delivery fields and relational conversation rows can create hot route identity. Runtime session entry normalization stripsoriginbefore persisting or projectingentry_json, and inbound metadata writes typed channel and chat fields plus relational conversation rows instead of creating new origin shadows. -
Transcript events, transcript snapshots, and trajectory runtime events now reference the canonical per-agent
sessionsroot and cascade on session deletion. Transcript identity and idempotency rows continue to cascade from the exact transcript event row. -
Memory-core indexes now use explicit agent-database tables
memory_index_meta,memory_index_sources,memory_index_chunks, andmemory_embedding_cache, withmemory_index_statetracking revision changes. Optional FTS and vector side indexes are namedmemory_index_chunks_ftsandmemory_index_chunks_vecinstead of genericmeta,files,chunks,chunks_fts, orchunks_vectables. The canonical names retain the current path and source row shape and serialized embedding compatibility. These tables are derived search caches, not canonical transcript storage; they can be deleted and rebuilt from memory workspace files and configured sources. Opening a shipped generic-name memory index migrates its metadata, sources, chunks, and embedding cache into the canonical tables; derived FTS and vector tables are rebuilt under their canonical names. -
Subagent run recovery state now resides in typed shared
subagent_runsrows with indexed child, requester, and controller session keys. The oldsubagents/runs.jsonfile is only used as Doctor cleanup input. Its run entries are transient recovery state, so Doctor records the retirement receipt and discards the file without importing it. Because a file cannot prove whether its entries are live or stale after SQLite rows have been pruned, operators must let file-era active runs settle before upgrading across this boundary. -
Current conversation bindings now live in typed shared
current_conversation_bindingsrows keyed by normalized conversation id, with target agent and session columns, conversation kind, status, expiry, and metadata stored as relational columns instead of a duplicated opaque binding record. The durable binding key includes the normalized conversation kind so direct, group, and channel refs cannot collide, and SQLite rejects invalid binding kind and status values. The oldbindings/current-conversations.jsonfile is doctor migration input only. -
Delivery queue recovery now overlays typed queue columns for channel, target, account, session, retry, error, platform-send, and recovery state onto the replay JSON.
entry_jsonkeeps the replay payloads, hooks, and formatting payload, but typed columns are authoritative for hot queue routing and state. -
TUI last-session restore pointers now live in typed shared
tui_last_sessionsrows keyed by the hashed TUI connection and session scope. Runtime reads and writes only SQLite, atomically upserts each scope, and excludes heartbeat sessions.openclaw doctor --fixstrictly validates the old TUI JSON file, keeps newer SQLite rows, verifies the canonical result, and removes the unchanged legacy file instead of leaving an archive. -
Discord command deployment hashes now live in the shared plugin-state SQLite store. Runtime reads and writes exact application-scoped keys only. Doctor deletes the rebuildable legacy
discord/command-deploy-cache.jsonfile without importing it, so the next startup performs one canonical reconcile. -
Default TTS prefs now live in shared plugin-state SQLite rows keyed under the
speech-coreplugin. The oldsettings/tts.jsonfile is doctor migration input only; runtime no longer reads or writes TTS prefs JSON files, and the legacy path resolver lives in the doctor migration module. -
Secret target metadata now discusses stores instead of pretending every credential target is a config file.
openclaw.jsonremains the config store; auth-profile targets use typed SQLiteauth_profile_storesrows with provider-shaped credentials kept as JSON payloads. -
Secret audit no longer scans retired per-agent
auth.jsonfiles. Doctor owns warning about, importing, and removing that legacy file. -
Legacy auth profile path helpers now live in doctor legacy code. Core auth profile path helpers expose SQLite auth-store identity and display locations, not
auth-profiles.jsonorauth-state.jsonruntime paths. -
Subagent run recovery and OpenRouter model capability cache runtime modules now keep SQLite snapshot readers and writers separate from doctor-only legacy JSON import helpers. OpenRouter capabilities use the typed generic
model_capability_cacherows underprovider_id = "openrouter"instead of one opaque cache blob or a provider-specific host table. Subagent runtaskNameis stored in the typedsubagent_runs.task_namecolumn. Thepayload_jsoncopy exists for replay and debugging, not as the source for hot display or lookup fields. -
src/agents/filesystem/virtual-agent-fs.sqlite.tsimplements a SQLite VFS on top of the agent databasevfs_entriestable. Directory reads, recursive exports, deletes, and renames use indexed(namespace, path)prefix ranges instead of scanning an entire namespace or depending onLIKEpath matching. -
src/agents/runtime-worker.entry.tscreates per-run SQLite VFS, tool artifact, run artifact, and scoped cache stores for workers. -
Workspace bootstrap completion, attestation recency, and generated bootstrap hashes now reside in typed shared
workspace_setup_state,workspace_path_aliases,workspace_attestations, andworkspace_generated_bootstrap_hashesrows keyed by canonical workspace identity. Persisted lexical and real-path aliases keep vanished-workspace protection stable after a configured symlink disappears; repointed aliases fail closed. The runtime no longer reads or writesopenclaw-workspace-state.json,.openclaw/workspace-state.json, state-dirworkspace-attestations/*.attested, or sibling<workspace>.attestedsidecars.openclaw doctor --fixvalidates and claims legacy sources, imports them into SQLite with migration receipts, verifies the canonical rows, and only then removes the claimed files. -
The shared schema reserves an
exec_approvals_configsingleton row, but the runtime cutover remains pending. TypeScript and the macOS companion still use the state-scoped JSON file and must move to SQLite together. -
TypeScript device identity now uses typed
device_identitiesrows, with a doctor-only legacy JSON import kept outside the runtime owner. Device auth is still file-backed pending a coordinated schema and cross-runtime migration;device_auth_tokensremains reserved for that follow-up. -
GitHub Copilot token exchange cache uses the shared SQLite plugin-state table under
github-copilot/token-cache/default. It is provider-owned cache state, so it intentionally does not add a host schema table. -
GitHub Copilot compaction no longer writes
openclaw-compaction-*.jsonworkspace sidecars. The harness calls the SDK history compaction RPC for the tracked SDK session, and OpenClaw keeps durable session and transcript state in SQLite instead of compatibility marker files. -
The shared Swift runtime (
OpenClawKit) uses the samestate/openclaw.sqlite#table/device_identitiesshape and row keys for device identity. Apple container legacy files are imported by the Swift migration owner because the TypeScript Doctor cannot access those containers. Swift device auth remains file-backed for the coordinated auth follow-up. -
Android device identity and cached device auth remain app-local stores. They require a separate Android-owned migration; the host SQLite claims do not describe current Android behavior.
-
Android notification recent-package history uses typed
android_notification_recent_packagesrows. The runtime no longer migrates or reads the old SharedPreferences CSV keys. -
Device identity creation fails closed when legacy
identity/device.jsonexists, when the SQLite identity row is invalid, or when the SQLite identity store cannot be opened. Doctor imports and removes that file first, so runtime startup cannot silently rotate pairing identity before migration. -
Device identity selection is a SQLite row key, not a JSON file locator. Tests and gateway helpers pass explicit identity keys; only doctor migration and the fail-closed startup gate know the retired
identity/device.jsonfilename. -
Session reset compatibility now lives in doctor config migration:
session.idleMinutesis moved intosession.reset.idleMinutes,session.resetByType.dmis moved intosession.resetByType.direct, and the runtime reset policy only reads canonical reset keys. -
Legacy config compatibility now lives under
src/commands/doctor/. NormalreadConfigFileSnapshot()validation does not import doctor legacy detectors or annotate legacy issues;runDoctorConfigPreflight()adds those issues for doctor repair and reporting. The doctor config flow importssrc/commands/doctor/legacy-config.ts, and old OAuth profile-id repair lives undersrc/commands/doctor/legacy/oauth-profile-ids.ts. -
Non-doctor commands do not auto-run legacy config repair. For example,
openclaw update --channelnow fails on invalid legacy config and asks the user to run doctor, rather than silently importing doctor migration code. -
Web push, APNs, Voice Wake, update checks, and config health now use typed shared SQLite tables for subscriptions, VAPID keys, node registrations, trigger rows, routing rows, update-notification state, and config health entries instead of whole opaque JSON blobs. Web Push and APNs writes upsert only the affected primary-key row; config health reconciles by config path. Their runtime modules remain separate from Doctor-only legacy JSON import helpers.
-
APNs runtime reads and writes only
apns_registrations. Explicitopenclaw doctor --fixstrictly imports the retiredpush/apns-registrations.json, preserves existing canonical rows, verifies the transaction, records a receipt, and removes the secret-bearing JSON. Receipt-backed retries perform cleanup only, whileapns_registration_tombstonescover invalidations before first repair, so stale relay grants or device tokens cannot resurrect. -
Node-host config now uses a typed singleton row in the shared SQLite database. Runtime fails closed while the old
node.jsonfile or an interrupted claim remains; explicitopenclaw doctor --fixstrictly imports and removes it before normal runtime use. -
Device/node pairing, channel pairing, channel allowlists, and bootstrap state now use typed SQLite rows instead of whole opaque JSON blobs. Plugin binding approvals and cron job state follow the same split: runtime modules expose SQLite-backed operations and neutral snapshot helpers, and pairing/bootstrap plus plugin binding approval snapshot writes reconcile rows by primary key instead of truncating tables, while doctor imports and removes the old JSON files through
src/commands/doctor/legacy/*modules. -
Installed plugin records now live in the SQLite installed-plugin index. Runtime config read and write no longer migrates or preserves old
plugins.installsauthored-config data; doctor imports that legacy config shape into SQLite before normal runtime use. -
QQBot credential recovery snapshots now live in SQLite plugin state under
qqbot/credential-backups. Runtime no longer writesqqbot/data/credential-backup*.json; the QQBot doctor contract imports and archives those legacy backup files from the active state directory. -
Gateway reload planning compares SQLite installed-plugin index snapshots under an internal
installedPluginIndex.installRecords.*diff namespace. Runtime reload decisions no longer wrap those rows in fakeplugins.installsconfig objects. -
Matrix account credentials now live in SQLite plugin state. Runtime reads only that canonical store; Doctor imports, verifies, and archives retired
credentials/matrix/credentials*.jsonfiles when their account can be resolved. -
Core pairing and cron runtime modules no longer use legacy JSON path builders. The deprecated pairing-path SDK helper remains migration-only compatibility; doctor state migration owns its file reads and imports. Doctor-owned legacy modules construct
pending.json,paired.json,bootstrap.json, andcron/jobs.jsonsource paths for import tests and migration only. Legacy cron job-shape normalization and JSONL history import live undersrc/commands/doctor/cron/; legacy SQLite history finalization runs during state database open. -
src/commands/doctor/legacy/runtime-state.tsimports legacy JSON state files, including node host config, into SQLite from doctor. New legacy file importers stay undersrc/commands/doctor/legacy/. -
src/commands/doctor/state-migrations.tsimports legacysessions.jsonand*.jsonltranscripts directly into SQLite and removes successful sources. It no longer stages root legacy transcripts throughagents/<agentId>/sessions/*.jsonlor creates a canonical JSONL target before import. -
State integrity doctor checks no longer scan legacy session directories or offer orphan JSONL deletion. Legacy transcript files are migration inputs only, and the migration step owns import plus source removal.
-
Legacy sandbox registry import lives under
src/commands/doctor/legacy/sandbox-registry.ts; active sandbox registry reads and writes remain SQLite-only. -
The legacy session transcript health/import repair lives under
src/commands/doctor/legacy/session-transcript-health.ts; runtime command modules no longer carry JSONL transcript parsing or active-branch repair code.
Completed consolidation and deletion highlights:
-
Plugin state now uses the shared
state/openclaw.sqlitedatabase. The old branch-localplugin-state/state.sqlitesidecar importer is removed because that SQLite layout never shipped. Probe/test helpers report the shareddatabasePathinstead of exposing a plugin-state-specific SQLite path. -
Task and Task Flow runtime tables now live in the shared
state/openclaw.sqlitedatabase instead oftasks/runs.sqliteandtasks/flows/registry.sqlite; the old sidecar importers are removed for the same unshipped-layout reason. -
src/config/sessions/store.tsno longer needsstorePathfor inbound metadata, route updates, or updated-at reads. Command persistence, CLI session cleanup, subagent depth, auth overrides, and transcript session identity use agent/session row APIs. Writes are applied as SQLite row patches with optimistic conflict retry. -
Session target resolution now exposes per-agent database targets, not legacy
sessions.jsonpaths. Shared gateway, ACP metadata, doctor route repair, andopenclaw sessionsenumerateagent_databasesplus configured agents. -
Gateway session routing now uses
resolveGatewaySessionDatabaseTarget; the returned target carriesdatabasePathand candidate SQLite row keys instead of a legacy session-store file path. -
Channel session runtime types now expose
{agentId, sessionKey}for updated-at reads, inbound metadata, and last-route updates. The oldsaveSessionStore(storePath, store)compatibility type is gone. -
Plugin runtime, extension API, and plugin SDK session surfaces now expose SQLite-backed session row helpers instead of active-session whole-store/file compatibility helpers. Root library compatibility exports remain available only outside the plugin SDK for legacy internal and migration callers. The old
resolveLegacySessionStorePathhelper is gone; legacysessions.jsonpath construction is now local to migration and test fixtures. -
src/config/sessions/session-entries.sqlite.tsnow stores canonical session entries in the per-agent database and has row-level read/upsert/delete patch support. Runtime upsert/patch/delete no longer scans for case variants or prunes legacy alias keys; doctor owns canonicalization. The standalone JSON import helper is gone, and migration merges upsert newer rows instead of replacing the whole session table. Public read/list/load helpers project hot session metadata from typedsessionsandconversationsrows;entry_jsonis a compatibility/debug shadow and can be stale or invalid without losing typed session identity or delivery context. -
src/config/sessions/delivery-info.tsnow resolves delivery context from the typed per-agentsessions+conversations+session_conversationsrows. It no longer reconstructs runtime delivery identity fromsession_entries.entry_json; a missing typed conversation row is a doctor migration/repair problem, not a runtime fallback. -
Stored-session reset decisions now prefer typed
sessions.session_scope,sessions.chat_type, andsessions.channelmetadata.sessionKeyparsing remains only for explicit thread/topic suffixes on command targets; group vs direct reset classification no longer comes from key shape. -
Session list/status display classification now uses typed chat metadata and gateway session kind. It no longer treats
:group:or:channel:substrings insidesession_keyas durable group/direct truth. -
Silent-reply policy selection now uses explicit conversation type or surface metadata only. It no longer guesses direct/group policy from
session_keysubstrings. -
Session display model resolution now receives the agent id from the SQLite session database target instead of splitting it out of
session_key. -
Agent-to-agent announce target hydration now uses typed
sessions.listdeliveryContextonly. It no longer recovers channel/account/thread routing from legacyorigin, mirroredlast*fields, orsession_keyshape. -
sessions_sendthread-target rejection now reads typed SQLite routing metadata. It no longer rejects or accepts targets by parsing thread suffixes out of the target key. -
Group-scoped tool policy validation now reads typed SQLite conversation routing for the current or spawned session. It no longer trusts group/channel identity by decoding
sessionKey; caller-provided group ids are dropped when no typed session row vouches for them. -
Channel model override matching now uses explicit group and parent conversation metadata. It no longer decodes parent conversation ids from
parentSessionKey. -
Stored model override inheritance now requires an explicit parent session key from typed session context. It no longer derives parent overrides from
:thread:or:topic:suffixes insessionKey. -
The old session thread-info wrapper and loaded-plugin thread parser are gone; no runtime code imports
config/sessions/thread-info. -
The channel conversation helper no longer exposes full-session-key parsing bridges. Core still normalizes provider-owned raw conversation ids through
resolveSessionConversation(...), but it does not reconstruct route facts fromsessionKey. -
Completion delivery, send policy, and task maintenance no longer derive chat type from
session_keyshape. The old chat-type key parser has been deleted; these paths require typed session metadata, typed delivery context, or explicit delivery target vocabulary. -
Session list/status, diagnostics, approval account binding, TUI heartbeat filtering, and usage summaries no longer mine
SessionEntry.originfor provider/account/thread/display routing. The only remaining runtimeoriginreads are non-session concepts or current-turn delivery objects. -
Approval-request native conversation lookup now reads typed per-agent session routing rows. It no longer parses channel/group/thread conversation identity from
sessionKey; missing typed metadata is a migration/repair issue. -
Gateway session changed/chat/session event payloads no longer echo
SessionEntry.originorlast*route shadows; clients receive typedchannel,chatType, anddeliveryContext. -
Heartbeat delivery resolution can now receive the typed SQLite
deliveryContextdirectly, and heartbeat runtime passes the per-agent session delivery row instead of relying on compatibilitysession_entriesshadows for current routing. -
Cron isolated-agent delivery target resolution also hydrates its current route from the typed per-agent session delivery row before falling back to the compatibility entry payload.
-
Subagent announce origin resolution now threads the typed requester-session delivery context through
loadRequesterSessionEntryand prefers that row over compatibilitylast*/deliveryContextshadows. -
Inbound session metadata updates now merge against the typed per-agent delivery row first. Old
SessionEntrydelivery fields serve only as a fallback when no typed conversation row exists. -
Restart and update delivery extraction now lets the typed SQLite delivery
threadIdtake precedence over topic or thread fragments parsed fromsessionKey. Parsing is only a fallback for legacy thread-shaped keys. -
Hook agent context channel ids now prefer typed SQLite conversation identity, then explicit message metadata. They no longer parse provider, group, or channel fragments from
sessionKey. -
Gateway
chat.sendexternal-route inheritance now reads typed SQLite session routing metadata instead of inferring channel, direct, or group scope fromsessionKeypieces. Channel-scoped sessions inherit only when the typed session channel and chat type match the stored delivery context. Shared-main sessions keep their stricter CLI or no-client-metadata rule. -
Restart-sentinel wake and continuation routing now reads typed SQLite delivery and routing rows before queueing heartbeat wakes or routed agent-turn continuations. It no longer reconstructs delivery context from the session-entry JSON shadow.
-
Gateway
tools.effectivecontext resolution now reads typed SQLite delivery and routing rows for provider, account, target, thread, and reply-mode inputs. It no longer recovers those hot routing fields from stalesession_entries.entry_jsonorigin shadows. -
Realtime voice consult routing now resolves parent and call delivery from typed per-agent SQLite session rows. It no longer falls back to compatibility
SessionEntry.deliveryContextshadows when choosing the embedded agent message route. -
ACP spawn heartbeat relay and parent-stream routing now read parent delivery from typed SQLite session rows. They no longer reconstruct parent delivery context from compatibility session-entry shadows.
-
Session delivery route preservation now follows typed chat metadata and persisted delivery columns. It no longer extracts channel hints, direct or main markers, or thread shape from
sessionKey. Internal webchat routes only inherit an external target when SQLite already has typed and persisted delivery identity for the session. -
Generic session delivery extraction now reads only the exact typed SQLite session delivery row. It no longer parses thread or topic suffixes or falls back from a thread-shaped key to a base session key.
-
Reply dispatch, restart sentinel recovery, and realtime voice consult routing now use exact typed SQLite session and conversation rows for thread routing. They no longer recover thread ids or base-session delivery context by parsing thread-shaped session keys.
-
Embedded PI history limiting now uses the typed SQLite session routing projection (
sessionsplus primaryconversations) for provider, chat type, and peer identity. It no longer parses provider, DM, group, or thread shape out ofsessionKey. -
Cron tool delivery inference now uses explicit delivery or the current typed delivery context only. It no longer decodes channel, peer, account, or thread targets from
agentSessionKey. -
Runtime session rows no longer carry the old
lastProviderroute alias. Helpers and tests use typedlastChannelanddeliveryContextfields. Doctor migration is the only place that should translate older route aliases or persistedoriginshadows. -
Transcript events, VFS rows, and tool artifact rows now write to the per-agent database. The unshipped global transcript-file mapping table is gone. Doctor records legacy source paths in durable migration rows instead.
-
Runtime transcript lookup no longer scans JSONL byte offsets or probes legacy transcript files. Gateway chat, media, and history paths read transcript rows from SQLite. Session JSONL is now only a legacy doctor input, not a runtime state or export format.
-
Transcript parent and branch relationships use structured
parentTranscriptScope: {agentId, sessionId}metadata in SQLite transcript headers, not path-likeagent-db:...transcript_events...locator strings. -
The transcript manager contract no longer exposes implicit persisted
create(cwd)orcontinueRecent(cwd)constructors. Persisted transcript managers are opened with an explicit{agentId, sessionId}scope. Only in-memory managers remain scope-free for tests and pure transcript transforms. -
Runtime transcript store APIs resolve SQLite scope, not filesystem paths. The old
resolve...ForPathhelper and unusedtranscriptPathwrite options are gone from runtime callers. -
Runtime session resolution now uses
{agentId, sessionId}and must not derivesqlite-transcript://<agent>/<session>strings for external boundaries. Legacy absolute JSONL paths are doctor migration inputs only. -
Native hook relay direct-bridge records now live in typed shared
native_hook_relay_bridgesrows keyed by relay id. Runtime no longer writes a/tmpJSON registry or opaque generic records for those short-lived bridge records. -
runEmbeddedPiAgent(...)no longer has a transcript-locator parameter. Prepared worker descriptors also omit transcript locators. Runtime session state and queued follow-up runs carry{agentId, sessionId}instead of derived transcript handles. -
Embedded compaction now takes SQLite scope from
agentIdandsessionId. Compaction hooks, context-engine calls, CLI delegation, and protocol replies must not receive derivedsqlite-transcript://...handles. Export and debug code can materialize explicit user artifacts from rows, but it does not provide a generic session JSONL export path or feed file names back into runtime identity. -
/export-sessionreads transcript rows from SQLite and writes the requested standalone HTML view only. The embedded viewer no longer reconstructs or downloads session JSONL from those rows. -
Context-engine delegation no longer parses a transcript locator to recover agent identity. The prepared runtime context carries the resolved
agentIdinto the built-in compaction adapter. -
Transcript rewrite and live tool-result truncation now read and persist transcript state by
{agentId, sessionId}and do not derive temporary locators for transcript-update event payloads. -
The transcript-state helper surface no longer has locator-based
readTranscriptState,replaceTranscriptStateEvents, orpersistTranscriptStateMutationvariants. Runtime callers must use the{agentId, sessionId}APIs. Doctor import reads legacy files by explicit file path and writes SQLite rows. It does not migrate locator strings. -
The runtime session-manager contract no longer exposes
open(locator),forkFrom(locator), orsetTranscriptLocator(...). Persisted session managers open by{agentId, sessionId}only. List and fork helpers live on row-oriented session and checkpoint APIs instead of the transcript manager facade. -
Gateway transcript reader APIs are scope-first. They take
{agentId, sessionId}and do not accept a positional transcript locator that could accidentally become runtime identity. Active transcript locator parsing is gone. Legacy source paths are read only by doctor import code. -
Transcript update events are also scope-first.
emitSessionTranscriptUpdateno longer accepts a bare locator string, and listeners route by{agentId, sessionId}without parsing a handle. -
Gateway session-message broadcast resolves session keys from agent and session scope, not from a transcript locator. The old transcript-locator-to-session key resolver and cache is gone.
-
Gateway session-history SSE filters live updates by agent and session scope. It no longer canonicalizes transcript locator candidates, realpaths, or file-shaped transcript identities to decide whether a stream should receive an update.
-
Session lifecycle hooks no longer derive or expose transcript locators on
session_end. Hook consumers getsessionId,sessionKey, next-session ids, and agent context. Transcript files are not part of the lifecycle contract. -
Reset hooks no longer derive or expose transcript locators either. The
before_resetpayload carries recovered SQLite messages plus the reset reason, while session identity stays in hook context. -
Agent harness reset no longer accepts a transcript locator. Reset dispatch is scoped by
sessionIdandsessionKeyplus reason. -
Agent extension session types no longer expose
transcriptLocator. Extensions should use session context and runtime APIs rather than reaching for a file-shaped transcript identity. -
Plugin compaction hooks no longer expose transcript locators. Hook context already carries session identity, and transcript reads must go through SQLite scope-aware APIs instead of file-shaped handles.
-
before_agent_finalizehooks no longer exposetranscriptPath, including native hook relay payloads. Finalization hooks use session context only. -
Gateway reset responses no longer synthesize a transcript locator on the returned entry. The reset creates SQLite transcript rows, returns the clean session entry, and leaves transcript access to scope-aware readers.
-
Embedded run and compaction results no longer surface transcript locators for session accounting. Automatic compaction updates only the active
sessionId, compaction counters, and token metadata. -
Embedded attempt results no longer return
transcriptLocatorUsed, and context-enginecompact()results no longer return transcript locators. Runtime retry loops only accept a successorsessionId. -
Delivery-mirror transcript append results no longer return transcript locators. Callers get the appended
messageId; transcript update signals use SQLite scope. -
Parent-session fork helpers return only the forked
sessionId. Subagent preparation passes the child agent/session scope to engines. -
CLI runner params and history reseeding no longer accept transcript locators. CLI history reads resolve the SQLite transcript scope from
{agentId, sessionId}and session key context. -
CLI and embedded-runner test fixtures now seed and read SQLite transcript rows by session id instead of pretending active sessions are
*.jsonlfiles or passing asqlite-transcript://...string through runtime params. -
Session tool-result guard events emit from known session scope even when an in-memory manager has no derived locator. Its tests no longer fake active
/tmp/*.jsonltranscript files. -
BTW and compaction-checkpoint helpers now read and fork transcript rows by SQLite scope. Checkpoint metadata now stores session ids and leaf/entry ids only; derived locators are no longer written into checkpoint payloads.
-
Gateway transcript-key lookup uses SQLite transcript scope at protocol boundaries and no longer realpaths or stats transcript filenames.
-
Automatic compaction transcript rotation writes successor transcript rows directly through the SQLite transcript store. Session rows keep only the successor session identity, not a durable JSONL path or persisted locator.
-
Embedded context-engine compaction uses SQLite-named transcript rotation helpers. The rotation tests no longer construct JSONL successor paths or model active sessions as files.
-
Managed outgoing image retention keys its transcript-message cache from SQLite transcript stats instead of filesystem stat calls.
-
Runtime session locks and the standalone legacy
.jsonl.lockdoctor lane have been removed. -
The Microsoft Teams runtime barrel and public plugin SDK no longer re-export the old file-lock helper; durable plugin state paths are SQLite-backed.
-
Session age/count pruning and explicit session cleanup have been removed. Doctor owns legacy import; stale sessions are reset or deleted explicitly.
-
Doctor integrity checks no longer count a legacy JSONL file as a valid active transcript for a SQLite session row. Active transcript health is SQLite-only; legacy JSONL files are reported as migration/orphan-cleanup inputs.
-
Doctor no longer treats
agents/<agent>/sessions/as required runtime state. It only scans that directory when it already exists, as legacy import or orphan-cleanup input. -
Gateway
sessions.resolve, session patch/reset/compact paths, subagent spawning, fast abort, ACP metadata, heartbeat-isolated sessions, and TUI patching no longer migrate or prune legacy session keys as a side effect of normal runtime work. -
CLI command session resolution now returns the owning
agentIdinstead of astorePath, and it no longer copies legacy main-session rows during normal--toor--session-idresolution. Legacy main-row canonicalization belongs to doctor only. -
Runtime subagent depth resolution no longer reads
sessions.jsonor JSON5 session stores. It reads SQLitesession_entriesby agent id, and legacy depth/session metadata can only enter through the doctor import path. -
Auth profile session overrides persist through direct
{agentId, sessionKey}row upserts instead of lazy-loading a file-shaped session-store runtime. -
Auto-reply verbose gating and session update helpers now read/upsert SQLite session rows by session identity and no longer require a legacy store path before touching persisted row state.
-
Command-run session metadata helpers now use entry-oriented names and module paths; the old
session-storecommand helper surface has been removed. -
Bootstrap header seeding and manual compaction boundary hardening now mutate SQLite transcript rows directly. Runtime callers pass session identity, not writable
.jsonlpaths. -
Silent session-rotation replay copies recent user/assistant turns by
{agentId, sessionId}from SQLite transcript rows. It no longer accepts source or target transcript locators. -
Fresh runtime session rows no longer store transcript locators. Callers use
{agentId, sessionId}directly; export/debug commands can choose output file names when they materialize rows. -
Starting a new persisted transcript session now always opens SQLite rows by scope. The session manager no longer reuses a previous file-era transcript path or locator as the identity for the new session.
-
Persisted transcript sessions use the explicit
openTranscriptSessionManagerForSession({agentId, sessionId})API. The old staticSessionManager.create/openForSession/list/forkFromSessionfacades are gone so tests and runtime code cannot accidentally recreate file-era session discovery. -
Plugin runtime no longer exposes
api.runtime.agent.session.resolveTranscriptLocatorPath; plugin code uses SQLite row helpers and scope values. -
The public
session-store-runtimeSDK surface now only exports session row and transcript row helpers. Focused SQLite schema/path/transaction helpers live insqlite-runtime; raw open/close/reset helpers remain local-only for first-party tests. -
Legacy
.jsonltrajectory/checkpoint filename classifiers now live in the doctor legacy session-file module. Core session validation no longer imports file-artifact helpers to decide normal SQLite session ids. -
Active-memory blocking subagent runs use SQLite transcript rows instead of creating temporary or persisted
session.jsonlfiles under plugin state. The oldtranscriptDiroption is removed. -
One-off slug generation and system-agent planner runs use SQLite transcript rows instead of creating temporary
session.jsonlfiles. -
llm-taskhelper runs and hidden commitment extraction also use SQLite transcript rows, so these model-only helper sessions no longer create temporary JSON/JSONL transcript files. -
TranscriptSessionManageris only an opened SQLite transcript scope now. Runtime code opens it withopenTranscriptSessionManagerForSession({agentId, sessionId}); create, branch, continue, list, and fork flows live in their owning SQLite row helpers rather than static manager facades. Doctor/import/debug code handles explicit legacy source files outside the runtime session manager. -
The stale
SessionManager.newSession()andSessionManager.createBranchedSession()facade methods were removed. New sessions and transcript descendants are created by their owning SQLite workflow instead of mutating an already-open manager into a different persisted session. -
Parent transcript fork decisions and fork creation no longer accept
storePathorsessionsDir; they use{agentId, sessionId}SQLite transcript scope instead of retained filesystem path metadata. -
Memory-host no longer exports no-op session-directory transcript classification helpers; transcript filtering now derives from SQLite row metadata during entry construction.
-
Memory-host and QMD session-export tests use SQLite transcript scopes. Old
agents/<agentId>/sessions/*.jsonlpaths stay covered only where a test is intentionally proving doctor/import/export compatibility. -
QA-lab raw session inspection now uses
sessions.listthrough the gateway instead of readingagents/qa/sessions/sessions.json; MSteams feedback appends directly to SQLite transcripts without fabricating a JSONL path. -
Shared inbound channel turns now carry
{agentId, sessionKey}rather than a legacystorePath. LINE, WhatsApp, Slack, Discord, Telegram, Matrix, Signal, iMessage, BlueBubbles, Feishu, Google Chat, IRC, Nextcloud Talk, Zalo, Zalo Personal, QA Channel, Microsoft Teams, Mattermost, Synology Chat, Tlon, Twitch, and QQBot recording paths now read updated-at metadata and record inbound session rows through SQLite identity. -
Transcript locator persistence is removed from active session rows.
resolveSessionTranscriptTargetreturnsagentId,sessionId, and optional topic metadata; doctor is the only code that imports legacy transcript file names. -
Runtime transcript headers start at SQLite version
1. Old JSONL V1/V2/V3 shape upgrades live only in doctor import and normalize imported headers to the current SQLite transcript version before rows are stored. -
The database-first guard now bans
SessionManager.listAllandSessionManager.forkFromSession; session listing and fork/restore workflows must stay on row/scoped SQLite APIs. -
The guard also bans legacy transcript JSONL parse/active-branch repair helper names outside doctor/import code, so runtime cannot grow a second legacy transcript migration path.
-
Embedded PI runs reject incoming transcript handles. They use the SQLite
{agentId, sessionId}identity before worker launch and again before the attempt touches transcript state. A stale/tmp/*.jsonlinput cannot select a runtime write target. -
Cache trace, Anthropic payload, raw stream, and diagnostics timeline records now write to typed SQLite
diagnostic_eventsrows. Gateway stability bundles now write to typed SQLitediagnostic_stability_bundlesrows. The olddiagnostics.cacheTrace.filePath,OPENCLAW_CACHE_TRACE_FILE,OPENCLAW_ANTHROPIC_PAYLOAD_LOG_FILE, andOPENCLAW_DIAGNOSTICS_TIMELINE_PATHJSONL override paths are removed, and Normal stability capture no longer writeslogs/stability/*.jsonfiles. -
Cron persistence now reconciles SQLite
cron_jobsrows instead of deleting and reinserting the entire job table on each save. Plugin target writebacks update matching cron rows directly and keep runtime cron state inside the same state-database transaction. -
Cron runtime callers now use a stable SQLite cron store key. Legacy
cron.storepaths serve only as doctor import inputs. Production gateway, task maintenance, status, run-history, and Telegram target writeback paths useresolveCronStoreKeyand no longer path-normalize the key. Cron status now reportsstoreKeyinstead of the old file-shapedstorePathfield. -
Cron runtime load and scheduling no longer normalize legacy persisted job shapes such as
jobId,schedule.cron, numericatMs, string booleans, or missingsessionTarget. Doctor legacy import owns those repairs before rows are inserted into SQLite. -
ACP spawn no longer resolves or persists transcript JSONL file paths. Spawn and thread-bind setup persist the SQLite session row directly and keep the session id as the retained transcript identity.
-
ACP session metadata APIs now read, list, and upsert SQLite rows by
agentIdand no longer exposestorePathas part of the ACP session entry contract. -
Session usage accounting and gateway usage aggregation now resolve transcripts by
{agentId, sessionId}only. The cost and usage cache and discovered session summaries no longer synthesize or return transcript locator strings. -
Gateway chat append, abort-partial persistence,
/sessions.send, and webchat media transcript writes append directly through SQLite transcript scope. The gateway transcript injection helper no longer accepts atranscriptLocatorparameter. -
SQLite transcript discovery now lists transcript scopes and stats only:
{agentId, sessionId, updatedAt, eventCount}. The deadlistSqliteSessionTranscriptLocatorscompatibility helper and per-rowlocatorfield are gone. -
Transcript repair runtime now exposes only
repairTranscriptSessionStateIfNeeded({agentId, sessionId}). The old locator-based repair helper is deleted. Doctor and debug code reads explicit source file paths and never migrates locator strings. -
ACP replay ledger runtime now stores per-session replay rows in the shared SQLite state database instead of
acp/event-ledger.json. Doctor imports and removes the legacy file. -
Gateway transcript reader helpers now live in
src/gateway/session-transcript-readers.tsinstead of the oldsession-utils.fsmodule name. The fallback retry history check is named for SQLite transcript content instead of the old file-helper surface. -
Gateway injected-chat and compaction helpers now pass SQLite transcript scope through internal helper APIs instead of naming values as transcript paths or source files.
-
Bootstrap continuation detection now checks SQLite transcript rows through
hasCompletedBootstrapTranscriptTurn. It no longer exposes a file-shaped helper name. -
Embedded-runner tests now use SQLite transcript identity, and opening a new transcript manager always requires an explicit
sessionId. -
Memory indexing helpers now use SQLite transcript terminology end to end: host exports
listSessionTranscriptScopesForAgentandsessionTranscriptKeyForScope, targeted sync queuessessionTranscripts, public session-search hits expose opaquetranscript:<agent>:<session>paths, and the internal DB source key issession:<session>undersource_kind='sessions'instead of a fake file path. -
The generic plugin SDK persistent-dedupe helper no longer exposes file-shaped options. Callers provide SQLite scope keys and durable dedupe rows live in shared plugin state.
-
Microsoft Teams SSO tokens moved from locked JSON files to SQLite plugin state. Doctor imports
msteams-sso-tokens.json, rebuilds canonical SSO token keys from payloads, and removes the source file. Delegated OAuth tokens stay on their existing private credential-file boundary. -
Matrix sync cache state moved from
bot-storage.jsonto SQLite plugin state. Doctor imports legacy raw or wrapped sync payloads and removes the source file. Active Matrix and QA Lab Matrix adapter clients pass a SQLite sync-store root directory, not a fakesync-store.jsonorbot-storage.jsonpath. -
Matrix legacy crypto migration status moved from
legacy-crypto-migration.jsonto SQLite plugin state. Doctor imports the old status file. Matrix SDK IndexedDB snapshots moved fromcrypto-idb-snapshot.jsonto SQLite plugin blobs. Matrix recovery keys and credentials are SQLite plugin-state rows. Their old JSON files are doctor migration inputs only. -
Memory Wiki activity logs now use SQLite plugin state instead of
.openclaw-wiki/log.jsonl. The Memory Wiki migration provider imports old JSONL logs. Wiki markdown and user vault content stay file-backed as workspace content. -
Memory Wiki no longer creates
.openclaw-wiki/state.jsonor the unused.openclaw-wiki/locksdirectory. The migration provider removes those retired plugin metadata files if an older vault still has them. -
System-agent audit entries now use core SQLite plugin state instead of
audit/crestodian.jsonl. Doctor imports the legacy JSONL audit log and removes it after successful import. -
Config write and observe audit entries now use core SQLite plugin state instead of
logs/config-audit.jsonl. Doctor imports the legacy JSONL audit log and removes it after successful import. -
The macOS companion no longer writes app-local
logs/config-audit.jsonlorlogs/config-health.jsonsidecars while editingopenclaw.json. The config file remains file-backed. Recovery snapshots stay next to the config file. Durable config audit and health state belongs to the Gateway SQLite store. -
System-agent rescue pending approvals now use core SQLite plugin state instead of
crestodian/rescue-pending/*.jsonoropenclaw/rescue-pending/*.json. These short-lived security capabilities are never imported. Doctor discards both retired directories so an upgrade cannot reactivate a stale write. -
Phone Control temporary arm state now uses SQLite plugin state instead of
plugins/phone-control/armed.json. Doctor imports the legacy armed-state file into thephone-control/arm-statenamespace and removes the file. -
Doctor no longer repairs JSONL transcripts in place or creates backup JSONL files. It imports the active branch into SQLite and removes the legacy source.
-
Session-memory hook transcript lookup uses
{agentId, sessionId}scope-only SQLite reads. Its helper no longer accepts or derives transcript locators, legacy file reads, or file-rewrite options. -
Codex app-server conversation bindings now key SQLite plugin state by OpenClaw session key or explicit
{agentId, sessionId}scope. They must not preserve transcript-path fallback bindings. -
Codex app-server mirrored-history reads use the SQLite transcript scope only. They must not recover identity from transcript file paths.
-
Role-ordering and compaction reset paths no longer unlink old transcript files. Reset only rotates the SQLite session row and transcript identity.
-
Gateway reset and checkpoint responses return clean session rows plus session ids. They no longer synthesize SQLite transcript locators for clients.
-
Memory-core dreaming no longer prunes session rows by probing for missing JSONL files. Subagent cleanup goes through the session runtime API instead of filesystem existence checks. Its transcript-ingestion tests seed SQLite rows directly instead of creating
agents/<id>/sessionsfixtures or locator placeholders. -
Memory transcript indexing may expose
transcript:<agentId>:<sessionId>as a virtual search-hit path for citation and read helpers. The durable index source is relational (source_kind='sessions',source_key='session:<sessionId>',session_id=<sessionId>), so the value is not a runtime transcript locator, not a filesystem path, and must never be passed back into session runtime APIs. -
Gateway doctor memory status reads short-term recall and phase-signal counts from SQLite plugin-state rows instead of
memory/.dreams/*.json. CLI and doctor output now label that storage as a SQLite store, not a path. -
Memory-core runtime, CLI status, Gateway doctor methods, and plugin SDK facades no longer audit or archive legacy
.dreams/session-corpusfiles. Those files are migration inputs only. Doctor imports them into SQLite and deletes the source after verification. Active session-ingestion evidence rows now use the virtual SQLite pathmemory/session-ingestion/<day>.txt. Runtime never writes or derives state from.dreams/session-corpus. -
Memory-core public artifacts expose SQLite host events as the virtual JSON artifact
memory/events/memory-host-events.json. They no longer reuse the legacy.dreams/events.jsonlsource path. -
Sandbox container and browser registries now use the shared
sandbox_registry_entriesSQLite table. It has typed columns for session, image, timestamp, backend and config, and browser port. Doctor imports legacy monolithic and sharded JSON registry files, then removes sources that imported successfully. Runtime reads use the typed row columns as the authoritative source;entry_jsonis kept only for replay and debugging. -
Commitments now use a typed shared
commitmentstable instead of a whole-store JSON blob. Runtime uses indexed queries for scope, delivery window, rolling cap, status, and attempt, plus synchronous SQLite transactions.record_jsonis only a replay and debug copy. Explicit doctor repair validates the complete legacycommitments.json, keeps newer SQLite rows, verifies the result, and only then removes the unchanged source. Runtime never reads or writes the retired file. -
Web Push subscriptions and the generated VAPID identity now use typed shared
web_push_subscriptionsandweb_push_vapid_keysrows. Runtime handles registration, expiry cleanup, and first-use key generation with row-level SQLite transactions. Explicit Doctor repair validates both retired JSON stores, claims them before the SQLite write, imports them atomically, rejects conflicting VAPID identities, verifies the result, and only then removes the claims. Doctor holds the state-directory maintenance lock for the entire import so an older Gateway cannot recreate the retired files. Registration, delivery, deletion, and key resolution fail closed until Doctor resolves pending legacy sources or interrupted claims. -
Cron job definitions, schedule state, and run history no longer have runtime JSON writers or readers. Runtime uses
cron_jobsrows with typed columns for schedule, payload, delivery, failure alert, session, status, and runtime state, plus cron-ownedtask_runsdetail for diagnostics, delivery, session and run, model, and token totals.job_jsonis only a replay and debug copy.state_jsonholds nested runtime diagnostics that do not yet have hot query fields, while runtime rehydrates hot state fields from typed columns. Doctor imports legacyjobs.json,jobs-state.json, andruns/*.jsonlfiles and removes the imported sources. Plugin target writebacks update matchingcron_jobsrows instead of loading and replacing the whole cron store. -
Gateway startup ignores legacy
notify: truemarkers in the runtime projection. Doctor reads the retired rawcron.webhookonly while translating those markers into explicit SQLite delivery, then removes the config key. -
Outbound and session delivery queues now store queue status, entry kind, session key, channel, target, account id, retry count, last attempt and error, recovery state, and platform-send markers as typed columns in the shared
delivery_queue_entriestable. Runtime recovery reads those hot fields from the typed columns, and retry and recovery mutations update those columns directly without rewriting replay JSON. The full JSON payload remains only as the replay and debug blob for message bodies and other cold replay data. -
Managed outgoing image records now use typed shared
managed_outgoing_image_recordsrows. Runtime reads typed columns only. The JSON column is a replay and debug copy. Original image bytes remain named attachment artifacts in the managed media directory. -
Discord model-picker preferences, command-deploy hashes, and thread bindings now use shared SQLite plugin state. Their legacy JSON import plans live in the Discord plugin setup and doctor migration surface, not in core migration code.
-
Plugin legacy import detectors use doctor-named modules such as
doctor-legacy-state.tsordoctor-state-imports.ts. Normal channel runtime modules must not import legacy JSON detectors. -
BlueBubbles catchup cursors and inbound dedupe markers now use shared SQLite plugin state. Their legacy JSON import plans live in the BlueBubbles plugin setup and doctor migration surface, not in core migration code.
-
Telegram update offsets, sticker cache rows, sent-message cache rows, topic-name cache rows, and thread bindings now use shared SQLite plugin state. Their legacy JSON import plans live in the Telegram plugin setup and doctor migration surface, not in core migration code.
-
iMessage catchup cursors, reply short-id mappings, and sent-echo dedupe rows now use shared SQLite plugin state. The old
imessage/catchup/*.json,imessage/reply-cache.jsonl, andimessage/sent-echoes.jsonlfiles are doctor inputs only. -
Feishu message dedupe rows now ride the core claimable dedupe (
feishu.dedup.*namespaces in shared SQLite plugin state) instead offeishu/dedup/*.jsonfiles or the retired hand-rolleddedup.*store, with no legacy import because replay-protection cache rebuilds after upgrade. -
Microsoft Teams conversations, polls, pending upload buffers, and feedback learnings now use shared SQLite plugin state and blob tables. The pending upload path uses
plugin_blob_entriesso media buffers are stored as SQLite BLOBs instead of base64 JSON. The runtime helper names now use SQLite and state naming rather than*-fsfile-store naming, and the oldstorePathshim is gone from these stores. Its legacy JSON import plan lives in the Microsoft Teams plugin setup and doctor migration surface. -
Zalo hosted outbound media now uses shared SQLite
plugin_blob_entriesinstead ofopenclaw-zalo-outbound-mediaJSON and binary temp sidecars. -
Diffs viewer HTML and metadata now use shared SQLite
plugin_blob_entriesinstead ofmeta.jsonandviewer.htmltemp files. Viewer HTML is stored as a gzip blob and only the URL token hash is persisted. Rendered PNG and PDF outputs stay temp materializations because channel delivery still needs a file path. Their expiry metadata is SQLite-owned without JSON sidecars. -
Canvas managed documents now use shared SQLite
plugin_blob_entriesinstead of a defaultstate/canvas/documentsdirectory. The Canvas host serves those blobs directly. Local files are created only for explicithost.rootoperator content or temporary materialization when a downstream media reader requires a path. -
File Transfer audit decisions now use shared SQLite
plugin_state_entriesinstead of the unboundedaudit/file-transfer.jsonlruntime log. Doctor imports the legacy JSONL audit file into plugin state and removes the source after a clean import. -
ACPX process leases and gateway instance identity now use shared SQLite plugin state. Doctor imports the legacy
gateway-instance-idfile into plugin state and removes the source. -
ACPX generated wrapper scripts and the isolated Codex home are temporary materialization under the OpenClaw temp root, not durable OpenClaw state. The durable ACPX runtime records are the SQLite lease and gateway-instance rows. The old ACPX
stateDirconfig surface is removed because no runtime state is written there anymore. -
Gateway media attachments now use the shared
media_blobsSQLite table as the canonical byte store. Local paths returned to channel and sandbox compatibility surfaces are temp materializations of the database row, not the durable media store. Runtime media allowlists no longer include legacy$OPENCLAW_STATE_DIR/mediaor config-dirmediaroots. Those directories are doctor import sources only. -
Shell completion no longer writes
$OPENCLAW_STATE_DIR/completions/*cache files. Install, doctor, update, and release smoke paths use generated completion output or profile sourcing instead of durable completion cache files. -
Gateway skill-upload staging now uses shared
skill_uploadsandskill_upload_chunksrows. Chunks stay individually transactional during upload, then commit assembles one verified archive BLOB and removes the chunk rows. The installer only receives a temporary materialized archive path while an install is running. Doctor discards the retired one-hour filesystem staging tree instead of importing transient uploads. -
Subagent inline attachments no longer materialize under workspace
.openclaw/attachments/*. The spawn path prepares SQLite VFS seed entries, inline runs seed those entries into the per-agent runtime scratch namespace, and disk-backed tools overlay that SQLite scratch for attachment paths. The old subagent-run attachment-dir registry columns and cleanup hooks are gone. -
CLI image hydration no longer maintains stable
openclaw-cli-imagescache files. External CLI backends still receive file paths, but those paths are per-run temp materializations with cleanup. -
Cache-trace diagnostics, Anthropic payload diagnostics, raw model stream diagnostics, diagnostics timeline events, and Gateway stability bundles now write SQLite rows instead of
logs/*.jsonlorlogs/stability/*.jsonfiles. Runtime path override flags and env vars have been removed. Export and debug commands can materialize files explicitly from database rows. -
The macOS companion no longer has a rolling
diagnostics.jsonlwriter. App logs go to unified logging, and durable Gateway diagnostics stay SQLite-backed. -
The macOS port-guardian record list now uses typed shared SQLite
macos_port_guardian_recordsrows instead of an Application Support JSON file or opaque singleton blob. All macOS app profiles use the same host-global native database because they coordinate machine-local ports. Every ledger operation blocks while an older JSON-writing app copy is running. Migration joins the old ledger's stable file-lock protocol only to snapshot and later revalidate the source. It resolves every legacy row from live command and process-start facts without holding that lock, then rereads authoritative SQLite rows, applies the plan, verifies every receipt, and removes the source. Removal retries replan missing rows so retired stale receipts cannot resurrect. The lock stays short-lived so it cannot strand an older writer after SSH has spawned. Cutover is intentionally one-way. Steady-state runtime never reads, projects, or writes JSON, and rollback to JSON-only builds does not preserve newer SQLite receipts. -
Gateway singleton locks now use typed shared SQLite
state_leasesrows under thegateway_locksscope instead of temp-dir lock files. Fly and OAuth troubleshooting docs now point at the SQLite lease and auth refresh lock instead of stale file-lock cleanup. -
Gateway restart sentinel state now uses typed shared SQLite
gateway_restart_sentinelrows instead ofrestart-sentinel.json. Runtime reads sentinel kind, status, routing, message, continuation, and stats from typed columns. Those columns are authoritative.payload_jsonis only a replay and debug shadow. Runtime read, write, and clear paths are SQLite-only. One bounded state-migration module runs during startup and Doctor to import a validated older post-update sentinel before normal restart recovery, verify the typed row, and remove the source file. No steady-state runtime module reads, writes, or cleans up the legacy file. -
Gateway restart intent and supervisor handoff state now use typed shared SQLite
gateway_restart_intentandgateway_restart_handoffrows instead ofgateway-restart-intent.jsonandgateway-supervisor-restart-handoff.jsonsidecars. -
Gateway singleton coordination now uses typed
state_leasesrows undergateway_locksinstead of writinggateway.<hash>.lockfiles. The lease row holds the lock owner, expiry, heartbeat, and debug payload; SQLite provides the atomic acquire/release boundary. The retired file-lock directory option is gone; tests use the SQLite row identity directly. -
The old unreferenced cron usage-report helper that scanned
cron/runs/*.jsonlfiles was deleted. Cron run history reports read cron-ownedtask_runsrows. -
Main-session restart recovery now discovers candidate agents through the SQLite
agent_databasesregistry instead of scanningagents/*/sessionsdirectories. -
Gemini session-corruption recovery now deletes only the SQLite session row; it no longer needs a legacy
storePathgate or attempts to unlink a derived transcript JSONL path. -
Path override handling now treats literal
undefined/nullenvironment values as unset, preventing accidental repo-rootundefined/state/*.sqlitedatabases during tests or shell handoffs. -
Config health fingerprints now use typed shared SQLite
config_health_entriesrows instead oflogs/config-health.json, keeping the normal config file as the only non-credential configuration document. The macOS companion keeps only process-local health state and does not recreate the old JSON sidecar. -
Auth profile runtime no longer imports or writes credential JSON files. The canonical credential store is SQLite;
auth-profiles.json, per-agentauth.json, and sharedcredentials/oauth.jsonare doctor migration inputs that are removed after import. -
Auth profile save/state tests now assert typed SQLite auth tables directly and only use legacy auth-profile filenames for doctor migration inputs.
-
openclaw secrets applyscrubs the config file, env file, and SQLite auth-profile store only. It no longer carries compatibility logic that edits retired per-agentauth.json; doctor owns importing and deleting that file. -
Hermes secret migration plans and applies imported API-key profiles directly into the SQLite auth-profile store. It no longer writes or verifies
auth-profiles.jsonas an intermediate target. -
User-facing auth docs now describe
state/openclaw.sqlite#table/auth_profile_stores/<agentDir>instead of telling users to inspect or copyauth-profiles.json; legacy OAuth/auth JSON names remain documented only as doctor-import inputs. -
MCP OAuth sessions now use versioned
mcp_oauth_storesrows in sharedstate/openclaw.sqlite. SDK-owned token, client-registration, and discovery objects remain one validated JSON payload so dependency extension fields survive, while every read/modify/write commits in one short Kysely transaction. One shared SQLite lease serializes refresh, login, and logout; embedded MCP transports no longer let the MCP SDK refresh outside that lease. Doctor exclusively imports and removes retiredmcp-oauth/*.jsonstores with source receipts, and runtime has no file fallback. -
Core state-path helpers no longer expose the retired
credentials/oauth.jsonfile. The legacy filename is local to the doctor auth import path. -
Install, security, onboarding, model-auth, and SecretRef docs now describe SQLite auth-profile rows and whole-state backup/migration instead of per-agent auth-profile JSON files.
-
PI model discovery now passes canonical credentials into in-memory
pi-coding-agentauth storage. It no longer creates, scrubs, or writes per-agentauth.jsonduring discovery. -
Voice Wake trigger and routing settings now use typed shared SQLite tables instead of
settings/voicewake.json,settings/voicewake-routing.json, or opaque generic rows; doctor imports the legacy JSON files and removes them after a successful migration. -
Update-check state now uses a typed shared
update_check_staterow instead ofupdate-check.jsonor an opaque generic blob; doctor imports the legacy JSON file and removes it after a successful migration. -
Config health state now uses typed shared
config_health_entriesrows instead oflogs/config-health.jsonor an opaque generic blob; doctor imports the legacy JSON file and removes it after a successful migration. -
Plugin conversation binding approvals now use typed
plugin_binding_approvalsrows instead of opaque shared SQLite state orplugin-binding-approvals.json; the legacy file is a doctor migration input. -
Generic current-conversation bindings now store typed
current_conversation_bindingsrows instead of rewritingbindings/current-conversations.json; doctor imports the legacy JSON file and removes it after a successful migration. -
Memory Wiki imported-source sync ledgers now store one SQLite plugin-state row per vault/source key instead of rewriting
.openclaw-wiki/source-sync.json; the migration provider imports and removes the legacy JSON ledger. -
Memory Wiki ChatGPT import-run records now store one SQLite plugin-state row per vault/run id instead of writing
.openclaw-wiki/import-runs/*.json. Rollback snapshots remain explicit vault files until import-run snapshot archival is moved into blob storage. -
Memory Wiki compiled digests now store compressed SQLite plugin-blob rows instead of writing
.openclaw-wiki/cache/agent-digest.jsonand.openclaw-wiki/cache/claims.jsonl. The cache is rebuildable, so doctor deletes old cache files without importing them. -
ClawHub skill install tracking now stores one SQLite plugin-state row per workspace/skill instead of writing or reading
.clawhub/lock.jsonand.clawhub/origin.jsonsidecars at runtime. Runtime code uses tracked-install state objects rather than file-shaped lockfile/origin abstractions. Doctor imports the legacy sidecars from configured agent workspaces and removes them after a clean import. -
The installed plugin index now reads and writes the typed shared SQLite
installed_plugin_indexsingleton row instead ofplugins/installs.json; the legacy JSON file is only a doctor migration input and is removed after import. -
The legacy
plugins/installs.jsonpath helper now lives in doctor legacy code. Runtime plugin-index modules expose only SQLite-backed persistence options, not a JSON file path. -
Gateway restart sentinel, restart intent, and supervisor handoff state now use typed shared SQLite rows (
gateway_restart_sentinel,gateway_restart_intent, andgateway_restart_handoff) instead of generic opaque blobs. Runtime restart code has no file-shaped sentinel/intent/handoff contract. -
Matrix sync cache, storage metadata, thread bindings, inbound dedupe markers, startup verification cooldown state, SDK IndexedDB crypto snapshots, credentials, and recovery keys now use shared SQLite plugin state/blob tables. Runtime path structs no longer expose a
storage-meta.jsonmetadata path; that filename is a legacy migration input only. Their legacy JSON import plan lives in the Matrix plugin setup/doctor migration surface. Inbound dedupe markers ride the core claimable dedupe (matrix.inbound-dedupe.*namespaces in the shared state DB); the Matrix doctor state migration imports the retired per-rootinbound-deduperows andinbound-dedupe.jsononce, then the runtime reads only the claimable-dedupe store. -
Matrix startup no longer scans, reports, or completes legacy Matrix file state. Matrix file detection, legacy crypto snapshot creation, room-key restore migration state, import, and source removal are all doctor-owned.
-
Matrix runtime migration barrels were removed. Legacy state/crypto detection and mutation helpers are imported by Matrix doctor directly instead of being part of runtime API surface.
-
Matrix migration snapshot reuse markers now live in SQLite plugin state instead of
matrix/migration-snapshot.json; doctor can still reuse the same verified pre-migration archive without writing a sidecar state file. -
Nostr bus cursors and profile publish state now use shared SQLite plugin state. Their legacy JSON import plan lives in the Nostr plugin setup/doctor migration surface.
-
Active Memory session toggles now use shared SQLite plugin state instead of
session-toggles.json; toggling memory back on deletes the row instead of rewriting a JSON object. -
Skill Workshop proposals and review counters now use shared SQLite plugin state instead of per-workspace
skill-workshop/<workspace>.jsonstores. Each The proposal is a separate row underskill-workshop/proposals, and the review counter is a separate row underskill-workshop/reviews. -
Skill Workshop reviewer subagent runs now use the runtime session transcript resolver instead of creating
skill-workshop/<sessionId>.jsonsidecar session paths. -
ACPX process leases now use shared SQLite plugin state under
acpx/process-leasesinstead of a whole-fileprocess-leases.jsonregistry. Each lease is stored as its own row, preserving startup stale-process reaping without a runtime JSON rewrite path. -
ACPX wrapper scripts and the isolated Codex home are generated in the OpenClaw temp root. They are recreated as needed and are not backup or migration inputs.
-
Subagent run registry persistence uses typed shared
subagent_runsrows. The oldsubagents/runs.jsonpath is now only a Doctor cleanup input. Doctor claims it under the state maintenance lock, records the discard decision in SQLite, and removes it without importing transient run state. No runtime JSON reader, writer, cache, or fallback remains; cross-version recovery of file-only in-flight runs is intentionally unsupported at this retirement boundary. Runtime tests no longer create invalid or emptyruns.jsonfixtures to prove registry behavior; they seed/read SQLite rows directly. -
Backup stages the state directory before archiving, copies non-database files, snapshots databases with online backup plus offline
VACUUM, omits live WAL/SHM sidecars, records snapshot metadata in the archive manifest, and records completed backup runs in SQLite with the archive manifest.openclaw backup createvalidates the written archive by default;--no-verifyis the explicit fast path. -
openclaw backup restorevalidates the archive before extraction, reuses the verifier's normalized manifest, and restores verified manifest assets to their recorded source paths. It requires--yesfor writes and supports--dry-runfor a restore plan. -
The old backup volatile-path filter is deleted. Backup no longer needs a live-tar skip list for legacy session or cron JSON/JSONL files because SQLite snapshots are staged before archive creation.
-
Plain setup and onboarding workspace preparation no longer create
agents/<agentId>/sessions/directories. They create config/workspace only; SQLite session rows and transcript rows are created on demand in the per-agent database. -
Security permission repair now targets the global and per-agent SQLite databases plus WAL/SHM sidecars instead of
sessions.jsonand transcript JSONL files. -
Sandbox registry runtime names now describe SQLite registry kinds directly instead of carrying legacy JSON registry terminology through the active store.
-
openclaw reset --scope config+creds+sessionsremoves per-agentopenclaw-agent.sqlitedatabases plus WAL/SHM sidecars, not only legacysessions/directories. -
Gateway aggregate session helpers now use entry-oriented names:
loadCombinedSessionEntriesForGatewayreturns{ databasePath, entries }. The old combined-store naming has been removed from runtime callers. -
Docker MCP channel seeding now writes the main session row and transcript events into the per-agent SQLite database instead of creating
sessions.jsonand a JSONL transcript. -
The bundled session-memory hook now resolves previous-session context from SQLite by
{agentId, sessionId}. It no longer scans, stores, or synthesizes transcript paths orworkspace/sessionsdirectories. -
The bundled command-logger hook now writes command audit rows to the shared SQLite
command_log_entriestable instead of appendinglogs/commands.log. -
Channel pairing allowlists now expose only SQLite-backed read/write helpers at runtime. The deprecated plugin SDK path resolver remains for migration compatibility; file readers live only in doctor state migration code.
-
migration_runsrecords legacy-state migration executions with status, timestamps, and JSON reports. -
migration_sourcesrecords each imported legacy file source with hash, size, record count, target table, run id, status, and source-removal state. -
backup_runsrecords backup archive paths, status, and JSON manifests. -
The global schema does not keep an unused
agentsregistry table. Agent database discovery is the canonicalagent_databasesregistry until runtime has a real agent-record owner. -
Generated model catalog config is stored in typed global SQLite
agent_model_catalogsrows keyed by agent directory. Runtime callers useensureOpenClawModelCatalog; there is nomodels.jsoncompatibility API in runtime code. The implementation writes SQLite and the embedded PI registry is hydrated from that stored payload without creating amodels.jsonfile. -
Optional
memory.qmd.sessionsexport reads canonical transcript rows from the per-agent database and materializes sanitized Markdown under the QMD home as an explicit QMD input artifact. QMD session collections and artifact identity mappings therefore remain part of the configured external-tool bridge; they are not a second canonical transcript store. -
QMD's own
index.sqlite, YAML collection config, and model downloads remain external-tool artifacts under~/.openclaw/agents/<agentId>/qmd; they are not mirrored intoplugin_blob_entries. OpenClaw-owned QMD coordination is database-first: sharedstate_leasesserialize embeds globally and per-agentstate_leasesserialize collection/update/embed writers. Runtime creates no QMD lock sidecars. -
The optional
memory-lancedbplugin no longer creates~/.openclaw/memory/lancedbas an implicit OpenClaw-managed store. It is an external LanceDB backend and stays disabled until the operator configures an explicitdbPath. -
check:database-first-legacy-storesfails new runtime source that pairs legacy store names with write-style filesystem APIs. It also fails runtime source that reintroduces the retired transcript bridge markerstranscriptLocatororsqlite-transcript://.... Migration, doctor, import, and explicit non-session export code remain allowed. Broader legacy contract names such assessionFile,storePath, and oldSessionManagerfile-era facades still have current owners and need separate migration guard work before they can become a required preflight check. The guard now also covers runtimecache/*.jsonstores, genericthread-bindings.jsonsidecars, cron state/run-log JSON, config health JSON, restart and lock sidecars, Voice Wake settings, plugin binding approvals, installed plugin index JSON, File Transfer audit JSONL, Memory Wiki activity logs, the old bundledcommand-loggertext log, and pi-mono raw-stream JSONL diagnostics knobs. It also bans old root-level doctor legacy module names so compatibility code stays undersrc/commands/doctor/. Android debug handlers also use logcat/in-memory output instead of stagingcamera_debug.logordebug_logs.txtcache files.
Target Schema Shape
Keep schemas explicit. Host-owned runtime state uses typed tables. Plugin-owned opaque state uses plugin_state_entries / plugin_blob_entries; there is no generic host kv table.
Global database:
state_leases(scope, lease_key, owner, expires_at, heartbeat_at, payload_json, created_at, updated_at)
exec_approvals_config(config_key, raw_json, socket_path, has_socket_token, default_security, default_ask, default_ask_fallback, auto_allow_skills, agent_count, allowlist_count, updated_at_ms)
schema_meta(meta_key, role, schema_version, agent_id, app_version, created_at, updated_at)
agent_databases(agent_id, path, schema_version, last_seen_at, size_bytes)
task_runs(...)
task_delivery_state(...)
flow_runs(...)
subagent_runs(run_id, child_session_key, requester_session_key, controller_session_key, created_at, ended_at, cleanup_handled, payload_json)
current_conversation_bindings(binding_key, binding_id, target_agent_id, target_session_id, target_session_key, channel, account_id, conversation_kind, parent_conversation_id, conversation_id, target_kind, status, bound_at, expires_at, metadata_json, updated_at)
plugin_binding_approvals(plugin_root, channel, account_id, plugin_id, plugin_name, approved_at)
tui_last_sessions(scope_key, session_key, updated_at)
plugin_state_entries(plugin_id, namespace, entry_key, value_json, created_at, expires_at)
plugin_blob_entries(plugin_id, namespace, entry_key, metadata_json, blob, created_at, expires_at)
media_blobs(subdir, id, content_type, size_bytes, blob, created_at, updated_at)
skill_uploads(upload_id, kind, slug, force, size_bytes, sha256, actual_sha256, received_bytes, archive_blob, created_at, expires_at, committed, committed_at, idempotency_key_hash)
skill_upload_chunks(upload_id, byte_offset, size_bytes, chunk_blob)
web_push_subscriptions(endpoint_hash, subscription_id, endpoint, p256dh, auth, created_at_ms, updated_at_ms)
web_push_vapid_keys(key_id, public_key, private_key, subject, updated_at_ms)
apns_registrations(node_id, transport, token, relay_handle, send_grant, installation_id, relay_origin, topic, environment, distribution, token_debug_suffix, updated_at_ms)
apns_registration_tombstones(node_id, deleted_at_ms)
node_host_config(config_key, version, node_id, token, display_name, gateway_host, gateway_port, gateway_tls, gateway_tls_fingerprint, gateway_context_path, updated_at_ms)
device_identities(identity_key, device_id, public_key_pem, private_key_pem, created_at_ms, updated_at_ms)
device_auth_tokens(device_id, role, token, scopes_json, updated_at_ms)
macos_port_guardian_records(pid, port, command, mode, timestamp)
workspace_setup_state(workspace_key, workspace_path, version, bootstrap_seeded_at, setup_completed_at, updated_at)
workspace_path_aliases(alias_key, alias_path, workspace_key, workspace_path, updated_at_ms)
workspace_attestations(workspace_key, attested_at_ms, updated_at_ms)
workspace_generated_bootstrap_hashes(workspace_key, filename, sha256)
native_hook_relay_bridges(relay_id, pid, hostname, port, token, expires_at_ms, updated_at_ms)
model_capability_cache(provider_id, model_id, name, input_text, input_image, reasoning, supports_tools, context_window, max_tokens, cost_input, cost_output, cost_cache_read, cost_cache_write, updated_at_ms)
agent_model_catalogs(catalog_key, agent_dir, raw_json, updated_at)
managed_outgoing_image_records(attachment_id, session_key, agent_id, message_id, created_at, updated_at, retention_class, alt, original_media_id, original_media_subdir, original_content_type, original_width, original_height, original_size_bytes, original_filename, record_json, cleanup_pending)
gateway_restart_sentinel(sentinel_key, version, kind, status, ts, session_key, thread_id, delivery_channel, delivery_to, delivery_account_id, message, continuation_json, doctor_hint, stats_json, payload_json, updated_at_ms)
channel_pairing_requests(channel_key, account_id, request_id, code, created_at, last_seen_at, meta_json)
channel_pairing_allow_entries(channel_key, account_id, entry, sort_order, updated_at)
voicewake_triggers(config_key, position, trigger, updated_at_ms)
voicewake_routing_config(config_key, version, default_target_mode, default_target_agent_id, default_target_session_key, updated_at_ms)
voicewake_routing_routes(config_key, position, trigger, target_mode, target_agent_id, target_session_key, updated_at_ms)
update_check_state(state_key, last_checked_at, last_notified_version, last_notified_tag, last_available_version, last_available_tag, auto_install_id, auto_first_seen_version, auto_first_seen_tag, auto_first_seen_at, auto_last_attempt_version, auto_last_attempt_at, auto_last_success_version, auto_last_success_at, updated_at_ms)
config_health_entries(config_path, last_known_good_json, last_promoted_good_json, last_observed_suspicious_signature, updated_at_ms)
sandbox_registry_entries(registry_kind, container_name, session_key, backend_id, runtime_label, image, created_at_ms, last_used_at_ms, config_label_kind, config_hash, cdp_port, no_vnc_port, entry_json, updated_at)
cron_jobs(store_key, job_id, name, description, enabled, delete_after_run, created_at_ms, agent_id, session_key, schedule_kind, schedule_expr, schedule_tz, every_ms, anchor_ms, at, stagger_ms, session_target, wake_mode, payload_kind, payload_message, payload_model, payload_fallbacks_json, payload_thinking, payload_timeout_seconds, payload_allow_unsafe_external_content, payload_external_content_source_json, payload_light_context, payload_tools_allow_json, delivery_mode, delivery_channel, delivery_to, delivery_thread_id, delivery_account_id, delivery_best_effort, failure_delivery_mode, failure_delivery_channel, failure_delivery_to, failure_delivery_account_id, failure_alert_disabled, failure_alert_after, failure_alert_channel, failure_alert_to, failure_alert_cooldown_ms, failure_alert_include_skipped, failure_alert_mode, failure_alert_account_id, next_run_at_ms, running_at_ms, last_run_at_ms, last_run_status, last_error, last_duration_ms, consecutive_errors, consecutive_skipped, schedule_error_count, last_delivery_status, last_delivery_error, last_delivered, last_failure_alert_at_ms, job_json, state_json, runtime_updated_at_ms, schedule_identity, sort_order, updated_at)
delivery_queue_entries(queue_name, id, status, entry_kind, session_key, channel, target, account_id, retry_count, last_attempt_at, last_error, recovery_state, platform_send_started_at, entry_json, enqueued_at, updated_at, failed_at)
commitments(id, agent_id, session_key, channel, account_id, recipient_id, thread_id, sender_id, kind, sensitivity, source, status, reason, suggested_text, dedupe_key, confidence, due_earliest_ms, due_latest_ms, due_timezone, source_message_id, source_run_id, created_at_ms, updated_at_ms, attempts, last_attempt_at_ms, sent_at_ms, dismissed_at_ms, snoozed_until_ms, expired_at_ms, record_json)
migration_runs(id, started_at, finished_at, status, report_json)
migration_sources(source_key, migration_kind, source_path, target_table, source_sha256, source_size_bytes, source_record_count, last_run_id, status, imported_at, removed_source, report_json)
backup_runs(id, created_at, archive_path, status, manifest_json)
Agent database:
schema_meta(meta_key, role, schema_version, agent_id, app_version, created_at, updated_at)
sessions(session_id, session_key, session_scope, created_at, updated_at, started_at, ended_at, status, chat_type, channel, account_id, primary_conversation_id, model_provider, model, agent_harness_id, parent_session_key, spawned_by, display_name)
conversations(conversation_id, channel, account_id, kind, peer_id, parent_conversation_id, thread_id, native_channel_id, native_direct_user_id, label, metadata_json, created_at, updated_at)
session_conversations(session_id, conversation_id, role, first_seen_at, last_seen_at)
session_routes(session_key, session_id, updated_at)
session_entries(session_id, session_key, entry_json, updated_at)
transcript_events(session_id, seq, event_json, created_at)
transcript_event_identities(session_id, event_id, seq, event_type, has_parent, parent_id, message_idempotency_key, created_at)
transcript_snapshots(session_id, snapshot_id, reason, event_count, created_at, metadata_json)
vfs_entries(namespace, path, kind, content_blob, metadata_json, updated_at)
tool_artifacts(run_id, artifact_id, kind, metadata_json, blob, created_at)
run_artifacts(run_id, path, kind, metadata_json, blob, created_at)
trajectory_runtime_events(session_id, run_id, seq, event_json, created_at)
memory_index_meta(key, value)
memory_index_sources(id, path, source, hash, mtime, size)
memory_index_chunks(id, path, source, start_line, end_line, hash, model, text, embedding, updated_at)
memory_embedding_cache(provider, model, provider_key, hash, embedding, dims, updated_at)
memory_index_state(id, revision)
cache_entries(scope, key, value_json, blob, expires_at, updated_at)
memory_index_sources.id serves as the stable integer primary key, while (path, source) stays unique.
Future search capabilities can introduce FTS tables without altering the canonical event tables:
transcript_events_fts(session_id, seq, text)
vfs_entries_fts(namespace, path, text)
Large values should go into blob columns instead of JSON string encoding. Reserve
value_json for small structured data that needs to stay inspectable with standard
SQLite tooling.
agent_databases acts as the canonical registry for this branch. Do not add an
agents table until a real agent-record owner exists; agent config remains in
openclaw.json.
Doctor Migration Shape
Doctor must invoke a single explicit migration step that is reportable and safe to rerun:
openclaw doctor --fix
openclaw doctor --fix triggers the state migration implementation after
ordinary config preflight and creates a verified backup before import. Runtime
startup and openclaw migrate must not import legacy OpenClaw state files.
Migration properties:
- One migration pass discovers all legacy file sources and produces a plan before any mutations occur.
- Doctor creates a verified pre-migration backup archive before importing legacy files.
- Imports are idempotent and keyed by source path, mtime, size, hash, and target table.
- Successful source files are removed or archived after the target database has committed.
- Failed imports leave the source untouched and record a warning in
migration_runs. - Runtime code reads SQLite only after the migration exists.
- No downgrade or export-to-runtime-files path is required.
Migration Inventory
Move these into the global database:
- Task registry runtime writes now go through the shared database. The unshipped
tasks/runs.sqlitesidecar importer has been removed. Snapshot saves perform upserts by task id and delete only rows for missing tasks or deliveries. - Task Flow runtime writes now go through the shared database. The unshipped
tasks/flows/registry.sqlitesidecar importer has been removed. Snapshot saves perform upserts by flow id and delete only rows for missing flows. - Plugin state runtime writes now go through the shared database. The unshipped
plugin-state/state.sqlitesidecar importer has been removed. - Builtin memory search no longer defaults to
memory/<agentId>.sqlite. Its index tables reside in the owning agent database, and the explicitmemorySearch.store.pathsidecar opt-in has been retired to doctor config migration. - Builtin memory reindex resets only memory-owned tables in the agent database. It must not replace the entire SQLite file, because the same database holds sessions, transcripts, VFS rows, artifacts, and runtime caches.
- Sandbox container and browser registries come from monolithic and sharded JSON. Runtime writes now use the shared database. Legacy JSON import remains available.
- Cron job definitions, schedule state, and run history now use shared SQLite. The doctor imports and removes legacy
jobs.json,jobs-state.json, andcron/runs/*.jsonlfiles. - Device identity and authentication, push, update check, commitments, OpenRouter model cache, installed plugin index, and app-server bindings.
- Device and node pairing and bootstrap records now use typed SQLite tables.
- Device-pair notification subscribers and delivered-request markers now use the shared SQLite plugin-state table instead of
device-pair-notify.json. - Voice-call call records now use the shared SQLite plugin-state table under the
voice-callandcallsnamespace instead ofcalls.jsonl. The plugin CLI tails and summarizes SQLite-backed call history. - QQBot gateway sessions, known-user records, and ref-index quote cache now use SQLite plugin state under
qqbotnamespaces (gateway-sessions,known-users,ref-index) instead ofsession-*.json,known-users.json, andref-index.jsonl. Those legacy files are caches and are not migrated. - Discord model-picker preferences, command-deploy hashes, and thread bindings now use SQLite plugin state under
discordnamespaces (model-picker-preferences,command-deploy-hashes,thread-bindings) instead ofmodel-picker-preferences.json,command-deploy-cache.json, andthread-bindings.json. The Discord doctor and setup migration imports and removes the legacy files. - BlueBubbles catchup cursors and inbound dedupe markers now use SQLite plugin state under
bluebubblesnamespaces (catchup-cursors,inbound-dedupe) instead ofbluebubbles/catchup/*.jsonandbluebubbles/inbound-dedupe/*.json. The BlueBubbles doctor and setup migration imports and removes the legacy files. - Telegram update offsets, sticker cache entries, reply-chain message cache entries, sent-message cache entries, topic-name cache entries, and thread bindings now use SQLite plugin state under
telegramnamespaces (update-offsets,sticker-cache,message-cache,sent-messages,topic-names,thread-bindings) instead ofupdate-offset-*.json,sticker-cache.json,*.telegram-messages.json,*.telegram-sent-messages.json,*.telegram-topic-names.json, andthread-bindings-*.json. The Telegram doctor and setup migration imports and removes the legacy files. - iMessage catchup cursors, reply short-id mappings, and sent-echo dedupe rows now use SQLite plugin state under
imessagenamespaces (catchup-cursors,reply-cache,sent-echoes) instead ofimessage/catchup/*.json,imessage/reply-cache.jsonl, andimessage/sent-echoes.jsonl. The iMessage doctor and setup migration imports and removes the legacy files. - Microsoft Teams conversations, polls, SSO tokens, and feedback learnings now use SQLite plugin state namespaces (
conversations,polls,sso-tokens,feedback-learnings) instead ofmsteams-conversations.json,msteams-polls.json,msteams-sso-tokens.json, and*.learnings.json. The Microsoft Teams doctor and setup migration imports and archives the legacy files. Pending uploads are a short-lived SQLite cache and old JSON cache files are not migrated. - Matrix sync cache, storage metadata, thread bindings, inbound dedupe markers, startup verification cooldown state, credentials, recovery keys, and SDK IndexedDB crypto snapshots now use SQLite plugin state and blob namespaces under
matrix(sync-store,storage-meta,thread-bindings,matrix.inbound-dedupe.*through the core claimable dedupe,startup-verification,credentials,recovery-key,idb-snapshots) rather thanbot-storage.json,storage-meta.json,thread-bindings.json,inbound-dedupe.json,startup-verification.json,credentials.json,recovery-key.json, andcrypto-idb-snapshot.json; the Matrix doctor/setup migration imports and removes those legacy files (along with the retired per-rootinbound-dedupeSQLite rows) from account-scoped Matrix storage roots. - Nostr bus cursors and profile publish state now reside in SQLite plugin state under
nostrnamespaces (bus-state,profile-state) instead ofbus-state-*.jsonandprofile-state-*.json; the Nostr doctor/setup migration imports and removes the legacy files. - Active Memory session toggles now live in SQLite plugin state under
active-memory/session-togglesinstead ofsession-toggles.json. - Skill Workshop proposal queues and review counters now sit in SQLite plugin state
under
skill-workshop/proposalsandskill-workshop/reviewsinstead of per-workspaceskill-workshop/<workspace>.jsonfiles. - Outbound delivery and session delivery queues now share the global SQLite
delivery_queue_entriestable under separate queue names (outbound-delivery,session-delivery) instead of durabledelivery-queue/*.json,delivery-queue/failed/*.json, andsession-delivery-queue/*.jsonfiles. The doctor legacy-state step imports pending and failed rows, removes stale delivered markers, and deletes the old JSON files after import. Hot routing and retry fields are typed columns; the JSON payload is retained only for replay/debug. - ACPX process leases now use SQLite plugin state under
acpx/process-leasesinstead ofprocess-leases.json. - Backup and migration run metadata
Move these into agent databases:
- Agent session roots and compatibility-shaped session-entry payloads. Done for
runtime writes: hot session metadata is queryable in
sessions, while the legacy-shaped fullSessionEntrypayload remains insession_entries. - Agent transcript events. Done for runtime writes.
- Compaction checkpoints and transcript snapshots. Done for runtime writes:
checkpoint transcript copies are SQLite transcript rows and checkpoint
metadata is recorded in
transcript_snapshots. Gateway checkpoint helpers now name these values as transcript snapshots rather than source files. - Agent VFS scratch/workspace namespaces. Done for runtime VFS writes.
- Subagent attachment payloads. Done for runtime writes: they are SQLite VFS seed entries and never durable workspace files.
- Tool artifacts. Done for runtime writes.
- Run artifacts. Done for worker runtime writes through the per-agent
run_artifactstable. - Agent-local runtime caches. Done for worker runtime scoped cache writes through
the per-agent
cache_entriestable. Gateway-wide model caches stay in the global database unless they become agent-specific. - ACP parent stream logs. Done for runtime writes.
- ACP replay ledger sessions. Done for runtime writes via
acp_replay_sessionsandacp_replay_events; legacyacp/event-ledger.jsonremains only as doctor input. - ACP session metadata. Done for runtime writes via
acp_sessions; legacyentry.acpblocks insessions.jsonare doctor migration input only. - Trajectory sidecars when they are not explicit export files. Done for runtime
writes: trajectory capture writes agent-database
trajectory_runtime_eventsrows and mirrors run-scoped artifacts into SQLite. Legacy sidecars are doctor import inputs only; export can materialize fresh JSONL support-bundle outputs but does not read or migrate old trajectory/transcript sidecars at runtime. Runtime trajectory capture exposes SQLite scope; JSONL path helpers are isolated to export/debug support and are not re-exported from the runtime module. Embedded-runner trajectory metadata records{agentId, sessionId, sessionKey}identity instead of persisting a transcript locator.
Keep these file-backed for now:
openclaw.json- provider or CLI credential files
- plugin/package manifests
- user workspaces and Git repositories when disk mode is selected
- logs intended for operator tailing, unless a specific log surface is moved
Migration Plan
Phase 0: Freeze The Boundary
Make the durable-state boundary explicit before moving more rows:
- Add a
migration_runstable to the global database. Done for legacy-state migration execution reports. - Add a single doctor-owned state migration service for file-to-database import.
Done:
openclaw doctor --fixuses the legacy-state migration implementation. - Make
planread-only and makeapplycreate a backup, import, verify, and then delete or quarantine old files. Done: doctor creates a verified pre-migration backup, passes the backup path intomigration_runs, and reuses the importer/removal paths. - Add static bans so new runtime code cannot write legacy state files while migration code and tests can still seed/read them. Done for the currently migrated legacy stores; the guard also scans nested tests for forbidden runtime transcript locator contracts.
Phase 1: Finish The Global Control Plane
Keep shared coordination state in state/openclaw.sqlite:
- Agents and agent database registry
- Task and Task Flow ledgers
- Plugin state
- Sandbox container/browser registry
- Cron/scheduler run history
- Pairing, device, push, update-check, TUI, OpenRouter/model caches, and other small gateway-scoped runtime state
- Backup and migration metadata
- Gateway media attachment bytes. Runtime writes are done; direct file paths are temporary materializations for compatibility with channel senders and sandbox staging. Runtime allowlists accept SQLite materialization paths, not legacy state/config media roots. Doctor imports legacy media files into
media_blobsand removes the source files after successful row writes. - Debug proxy capture sessions, events, and payload blobs. Captures live in the shared state DB and open through the shared state DB bootstrap, schema, WAL, and busy-timeout settings. Payload bytes are gzip-compressed in
capture_blobs.data; there is no debug proxy runtime sidecar DB override, blob directory, or proxy-capture-only generated schema/codegen target. Doctor/startup migration imports shippeddebug-proxy/capture.sqliterows and referenced payload blobs, including active legacy DB/blob environment overrides, then archives those sources while leaving CA certificates intact.
This phase also deletes duplicate sidecar openers, permission helpers, WAL setup, filesystem pruning, and compatibility writers from those subsystems.
Phase 2: Introduce Per-Agent Databases
Create one database per agent and register it from the global DB:
~/.openclaw/state/openclaw.sqlite
~/.openclaw/agents/<agentId>/agent/openclaw-agent.sqlite
The global agent_databases row stores the path, schema version, last-seen timestamp, and basic size/integrity metadata. Runtime code asks the registry for the agent DB instead of deriving file paths directly.
The agent DB owns:
sessionsas the canonical session root, withsession_entriesas the compatibility-shaped payload table attached to that root, andsession_routesas the unique activesession_keylookupconversationsandsession_conversationsas the normalized provider routing identity attached to sessionstranscript_events- transcript snapshots and compaction checkpoints. Runtime writes are done.
vfs_entriestool_artifactsand run artifacts- agent-local runtime/cache rows. Worker scoped caches are done.
- ACP parent stream events
- trajectory runtime events when they are not explicit export artifacts
Phase 3: Replace Session Store APIs
Runtime writes are done. The file-shaped session store surface is not an active runtime contract:
- Runtime no longer calls
loadSessionStore(storePath)or treatsstorePathas session identity. - Runtime row operations are
getSessionEntry,upsertSessionEntry,patchSessionEntry,deleteSessionEntry, andlistSessionEntries. - Whole-store rewrite helpers, file writers, queue tests, alias pruning, and legacy-key deletion parameters are gone from runtime.
- Deprecated root-package compatibility exports delegate to the doctor-only
sessions.jsonimporter through 2026-10-12; Plugin SDK compatibility reads continue to project canonical SQLite rows. sessions.jsonparsing remains only in doctor migration/import code and doctor tests.- Runtime lifecycle fallback reads SQLite transcript headers, not JSONL first lines.
Keep deleting anything that reintroduces file-lock parameters, pruning/truncation-as-file-maintenance vocabulary, store-path identity, or tests whose only assertion is JSON persistence.
Phase 4: Move Transcripts, ACP Streams, Trajectories, And VFS
Make every agent data stream database-native:
- Transcript append writes go through one SQLite transaction that ensures the session header, checks message idempotency, selects the parent tail, inserts into
transcript_events, and records queryable identity metadata intranscript_event_identities. Direct transcript message appends and normal persistedTranscriptSessionManagerappends are done; explicit branch operations keep their explicit parent choice and still write SQLite rows without deriving any file locator. - ACP parent stream logs become rows, not
.acp-stream.jsonlfiles. Done. - ACP spawn setup no longer persists transcript JSONL paths. Done.
- Runtime trajectory capture writes event rows/artifacts directly. The explicit support/export command can still produce support-bundle JSONL artifacts as an export format, but session export does not recreate session JSONL. Done.
- Disk workspaces stay on disk when configured as disk mode.
- VFS scratch and experimental VFS-only workspace mode use the agent DB.
The migration imports old JSONL files once, records counts/hashes in migration_runs, and removes imported files after integrity checks.
Phase 5: Backup, Restore, Vacuum, And Verify
Backups remain one archive file:
- Checkpoint every global and agent database.
- Snapshot each DB with SQLite online backup followed by offline
VACUUM. - Archive compact DB snapshots, config, external credentials, and requested workspace exports.
- Omit raw live
*.sqlite-waland*.sqlite-shmfiles. - Verify by opening every DB snapshot and running
PRAGMA integrity_check.openclaw backup createdoes this archive verification by default;--no-verifyskips only the post-write archive pass, not the snapshot creation integrity check. - Restore copies snapshots back to their target paths. Restored global DBs use version
1; restored per-agent DBs use version2, with version1snapshots upgraded atomically when opened.
Phase 6: Worker Runtime
Keep worker mode experimental while the database split lands:
- Workers receive agent id, run id, filesystem mode, and DB registry identity.
- Each worker opens its own SQLite connection.
- Parent keeps channel delivery, approvals, config, and cancellation authority.
- Start with one worker per active run; add pooling only after lifecycle and DB connection ownership are stable.
Phase 7: Delete The Old World
Runtime session management is done. The old world is allowed only as explicit doctor input or support/export output:
- No runtime
sessions.json, transcript JSONL, sandbox registry JSON, task sidecar SQLite, or plugin-state sidecar SQLite writes. - No JSON/session file pruning, file transcript truncation, session file locks, or lock-shaped session tests.
- No runtime compatibility exports whose purpose is keeping old session files current.
- Explicit support exports remain user-requested archive/materialization formats and must not feed file names back into runtime identity.
Backup And Restore
Backups should be one archive file, but database capture should be SQLite-native:
- Keep write transactions bounded so online backup can make forward progress.
- Verify every live global and agent database before capture.
- Capture each database with SQLite online backup into a temporary backup directory, then close the live connection and
VACUUMthe private copy. Plugin schemas that require owner-defined SQLite capabilities fail closed until the owner provides a safe snapshot contract. - Archive the database snapshots, config file, credentials directory, selected workspaces, and a manifest.
- Verify every SQLite snapshot's file shape, then open canonical OpenClaw databases and run
PRAGMA integrity_checkplus role validation. Dedicated plugin schemas remain opaque unless their owner supplies a verifier.openclaw backup createdoes this by default;--no-verifyis only for intentionally skipping the post-write archive pass.
Do not rely on raw live *.sqlite, *.sqlite-wal, and *.sqlite-shm copies as the primary backup format. The archive manifest should record database role, agent id, schema version, source path, snapshot path, byte size, and integrity status.
Restore should rebuild the global database and agent database files from the archive snapshots. The global schema remains version 1; per-agent version 1 snapshots receive the bounded runtime upgrade to version 2. Doctor remains the only owner of file-to-database import. The restore command validates the archive first, then replaces each manifest asset from the verified extracted payload.
Runtime Refactor Plan
-
Add database registry APIs.
- Resolve global DB and per-agent DB paths.
- Keep the global schema at
user_version = 1. Per-agent DBs use version2with one atomic migration from the shipped version1memory-source shape. - Add close/checkpoint/integrity helpers used by tests, backup, and doctor.
-
Collapse sidecar SQLite stores.
- Move plugin state tables into the global database. Runtime writes are done; the unshipped legacy sidecar importer is deleted.
- Move task registry tables into the global database. Runtime writes are done; the unshipped legacy sidecar importer is deleted.
- Move Task Flow tables into the global database. Runtime writes are done; the unshipped legacy sidecar importer is deleted.
- Move builtin memory-search tables into each agent database. Done; explicit custom
memorySearch.store.pathis now removed by doctor config migration. Full reindex runs in place against memory tables only; the old whole-file swap path and sidecar index swap helper are deleted. - Delete duplicate database openers, WAL setup, permission helpers, and close paths from those subsystems.
-
Move agent-owned tables into per-agent databases.
- Create agent DB on demand through the global database registry. Done.
- Move runtime session entries, transcript events, VFS rows, and tool artifacts to agent DBs. Done.
- Do not migrate branch-local shared-DB session entries, transcript events, VFS rows, or tool artifacts; that layout never shipped. Keep only legacy file-to-database import in doctor.
-
Replace session store APIs.
- Remove
storePathas the runtime identity. Done for runtime and guarded bycheck:database-first-legacy-stores: session metadata, route updates, command persistence, CLI session cleanup, Feishu reasoning previews, transcript-state persistence, subagent depth, auth profile session overrides, parent-fork logic, and QA-lab inspection now resolve the database from canonical agent/session keys. Gateway/TUI/UI/macOS session-list responses now exposedatabasePathinstead of legacypath; macOS debug surfaces show the per-agent database as read-only state instead of writingsession.storeconfig./status, chat-driven trajectory export, and CLI dependency proxies no longer propagate legacy store paths; transcript usage fallback reads SQLite by agent/session identity. Runtime and bridge tests no longer exposestorePath; doctor/migration inputs own that legacy field name. Gateway combined-session loading no longer has a special runtime branch for non-templatedsession.storevalues; it aggregates per-agent SQLite rows. The legacy session-lock doctor lane and its.jsonl.lockcleanup helper were removed; SQLite is the session concurrency boundary now. Hot runtime call sites use row-oriented helper names such asresolveSessionRowEntry; the oldresolveSessionStoreEntrycompatibility alias has been removed from runtime and plugin SDK exports.
- Remove
- Use
{ agentId, sessionKey }row operations. Done:getSessionEntry,upsertSessionEntry,deleteSessionEntry,patchSessionEntry, andlistSessionEntriesare SQLite-first APIs that do not require a session store path. Status summary, local agent status, health, and theopenclaw sessionslisting command now read per-agent rows directly and display per-agent SQLite database paths instead ofsessions.jsonpaths. - Replace whole-store delete/insert with
upsertSessionEntry,deleteSessionEntry,listSessionEntries, and SQL cleanup queries. Done for runtime: hot paths now use row APIs and conflict-retried row patches; remaining whole-store import/replace helpers are limited to migration import code and SQLite backend tests.- Delete
store-writer.tsand writer-queue tests. Done. - Delete runtime legacy-key pruning and alias-delete parameters from session row upserts/patches. Done.
- Delete
- Delete runtime JSON registry behavior.
- Make sandbox registry reads and writes SQLite-only. Done.
- Import monolithic and sharded JSON only from the migration step. Done.
- Remove sharded registry locks and JSON writes. Done.
- Keep one typed registry table instead of storing registry rows as generic opaque JSON if the shape remains hot-path operational state. Done.
-
Delete file-lock-shaped session mutation.
- Done for runtime lock creation and runtime lock APIs.
- The standalone legacy
.jsonl.lockdoctor cleanup lane is removed. - State integrity no longer has a separate orphan transcript-file pruning path; doctor migration imports/removes legacy JSONL sources in one place.
- Gateway singleton coordination uses typed SQLite
state_leasesrows undergateway_locksand no longer exposes a file-lock directory seam. - Generic plugin SDK dedupe persistence no longer uses file locks or JSON files; it writes shared SQLite plugin-state rows. Done.
- QMD coordination uses a shared SQLite lease for embeds and a per-agent SQLite lease for every collection/update/embed writer. Runtime no longer creates
qmd/embed.lock.lockoragents/<agentId>/qmd-write.lock.lock; Doctor removes only definitely stale retired sidecars. Done.
-
Make workers database-aware.
- Workers open their own SQLite connections.
- Parent owns delivery, channel callbacks, and config.
- Worker receives agent id, run id, filesystem mode, and DB registry identity, not live handles.
vfs-onlystays experimental and uses the agent database as its storage root.- Keep one worker per active run first. Pooling can wait until DB connection lifetime and cancellation behavior are boring.
-
Backup integration.
- Teach backup to snapshot global, agent, and plugin databases with online backup followed by offline
VACUUM. Done for discovered*.sqlitefiles under the state asset; plugin schemas requiring unavailable owner capabilities fail closed. - Add backup verification for canonical SQLite integrity and schema identity, plus generic file-shape validation for dedicated plugin snapshots. Done for backup creation and default archive verification.
- Record backup run metadata in SQLite. Done via the shared
backup_runstable with archive path, status, and manifest JSON. - Add restore from verified archive snapshots. Done:
openclaw backup restorevalidates before extraction, uses the verifier's normalized manifest, supports--dry-run, and requires--yesbefore replacing recorded source paths. - Include VFS/workspace export only when requested; do not export session internals as JSON or JSONL.
- Teach backup to snapshot global, agent, and plugin databases with online backup followed by offline
-
Delete obsolete tests and code. Done for the known runtime session surfaces.
-
Tests that assert runtime creation of
sessions.jsonor transcript JSONL files have been removed. This covers core session store, chat, gateway transcript events, preview, lifecycle, command session-entry updates, auto-reply reset/trace, memory-core dreaming fixtures, approval target routing, session transcript repair, security permission repair, trajectory export, and session export. Active-memory transcript tests now verify SQLite scopes and confirm no temporary or persisted JSONL file creation. The old heartbeat transcript-pruning regression was removed because the runtime no longer truncates JSONL transcripts. Agent session-list tool tests no longer model legacysessions.jsonpaths as the gateway response shape; app/UI/macOS tests usedatabasePath./statustranscript-usage tests now seed SQLite transcript rows directly instead of writing JSONL files. Gateway session lifecycle tests now use SQLite transcript seeding helpers directly; the old single-line session-file fixture shape is gone from reset and delete coverage.sessions.deleteno longer returns a file-eraarchived: []field; deletion reports only the row mutation result. The olddeleteTranscriptoption is also gone: deleting a session removes the canonicalsessionsroot and lets SQLite cascade session-owned transcript, snapshot, and trajectory rows, so no caller can leave transcript orphans behind or forget a cleanup branch. Context-engine trajectory capture tests now readtrajectory_runtime_eventsrows from an isolated agent database instead of readingsession.trajectory.jsonl. Docker MCP channel seed scripts now seed SQLite rows directly. Directsessions.jsonwrites are limited to doctor fixtures. Tool Search Gateway E2E reads tool-call evidence from SQLite transcript rows instead of scanningagents/<agentId>/sessions/*.jsonlfiles. Memory-core host events and session-corpus scratch rows now live in shared SQLite plugin-state;events.jsonlandsession-corpus/*.txtare legacy doctor migration inputs only. Active rows usememory/session-ingestion/virtual paths, not.dreams/session-corpus. The old memory-core dreaming repair module and its CLI/Gateway tests were removed because the runtime no longer owns file archive repair for that corpus. Memory-core bridge/public-artifact tests no longer surface.dreams/events.jsonl; they use the SQLite-backed virtual JSON artifact name. Public SDK/Codex testing docs now refer to SQLite session state instead of session files, and the channel-turn example no longer exposes astorePathargument. Matrix sync state now uses the SQLite plugin-state store directly. Active client/runtime contracts pass an account storage root, not abot-storage.jsonpath, and doctor imports legacybot-storage.jsoninto SQLite before deleting the source. QA Lab Matrix restart/destructive scenarios now mutate the SQLite sync row directly instead of creating or deleting fakebot-storage.jsonfiles, and the E2EE substrate passes a sync-store root instead of a fakesync-store.jsonpath. Matrix storage-root selection no longer scores roots by legacy sync/thread JSON files; it uses durable root metadata plus real crypto state. The runtime SQLite session backend test suite no longer fabricates asessions.json; legacy source fixtures now live in the doctor tests that import them. Gateway session tests no longer expose acreateSessionStoreDirhelper or unused temp session-store path setup; fixture dirs are explicit, and direct row setup uses SQLite session-row naming. Doctor-only JSON5 session-store parser coverage moved out of infra tests and into doctor migration tests, so runtime test suites no longer own legacy session-file parsing. Microsoft Teams runtime SSO/pending-upload tests no longer carry JSON sidecar fixtures or parsers; legacy SSO token parsing lives only in the plugin migration module. Telegram tests no longer seed fake/tmp/*.jsonstore paths; they reset the SQLite-backed message cache directly. The generic OpenClaw test-state helper no longer exposes a legacyauth-profiles.jsonwriter; doctor auth migration tests own that fixture locally. Runtime tests for TUI last-session pointers, exec approvals, active-memory toggles, Matrix dedupe/startup verification, Memory Wiki source sync, current-conversation bindings, onboarding auth, and Hermes secret imports no longer manufacture old sidecar files or assert old filenames are absent. They prove behavior through SQLite rows and public store APIs; doctor/migration tests are the only place legacy source filenames belong. Runtime tests for device/node pairing, channel allowFrom, restart intents, restart handoff, session delivery queue entries, config health, iMessage caches, cron jobs, PI transcript headers, subagent registries, and managed image attachments also no longer create retired JSON/JSONL files just to prove they are ignored or absent. PI overflow recovery no longer has a SessionManager rewrite/truncation fallback: tool-result truncation and context-engine transcript rewrites mutate SQLite transcript rows, then refresh active prompt state from the database. Persisted SessionManager message appends delegate to the atomic SQLite transcript append helper for parent selection and idempotency. Normal metadata/custom entry appends also select the current parent inside SQLite, so stale manager instances do not resurrect pre-SQLite parent-chain races. Synthetic PI tail cleanup for mid-turn prechecks andsessions_yieldnow trims SQLite transcript state directly; the old SessionManager tail-removal bridge and its tests are deleted. Compaction checkpoint capture also snapshots from SQLite only; callers no longer pass a live SessionManager as an alternate transcript source. -
Keep tests that seed legacy files only for migration.
-
JSON-file proof has been replaced with SQL row proof for active runtime surfaces.
-
Add static bans for runtime writes to legacy session/cache JSON paths. Done for the repo guard.
- Make the migration report auditable.
- Record migration runs in SQLite with started/finished timestamps, source paths, source hashes, counts, warnings, and backup path. Done: legacy-state migration executions now persist a
migration_runsreport with source path/table inventory, source file SHA-256, sizes, record counts, warnings, and backup path. Done: legacy-state migration executions also persistmigration_sourcesrows for source-level audit and future skip/backfill decisions. - Make apply idempotent. Re-running after a partial import should either skip an already imported source or merge by stable key. Done: session indexes, transcripts, delivery queues, plugin state, task ledgers, and agent-owned global SQLite rows import through stable keys or upsert/replace semantics, so reruns merge without duplicating durable rows.
- Failed imports must keep the original source file in place. Done: failed transcript imports now leave the original JSONL source at its detected path, and
migration_sourcesrecords the source aswarningwithremoved_source=0for the next doctor run.
- Record migration runs in SQLite with started/finished timestamps, source paths, source hashes, counts, warnings, and backup path. Done: legacy-state migration executions now persist a
Performance Rules
- One connection per thread/process is fine; do not share handles across workers.
- Use WAL,
foreign_keys=ON, a 5s busy timeout, and shortBEGIN IMMEDIATEwrite transactions. Do not layer synchronous lock retries above SQLite's single busy wait. - Keep write transaction helpers synchronous unless/until an async transaction API adds explicit mutex/backpressure semantics.
- Keep parent delivery writes small and transactional.
- Avoid whole-store rewrites; use row-level upsert/delete.
- Add indexes for list-by-agent, list-by-session, updated-at, run id, and expiration paths before moving hot code.
- Store large artifacts, media, and vectors as BLOBs or chunked BLOB rows, not base64 or numeric-array JSON.
- Keep opaque plugin-state entries small and scoped.
- Add SQL cleanup for TTL/expiration instead of filesystem pruning. Done for database-owned runtime stores: media, plugin state, plugin blobs, persistent dedupe, and agent cache all expire through SQLite rows. Remaining filesystem cleanup is limited to temporary materializations or explicit removal commands.
Static Bans
Add a repo check that fails new runtime writes to legacy state paths.
sessions.json*.trajectory.jsonlexcept materialized support-bundle outputs.acp-stream.jsonlacp/event-ledger.jsoncache/*.jsonruntime cache filesagents/<agentId>/agent/auth.jsonagents/<agentId>/agent/models.jsoncredentials/oauth.jsongithub-copilot.token.jsonopenrouter-models.jsonauth-profiles.jsonauth-state.jsonexec-approvals.jsonopenclaw-workspace-state.jsonworkspace-state.jsonworkspace-attestations/*.attested- sibling
<workspace>.attested - Matrix
credentials*.jsonandrecovery-key.json cron/runs/*.jsonlcron/jobs.jsonjobs-state.jsondevice-pair-notify.jsondevices/pending.json/devices/paired.json/devices/bootstrap.json(retired 2026.7: runtime store isdevice_pairing_*/device_bootstrap_tokensin the shared state DB; paired records import at gateway startup, transient pending/bootstrap rows are dropped)nodes/pending.json/nodes/paired.json(retired 2026.7: folded into paired device records at gateway startup)identity/device.jsonidentity/device-auth.json(retired; Doctor-only import intodevice_auth_tokens)push/web-push-subscriptions.json(retired; Doctor-only import intoweb_push_subscriptions)push/vapid-keys.json(retired; Doctor-only import intoweb_push_vapid_keys)push/apns-registrations.json(retired; Doctor-only import intoapns_registrations)process-leases.jsongateway-instance-idsession-toggles.json- Memory-core
.dreams/events.jsonl - Memory-core
.dreams/session-corpus/ - Memory-core
.dreams/daily-ingestion.json - Memory-core
.dreams/session-ingestion.json - Memory-core
.dreams/short-term-recall.json - Memory-core
.dreams/phase-signals.json - Memory-core
.dreams/short-term-promotion.lock - Skill Workshop
skill-workshop/<workspace>.json - Skill Workshop
skill-workshop/skill-workshop-review-*.json - Nostr
bus-state-*.json - Nostr
profile-state-*.json calls.jsonlknown-users.jsonref-index.jsonl- QQBot
session-*.json - BlueBubbles
bluebubbles/catchup/*.json - BlueBubbles
bluebubbles/inbound-dedupe/*.json - Telegram
update-offset-*.json - Telegram
sticker-cache.json - Telegram
*.telegram-messages.json - Telegram
*.telegram-sent-messages.json - Telegram
*.telegram-topic-names.json - Telegram
thread-bindings-*.json - iMessage
catchup/*.json - iMessage
reply-cache.jsonl - iMessage
sent-echoes.jsonl - Microsoft Teams
msteams-conversations.json - Microsoft Teams
msteams-polls.json - Microsoft Teams
msteams-sso-tokens.json - Microsoft Teams
*.learnings.json - Matrix
bot-storage.json - Matrix
sync-store.json - Matrix
thread-bindings.json - Matrix
inbound-dedupe.json - Matrix
startup-verification.json - Matrix
storage-meta.json - Matrix
crypto-idb-snapshot.json - Discord
model-picker-preferences.json - Discord
command-deploy-cache.json - sandbox registry shard JSON files
plugin-state/state.sqlite- ad-hoc
openclaw-state.sqliteruntime sidecars tasks/runs.sqlitetasks/flows/registry.sqlitebindings/current-conversations.jsonrestart-sentinel.jsongateway-restart-intent.jsongateway-supervisor-restart-handoff.jsongateway.<hash>.lockqmd/embed.lock.lockagents/<agentId>/qmd-write.lock.lockcommands.logconfig-health.jsonport-guard.jsonsettings/voicewake.jsonsettings/voicewake-routing.jsonplugin-binding-approvals.jsonplugins/installs.jsonaudit/file-transfer.jsonlaudit/crestodian.jsonlcrestodian/rescue-pending/*.jsonopenclaw/rescue-pending/*.jsonplugins/phone-control/armed.json- Memory Wiki
.openclaw-wiki/log.jsonl - Memory Wiki
.openclaw-wiki/state.json - Memory Wiki
.openclaw-wiki/locks/ - Memory Wiki
.openclaw-wiki/source-sync.json - Memory Wiki
.openclaw-wiki/import-runs/*.json - Memory Wiki
.openclaw-wiki/cache/agent-digest.json - Memory Wiki
.openclaw-wiki/cache/claims.jsonl - ClawHub
.clawhub/lock.json - ClawHub
.clawhub/origin.json - Browser profile decoration
.openclaw-profile-decorated SessionManager.open(...)file-backed session openersSessionManager.listAll(...)andTranscriptSessionManager.listAll(...)transcript listing facadesSessionManager.forkFromSession(...)andTranscriptSessionManager.forkFromSession(...)transcript fork facadesSessionManager.newSession(...)andTranscriptSessionManager.newSession(...)mutable facades that replace sessionsSessionManager.createBranchedSession(...)andTranscriptSessionManager.createBranchedSession(...)branch-session facades
The ban must let tests create legacy fixtures and let migration code read, import, or remove legacy file sources. Unshipped SQLite sidecars remain banned and do not receive doctor import allowances.
Done Criteria
- Runtime data and cache writes target the global or agent SQLite database.
- Runtime no longer writes session indexes, transcript JSONL, sandbox registry JSON, task sidecar SQLite, or plugin-state sidecar SQLite. The unshipped task and plugin-state sidecar SQLite importers are removed.
- Legacy file import is restricted to doctor only.
- Backup produces a single archive containing compact SQLite snapshots and an integrity proof.
- Agent workers can operate with disk, VFS scratch, or experimental VFS-only storage.
- Config and explicit credential files remain the only expected persistent non-database control files.
- Repo checks prevent reintroduction of legacy runtime file stores.