openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
257 lines (256 loc) • 13.8 kB
JavaScript
import { t as formatErrorMessage } from "./errors-Db3Ymjlb.js";
import { h as clearNodeSqliteKyselyCacheForDatabase, r as resolveImmutableSqliteFileUri, t as openNodeSqliteDatabase } from "./node-sqlite-BpQX3W0e.js";
import { a as getNodeSqliteKysely, r as executeSqliteQuerySync } from "./kysely-sync-COmh4HWh.js";
import { i as prepareSqliteReadOnlyLocationSync } from "./sqlite-readonly-location-BC9PgENz.js";
import { N as collectSqliteSchemaIssues, S as OPENCLAW_STATE_SCHEMA_SQL, _ as OPENCLAW_STATE_MAINTENANCE_SCHEMA_COMPATIBILITY, b as isOpenClawStateFirstUseSchemaIssue, l as assertOpenClawStateDatabaseForMaintenance, u as assertOpenClawStateDatabaseOwner, v as STATE_PERSISTENT_SCHEMA_COMPATIBILITY, x as isOpenClawStateStartupRepairableSchemaIssue, y as getOpenClawStateRuntimeSchema } from "./openclaw-state-db-cache-C7ljO0xP.js";
import { a as OPENCLAW_DATABASE_SCHEMA_DOCS_URL, o as OPENCLAW_SQLITE_BUSY_TIMEOUT_MS } from "./openclaw-state-db-contract-DYCYxE4w.js";
import { a as readSqliteUserVersion, r as describeRunningOpenClawBuild, t as SqliteSchemaVersionError } from "./sqlite-user-version-DFJCxX41.js";
import { i as resolveOpenClawRegisteredAgentDatabasePath, s as resolveOpenClawStateSqlitePath } from "./openclaw-state-db-schema-version-c1ZL6JGz.js";
import { t as assertSqliteIntegrity } from "./sqlite-integrity-NpEtFIdK.js";
import { g as inspectOpenClawStateOwnershipFromDatabase } from "./openclaw-state-db-BRTnL-D8.js";
import { t as assertOpenClawAgentDatabaseForMaintenance } from "./openclaw-agent-db-maintenance-wTIy-jt-.js";
import "./openclaw-agent-db-contract-CGTyjij4.js";
import { t as discoverAgentDatabaseMigrationTargets } from "./state-migrations.media-persistence-targets-D45MAuwp.js";
import { existsSync, realpathSync } from "node:fs";
import path from "node:path";
//#region src/state/openclaw-database-preflight.ts
function formatDoctorIncompatibleDatabase(database) {
const agent = database.agentId ? ` for agent ${database.agentId}` : "";
const writer = database.writerAppVersion ? `; writer build ${database.writerAppVersion}` : "";
return `${database.kind} database${agent} ${database.path} uses schema ${database.foundVersion}; this build supports ${database.supportedVersion}${writer}.`;
}
/** Fatal refusal when persisted schemas were written by a newer build. */
var OpenClawDatabaseSchemaPreflightError = class extends SqliteSchemaVersionError {
constructor(incompatibleDatabases, options = {}) {
const operation = options.operation ?? "gateway-startup";
const prefix = operation === "doctor" ? "Doctor refused to continue" : operation === "gateway-restart" ? "Gateway refused restart" : "Gateway refused startup";
const doctorGuidance = operation === "doctor" ? ` ${incompatibleDatabases.map(formatDoctorIncompatibleDatabase).join(" ")} Run Doctor with the OpenClaw install that wrote this state (typically the active Gateway install), or another build that supports these schemas.` : "";
super(`${prefix} because ${incompatibleDatabases.length} OpenClaw database schema(s) are newer than this build. Refused by ${describeRunningOpenClawBuild()}.${doctorGuidance} See ${OPENCLAW_DATABASE_SCHEMA_DOCS_URL}.`);
this.incompatibleDatabases = incompatibleDatabases;
this.name = "OpenClawDatabaseSchemaPreflightError";
}
};
/** Verify persisted runtime schemas before certifying repair or accepting restart. */
function assertOpenClawDatabasesReady(options) {
const schemas = preflightOpenClawDatabaseSchemas({
env: options.env,
supportedVersions: {
state: 15,
agent: 19
},
verifyCurrentSchemaShape: true,
...options.operation === "doctor" ? { configuredAgentDatabaseTargets: options.configuredAgentDatabaseTargets } : {}
});
if (schemas.incompatible.length > 0) throw new OpenClawDatabaseSchemaPreflightError(schemas.incompatible, { operation: options.operation });
if (schemas.indeterminate.length === 0) return;
const shown = schemas.indeterminate.slice(0, 3).map((database) => `${database.kind} ${database.path}: ${database.reason}`);
const omitted = schemas.indeterminate.length - shown.length;
const action = options.operation === "doctor" ? "Doctor could not complete repair" : "Gateway refused restart";
throw new Error(`${action} because persisted database readiness could not be verified: ${shown.join("; ")}${omitted > 0 ? `; +${omitted} more` : ""}. Stop the Gateway and other OpenClaw processes, run openclaw doctor --fix, then retry.`);
}
function readWriterAppVersion(database) {
try {
const row = database.prepare("SELECT app_version FROM schema_meta WHERE meta_key = 'primary' LIMIT 1").get();
return typeof row?.app_version === "string" && row.app_version.length > 0 ? row.app_version : void 0;
} catch {
return;
}
}
function readRegisteredAgentDatabases(database, registryPath) {
if (!database.prepare("SELECT 1 AS ok FROM sqlite_master WHERE type = 'table' AND name = 'agent_databases'").get()) return [];
const db = getNodeSqliteKysely(database);
return executeSqliteQuerySync(database, db.selectFrom("agent_databases").select(["agent_id", "path"])).rows.flatMap((row) => typeof row.agent_id === "string" && typeof row.path === "string" ? [{
agentId: row.agent_id,
path: resolveOpenClawRegisteredAgentDatabasePath(registryPath, row.path)
}] : []);
}
function deduplicateSchemaIssues(issues) {
return [...new Map(issues.map((issue) => [`${issue.code}\0${issue.objectName}`, issue])).values()];
}
/** Compare one explicit SQLite file with this release's canonical shared-state schema. */
async function preflightOpenClawStateDatabasePath(databasePath) {
const resolvedPath = path.resolve(databasePath);
const base = {
schema: "openclaw.state-schema-preflight.v1",
databasePath: resolvedPath,
targetVersion: 15
};
let database;
let foundVersion = null;
let ownership = null;
const result = (status, details = {}) => ({
...base,
foundVersion,
ownership,
issues: details.issues ?? [],
status,
requiresWrite: details.requiresWrite ?? false,
...details.reason ? { reason: details.reason } : {}
});
try {
const inspectionPath = realpathSync.native(resolvedPath);
const sidecars = [
"-wal",
"-shm",
"-journal"
].filter((suffix) => existsSync(`${inspectionPath}${suffix}`));
if (sidecars.length > 0) throw new Error(`SQLite preflight requires a consolidated snapshot with no sidecars; found ${sidecars.join(", ")}. Create a WAL-aware online backup and preflight the resulting standalone file.`);
database = openNodeSqliteDatabase(resolveImmutableSqliteFileUri(inspectionPath), { readOnly: true });
database.exec(`PRAGMA busy_timeout = ${OPENCLAW_SQLITE_BUSY_TIMEOUT_MS}; PRAGMA query_only = ON; PRAGMA trusted_schema = OFF;`);
assertSqliteIntegrity(database, resolvedPath);
foundVersion = readSqliteUserVersion(database);
if (!Number.isSafeInteger(foundVersion) || foundVersion < 0) throw new Error(`OpenClaw state database ${resolvedPath} has invalid schema version metadata.`);
if (foundVersion > 15) {
try {
ownership = inspectOpenClawStateOwnershipFromDatabase(database, resolvedPath);
} catch {}
return result("incompatible");
}
ownership = inspectOpenClawStateOwnershipFromDatabase(database, resolvedPath);
if (foundVersion < 15) return result("migration-required", { requiresWrite: true });
assertOpenClawStateDatabaseOwner(database, { pathname: resolvedPath });
const metadata = database.prepare("SELECT schema_version FROM schema_meta WHERE meta_key = 'primary' LIMIT 1").get();
if (metadata?.schema_version !== foundVersion) throw new Error(`OpenClaw state database ${resolvedPath} metadata schema version ${typeof metadata?.schema_version === "number" ? metadata.schema_version : "invalid"} does not match ${foundVersion}.`);
const maintenanceIssues = collectSqliteSchemaIssues(database, OPENCLAW_STATE_SCHEMA_SQL, OPENCLAW_STATE_MAINTENANCE_SCHEMA_COMPATIBILITY);
const blockingIssues = maintenanceIssues.filter((issue) => !isOpenClawStateStartupRepairableSchemaIssue(issue) && !isOpenClawStateFirstUseSchemaIssue(issue));
if (blockingIssues.length > 0) return result("incompatible", { issues: deduplicateSchemaIssues(blockingIssues) });
const projectedRuntimeIssues = collectSqliteSchemaIssues(database, getOpenClawStateRuntimeSchema({ includeVersionLazyAdditiveTables: false }), STATE_PERSISTENT_SCHEMA_COMPATIBILITY);
const projectedRuntimeBlockingIssues = projectedRuntimeIssues.filter((issue) => !isOpenClawStateStartupRepairableSchemaIssue(issue) && !isOpenClawStateFirstUseSchemaIssue(issue));
if (projectedRuntimeBlockingIssues.length > 0) return result("incompatible", { issues: deduplicateSchemaIssues(projectedRuntimeBlockingIssues) });
const startupRepairableIssues = deduplicateSchemaIssues([...maintenanceIssues.filter(isOpenClawStateStartupRepairableSchemaIssue), ...projectedRuntimeIssues.filter(isOpenClawStateStartupRepairableSchemaIssue)]);
return result(startupRepairableIssues.length > 0 ? "startup-repairable" : "exact", {
issues: startupRepairableIssues,
requiresWrite: startupRepairableIssues.length > 0
});
} catch (error) {
return result("indeterminate", { reason: formatErrorMessage(error) });
} finally {
database?.close();
}
}
/** Read schema headers and optionally verify current schema shape without repairing it. */
function preflightOpenClawDatabaseSchemas(options) {
const result = {
incompatible: [],
indeterminate: []
};
const statePath = path.resolve(resolveOpenClawStateSqlitePath(options.env));
let registeredDatabases = [];
let stateDatabase;
let stateSnapshot;
try {
if (existsSync(statePath)) {
stateSnapshot = prepareSqliteReadOnlyLocationSync(realpathSync.native(statePath));
stateDatabase = openNodeSqliteDatabase(stateSnapshot.location, { readOnly: true });
stateDatabase.exec(`PRAGMA busy_timeout = ${OPENCLAW_SQLITE_BUSY_TIMEOUT_MS};`);
const stateVersion = readSqliteUserVersion(stateDatabase);
if (stateVersion > options.supportedVersions.state) {
const writerAppVersion = readWriterAppVersion(stateDatabase);
result.incompatible.push({
kind: "state",
path: statePath,
foundVersion: stateVersion,
supportedVersion: options.supportedVersions.state,
...writerAppVersion ? { writerAppVersion } : {}
});
}
if (options.verifyCurrentSchemaShape === true && stateVersion === 15) try {
assertOpenClawStateDatabaseForMaintenance(stateDatabase, { pathname: statePath });
} catch (error) {
result.indeterminate.push({
kind: "state",
path: statePath,
reason: formatErrorMessage(error)
});
}
try {
registeredDatabases = readRegisteredAgentDatabases(stateDatabase, statePath);
} catch (error) {
result.indeterminate.push({
kind: "state",
path: statePath,
reason: `agent database registry query failed: ${formatErrorMessage(error)}`
});
return result;
}
}
} catch (error) {
result.indeterminate.push({
kind: "state",
path: statePath,
reason: formatErrorMessage(error)
});
return result;
} finally {
try {
if (stateDatabase) {
clearNodeSqliteKyselyCacheForDatabase(stateDatabase);
stateDatabase.close();
}
} finally {
stateSnapshot?.cleanup();
}
}
let agentTargets = registeredDatabases;
if (options.configuredAgentDatabaseTargets !== void 0) {
const configuredTargets = typeof options.configuredAgentDatabaseTargets === "function" ? options.configuredAgentDatabaseTargets(registeredDatabases) : options.configuredAgentDatabaseTargets;
const discovery = discoverAgentDatabaseMigrationTargets({
env: options.env,
configuredAgentDatabaseTargets: configuredTargets,
registeredAgentDatabases: registeredDatabases
});
agentTargets = discovery.targets;
for (const failure of discovery.failures) result.indeterminate.push({
kind: "agent",
...failure
});
}
const inspectionTargets = [...agentTargets, ...(options.configuredAgentDatabaseCandidatePaths ?? []).map((candidatePath) => ({ path: candidatePath }))];
const inspectedAgentPaths = /* @__PURE__ */ new Set();
for (const row of inspectionTargets) {
const agentPath = row.path;
if (!existsSync(agentPath)) continue;
let agentDatabase;
let agentSnapshot;
try {
const realAgentPath = realpathSync.native(agentPath);
if (row.agentId === void 0 && inspectedAgentPaths.has(realAgentPath)) continue;
inspectedAgentPaths.add(realAgentPath);
agentSnapshot = prepareSqliteReadOnlyLocationSync(realAgentPath);
agentDatabase = openNodeSqliteDatabase(agentSnapshot.location, { readOnly: true });
agentDatabase.exec(`PRAGMA busy_timeout = ${OPENCLAW_SQLITE_BUSY_TIMEOUT_MS};`);
const agentVersion = readSqliteUserVersion(agentDatabase);
if (agentVersion <= options.supportedVersions.agent) {
if (options.verifyCurrentSchemaShape === true && row.agentId !== void 0) assertOpenClawAgentDatabaseForMaintenance(agentDatabase, {
agentId: row.agentId,
pathname: agentPath
});
continue;
}
const writerAppVersion = readWriterAppVersion(agentDatabase);
result.incompatible.push({
kind: "agent",
path: agentPath,
...row.agentId !== void 0 ? { agentId: row.agentId } : {},
foundVersion: agentVersion,
supportedVersion: options.supportedVersions.agent,
...writerAppVersion ? { writerAppVersion } : {}
});
} catch (error) {
result.indeterminate.push({
kind: "agent",
path: agentPath,
reason: formatErrorMessage(error)
});
} finally {
try {
agentDatabase?.close();
} finally {
agentSnapshot?.cleanup();
}
}
}
return result;
}
//#endregion
export { preflightOpenClawStateDatabasePath as i, assertOpenClawDatabasesReady as n, preflightOpenClawDatabaseSchemas as r, OpenClawDatabaseSchemaPreflightError as t };