File size: 6,829 Bytes
205131a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 | import { isSupportedOpenClawNodeVersion, parseNodeReleaseVersion } from "./node-version.mjs";
// Self-contained so a different runtime can execute exactly the same probe with -e.
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;
}
// The launcher and bundled runtime chunks must share one process-local 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";
// Older diagnostic runtimes need SQLite enabled, but must not replay entry preloads.
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 {
// The scratch database and worker must both close before startup is admitted.
resolve(result);
}
};
worker.on("message", onMessage);
worker.on("error", fail);
worker.on("messageerror", fail);
worker.once("exit", onExit);
});
}
export function detectCurrentSqliteCapabilities() {
// Publish the Promise before Worker construction can notify another startup caller.
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.`;
}
// Node 22/23 remain excluded until their separate compatibility work is complete.
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)`;
|