openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
213 lines (212 loc) • 9.47 kB
JavaScript
import { t as formatErrorMessage } from "./errors-O33D01XW.mjs";
import { i as resolveImmutableSqliteFileUri, t as openNodeSqliteDatabase } from "./node-sqlite-XIcwmOrl.mjs";
import "./openclaw-state-db-contract-Cm9fKF2T.mjs";
import { c as resolveRuntimeServiceCommit, s as resolveRuntimeServiceBuildId, t as VERSION } from "./version-BJGPJZu6.mjs";
import { l as getOpenClawDatabaseMaintenanceScope } from "./openclaw-state-db-async-lifecycle-DLKxSPCk.mjs";
import { c as resolveOpenClawStateSqlitePath } from "./openclaw-state-db.paths-DfifnSGp.mjs";
import { i as needsOpenClawStateDatabaseSchemaRepair } from "./openclaw-state-db-fast-path-CPAamWzU.mjs";
import { t as createDoctorRehearsalDatabaseCoverage } from "./doctor-rehearsal-databases-jWB-feGw.mjs";
import { p as syncDirectorySync, s as requireDirectorySync } from "./directory-durability-CqSvP1tc.mjs";
import { i as sameFileMutationFingerprint } from "./file-descriptor-B4yBUiAP.mjs";
import { n as assertSqliteIntegrity } from "./sqlite-integrity-GtqcmwG3.mjs";
import "./openclaw-agent-db-contract-ClUH-Vmp.mjs";
import { t as DoctorMaintenanceRefusalError } from "./update-doctor-result-b1ffujJO.mjs";
import { t as loadSqliteVecExtension } from "./sqlite-vec-BcbJNyja.mjs";
import { n as recordDoctorMigrationBackups } from "./doctor-migration-backup-artifacts-CBezQX4r.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.");
return backupDoctorSqliteDatabases({
...params,
pendingDatabasePaths: [...pending],
authority: { assertCurrent: () => maintenance.assertAdmission() }
});
}
/** 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 = 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-V9wGePca.mjs");
const { sanitizeOpenClawStateLeaseRows } = await import("./openclaw-state-snapshot-sanitizer-C60iEdnN.mjs");
authority.assertCurrent();
disposable?.assertCurrent();
const sources = [...new Set([...pending].filter((pathname) => !disposable?.excludes(pathname)).map((pathname) => realpathSync.native(pathname)))];
if (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,
24,
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,
allowExtension: true
});
try {
snapshot.exec("PRAGMA trusted_schema = OFF;");
await loadSqliteVecExtension({ db: snapshot });
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
};
}
//#endregion
export { backupDoctorSqliteDatabases as n, backupDoctorMigrationDatabases as t };