| import { isSupportedOpenClawNodeVersion, parseNodeReleaseVersion } from "./node-version.mjs"; |
|
|
| |
| function probeSqlite(DatabaseSync) { |
| const result = { available: false, version: null, text: false, blob: false, json: false }; |
| let database; |
| try { |
| database = new DatabaseSync(":memory:"); |
| result.available = true; |
| const version = database.prepare("SELECT sqlite_version() AS version").get()?.version; |
| result.version = typeof version === "string" ? version : null; |
| const text = "a\u0000b\u0000"; |
| const bytes = Buffer.from(text, "utf8"); |
| const json = JSON.stringify({ value: text }); |
| database.exec("CREATE TABLE probe (text_value TEXT, blob_value BLOB, json_value TEXT)"); |
| database.prepare("INSERT INTO probe VALUES (?, ?, ?)").run(text, bytes, json); |
| const row = database.prepare("SELECT text_value, blob_value, json_value FROM probe").get(); |
| result.text = |
| typeof row?.text_value === "string" && |
| row.text_value.length === text.length && |
| Buffer.from(row.text_value, "utf8").equals(bytes); |
| result.blob = |
| row?.blob_value instanceof Uint8Array && Buffer.from(row.blob_value).equals(bytes); |
| result.json = row?.json_value === json && JSON.parse(row.json_value).value === text; |
| } catch (error) { |
| result.error = error instanceof Error ? error.message : String(error); |
| } finally { |
| database?.close(); |
| } |
| return result; |
| } |
|
|
| |
| const capabilityCacheKey = Symbol.for("openclaw.sqliteCapabilities"); |
| function unavailableSqliteCapabilities(error) { |
| return { |
| available: false, |
| version: null, |
| text: false, |
| blob: false, |
| json: false, |
| error: error instanceof Error ? error.message : String(error), |
| }; |
| } |
|
|
| function isSqliteCapabilities(value) { |
| return ( |
| value !== null && |
| typeof value === "object" && |
| typeof value.available === "boolean" && |
| (value.version === null || typeof value.version === "string") && |
| typeof value.text === "boolean" && |
| typeof value.blob === "boolean" && |
| typeof value.json === "boolean" && |
| (value.error === undefined || typeof value.error === "string") |
| ); |
| } |
|
|
| async function probeCurrentSqliteInWorker() { |
| let worker; |
| try { |
| if (typeof process.getBuiltinModule?.("node:sqlite")?.DatabaseSync !== "function") { |
| return unavailableSqliteCapabilities(new Error("node:sqlite is unavailable")); |
| } |
| const env = Object.fromEntries( |
| Object.entries(process.env).filter(([name]) => !/^(NODE_OPTIONS|BUN_OPTIONS)$/i.test(name)), |
| ); |
| env.NODE_NO_WARNINGS = "1"; |
| |
| const execArgv = |
| !process.versions.bun && process.allowedNodeEnvironmentFlags?.has("--experimental-sqlite") |
| ? ["--experimental-sqlite"] |
| : []; |
| const { Worker } = await import("node:worker_threads"); |
| worker = new Worker( |
| `const { parentPort } = require("node:worker_threads"); |
| parentPort.postMessage(${SQLITE_CAPABILITY_PROBE}); |
| parentPort.close();`, |
| { eval: true, env, execArgv }, |
| ); |
| } catch (error) { |
| return unavailableSqliteCapabilities(error); |
| } |
| return new Promise((resolve) => { |
| let result; |
| let failure; |
| let retirement; |
| const fail = (error) => { |
| failure ??= { error }; |
| retirement ??= Promise.resolve() |
| .then(() => worker.terminate()) |
| .catch((error) => { |
| failure ??= { error }; |
| }); |
| }; |
| const onMessage = (value) => { |
| if (result !== undefined || !isSqliteCapabilities(value)) { |
| fail(new Error("SQLite capability worker returned an invalid result")); |
| return; |
| } |
| result = value; |
| }; |
| const onExit = (code) => { |
| worker.off("message", onMessage); |
| worker.off("error", fail); |
| worker.off("messageerror", fail); |
| if (failure) { |
| resolve(unavailableSqliteCapabilities(failure.error)); |
| } else if (code !== 0 || result === undefined) { |
| resolve( |
| unavailableSqliteCapabilities( |
| new Error(`SQLite capability worker exited without a complete result (code ${code})`), |
| ), |
| ); |
| } else { |
| |
| resolve(result); |
| } |
| }; |
| worker.on("message", onMessage); |
| worker.on("error", fail); |
| worker.on("messageerror", fail); |
| worker.once("exit", onExit); |
| }); |
| } |
|
|
| export function detectCurrentSqliteCapabilities() { |
| |
| globalThis[capabilityCacheKey] ??= Promise.resolve().then(probeCurrentSqliteInWorker); |
| return Promise.resolve(globalThis[capabilityCacheKey]); |
| } |
|
|
| export function isSqliteWalResetSafeVersion(value) { |
| const match = /^(\d+)\.(\d+)\.(\d+)$/u.exec(value.trim()); |
| if (!match) { |
| return false; |
| } |
| const [major, minor, patch] = match.slice(1).map(Number); |
| if (![major, minor, patch].every(Number.isSafeInteger)) { |
| return false; |
| } |
| return ( |
| major > 3 || |
| (major === 3 && |
| (minor > 51 || |
| (minor === 51 && patch >= 3) || |
| (minor === 50 && patch >= 7) || |
| (minor === 44 && patch >= 6))) |
| ); |
| } |
|
|
| export function nodeRuntimeFailure(version, probe) { |
| const label = `Node ${version ?? "unknown"}`; |
| if (!probe.available) { |
| return `${label}: node:sqlite is unavailable; use 24.16+/26.1+ or a build with the fix.`; |
| } |
| if (!probe.text && !probe.error) { |
| return `${label}: node:sqlite truncates TEXT at embedded NUL (nodejs/node#61954); use 24.16+/26.1+ or a build with the fix`; |
| } |
| if (probe.error || !probe.text || !probe.blob || !probe.json) { |
| return `${label}: node:sqlite NUL round-trip capability probe failed; use 24.16+/26.1+ or a build with the fix.`; |
| } |
| if (!probe.version || !isSqliteWalResetSafeVersion(probe.version)) { |
| return `${label}: SQLite ${probe.version ?? "unknown"} is not WAL-reset-safe; use SQLite 3.51.3+, 3.50.7+, or 3.44.6+ on its patched release line.`; |
| } |
| |
| const release = parseNodeReleaseVersion(version); |
| if (!release || release.major < 24) { |
| return `${label}: openclaw requires Node >=24.16.0 <25, or >=26.1.0.`; |
| } |
| return null; |
| } |
|
|
| export function nodeRuntimeNote(version, probe) { |
| return !nodeRuntimeFailure(version, probe) && !isSupportedOpenClawNodeVersion(version) |
| ? `Node ${version}: unsupported version, capability probe passed. Supported releases: 24.16+/26.1+.` |
| : null; |
| } |
|
|
| export const SQLITE_CAPABILITY_PROBE = `(${probeSqlite.toString()})(require("node:sqlite").DatabaseSync)`; |
|
|