Skip to content

Commit db3213e

Browse files
author
OpenCode Agent
committed
fix(db): eliminate 'database is locked' plugin-load failures
Three-layer fix for the recurring 'failed to load plugin @cortexkit/opencode-magic-context error="database is locked"' errors that crash opencode startup when multiple processes cold-open context.db concurrently. Root cause: during a concurrent cold open (two opencode processes booting, or a child session spawning while the parent is mid-checkpoint), the WAL writer lock is held by a sibling. The first read in openDatabase() — enforceSchemaFence() -> getPersistedSchemaVersion() — ran BEFORE busy_timeout was installed (it was set later inside initializeDatabase()), so SQLite returned SQLITE_BUSY immediately instead of waiting. Layer 1 (storage-db.ts): Set PRAGMA busy_timeout=5000 immediately after new Database(), before enforceSchemaFence()'s schema_migrations read. This was the actual race window. Layer 2 (dream-timer.ts): openTimerDatabaseOrNull() only handled openDatabase()'s null return (schema-fence path) but NOT its throw path (fatal open error). The throw propagated through startDreamScheduleTimer() -> plugin() -> opencode's plugin loader, which logged 'failed to load plugin' and disabled magic-context for the entire session. Now wrapped in try/catch. Layer 3 (index.ts): Defensive try/catch around the startDreamScheduleTimer() await in the plugin entry, so even if a future code path inside the timer setup throws, plugin() survives and the rest of magic-context (hooks, tools, RPC) still initializes.
1 parent d2e0a6f commit db3213e

3 files changed

Lines changed: 22 additions & 2 deletions

File tree

packages/plugin/src/features/magic-context/storage-db.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1394,6 +1394,14 @@ export function openDatabase(dbPathOrOptions?: string | OpenDatabaseOptions): Da
13941394
mkdirSync(dbDir, { recursive: true });
13951395

13961396
const db = new Database(dbPath);
1397+
// Install busy_timeout IMMEDIATELY after open, before any read. Without
1398+
// this, enforceSchemaFence()'s schema_migrations read below can throw
1399+
// SQLITE_BUSY when a sibling process holds the WAL writer lock during a
1400+
// concurrent cold open (two opencode processes booting at once, or a
1401+
// child session spawning while the parent is mid-checkpoint). The
1402+
// busy_timeout set later inside initializeDatabase() is too late for
1403+
// this first read. See the "database is locked" plugin-load failures.
1404+
db.exec("PRAGMA busy_timeout=5000");
13971405
if (!enforceSchemaFence(db, dbPath, latestSupportedVersion)) {
13981406
closeQuietly(db);
13991407
return null;

packages/plugin/src/index.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,11 @@ const plugin: Plugin = async (ctx) => {
209209
: undefined,
210210
ensureRegistered: ensureProjectRegisteredFromOpenCodeDirectory,
211211
};
212-
stopDreamTimerRegistration = await startDreamScheduleTimer(timerRegistration);
212+
try {
213+
stopDreamTimerRegistration = await startDreamScheduleTimer(timerRegistration);
214+
} catch (err) {
215+
log(`[magic-context] dream schedule timer failed to start (non-fatal): ${err}`);
216+
}
213217

214218
// Start RPC server for TUI↔server communication (replaces SQLite plugin_messages bus).
215219
// `storageDir` is hoisted above so the auto-update checker can also use it.

packages/plugin/src/plugin/dream-timer.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,15 @@ let activeTimer: ReturnType<typeof setInterval> | null = null;
5959
* deep in embedding registration and throws a confusing TypeError.
6060
*/
6161
function openTimerDatabaseOrNull(context: string): Database | null {
62-
const db = openDatabase();
62+
let db: Database | null;
63+
try {
64+
db = openDatabase();
65+
} catch (error) {
66+
log(
67+
`[dreamer] storage open threw; skipping ${context}: ${error instanceof Error ? error.message : String(error)}`,
68+
);
69+
return null;
70+
}
6371
if (!db) {
6472
log(
6573
`[dreamer] storage unavailable; skipping ${context} (the cache schema is newer than this binary supports — restart/upgrade OpenCode/Pi/Magic Context to recover)`,

0 commit comments

Comments
 (0)