Get started

Database access in workers

Runtime database access belongs in workers. The Gateway main thread owns live projections, caches, and caller authority; it awaits prepared facts and installs committed results. Synchronous boot admission, migrations, Doctor/CLI one-shots, and lock/lease primitives are the limited exceptions. Existing synchronous runtime paths are migration debt, not a pattern to extend. The migration inventory separates candidate main-thread paths from SQL already executing in workers.

Keep one store owner

Move an existing domain operation across its worker boundary instead of creating a second store, generic SQL service, or cache manager. Read-only operations use the existing read-only worker scope and the relevant domain reader. Shared-state fixed reads, session transcript/history reads, and task registry reads retain their established adapters and cleanup owners. A Promise around synchronous SQL, or withOpenClawAgentDatabaseReadOnly alone, does not move execution off thread. readWithCanonicalSessionAdmission validates session reads on the executing thread; invoke it inside the worker's admitted reader.

Writers use the SQLite worker broker's state.write or agent.write operation through their existing domain adapter, such as runOpenClawStateWorkerOperation. The connection-bound Kysely kernel and transaction callback remain synchronous inside the worker. Complete asynchronous planning first, then reread authoritative rows inside the admitted transaction. Preserve FIFO order, coordinator custody, transaction/commit grants, and settlement of accepted write-capable work.

Carry facts, publish after commit

Before yielding, capture the physical store target, source/admission scope, request identity, and the owning projection revision. The lifecycle owner retains that source until reader cleanup or write settlement completes. Workers return plain prepared rows, domain results, and the revision/identity evidence already owned by that operation. Database connections and live authority stay with their owners; serialized tokens or prepared rows do not grant permission.

After an awaited read, revalidate the captured lifecycle and current caller access before disclosing data. Install results only if the owner's revision still matches; otherwise use its existing invalidation/refresh path. Preserve identity-keyed sharing caches, bounded reuse, ordering, and byte-stable codecs. Reuse published facts through the request rather than reopening SQLite for each viewer or row. Do not add an independent freshness clock or cache lifecycle.

A writer publishes projections, revision changes, and observer notifications only after the committed result is acknowledged. A delayed reply cannot replace a newer native or worker publication. If result delivery is uncertain, retain the existing reconciliation custody: do not replay the write. Cancellation before dispatch can refuse work; cancellation after execution must still join its native settlement. Close and shutdown join accepted work and cleanup before releasing the store or replacing its generation.

Migrate a caller

  1. Trace the registered request, event, or timer through the store owner. Check whether a worker adapter already exists; separate durable databases from process-held incognito stores, which cannot be reopened by path in another isolate. An unresolved in-memory path remains explicit migration debt, not a new synchronous exception.
  2. Put the smallest complete read or mutation in that adapter, preserving its row codecs, missing-store behavior, snapshot/canonical admission, and error contract. Move all affected runtime callers together; never fall back to host SQLite after a worker failure.
  3. Await the domain operation, check current authority, and install the prepared result through the existing projection owner. Retain existing revisions and sharing identities. Remove the superseded main-thread call path.
  4. Compare serialized results against the original entry point on representative fixtures. Exercise stale replies, close/cancellation, sharing changes, and committed-write visibility where relevant. Measure main-thread time separately from total latency; worker startup and transfer costs still affect users.

For an example, ordinary durable pages in src/gateway/server-methods/chat-history-pages.ts already await readSessionHistoryPageInWorker. Raw cursor delta reads now use that same worker for SQLite and JSON parsing. The main thread retains display/profile projection, byte budgets, and fresh sharing checks. Selected/current entries, pending inputs and receipts, retained transcript-session keys, and lazy subagent source/visibility reads remain migration debt. Process-held incognito databases and the existing CLI-import history path still need their owner/lifetime migration; they are not new synchronous exceptions or fallbacks for a failed durable worker read.

The asynchronous transcript-search facade similarly moves durable FTS reads for all four Gateway/tool callers through the existing worker lifecycle. Each caller rechecks current scope and authorization after awaiting. Warm sessions.list already selects resident projection rows without host Kysely reads; its remaining database work is hydration, dirty/archived-row refresh, and membership. Preserve that projection and its identity/revision invalidation instead of replacing it with another per-request store scan. See the inventory baseline for measurements and the next owners to migrate.

For writes, shared-state domain operations registered by src/state/openclaw-state-worker-runtime.ts reuse the broker and publish results through their original store/projection owner.

This execution cutover does not change schemas, stored bytes, retention, config, or update behavior. A change to those contracts follows the storage review checkpoint.

Was this useful?
On this page

On this page