UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

1,055 lines (1,054 loc) • 43.8 kB
import { s as toErrorObject } from "./error-coercion-DKQY4wyF.mjs"; import { S as resolveGlobalSingleton } from "./number-coercion-CmpLVzZ6.mjs"; import { a as StartupMaintenanceRequiredError, d as kyselyByDatabase, f as queryErrorHandlerByDatabase, l as executeWithCachedStatement, p as registerNodeSqliteDisposeCallback, u as installStatementInvalidation } from "./sqlite-user-version-C7PVO8Wf.mjs"; import { a as normalizeWindowsPathPreservingCase } from "./utils-BfTq4rUg.mjs"; import { A as hasErrnoCode } from "./redact-TLu2QTmL.mjs"; import { t as formatErrorMessage } from "./errors-BwnhmTMH.mjs"; import { a as isSqliteWalResetSafeVersion, i as getSqliteRuntimeCapabilities, o as isNodeVersionAtLeast, r as ensureSqliteLibrarySelected, s as parseNodeReleaseVersion } from "./bun-sqlite-library-DhkLqQoS.mjs"; import { n as compareValidSemver } from "./semver-aYpwYdrQ.mjs"; import { createRequire } from "node:module"; import { realpathSync, statSync } from "node:fs"; import path from "node:path"; import { pathToFileURL } from "node:url"; import { AsyncLocalStorage } from "node:async_hooks"; import { normalizeWindowsPathForComparison } from "@openclaw/fs-safe/path"; import { isMainThread, threadId } from "node:worker_threads"; import { realpath, stat } from "node:fs/promises"; import "node:util"; import { InsertQueryNode, Kysely, SelectQueryNode, SqliteDialect, sql } from "kysely"; //#region src/infra/sqlite-reader-lifecycle.ts const readerOwners = resolveGlobalSingleton(Symbol.for("openclaw.sqliteReaderOwners"), () => new AsyncLocalStorage()); const activeReaders = resolveGlobalSingleton(Symbol.for("openclaw.sqliteActiveReaders"), () => ({ byDatabase: /* @__PURE__ */ new WeakMap(), byPath: /* @__PURE__ */ new Map() })); const connections = resolveGlobalSingleton(Symbol.for("openclaw.sqliteReaderConnections"), () => ({ nextId: 0, byDatabase: /* @__PURE__ */ new WeakMap(), byPath: /* @__PURE__ */ new Map(), finalizer: new FinalizationRegistry((connection) => forgetConnection(connection)) })); /** The same Windows file can arrive with a namespaced or differently cased path. */ function sqliteReaderDatabasePathKey(databasePath) { const resolved = path.resolve(databasePath); return process.platform === "win32" ? normalizeWindowsPathForComparison(resolved) : resolved; } function boundedOperation(operation) { return (operation.trim() || "sqlite reader").slice(0, 120); } function captureSqliteReaderOwner() { const owner = readerOwners.getStore(); return owner ? { ...owner } : void 0; } function currentOwner(fallbackOperation, capturedOwner) { const inherited = capturedOwner ?? readerOwners.getStore(); return { operation: boundedOperation(inherited?.operation ?? fallbackOperation), ownerKind: inherited?.ownerKind ?? (isMainThread ? "main" : "worker"), ...inherited?.actorId !== void 0 ? { actorId: inherited.actorId } : {} }; } function connectionFor(database) { let connection = connections.byDatabase.get(database); if (!connection) { const now = Date.now(); const location = database.location(); connection = { id: ++connections.nextId, path: location ? sqliteReaderDatabasePathKey(location) : void 0, database: new WeakRef(database), owner: currentOwner("sqlite connection"), openedAtMs: now }; connections.byDatabase.set(database, connection); if (connection.path) { const entries = connections.byPath.get(connection.path) ?? /* @__PURE__ */ new Map(); entries.set(connection.id, connection); connections.byPath.set(connection.path, entries); } connections.finalizer.register(database, connection, connection); } return connection; } /** Forget observations only; explicit reader custody belongs to its caller. */ function forgetConnection(connection) { if (connection.path) { const entries = connections.byPath.get(connection.path); entries?.delete(connection.id); if (entries?.size === 0) connections.byPath.delete(connection.path); } const database = connection.database.deref(); if (database && connections.byDatabase.get(database) === connection) connections.byDatabase.delete(database); connections.finalizer.unregister(connection); } /** Register weak metadata without replacing native methods or acquiring lifecycle custody. */ function registerSqliteReaderConnection(database) { if (database.isOpen) connectionFor(database); } function retainSqliteReader(database, fallbackOperation, capturedOwner) { const connection = connectionFor(database); const now = Date.now(); const reader = { ...currentOwner(fallbackOperation, capturedOwner), kind: "iterator", connectionId: connection.id, threadId, startedAtMs: now, lastProgressAtMs: now }; const token = Symbol(reader.operation); const databaseReaders = activeReaders.byDatabase.get(database) ?? /* @__PURE__ */ new Map(); databaseReaders.set(token, reader); activeReaders.byDatabase.set(database, databaseReaders); const databasePath = connection.path; const pathReaders = databasePath ? activeReaders.byPath.get(databasePath) ?? /* @__PURE__ */ new Map() : void 0; pathReaders?.set(token, reader); if (databasePath && pathReaders) activeReaders.byPath.set(databasePath, pathReaders); let released = false; return { progress() { if (!released) reader.lastProgressAtMs = Date.now(); }, release() { if (released) return; released = true; databaseReaders.delete(token); if (databaseReaders.size === 0 && activeReaders.byDatabase.get(database) === databaseReaders) activeReaders.byDatabase.delete(database); pathReaders?.delete(token); if (databasePath && pathReaders?.size === 0 && activeReaders.byPath.get(databasePath) === pathReaders) activeReaders.byPath.delete(databasePath); } }; } function diagnostics(readers) { const now = Date.now(); return [...readers].map(({ startedAtMs, lastProgressAtMs, ...reader }) => Object.assign(reader, { ageMs: Math.max(0, now - startedAtMs), idleMs: Math.max(0, now - lastProgressAtMs) })).toSorted((left, right) => right.ageMs - left.ageMs).slice(0, 8); } /** Observed local activity is diagnostic evidence, not proof of which connection owns a WAL lock. */ function readSqliteReaderDiagnosticsForPath(databasePath) { const key = sqliteReaderDatabasePathKey(databasePath); const entries = connections.byPath.get(key); const localConnections = []; const now = Date.now(); for (const connection of entries?.values() ?? []) { const database = connection.database.deref(); if (!database?.isOpen) { forgetConnection(connection); continue; } const location = database.location(); if (!location || sqliteReaderDatabasePathKey(location) !== key) { forgetConnection(connection); continue; } const readers = activeReaders.byDatabase.get(database); localConnections.push({ ...connection.owner, connectionId: connection.id, threadId, ageMs: Math.max(0, now - connection.openedAtMs), transactionOpen: database.isTransaction, trackedReaders: readers?.size ?? 0 }); } const readers = activeReaders.byPath.get(key); return { scope: "current-thread", blockingOwner: "unknown", nativeStatements: "unobserved", threadId, observedAtMs: now, connectionCount: localConnections.length, readerCount: readers?.size ?? 0, connections: localConnections.toSorted((left, right) => right.trackedReaders - left.trackedReaders || Number(right.transactionOpen) - Number(left.transactionOpen)).slice(0, 8), activeReaders: diagnostics(readers?.values() ?? []) }; } //#endregion //#region src/infra/kysely-sync.ts const nodeVersion = parseNodeReleaseVersion(process.versions.node); const supportsRepreparedAll = !process.versions.bun && (nodeVersion?.major === 24 && isNodeVersionAtLeast(nodeVersion, { major: 24, minor: 20, patch: 0 }) || isNodeVersionAtLeast(nodeVersion, { major: 26, minor: 6, patch: 0 })); const compileOnlySqliteDialect = new SqliteDialect({ database: async () => { throw new Error("getNodeSqliteKysely() returns a compile-only Kysely facade; use executeSqliteQuerySync() to execute node:sqlite queries."); } }); function getNodeSqliteKysely(db) { const existing = kyselyByDatabase.get(db); if (existing) return existing; const kysely = new Kysely({ dialect: compileOnlySqliteDialect }); kyselyByDatabase.set(db, kysely); return kysely; } function reportNodeSqliteKyselyQueryError(db, error) { try { queryErrorHandlerByDatabase.get(db)?.(error); } catch {} } function throwSqliteIteratorCleanupError(error) { throw toErrorObject(error, "SQLite iterator cleanup failed"); } /** Execute a compiled Kysely query synchronously against node:sqlite. */ function executeCompiledSqliteQuerySync(db, compiledQuery, firstRowOnly = false, parameters = compiledQuery.parameters) { try { const sql = compiledQuery.sql; installStatementInvalidation(db); return executeWithCachedStatement(db, sql, parameters, (statement) => { if (firstRowOnly && SelectQueryNode.is(compiledQuery.query)) { const row = statement.get(...parameters); return { rows: row === void 0 ? [] : [row] }; } if (SelectQueryNode.is(compiledQuery.query) || statement.columns().length > 0) { if (supportsRepreparedAll) return { rows: statement.all(...parameters) }; const iterator = statement.iterate(...parameters); const reader = retainSqliteReader(db, "kysely eager query"); let cleanupError; let failed = false; let failure; const rows = []; try { for (const row of iterator) { reader.progress(); rows.push(row); } } catch (error) { failed = true; failure = error; } try { iterator.return?.(); } catch (error) { cleanupError = error; } reader.release(); if (failed) throw toErrorObject(failure, "SQLite query failed"); if (cleanupError !== void 0) throw toErrorObject(cleanupError, "SQLite query cleanup failed"); return { rows }; } statement.setReadBigInts(true); let outcome; try { outcome = statement.run(...parameters); } finally { statement.setReadBigInts(false); } const { changes, lastInsertRowid } = outcome; const result = { numAffectedRows: BigInt(changes), rows: [] }; if (InsertQueryNode.is(compiledQuery.query) && changes > 0) return { ...result, insertId: BigInt(lastInsertRowid) }; return result; }); } catch (error) { reportNodeSqliteKyselyQueryError(db, error); throw error; } } /** Compile and execute a Kysely query synchronously. */ function executeSqliteQuerySync(db, query) { return executeCompiledSqliteQuerySync(db, query.compile()); } /** Cache compiled query functions or bundles by native connection. */ function createSqliteQueryCache(create) { const queriesByDatabase = /* @__PURE__ */ new WeakMap(); return (database) => { let queries = queriesByDatabase.get(database); if (queries === void 0) { queries = create(database); queriesByDatabase.set(database, queries); } return queries; }; } /** Compile fixed SQL and fresh bindings without taking ownership of a native statement. */ function compileSqliteQueryBindings(build) { const bindings = /* @__PURE__ */ new Map(); const compiled = build((read) => { const marker = Symbol("sqlite-query-parameter"); bindings.set(marker, read); return sql`${marker}`; }).compile(); const readers = compiled.parameters.map((value) => bindings.get(value) ?? (() => value)); return { compiled, bind: (params) => readers.map((read) => read(params)) }; } /** Compile a fixed query once; bind fresh values through the normal sync executor on each call. */ function prepareSqliteQuerySync(db, build) { const { compiled, bind } = compileSqliteQueryBindings(build); return (params) => executeCompiledSqliteQuerySync(db, compiled, false, bind(params)); } /** Compile and lazily iterate a Kysely query synchronously against node:sqlite. */ function iterateSqliteQuerySync(db, query) { const owner = captureSqliteReaderOwner(); return (function* () { const compiledQuery = query.compile(); try { const statement = db.prepare(compiledQuery.sql); if (!SelectQueryNode.is(compiledQuery.query) && statement.columns().length === 0) return; const parameters = compiledQuery.parameters; const iterator = statement.iterate(...parameters); const reader = retainSqliteReader(db, "kysely iterator", owner); let cleanupError; let failed = false; try { for (const row of iterator) { reader.progress(); yield row; } } catch (error) { failed = true; throw toErrorObject(error, "SQLite iterator failed"); } finally { try { iterator.return?.(); } catch (error) { cleanupError = error; } reader.release(); if (!failed && cleanupError !== void 0) throwSqliteIteratorCleanupError(cleanupError); } } catch (error) { reportNodeSqliteKyselyQueryError(db, error); throw error; } })(); } /** Execute a Kysely query synchronously and return its first row. */ function executeSqliteQueryTakeFirstSync(db, query) { return executeCompiledSqliteQuerySync(db, query.compile(), true).rows[0]; } //#endregion //#region src/infra/sqlite-pinned-read-snapshot.ts const snapshots = resolveGlobalSingleton(Symbol.for("openclaw.sqlitePinnedReadSnapshots"), () => /* @__PURE__ */ new WeakMap()); function getSqlitePinnedReadSnapshot(db) { return snapshots.get(db); } /** Pin an implicit read snapshot without requiring transaction-control authorization. */ function runSqlitePinnedReadSnapshotSync(db, operation) { const parent = snapshots.get(db); snapshots.set(db, parent ?? {}); try { return executeWithCachedStatement(db, "PRAGMA schema_version", [], (statement) => { const snapshot = statement.iterate(); try { const first = snapshot.next(); if (first.done) throw new Error("SQLite schema version query returned no row"); return operation(Number(first.value.schema_version)); } finally { snapshot.return?.(); } }); } finally { if (!parent) snapshots.delete(db); } } //#endregion //#region src/infra/sqlite-schema-sql.ts const TABLE_CONSTRAINT_KEYWORDS = /* @__PURE__ */ new Set([ "CHECK", "FOREIGN", "PRIMARY", "UNIQUE" ]); /** Select canonical DDL without opening a database or changing its installation lifecycle. */ function extractSqliteTableSchema(schema, table, options = {}) { const start = schema.indexOf(`CREATE TABLE IF NOT EXISTS ${table} (`); const endMarker = options.endMarker ?? "\n) STRICT;"; const end = schema.indexOf(endMarker, start); if (start < 0 || end < start) throw new Error(options.errorMessage ?? `Canonical schema markers are missing for ${table}`); return schema.slice(start, end + (options.includeEndMarker === false ? 0 : endMarker.length)); } function readTableConstraintKeyword(sql, first) { let token = first; if (token.keyword === "CONSTRAINT") { const name = readSqlToken(sql, token.end); token = name ? readSqlToken(sql, name.end) : null; } return token?.keyword && TABLE_CONSTRAINT_KEYWORDS.has(token.keyword) ? token.keyword : null; } function readSqlToken(sql, start) { let index = start; while (index < sql.length && /\s/u.test(sql[index] ?? "")) index += 1; const char = sql[index]; if (!char) return null; if (char === "\"" || char === "`" || char === "[") { const end = skipSqlQuoted(sql, index, char); return { end, keyword: null, raw: sql.slice(index, end) }; } let end = index; while (end < sql.length && !/[\s(,]/u.test(sql[end] ?? "")) end += 1; const raw = sql.slice(index, end); return { end, keyword: raw.toUpperCase(), raw }; } function normalizeSqlIdentifier(identifier) { if (identifier.startsWith("\"") && identifier.endsWith("\"")) return identifier.slice(1, -1).replaceAll("\"\"", "\"").toLowerCase(); if (identifier.startsWith("`") && identifier.endsWith("`")) return identifier.slice(1, -1).replaceAll("``", "`").toLowerCase(); if (identifier.startsWith("[") && identifier.endsWith("]")) return identifier.slice(1, -1).toLowerCase(); return identifier.toLowerCase(); } function normalizeSchemaSql(sql) { if (sql === null) return null; return normalizeSqlWhitespace(sql).replace(/;\s*$/u, "").trim().replace(/^(CREATE (?:TABLE|VIRTUAL TABLE|UNIQUE INDEX|INDEX|TRIGGER)) IF NOT EXISTS /iu, "$1 "); } function splitSqlList(sql) { const items = []; let depth = 0; let start = 0; let index = 0; while (index < sql.length) { const next = skipSqlQuotedOrComment(sql, index); if (next !== index) { index = next; continue; } const char = sql[index]; if (char === "(") depth += 1; else if (char === ")") depth -= 1; else if (char === "," && depth === 0) { items.push(sql.slice(start, index)); start = index + 1; } index += 1; } items.push(sql.slice(start)); return items; } function findSqlCharacter(sql, character) { let index = 0; while (index < sql.length) { const next = skipSqlQuotedOrComment(sql, index); if (next !== index) { index = next; continue; } if (sql[index] === character) return index; index += 1; } return -1; } function findSqlClosingParenthesis(sql, open) { let depth = 0; let index = open; while (index < sql.length) { const next = skipSqlQuotedOrComment(sql, index); if (next !== index) { index = next; continue; } const char = sql[index]; if (char === "(") depth += 1; else if (char === ")") { depth -= 1; if (depth === 0) return index; } index += 1; } throw new Error("SQLite schema contains an unterminated table definition."); } function normalizeSqlWhitespace(sql) { let normalized = ""; let pendingSpace = false; const segments = /[^\s'"`[/-]+|\s+|./gsu; let segment; while (segment = segments.exec(sql)) { const index = segment.index; const char = segment[0][0] ?? ""; const quoted = skipSqlQuoted(sql, index, char); if (quoted !== index) { if (pendingSpace && normalized.length > 0) normalized += " "; normalized += sql.slice(index, quoted); pendingSpace = false; segments.lastIndex = quoted; continue; } const comment = skipSqlComment(sql, index); if (comment !== index) { pendingSpace = true; segments.lastIndex = comment; continue; } if (/\s/u.test(char)) pendingSpace = true; else { if (pendingSpace && normalized.length > 0) normalized += " "; normalized += segment[0]; pendingSpace = false; } } return normalized.trim(); } function quoteSqliteIdentifier(identifier) { return `"${identifier.replaceAll("\"", "\"\"")}"`; } function skipSqlQuotedOrComment(sql, index) { const quoted = skipSqlQuoted(sql, index, sql[index] ?? ""); return quoted !== index ? quoted : skipSqlComment(sql, index); } function skipSqlQuoted(sql, index, quote) { if (quote !== "'" && quote !== "\"" && quote !== "`" && quote !== "[") return index; const closingQuote = quote === "[" ? "]" : quote; let cursor = index + 1; while (cursor < sql.length) { if (sql[cursor] !== closingQuote) { cursor += 1; continue; } if (quote !== "[" && sql[cursor + 1] === closingQuote) { cursor += 2; continue; } return cursor + 1; } return sql.length; } function skipSqlComment(sql, index) { if (sql.startsWith("--", index)) { const newline = sql.indexOf("\n", index + 2); return newline === -1 ? sql.length : newline + 1; } if (sql.startsWith("/*", index)) { const close = sql.indexOf("*/", index + 2); return close === -1 ? sql.length : close + 2; } return index; } //#endregion //#region src/infra/sqlite-worker-identity.ts const useDatabaseBirthtime = process.platform !== "linux"; function databaseFileIdentityKey(file) { return `${file.dev}:${file.ino}`; } function readDatabaseIdentityBirthtime(file) { return useDatabaseBirthtime ? file.birthtimeNs.toString() : "0"; } /** Native SQLite namespaces are filesystem locators, not distinct database owners. */ function normalizeDatabasePath(location) { const normalized = process.platform === "win32" ? normalizeWindowsPathPreservingCase(location) : location; return process.platform === "win32" && !/^(?:[a-z]:[\\/]|[\\/]{2})/iu.test(normalized) ? location : normalized; } function readDatabaseFileIdentity(value) { if (!value || typeof value !== "object" || Array.isArray(value) || !("key" in value) || typeof value.key !== "string" || !/^file:(?:0|[1-9]\d{0,19}):(?:0|[1-9]\d{0,19})$/.test(value.key) || "birthtime" in value && value.birthtime !== void 0 && (typeof value.birthtime !== "string" || !/^-?(?:0|[1-9]\d{0,29})$/.test(value.birthtime))) throw new Error("SQLite snapshot requires a captured physical file identity"); return { key: value.key, ..."birthtime" in value && typeof value.birthtime === "string" ? { birthtime: value.birthtime } : {} }; } function assertDatabaseFileIdentity(file, expected) { if (!file.isFile() || `file:${databaseFileIdentityKey(file)}` !== expected.key || expected.birthtime !== void 0 && readDatabaseIdentityBirthtime(file) !== expected.birthtime) throw new Error("SQLite database file identity changed before existing-only open"); } function existingIdentity(file, canonicalFile, canonicalPath) { if (!file.isFile()) throw new Error("SQLite worker database path must identify a regular file"); if (file.dev !== canonicalFile.dev || file.ino !== canonicalFile.ino || readDatabaseIdentityBirthtime(file) !== readDatabaseIdentityBirthtime(canonicalFile)) throw new Error("SQLite database pathname changed during admission"); return { key: `file:${databaseFileIdentityKey(file)}`, canonicalPath: normalizeDatabasePath(canonicalPath), birthtime: readDatabaseIdentityBirthtime(file) }; } /** Inspect a native-owner path without replacing its diagnostic for a non-file target. */ function inspectDatabasePathIdentitySync(databasePath) { const resolvedPath = path.resolve(databasePath); let file; try { file = statSync(resolvedPath, { bigint: true }); } catch (error) { if (!hasErrnoCode(error, "ENOENT")) throw error; } if (file) { if (!file.isFile()) return; const canonicalPath = realpathSync.native(resolvedPath); return existingIdentity(file, statSync(canonicalPath, { bigint: true }), canonicalPath); } const missing = []; let ancestor = resolvedPath; while (true) try { const canonicalPath = normalizeDatabasePath(path.join(realpathSync.native(ancestor), ...missing)); return { key: `path:${canonicalPath}`, canonicalPath }; } catch (error) { if (!hasErrnoCode(error, "ENOENT")) throw error; missing.unshift(path.basename(ancestor)); const parent = path.dirname(ancestor); if (parent === ancestor) throw error; ancestor = parent; } } /** Capture identity before yielding; worker admission requires a regular file or absent path. */ function readDatabasePathIdentitySync(databasePath) { const identity = inspectDatabasePathIdentitySync(databasePath); if (!identity) throw new Error("SQLite worker database path must identify a regular file"); return identity; } /** Inspect retained aliases only while binding a newly observed database path. */ function findChangedDatabasePaths(paths, observed) { return [...paths].filter((pathname) => inspectDatabasePathIdentitySync(pathname)?.key !== observed.key); } async function readDatabasePathIdentity(databasePath) { const file = await stat(databasePath, { bigint: true }).catch((error) => { if (hasErrnoCode(error, "ENOENT")) return; throw error; }); if (file) try { const canonicalPath = await realpath(databasePath); return existingIdentity(file, await stat(canonicalPath, { bigint: true }), canonicalPath); } catch (error) { if (hasErrnoCode(error, "ENOENT")) throw new Error("SQLite database pathname changed during admission", { cause: error }); throw error; } const missing = []; let ancestor = databasePath; while (true) try { const canonicalPath = normalizeDatabasePath(path.join(await realpath(ancestor), ...missing)); return { key: `path:${canonicalPath}`, canonicalPath }; } catch (error) { if (!hasErrnoCode(error, "ENOENT")) throw error; missing.unshift(path.basename(ancestor)); const parent = path.dirname(ancestor); if (parent === ancestor) throw error; ancestor = parent; } } function assertExistingDatabaseIdentity(databasePath, expected, expectedBirthtime) { assertDatabaseFileIdentity(statSync(databasePath, { bigint: true }), { key: expected, birthtime: expectedBirthtime }); } //#endregion //#region src/infra/sqlite-schema-facts.ts const scopes = resolveGlobalSingleton(Symbol.for("openclaw.sqliteSchemaScopes"), () => { const byIdentity = /* @__PURE__ */ new Map(); const release = (scope) => { scope.users -= 1; if (scope.users === 0 && scope.key && byIdentity.get(scope.key) === scope) byIdentity.delete(scope.key); }; return { byIdentity, release, finalizer: new FinalizationRegistry(release) }; }); const owners = resolveGlobalSingleton(Symbol.for("openclaw.sqliteSchemaFacts"), () => /* @__PURE__ */ new WeakMap()); function invalidate(owner) { owner.revision += 1; owner.facts = void 0; } function bindScope(database, owner) { if (owner.scope) return owner.scope; const location = database.location(); const key = location ? readDatabasePathIdentitySync(location).key : void 0; const scope = key && scopes.byIdentity.get(key) || { key, revision: 0, users: 0 }; if (key) scopes.byIdentity.set(key, scope); scope.users += 1; owner.scope = scope; owner.scopeRevision = scope.revision; scopes.finalizer.register(database, scope, owner); return scope; } function publishSchemaChange(database, owner) { const scope = bindScope(database, owner); scope.revision += 1; owner.scopeRevision = scope.revision; } /** Schema publications outside DDL (such as a deferred version marker) share this revision. */ function invalidateSqliteSchemaFacts(database) { const owner = owners.get(database); if (owner) { bindScope(database, owner); invalidate(owner); owner.transactionalSchema ||= database.isTransaction; if (!database.isTransaction) publishSchemaChange(database, owner); } } function changesSchema(sql) { return /\b(?:CREATE|ALTER|DROP|REINDEX|VACUUM)\b|\bPRAGMA\b[\s\S]*\b(?:user_version|schema_version|writable_schema)\b[\s\S]*[=(]/i.test(sql); } const transactionControlPrefix = /^(?:\s|;|--[^\n]*(?:\n|$)|\/\*(?:[^*]|\*(?!\/))*\*\/)*(BEGIN|SAVEPOINT|COMMIT|END|RELEASE|ROLLBACK)\b/i; function batchTransactionControl(sql) { let control; let remaining = sql; while (remaining) { const next = transactionControlPrefix.exec(remaining)?.[1]?.toUpperCase(); if (next === "ROLLBACK") return next; control ||= next; const end = remaining.includes(";") ? findSqlCharacter(remaining, ";") : -1; if (end < 0) break; remaining = remaining.slice(end + 1); } return control; } function callStatement(method, [first, ...remaining]) { if (first === void 0) return method(); if (typeof first === "object" && first !== null && !ArrayBuffer.isView(first)) return method(first, ...remaining); return method(first, ...remaining); } function trackSchemaChanges(database, owner, native) { const settle = (boundary = false) => { if ((boundary || !database.isTransaction) && (owner.transactionalSchema || owner.transactionalFacts)) { if (owner.transactionalSchema) publishSchemaChange(database, owner); invalidate(owner); owner.transactionalSchema &&= database.isTransaction; owner.transactionalFacts = false; } }; const execute = (operation, schemaChange, control) => { if (owner.transactionalSchema && !owner.scope) bindScope(database, owner); settle(); const invalidates = schemaChange || control === "ROLLBACK" && owner.transactionalSchema; if (invalidates) invalidateSqliteSchemaFacts(database); try { return operation(); } finally { if (invalidates) invalidateSqliteSchemaFacts(database); settle(Boolean(control)); } }; database.exec = (sql) => execute(() => native.DatabaseSync.prototype.exec.call(database, sql), changesSchema(sql), batchTransactionControl(sql)); database.prepare = (...prepareArgs) => { const [sql] = prepareArgs; const statement = native.DatabaseSync.prototype.prepare.call(database, ...prepareArgs); const schemaChange = changesSchema(sql); const control = transactionControlPrefix.exec(sql)?.[1]?.toUpperCase(); if (schemaChange || control) { const run = Object.hasOwn(statement, "run") ? statement.run.bind(statement) : void 0; const get = Object.hasOwn(statement, "get") ? statement.get.bind(statement) : void 0; const all = Object.hasOwn(statement, "all") ? statement.all.bind(statement) : void 0; const iterate = Object.hasOwn(statement, "iterate") ? statement.iterate.bind(statement) : void 0; statement.run = (...bindings) => execute(() => callStatement(run ?? native.StatementSync.prototype.run.bind(statement), bindings), schemaChange, control); statement.get = (...bindings) => execute(() => callStatement(get ?? native.StatementSync.prototype.get.bind(statement), bindings), schemaChange, control); statement.all = (...bindings) => execute(() => callStatement(all ?? native.StatementSync.prototype.all.bind(statement), bindings), schemaChange, control); statement.iterate = function* (...bindings) { if (owner.transactionalSchema && !owner.scope) bindScope(database, owner); settle(); const invalidates = schemaChange || control === "ROLLBACK" && owner.transactionalSchema; if (invalidates) invalidateSqliteSchemaFacts(database); try { yield* callStatement(iterate ?? native.StatementSync.prototype.iterate.bind(statement), bindings); } finally { if (invalidates) invalidateSqliteSchemaFacts(database); settle(Boolean(control)); } }; } return statement; }; if (typeof database.setAuthorizer === "function") database.setAuthorizer = (callback) => { native.DatabaseSync.prototype.setAuthorizer.call(database, callback); owner.authorizerActive = callback !== null; invalidate(owner); }; registerNodeSqliteDisposeCallback(database, () => { invalidate(owner); owner.dataVersion = void 0; owner.readDataVersion = void 0; if (owner.scope) { scopes.finalizer.unregister(owner); scopes.release(owner.scope); owner.scope = void 0; owner.scopeRevision = void 0; } }); } /** Share freshness only within this synchronous call stack, never across an await. */ function runSqliteReadOperationSync(database, operation) { const owner = owners.get(database); if (!owner?.admitted || owner.authorizerActive) return operation(); owner.readDepth += 1; try { return operation(); } finally { owner.readDepth -= 1; if (owner.readDepth === 0) owner.readDataVersion = void 0; } } /** Always execute a fresh probe; compare versions only on the same connection. */ function readSqliteDataVersion(database) { const row = executeWithCachedStatement(database, "PRAGMA data_version", [], (statement) => statement.get()); if (typeof row?.data_version !== "number") throw new Error("SQLite did not return a numeric PRAGMA data_version"); return row.data_version; } /** Foreign commits are observed on the next operation; SQLite owns snapshot visibility. */ function readSqliteCacheDataVersion(database) { const tracked = owners.get(database); const owner = tracked?.admitted ? tracked : void 0; if (owner && !owner.authorizerActive && owner.readDataVersion !== void 0) return owner.readDataVersion; const dataVersion = readSqliteDataVersion(database); if (owner) { if (owner.dataVersion !== dataVersion) { const facts = owner.facts; if (!(facts && runSqlitePinnedReadSnapshotSync(database, (schemaVersion) => { const userVersion = executeWithCachedStatement(database, "PRAGMA user_version", [], (s) => s.get()); return facts.schemaVersion === schemaVersion && facts.userVersion === userVersion?.user_version; }))) invalidate(owner); owner.dataVersion = dataVersion; } if (owner.readDepth > 0 && !owner.authorizerActive) owner.readDataVersion = dataVersion; } return dataVersion; } /** Install at native open, before callers can retain statements or install an authorizer. */ function trackSqliteSchema(database, native) { if (!owners.has(database)) { const owner = { admitted: false, revision: 0, readDepth: 0, transactionalSchema: false, transactionalFacts: false, authorizerActive: false }; owners.set(database, owner); trackSchemaChanges(database, owner, native); } } /** Only database admission opts a connection into retained schema facts. */ function admitSqliteSchema(database) { const owner = owners.get(database); if (!owner) throw new Error("SQLite schema admission requires a connection tracked from native open"); owner.admitted = true; getAdmittedSqliteSchemaFacts(database); } /** Schema changes revoke the admission; ordinary reads consume its recorded facts. */ function getAdmittedSqliteSchemaFacts(database) { const owner = owners.get(database); if (!owner?.admitted || owner.authorizerActive) return; readSqliteCacheDataVersion(database); const snapshot = getSqlitePinnedReadSnapshot(database); if (owner.snapshot && owner.snapshot !== snapshot) { invalidate(owner); owner.snapshot = void 0; } const scope = bindScope(database, owner); if (owner.scopeRevision !== scope.revision) { invalidate(owner); owner.scopeRevision = scope.revision; } if ((owner.transactionalSchema || owner.transactionalFacts) && !database.isTransaction) { if (owner.transactionalSchema) publishSchemaChange(database, owner); invalidate(owner); owner.transactionalSchema = false; owner.transactionalFacts = false; } if (!owner.facts) { owner.snapshot = snapshot; owner.transactionalFacts = database.isTransaction; owner.facts = runSqlitePinnedReadSnapshotSync(database, (schemaVersion) => { const userVersion = executeWithCachedStatement(database, "PRAGMA user_version", [], (s) => s.get()); const tables = executeWithCachedStatement(database, "SELECT name, sql FROM main.sqlite_schema WHERE type = 'table'", [], (s) => s.all()); return { revision: owner.revision, userVersion: Number(userVersion?.user_version ?? 0), schemaVersion, tables: new Set(tables.flatMap((row) => typeof row.name === "string" ? [row.name] : [])), tableSql: new Map(tables.flatMap((row) => typeof row.name === "string" ? [[row.name, typeof row.sql === "string" ? row.sql : null]] : [])) }; }); } return owner.facts; } //#endregion //#region src/infra/warning-filter.ts const warningFilterKey = Symbol.for("openclaw.warning-filter"); function shouldIgnoreWarning(warning) { return Boolean(warning.code === "DEP0040" && warning.message?.includes("punycode") || warning.code === "DEP0060" && warning.message?.includes("util._extend") || warning.name === "ExperimentalWarning" && warning.message?.includes("SQLite is an experimental feature")); } function normalizeWarningArgs(args) { const warningArg = args[0]; const secondArg = args[1]; const thirdArg = args[2]; let name; let code; let message; if (warningArg instanceof Error) { name = warningArg.name; message = warningArg.message; code = warningArg.code; } else if (typeof warningArg === "string") message = warningArg; if (secondArg && typeof secondArg === "object" && !Array.isArray(secondArg)) { const options = secondArg; if (typeof options.type === "string") name = options.type; if (typeof options.code === "string") code = options.code; } else { if (typeof secondArg === "string") name = secondArg; if (typeof thirdArg === "string") code = thirdArg; } return { name, code, message }; } /** Installs the global process warning filter once for the current JS realm. */ function installProcessWarningFilter() { const state = resolveGlobalSingleton(warningFilterKey, () => ({ installed: false })); if (state.installed) return; const originalEmitWarning = process.emitWarning.bind(process); const wrappedEmitWarning = ((...args) => { if (shouldIgnoreWarning(normalizeWarningArgs(args))) return; if (args[0] instanceof Error && args[1] && typeof args[1] === "object" && !Array.isArray(args[1])) { const warning = args[0]; const emitted = Object.assign(new Error(warning.message), { name: warning.name, code: warning.code }); process.emit("warning", emitted); return; } Reflect.apply(originalEmitWarning, process, args); }); process.emitWarning = wrappedEmitWarning; state.installed = true; } //#endregion //#region src/infra/node-sqlite.ts const require = createRequire(import.meta.url); let validatedSqliteModule; function resolveSqliteFilesystemPath(pathname) { if (process.platform !== "win32") return pathname; return path.toNamespacedPath(path.resolve(pathname)); } function resolveNodeSqliteLocation(location) { if (location === "" || location === ":memory:" || location.startsWith("file:")) return location; return resolveSqliteFilesystemPath(location); } /** Preserve native Windows path prefixes before adding SQLite URI parameters. */ function resolveSqliteFileUriPath(pathname, platform) { if (platform === "win32") { const namespacedPath = path.win32.toNamespacedPath(path.win32.resolve(pathname)); return `file:${encodeURIComponent(namespacedPath)}`; } return pathToFileURL(path.resolve(pathname)).href; } /** Open an existing writable database without SQLite's create-if-missing flag. */ function resolveExistingSqliteFileUri(pathname, platform = process.platform) { return `${resolveSqliteFileUriPath(pathname, platform)}?mode=rw`; } function assertSqliteWalResetSafeVersion(version, nodeVersion) { if (isSqliteWalResetSafeVersion(version)) return; const variables = process.config?.variables; const isShared = variables?.node_shared_sqlite === true || variables?.node_shared_sqlite === "true"; throw new Error(`OpenClaw requires SQLite 3.51.3+, 3.50.7+ within 3.50.x, or 3.44.6+ within 3.44.x for WAL safety; Node ${nodeVersion} ${isShared ? "uses shared system" : "embeds"} SQLite ${version}, which is affected by the upstream WAL-reset database corruption bug. ${isShared ? "Upgrade the system SQLite library to one of those safe versions, or use a Node build embedding a safe version." : "Upgrade to Node 24.16.0+ or 26.1.0+ before retrying."}`); } function assertSafeSqliteRuntime(sqlite) { if (validatedSqliteModule === sqlite) return; const database = new sqlite.DatabaseSync(":memory:"); try { const row = database.prepare("SELECT sqlite_version() AS version").get(); const version = typeof row?.version === "string" ? row.version : "unknown"; assertSqliteWalResetSafeVersion(version, process.versions.node); compareValidSemver(version, "3.45.0"); database.prepare("SELECT sqlite_compileoption_used('OMIT_LOAD_EXTENSION') AS omitted").get()?.omitted; validatedSqliteModule = sqlite; } finally { database.close(); } } /** Load node:sqlite after installing the process warning filter. */ function requireNodeSqlite() { installProcessWarningFilter(); try { ensureSqliteLibrarySelected(); const sqlite = require("node:sqlite"); assertSafeSqliteRuntime(sqlite); return sqlite; } catch (err) { const message = formatErrorMessage(err); throw new Error(`SQLite support is unavailable or unsafe in this Node runtime. ${message}`, { cause: err }); } } /** Open node:sqlite through OpenClaw's runtime and filesystem-location boundary. */ function openNodeSqliteDatabase(location, options) { const sqlite = requireNodeSqlite(); const resolvedLocation = resolveNodeSqliteLocation(location); const database = new sqlite.DatabaseSync(resolvedLocation, options ?? {}); trackSqliteSchema(database, sqlite); if (!getSqliteRuntimeCapabilities().explicitSqliteCloseReleasesNativeResources) registerNodeSqliteDisposeCallback(database, () => {}); registerSqliteReaderConnection(database); return database; } //#endregion //#region src/infra/sqlite-busy-timeout.ts const lockFailureReportingByDatabase = /* @__PURE__ */ new WeakMap(); function normalizeSqliteNonNegativeInteger(value, label) { if (!Number.isInteger(value) || value < 0) throw new Error(`${label} must be a non-negative integer`); return value; } function readSqliteBusyTimeout(database) { const row = database.prepare("PRAGMA busy_timeout").get(); const value = row?.busy_timeout ?? row?.timeout; return typeof value === "bigint" ? Number(value) : Number(value ?? 0); } function setSqliteBusyTimeout(database, busyTimeoutMs) { const normalizedTimeoutMs = normalizeSqliteNonNegativeInteger(busyTimeoutMs, "busyTimeoutMs"); database.exec(`PRAGMA busy_timeout = ${normalizedTimeoutMs}`); } function shouldReportSqliteLockFailure(database) { return lockFailureReportingByDatabase.get(database) !== "suppress"; } /** Run with a temporary busy policy; restore early when write admission finishes. */ function runWithSqliteBusyTimeout(database, busyTimeoutMs, operation, options = {}) { const normalizedTimeoutMs = normalizeSqliteNonNegativeInteger(busyTimeoutMs, "busyTimeoutMs"); const previousBusyTimeoutMs = readSqliteBusyTimeout(database); const previousLockFailureReporting = lockFailureReportingByDatabase.get(database); if (options.lockFailureReporting) lockFailureReportingByDatabase.set(database, options.lockFailureReporting); if (previousBusyTimeoutMs !== normalizedTimeoutMs) setSqliteBusyTimeout(database, normalizedTimeoutMs); const restore = () => { if (database.isOpen && previousBusyTimeoutMs !== normalizedTimeoutMs) setSqliteBusyTimeout(database, previousBusyTimeoutMs); if (previousLockFailureReporting) lockFailureReportingByDatabase.set(database, previousLockFailureReporting); else lockFailureReportingByDatabase.delete(database); }; try { return operation(restore); } finally { restore(); } } var OpenClawStateDatabaseSchemaMigrationRequiredError = class extends StartupMaintenanceRequiredError { constructor(kind, pathname) { super(kind, `OpenClaw state database schema migration required (${kind}) at ${pathname}; run openclaw doctor --fix to migrate it.`); this.kind = kind; this.pathname = pathname; this.name = "OpenClawStateDatabaseSchemaMigrationRequiredError"; } }; /** Runtime readers report malformed legacy state without entering repair mode. */ function normalizeOpenClawStateSchemaReadError(error, pathname) { if (error instanceof Error && error.message.startsWith(`malformed database schema (idx_skill_workshop_collection_reviews_workspace_time)`)) { const required = new OpenClawStateDatabaseSchemaMigrationRequiredError("legacy-workshop-review-index", pathname); required.cause = error; return required; } return error; } //#endregion export { quoteSqliteIdentifier as A, iterateSqliteQuerySync as B, readDatabasePathIdentitySync as C, normalizeSchemaSql as D, findSqlClosingParenthesis as E, compileSqliteQueryBindings as F, captureSqliteReaderOwner as H, createSqliteQueryCache as I, executeSqliteQuerySync as L, readTableConstraintKeyword as M, splitSqlList as N, normalizeSqlIdentifier as O, runSqlitePinnedReadSnapshotSync as P, executeSqliteQueryTakeFirstSync as R, readDatabasePathIdentity as S, findSqlCharacter as T, readSqliteReaderDiagnosticsForPath as U, prepareSqliteQuerySync as V, sqliteReaderDatabasePathKey as W, findChangedDatabasePaths as _, runWithSqliteBusyTimeout as a, readDatabaseFileIdentity as b, openNodeSqliteDatabase as c, resolveSqliteFilesystemPath as d, admitSqliteSchema as f, assertExistingDatabaseIdentity as g, runSqliteReadOperationSync as h, readSqliteBusyTimeout as i, readSqlToken as j, normalizeSqlWhitespace as k, requireNodeSqlite as l, invalidateSqliteSchemaFacts as m, normalizeOpenClawStateSchemaReadError as n, setSqliteBusyTimeout as o, getAdmittedSqliteSchemaFacts as p, normalizeSqliteNonNegativeInteger as r, shouldReportSqliteLockFailure as s, OpenClawStateDatabaseSchemaMigrationRequiredError as t, resolveExistingSqliteFileUri as u, inspectDatabasePathIdentitySync as v, extractSqliteTableSchema as w, readDatabaseIdentityBirthtime as x, normalizeDatabasePath as y, getNodeSqliteKysely as z };