{"record":{"id":"5a5c1ab432d52a5c","repo":"can1357/oh-my-pi","slug":"wal-checkpoint-failed-for-dbpath-busy-result","errorCode":null,"errorMessage":"WAL checkpoint failed for ${dbPath}: busy=${result.busy}, walBytes=${result.walBytes}","messagePattern":"WAL checkpoint failed for (.+?): busy=(.+?), walBytes=(.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/cli/gc-cli.ts","lineNumber":1336,"sourceCode":"\tlet checkpointAttempted = false;\n\ttry {\n\t\tdb.run(\"PRAGMA busy_timeout = 5000\");\n\t\tconst row = db.prepare(\"PRAGMA wal_checkpoint(TRUNCATE)\").get() as WalCheckpointRow | null;\n\t\tcheckpointAttempted = true;\n\t\tresult.busy = sqliteNumber(row?.busy);\n\t\tresult.log = sqliteNumber(row?.log);\n\t\tresult.checkpointedFrames = sqliteNumber(row?.checkpointed);\n\t} finally {\n\t\tdb.close();\n\t}\n\ttry {\n\t\tresult.walBytes = (await fs.stat(walPath)).size;\n\t} catch (error) {\n\t\tif (codeOf(error) !== \"ENOENT\") throw error;\n\t\tresult.walBytes = 0;\n\t}\n\tif (checkpointAttempted && (result.busy > 0 || result.walBytes > 0)) {\n\t\tthrow new Error(`WAL checkpoint failed for ${dbPath}: busy=${result.busy}, walBytes=${result.walBytes}`);\n\t}\n\tresult.checkpointed = checkpointAttempted;\n\treturn result;\n}\n\nasync function runWalGc(options: ResolvedGcOptions): Promise<WalGcResult> {\n\tconst databases = await Promise.all(\n\t\t[getHistoryDbPath(options.agentDir), getModelDbPath(options.agentDir)].map(dbPath =>\n\t\t\tcheckpointWal(dbPath, options.apply),\n\t\t),\n\t);\n\treturn {\n\t\tdatabases,\n\t\twalBytes: databases.reduce((total, db) => total + db.walBytes, 0),\n\t\twouldCheckpoint: databases.some(db => db.wouldCheckpoint),\n\t\tcheckpointed: databases.some(db => db.checkpointed),\n\t};\n}","sourceCodeStart":1318,"sourceCodeEnd":1354,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/cli/gc-cli.ts#L1318-L1354","documentation":"After attempting a WAL checkpoint on the stats SQLite database, the code requires success: zero busy frames and a zero-byte WAL file. If either remains non-zero, other connections still hold the database and unflushed WAL data, so it throws rather than deleting/truncating a database that is still in use.","triggerScenarios":"Running stats GC while another process (TUI, stats dashboard, worker) keeps an open SQLite connection that wrote to the WAL between checkpoint and verification, leaving busy>0 or walBytes>0.","commonSituations":"`omp gc` executed while the omp TUI or `omp stats` dashboard is running; a crashed process leaving a hot WAL plus a live lock holder; network filesystems where checkpointing behaves poorly.","solutions":["Close other omp processes (TUI, stats dashboard, workers) holding the stats DB open, then re-run GC","Retry the checkpoint after the readers exit — WAL checkpoint TRUNCATE succeeds once no other connection is active","Check for stale/hung processes (`lsof` on the db/wal files) and kill them before retrying","Move the DB off network filesystems if checkpointing persistently fails"],"exampleFix":"// before (fails while dashboard holds the DB)\nomp stats sync && omp gc --wal\n// after\n# stop the dashboard/TUI, then\nomp gc --wal","handlingStrategy":"retry","validationCode":"import { Database } from \"bun:sqlite\";\nconst db = new Database(dbPath, { readonly: true });\nconst busy = db.query(\"PRAGMA wal_checkpoint(PASSIVE)\").get();\ndb.close();\nif (busy && (busy.busy > 0)) throw new Error(\"stats DB is in use; close other omp processes before GC\");","typeGuard":null,"tryCatchPattern":"try {\n  await checkpointWal(dbPath);\n} catch (err) {\n  if (err instanceof Error && err.message.startsWith(\"WAL checkpoint failed\")) {\n    // other connections hold the DB — wait for readers to exit, then retry\n    await Bun.sleep(1000);\n    await checkpointWal(dbPath);\n  } else throw err;\n}","preventionTips":["Close the omp TUI / stats dashboard before running WAL GC","Use a retry with backoff for checkpointing instead of treating busy>0 as fatal","Watch for processes holding the db/-wal files open (lsof) on shared machines","Keep the stats DB on a local filesystem, not network mounts"],"tags":["sqlite","wal","gc","locking","concurrency"],"backgroundTag":"sqlite-database-locked","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}