openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
924 lines (923 loc) • 43 kB
JavaScript
import { c as isRecord } from "./record-coerce-TIsquoVR.mjs";
import { t as hasErrnoCode } from "./errno-FR4CRyya.mjs";
import { r as resolveStateDir } from "./state-dir-BHJORNly.mjs";
import "./paths-DGIwWbpu.mjs";
import { a as getNodeSqliteKysely, i as executeSqliteQueryTakeFirstSync, r as executeSqliteQuerySync, u as sqliteStringSet } from "./kysely-sync-K7ftY-g4.mjs";
import { a as readSqliteDataVersion } from "./sqlite-schema-facts-CG0Tk84j.mjs";
import { r as runSqliteDeferredTransactionSync } from "./sqlite-transaction-DsZ3X6nD.mjs";
import { n as sha256Hex } from "./node-crypto-BG4u3UnX.mjs";
import { _ as isOpenClawAgentDatabasePathCurrent, v as readOpenClawAgentDatabaseIdentity } from "./openclaw-agent-db-validation-cache-_HDeZNQr.mjs";
import { n as normalizeAgentId } from "./agent-id-D1-Q6vR4.mjs";
import { f as withExistingOpenClawStateDatabaseReadOnly } from "./openclaw-state-db-readonly-CC57AD_q.mjs";
import "./agent-scope-config-CDYpzA0r.mjs";
import { i as normalizeMainKey } from "./session-key-CmZ4o9-l.mjs";
import { M as parseAgentSessionKey } from "./session-key-C09ow4h0.mjs";
import { i as listAgentIds, l as tryResolveSoleAgentId } from "./agent-roster-BXD1uMJD.mjs";
import { o as isSameOpenClawAgentDatabasePath } from "./openclaw-agent-db.paths-DVbbFaej.mjs";
import { l as resolveSessionStorePathCore, o as resolveSessionArtifactDirectory } from "./paths-dpReqUI8.mjs";
import { l as runOpenClawStateWriteTransaction } from "./openclaw-state-db-D1jWDzK3.mjs";
import { n as resolveSessionStoreCompatibilityAgentId } from "./legacy.default-agent-owner-CiZjlN5C.mjs";
import { a as getOpenClawAgentDatabaseIfOpen, t as borrowOpenClawAgentDatabase, u as runOpenClawAgentWriteTransaction } from "./openclaw-agent-db-DBug3LUA.mjs";
import { n as openOpenClawAgentDatabaseReadOnly, t as hasOpenClawAgentReadOnlySchema } from "./openclaw-agent-db-readonly-open-hQ4ohjUT.mjs";
import { n as withOpenClawAgentDatabaseReadOnly } from "./openclaw-agent-db-readonly-C4wDuLrp.mjs";
import { o as resolveSqliteTargetFromSessionStorePath } from "./session-sqlite-target-Cnk9g9d0.mjs";
import { a as normalizeStoreSessionKey } from "./store-entry-Bb_svqyv.mjs";
import { t as getSessionKysely } from "./session-accessor.sqlite-scope-helpers-DOK3pvYX.mjs";
import { p as runExclusiveSqliteSessionWrite } from "./session-accessor.sqlite-scope-CdpbxR_m.mjs";
import { D as invalidateSessionEntryMaintenanceAgeFact } from "./session-accessor.sqlite-entry-cache-nS4OEr3G.mjs";
import { a as readExactSessionEntryRow } from "./session-accessor.sqlite-entry-read-CWTZjXLR.mjs";
import { n as assertSessionTranscriptHot } from "./session-cold-storage-state-CzcE3h3O.mjs";
import { n as replaceSessionOwnerInTransaction } from "./session-accessor.sqlite-owner-ylhcUaWK.mjs";
import { i as resolveAllAgentSessionStoreCandidateTargetsSync, r as resolveAgentSessionStoreTargetsSync } from "./targets-BbcRkdUC.mjs";
import { D as withSqliteSessionDeletions, d as writeSessionEntry, h as copySessionNodeArtifactsForRepair, t as deleteLegacySessionEntryRows, v as readSessionNodeArtifactFingerprint, w as runSqliteSessionDeletionTransaction } from "./session-accessor.sqlite-entry-store-CS34_uoa.mjs";
import { r as collectSessionStateIdsForEntry } from "./session-accessor.sqlite-references-DaTkiOgv.mjs";
import { c as readSqliteSessionGenerationClaim, i as readExactSessionEntryRowForCanonicalRepair, l as readSqliteSessionGenerationWindows, s as copySqliteSessionGenerationRows, u as rehomeSqliteSessionGenerationWindow } from "./session-accessor.sqlite-canonical-repair-CmEdlsBa.mjs";
import { n as deleteSessionEntryLifecycle } from "./session-accessor.sqlite-lifecycle-OEtYbcf3.mjs";
import fs from "node:fs";
import path from "node:path";
import { isDeepStrictEqual } from "node:util";
import { resolvePathPrefixSync } from "@openclaw/fs-safe/advanced";
//#region src/config/sessions/legacy-main-session-migration-claims.ts
function projectEntryIdentity(entry) {
const projected = { ...entry };
delete projected.sessionFile;
delete projected.transcriptPath;
return projected;
}
function generationWindowIdentity(generation, canonicalKey) {
const { updated_at: _updatedAt, transcript_updated_at: _transcriptUpdatedAt, transcript_observed_at: _transcriptObservedAt, ...identity } = rehomeSqliteSessionGenerationWindow(generation.window, canonicalKey, /* @__PURE__ */ new Set([normalizeStoreSessionKey(generation.window.session_key.trim())]));
return identity;
}
function generationsMatch(left, right, canonicalKey) {
return left.contentFingerprint === right.contentFingerprint && isDeepStrictEqual(generationWindowIdentity(left, canonicalKey), generationWindowIdentity(right, canonicalKey));
}
function claimsMatch(left, right) {
const generations = new Map(right.generations.map((generation) => [generation.window.session_id, generation]));
return isDeepStrictEqual(projectEntryIdentity(left.entry), projectEntryIdentity(right.entry)) && left.generations.every((generation) => {
const other = generations.get(generation.window.session_id);
return !other || generationsMatch(generation, other, left.canonicalKey);
});
}
function claimUnchanged(current, expected) {
return current.databaseIdentity === expected.databaseIdentity && current.nodeArtifactFingerprint === expected.nodeArtifactFingerprint && isDeepStrictEqual(projectEntryIdentity(current.entry), projectEntryIdentity(expected.entry)) && current.generations.length === expected.generations.length && current.generations.every((generation, index) => generation.fingerprint === expected.generations[index]?.fingerprint);
}
function claimFullyCopied(source, destination) {
const sessionIds = new Set(destination.generations.map((generation) => generation.window.session_id));
return claimsMatch(source, destination) && source.generations.every((generation) => sessionIds.has(generation.window.session_id));
}
function readClaim(database, store, key, canonicalKey) {
return runSqliteDeferredTransactionSync(database.db, () => {
const row = readExactSessionEntryRowForCanonicalRepair(database, key);
if (!row) return;
const windows = readSqliteSessionGenerationWindows(database, [key], collectSessionStateIdsForEntry(row.entry));
return {
canonicalKey,
databaseIdentity: readOpenClawAgentDatabaseIdentity(database).identity,
entry: row.entry,
generations: windows.map((window) => readSqliteSessionGenerationClaim(database, window)),
key,
nodeArtifactFingerprint: readSessionNodeArtifactFingerprint(database, key),
store
};
});
}
async function restoreColdSessionClaims(claims, env, beforePersistentApply) {
for (const [index, claim] of claims.entries()) {
const cold = withOpenClawAgentDatabaseReadOnly((database) => executeSqliteQuerySync(database.db, getSessionKysely(database.db).selectFrom("session_transcript_cold_archives").select("session_id").where("session_id", "in", sqliteStringSet(claim.generations.map((generation) => generation.window.session_id)))).rows, {
agentId: claim.store.databaseAgentId,
env,
path: claim.store.path
});
if (!cold.found) throw new Error(`Legacy session store changed before history restoration: ${claim.key}`);
if (cold.value.length === 0 && claim.generations.every((generation) => !generation.coldArchive)) continue;
const { restoreSessionColdTranscript } = await import("./session-cold-storage-DF3NHtHa.mjs");
for (const generation of cold.value) await restoreSessionColdTranscript({
agentId: claim.store.databaseAgentId,
env,
sessionId: generation.session_id,
storePath: claim.store.path
}, beforePersistentApply);
const refreshed = withOpenClawAgentDatabaseReadOnly((database) => readClaim(database, claim.store, claim.key, claim.canonicalKey), {
agentId: claim.store.databaseAgentId,
env,
path: claim.store.path
});
if (!refreshed.found || !refreshed.value) throw new Error(`Legacy session changed during history restoration: ${claim.key}`);
claims[index] = refreshed.value;
}
}
//#endregion
//#region src/config/sessions/legacy-main-session-key-scan.ts
function inspectSessionStorePath(pathname) {
let entry;
try {
entry = fs.lstatSync(pathname);
} catch (error) {
if (hasErrnoCode(error, "ENOENT")) return "missing";
throw error;
}
if (!(entry.isSymbolicLink() ? fs.statSync(pathname) : entry).isFile()) throw new Error(`session store is not a regular file: ${pathname}`);
return "present";
}
/** Returns the stored `agent:<id>:` prefix when the key is owned by the legacy agent. */
function legacyAgentKeyPrefix(key, legacyAgentId) {
const parsed = parseAgentSessionKey(key);
if (!parsed || normalizeAgentId(parsed.agentId) !== legacyAgentId) return null;
const prefix = `agent:${parsed.agentId}:`;
return key.startsWith(prefix) ? prefix : null;
}
function canonicalKeyFor(key, legacyAgentId, ownerAgentId) {
const prefix = legacyAgentKeyPrefix(key, legacyAgentId);
return prefix ? `agent:${ownerAgentId}:${key.slice(prefix.length)}` : null;
}
function storeHasLegacyAgentSessionKey(params) {
const result = withOpenClawAgentDatabaseReadOnly((database) => executeSqliteQuerySync(database.db, getSessionKysely(database.db).selectFrom("session_nodes").select("session_key")).rows.some((row) => legacyAgentKeyPrefix(row.session_key, params.legacyAgentId) !== null), {
agentId: params.store.databaseAgentId,
env: params.env,
path: params.store.path
});
return result.found ? result.value : false;
}
function readClaimsFromStores(params) {
const targets = /* @__PURE__ */ new Set();
const candidates = /* @__PURE__ */ new Map();
const readStore = (store, read) => {
try {
if (inspectSessionStorePath(store.path) === "missing") return;
const result = withOpenClawAgentDatabaseReadOnly(read, {
agentId: store.databaseAgentId,
env: params.env,
path: store.path
});
return result.found ? result.value : void 0;
} catch (error) {
params.onUnreadable(store, error);
return;
}
};
for (const store of params.stores) {
const keys = readStore(store, (database) => executeSqliteQuerySync(database.db, getSessionKysely(database.db).selectFrom("session_nodes").select("session_key")).rows);
if (keys) candidates.set(store, keys.map(({ session_key: key }) => {
const canonicalKey = canonicalKeyFor(key, params.legacyAgentId, params.ownerAgentId);
if (canonicalKey) targets.add(canonicalKey);
return {
key,
canonicalKey: canonicalKey ?? key
};
}));
}
const canonical = [];
const legacy = [];
for (const [store, keys] of candidates) {
const targeted = keys.filter(({ canonicalKey }) => targets.has(canonicalKey));
if (targeted.length === 0) continue;
const claims = readStore(store, (database) => targeted.flatMap(({ key, canonicalKey }) => {
const claim = readClaim(database, store, key, canonicalKey);
return claim ? [claim] : [];
}));
for (const claim of claims ?? []) (claim.key === claim.canonicalKey ? canonical : legacy).push(claim);
}
return {
canonical,
legacy
};
}
//#endregion
//#region src/config/sessions/legacy-main-session-migration-operations.ts
function samePhysicalStore(left, right) {
return isSameOpenClawAgentDatabasePath(left.path, right.path);
}
function freshestClaim(claims) {
return claims.toSorted((left, right) => {
return (right.entry.updatedAt ?? 0) - (left.entry.updatedAt ?? 0) || left.key.localeCompare(right.key) || left.store.path.localeCompare(right.store.path);
})[0];
}
function warningForDivergence(kind, canonicalKey, claims) {
return `session: ${kind} for ${canonicalKey}; preserved claims ${claims.map((claim) => `${claim.store.path}#${claim.key}`).join(", ")}. Run openclaw doctor --fix to quarantine the losing claims.`;
}
function writeMigratedSessionClaim(database, sessionKey, entry) {
invalidateSessionEntryMaintenanceAgeFact(database.db);
writeSessionEntry(database, sessionKey, entry, {
allowStoredAliases: true,
previousEntry: null
});
replaceSessionOwnerInTransaction(database, sessionKey, entry.owner);
}
function mutateLegacySessionClaims(params, commit) {
const scope = {
agentId: params.store.databaseAgentId,
env: params.env,
path: params.store.path,
ownerStorePath: params.store.ownerStorePath
};
return withSqliteSessionDeletions(scope, params.claims.map(({ key: sessionKey, entry }) => ({
sessionKey,
entry
})), async (assertCurrent) => runExclusiveSqliteSessionWrite(scope, async () => {
assertCurrent();
params.beforePersistentApply?.();
return runSqliteSessionDeletionTransaction(commit, scope, { operationLabel: params.operationLabel });
}, params.operationLabel));
}
function migrateClaimsInPlace(params) {
return mutateLegacySessionClaims({
...params,
claims: params.aliases.filter((claim) => claim.key !== params.canonicalKey),
operationLabel: "session-migration.legacy-main-in-place"
}, (database) => {
const currentAliases = params.aliases.map((claim) => readClaim(database, params.store, claim.key, params.canonicalKey));
const currentCanonical = readClaim(database, params.store, params.canonicalKey, params.canonicalKey);
if (currentAliases.some((claim, index) => !claim || !claimUnchanged(claim, params.aliases[index])) || (params.canonical ? !currentCanonical || !claimUnchanged(currentCanonical, params.canonical) : currentCanonical !== void 0)) return;
if (!currentCanonical) writeMigratedSessionClaim(database, params.canonicalKey, params.winner.entry);
deleteLegacySessionEntryRows(database, params.aliases.map((claim) => claim.key), params.canonicalKey, {
rehomeMembers: true,
validatedEntries: new Map(currentAliases.map((claim) => [claim.key, claim.entry]))
});
return readClaim(database, params.store, params.canonicalKey, params.canonicalKey);
});
}
async function copyClaimCrossStore(params) {
const destinationOptions = {
agentId: params.destination.databaseAgentId,
env: params.env,
path: params.destination.path
};
return await runExclusiveSqliteSessionWrite(destinationOptions, async () => {
params.beforePersistentApply?.();
return runOpenClawAgentWriteTransaction((destinationDatabase) => {
const current = readClaim(destinationDatabase, params.destination, params.canonicalKey, params.canonicalKey);
if (params.expectedDestination && (!current || !claimUnchanged(current, params.expectedDestination)) || current && !claimsMatch(params.source, current)) return;
const source = withOpenClawAgentDatabaseReadOnly((sourceDatabase) => runSqliteDeferredTransactionSync(sourceDatabase.db, () => {
const fresh = readClaim(sourceDatabase, params.source.store, params.source.key, params.canonicalKey);
if (!fresh || !claimUnchanged(fresh, params.source) || fresh.databaseIdentity === readOpenClawAgentDatabaseIdentity(destinationDatabase).identity) return;
const destinationWindows = new Map(readSqliteSessionGenerationWindows(destinationDatabase, [], fresh.generations.map((generation) => generation.window.session_id)).map((window) => [window.session_id, window]));
const currentGenerations = new Map(current?.generations.map((generation) => [generation.window.session_id, generation]));
const missing = [];
for (const generation of fresh.generations) {
assertSessionTranscriptHot(sourceDatabase.db, generation.window.session_id);
if (normalizeStoreSessionKey(generation.window.session_key.trim()) !== normalizeStoreSessionKey(params.source.key.trim())) return;
const existing = destinationWindows.get(generation.window.session_id);
if (!existing) missing.push(generation);
else if (existing.session_key !== params.canonicalKey || !generationsMatch(generation, currentGenerations.get(existing.session_id) ?? readSqliteSessionGenerationClaim(destinationDatabase, existing), params.canonicalKey)) return;
}
if (!current) {
if (readExactSessionEntryRow(destinationDatabase, params.canonicalKey)) return;
writeMigratedSessionClaim(destinationDatabase, params.canonicalKey, fresh.entry);
}
const sourceDb = getSessionKysely(sourceDatabase.db);
const destinationDb = getSessionKysely(destinationDatabase.db);
const sourceKeys = /* @__PURE__ */ new Set([normalizeStoreSessionKey(params.source.key.trim())]);
for (const generation of missing) {
const window = generation.window;
const links = executeSqliteQuerySync(sourceDatabase.db, sourceDb.selectFrom("session_conversations").selectAll().where("session_id", "=", window.session_id)).rows;
const conversationIds = [...window.primary_conversation_id ? [window.primary_conversation_id] : [], ...links.map((link) => link.conversation_id)];
for (const conversation of executeSqliteQuerySync(sourceDatabase.db, sourceDb.selectFrom("conversations").selectAll().where("conversation_id", "in", sqliteStringSet(conversationIds))).rows) executeSqliteQuerySync(destinationDatabase.db, destinationDb.insertInto("conversations").values(conversation).onConflict((conflict) => conflict.column("conversation_id").doNothing()));
const mapped = rehomeSqliteSessionGenerationWindow(window, params.canonicalKey, sourceKeys);
executeSqliteQuerySync(destinationDatabase.db, destinationDb.insertInto("session_windows").values(mapped).onConflict((conflict) => conflict.column("session_id").doUpdateSet(mapped)));
copySqliteSessionGenerationRows({
destination: destinationDatabase,
source: sourceDatabase,
sessionId: window.session_id,
sourceWindowPresent: true
});
executeSqliteQuerySync(destinationDatabase.db, destinationDb.deleteFrom("session_conversations").where("session_id", "=", window.session_id));
for (const link of links) executeSqliteQuerySync(destinationDatabase.db, destinationDb.insertInto("session_conversations").values(link));
}
copySessionNodeArtifactsForRepair(sourceDatabase, destinationDatabase, [fresh.key], params.canonicalKey, { includeMembers: false });
return readClaim(destinationDatabase, params.destination, params.canonicalKey, params.canonicalKey);
}), {
agentId: params.source.store.databaseAgentId,
env: params.env,
path: params.source.store.path
});
return source.found ? source.value : void 0;
}, destinationOptions, { operationLabel: "session.legacy-main.copy-claim" });
}, "session-migration.legacy-main-copy");
}
async function deleteExpectedClaim(claim, commitGuard) {
return (await deleteSessionEntryLifecycle({
commitGuard,
agentId: claim.store.databaseAgentId,
archiveTranscript: false,
deleteTranscriptWithoutArchive: true,
expectedEntry: claim.entry,
expectedDatabaseIdentity: claim.databaseIdentity,
expectedGenerations: claim.generations,
expectedNodeArtifactFingerprint: claim.nodeArtifactFingerprint,
requireWriteSuccess: true,
storePath: claim.store.ownerStorePath,
target: {
canonicalKey: claim.key,
storeKeys: [claim.key]
}
})).deleted;
}
async function deleteCopiedClaims(params) {
const sources = params.aliases.filter((claim) => !samePhysicalStore(claim.store, params.destination));
if (sources.length === 0) return;
const options = {
agentId: params.destination.databaseAgentId,
env: params.env,
path: params.destination.path
};
const changed = () => /* @__PURE__ */ new Error(`Canonical session changed before legacy cleanup: ${params.canonicalKey}`);
const writer = getOpenClawAgentDatabaseIfOpen(options);
if (!writer) throw changed();
const destinationIdentity = readOpenClawAgentDatabaseIdentity(writer).identity;
const retained = borrowOpenClawAgentDatabase(options);
let reader;
try {
const opened = openOpenClawAgentDatabaseReadOnly(options);
if (!opened.found) throw changed();
reader = opened.database;
const destinationReader = reader;
const assertCurrent = () => {
if (retained.db !== writer.db || sources.some((claim) => claim.databaseIdentity === destinationIdentity) || getOpenClawAgentDatabaseIfOpen(options) !== writer || !isOpenClawAgentDatabasePathCurrent(writer) || writer.db.isTransaction || !isOpenClawAgentDatabasePathCurrent(destinationReader) || readOpenClawAgentDatabaseIdentity(destinationReader).identity !== destinationIdentity) throw changed();
};
let verifiedVersion;
const assertCopied = () => {
params.beforePersistentApply?.();
assertCurrent();
const version = readSqliteDataVersion(destinationReader.db);
if (version === verifiedVersion) return;
if (!hasOpenClawAgentReadOnlySchema(destinationReader)) throw changed();
const destination = readClaim(destinationReader, params.destination, params.canonicalKey, params.canonicalKey);
if (!destination || !claimUnchanged(destination, params.receipt)) throw changed();
assertCurrent();
if (readSqliteDataVersion(destinationReader.db) !== version) throw changed();
verifiedVersion = version;
};
assertCopied();
for (const claim of sources) if (!await deleteExpectedClaim(claim, assertCopied)) return claim;
return;
} finally {
try {
reader?.close();
} finally {
retained.release();
}
}
}
function quarantineClaim(params) {
return mutateLegacySessionClaims({
beforePersistentApply: params.beforePersistentApply,
store: params.claim.store,
env: params.env,
claims: [params.claim],
operationLabel: "session-migration.legacy-main-quarantine"
}, (database) => {
const fresh = readClaim(database, params.claim.store, params.claim.key, params.claim.canonicalKey);
if (!fresh || !claimUnchanged(fresh, params.claim)) return;
let quarantineKey;
for (let index = 1;; index += 1) {
const candidate = `agent:${params.ownerAgentId}:legacy-main-conflict-${index}`;
if (!readExactSessionEntryRow(database, candidate)) {
quarantineKey = candidate;
break;
}
}
writeMigratedSessionClaim(database, quarantineKey, params.claim.entry);
deleteLegacySessionEntryRows(database, [params.claim.key], quarantineKey, {
rehomeMembers: true,
validatedEntries: /* @__PURE__ */ new Map([[fresh.key, fresh.entry]])
});
return quarantineKey;
});
}
async function processIdenticalClaims(params) {
const winner = params.canonical ?? freshestClaim(params.aliases);
const crossStore = params.aliases.some((claim) => !samePhysicalStore(claim.store, params.destination));
const completedOutcome = () => ({
kind: params.canonical ? "canonical-exists-identical" : crossStore ? "migrated-cross-store" : "migrated-in-place",
canonicalKey: params.canonicalKey,
paths: [...new Set(params.aliases.map((claim) => claim.store.path))],
sourceKeys: params.aliases.map((claim) => claim.key)
});
if (params.mode !== "doctor-fix") return completedOutcome();
let canonical = params.canonical;
const destinationAliases = params.aliases.filter((claim) => samePhysicalStore(claim.store, params.destination));
if (destinationAliases.length > 0) {
canonical = await migrateClaimsInPlace({
beforePersistentApply: params.beforePersistentApply,
aliases: destinationAliases,
...canonical ? { canonical } : {},
canonicalKey: params.canonicalKey,
env: params.env,
store: params.destination,
winner: canonical ?? freshestClaim(destinationAliases)
});
if (!canonical) return {
kind: "divergent-aliases",
canonicalKey: params.canonicalKey,
detail: "source aliases changed during the in-place transaction"
};
}
for (const sourceBefore of params.aliases) {
if (samePhysicalStore(sourceBefore.store, params.destination)) continue;
const copied = await copyClaimCrossStore({
beforePersistentApply: params.beforePersistentApply,
canonicalKey: params.canonicalKey,
destination: params.destination,
...canonical ? { expectedDestination: canonical } : {},
env: params.env,
source: sourceBefore
});
const sourceAfter = withOpenClawAgentDatabaseReadOnly((database) => readClaim(database, sourceBefore.store, sourceBefore.key, params.canonicalKey), {
agentId: sourceBefore.store.databaseAgentId,
env: params.env,
path: sourceBefore.store.path
});
if (!copied || !claimFullyCopied(sourceBefore, copied) || !sourceAfter.found || !sourceAfter.value || !claimUnchanged(sourceAfter.value, sourceBefore)) return {
kind: "divergent-canonical",
canonicalKey: params.canonicalKey,
detail: "source or imported canonical changed during cross-store copy verification"
};
canonical = copied;
}
if (!canonical || !claimsMatch(canonical, winner)) return {
kind: "divergent-canonical",
canonicalKey: params.canonicalKey,
detail: "canonical content differs from the legacy claim"
};
const changedSource = await deleteCopiedClaims({
...params,
receipt: canonical
});
if (changedSource) return {
kind: "divergent-canonical",
canonicalKey: params.canonicalKey,
detail: `source changed before expected-entry cleanup: ${changedSource.store.path}#${changedSource.key}`
};
return completedOutcome();
}
async function repairDivergentClaims(params) {
const winner = params.destinationCanonical ?? freshestClaim(params.claims);
if (!params.destinationCanonical) {
const migrated = await processIdenticalClaims({
beforePersistentApply: params.beforePersistentApply,
aliases: [winner],
canonicalKey: params.canonicalKey,
destination: params.destination,
env: params.env,
mode: "doctor-fix"
});
if (migrated.kind === "divergent-aliases" || migrated.kind === "divergent-canonical") return {
quarantinedKeys: [],
resolved: false
};
}
const canonicalResult = withOpenClawAgentDatabaseReadOnly((database) => readClaim(database, params.destination, params.canonicalKey, params.canonicalKey), {
agentId: params.destination.databaseAgentId,
env: params.env,
path: params.destination.path
});
const canonical = canonicalResult.found ? canonicalResult.value : void 0;
if (!canonical || !claimsMatch(canonical, winner)) return {
quarantinedKeys: [],
resolved: false
};
const remaining = params.claims.filter((claim) => claim !== winner && claim !== params.destinationCanonical);
const identical = remaining.filter((claim) => claimsMatch(claim, canonical));
if (identical.length > 0) {
const migrated = await processIdenticalClaims({
beforePersistentApply: params.beforePersistentApply,
aliases: identical,
canonical,
canonicalKey: params.canonicalKey,
destination: params.destination,
env: params.env,
mode: "doctor-fix"
});
if (migrated.kind === "divergent-aliases" || migrated.kind === "divergent-canonical") return {
quarantinedKeys: [],
resolved: false
};
}
const quarantinedKeys = [];
for (const claim of remaining) {
if (identical.includes(claim)) continue;
const quarantineKey = await quarantineClaim({
beforePersistentApply: params.beforePersistentApply,
claim,
env: params.env,
ownerAgentId: params.ownerAgentId
});
if (!quarantineKey) return {
quarantinedKeys,
resolved: false
};
quarantinedKeys.push(quarantineKey);
}
return {
quarantinedKeys,
resolved: true
};
}
//#endregion
//#region src/config/sessions/legacy-main-session-migration.ts
const SOURCE_KEY = "legacy-main-session-keys";
const MIGRATION_KIND = "legacy-main-session-keys-v1";
const REPORT_VERSION = 1;
function resolveArmingDecision(cfg, legacyAgentId) {
const roster = new Set(listAgentIds(cfg).map(normalizeAgentId));
if (roster.has(legacyAgentId)) return {
armed: false,
reason: "legacy-agent-present"
};
const sole = tryResolveSoleAgentId(cfg);
if (sole && roster.has(normalizeAgentId(sole))) return {
armed: true,
ownerAgentId: normalizeAgentId(sole)
};
const sessionStoreOwner = cfg.agents?.defaults?.sessionStore?.agentId?.trim();
if (sessionStoreOwner) {
const normalized = normalizeAgentId(sessionStoreOwner);
if (roster.has(normalized)) return {
armed: true,
ownerAgentId: normalized
};
}
return {
armed: false,
reason: "owner-unresolved"
};
}
function addPhysicalStore(stores, candidate) {
if (!stores.some((store) => samePhysicalStore(store, candidate))) stores.push(candidate);
}
function resolveMissingPhysicalPath(pathname) {
const prefix = resolvePathPrefixSync(path.resolve(pathname));
return path.join(prefix.existingPath, ...prefix.unresolvedSegments);
}
function resolvePhysicalPathIdentity(pathname) {
try {
const stat = fs.statSync(pathname, { bigint: true });
if (!stat.isFile()) throw new Error(`session store is not a regular file: ${pathname}`);
return `file:${stat.dev}:${stat.ino}`;
} catch (error) {
if (error.code !== "ENOENT") throw error;
return `missing:${resolveMissingPhysicalPath(pathname)}`;
}
}
function resolvePhysicalStores(params) {
const logicalTargets = [
...resolveAllAgentSessionStoreCandidateTargetsSync(params.cfg, { env: params.env }),
...resolveAgentSessionStoreTargetsSync(params.cfg, params.legacyAgentId, { env: params.env }),
{
agentId: params.legacyAgentId,
storePath: resolveSessionStorePathCore(params.cfg.session?.store, {
agentId: params.legacyAgentId,
env: params.env
})
}
];
if (params.ownerAgentId) logicalTargets.push({
agentId: params.ownerAgentId,
storePath: resolveSessionStorePathCore(params.cfg.session?.store, {
agentId: params.ownerAgentId,
env: params.env
})
});
const defaultAgentId = params.ownerAgentId ?? resolveSessionStoreCompatibilityAgentId(params.cfg);
const stores = [];
const jsonPaths = /* @__PURE__ */ new Set();
const unreadable = [];
for (const target of logicalTargets) try {
if (!target.storePath.endsWith(".sqlite") && inspectSessionStorePath(target.storePath) === "present") jsonPaths.add(path.resolve(target.storePath));
const resolved = resolveSqliteTargetFromSessionStorePath(target.storePath, {
agentId: target.agentId,
defaultAgentId,
env: params.env
});
const physical = {
databaseAgentId: normalizeAgentId(resolved.agentId ?? target.agentId),
ownerStorePath: target.storePath,
path: resolved.path
};
resolvePhysicalPathIdentity(physical.path);
addPhysicalStore(stores, physical);
} catch (error) {
if (params.mode === "doctor-fix") throw new Error(`cannot inspect legacy session store ${target.storePath}: ${String(error)}`, { cause: error });
unreadable.push({
kind: "store-unreadable",
detail: String(error),
paths: [target.storePath]
});
}
return {
jsonPaths: [...jsonPaths],
stores,
unreadable
};
}
function resolveSourceLayout(resolved) {
return [.../* @__PURE__ */ new Set([...resolved.stores.map((store) => `sqlite:${store.databaseAgentId}:${resolvePhysicalPathIdentity(store.path)}`), ...resolved.jsonPaths.map((pathname) => `json:${resolvePhysicalPathIdentity(pathname)}`)])].toSorted();
}
function readLedger(env) {
return withExistingOpenClawStateDatabaseReadOnly(({ db }) => {
const row = executeSqliteQueryTakeFirstSync(db, getNodeSqliteKysely(db).selectFrom("migration_sources").select(["report_json", "status"]).where("source_key", "=", SOURCE_KEY));
if (!row) return;
try {
const parsed = JSON.parse(row.report_json);
if (!isRecord(parsed) || parsed.version !== REPORT_VERSION || typeof parsed.legacyAgentId !== "string" || typeof parsed.mainKey !== "string" || typeof parsed.ownerAgentId !== "string" || !Array.isArray(parsed.outcomes) || !Array.isArray(parsed.sourceLayout) || parsed.sourceLayout.some((entry) => typeof entry !== "string") || parsed.status !== "complete") return;
return {
report: parsed,
status: row.status
};
} catch {
return;
}
}, { env }) ?? void 0;
}
function ledgerMatches(ledger, identity) {
return ledger?.status === "completed" && ledger.report.version === REPORT_VERSION && ledger.report.status === "complete" && ledger.report.legacyAgentId === identity.legacyAgentId && ledger.report.ownerAgentId === identity.ownerAgentId && ledger.report.mainKey === identity.mainKey && ledger.report.sourceLayout.length === identity.sourceLayout.length && ledger.report.sourceLayout.every((entry, index) => entry === identity.sourceLayout[index]);
}
function writeLedger(params) {
const report = {
version: REPORT_VERSION,
...params.identity,
outcomes: params.outcomes,
status: "complete"
};
const reportJson = JSON.stringify(report);
const identityHash = sha256Hex(JSON.stringify(params.identity));
const runId = `${SOURCE_KEY}:${identityHash.slice(0, 24)}`;
params.beforePersistentApply?.();
runOpenClawStateWriteTransaction(({ db }) => {
const kysely = getNodeSqliteKysely(db);
const completion = {
finished_at: params.now,
status: "completed",
report_json: reportJson
};
executeSqliteQuerySync(db, kysely.insertInto("migration_runs").values({
id: runId,
started_at: params.now,
...completion
}).onConflict((conflict) => conflict.column("id").doUpdateSet(completion)));
const sourceCompletion = {
source_path: params.stateDir,
source_sha256: identityHash,
source_record_count: params.outcomes.length,
last_run_id: runId,
status: "completed",
imported_at: params.now,
removed_source: 1,
report_json: reportJson
};
executeSqliteQuerySync(db, kysely.insertInto("migration_sources").values({
source_key: SOURCE_KEY,
migration_kind: MIGRATION_KIND,
target_table: "session_nodes",
source_size_bytes: null,
...sourceCompletion
}).onConflict((conflict) => conflict.column("source_key").doUpdateSet(sourceCompletion)));
}, { env: params.env }, { operationLabel: "session-migration.legacy-main-ledger" });
}
/** Migrates retired agent-owned session keys without adding runtime read aliases. */
async function migrateLegacyMainSessionKeysInternal(params) {
const env = params.env ?? process.env;
const legacyAgentId = normalizeAgentId(params.legacyAgentId ?? "main");
const mainKey = normalizeMainKey(params.cfg.session?.mainKey);
const arming = resolveArmingDecision(params.cfg, legacyAgentId);
const base = {
changes: [],
legacyAgentId,
mainKey,
warnings: []
};
if (!arming.armed) {
if (arming.reason === "owner-unresolved") {
let rowsMayExist;
try {
const resolved = resolvePhysicalStores({
cfg: params.cfg,
env,
legacyAgentId,
mode: params.mode
});
rowsMayExist = resolved.unreadable.length > 0 || resolved.jsonPaths.length > 0 || resolved.stores.some((store) => inspectSessionStorePath(store.path) === "present" && storeHasLegacyAgentSessionKey({
env,
legacyAgentId,
store
}));
} catch {
rowsMayExist = true;
}
if (!rowsMayExist) return {
...base,
armed: false,
complete: true,
ledgerComplete: false,
outcomes: [{
kind: "no-legacy-rows",
detail: "no configured owner"
}]
};
}
const unresolved = arming.reason === "owner-unresolved";
return {
...base,
armed: false,
complete: false,
ledgerComplete: false,
outcomes: [{
kind: "not-armed",
detail: arming.reason
}],
warnings: unresolved ? [`session: legacy ${legacyAgentId} rows have no unambiguous configured owner; preserve them and run openclaw doctor --fix after assigning agents.defaults.sessionStore.agentId`] : []
};
}
const ownerAgentId = arming.ownerAgentId;
const resolved = resolvePhysicalStores({
cfg: params.cfg,
env,
legacyAgentId,
mode: params.mode,
ownerAgentId
});
const outcomes = [...resolved.jsonPaths.map((pathname) => ({
kind: "legacy-json-store",
paths: [pathname],
detail: "Doctor must migrate JSON sessions to SQLite before legacy-main key migration"
})), ...resolved.unreadable];
const warnings = [...base.warnings];
for (const unreadable of resolved.unreadable) warnings.push(`session: could not inspect ${unreadable.paths?.[0] ?? "session store"}: ${unreadable.detail ?? "unknown error"}; run openclaw doctor --fix`);
for (const pathname of resolved.jsonPaths) warnings.push(`session: deferred legacy-main session migration for JSON store ${pathname}; run openclaw doctor --fix`);
const identityBase = {
legacyAgentId,
mainKey,
ownerAgentId
};
const identity = {
...identityBase,
sourceLayout: resolveSourceLayout(resolved)
};
let matchingCompletedLedger = false;
if (params.mode !== "doctor-fix" && outcomes.length === 0) try {
if (ledgerMatches(readLedger(env), identity)) {
matchingCompletedLedger = true;
if (!params.forceScan) return {
...base,
armed: true,
complete: true,
ledgerComplete: true,
ownerAgentId,
outcomes: [{
kind: "no-legacy-rows",
detail: "matching completed ledger"
}]
};
}
} catch (error) {
return {
...base,
armed: true,
complete: false,
ledgerComplete: false,
ownerAgentId,
outcomes: [{
kind: "store-unreadable",
detail: String(error)
}],
warnings: [`session: could not read the legacy-main migration ledger: ${String(error)}; run openclaw doctor --fix`]
};
}
const { legacy: allLegacy, canonical: allCanonical } = readClaimsFromStores({
env,
legacyAgentId,
ownerAgentId,
stores: resolved.stores,
onUnreadable: (store, error) => {
if (params.mode === "doctor-fix") throw new Error(`cannot read legacy session store ${store.path}: ${String(error)}`, { cause: error });
outcomes.push({
kind: "store-unreadable",
detail: String(error),
paths: [store.path]
});
warnings.push(`session: could not inspect ${store.path}: ${String(error)}; run openclaw doctor --fix`);
}
});
if (params.mode === "detect" && allLegacy.length > 0) warnings.push(`session: ${allLegacy.length} retained legacy ${legacyAgentId} session claim(s) require Doctor repair; run openclaw doctor --fix`);
const destinationLogical = resolveSessionStorePathCore(params.cfg.session?.store, {
agentId: ownerAgentId,
env
});
const destinationResolved = resolveSqliteTargetFromSessionStorePath(destinationLogical, {
agentId: ownerAgentId,
defaultAgentId: ownerAgentId,
env
});
const destination = resolved.stores.find((store) => isSameOpenClawAgentDatabasePath(store.path, destinationResolved.path)) ?? {
databaseAgentId: normalizeAgentId(destinationResolved.agentId ?? ownerAgentId),
ownerStorePath: destinationLogical,
path: destinationResolved.path
};
const destinationArchiveDirectory = params.mode !== "doctor-fix" ? void 0 : resolveMissingPhysicalPath(path.join(resolveSessionArtifactDirectory(destinationResolved.path), "cold"));
const byCanonical = /* @__PURE__ */ new Map();
for (const claim of allLegacy) {
const claims = byCanonical.get(claim.canonicalKey) ?? [];
claims.push(claim);
byCanonical.set(claim.canonicalKey, claims);
}
for (const [canonicalKey, aliases] of byCanonical) {
const canonicalClaims = allCanonical.filter((claim) => claim.key === canonicalKey);
if (params.mode === "doctor-fix" && [...aliases, ...canonicalClaims].some((claim) => !samePhysicalStore(claim.store, destination) || resolveMissingPhysicalPath(path.join(resolveSessionArtifactDirectory(claim.store.path), "cold")) !== destinationArchiveDirectory)) {
await restoreColdSessionClaims(aliases, env, params.beforePersistentApply);
await restoreColdSessionClaims(canonicalClaims, env, params.beforePersistentApply);
}
const destinationCanonical = canonicalClaims.find((claim) => samePhysicalStore(claim.store, destination));
const foreignCanonical = canonicalClaims.filter((claim) => !samePhysicalStore(claim.store, destination));
const aliasesIdentical = aliases.every((claim) => claimsMatch(claim, aliases[0]));
const canonicalMatches = destinationCanonical ? aliases.every((claim) => claimsMatch(claim, destinationCanonical)) : false;
const divergence = foreignCanonical.length > 0 || destinationCanonical && !canonicalMatches ? "divergent-canonical" : !aliasesIdentical ? "divergent-aliases" : void 0;
if (divergence) {
const divergentClaims = divergence === "divergent-canonical" ? [...canonicalClaims, ...aliases] : aliases;
const outcome = {
kind: divergence,
canonicalKey,
paths: [...new Set(divergentClaims.map((claim) => claim.store.path))],
sourceKeys: divergentClaims.map((claim) => claim.key)
};
if (params.mode === "doctor-fix") {
const repaired = await repairDivergentClaims({
beforePersistentApply: params.beforePersistentApply,
canonicalKey,
claims: divergentClaims,
destination,
...divergence === "divergent-canonical" && destinationCanonical ? { destinationCanonical } : {},
env,
ownerAgentId
});
outcome.quarantinedKeys = repaired.quarantinedKeys;
if (repaired.resolved) outcome.resolved = true;
}
if (!outcome.resolved) warnings.push(warningForDivergence(divergence, canonicalKey, divergentClaims));
outcomes.push(outcome);
continue;
}
const outcome = await processIdenticalClaims({
beforePersistentApply: params.beforePersistentApply,
aliases,
...destinationCanonical ? { canonical: destinationCanonical } : {},
canonicalKey,
destination,
env,
mode: params.mode
});
outcomes.push(outcome);
if (outcome.kind === "divergent-aliases" || outcome.kind === "divergent-canonical") warnings.push(warningForDivergence(outcome.kind, canonicalKey, aliases));
}
if (allLegacy.length === 0 && outcomes.length === 0) outcomes.push({ kind: "no-legacy-rows" });
const complete = !outcomes.some((outcome) => outcome.kind === "legacy-json-store" || outcome.kind === "store-unreadable" || (outcome.kind === "divergent-aliases" || outcome.kind === "divergent-canonical") && outcome.resolved !== true);
const changes = params.mode !== "doctor-fix" ? [] : outcomes.flatMap((outcome) => outcome.kind === "migrated-in-place" || outcome.kind === "migrated-cross-store" || outcome.kind === "canonical-exists-identical" ? [`Migrated legacy ${legacyAgentId} session claim ${outcome.canonicalKey}.`] : outcome.quarantinedKeys?.length ? [`Quarantined ${outcome.quarantinedKeys.length} legacy ${legacyAgentId} session conflict(s).`] : []);
if (complete && params.mode === "doctor-fix") writeLedger({
beforePersistentApply: params.beforePersistentApply,
env,
identity: {
...identityBase,
sourceLayout: resolveSourceLayout(resolved)
},
now: params.now?.() ?? Date.now(),
outcomes,
stateDir: resolveStateDir(env)
});
return {
armed: true,
changes,
complete,
ledgerComplete: complete && (params.mode === "doctor-fix" || matchingCompletedLedger && allLegacy.length === 0),
legacyAgentId,
mainKey,
outcomes,
ownerAgentId,
warnings
};
}
async function migrateLegacyMainSessionKeys(params) {
try {
return await migrateLegacyMainSessionKeysInternal(params);
} catch (error) {
params.beforePersistentApply?.();
if (params.mode === "doctor-fix") throw error;
const legacyAgentId = normalizeAgentId(params.legacyAgentId ?? "main");
const mainKey = normalizeMainKey(params.cfg.session?.mainKey);
const arming = resolveArmingDecision(params.cfg, legacyAgentId);
return {
armed: arming.armed,
changes: [],
complete: false,
ledgerComplete: false,
legacyAgentId,
mainKey,
outcomes: [{
kind: "store-unreadable",
detail: String(error)
}],
...arming.armed ? { ownerAgentId: arming.ownerAgentId } : {},
warnings: [`session: legacy-main session migration deferred: ${String(error)}; run openclaw doctor --fix`]
};
}
}
//#endregion
export { migrateLegacyMainSessionKeys as t };