UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

257 lines (256 loc) 13.8 kB
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 };