diff --git a/extensions/browser/src/browser/system-chrome-cookies.ts b/extensions/browser/src/browser/system-chrome-cookies.ts index 0058e02a150f..1885f7055c1d 100644 --- a/extensions/browser/src/browser/system-chrome-cookies.ts +++ b/extensions/browser/src/browser/system-chrome-cookies.ts @@ -1,7 +1,7 @@ /** macOS Chrome-family cookie database decryption and Playwright mapping. */ import crypto from "node:crypto"; -import { DatabaseSync } from "node:sqlite"; import { runCommandBuffered } from "openclaw/plugin-sdk/process-runtime"; +import { openNodeSqliteDatabase } from "openclaw/plugin-sdk/sqlite-runtime"; export type SystemBrowser = "chrome" | "brave" | "edge" | "chromium"; @@ -265,7 +265,7 @@ export async function readChromeCookiesDatabase(params: { readSecret?: KeychainSecretReader; signal?: AbortSignal; }) { - const database = new DatabaseSync(params.databasePath, { readOnly: true }); + const database = openNodeSqliteDatabase(params.databasePath, { readOnly: true }); try { const statement = database.prepare(COOKIE_QUERY); statement.setReadBigInts(true); diff --git a/extensions/browser/src/browser/system-profiles.ts b/extensions/browser/src/browser/system-profiles.ts index 612b435ddddd..ca5ac2fceedd 100644 --- a/extensions/browser/src/browser/system-profiles.ts +++ b/extensions/browser/src/browser/system-profiles.ts @@ -2,7 +2,7 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; -import { DatabaseSync } from "node:sqlite"; +import { openNodeSqliteDatabase } from "openclaw/plugin-sdk/sqlite-runtime"; import type { OpenClawConfig } from "../config/config.js"; import { getRuntimeConfig } from "../config/config.js"; import { resolvePreferredOpenClawTmpDir } from "../infra/tmp-openclaw-dir.js"; @@ -142,7 +142,7 @@ function snapshotCookieDatabase(source: string): { fs.mkdirSync(tmpRoot, { recursive: true }); const tempDir = fs.mkdtempSync(path.join(tmpRoot, "openclaw-system-cookies-")); const databasePath = path.join(tempDir, "Cookies"); - const sourceDatabase = new DatabaseSync(source, { readOnly: true }); + const sourceDatabase = openNodeSqliteDatabase(source, { readOnly: true }); try { sourceDatabase.exec("PRAGMA busy_timeout = 5000"); sourceDatabase.prepare("VACUUM INTO ?").run(databasePath); diff --git a/extensions/imessage/src/message-resource-db.ts b/extensions/imessage/src/message-resource-db.ts index 32923cd899e7..12bbf2a8d063 100644 --- a/extensions/imessage/src/message-resource-db.ts +++ b/extensions/imessage/src/message-resource-db.ts @@ -1,10 +1,8 @@ // Imessage plugin module verifies provider message ownership in the local Messages database. -import { createRequire } from "node:module"; +import { openNodeSqliteDatabase } from "openclaw/plugin-sdk/sqlite-runtime"; import { isIMessageEmailChatIdentifier, type IMessageChatContext } from "./chat-context.js"; import { resolveLocalIMessageChatDbPath } from "./cli-path.js"; -const require = createRequire(import.meta.url); - type IMessageResourceBinding = "match" | "mismatch" | "unavailable"; type IMessageChatRow = { chatGuid: unknown; @@ -81,14 +79,6 @@ function matchesAnyChatCandidate(stored: unknown, candidates: string[]): boolean return candidates.some((candidate) => matchesChatCandidate(stored, candidate)); } -function loadNodeSqlite(): typeof import("node:sqlite") | null { - try { - return require("node:sqlite") as typeof import("node:sqlite"); - } catch { - return null; - } -} - export function checkIMessageResourceBinding(params: { chatContext: IMessageChatContext; cliPath: string; @@ -97,8 +87,7 @@ export function checkIMessageResourceBinding(params: { remoteHost?: string; }): IMessageResourceBinding { const dbPath = resolveLocalIMessageChatDbPath(params); - const sqlite = loadNodeSqlite(); - if (!dbPath || !sqlite) { + if (!dbPath) { return "unavailable"; } const messageGuid = normalizeIMessageMessageGuidForLookup(params.messageId); @@ -123,7 +112,7 @@ export function checkIMessageResourceBinding(params: { let db: import("node:sqlite").DatabaseSync | undefined; try { - db = new sqlite.DatabaseSync(dbPath, { readOnly: true }); + db = openNodeSqliteDatabase(dbPath, { readOnly: true }); const rows = db .prepare( `SELECT cmj.chat_id AS chatId, diff --git a/extensions/imessage/src/monitor/monitor-provider.ts b/extensions/imessage/src/monitor/monitor-provider.ts index 5e1d5f222d2c..eca5be181788 100644 --- a/extensions/imessage/src/monitor/monitor-provider.ts +++ b/extensions/imessage/src/monitor/monitor-provider.ts @@ -45,6 +45,7 @@ import { resolveSendPolicy, resolveStorePath, } from "openclaw/plugin-sdk/session-store-runtime"; +import { openNodeSqliteDatabase } from "openclaw/plugin-sdk/sqlite-runtime"; import { sliceUtf16Safe, truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime"; import { waitForTransportReady } from "openclaw/plugin-sdk/transport-ready-runtime"; import { resolveIMessageAccount } from "../accounts.js"; @@ -270,8 +271,7 @@ async function resolveIMessageStartupRowidWatermark(dbPath: string): Promise; const MIN_PENDING_PERSISTED_ECHO_TTL_MS = 60_000; const PENDING_PERSISTED_ECHO_GRACE_MS = 5_000; @@ -177,14 +176,6 @@ function normalizeResolvedMessageGuid(value: unknown): string | null { return trimmed && !isNumericMessageRowId(trimmed) ? trimmed : null; } -function loadNodeSqlite(): typeof import("node:sqlite") | null { - try { - return require("node:sqlite") as typeof import("node:sqlite"); - } catch { - return null; - } -} - function resolveMessageGuidFromChatDb(params: { dbPath?: string; messageId: string; @@ -194,13 +185,9 @@ function resolveMessageGuidFromChatDb(params: { if (!dbPath || !isNumericMessageRowId(messageId)) { return null; } - const sqlite = loadNodeSqlite(); - if (!sqlite) { - return null; - } let db: import("node:sqlite").DatabaseSync | null = null; try { - db = new sqlite.DatabaseSync(dbPath, { readOnly: true }); + db = openNodeSqliteDatabase(dbPath, { readOnly: true }); const row = db.prepare("SELECT guid FROM message WHERE ROWID = ?").get(messageId) as | { guid?: unknown } | undefined; @@ -239,13 +226,9 @@ function resolveLatestSentMessageGuidFromChatDb(params: { if (!dbPath) { return null; } - const sqlite = loadNodeSqlite(); - if (!sqlite) { - return null; - } let db: import("node:sqlite").DatabaseSync | null = null; try { - db = new sqlite.DatabaseSync(dbPath, { readOnly: true }); + db = openNodeSqliteDatabase(dbPath, { readOnly: true }); const targetClauses: string[] = []; const targetParams: Array = []; const lowerBound = appleMessageDateLowerBoundMs(params.sentAfterMs); @@ -298,7 +281,7 @@ function resolveLatestSentMessageGuidFromChatDb(params: { function canResolveLatestSentMessageGuidFromChatDb(dbPath?: string): boolean { const normalizedDbPath = dbPath?.trim(); - if (!normalizedDbPath || !loadNodeSqlite()) { + if (!normalizedDbPath) { return false; } try { diff --git a/extensions/logbook/src/store.ts b/extensions/logbook/src/store.ts index ea0efc7a498b..4fc470bdc97d 100644 --- a/extensions/logbook/src/store.ts +++ b/extensions/logbook/src/store.ts @@ -2,13 +2,15 @@ // Uses node:sqlite prepared statements directly (extension-local store, same // pattern as memory-core/imessage); the shared Kysely helpers are core-only. import { chmodSync, mkdirSync, rmdirSync, rmSync } from "node:fs"; -import { createRequire } from "node:module"; import path from "node:path"; import { configureSqliteConnectionPragmas, migrateSqliteSchemaToStrict, } from "openclaw/plugin-sdk/plugin-state-runtime"; -import { runSqliteImmediateTransactionSync } from "openclaw/plugin-sdk/sqlite-runtime"; +import { + openNodeSqliteDatabase, + runSqliteImmediateTransactionSync, +} from "openclaw/plugin-sdk/sqlite-runtime"; import type { LogbookBatch, LogbookBatchStatus, @@ -20,14 +22,8 @@ import type { LogbookObservation, } from "./types.js"; -type SqliteModule = typeof import("node:sqlite"); type Database = import("node:sqlite").DatabaseSync; -function loadNodeSqlite(): SqliteModule { - const req = createRequire(import.meta.url); - return req("node:sqlite") as SqliteModule; -} - const LOGBOOK_SCHEMA_VERSION = 1; const LOGBOOK_SQLITE_BUSY_TIMEOUT_MS = 5_000; const SCHEMA = ` @@ -213,9 +209,8 @@ export class LogbookStore { this.framesDir = path.join(dataDir, "frames"); mkdirSync(this.framesDir, { recursive: true, mode: 0o700 }); chmodSync(this.framesDir, 0o700); - const { DatabaseSync } = loadNodeSqlite(); const dbPath = path.join(dataDir, "logbook.sqlite"); - const db = new DatabaseSync(dbPath); + const db = openNodeSqliteDatabase(dbPath); let walMaintenance: ReturnType | undefined; try { // WAL/SHM sidecars inherit the main DB file's permissions. diff --git a/extensions/memory-core/doctor-contract-api.ts b/extensions/memory-core/doctor-contract-api.ts index 546d2c893f16..335e969df500 100644 --- a/extensions/memory-core/doctor-contract-api.ts +++ b/extensions/memory-core/doctor-contract-api.ts @@ -16,7 +16,6 @@ import { MEMORY_INDEX_META_TABLE, MEMORY_INDEX_SOURCES_TABLE, MEMORY_INDEX_VECTOR_TABLE, - requireNodeSqlite, } from "openclaw/plugin-sdk/memory-core-host-engine-storage"; import { resolveMemoryDreamingWorkspaces } from "openclaw/plugin-sdk/memory-core-host-status"; import { @@ -32,6 +31,7 @@ import { } from "openclaw/plugin-sdk/runtime-doctor"; import { ensureOpenClawAgentDatabaseSchema, + openNodeSqliteDatabase, resolveOpenClawAgentSqlitePath, } from "openclaw/plugin-sdk/sqlite-runtime"; import { @@ -988,8 +988,7 @@ async function migrateLegacyMemorySidecarSource(params: { warnings: string[]; }): Promise<{ archiveReady: boolean }> { await fs.mkdir(path.dirname(params.source.agentDatabasePath), { recursive: true }); - const sqlite = requireNodeSqlite(); - const db = new sqlite.DatabaseSync(params.source.agentDatabasePath, { allowExtension: true }); + const db = openNodeSqliteDatabase(params.source.agentDatabasePath, { allowExtension: true }); try { const migrationEnv = { ...params.env, diff --git a/extensions/memory-core/src/memory/manager-db.ts b/extensions/memory-core/src/memory/manager-db.ts index 56591c1ad732..c7df33dd56c5 100644 --- a/extensions/memory-core/src/memory/manager-db.ts +++ b/extensions/memory-core/src/memory/manager-db.ts @@ -10,10 +10,10 @@ import { ensureMemoryPathFtsTriggers, loadSqliteVecExtension, MEMORY_INDEX_PATHS_FTS_TABLE, - requireNodeSqlite, } from "openclaw/plugin-sdk/memory-core-host-engine-storage"; import { ensureOpenClawAgentDatabaseSchema, + openNodeSqliteDatabase, runSqliteImmediateTransactionSync, } from "openclaw/plugin-sdk/sqlite-runtime"; import { @@ -322,8 +322,7 @@ export function openMemoryDatabaseAtPath( agentId?: string, ): DatabaseSync { ensureDir(path.dirname(dbPath)); - const { DatabaseSync } = requireNodeSqlite(); - const db = new DatabaseSync(dbPath, { allowExtension }); + const db = openNodeSqliteDatabase(dbPath, { allowExtension }); try { configureMemorySqliteWalMaintenance(db, { busyTimeoutMs: 5000, diff --git a/extensions/memory-core/src/memory/manager-reindex-lock.ts b/extensions/memory-core/src/memory/manager-reindex-lock.ts index 71a99f61ab8a..14d44a2f4e94 100644 --- a/extensions/memory-core/src/memory/manager-reindex-lock.ts +++ b/extensions/memory-core/src/memory/manager-reindex-lock.ts @@ -1,6 +1,6 @@ // Memory Core plugin module serializes full memory reindex builds across processes. import type { DatabaseSync } from "node:sqlite"; -import { requireNodeSqlite } from "openclaw/plugin-sdk/memory-core-host-engine-storage"; +import { openNodeSqliteDatabase } from "openclaw/plugin-sdk/sqlite-runtime"; export type MemoryReindexLockHandle = { release: () => void; @@ -20,8 +20,7 @@ function isSqliteBusyError(err: unknown): boolean { } function openMemoryLockDatabase(lockPath: string): DatabaseSync { - const { DatabaseSync } = requireNodeSqlite(); - const lockDb = new DatabaseSync(lockPath); + const lockDb = openNodeSqliteDatabase(lockPath); try { lockDb.exec("PRAGMA busy_timeout = 0"); return lockDb; diff --git a/extensions/memory-core/src/memory/qmd-manager.ts b/extensions/memory-core/src/memory/qmd-manager.ts index 49f2121a0f95..6f98b381c63b 100644 --- a/extensions/memory-core/src/memory/qmd-manager.ts +++ b/extensions/memory-core/src/memory/qmd-manager.ts @@ -28,7 +28,6 @@ import { DEFAULT_MEMORY_READ_LINES, isFileMissingError, type MemoryReadResult, - requireNodeSqlite, statRegularFile, type MemoryEmbeddingProbeResult, type MemoryProviderStatus, @@ -50,6 +49,7 @@ import { type PluginStateLeaseContext, type PluginStateLeaseRunner, } from "openclaw/plugin-sdk/plugin-state-runtime"; +import { openNodeSqliteDatabase } from "openclaw/plugin-sdk/sqlite-runtime"; import { normalizeLowercaseStringOrEmpty, uniqueValues, @@ -1806,8 +1806,7 @@ export class QmdMemoryManager implements MemorySearchManager { if (this.db) { return this.db; } - const { DatabaseSync } = requireNodeSqlite(); - this.db = new DatabaseSync(this.indexPath, { readOnly: true }); + this.db = openNodeSqliteDatabase(this.indexPath, { readOnly: true }); // busy_timeout is per-connection; set it on every open so concurrent // processes retry instead of failing immediately with SQLITE_BUSY. // Use a lower value than the write path (5 s) because this read-only diff --git a/extensions/memory-core/src/qmd-session-artifacts.ts b/extensions/memory-core/src/qmd-session-artifacts.ts index b789ea970219..f9cbb94d9d63 100644 --- a/extensions/memory-core/src/qmd-session-artifacts.ts +++ b/extensions/memory-core/src/qmd-session-artifacts.ts @@ -1,9 +1,9 @@ import fsSync from "node:fs"; import path from "node:path"; import type { DatabaseSync } from "node:sqlite"; -import { requireNodeSqlite } from "openclaw/plugin-sdk/memory-core-host-engine-storage"; import type { MemorySearchResult } from "openclaw/plugin-sdk/memory-core-host-runtime-files"; import { migrateSqliteSchemaToStrict } from "openclaw/plugin-sdk/plugin-state-runtime"; +import { openNodeSqliteDatabase } from "openclaw/plugin-sdk/sqlite-runtime"; const QMD_SESSION_ARTIFACT_TABLE = "openclaw_qmd_session_artifacts"; const QMD_SESSION_ARTIFACT_SCHEMA = ` @@ -90,11 +90,10 @@ function ensureQmdSessionArtifactSchema(db: DatabaseSync): void { } function openQmdSessionArtifactDb(indexPath: string, readOnly = false): DatabaseSync { - const { DatabaseSync: SqliteDatabase } = requireNodeSqlite(); if (!readOnly) { fsSync.mkdirSync(path.dirname(indexPath), { recursive: true }); } - const db = new SqliteDatabase(indexPath, { readOnly }); + const db = openNodeSqliteDatabase(indexPath, { readOnly }); db.exec("PRAGMA busy_timeout = 1000"); return db; } diff --git a/extensions/migrate-hermes/apply.ts b/extensions/migrate-hermes/apply.ts index ac2453d9023b..a0bda5070064 100644 --- a/extensions/migrate-hermes/apply.ts +++ b/extensions/migrate-hermes/apply.ts @@ -19,6 +19,7 @@ import type { MigrationPlan, MigrationProviderContext, } from "openclaw/plugin-sdk/plugin-entry"; +import { openNodeSqliteDatabase } from "openclaw/plugin-sdk/sqlite-runtime"; import { resolvePreferredOpenClawTmpDir, withTempWorkspace } from "openclaw/plugin-sdk/temp-path"; import { applyAuthItem } from "./auth.js"; import { applyConfigItem, applyManualItem } from "./config.js"; @@ -77,8 +78,7 @@ async function archiveHermesItem(item: MigrationItem, reportDir: string): Promis { rootDir: resolvePreferredOpenClawTmpDir(), prefix: HERMES_SQLITE_SNAPSHOT_PREFIX }, async ({ dir: tempDir }) => { const snapshotPath = path.join(tempDir, path.basename(sourcePath)); - const { DatabaseSync } = await import("node:sqlite"); - const source = new DatabaseSync(sourcePath, { readOnly: true }); + const source = openNodeSqliteDatabase(sourcePath, { readOnly: true }); try { source.exec("PRAGMA busy_timeout = 30000;"); source.prepare("VACUUM INTO ?").run(snapshotPath); diff --git a/extensions/qa-lab/src/fixture-utils.ts b/extensions/qa-lab/src/fixture-utils.ts index 2536be065449..18b3a15fb677 100644 --- a/extensions/qa-lab/src/fixture-utils.ts +++ b/extensions/qa-lab/src/fixture-utils.ts @@ -1,8 +1,9 @@ // Qa Lab plugin module provides reusable fixture utilities. import fs from "node:fs/promises"; import path from "node:path"; -import { DatabaseSync } from "node:sqlite"; +import type { DatabaseSync } from "node:sqlite"; import { clearTimeout as clearNodeTimeout, setTimeout as setNodeTimeout } from "node:timers"; +import { openNodeSqliteDatabase } from "openclaw/plugin-sdk/sqlite-runtime"; export type QaFixtureFetchJsonOptions = { fetchImpl?: (url: string, init: RequestInit) => Promise; @@ -331,7 +332,7 @@ function countNeedlesInSqliteTranscriptEvents( const counts = createCounts(needles); let db: DatabaseSync | null = null; try { - db = new DatabaseSync(sqlitePath, { readOnly: true }); + db = openNodeSqliteDatabase(sqlitePath, { readOnly: true }); const hasTranscriptEvents = db .prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'transcript_events'") .get(); diff --git a/extensions/qa-lab/src/live-transports/matrix/scenarios/scenario-runtime-state-files.ts b/extensions/qa-lab/src/live-transports/matrix/scenarios/scenario-runtime-state-files.ts index c9e45caa181e..c05064f0375c 100644 --- a/extensions/qa-lab/src/live-transports/matrix/scenarios/scenario-runtime-state-files.ts +++ b/extensions/qa-lab/src/live-transports/matrix/scenarios/scenario-runtime-state-files.ts @@ -3,6 +3,7 @@ import { createHash, randomUUID } from "node:crypto"; import fs from "node:fs/promises"; import path from "node:path"; import { setTimeout as sleep } from "node:timers/promises"; +import { openNodeSqliteDatabase } from "openclaw/plugin-sdk/sqlite-runtime"; import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime"; import type { MatrixQaScenarioContext } from "./scenario-runtime-shared.js"; @@ -183,46 +184,41 @@ async function readMatrixSyncCacheCursorsFromSqlite(params: { maxDepth: 10, }); const cursors: Array = []; - try { - const sqlite = await import("node:sqlite"); - for (const databasePath of databasePaths) { + for (const databasePath of databasePaths) { + try { + const db = openNodeSqliteDatabase(databasePath, { readOnly: true }); try { - const db = new sqlite.DatabaseSync(databasePath, { readOnly: true }); - try { - const rows = db - .prepare( - `SELECT entry_key AS entryKey, value_json AS valueJson + const rows = db + .prepare( + `SELECT entry_key AS entryKey, value_json AS valueJson FROM plugin_state_entries WHERE plugin_id = ? AND namespace = ? AND (expires_at IS NULL OR expires_at > ?)`, - ) - .all(MATRIX_PLUGIN_ID, MATRIX_SYNC_CACHE_NAMESPACE, Date.now()) as Array<{ - entryKey?: unknown; - valueJson?: unknown; - }>; - for (const cursor of readMatrixSyncCacheCursorFromRows(rows)) { - const storageRootDir = path.dirname(path.dirname(databasePath)); - cursors.push({ - ...cursor, - pathname: databasePath, - score: await scoreMatrixStateFile({ - context: params.context, - pathname: path.join(storageRootDir, MATRIX_SYNC_STORE_FILENAME), - ...(params.accountId ? { accountId: params.accountId } : {}), - ...(params.userId ? { userId: params.userId } : {}), - }), - }); - } - } finally { - db.close(); + ) + .all(MATRIX_PLUGIN_ID, MATRIX_SYNC_CACHE_NAMESPACE, Date.now()) as Array<{ + entryKey?: unknown; + valueJson?: unknown; + }>; + for (const cursor of readMatrixSyncCacheCursorFromRows(rows)) { + const storageRootDir = path.dirname(path.dirname(databasePath)); + cursors.push({ + ...cursor, + pathname: databasePath, + score: await scoreMatrixStateFile({ + context: params.context, + pathname: path.join(storageRootDir, MATRIX_SYNC_STORE_FILENAME), + ...(params.accountId ? { accountId: params.accountId } : {}), + ...(params.userId ? { userId: params.userId } : {}), + }), + }); } - } catch { - continue; + } finally { + db.close(); } + } catch { + continue; } - } catch { - return []; } return cursors .toSorted((a, b) => b.score - a.score || a.pathname.localeCompare(b.pathname)) @@ -258,8 +254,7 @@ async function rewriteMatrixSyncCacheRows(params: { pathname: string; stateKey: string; }) { - const sqlite = await import("node:sqlite"); - const db = new sqlite.DatabaseSync(params.pathname); + const db = openNodeSqliteDatabase(params.pathname); try { const rows = db .prepare( @@ -377,8 +372,7 @@ export async function deleteMatrixSyncStoreCursor(params: MatrixSyncStoreCursor) await fs.rm(params.pathname, { force: true }); return; } - const sqlite = await import("node:sqlite"); - const db = new sqlite.DatabaseSync(params.pathname); + const db = openNodeSqliteDatabase(params.pathname); try { db.prepare( `DELETE FROM plugin_state_entries @@ -507,15 +501,9 @@ async function hasPersistedMatrixPluginStateDedupeEntry(params: { rootDir: params.stateDir, maxDepth: 10, }); - let sqlite: typeof import("node:sqlite"); - try { - sqlite = await import("node:sqlite"); - } catch { - return null; - } for (const databasePath of databasePaths) { try { - const db = new sqlite.DatabaseSync(databasePath, { readOnly: true }); + const db = openNodeSqliteDatabase(databasePath, { readOnly: true }); try { const rows = db .prepare( diff --git a/extensions/workboard/src/sqlite-store.ts b/extensions/workboard/src/sqlite-store.ts index 39520e639c86..c75f274529e1 100644 --- a/extensions/workboard/src/sqlite-store.ts +++ b/extensions/workboard/src/sqlite-store.ts @@ -1,6 +1,6 @@ import fs from "node:fs"; import path from "node:path"; -import { DatabaseSync, type SQLInputValue } from "node:sqlite"; +import type { DatabaseSync, SQLInputValue } from "node:sqlite"; import type { WorkboardArtifact, WorkboardAttachment, @@ -20,6 +20,7 @@ import { configureSqliteConnectionPragmas, migrateSqliteSchemaToStrict, } from "openclaw/plugin-sdk/plugin-state-runtime"; +import { openNodeSqliteDatabase } from "openclaw/plugin-sdk/sqlite-runtime"; import { resolveStateDir } from "openclaw/plugin-sdk/state-paths"; import type { PersistedWorkboardAttachment, @@ -399,7 +400,7 @@ function createDatabase(dbPath: string): { if (!fs.existsSync(dbPath)) { fs.closeSync(fs.openSync(dbPath, "a", WORKBOARD_SQLITE_FILE_MODE)); } - const db = new DatabaseSync(dbPath); + const db = openNodeSqliteDatabase(dbPath); let maintenance: ReturnType | undefined; try { maintenance = configureSqliteConnectionPragmas(db, { diff --git a/scripts/check-kysely-guardrails.mjs b/scripts/check-kysely-guardrails.mjs index dd3422f2db1e..623114f1b7f5 100644 --- a/scripts/check-kysely-guardrails.mjs +++ b/scripts/check-kysely-guardrails.mjs @@ -18,6 +18,13 @@ const ts = require("typescript"); const repoRoot = resolveRepoRoot(import.meta.url); const sourceRoots = [path.join(repoRoot, "src")]; +const nodeSqliteBoundaryRoots = [ + path.join(repoRoot, "src"), + path.join(repoRoot, "extensions"), + path.join(repoRoot, "packages"), +]; + +const nodeSqliteConstructorOwnerPaths = new Set(["src/infra/node-sqlite.ts"]); const kyselyRawAllowPaths = new Set(["src/infra/kysely-sync.ts"]); @@ -235,6 +242,63 @@ function isSqliteStorePath(relativePath) { return relativePath.endsWith(".sqlite.ts") || relativePath.includes(".store.sqlite.ts"); } +function collectNodeSqliteBoundaryViolations(content, relativePath) { + if (isTestPath(relativePath) || nodeSqliteConstructorOwnerPaths.has(relativePath)) { + return []; + } + const sourceFile = ts.createSourceFile(relativePath, content, ts.ScriptTarget.Latest, true); + const constructorNames = new Set(); + + function collectConstructorNames(node) { + if (ts.isImportDeclaration(node) && importSource(node) === "node:sqlite") { + const namedBindings = node.importClause?.namedBindings; + if (namedBindings && ts.isNamedImports(namedBindings)) { + for (const element of namedBindings.elements) { + if ((element.propertyName?.text ?? element.name.text) === "DatabaseSync") { + constructorNames.add(element.name.text); + } + } + } + } + if (ts.isVariableDeclaration(node) && ts.isObjectBindingPattern(node.name)) { + for (const element of node.name.elements) { + if ( + !element.dotDotDotToken && + ts.isIdentifier(element.name) && + (element.propertyName ? getPropertyNameText(element.propertyName) : element.name.text) === + "DatabaseSync" + ) { + constructorNames.add(element.name.text); + } + } + } + ts.forEachChild(node, collectConstructorNames); + } + + collectConstructorNames(sourceFile); + const violations = []; + function visit(node) { + if (ts.isNewExpression(node)) { + const expression = unwrapExpression(node.expression); + const isRawConstructor = + (ts.isIdentifier(expression) && constructorNames.has(expression.text)) || + (ts.isPropertyAccessExpression(expression) && + getPropertyNameText(expression.name) === "DatabaseSync"); + if (isRawConstructor) { + addViolation( + violations, + sourceFile, + node, + "production node:sqlite connections must use openNodeSqliteDatabase", + ); + } + } + ts.forEachChild(node, visit); + } + visit(sourceFile); + return violations; +} + function isLikelySqliteReceiver(expression) { const unwrapped = unwrapExpression(expression); if (ts.isIdentifier(unwrapped)) { @@ -403,6 +467,16 @@ async function collectKyselyGuardrails() { violations.push({ path: relativePath, ...violation }); } } + const nodeSqliteFiles = await collectTypeScriptFilesFromRoots(nodeSqliteBoundaryRoots, { + includeTests: false, + }); + for (const filePath of nodeSqliteFiles) { + const relativePath = path.relative(repoRoot, filePath).split(path.sep).join("/"); + const content = await fs.readFile(filePath, "utf8"); + for (const violation of collectNodeSqliteBoundaryViolations(content, relativePath)) { + violations.push({ path: relativePath, ...violation }); + } + } return violations; } diff --git a/src/agents/auth-profiles/sqlite.ts b/src/agents/auth-profiles/sqlite.ts index 5815ce6d209b..f43d3b1490bb 100644 --- a/src/agents/auth-profiles/sqlite.ts +++ b/src/agents/auth-profiles/sqlite.ts @@ -13,7 +13,7 @@ import { executeSqliteQueryTakeFirstSync, getNodeSqliteKysely, } from "../../infra/kysely-sync.js"; -import { requireNodeSqlite, resolveNodeSqliteLocation } from "../../infra/node-sqlite.js"; +import { openNodeSqliteDatabase } from "../../infra/node-sqlite.js"; import { resolveSqliteDatabaseFilePaths } from "../../infra/sqlite-files.js"; import { readSqliteUserVersion } from "../../infra/sqlite-user-version.js"; import type { DB as OpenClawAgentKyselyDatabase } from "../../state/openclaw-agent-db.generated.js"; @@ -102,10 +102,9 @@ function inspectAuthProfileJsonCellReadOnly( pathname: string, target: "store" | "state", ): PersistedAuthProfileStoreInspection { - const sqlite = requireNodeSqlite(); let db: DatabaseSync | undefined; try { - db = new sqlite.DatabaseSync(resolveNodeSqliteLocation(pathname), { readOnly: true }); + db = openNodeSqliteDatabase(pathname, { readOnly: true }); // This short-lived reader bypasses the canonical agent DB bootstrap, but it // must share its busy policy so brief rollback-journal locks do not look // like missing credentials. diff --git a/src/commands/backup-verify.ts b/src/commands/backup-verify.ts index 50ca90a2b16f..1a2ac2ee6b07 100644 --- a/src/commands/backup-verify.ts +++ b/src/commands/backup-verify.ts @@ -7,7 +7,7 @@ import { readStringValue } from "@openclaw/normalization-core/string-coerce"; import * as tar from "tar"; import { loadSqliteVecExtension } from "../../packages/memory-host-sdk/src/engine-storage.js"; import { formatDiskSpaceBytes, tryReadDiskSpace } from "../infra/disk-space.js"; -import { requireNodeSqlite, resolveNodeSqliteLocation } from "../infra/node-sqlite.js"; +import { openNodeSqliteDatabase } from "../infra/node-sqlite.js"; import { assertSqliteIntegrity } from "../infra/sqlite-integrity.js"; import { type RuntimeEnv, writeRuntimeJson } from "../runtime.js"; import { isRecord, resolveUserPath } from "../utils.js"; @@ -690,8 +690,7 @@ async function verifySqliteSnapshots(params: { // snapshot shape, but only canonical schemas are safe to interpret. continue; } - const sqlite = requireNodeSqlite(); - database = new sqlite.DatabaseSync(resolveNodeSqliteLocation(extractedPath), { + database = openNodeSqliteDatabase(extractedPath, { allowExtension: true, readOnly: true, }); diff --git a/src/commands/doctor-db-bloat.ts b/src/commands/doctor-db-bloat.ts index b659cfea8c92..d89109fef1a4 100644 --- a/src/commands/doctor-db-bloat.ts +++ b/src/commands/doctor-db-bloat.ts @@ -2,9 +2,10 @@ // Registered size_bytes existed for a while with no reader; production bloat // (multi-hundred-MB stores, blocking vacuums) surfaced only after user harm. import fs from "node:fs"; +import type { DatabaseSync } from "node:sqlite"; import { note } from "../../packages/terminal-core/src/note.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; -import { requireNodeSqlite, resolveNodeSqliteLocation } from "../infra/node-sqlite.js"; +import { openNodeSqliteDatabase } from "../infra/node-sqlite.js"; import { listOpenClawRegisteredAgentDatabases } from "../state/openclaw-agent-db.js"; import { resolveOpenClawStateSqlitePath } from "../state/openclaw-state-db.paths.js"; import { formatBytes } from "./doctor-disk-space.js"; @@ -34,10 +35,9 @@ function readSqliteBloatStats(pathname: string): SqliteBloatStats | null { if (fileBytes <= 0) { return null; } - const sqlite = requireNodeSqlite(); - let db: InstanceType | undefined; + let db: DatabaseSync | undefined; try { - db = new sqlite.DatabaseSync(resolveNodeSqliteLocation(pathname), { readOnly: true }); + db = openNodeSqliteDatabase(pathname, { readOnly: true }); const pageSize = readPragmaNumber(db, "page_size") ?? 4096; const freelistCount = readPragmaNumber(db, "freelist_count") ?? 0; const autoVacuum = readPragmaNumber(db, "auto_vacuum") ?? 0; diff --git a/src/commands/doctor-session-sqlite-readers.ts b/src/commands/doctor-session-sqlite-readers.ts index 5b14a22f3451..a3d528f2c00d 100644 --- a/src/commands/doctor-session-sqlite-readers.ts +++ b/src/commands/doctor-session-sqlite-readers.ts @@ -1,5 +1,6 @@ /** Read-only diagnostic readers used by the session SQLite doctor mode. */ import fs from "node:fs"; +import type { DatabaseSync } from "node:sqlite"; import { TextDecoder } from "node:util"; import { isRecord } from "@openclaw/normalization-core/record-coerce"; import { normalizeLoadedFileEntry, type FileEntry } from "../agents/sessions/session-manager.js"; @@ -7,7 +8,7 @@ import type { TranscriptEvent } from "../config/sessions/session-accessor.js"; import { resolveSqliteTargetFromSessionStorePath } from "../config/sessions/session-sqlite-target.js"; import type { SessionStoreTarget } from "../config/sessions/targets.js"; import type { SessionEntry } from "../config/sessions/types.js"; -import { requireNodeSqlite, resolveNodeSqliteLocation } from "../infra/node-sqlite.js"; +import { openNodeSqliteDatabase } from "../infra/node-sqlite.js"; import { resolveOpenClawAgentSqlitePath } from "../state/openclaw-agent-db.js"; type ReadOnlySqliteSessionSummary = { @@ -129,12 +130,9 @@ export function readOnlySqliteSessionEntries( if (!fs.existsSync(sqlitePath)) { return { exists: false, ok: true, summaries: [] }; } - const sqlite = requireNodeSqlite(); - let database: InstanceType | undefined; + let database: DatabaseSync | undefined; try { - database = new sqlite.DatabaseSync(resolveNodeSqliteLocation(sqlitePath), { - readOnly: true, - }); + database = openNodeSqliteDatabase(sqlitePath, { readOnly: true }); const nodeTable = database .prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?") .get("session_nodes"); @@ -179,12 +177,9 @@ export function readOnlySqliteTranscriptEventCount( if (!fs.existsSync(sqlitePath)) { return { events: 0, exists: false, ok: true }; } - const sqlite = requireNodeSqlite(); - let database: InstanceType | undefined; + let database: DatabaseSync | undefined; try { - database = new sqlite.DatabaseSync(resolveNodeSqliteLocation(sqlitePath), { - readOnly: true, - }); + database = openNodeSqliteDatabase(sqlitePath, { readOnly: true }); const table = database .prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?") .get("transcript_events"); @@ -227,12 +222,9 @@ export function readOnlySqliteDbStats(target: SessionStoreTarget): ReadOnlySqlit }, }; } - const sqlite = requireNodeSqlite(); - let database: InstanceType | undefined; + let database: DatabaseSync | undefined; try { - database = new sqlite.DatabaseSync(resolveNodeSqliteLocation(sqlitePath), { - readOnly: true, - }); + database = openNodeSqliteDatabase(sqlitePath, { readOnly: true }); const hasTranscriptEvents = database .prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?") .get("transcript_events"); diff --git a/src/commands/doctor-session-sqlite-recover-report.ts b/src/commands/doctor-session-sqlite-recover-report.ts index 3374accfef7e..a40a9b7d7499 100644 --- a/src/commands/doctor-session-sqlite-recover-report.ts +++ b/src/commands/doctor-session-sqlite-recover-report.ts @@ -4,7 +4,7 @@ import os from "node:os"; import path from "node:path"; import type { DatabaseSync } from "node:sqlite"; import type { SessionStoreTarget } from "../config/sessions/targets.js"; -import { requireNodeSqlite, resolveNodeSqliteLocation } from "../infra/node-sqlite.js"; +import { openNodeSqliteDatabase } from "../infra/node-sqlite.js"; import { resolveSqliteDatabaseFilePaths } from "../infra/sqlite-files.js"; import { assertSqliteIntegrity } from "../infra/sqlite-integrity.js"; import { getCanonicalSqliteNamedIndexContracts } from "../infra/sqlite-schema-contract.js"; @@ -180,7 +180,6 @@ function inspectSqliteForRecovery( let database: DatabaseSync | undefined; let inspectionError: unknown; try { - const sqlite = requireNodeSqlite(); inspectionDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-sqlite-recovery-")); const inspectionPath = path.join(inspectionDir, path.basename(sqlitePath)); for (const sourcePath of sourcePaths) { @@ -191,7 +190,7 @@ function inspectSqliteForRecovery( } // Writable inspection of the disposable copy lets SQLite roll back a hot // journal without changing the original forensic file set. - database = new sqlite.DatabaseSync(resolveNodeSqliteLocation(inspectionPath)); + database = openNodeSqliteDatabase(inspectionPath); database.exec(`PRAGMA busy_timeout = ${OPENCLAW_SQLITE_BUSY_TIMEOUT_MS};`); database.exec("PRAGMA trusted_schema = OFF;"); assertSqliteIntegrity(database, inspectionPath); diff --git a/src/commands/doctor-session-sqlite.test.ts b/src/commands/doctor-session-sqlite.test.ts index 22b5ff9c5fcc..e9003714ef9a 100644 --- a/src/commands/doctor-session-sqlite.test.ts +++ b/src/commands/doctor-session-sqlite.test.ts @@ -2530,7 +2530,7 @@ describe("runDoctorSessionSqlite", () => { ); fs.mkdirSync(path.dirname(sqlitePath), { recursive: true }); fs.writeFileSync(sqlitePath, "not a sqlite database\n", { mode: 0o600 }); - const requireSqlite = vi.spyOn(nodeSqlite, "requireNodeSqlite").mockImplementationOnce(() => { + const openSqlite = vi.spyOn(nodeSqlite, "openNodeSqliteDatabase").mockImplementationOnce(() => { throw new Error("node:sqlite unavailable"); }); @@ -2542,7 +2542,7 @@ describe("runDoctorSessionSqlite", () => { store: store.storePath, }); } finally { - requireSqlite.mockRestore(); + openSqlite.mockRestore(); } expect(report?.totals.issues).toBe(1); diff --git a/src/commands/doctor-sqlite-compact.ts b/src/commands/doctor-sqlite-compact.ts index 2c5adb4ed22c..b7457250be21 100644 --- a/src/commands/doctor-sqlite-compact.ts +++ b/src/commands/doctor-sqlite-compact.ts @@ -1,7 +1,7 @@ /** Shared doctor-only SQLite compaction mechanics. */ import fs from "node:fs"; import type { DatabaseSync } from "node:sqlite"; -import { requireNodeSqlite, resolveNodeSqliteLocation } from "../infra/node-sqlite.js"; +import { openNodeSqliteDatabase } from "../infra/node-sqlite.js"; import { assertSqliteIntegrity } from "../infra/sqlite-integrity.js"; import { OPENCLAW_SQLITE_BUSY_TIMEOUT_MS } from "../state/openclaw-state-db.js"; @@ -37,8 +37,7 @@ type DoctorSqliteCompactOptions = { export function compactDoctorSqliteFile( options: DoctorSqliteCompactOptions, ): DoctorSqliteCompactResult { - const sqlite = requireNodeSqlite(); - const database = new sqlite.DatabaseSync(resolveNodeSqliteLocation(options.sqlitePath)); + const database = openNodeSqliteDatabase(options.sqlitePath); let operationError: unknown; let result: DoctorSqliteCompactResult | undefined; try { diff --git a/src/commands/doctor/cron/migration-ledger.ts b/src/commands/doctor/cron/migration-ledger.ts index 10a3424f2a40..9d1f70ea77c9 100644 --- a/src/commands/doctor/cron/migration-ledger.ts +++ b/src/commands/doctor/cron/migration-ledger.ts @@ -6,7 +6,7 @@ import { executeSqliteQueryTakeFirstSync, getNodeSqliteKysely, } from "../../../infra/kysely-sync.js"; -import { requireNodeSqlite, resolveNodeSqliteLocation } from "../../../infra/node-sqlite.js"; +import { openNodeSqliteDatabase } from "../../../infra/node-sqlite.js"; import type { DB as OpenClawStateDatabase } from "../../../state/openclaw-state-db.generated.js"; import { openOpenClawStateDatabase, @@ -52,8 +52,7 @@ export function hasLegacyCronMigrationReceiptReadOnly(source: LegacyCronMigratio if (!fs.existsSync(statePath)) { return false; } - const sqlite = requireNodeSqlite(); - const db = new sqlite.DatabaseSync(resolveNodeSqliteLocation(statePath), { readOnly: true }); + const db = openNodeSqliteDatabase(statePath, { readOnly: true }); try { if (!tableExists(db, "migration_sources")) { return false; diff --git a/src/commands/status.scan.shared.ts b/src/commands/status.scan.shared.ts index 1f327734ac00..11949b1a4ebb 100644 --- a/src/commands/status.scan.shared.ts +++ b/src/commands/status.scan.shared.ts @@ -18,7 +18,7 @@ import { buildGatewayConnectionDetailsWithResolvers } from "../gateway/connectio import { normalizeControlUiBasePath } from "../gateway/control-ui-shared.js"; import { resolveGatewayProbeTarget } from "../gateway/probe-target.js"; import type { GatewayProbeResult, probeGateway as probeGatewayFn } from "../gateway/probe.js"; -import { requireNodeSqlite, resolveNodeSqliteLocation } from "../infra/node-sqlite.js"; +import { openNodeSqliteDatabase } from "../infra/node-sqlite.js"; import { MEMORY_INDEX_CHUNKS_TABLE, MEMORY_INDEX_META_TABLE, @@ -53,10 +53,9 @@ function hasBuiltInMemoryState(databasePath: string): boolean { if (!existsSync(databasePath)) { return false; } - const { DatabaseSync } = requireNodeSqlite(); let db: DatabaseSync | undefined; try { - db = new DatabaseSync(resolveNodeSqliteLocation(databasePath), { readOnly: true }); + db = openNodeSqliteDatabase(databasePath, { readOnly: true }); const builtInMemoryTableSets = [ { meta: MEMORY_INDEX_META_TABLE, diff --git a/src/cron/store.ts b/src/cron/store.ts index 3c3ca223ef29..d070b374c7b5 100644 --- a/src/cron/store.ts +++ b/src/cron/store.ts @@ -5,7 +5,7 @@ import type { DatabaseSync } from "node:sqlite"; import { isRecord } from "@openclaw/normalization-core/record-coerce"; import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; import { expandHomePrefix } from "../infra/home-dir.js"; -import { requireNodeSqlite, resolveNodeSqliteLocation } from "../infra/node-sqlite.js"; +import { openNodeSqliteDatabase } from "../infra/node-sqlite.js"; import { replaceFileAtomic } from "../infra/replace-file.js"; import { openOpenClawStateDatabase, @@ -120,8 +120,7 @@ export async function loadCronJobsStoreWithConfigJobsReadOnly( } const resolvedStorePath = path.resolve(storePath); const storeKey = cronStoreKey(resolvedStorePath); - const sqlite = requireNodeSqlite(); - const db = new sqlite.DatabaseSync(resolveNodeSqliteLocation(statePath), { readOnly: true }); + const db = openNodeSqliteDatabase(statePath, { readOnly: true }); try { if (!tableExists(db, "cron_jobs")) { return emptyLoadedCronStore(); diff --git a/src/infra/device-identity-coordinator.ts b/src/infra/device-identity-coordinator.ts index 4cb5e79ed2f4..296d680adadf 100644 --- a/src/infra/device-identity-coordinator.ts +++ b/src/infra/device-identity-coordinator.ts @@ -2,7 +2,7 @@ import crypto from "node:crypto"; import fs from "node:fs"; import path from "node:path"; import { resolveGatewayLockDir } from "../config/paths.js"; -import { requireNodeSqlite, resolveNodeSqliteLocation } from "./node-sqlite.js"; +import { openNodeSqliteDatabase } from "./node-sqlite.js"; const DEFAULT_BUSY_TIMEOUT_MS = 5000; @@ -94,8 +94,7 @@ export function acquireDeviceIdentityCoordinator(params: { }): { release: () => void } { const coordinatorPath = resolveDeviceIdentityCoordinatorPath(params.databasePath, params.lockDir); ensurePrivateCoordinatorDirectory(path.dirname(coordinatorPath)); - const { DatabaseSync } = requireNodeSqlite(); - const database = new DatabaseSync(resolveNodeSqliteLocation(coordinatorPath)); + const database = openNodeSqliteDatabase(coordinatorPath); try { const timeout = Math.max(0, Math.trunc(params.busyTimeoutMs ?? DEFAULT_BUSY_TIMEOUT_MS)); database.exec(`PRAGMA busy_timeout = ${timeout}; BEGIN EXCLUSIVE;`); diff --git a/src/infra/gateway-lock.ts b/src/infra/gateway-lock.ts index d69a064aa11d..04be071b59dd 100644 --- a/src/infra/gateway-lock.ts +++ b/src/infra/gateway-lock.ts @@ -16,7 +16,7 @@ import { getFileLockProcessStartTime, isPidAlive } from "../shared/pid-alive.js" import { safeParseJsonWithSchema } from "../utils/zod-parse.js"; import { sha256HexPrefix } from "./crypto-digest.js"; import { isGatewayArgv, isOpenClawCommandArgv, parseProcCmdline } from "./gateway-process-argv.js"; -import { requireNodeSqlite, resolveNodeSqliteLocation } from "./node-sqlite.js"; +import { openNodeSqliteDatabase } from "./node-sqlite.js"; import { isSqliteLockError } from "./sqlite-transaction.js"; import { readWindowsProcessArgsSync, @@ -115,10 +115,7 @@ type GatewayLockCoordinator = { }; function tryAcquireGatewayLockCoordinator(lockPath: string): GatewayLockCoordinator | null { - const { DatabaseSync } = requireNodeSqlite(); - const coordinatorDb: DatabaseSync = new DatabaseSync( - resolveNodeSqliteLocation(`${lockPath}.sqlite`), - ); + const coordinatorDb: DatabaseSync = openNodeSqliteDatabase(`${lockPath}.sqlite`); try { coordinatorDb.exec("PRAGMA busy_timeout = 0; BEGIN EXCLUSIVE;"); } catch (error) { diff --git a/src/infra/node-sqlite.test.ts b/src/infra/node-sqlite.test.ts index 109263a04d60..e2eb9ae5bcd2 100644 --- a/src/infra/node-sqlite.test.ts +++ b/src/infra/node-sqlite.test.ts @@ -3,7 +3,11 @@ import path from "node:path"; import { DatabaseSync, type StatementSync } from "node:sqlite"; import { pathToFileURL } from "node:url"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { resolveNodeSqliteLocation, resolveNodeSqliteReadOnlyLocation } from "./node-sqlite.js"; +import { + openNodeSqliteDatabase, + resolveNodeSqliteLocation, + resolveNodeSqliteReadOnlyLocation, +} from "./node-sqlite.js"; const originalPrepare = Reflect.get(DatabaseSync.prototype, "prepare") as DatabaseSync["prepare"]; @@ -73,6 +77,15 @@ describe("node SQLite locations", () => { expect(resolveNodeSqliteLocation("relative/openclaw.sqlite")).toBe("relative/openclaw.sqlite"); }); + it("opens special locations through the shared connection boundary", () => { + const database = openNodeSqliteDatabase(":memory:"); + try { + expect(database.prepare("SELECT 1 AS ok").get()).toEqual({ ok: 1 }); + } finally { + database.close(); + } + }); + it("normalizes ordinary filesystem paths through the Windows VFS boundary", () => { vi.spyOn(process, "platform", "get").mockReturnValue("win32"); const resolveSpy = vi.spyOn(path, "resolve").mockReturnValue("resolved-openclaw.sqlite"); @@ -99,6 +112,8 @@ describe("node SQLite locations", () => { expect(resolveNodeSqliteReadOnlyLocation(pathname, true)).toBe( resolveNodeSqliteLocation(pathname), ); + const immutableLocation = resolveNodeSqliteReadOnlyLocation(pathname, false); + expect(resolveNodeSqliteLocation(immutableLocation)).toBe(immutableLocation); }); it("keeps UNC and namespaced Windows paths out of SQLite URI authority parsing", () => { @@ -127,10 +142,12 @@ describe("node SQLite locations", () => { .mockImplementation((pathname) => pathname); for (const [pathname, resolvedPath] of resolvedPaths) { - expect(resolveNodeSqliteReadOnlyLocation(pathname, false)).toBe(resolvedPath); + const readOnlyLocation = resolveNodeSqliteReadOnlyLocation(pathname, false); + expect(readOnlyLocation).toBe(resolvedPath); + expect(resolveNodeSqliteLocation(readOnlyLocation)).toBe(resolvedPath); } - expect(resolveSpy).toHaveBeenCalledTimes(resolvedPaths.size); - expect(namespacedSpy).toHaveBeenCalledTimes(resolvedPaths.size); + expect(resolveSpy).toHaveBeenCalledTimes(resolvedPaths.size * 2); + expect(namespacedSpy).toHaveBeenCalledTimes(resolvedPaths.size * 2); }); }); diff --git a/src/infra/node-sqlite.ts b/src/infra/node-sqlite.ts index 2d8927e126f0..55de19f86b36 100644 --- a/src/infra/node-sqlite.ts +++ b/src/infra/node-sqlite.ts @@ -9,6 +9,10 @@ import { installProcessWarningFilter } from "./warning-filter.js"; const require = createRequire(import.meta.url); let validatedSqliteModule: typeof import("node:sqlite") | undefined; +type NodeSqliteDatabaseOptions = ConstructorParameters< + typeof import("node:sqlite").DatabaseSync +>[1]; + export function resolveSqliteFilesystemPath(pathname: string): string { if (process.platform !== "win32") { return pathname; @@ -98,3 +102,17 @@ export function requireNodeSqlite(): typeof import("node:sqlite") { }); } } + +/** Open node:sqlite through OpenClaw's runtime and filesystem-location boundary. */ +export function openNodeSqliteDatabase( + location: string, + options?: NodeSqliteDatabaseOptions, +): import("node:sqlite").DatabaseSync { + const sqlite = requireNodeSqlite(); + // Callers may pass file: URIs or already-namespaced paths from specialized + // resolvers; location normalization must remain idempotent for those forms. + const resolvedLocation = resolveNodeSqliteLocation(location); + return options === undefined + ? new sqlite.DatabaseSync(resolvedLocation) + : new sqlite.DatabaseSync(resolvedLocation, options); +} diff --git a/src/infra/sqlite-schema-contract.ts b/src/infra/sqlite-schema-contract.ts index a3ab134b1060..e0ba573fe42d 100644 --- a/src/infra/sqlite-schema-contract.ts +++ b/src/infra/sqlite-schema-contract.ts @@ -1,5 +1,5 @@ import type { DatabaseSync } from "node:sqlite"; -import { requireNodeSqlite } from "./node-sqlite.js"; +import { openNodeSqliteDatabase } from "./node-sqlite.js"; type SqliteIndexListRow = { name: string; @@ -265,8 +265,7 @@ function getSqliteSchemaContract(schemaSql: string): SqliteSchemaContract { } function buildSqliteSchemaContract(schemaSql: string): SqliteSchemaContract { - const sqlite = requireNodeSqlite(); - const database = new sqlite.DatabaseSync(":memory:"); + const database = openNodeSqliteDatabase(":memory:"); try { database.exec(schemaSql); const rows = database diff --git a/src/infra/sqlite-snapshot.ts b/src/infra/sqlite-snapshot.ts index 219b528c7716..b3ca35dc04ed 100644 --- a/src/infra/sqlite-snapshot.ts +++ b/src/infra/sqlite-snapshot.ts @@ -10,8 +10,8 @@ import { runExec } from "../process/exec.js"; import { formatErrorMessage } from "./errors.js"; import { sameFileIdentity } from "./fs-safe-advanced.js"; import { + openNodeSqliteDatabase, requireNodeSqlite, - resolveNodeSqliteLocation, resolveSqliteFilesystemPath, } from "./node-sqlite.js"; import { resolveSystemBin } from "./resolve-system-bin.js"; @@ -821,7 +821,7 @@ export async function createVerifiedSqliteSnapshot( const sqlite = requireNodeSqlite(); let stagedIdentity: Stats | undefined; try { - const source = new sqlite.DatabaseSync(resolveNodeSqliteLocation(options.sourcePath), { + const source = openNodeSqliteDatabase(options.sourcePath, { allowExtension: true, readOnly: true, }); @@ -838,7 +838,7 @@ export async function createVerifiedSqliteSnapshot( } await fs.chmod(stagedPath, 0o600); - const snapshot = new sqlite.DatabaseSync(resolveNodeSqliteLocation(stagedPath), { + const snapshot = openNodeSqliteDatabase(stagedPath, { allowExtension: true, }); try { @@ -868,7 +868,7 @@ export async function createVerifiedSqliteSnapshot( beforePublish: options.beforePublish, afterPublish: options.afterPublish, validatePublished: async (publishedPath) => { - const published = new sqlite.DatabaseSync(resolveNodeSqliteLocation(publishedPath), { + const published = openNodeSqliteDatabase(publishedPath, { allowExtension: true, readOnly: true, }); diff --git a/src/infra/sqlite-strict.ts b/src/infra/sqlite-strict.ts index 8c2fb84957e2..757715fe9598 100644 --- a/src/infra/sqlite-strict.ts +++ b/src/infra/sqlite-strict.ts @@ -1,6 +1,6 @@ // Migrates OpenClaw-owned SQLite tables to canonical STRICT schemas. import type { DatabaseSync } from "node:sqlite"; -import { requireNodeSqlite } from "./node-sqlite.js"; +import { openNodeSqliteDatabase } from "./node-sqlite.js"; import { assertSqliteIntegrity } from "./sqlite-integrity.js"; import { runSqliteImmediateTransactionSync } from "./sqlite-transaction.js"; @@ -126,8 +126,7 @@ function readTableRowidModel( } function readCanonicalStrictTables(schemaSql: string): CanonicalStrictTable[] { - const sqlite = requireNodeSqlite(); - const canonical = new sqlite.DatabaseSync(":memory:"); + const canonical = openNodeSqliteDatabase(":memory:"); try { canonical.exec(schemaSql); const tables = readMainTableList(canonical).filter((row) => row.type === "table"); diff --git a/src/infra/state-migrations.debug-proxy.ts b/src/infra/state-migrations.debug-proxy.ts index f28188f8693b..556f2dead00d 100644 --- a/src/infra/state-migrations.debug-proxy.ts +++ b/src/infra/state-migrations.debug-proxy.ts @@ -6,7 +6,7 @@ import { gunzipSync } from "node:zlib"; import { runOpenClawStateWriteTransaction } from "../state/openclaw-state-db.js"; import { resolveOpenClawStateSqlitePath } from "../state/openclaw-state-db.paths.js"; import { sha256Hex } from "./crypto-digest.js"; -import { requireNodeSqlite, resolveNodeSqliteLocation } from "./node-sqlite.js"; +import { openNodeSqliteDatabase } from "./node-sqlite.js"; const DEBUG_PROXY_SQLITE_SIDECAR_SUFFIXES = ["", "-shm", "-wal", "-journal"] as const; @@ -153,8 +153,7 @@ function readLegacyDebugProxyCapture(params: { sourcePath: string; blobDir: stri blobs: LegacyCaptureBlobRow[]; blobDirs: string[]; } { - const sqlite = requireNodeSqlite(); - const db = new sqlite.DatabaseSync(resolveNodeSqliteLocation(params.sourcePath), { + const db = openNodeSqliteDatabase(params.sourcePath, { readOnly: true, }); try { diff --git a/src/infra/state-migrations.meeting-transcripts-detection.ts b/src/infra/state-migrations.meeting-transcripts-detection.ts index 9d743abd9d23..3179a4bd6b12 100644 --- a/src/infra/state-migrations.meeting-transcripts-detection.ts +++ b/src/infra/state-migrations.meeting-transcripts-detection.ts @@ -1,9 +1,8 @@ // Doctor detection for legacy meeting transcript files and interrupted imports. import fs from "node:fs"; import path from "node:path"; -import { DatabaseSync } from "node:sqlite"; import { resolveOpenClawStateSqlitePath } from "../state/openclaw-state-db.paths.js"; -import { resolveNodeSqliteLocation } from "./node-sqlite.js"; +import { openNodeSqliteDatabase } from "./node-sqlite.js"; import { hasMatchingRecordedTranscriptArtifact, isRecordedCanonicalTranscriptExport, @@ -193,7 +192,7 @@ export function readMeetingTranscriptMigrationDetectionState(params: { pendingImportCount: 0, }; } - const database = new DatabaseSync(resolveNodeSqliteLocation(databasePath), { readOnly: true }); + const database = openNodeSqliteDatabase(databasePath, { readOnly: true }); try { const tables = new Set( database diff --git a/src/infra/state-migrations.meeting-transcripts-files.ts b/src/infra/state-migrations.meeting-transcripts-files.ts index e87f774e7ee1..a2a5e0d13b05 100644 --- a/src/infra/state-migrations.meeting-transcripts-files.ts +++ b/src/infra/state-migrations.meeting-transcripts-files.ts @@ -4,7 +4,7 @@ import fsSync, { createReadStream } from "node:fs"; import fs from "node:fs/promises"; import path from "node:path"; import { createInterface } from "node:readline"; -import { DatabaseSync } from "node:sqlite"; +import type { DatabaseSync } from "node:sqlite"; import type { TranscriptSessionDescriptor, TranscriptUtterance, @@ -13,7 +13,7 @@ import type { TranscriptsSummary } from "../transcripts/summary.js"; import { renderTranscriptsMarkdown } from "../transcripts/summary.js"; import { sha256File, sha256Hex } from "./crypto-digest.js"; import { assertNoSymlinkParents } from "./fs-safe-advanced.js"; -import { resolveNodeSqliteLocation } from "./node-sqlite.js"; +import { openNodeSqliteDatabase } from "./node-sqlite.js"; const TRANSCRIPT_EXPORT_FILE_NAMES = new Set([ "metadata.json", @@ -207,7 +207,7 @@ async function optionalRegularFile(filePath: string): Promise { } export function openLegacyMeetingTranscriptStage(databasePath: string): DatabaseSync { - const database = new DatabaseSync(resolveNodeSqliteLocation(databasePath)); + const database = openNodeSqliteDatabase(databasePath); database.exec(` PRAGMA journal_mode = WAL; CREATE TABLE staged_utterances ( diff --git a/src/infra/state-migrations.meeting-transcripts.ts b/src/infra/state-migrations.meeting-transcripts.ts index f5662eba17f6..fad7e3710ace 100644 --- a/src/infra/state-migrations.meeting-transcripts.ts +++ b/src/infra/state-migrations.meeting-transcripts.ts @@ -2,7 +2,7 @@ import { randomUUID } from "node:crypto"; // Doctor-only import for the retired meeting-capture JSON/JSONL store. import fsSync from "node:fs"; import path from "node:path"; -import { DatabaseSync } from "node:sqlite"; +import type { DatabaseSync } from "node:sqlite"; import { openOpenClawStateDatabase, runOpenClawStateWriteTransaction, diff --git a/src/infra/state-migrations.storage.ts b/src/infra/state-migrations.storage.ts index 3991d77946d9..da5bd30b491a 100644 --- a/src/infra/state-migrations.storage.ts +++ b/src/infra/state-migrations.storage.ts @@ -10,7 +10,7 @@ import { type InstalledPluginIndex, } from "../plugins/installed-plugin-index.js"; import { runOpenClawStateWriteTransaction } from "../state/openclaw-state-db.js"; -import { requireNodeSqlite, resolveNodeSqliteLocation } from "./node-sqlite.js"; +import { openNodeSqliteDatabase } from "./node-sqlite.js"; import { parseRegistryNpmSpec } from "./npm-registry-spec.js"; import { fileExists, safeReadDir } from "./state-migrations.fs.js"; import { @@ -78,8 +78,7 @@ export function resolveLegacyFlowRunsSidecarPath(stateDir: string): string { export function readLegacyPluginStateSidecarRows( sourcePath: string, ): LegacyPluginStateSidecarRow[] { - const sqlite = requireNodeSqlite(); - const db = new sqlite.DatabaseSync(resolveNodeSqliteLocation(sourcePath), { readOnly: true }); + const db = openNodeSqliteDatabase(sourcePath, { readOnly: true }); try { return db .prepare( @@ -589,8 +588,7 @@ function legacyRowsMatch( } function readLegacyFlowRows(sourcePath: string): SqliteBindRow[] { - const sqlite = requireNodeSqlite(); - const db = new sqlite.DatabaseSync(resolveNodeSqliteLocation(sourcePath), { readOnly: true }); + const db = openNodeSqliteDatabase(sourcePath, { readOnly: true }); try { const columns = listSqliteColumns(db, "flow_runs"); if (columns.size === 0) { diff --git a/src/infra/state-migrations.task-sidecar-rows.ts b/src/infra/state-migrations.task-sidecar-rows.ts index f806f3f99f3b..36bbbcbb5b9a 100644 --- a/src/infra/state-migrations.task-sidecar-rows.ts +++ b/src/infra/state-migrations.task-sidecar-rows.ts @@ -1,7 +1,7 @@ // Reads, normalizes, and inserts rows from the legacy task-runs SQLite sidecar. import type { DatabaseSync, SQLInputValue } from "node:sqlite"; import { parseAgentSessionKey } from "../routing/session-key.js"; -import { requireNodeSqlite, resolveNodeSqliteLocation } from "./node-sqlite.js"; +import { openNodeSqliteDatabase } from "./node-sqlite.js"; export type SqliteBindRow = Record; @@ -96,8 +96,7 @@ function normalizeLegacyTaskRow(row: Record): SqliteBindRow { } export function readLegacyTaskRows(sourcePath: string): SqliteBindRow[] { - const sqlite = requireNodeSqlite(); - const db = new sqlite.DatabaseSync(resolveNodeSqliteLocation(sourcePath), { readOnly: true }); + const db = openNodeSqliteDatabase(sourcePath, { readOnly: true }); try { const columns = listSqliteColumns(db, "task_runs"); if (columns.size === 0) { @@ -145,8 +144,7 @@ export function readLegacyTaskRows(sourcePath: string): SqliteBindRow[] { } export function readLegacyTaskDeliveryRows(sourcePath: string): SqliteBindRow[] { - const sqlite = requireNodeSqlite(); - const db = new sqlite.DatabaseSync(resolveNodeSqliteLocation(sourcePath), { readOnly: true }); + const db = openNodeSqliteDatabase(sourcePath, { readOnly: true }); try { const columns = listSqliteColumns(db, "task_delivery_state"); if (columns.size === 0) { diff --git a/src/plugin-sdk/sqlite-runtime.ts b/src/plugin-sdk/sqlite-runtime.ts index 299d378703f1..ddd49a830511 100644 --- a/src/plugin-sdk/sqlite-runtime.ts +++ b/src/plugin-sdk/sqlite-runtime.ts @@ -4,4 +4,5 @@ export { ensureOpenClawAgentDatabaseSchema, resolveOpenClawAgentSqlitePath, } from "../state/openclaw-agent-db.js"; +export { openNodeSqliteDatabase } from "../infra/node-sqlite.js"; export { runSqliteImmediateTransactionSync } from "../infra/sqlite-transaction.js"; diff --git a/src/proxy-capture/store.sqlite.ts b/src/proxy-capture/store.sqlite.ts index a7d550e518f4..074926061a5e 100644 --- a/src/proxy-capture/store.sqlite.ts +++ b/src/proxy-capture/store.sqlite.ts @@ -7,7 +7,7 @@ import { gunzipSync, gzipSync } from "node:zlib"; import { normalizeNullableString as normalizeObservedValue } from "@openclaw/normalization-core/string-coerce"; import { normalizeUniqueStringEntries } from "@openclaw/normalization-core/string-normalization"; import { sha256Hex } from "../infra/crypto-digest.js"; -import { requireNodeSqlite, resolveNodeSqliteLocation } from "../infra/node-sqlite.js"; +import { openNodeSqliteDatabase } from "../infra/node-sqlite.js"; import { applyPrivateModeSync } from "../infra/private-mode.js"; import { resolveSqliteDatabaseFilePaths } from "../infra/sqlite-files.js"; import { migrateSqliteSchemaToStrict } from "../infra/sqlite-strict.js"; @@ -128,8 +128,7 @@ function openPathBasedDebugProxyCaptureStore( fs.closeSync(fs.openSync(fileBackedPath, "a", DEBUG_PROXY_CAPTURE_FILE_MODE)); } } - const { DatabaseSync } = requireNodeSqlite(); - const db = new DatabaseSync(resolveNodeSqliteLocation(dbPath)); + const db = openNodeSqliteDatabase(dbPath); let walMaintenance: SqliteWalMaintenance | undefined; try { if (fileBackedPath) { diff --git a/src/snapshot/local-repository.ts b/src/snapshot/local-repository.ts index b46db5a0f47c..0f5032e47bc9 100644 --- a/src/snapshot/local-repository.ts +++ b/src/snapshot/local-repository.ts @@ -13,7 +13,7 @@ import { ensureAbsoluteDirectory, isPathInside, } from "../infra/fs-safe.js"; -import { requireNodeSqlite, resolveNodeSqliteLocation } from "../infra/node-sqlite.js"; +import { openNodeSqliteDatabase } from "../infra/node-sqlite.js"; import { applyPrivateModeSync } from "../infra/private-mode.js"; import { resolveSystemBin } from "../infra/resolve-system-bin.js"; import { assertSqliteIntegrity } from "../infra/sqlite-integrity.js"; @@ -678,8 +678,7 @@ async function verifySnapshotDatabaseFile( validationPath, ); assertArtifactMatchesManifest(validationPath, validationArtifact, manifest); - const sqlite = requireNodeSqlite(); - const database = new sqlite.DatabaseSync(resolveNodeSqliteLocation(validationPath), { + const database = openNodeSqliteDatabase(validationPath, { allowExtension: true, readOnly: true, }); diff --git a/src/state/openclaw-agent-db-maintenance.ts b/src/state/openclaw-agent-db-maintenance.ts index f0bcfdcfb369..ee53e25063b7 100644 --- a/src/state/openclaw-agent-db-maintenance.ts +++ b/src/state/openclaw-agent-db-maintenance.ts @@ -4,7 +4,7 @@ import { MEMORY_PATH_FTS_TRIGGER_DEFINITIONS, } from "../../packages/memory-host-sdk/src/host/memory-schema.js"; import { clearNodeSqliteKyselyCacheForDatabase } from "../infra/kysely-sync.js"; -import { requireNodeSqlite, resolveNodeSqliteLocation } from "../infra/node-sqlite.js"; +import { openNodeSqliteDatabase } from "../infra/node-sqlite.js"; import { repairCanonicalSqliteIndexes } from "../infra/sqlite-index-schema.js"; import { assertSqliteSchemaContains, @@ -98,8 +98,7 @@ export function migrateOpenClawAgentDatabaseForMaintenance(options: { pathname: string; }): void { const agentId = normalizeAgentId(options.agentId); - const sqlite = requireNodeSqlite(); - const database = new sqlite.DatabaseSync(resolveNodeSqliteLocation(options.pathname)); + const database = openNodeSqliteDatabase(options.pathname); try { database.exec(`PRAGMA busy_timeout = ${OPENCLAW_SQLITE_BUSY_TIMEOUT_MS};`); const metadata = readExistingAgentSchemaMeta(database); diff --git a/src/state/openclaw-agent-db-readonly.ts b/src/state/openclaw-agent-db-readonly.ts index cfb90b64a2b3..9cd477720410 100644 --- a/src/state/openclaw-agent-db-readonly.ts +++ b/src/state/openclaw-agent-db-readonly.ts @@ -1,7 +1,7 @@ import fs from "node:fs"; import type { DatabaseSync } from "node:sqlite"; import { clearNodeSqliteKyselyCacheForDatabase } from "../infra/kysely-sync.js"; -import { requireNodeSqlite, resolveNodeSqliteLocation } from "../infra/node-sqlite.js"; +import { openNodeSqliteDatabase } from "../infra/node-sqlite.js"; import { normalizeAgentId } from "../routing/session-key.js"; import type { OpenClawAgentDatabaseOptions } from "./openclaw-agent-db-contract.js"; import { @@ -52,8 +52,7 @@ export function withOpenClawAgentDatabaseReadOnly( if (!fs.existsSync(pathname)) { return { found: false, reason: "database-missing" }; } - const sqlite = requireNodeSqlite(); - const db = new sqlite.DatabaseSync(resolveNodeSqliteLocation(pathname), { readOnly: true }); + const db = openNodeSqliteDatabase(pathname, { readOnly: true }); try { db.exec(`PRAGMA busy_timeout = ${OPENCLAW_SQLITE_BUSY_TIMEOUT_MS};`); assertSupportedAgentSchemaVersion(db, pathname); diff --git a/src/state/openclaw-agent-db-registry.ts b/src/state/openclaw-agent-db-registry.ts index 1bc316d48d41..1b307e9ac2c7 100644 --- a/src/state/openclaw-agent-db-registry.ts +++ b/src/state/openclaw-agent-db-registry.ts @@ -5,7 +5,7 @@ import { executeSqliteQuerySync, getNodeSqliteKysely, } from "../infra/kysely-sync.js"; -import { requireNodeSqlite, resolveNodeSqliteLocation } from "../infra/node-sqlite.js"; +import { openNodeSqliteDatabase } from "../infra/node-sqlite.js"; import { resolveSqliteDatabaseFilePaths } from "../infra/sqlite-files.js"; import { readSqliteUserVersion } from "../infra/sqlite-user-version.js"; import { normalizeAgentId } from "../routing/session-key.js"; @@ -149,8 +149,7 @@ export function listOpenClawRegisteredAgentDatabases( ); } - const sqlite = requireNodeSqlite(); - const database = new sqlite.DatabaseSync(resolveNodeSqliteLocation(pathname), { + const database = openNodeSqliteDatabase(pathname, { readOnly: true, }); try { diff --git a/src/state/openclaw-agent-db.ts b/src/state/openclaw-agent-db.ts index 43e7a15bc49d..2cdd34a01e6e 100644 --- a/src/state/openclaw-agent-db.ts +++ b/src/state/openclaw-agent-db.ts @@ -3,7 +3,7 @@ import { existsSync } from "node:fs"; import path from "node:path"; import type { DatabaseSync } from "node:sqlite"; import { clearNodeSqliteKyselyCacheForDatabase } from "../infra/kysely-sync.js"; -import { requireNodeSqlite, resolveNodeSqliteLocation } from "../infra/node-sqlite.js"; +import { openNodeSqliteDatabase } from "../infra/node-sqlite.js"; import { isTerminalSqliteIntegrityError } from "../infra/sqlite-integrity.js"; import { createSqliteTerminalOpenLatch } from "../infra/sqlite-terminal-open-latch.js"; import { @@ -155,10 +155,9 @@ function logSlowAgentDatabaseOpen(params: { export function inspectOpenClawAgentDatabaseOwner( pathname: string, ): OpenClawAgentDatabaseOwnerInspection { - const sqlite = requireNodeSqlite(); let db: DatabaseSync | undefined; try { - db = new sqlite.DatabaseSync(resolveNodeSqliteLocation(pathname), { readOnly: true }); + db = openNodeSqliteDatabase(pathname, { readOnly: true }); db.exec(`PRAGMA busy_timeout = ${OPENCLAW_SQLITE_BUSY_TIMEOUT_MS};`); assertSupportedAgentSchemaVersion(db, pathname); const existing = readExistingAgentSchemaMeta(db); @@ -210,10 +209,9 @@ export function openOpenClawAgentDatabase( cachedDatabases.delete(pathname); cachedDatabaseOpenFailures.delete(pathname); } - const sqlite = requireNodeSqlite(); // After the collision probe, this sentinel is only a cache key: SQLite opens :memory:, // and no directory, lease, registry row, WAL sidecar, or file write may be created. - const db = new sqlite.DatabaseSync(":memory:"); + const db = openNodeSqliteDatabase(":memory:"); configureSqlitePreSchemaPragmas(db, { busyTimeoutMs: OPENCLAW_SQLITE_BUSY_TIMEOUT_MS, }); @@ -274,8 +272,7 @@ export function openOpenClawAgentDatabase( // Free a slot before constructing the new handle: under real descriptor // pressure the 65th open would otherwise fail before eviction could run. evictLruAgentDatabaseHandles(); - const sqlite = requireNodeSqlite(); - const db = new sqlite.DatabaseSync(resolveNodeSqliteLocation(pathname)); + const db = openNodeSqliteDatabase(pathname); openedDb = db; // Eviction churn must avoid schema/registry busy waits on the event loop while // reconcile workers hold write transactions on these same agent databases. diff --git a/src/state/openclaw-database-preflight.ts b/src/state/openclaw-database-preflight.ts index ba3fa4bb93b4..27213738f790 100644 --- a/src/state/openclaw-database-preflight.ts +++ b/src/state/openclaw-database-preflight.ts @@ -6,7 +6,7 @@ import { executeSqliteQuerySync, getNodeSqliteKysely, } from "../infra/kysely-sync.js"; -import { requireNodeSqlite, resolveNodeSqliteLocation } from "../infra/node-sqlite.js"; +import { openNodeSqliteDatabase } from "../infra/node-sqlite.js"; import { readSqliteUserVersion } from "../infra/sqlite-user-version.js"; import type { OpenClawSchemaVersions } from "./openclaw-schema-versions.js"; import type { DB as OpenClawStateKyselyDatabase } from "./openclaw-state-db.generated.js"; @@ -99,10 +99,9 @@ export function preflightOpenClawDatabaseSchemas(options: { return result; } - const sqlite = requireNodeSqlite(); let stateDatabase: DatabaseSync | undefined; try { - stateDatabase = new sqlite.DatabaseSync(resolveNodeSqliteLocation(statePath), { + stateDatabase = openNodeSqliteDatabase(statePath, { readOnly: true, }); stateDatabase.exec(`PRAGMA busy_timeout = ${OPENCLAW_SQLITE_BUSY_TIMEOUT_MS};`); @@ -137,7 +136,7 @@ export function preflightOpenClawDatabaseSchemas(options: { } let agentDatabase: DatabaseSync | undefined; try { - agentDatabase = new sqlite.DatabaseSync(resolveNodeSqliteLocation(agentPath), { + agentDatabase = openNodeSqliteDatabase(agentPath, { readOnly: true, }); agentDatabase.exec(`PRAGMA busy_timeout = ${OPENCLAW_SQLITE_BUSY_TIMEOUT_MS};`); diff --git a/src/state/openclaw-database-verify.worker.ts b/src/state/openclaw-database-verify.worker.ts index 4ecd09af2a4e..7171995bf9f6 100644 --- a/src/state/openclaw-database-verify.worker.ts +++ b/src/state/openclaw-database-verify.worker.ts @@ -1,5 +1,5 @@ import { parentPort, workerData } from "node:worker_threads"; -import { requireNodeSqlite, resolveNodeSqliteLocation } from "../infra/node-sqlite.js"; +import { openNodeSqliteDatabase } from "../infra/node-sqlite.js"; import { assertSqliteIntegrity, isTerminalSqliteIntegrityError, @@ -35,11 +35,10 @@ function isVerifyTarget(value: unknown): value is OpenClawDatabaseVerifyTarget { export function verifyOpenClawDatabases( targets: readonly OpenClawDatabaseVerifyTarget[], ): OpenClawDatabaseVerifyResult[] { - const sqlite = requireNodeSqlite(); return targets.map((target) => { - let database: InstanceType | undefined; + let database: import("node:sqlite").DatabaseSync | undefined; try { - database = new sqlite.DatabaseSync(resolveNodeSqliteLocation(target.path), { + database = openNodeSqliteDatabase(target.path, { readOnly: true, }); database.exec(`PRAGMA busy_timeout = ${OPENCLAW_SQLITE_BUSY_TIMEOUT_MS};`); diff --git a/src/state/openclaw-quarantine-store.ts b/src/state/openclaw-quarantine-store.ts index 0479308429fa..addf191456d9 100644 --- a/src/state/openclaw-quarantine-store.ts +++ b/src/state/openclaw-quarantine-store.ts @@ -2,7 +2,7 @@ import { existsSync, mkdirSync } from "node:fs"; import path from "node:path"; import type { DatabaseSync } from "node:sqlite"; -import { requireNodeSqlite, resolveNodeSqliteLocation } from "../infra/node-sqlite.js"; +import { openNodeSqliteDatabase } from "../infra/node-sqlite.js"; import { applyPrivateModeSync } from "../infra/private-mode.js"; import { VERSION } from "../version.js"; import { resolveOpenClawStateSqliteDir } from "./openclaw-state-db.paths.js"; @@ -74,8 +74,7 @@ function withQuarantineWriter(env: NodeJS.ProcessEnv, operation: (db: Databas const storePath = resolveQuarantineStorePath(env); const existed = existsSync(storePath); ensureQuarantineStoreDirectory(storePath); - const sqlite = requireNodeSqlite(); - const database = new sqlite.DatabaseSync(resolveNodeSqliteLocation(storePath)); + const database = openNodeSqliteDatabase(storePath); let completed = false; try { if (!existed) { @@ -103,8 +102,7 @@ export function readOpenClawDatabaseQuarantine( if (!existsSync(storePath)) { return undefined; } - const sqlite = requireNodeSqlite(); - const database = new sqlite.DatabaseSync(resolveNodeSqliteLocation(storePath)); + const database = openNodeSqliteDatabase(storePath); try { database.exec(`PRAGMA busy_timeout = ${OPENCLAW_QUARANTINE_BUSY_TIMEOUT_MS};`); const userVersion = readQuarantineSchemaVersion(database, storePath); diff --git a/src/state/openclaw-state-db-readonly.ts b/src/state/openclaw-state-db-readonly.ts index f7c44dec9c37..d13e60b9e74a 100644 --- a/src/state/openclaw-state-db-readonly.ts +++ b/src/state/openclaw-state-db-readonly.ts @@ -1,7 +1,7 @@ import path from "node:path"; import type { DatabaseSync } from "node:sqlite"; import { clearNodeSqliteKyselyCacheForDatabase } from "../infra/kysely-sync.js"; -import { requireNodeSqlite, resolveNodeSqliteLocation } from "../infra/node-sqlite.js"; +import { openNodeSqliteDatabase } from "../infra/node-sqlite.js"; import { createNewerSqliteSchemaVersionError, readSqliteUserVersion, @@ -43,8 +43,7 @@ export function withOpenClawStateDatabaseReadOnly( const pathname = path.resolve( options.path ?? resolveOpenClawStateSqlitePath(options.env ?? process.env), ); - const sqlite = requireNodeSqlite(); - const db = new sqlite.DatabaseSync(resolveNodeSqliteLocation(pathname), { readOnly: true }); + const db = openNodeSqliteDatabase(pathname, { readOnly: true }); try { db.exec(`PRAGMA busy_timeout = ${OPENCLAW_SQLITE_BUSY_TIMEOUT_MS};`); assertSupportedSchemaVersion(db, pathname); diff --git a/src/state/openclaw-state-db-schema-repair.ts b/src/state/openclaw-state-db-schema-repair.ts index 3cde7bcc98ff..ac4f5e0cf1e9 100644 --- a/src/state/openclaw-state-db-schema-repair.ts +++ b/src/state/openclaw-state-db-schema-repair.ts @@ -1,6 +1,6 @@ import { existsSync } from "node:fs"; import type { DatabaseSync } from "node:sqlite"; -import { requireNodeSqlite, resolveNodeSqliteLocation } from "../infra/node-sqlite.js"; +import { openNodeSqliteDatabase } from "../infra/node-sqlite.js"; import { readSqliteUserVersion } from "../infra/sqlite-user-version.js"; import { canRepairLegacyAuditEventsSchema, @@ -151,8 +151,7 @@ export function detectOpenClawStateDatabaseSchemaMigrations( if (!existsSync(pathname)) { return []; } - const sqlite = requireNodeSqlite(); - const db = new sqlite.DatabaseSync(resolveNodeSqliteLocation(pathname), { readOnly: true }); + const db = openNodeSqliteDatabase(pathname, { readOnly: true }); try { const migrations: OpenClawStateDatabaseSchemaMigration[] = []; const userVersion = readSqliteUserVersion(db); diff --git a/src/state/openclaw-state-db-startup-checkpoint.ts b/src/state/openclaw-state-db-startup-checkpoint.ts index f01eef5610e4..65acd03bd398 100644 --- a/src/state/openclaw-state-db-startup-checkpoint.ts +++ b/src/state/openclaw-state-db-startup-checkpoint.ts @@ -1,5 +1,5 @@ import type { DatabaseSync } from "node:sqlite"; -import { requireNodeSqlite, resolveNodeSqliteLocation } from "../infra/node-sqlite.js"; +import { openNodeSqliteDatabase } from "../infra/node-sqlite.js"; import { assertSqliteIntegrity } from "../infra/sqlite-integrity.js"; import { runSqliteImmediateTransactionSync } from "../infra/sqlite-transaction.js"; import { @@ -62,8 +62,7 @@ export function withOpenClawStateStartupMigrationCheckpointDatabase( const env = options.env ?? process.env; const pathname = resolveDatabasePath(options); ensureOpenClawStatePermissions(pathname, env); - const sqlite = requireNodeSqlite(); - const db = new sqlite.DatabaseSync(resolveNodeSqliteLocation(pathname)); + const db = openNodeSqliteDatabase(pathname); try { assertSqliteIntegrity(db, pathname); ensureStartupMigrationCheckpointSchema(db, pathname); diff --git a/src/state/openclaw-state-db.ts b/src/state/openclaw-state-db.ts index a6761b83183a..ce609a3607d0 100644 --- a/src/state/openclaw-state-db.ts +++ b/src/state/openclaw-state-db.ts @@ -7,11 +7,7 @@ import { executeSqliteQuerySync, getNodeSqliteKysely, } from "../infra/kysely-sync.js"; -import { - requireNodeSqlite, - resolveNodeSqliteLocation, - resolveNodeSqliteReadOnlyLocation, -} from "../infra/node-sqlite.js"; +import { openNodeSqliteDatabase, resolveNodeSqliteReadOnlyLocation } from "../infra/node-sqlite.js"; import { repairCanonicalSqliteIndexes } from "../infra/sqlite-index-schema.js"; import { assertSqliteIntegrity, @@ -129,8 +125,7 @@ export function repairOpenClawStateDatabaseSchema(options: OpenClawStateDatabase return { changes: [], warnings: [] }; } ensureOpenClawStatePermissions(pathname, env); - const sqlite = requireNodeSqlite(); - const db = new sqlite.DatabaseSync(resolveNodeSqliteLocation(pathname)); + const db = openNodeSqliteDatabase(pathname); const rebuiltIndexNames = new Set(); try { assertSupportedSchemaVersion(db, pathname); @@ -303,9 +298,8 @@ export function openExistingOpenClawStateDatabaseReadOnly( if (!existsSync(pathname)) { return undefined; } - const sqlite = requireNodeSqlite(); const hasWalSidecars = existsSync(`${pathname}-wal`) || existsSync(`${pathname}-shm`); - const db = new sqlite.DatabaseSync(resolveNodeSqliteReadOnlyLocation(pathname, hasWalSidecars), { + const db = openNodeSqliteDatabase(resolveNodeSqliteReadOnlyLocation(pathname, hasWalSidecars), { readOnly: true, }); try { @@ -404,8 +398,7 @@ export function openOpenClawStateDatabase( throw quarantineFailure; } ensureOpenClawStatePermissions(pathname, env); - const sqlite = requireNodeSqlite(); - const db = new sqlite.DatabaseSync(resolveNodeSqliteLocation(pathname)); + const db = openNodeSqliteDatabase(pathname); const walMaintenance = (() => { let maintenance: SqliteWalMaintenance | undefined; try {