Summary
The memory store opens its SQLite database with no busy timeout — the runtime never
executes PRAGMA busy_timeout (or passes an equivalent driver option), so the default
timeout of 0 ms applies. Any write that collides with another holder fails immediately
with database is locked instead of waiting for the lock to clear.
Environment
| Item |
Value |
| Build |
v2026.923.0, commit 5a0efbc5106e7c80c3df8e20184c439ed3d0603b |
| Runtime |
packaged ESM JS at resources/runtime/dist |
| Host |
Windows 11, Node 22.x |
| Database |
<pilotdeck home>/memory/workspaces/<project>/control.sqlite |
Evidence
1. No busy timeout is configured anywhere in the runtime
A search of the entire shipped runtime returns zero matches:
$ grep -rn "busy_timeout" resources/runtime/dist
$ echo $?
1
The only PRAGMA statements executed against the memory database are
journal_mode and a schema introspection query, in
dist/src/context/memory/edgeclaw-memory-core/src/core/storage/sqlite.js:
// sqlite.js:386
PRAGMA journal_mode = WAL;
// sqlite.js:407
const columns = this.db.prepare("PRAGMA table_info(pipeline_state)").all();
WAL mode is therefore enabled, but the connection still has a 0 ms lock-acquisition
timeout, so it takes no advantage of WAL's concurrency benefits.
2. The database handle is created without any lock-related option
The connection factory loads the driver and constructs the handle with no options:
// dist/src/context/memory/edgeclaw-memory-core/src/core/storage/sqlite.js (~:259)
async function loadSqlDatabaseFactory() {
if (typeof globalThis.Bun !== "undefined") {
const bunSqliteModuleName = "bun:sqlite";
const bunSqlite = await import(bunSqliteModuleName);
return (dbPath) => {
const db = new bunSqlite.Database(dbPath, { create: true });
...
on the Node path it uses the built-in node:sqlite DatabaseSync (declared for
engines: ">=22.13.0 <23"), likewise without a timeout option.
The database path is a single shared file per workspace:
// sqlite.js:430
this.dbPath = resolve(options.dbPath ?? join(this.dataDir, "control.sqlite"));
3. The lock error is real and observed in the wild
runtime.log, note the [server] process prefix — the memory scheduler is not
running in the process the user's session is writing from:
2026-09-25T13:26:05.399Z [server] [memory-scheduler] scheduled maintenance failed for
C:\Users\Administrator\.pilotdeck\memory\workspaces\6278025c52: Error: database is locked
2026-09-25T13:26:05.399Z [server] errstr: 'database is locked'
Two occurrences were observed in a single day of logs. The string SQLITE_BUSY does not
appear at all in the logs, because the driver surfaces this condition as the message
database is locked.
4. Concurrent access is expected, not exceptional
- The memory subsystem is enabled by default and is opened by more than one entry point in
the same installation; multiple processes (the gateway handling the user's session and the
desktop/WebUI server process that runs the scheduled maintenance task) can target the same
control.sqlite for a given workspace.
- The observed failure is exactly a cross-process symptom: the process that logs the
error is the [server] one, while the workspace in question is simultaneously being
written by an active agent session.
- No retry/backoff wrapper around database writes could be located in the runtime, so a
failed write is not retried.
To be explicit about the limit of this analysis: I confirmed that no busy timeout exists and
that the failure occurs in a process other than the active session. I did not instrument the
two processes to prove which one held the competing lock at that instant, so the precise
lock pairing is inferred rather than captured.
Impact
The scheduled memory-maintenance task aborts for that workspace. It appears to be
recoverable on the next cycle, so the practical impact is degraded/ delayed memory
maintenance and noisy logs rather than data loss — but the same 0 ms timeout applies to any
write the memory subsystem performs, so any future concurrent writer inherits the same
failure mode. Severity: moderate.
Suggested fix
Set a lock timeout once, right after the connection is opened, in whatever place the runtime
already applies its PRAGMAs (sqlite.js, next to PRAGMA journal_mode = WAL):
// Wait up to 5s for a competing writer instead of failing immediately.
db.exec("PRAGMA busy_timeout = 5000");
Notes for whoever picks this up:
PRAGMA busy_timeout is driver-agnostic, so it also covers the better-sqlite3 handle
used for auth.db (resources/runtime/package.json:15, better-sqlite3 ^12.6.2), which
has its own separate connection configuration and should be audited independently.
- A constructor-level equivalent also exists on some drivers (e.g. Bun's
Database accepts
a timeout option), but the PRAGMA is the portable choice.
- On top of the timeout, the maintenance path could treat
SQLITE_BUSY / "database is locked"
as a retryable condition and re-schedule the task rather than logging a failure.
How to reproduce
- Have an active agent session writing to a workspace's memory database.
- Trigger the scheduled memory maintenance for the same workspace (or otherwise have two
processes write to the same control.sqlite concurrently).
- The losing writer fails immediately with
database is locked; there is no retry window.
Summary
The memory store opens its SQLite database with no busy timeout — the runtime never
executes
PRAGMA busy_timeout(or passes an equivalent driver option), so the defaulttimeout of 0 ms applies. Any write that collides with another holder fails immediately
with
database is lockedinstead of waiting for the lock to clear.Environment
v2026.923.0, commit5a0efbc5106e7c80c3df8e20184c439ed3d0603bresources/runtime/dist<pilotdeck home>/memory/workspaces/<project>/control.sqliteEvidence
1. No busy timeout is configured anywhere in the runtime
A search of the entire shipped runtime returns zero matches:
The only
PRAGMAstatements executed against the memory database arejournal_modeand a schema introspection query, indist/src/context/memory/edgeclaw-memory-core/src/core/storage/sqlite.js:WAL mode is therefore enabled, but the connection still has a 0 ms lock-acquisition
timeout, so it takes no advantage of WAL's concurrency benefits.
2. The database handle is created without any lock-related option
The connection factory loads the driver and constructs the handle with no options:
on the Node path it uses the built-in
node:sqliteDatabaseSync(declared forengines: ">=22.13.0 <23"), likewise without a timeout option.The database path is a single shared file per workspace:
3. The lock error is real and observed in the wild
runtime.log, note the[server]process prefix — the memory scheduler is notrunning in the process the user's session is writing from:
Two occurrences were observed in a single day of logs. The string
SQLITE_BUSYdoes notappear at all in the logs, because the driver surfaces this condition as the message
database is locked.4. Concurrent access is expected, not exceptional
the same installation; multiple processes (the gateway handling the user's session and the
desktop/WebUI server process that runs the scheduled maintenance task) can target the same
control.sqlitefor a given workspace.error is the
[server]one, while the workspace in question is simultaneously beingwritten by an active agent session.
failed write is not retried.
To be explicit about the limit of this analysis: I confirmed that no busy timeout exists and
that the failure occurs in a process other than the active session. I did not instrument the
two processes to prove which one held the competing lock at that instant, so the precise
lock pairing is inferred rather than captured.
Impact
The scheduled memory-maintenance task aborts for that workspace. It appears to be
recoverable on the next cycle, so the practical impact is degraded/ delayed memory
maintenance and noisy logs rather than data loss — but the same 0 ms timeout applies to any
write the memory subsystem performs, so any future concurrent writer inherits the same
failure mode. Severity: moderate.
Suggested fix
Set a lock timeout once, right after the connection is opened, in whatever place the runtime
already applies its PRAGMAs (
sqlite.js, next toPRAGMA journal_mode = WAL):Notes for whoever picks this up:
PRAGMA busy_timeoutis driver-agnostic, so it also covers thebetter-sqlite3handleused for
auth.db(resources/runtime/package.json:15,better-sqlite3 ^12.6.2), whichhas its own separate connection configuration and should be audited independently.
Databaseacceptsa
timeoutoption), but the PRAGMA is the portable choice.SQLITE_BUSY/ "database is locked"as a retryable condition and re-schedule the task rather than logging a failure.
How to reproduce
processes write to the same
control.sqliteconcurrently).database is locked; there is no retry window.