UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

229 lines (228 loc) • 10.3 kB
import { i as collectNestedErrorCandidates } from "./error-coercion-W9BtigRm.mjs"; import { t as formatErrorMessage } from "./errors-BvOkmQM5.mjs"; import { o as resolveImmutableSqliteFileUri, r as openNodeSqliteDatabase } from "./node-sqlite-DyeJqxPf.mjs"; import "./openclaw-state-db-contract-BYv0nUQr.mjs"; import { c as resolveRuntimeServiceCommit, s as resolveRuntimeServiceBuildId, t as VERSION } from "./version-D0DaWqMn.mjs"; import { c as resolveOpenClawStateSqlitePath } from "./openclaw-state-db.paths-CmKkMYdo.mjs"; import { u as getOpenClawDatabaseMaintenanceScope } from "./openclaw-state-db-async-lifecycle-DwFy2ZTk.mjs"; import { i as needsOpenClawStateDatabaseSchemaRepair } from "./openclaw-state-db-fast-path-Kg79WElU.mjs"; import { a as sameFileMutationFingerprint } from "./file-descriptor-BWLpOgrg.mjs"; import { n as assertSqliteIntegrity, s as isTerminalSqliteIntegrityError } from "./sqlite-integrity-D3yHPOet.mjs"; import "./openclaw-agent-db-contract-OYt2DCoK.mjs"; import { p as syncDirectorySync, s as requireDirectorySync } from "./directory-durability-r17SFK6C.mjs"; import { t as createDoctorRehearsalDatabaseCoverage } from "./doctor-rehearsal-databases-BykEBJcP.mjs"; import { t as DoctorMaintenanceRefusalError } from "./update-doctor-result-Buvixhp2.mjs"; import { n as recordDoctorMigrationBackups } from "./doctor-migration-backup-artifacts-DXCEfd4j.mjs"; import { closeSync, existsSync, fsyncSync, lstatSync, openSync, realpathSync, statSync, unlinkSync } from "node:fs"; import path from "node:path"; import { createHash } from "node:crypto"; //#region src/commands/doctor-migration-backup.ts /** Preserve the old database generation before Doctor advances its schemas. */ async function backupDoctorMigrationDatabases(params) { const sharedPath = resolveOpenClawStateSqlitePath(params.env); const pending = new Set(params.pendingDatabasePaths); if (existsSync(sharedPath) && needsOpenClawStateDatabaseSchemaRepair(sharedPath)) pending.add(sharedPath); if (pending.size === 0) return { changes: [], warnings: [] }; const maintenance = getOpenClawDatabaseMaintenanceScope(); if (!maintenance?.ownsSchemaMaintenance) throw new Error("Pre-migration SQLite backups require Doctor maintenance ownership."); const backup = () => backupDoctorSqliteDatabases({ ...params, pendingDatabasePaths: [...pending], authority: { assertCurrent: () => maintenance.assertAdmission() } }); try { return await backup(); } catch (error) { if (error instanceof DoctorMaintenanceRefusalError || !params.agentDatabaseTargets?.length || !collectNestedErrorCandidates(error).some((cause) => cause instanceof Error && isTerminalSqliteIntegrityError(cause))) throw error; const { repairDoctorSessionWindowsBeforeMigration } = await import("./state-migrations.session-window-repair-B5bPALKW.mjs"); const changes = await repairDoctorSessionWindowsBeforeMigration({ env: params.env, targets: params.agentDatabaseTargets }); if (changes.length === 0) throw error; const result = await backup(); return { ...result, changes: [...changes, ...result.changes] }; } } /** Schema and same-schema repairs share verified snapshots under their existing Doctor owner. */ async function backupDoctorSqliteDatabases(params) { const pending = new Set(params.pendingDatabasePaths); if (pending.size === 0) return { changes: [], warnings: [] }; const sharedPath = resolveOpenClawStateSqlitePath(params.env); if (existsSync(sharedPath)) pending.add(sharedPath); const { authority } = params; authority.assertCurrent(); const disposable = params.requireSnapshots ? void 0 : createDoctorRehearsalDatabaseCoverage(params.env); disposable?.admit([...pending, ...params.databasePaths]); const retainedPaths = [.../* @__PURE__ */ new Set([...pending, ...params.databasePaths])].filter((pathname) => !disposable?.excludes(pathname)); if (retainedPaths.length === 0) { disposable?.assertCurrent(); return { changes: [], warnings: [] }; } const { createVerifiedSqliteSnapshot } = await import("./sqlite-snapshot-Chf2pY80.mjs"); const { sanitizeOpenClawStateLeaseRows } = await import("./openclaw-state-snapshot-sanitizer-Bs_m9sXO.mjs"); authority.assertCurrent(); disposable?.assertCurrent(); const sources = [...new Set([...pending].filter((pathname) => !disposable?.excludes(pathname)).map((pathname) => realpathSync.native(pathname)))]; if (!params.requireSnapshots && sources.length > 0 && sources.every((sourcePath) => { const { dev, ino } = statSync(sourcePath); return dev !== 0 && ino !== 0 && params.verifiedSnapshots?.some((snapshot) => snapshot.dev === dev && snapshot.ino === ino); })) return { changes: [], warnings: [] }; const inventory = [.../* @__PURE__ */ new Set([...sources, ...retainedPaths.map((pathname) => realpathSync.native(pathname))])].toSorted().map((pathname) => ({ path: pathname, identity: statSync(pathname, { bigint: true }) })); const backupDigest = createHash("sha256").update(JSON.stringify([ ...params.repair ? [params.repair.key] : [], VERSION, resolveRuntimeServiceBuildId(), resolveRuntimeServiceCommit(), 20, 25, inventory.map(({ path, identity }) => [ path, String(identity.dev), String(identity.ino) ]) ])).digest("hex"); const backupId = [ backupDigest.slice(0, 8), backupDigest.slice(8, 12), `8${backupDigest.slice(13, 16)}`, `8${backupDigest.slice(17, 20)}`, backupDigest.slice(20, 32) ].join("-"); const assertInventory = () => { authority.assertCurrent(); disposable?.assertCurrent(); for (const { path: pathname, identity } of inventory) { const current = statSync(pathname, { bigint: true }); if (!current.isFile() || current.dev !== identity.dev || current.ino !== identity.ino) throw new Error(`Pre-migration SQLite backup source changed: ${pathname}`); } }; const changes = []; const targetFor = (sourcePath) => `${sourcePath}.pre-startup-migration-${backupId}.bak`; const capturePath = `${targetFor(existsSync(sharedPath) ? realpathSync.native(sharedPath) : inventory[0].path)}.capturing`; let capture = lstatSync(capturePath, { bigint: true, throwIfNoEntry: false }); const hasSnapshots = inventory.some(({ path: sourcePath }) => lstatSync(targetFor(sourcePath), { throwIfNoEntry: false }) !== void 0); if (!capture && !hasSnapshots) { assertInventory(); const descriptor = openSync(capturePath, "wx", 384); try { fsyncSync(descriptor); } finally { closeSync(descriptor); } capture = lstatSync(capturePath, { bigint: true }); } const assertCapture = () => { assertInventory(); if (capture && (!capture.isFile() || capture.size !== 0n || !sameFileMutationFingerprint(capture, lstatSync(capturePath, { bigint: true })))) throw new DoctorMaintenanceRefusalError(`Pre-migration SQLite backup capture marker changed: ${capturePath}`, { kind: "data-at-risk", reason: "incomplete-migration" }); }; const syncCaptureDirectory = () => requireDirectorySync(syncDirectorySync(path.dirname(capturePath)), "Migration backup directory"); if (capture) { assertCapture(); syncCaptureDirectory(); for (const { path: sourcePath } of inventory) { const targetPath = targetFor(sourcePath); const incomplete = lstatSync(targetPath, { bigint: true, throwIfNoEntry: false }); if (!incomplete) continue; assertCapture(); if (!incomplete.isFile() || !sameFileMutationFingerprint(incomplete, lstatSync(targetPath, { bigint: true }))) throw new Error(`Incomplete pre-migration SQLite backup changed: ${targetPath}`); unlinkSync(targetPath); changes.push(`Discarded incomplete pre-migration SQLite backup: ${targetPath}`); } } for (const { path: sourcePath } of inventory) { assertCapture(); const targetPath = targetFor(sourcePath); const existing = lstatSync(targetPath, { bigint: true, throwIfNoEntry: false }); if (existing) { try { if (!existing.isFile()) throw new Error("Backup must be a regular file"); const snapshot = openNodeSqliteDatabase(resolveImmutableSqliteFileUri(targetPath), { readOnly: true }); try { snapshot.exec("PRAGMA trusted_schema = OFF;"); assertCapture(); assertSqliteIntegrity(snapshot, targetPath); params.repair?.validate(snapshot); } finally { snapshot.close(); } if (!sameFileMutationFingerprint(existing, lstatSync(targetPath, { bigint: true }))) throw new Error("Backup changed during verification"); } catch (cause) { throw new DoctorMaintenanceRefusalError(`Cannot verify pre-migration SQLite backup: ${targetPath}. ${formatErrorMessage(cause)}. Restore the matching backup group before retrying; surviving snapshots were preserved.`, { kind: "data-at-risk", reason: "incomplete-migration" }, { cause }); } changes.push(`Reused pre-migration SQLite backup: ${targetPath}`); continue; } if (!capture) throw new DoctorMaintenanceRefusalError(`Completed pre-migration SQLite backup group is missing: ${targetPath}. Restore the matching backup group before retrying; surviving snapshots were preserved.`, { kind: "data-at-risk", reason: "incomplete-migration" }); const backup = await createVerifiedSqliteSnapshot({ sourcePath, targetPath, preserveRowIds: true, transform: sanitizeOpenClawStateLeaseRows, beforePublish: assertCapture, validate: params.repair?.validate }); assertCapture(); changes.push(`Saved pre-migration SQLite backup: ${backup.path}`); } assertCapture(); if (capture) unlinkSync(capturePath); syncCaptureDirectory(); assertInventory(); if (lstatSync(capturePath, { throwIfNoEntry: false })) throw new DoctorMaintenanceRefusalError(`Pre-migration SQLite backup capture marker appeared during verification: ${capturePath}`, { kind: "data-at-risk", reason: "incomplete-migration" }); const warnings = []; try { recordDoctorMigrationBackups(params.env, backupId, inventory); } catch (error) { warnings.push(`Migration backups remain protected; cleanup registration failed: ${formatErrorMessage(error)}`); } return { changes, warnings, snapshots: inventory.map(({ path: sourcePath }) => ({ sourcePath, path: targetFor(sourcePath) })) }; } //#endregion export { backupDoctorSqliteDatabases as n, backupDoctorMigrationDatabases as t };