mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
refactor(sqlite): enforce one database connection boundary (#113418)
* refactor(sqlite): centralize database opens * test(sqlite): mock connection owner boundary
This commit is contained in:
committed by
GitHub
parent
4e83914748
commit
1e93465a2a
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
+31
-43
@@ -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(
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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,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;`);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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};`);
|
||||
|
||||
@@ -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};`);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user