refactor(sqlite): enforce one database connection boundary (#113418)

* refactor(sqlite): centralize database opens

* test(sqlite): mock connection owner boundary
This commit is contained in:
Peter Steinberger
2026-07-24 15:15:17 -07:00
committed by GitHub
parent 4e83914748
commit 1e93465a2a
53 changed files with 268 additions and 255 deletions
@@ -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);
@@ -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);
+3 -14
View File
@@ -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,
@@ -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<num
}
| undefined;
try {
const { DatabaseSync } = await import("node:sqlite");
database = new DatabaseSync(resolvedDbPath, { readOnly: true });
database = openNodeSqliteDatabase(resolvedDbPath, { readOnly: true });
const row = database.prepare("SELECT MAX(ROWID) AS maxRowid FROM message").get() as
| { maxRowid?: unknown }
| undefined;
+4 -21
View File
@@ -1,6 +1,5 @@
// Imessage plugin module implements send behavior.
import { constants, accessSync } from "node:fs";
import { createRequire } from "node:module";
import type { MediaPlaceholderTextFact } from "openclaw/plugin-sdk/channel-inbound";
import {
createMessageReceiptFromOutboundResults,
@@ -13,6 +12,7 @@ import { resolveMarkdownTableMode } from "openclaw/plugin-sdk/markdown-table-run
import { kindFromMime, resolveOutboundAttachmentFromUrl } from "openclaw/plugin-sdk/media-runtime";
import { requireRuntimeConfig } from "openclaw/plugin-sdk/plugin-config-runtime";
import { sleep as delay } from "openclaw/plugin-sdk/runtime-env";
import { openNodeSqliteDatabase } from "openclaw/plugin-sdk/sqlite-runtime";
import { convertMarkdownTables } from "openclaw/plugin-sdk/text-chunking";
import { stripInlineDirectiveTagsForDelivery } from "openclaw/plugin-sdk/text-chunking";
import {
@@ -45,7 +45,6 @@ import {
parseIMessageTarget,
} from "./targets.js";
const require = createRequire(import.meta.url);
type ParsedIMessageTarget = ReturnType<typeof parseIMessageTarget>;
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<string | number> = [];
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 {
+5 -10
View File
@@ -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<typeof configureSqliteConnectionPragmas> | undefined;
try {
// WAL/SHM sidecars inherit the main DB file's permissions.
@@ -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,
@@ -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,
@@ -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;
@@ -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
@@ -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;
}
+2 -2
View File
@@ -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);
+3 -2
View File
@@ -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<Response>;
@@ -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();
@@ -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<MatrixSyncStoreCursor & { score: number }> = [];
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(
+3 -2
View File
@@ -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<typeof configureSqliteConnectionPragmas> | undefined;
try {
maintenance = configureSqliteConnectionPragmas(db, {
+74
View File
@@ -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;
}
+2 -3
View File
@@ -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.
+2 -3
View File
@@ -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,
});
+4 -4
View File
@@ -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<typeof sqlite.DatabaseSync> | 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;
+8 -16
View File
@@ -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<typeof sqlite.DatabaseSync> | 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<typeof sqlite.DatabaseSync> | 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<typeof sqlite.DatabaseSync> | 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");
@@ -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);
+2 -2
View File
@@ -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);
+2 -3
View File
@@ -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 {
+2 -3
View File
@@ -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;
+2 -3
View File
@@ -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,
+2 -3
View File
@@ -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();
+2 -3
View File
@@ -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;`);
+2 -5
View File
@@ -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) {
+21 -4
View File
@@ -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);
});
});
+18
View File
@@ -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);
}
+2 -3
View File
@@ -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
+4 -4
View File
@@ -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,
});
+2 -3
View File
@@ -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");
+2 -3
View File
@@ -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 {
@@ -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
@@ -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<boolean> {
}
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 (
@@ -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,
+3 -5
View File
@@ -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) {
@@ -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<string, SQLInputValue>;
@@ -96,8 +96,7 @@ function normalizeLegacyTaskRow(row: Record<string, unknown>): 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) {
+1
View File
@@ -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";
+2 -3
View File
@@ -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) {
+2 -3
View File
@@ -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,
});
+2 -3
View File
@@ -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);
+2 -3
View File
@@ -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<T>(
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);
+2 -3
View File
@@ -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 {
+4 -7
View File
@@ -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.
+3 -4
View File
@@ -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};`);
+3 -4
View File
@@ -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<typeof sqlite.DatabaseSync> | 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};`);
+3 -5
View File
@@ -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<T>(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);
+2 -3
View File
@@ -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<T>(
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);
+2 -3
View File
@@ -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);
@@ -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<T>(
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);
+4 -11
View File
@@ -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<string>();
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 {