openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
304 lines (303 loc) • 16.6 kB
JavaScript
import { i as resolveRequiredHomeDir } from "./home-dir-Dc6nN6HB.mjs";
import { t as formatErrorMessage } from "./errors-BvOkmQM5.mjs";
import { a as executeSqliteQueryTakeFirstSync, i as executeSqliteQuerySync, o as getNodeSqliteKysely } from "./kysely-sync-DUWzQvCU.mjs";
import { d as readDatabasePathIdentitySync } from "./sqlite-worker-identity-BRNRpRyc.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 { n as sha256Hex } from "./node-crypto-BG4u3UnX.mjs";
import { n as hasOrphanedSqliteSidecars } from "./sqlite-files-D1rDNZzV.mjs";
import { a as resolveUpdateRehearsalRoot, i as isUpdateRehearsalReadOnlyPath } from "./update-rehearsal-paths-FgHeU_TR.mjs";
import "./session-key-DY3MOX4N.mjs";
import { h as parseAgentSessionKey } from "./session-key-utils-CDyBC9Gn.mjs";
import { i as listAgentIds } from "./agent-roster-DH6r_8f4.mjs";
import { c as resolveSessionStorePathCore, g as resolveUnsuffixedSqliteTargetFromSessionStorePath } from "./paths-Bonddxfk.mjs";
import { o as isSameOpenClawAgentDatabasePath } from "./openclaw-agent-db.paths-BbmZlEP5.mjs";
import { n as withOpenClawAgentDatabaseReadOnly } from "./openclaw-agent-db-readonly-DAqV3tqr.mjs";
import { c as listOpenClawRegisteredAgentDatabases } from "./openclaw-agent-db-registry-listing-DIA5wIwi.mjs";
import { n as unregisterOpenClawAgentDatabase } from "./openclaw-agent-db-registry-iOyfY9cK.mjs";
import { n as resolveSessionStoreCompatibilityAgentId } from "./legacy.default-agent-owner-CMzbXvh6.mjs";
import { t as getSessionKysely } from "./session-accessor.sqlite-scope-helpers-CfDTAEsL.mjs";
import { o as resolveSqliteTargetFromSessionStorePath } from "./session-sqlite-target-Dd3VSz82.mjs";
import "./targets-kx08xomQ.mjs";
import { n as resolveRelativeSessionStoreMigrationTargets } from "./doctor-relative-session-store-paths-uvD2_8cA.mjs";
import { n as readConfigMachineState } from "./config-machine-state-BAsvqrtu.mjs";
import { a as writeConfigMachineState } from "./config-machine-state-write-BAE6QBnV.mjs";
import { t as DoctorMaintenanceRefusalError } from "./update-doctor-result-Buvixhp2.mjs";
import "./session-accessor.sqlite-scope-BkgwxatT.mjs";
import { r as listSessionMembersInDatabase } from "./session-sharing-store.kernel-BRCymWxs.mjs";
import { a as readSessionTranscriptArchiveResetInventory } from "./session-accessor.sqlite-archive-store-kernel-LqEJdBnu.mjs";
import { c as readSessionDeliveryRepairInventory, l as resolveSqliteCanonicalRepairLookupKeys } from "./session-accessor.sqlite-canonical-repair-BOu11buT.mjs";
import { a as readClaim, r as claimsMatch, s as restoreColdSessionClaims, t as claimFullyCopied } from "./legacy-main-session-migration-claims-DecbUBW5.mjs";
import { t as copySessionClaimForRepair } from "./session-claim-repair-copy-CWGNG7ln.mjs";
import { n as backupDoctorSqliteDatabases } from "./doctor-migration-backup-Bn87StWX.mjs";
import fs from "node:fs";
import path from "node:path";
//#region src/commands/doctor-relative-session-store-preservation.ts
/** A retired store must have no independent owner still settling work against it. */
function readRelativeStorePreservationState(database, keys, refuse) {
const pending = getNodeSqliteKysely(database.db);
if (executeSqliteQueryTakeFirstSync(database.db, pending.selectFrom("context_engine_turn_outbox").select("advancement_key").limit(1))) refuse("pending context-engine advancements need to drain through their original Gateway before recovery");
const goalOperation = executeSqliteQueryTakeFirstSync(database.db, pending.selectFrom("session_goal_operations").select("expires_at").where("expires_at", ">", Date.now()).orderBy("expires_at", "desc").limit(1));
if (goalOperation) refuse(`goal-operation receipts prevent delayed goal recreation until ${new Date(goalOperation.expires_at).toISOString()}; retain the old store until that time before retrying`);
if (readSessionTranscriptArchiveResetInventory(database).length > 0) refuse("retained transcript archives require explicit recovery from the original store; automatic archive relocation is unsupported");
const inventory = readSessionDeliveryRepairInventory(database);
if (inventory.deliveries.some((row) => [
"created",
"queued",
"unknown"
].includes(row.status))) refuse("pending conversation deliveries must settle through their original Gateway and queue before recovery");
const knownKeys = new Set(keys.flatMap((key) => resolveSqliteCanonicalRepairLookupKeys(key, [key])));
if (inventory.deliveries.some((row) => row.source_session_key === null || !knownKeys.has(row.source_session_key))) refuse("delivery receipts without a retained session require explicit recovery from the original store");
const operations = new Set(inventory.deliveries.map((row) => row.operation_id));
if (inventory.progress.some((row) => !operations.has(row.key))) refuse("conversation progress without its delivery receipt requires explicit recovery from the original store");
return inventory;
}
//#endregion
//#region src/commands/doctor-relative-session-store.ts
function refuse(message) {
throw new DoctorMaintenanceRefusalError(`Cannot merge the old relative session.store: ${message}. Original stores and existing backups are retained. Resolve the reported conflict before retrying Doctor or the update.`, {
kind: "data-at-risk",
reason: "incomplete-migration"
});
}
function discoverStoreMoves(cfg, env, priorRegistry) {
const store = cfg.session?.store;
if (!store || store.startsWith("~") || path.isAbsolute(store)) return [];
const registered = [...new Map([...priorRegistry, ...listOpenClawRegisteredAgentDatabases({
env,
includeIncompatibleSchemaVersions: true
})].map((row) => [`${row.agentId}\n${row.path}`, row])).values()];
const legacyTargets = resolveRelativeSessionStoreMigrationTargets(cfg, env, registered);
const defaultAgentId = resolveSessionStoreCompatibilityAgentId(cfg);
const rehearsal = resolveUpdateRehearsalRoot(env);
const moves = /* @__PURE__ */ new Map();
for (const agentId of listAgentIds(cfg)) {
const expanded = store.replaceAll("{agentId}", agentId);
const canonicalStore = resolveSessionStorePathCore(store, {
agentId,
env
});
const canonicalPath = resolveUnsuffixedSqliteTargetFromSessionStorePath(canonicalStore).path;
if (isUpdateRehearsalReadOnlyPath(canonicalPath, env)) refuse(`the new destination ${canonicalPath} is outside the copied update state; run openclaw doctor --fix against the original state directory outside the update rehearsal first`);
const canonical = resolveSqliteTargetFromSessionStorePath(canonicalStore, {
agentId,
defaultAgentId,
env,
registeredDatabases: registered
});
const candidates = [];
if (!rehearsal) for (const base of /* @__PURE__ */ new Set([process.cwd(), resolveRequiredHomeDir(env)])) candidates.push({
storePath: path.resolve(base, expanded),
owner: agentId
});
candidates.push(...legacyTargets.filter((target) => target.configuredAgentId === agentId).map((target) => ({
storePath: target.storePath,
path: target.path,
owner: target.agentId
})));
for (const candidate of candidates) {
if (candidate.storePath === canonicalStore || candidate.path === canonical.path) continue;
const unsuffixed = resolveUnsuffixedSqliteTargetFromSessionStorePath(candidate.storePath);
if (isUpdateRehearsalReadOnlyPath(candidate.path ?? unsuffixed.path, env)) refuse(`the recorded source ${candidate.path ?? unsuffixed.path} was not copied for rehearsal; run openclaw doctor --fix against the original state directory outside the update rehearsal first`);
const source = candidate.path ? {
path: candidate.path,
agentId: candidate.owner
} : resolveSqliteTargetFromSessionStorePath(candidate.storePath, {
agentId,
defaultAgentId,
env,
registeredDatabases: registered
});
const sourceExists = fs.existsSync(source.path);
if (!sourceExists && hasOrphanedSqliteSidecars(source.path)) refuse(`SQLite sidecars at ${source.path} need their matching main database restored`);
if (!sourceExists && !fs.existsSync(candidate.storePath)) continue;
if (isSameOpenClawAgentDatabasePath(source.path, canonical.path)) continue;
const identity = sourceExists ? readDatabasePathIdentitySync(source.path) : void 0;
const owner = source.agentId ?? candidate.owner;
const key = `${identity?.key ?? source.path}\n${canonical.path}`;
if (!moves.has(key)) moves.set(key, {
source: {
path: identity?.canonicalPath ?? source.path,
ownerStorePath: source.path,
databaseAgentId: owner
},
destination: {
path: canonical.path,
ownerStorePath: canonical.path,
databaseAgentId: canonical.agentId ?? owner
},
aliases: registered.filter((row) => row.agentId === owner && !isUpdateRehearsalReadOnlyPath(row.path, env) && isSameOpenClawAgentDatabasePath(row.path, source.path)).map((row) => row.path),
legacyStorePath: candidate.storePath
});
}
}
return [...moves.values()];
}
function readStoreSnapshot(store, env, retiring = false) {
const result = withOpenClawAgentDatabaseReadOnly((database) => {
const claims = executeSqliteQuerySync(database.db, getSessionKysely(database.db).selectFrom("session_nodes").select("session_key").orderBy("session_key")).rows.map(({ session_key: key }) => {
const claim = readClaim(database, store, key, key);
if (!claim) refuse(`source session ${key} could not be read from ${store.path}`);
return claim;
});
return {
claims,
memberIds: new Map(claims.map((claim) => [claim.key, listSessionMembersInDatabase(database, claim.key).map((member) => member.identityId)])),
deliveryInventory: retiring ? readRelativeStorePreservationState(database, claims.map((claim) => claim.key), (message) => refuse(`${store.path}: ${message}`)) : void 0
};
}, {
agentId: store.databaseAgentId,
path: store.path,
env
});
return result.found ? result.value : {
claims: [],
memberIds: /* @__PURE__ */ new Map(),
deliveryInventory: void 0
};
}
function sourceFingerprint(store, snapshot) {
return sha256Hex(JSON.stringify([
readDatabasePathIdentitySync(store.path),
snapshot.claims.map((claim) => [
claim.key,
claim.entry,
claim.nodeArtifactFingerprint,
claim.generations.map((generation) => generation.fingerprint)
]),
snapshot.deliveryInventory
]));
}
/** Offline repair retires the old selector only after backed-up, verified same-owner copies. */
async function repairRelativeSessionStores(params) {
const changes = [];
const warnings = [];
let moves;
try {
moves = discoverStoreMoves(params.cfg, params.env, params.registeredDatabases ?? []);
} catch (error) {
if (error instanceof DoctorMaintenanceRefusalError) throw error;
refuse(`store discovery failed: ${formatErrorMessage(error)}`);
}
if (moves.length === 0) return {
changes,
warnings
};
const maintenance = getOpenClawDatabaseMaintenanceScope();
if (!maintenance?.ownsSchemaMaintenance) refuse("repair requires Doctor maintenance ownership");
const authority = { assertCurrent: () => maintenance.assertAdmission() };
for (const { source, destination, aliases, legacyStorePath } of moves) try {
authority.assertCurrent();
const receiptKey = `sessions.relative-store.v1.${sha256Hex(JSON.stringify([source.path, destination.path]))}`;
const completed = readConfigMachineState(receiptKey, { env: params.env });
let claims;
let fingerprint;
if (completed === void 0) {
const paths = [
source.path,
destination.path,
resolveOpenClawStateSqlitePath(params.env)
].filter((pathname) => fs.existsSync(pathname));
const backup = await backupDoctorSqliteDatabases({
env: params.env,
pendingDatabasePaths: paths,
databasePaths: paths,
authority,
requireSnapshots: true
});
changes.push(...backup.changes);
warnings.push(...backup.warnings);
if (!legacyStorePath.endsWith(".sqlite") && fs.existsSync(legacyStorePath)) {
const { runDoctorSessionSqlite } = await import("./doctor-session-sqlite-BdzWxAK6.mjs");
const { countBlockingSessionSqliteIssues } = await import("./doctor-session-sqlite-types-CXQfxbTB.mjs");
if ((await runDoctorSessionSqlite({
cfg: params.cfg,
env: params.env,
mode: "import",
agent: source.databaseAgentId,
store: legacyStorePath
}, authority)).targets.some((target) => countBlockingSessionSqliteIssues(target) > 0)) refuse(`legacy JSON at ${legacyStorePath} has unresolved Doctor import issues`);
}
if (!fs.existsSync(source.path)) {
authority.assertCurrent();
for (const pathname of /* @__PURE__ */ new Set([source.path, ...aliases])) unregisterOpenClawAgentDatabase({
agentId: source.databaseAgentId,
path: pathname,
env: params.env
});
continue;
}
const sourceSnapshot = readStoreSnapshot(source, params.env, true);
claims = sourceSnapshot.claims;
await restoreColdSessionClaims(claims, params.env, authority.assertCurrent);
fingerprint = sourceFingerprint(source, {
...sourceSnapshot,
claims
});
const destinationSnapshot = readStoreSnapshot(destination, params.env);
const destinationClaims = new Map(destinationSnapshot.claims.map((claim) => [claim.key, claim]));
const prepareCanonical = async (key) => {
const canonical = destinationClaims.get(key);
if (!canonical) return;
const restored = [canonical];
await restoreColdSessionClaims(restored, params.env, authority.assertCurrent);
destinationClaims.set(key, restored[0]);
return restored[0];
};
for (const sourceClaim of claims) {
authority.assertCurrent();
let key = sourceClaim.key;
let canonical = await prepareCanonical(key);
const memberIds = sourceSnapshot.memberIds.get(sourceClaim.key);
const sameMembers = (candidateKey) => JSON.stringify(memberIds) === JSON.stringify(destinationSnapshot.memberIds.get(candidateKey) ?? []);
if (canonical && (!claimsMatch(sourceClaim, canonical) || !sameMembers(key))) {
key = `agent:${parseAgentSessionKey(sourceClaim.key)?.agentId ?? source.databaseAgentId}:relative-store-recovery-${sha256Hex(`${source.path}\n${sourceClaim.key}`).slice(0, 24)}`;
canonical = await prepareCanonical(key);
if (canonical && !sameMembers(key)) refuse(`recovered session ${key} already has different sharing permissions`);
}
const copySource = {
...sourceClaim,
canonicalKey: key
};
const copied = await copySessionClaimForRepair({
source: copySource,
destination,
canonicalKey: key,
...canonical ? { expectedDestination: canonical } : {},
mode: "store-relocation",
env: params.env,
beforePersistentApply: authority.assertCurrent
});
if (!copied || !claimFullyCopied(copySource, copied)) refuse(`conflicting session generations or sharing in ${source.path} and ${destination.path}; preserve the originals and their pre-migration backups for recovery`);
destinationClaims.set(key, copied);
destinationSnapshot.memberIds.set(key, memberIds);
}
authority.assertCurrent();
const verifiedSource = readStoreSnapshot(source, params.env, true);
claims = verifiedSource.claims;
if (sourceFingerprint(source, verifiedSource) !== fingerprint) refuse(`source ${source.path} changed during the copy`);
writeConfigMachineState(receiptKey, fingerprint, { env: params.env });
changes.push(`Merged ${claims.length} session(s) from ${source.path} into ${destination.path}; retained the original store and its pre-migration backup for recovery.`);
} else {
const retiredSource = readStoreSnapshot(source, params.env, true);
claims = retiredSource.claims;
fingerprint = sourceFingerprint(source, retiredSource);
if (completed !== fingerprint || !legacyStorePath.endsWith(".sqlite") && fs.existsSync(legacyStorePath)) refuse(`the retired source ${source.path} changed; preserve it for explicit recovery without replaying deleted conversations`);
}
authority.assertCurrent();
for (const pathname of /* @__PURE__ */ new Set([source.path, ...aliases])) unregisterOpenClawAgentDatabase({
agentId: source.databaseAgentId,
path: pathname,
env: params.env
});
} catch (error) {
if (error instanceof DoctorMaintenanceRefusalError) throw error;
refuse(`${source.path}: ${formatErrorMessage(error)}`);
}
return {
changes,
warnings
};
}
//#endregion
export { repairRelativeSessionStores };