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
- 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.
- 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.
- 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.
- 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.