openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
173 lines (172 loc) • 7.84 kB
JavaScript
import { t as openNodeSqliteDatabase } from "./node-sqlite-DWTcT2ek.mjs";
import { l as createPrivateSqliteTempDirectorySync } from "./sqlite-snapshot-staging-BlSkZs0_.mjs";
import { i as tableExists } from "./openclaw-state-db-schema-helpers-DsjDX6BL.mjs";
import { r as prepareSqliteReadOnlyLocationSync } from "./sqlite-snapshot-source-Qcp-YQWO.mjs";
import { S as assertOpenClawStateDatabaseOwner } from "./openclaw-state-db-existing-schema-CUJtAJx4.mjs";
import { p as syncDirectorySync, s as requireDirectorySync } from "./directory-durability-B6uYEZLt.mjs";
import { n as hashFileDescriptorSync } from "./file-descriptor-B4yBUiAP.mjs";
import { n as assertSqliteIntegrity, t as SqliteRepairableForeignKeyError } from "./sqlite-integrity-Da9mtGpK.mjs";
import fs from "node:fs";
import path from "node:path";
import { createHash } from "node:crypto";
//#region src/state/openclaw-state-db-task-delivery-recovery.ts
const ORPHAN_PREDICATE = "NOT EXISTS (SELECT 1 FROM task_runs WHERE task_runs.task_id = task_delivery_state.task_id)";
function orphanRows(database) {
const statement = database.prepare(`SELECT rowid, * FROM task_delivery_state
WHERE ${ORPHAN_PREDICATE} ORDER BY rowid`);
statement.setReadBigInts(true);
return statement.iterate();
}
function encodeOrphanRow(row) {
if (typeof row.rowid !== "bigint" || typeof row.task_id !== "string" || row.requester_origin_json !== null && typeof row.requester_origin_json !== "string" || row.last_notified_event_at !== null && typeof row.last_notified_event_at !== "bigint") throw new Error("Orphan task delivery values do not match the supported recovery contract.");
return `${JSON.stringify(row, (_key, value) => typeof value === "bigint" ? value.toString() : value)}\n`;
}
function assertRecoveryShape(database) {
const columns = database.prepare("PRAGMA table_xinfo(task_delivery_state)").all();
const expected = [
{
cid: 0,
name: "task_id",
type: "TEXT",
notnull: 1,
dflt_value: null,
pk: 1,
hidden: 0
},
{
cid: 1,
name: "requester_origin_json",
type: "TEXT",
notnull: 0,
dflt_value: null,
pk: 0,
hidden: 0
},
{
cid: 2,
name: "last_notified_event_at",
type: "INTEGER",
notnull: 0,
dflt_value: null,
pk: 0,
hidden: 0
}
];
const foreignKeys = database.prepare("PRAGMA foreign_key_list(task_delivery_state)").all();
const canonicalForeignKey = [{
id: 0,
seq: 0,
table: "task_runs",
from: "task_id",
to: "task_id",
on_update: "NO ACTION",
on_delete: "CASCADE",
match: "NONE"
}];
const triggers = database.prepare("SELECT 1 FROM sqlite_schema WHERE type = 'trigger' AND tbl_name = 'task_delivery_state'").get();
if (JSON.stringify(columns) !== JSON.stringify(expected) || JSON.stringify(foreignKeys) !== JSON.stringify(canonicalForeignKey) || triggers) throw new Error("Orphan task delivery recovery refused an unrecognized table, foreign key, or trigger.");
}
function assertKnownOrphanIntegrity(database) {
try {
assertSqliteIntegrity(database, "task delivery recovery database");
return 0;
} catch (error) {
if (error instanceof SqliteRepairableForeignKeyError) return error.repair.orphanCount;
throw error;
}
}
function syncAndHash(filePath) {
const descriptor = fs.openSync(filePath, "r+");
try {
fs.fsyncSync(descriptor);
return hashFileDescriptorSync(descriptor);
} finally {
fs.closeSync(descriptor);
}
}
/**
* Caller holds Doctor's ownership/schema fences and its immediate write transaction.
* Foreign-key actions stay disabled: inbound dependents must fail the post-check,
* never cascade into unrelated data. The caller rolls back any failed check.
*/
function recoverOrphanTaskDeliveryRows(database, pathname) {
if (!tableExists(database, "task_delivery_state") || !tableExists(database, "task_runs") || !database.prepare(`SELECT 1 FROM task_delivery_state WHERE ${ORPHAN_PREDICATE} LIMIT 1`).get()) return [];
assertOpenClawStateDatabaseOwner(database, { pathname });
assertRecoveryShape(database);
const count = assertKnownOrphanIntegrity(database);
if (!database.isTransaction) throw new Error("Orphan task delivery recovery requires the Doctor write transaction.");
const prepared = prepareSqliteReadOnlyLocationSync(pathname);
let recoveryDirectory;
try {
const snapshot = openNodeSqliteDatabase(prepared.location);
try {
snapshot.exec("PRAGMA journal_mode = DELETE;");
assertRecoveryShape(snapshot);
if (assertKnownOrphanIntegrity(snapshot) !== count) throw new Error("Orphan task delivery snapshot changed before preservation.");
const sourceHash = createHash("sha256");
const snapshotHash = createHash("sha256");
for (const row of orphanRows(database)) sourceHash.update(encodeOrphanRow(row));
for (const row of orphanRows(snapshot)) snapshotHash.update(encodeOrphanRow(row));
if (sourceHash.digest("hex") !== snapshotHash.digest("hex")) throw new Error("Orphan task delivery snapshot does not preserve the current payload.");
} finally {
snapshot.close();
}
recoveryDirectory = createPrivateSqliteTempDirectorySync(path.dirname(pathname), "openclaw-task-delivery-recovery-");
const backupPath = path.join(recoveryDirectory, "database.sqlite");
const expectedBackup = syncAndHash(prepared.location);
fs.copyFileSync(prepared.location, backupPath, fs.constants.COPYFILE_EXCL);
fs.chmodSync(backupPath, 384);
if (JSON.stringify(syncAndHash(backupPath)) !== JSON.stringify(expectedBackup)) throw new Error("Orphan task delivery backup verification failed.");
const exportPath = path.join(recoveryDirectory, "orphan-rows.jsonl");
const exportDescriptor = fs.openSync(exportPath, "wx+", 384);
const exportHash = createHash("sha256");
let exportedCount = 0;
try {
for (const row of orphanRows(database)) {
const encoded = encodeOrphanRow(row);
fs.writeFileSync(exportDescriptor, encoded);
exportHash.update(encoded);
exportedCount += 1;
}
fs.fsyncSync(exportDescriptor);
if (hashFileDescriptorSync(exportDescriptor).sha256 !== exportHash.digest("hex")) throw new Error("Orphan task delivery export verification failed.");
} finally {
fs.closeSync(exportDescriptor);
}
if (exportedCount !== count) throw new Error("Orphan task delivery export did not account for every foreign-key violation.");
fs.writeFileSync(path.join(recoveryDirectory, "manifest.json"), JSON.stringify({
format: "openclaw.task-delivery-recovery.v1",
state: "preserved-before-repair",
sourcePath: pathname,
backup: {
file: "database.sqlite",
...expectedBackup,
integrity: "structural-ok-known-orphans"
},
export: {
file: "orphan-rows.jsonl",
...syncAndHash(exportPath),
count,
integerEncoding: "decimal-string"
},
relation: "task_delivery_state.task_id -> task_runs.task_id",
createdAt: (/* @__PURE__ */ new Date()).toISOString()
}, null, 2), {
flag: "wx",
mode: 384,
flush: true
});
requireDirectorySync(syncDirectorySync(recoveryDirectory), "Task delivery recovery directory");
requireDirectorySync(syncDirectorySync(path.dirname(recoveryDirectory)), "Task delivery recovery parent");
const removed = database.prepare(`DELETE FROM task_delivery_state WHERE ${ORPHAN_PREDICATE}`).run();
if (Number(removed.changes) !== count) throw new Error("Orphan task delivery recovery removed an unexpected number of rows.");
assertSqliteIntegrity(database, pathname);
return [`Preserved and recovered ${count} orphan task delivery rows; backup and row export: ${recoveryDirectory}`];
} catch (error) {
throw new Error(`Task delivery recovery failed without committing row removal${recoveryDirectory ? `; retained artifacts: ${recoveryDirectory}` : ""}: ${String(error)}`, { cause: error });
} finally {
prepared.cleanup();
}
}
//#endregion
export { recoverOrphanTaskDeliveryRows as t };