openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
338 lines (337 loc) • 15.3 kB
JavaScript
import { s as asFiniteNumber } from "./number-coercion-CkhMiRw8.mjs";
import { d as normalizeStringEntries } from "./string-normalization-D6DKCpwE.mjs";
import { r as resolveStateDir } from "./state-dir-CYkBUd_G.mjs";
import "./paths-BP0DYpE8.mjs";
import { o as resolveSqliteTargetFromSessionStorePath } from "./session-sqlite-target-Dd3VSz82.mjs";
import { F as listAgentRunsForSession, m as hasLiveAgentRunContext } from "./agent-run-registry-Cm8FlLIi.mjs";
import { d as getAgentEventLifecycleGeneration, f as isAgentEventLifecycleGenerationCurrent, t as assertAgentRunLifecycleGenerationCurrent } from "./agent-events-jWdQ_ESE.mjs";
import { F as isRetryableUnadoptedChatClaim, N as hasRestartRecoveryTerminalRun, P as isMainRestartRecoveryCandidate, R as normalizeMainSessionRecoveryRunFences, j as hasMainSessionRecoveryClaim } from "./session-accessor.sqlite-entry-worker-publication-BNeFwCVE.mjs";
import { r as captureGatewaySessionWorkAdmissions } from "./session-lifecycle-admission-B5CJ-emz.mjs";
import { t as listSessionEntriesReadOnly } from "./session-accessor.sqlite-entry-list.read-yRt0ntMz.mjs";
import "./session-accessor-C3ljJ974.mjs";
import { a as readSessionEntrySummariesInWorker, i as readSessionEntryReadOnlyInWorker } from "./session-entry-read-runtime-BdnQjoL6.mjs";
import { g as applySessionEntryExactReplacements } from "./session-accessor.entry-mutation-telsi3Ig.mjs";
import { n as listActiveEmbeddedRunSessionIds, r as listActiveEmbeddedRunSessionKeys } from "./active-run-projections-TAxZeJ_S.mjs";
import { i as isMainRestartRecoveryTerminalOnly, n as hasCompletedMainSessionRecoveryOutcome, s as transitionMainSessionRecovery } from "./main-session-recovery-state-DuO6mGeL.mjs";
import { i as mainSessionRecoveryLog, n as discoverRestartRecoveryStoreTargets, r as isKilledMainSessionRecoveryFenceOnly } from "./main-session-restart-recovery-shared-BcZUyAFd.mjs";
import { t as captureYieldedMainSessionContinuation } from "./main-session-restart-recovery-target-CHHaj5yO.mjs";
import { n as recordStartupRecoveryStoreResult, r as restartRecoveryStoreTargetKey } from "./main-session-restart-recovery-diagnostics-BO1MpxZW.mjs";
import { randomUUID } from "node:crypto";
//#region src/agents/main-session-recovery/main-session-recovery-live-owners.ts
function normalizeStringSet(values) {
return new Set(normalizeStringEntries(values ? [...values] : void 0));
}
function createCurrentProcessOwnerLookup(params) {
const providedIds = params.activeSessionIds === void 0 ? void 0 : normalizeStringSet(params.activeSessionIds);
const providedKeys = params.activeSessionKeys === void 0 ? void 0 : normalizeStringSet(params.activeSessionKeys);
return (entry, sessionKey) => {
const ids = providedIds ?? normalizeStringSet(listActiveEmbeddedRunSessionIds());
const keys = providedKeys ?? normalizeStringSet(listActiveEmbeddedRunSessionKeys());
return ids.has(entry.sessionId) || ids.size === 0 && keys.has(sessionKey);
};
}
//#endregion
//#region src/agents/main-session-recovery/main-session-restart-recovery-marking.ts
async function markRecoveryStore(params) {
const yieldOwners = [];
return await applySessionEntryExactReplacements({
agentId: params.agentId,
storePath: params.storePath,
sessionKeys: params.sessionKeys,
requireWriteSuccess: true,
assertCommitAllowed: () => {
params.assertCommitAllowed?.();
if (yieldOwners.some((isCurrent) => !isCurrent())) throw new Error("Yielded requester continuation changed before recovery handoff");
},
update: (entries) => {
const replacements = [];
const counts = {
marked: 0,
skipped: 0
};
for (const { sessionKey, entry } of entries) {
const plan = params.plan(entry, sessionKey);
if (!plan) continue;
if (!isMainRestartRecoveryCandidate(entry, sessionKey)) {
counts.skipped++;
continue;
}
if (plan.action !== "mark") {
if (plan.action === "restore_yielded") yieldOwners.push(plan.isCurrent);
transitionMainSessionRecovery(entry, plan.action === "restore_yielded" || plan.action === "release_killed" ? { kind: "clear" } : {
kind: "observe",
cycleId: randomUUID(),
lifecycleGeneration: getAgentEventLifecycleGeneration(),
sessionKey
});
replacements.push({
sessionKey,
entry
});
counts.skipped++;
continue;
}
if (plan.replaceRuns) entry.restartRecoveryRuns = plan.runs;
if (plan.forceRestartSafeTools) entry.restartRecoveryForceSafeTools = true;
transitionMainSessionRecovery(entry, {
kind: "mark_interrupted",
cycleId: randomUUID(),
now: Date.now(),
...plan
});
replacements.push({
sessionKey,
entry
});
counts.marked++;
}
return {
result: counts,
replacements
};
}
});
}
async function markRestartAbortedMainSessions(params) {
const activeRuns = [...params.activeRuns];
const currentLifecycleGeneration = getAgentEventLifecycleGeneration();
const result = {
marked: 0,
skipped: 0
};
const skipReasons = /* @__PURE__ */ new Map();
const recordSkip = (reason) => {
skipReasons.set(reason, (skipReasons.get(reason) ?? 0) + 1);
};
const activeAdmissions = captureGatewaySessionWorkAdmissions(params.resolveGatewayContext);
if (activeRuns.length === 0 && activeAdmissions.targets.size === 0) return result;
const storeTargets = /* @__PURE__ */ new Map();
const addStoreTarget = (target) => {
const resolved = resolveSqliteTargetFromSessionStorePath(target.storePath, { agentId: target.agentId });
const key = JSON.stringify([target.storePath, resolved.path]);
if (!storeTargets.has(key)) storeTargets.set(key, target);
};
const stateDir = params.stateDir ?? resolveStateDir(process.env);
try {
for (const target of await discoverRestartRecoveryStoreTargets({
cfg: params.cfg,
stateDir
})) addStoreTarget(target);
} catch (err) {
if (!params.cfg) throw err;
mainSessionRecoveryLog.warn(`failed to resolve configured session stores for restart marker: ${String(err)}`);
}
for (const storePath of activeAdmissions.targets.keys()) addStoreTarget({ storePath });
for (const target of storeTargets.values()) {
const { storePath } = target;
const sessionKeys = listSessionEntriesReadOnly({
...target,
projection: "list",
clone: false
}).filter(({ sessionKey, entry }) => activeRuns.some((run) => run.sessionKey === sessionKey && run.sessionId === entry.sessionId) || activeAdmissions.isActive({
scope: storePath,
sessionKey,
sessionId: entry.sessionId
})).map(({ sessionKey }) => sessionKey);
for (const selectedSessionKey of sessionKeys) {
let isCurrent;
try {
const storeResult = await markRecoveryStore({
...target,
sessionKeys: [selectedSessionKey],
assertCommitAllowed: () => {
if (isCurrent && !isCurrent()) throw new Error("Restart recovery owner changed before commit");
},
plan: (entry, sessionKey) => {
const matchingActiveRuns = activeRuns.filter((run) => run.sessionKey === sessionKey && run.sessionId === entry.sessionId && (entry.lifecycleRunId === run.runId || entry.restartRecoveryDeliveryRunId === run.runId || run.observedAt === void 0 || asFiniteNumber(entry.updatedAt) === void 0 || entry.updatedAt < run.observedAt && run.lifecycleGeneration !== currentLifecycleGeneration) && params.isActiveRun?.(run) !== false);
const matchedActiveAdmission = activeAdmissions.isActive({
scope: storePath,
sessionKey,
sessionId: entry.sessionId
});
if (matchingActiveRuns.length === 0 && !matchedActiveAdmission) {
recordSkip("owner_changed");
return;
}
if (captureYieldedMainSessionContinuation({
storeAgentId: target.agentId,
cfg: params.cfg,
entry,
sessionKey,
storePath
})) {
recordSkip("yielded_continuation");
return;
}
if (!isMainRestartRecoveryCandidate(entry, sessionKey)) recordSkip("not_main_session");
const runs = normalizeMainSessionRecoveryRunFences([
...(entry.restartRecoveryRuns ?? []).filter((run) => run.lifecycleGeneration === currentLifecycleGeneration),
...listAgentRunsForSession({
sessionKey,
sessionId: entry.sessionId
}),
...matchingActiveRuns.map(({ runId, lifecycleGeneration }) => ({
runId,
lifecycleGeneration
}))
]);
isCurrent = () => isAgentEventLifecycleGenerationCurrent(currentLifecycleGeneration) && (matchedActiveAdmission && activeAdmissions.isActive({
scope: storePath,
sessionKey,
sessionId: entry.sessionId
}) || matchingActiveRuns.some((run) => params.isActiveRun?.(run) !== false));
return {
action: "mark",
forceRestartSafeTools: matchedActiveAdmission,
replaceRuns: true,
runs
};
}
});
result.marked += storeResult.marked;
result.skipped += storeResult.skipped;
} catch (error) {
assertAgentRunLifecycleGenerationCurrent(currentLifecycleGeneration);
if (!isCurrent || isCurrent()) throw error;
result.skipped++;
recordSkip("owner_changed");
}
}
}
if (result.marked > 0) mainSessionRecoveryLog.warn(`marked ${result.marked} interrupted main session(s) for restart recovery${params.reason ? ` (${params.reason})` : ""}`);
else if (activeRuns.length > 0) {
const dominantSkipReason = [...skipReasons].toSorted((a, b) => b[1] - a[1])[0]?.[0];
mainSessionRecoveryLog.warn(`marked 0 interrupted main session(s) for restart recovery: activeRuns=${activeRuns.length} skipped=${result.skipped} skipReason=${dominantSkipReason ?? "no_matching_session"}${params.reason ? ` (${params.reason})` : ""}`);
}
return result;
}
function hasOrphanedMainSessionWork(entry) {
return hasMainSessionRecoveryClaim(entry) || entry.abortedLastRun === true || entry.status === "interrupted";
}
async function markOrphanedMainSessionStore(params) {
const hasCurrentProcessOwner = createCurrentProcessOwnerLookup(params);
const updatedBeforeMs = asFiniteNumber(params.updatedBeforeMs);
const key = params.target.sessionKey;
let sessionKeys;
if (key) {
const entry = await readSessionEntryReadOnlyInWorker({
...params.target,
sessionKey: key
});
sessionKeys = entry && hasOrphanedMainSessionWork(entry) ? [key] : [];
} else sessionKeys = (await readSessionEntrySummariesInWorker(params.target)).filter(({ entry }) => hasOrphanedMainSessionWork(entry)).map(({ sessionKey }) => sessionKey);
if (sessionKeys.length === 0) return {
marked: 0,
skipped: 0
};
const orphanChecks = [];
return await markRecoveryStore({
...params.target,
sessionKeys,
assertCommitAllowed: () => {
assertAgentRunLifecycleGenerationCurrent(params.lifecycleGeneration);
params.assertCommitAllowed?.();
if (orphanChecks.some((hasLiveOwner) => hasLiveOwner())) throw new Error("Startup orphan acquired a live owner before recovery commit");
},
plan: (entry, sessionKey) => {
params.assertCommitAllowed?.();
if (!hasOrphanedMainSessionWork(entry) || entry.archivedAt !== void 0 || isRetryableUnadoptedChatClaim(entry) || entry.mainRestartRecovery?.tombstone || params.expectedSessionId !== void 0 && entry.sessionId !== params.expectedSessionId || params.expectedLifecycleRevision !== void 0 && entry.lifecycleRevision !== params.expectedLifecycleRevision) return;
const updatedAt = asFiniteNumber(entry.updatedAt);
if (updatedBeforeMs !== void 0 && updatedAt !== void 0 && updatedAt > updatedBeforeMs) return;
const writerRunIds = [
entry.activeWriterRunId,
entry.lifecycleRunId,
...(entry.restartRecoveryRuns ?? []).map((run) => run.runId)
];
const hasLiveOwner = () => writerRunIds.some((runId) => runId && hasLiveAgentRunContext(runId)) || listAgentRunsForSession({
sessionKey,
sessionId: entry.sessionId
}).some(({ runId }) => hasLiveAgentRunContext(runId)) || hasCurrentProcessOwner(entry, sessionKey);
if (hasLiveOwner()) return;
const continuation = captureYieldedMainSessionContinuation({
storeAgentId: params.target.agentId,
cfg: params.cfg,
entry,
sessionKey,
storePath: params.target.storePath
});
if (continuation) {
const state = entry.mainRestartRecovery;
if (entry.abortedLastRun === true && !state?.reservation && !state?.foregroundClaims && !state?.tombstone && !entry.restartRecoveryDeliveryRunId) return {
action: "restore_yielded",
isCurrent: () => continuation() && !hasLiveOwner()
};
return;
}
if (isKilledMainSessionRecoveryFenceOnly(entry)) {
orphanChecks.push(hasLiveOwner);
return { action: "release_killed" };
}
const hasClaim = hasMainSessionRecoveryClaim(entry);
const completed = hasCompletedMainSessionRecoveryOutcome(entry);
if (!hasClaim && (completed || [
entry.lifecycleRunId,
entry.activeWriterRunId,
entry.restartRecoveryDeliverySourceRunId
].some((runId) => runId !== void 0 && hasRestartRecoveryTerminalRun(entry, runId)))) return;
if (hasClaim && !completed && (entry.abortedLastRun === true || !entry.pendingFinalDelivery && entry.status !== void 0 && entry.status !== "failed" && entry.status !== "interrupted")) return;
orphanChecks.push(hasLiveOwner);
return completed || isMainRestartRecoveryTerminalOnly(entry) ? { action: "retire_terminal" } : { action: "mark" };
}
});
}
/** Reconcile one exact session through the same owner used by startup. */
async function markOrphanedMainSessionForRecovery(params) {
return await markOrphanedMainSessionStore({
...params,
lifecycleGeneration: getAgentEventLifecycleGeneration()
});
}
async function markStartupOrphanedMainSessionsForRecovery(params) {
const result = {
marked: 0,
skipped: 0
};
const failedTargets = [];
const lifecycleGeneration = getAgentEventLifecycleGeneration();
const storeTargets = await discoverRestartRecoveryStoreTargets(params);
for (const target of storeTargets) {
const key = restartRecoveryStoreTargetKey(target);
if (params.startupCheckedStorePaths?.has(key)) continue;
try {
const storeResult = await markOrphanedMainSessionStore({
...params,
target,
lifecycleGeneration
});
assertAgentRunLifecycleGenerationCurrent(lifecycleGeneration);
result.marked += storeResult.marked;
result.skipped += storeResult.skipped;
params.startupCheckedStorePaths?.add(key);
recordStartupRecoveryStoreResult({
target,
lifecycleGeneration,
outcome: { ok: true }
});
} catch (error) {
assertAgentRunLifecycleGenerationCurrent(lifecycleGeneration);
failedTargets.push(target);
recordStartupRecoveryStoreResult({
target,
lifecycleGeneration,
outcome: {
ok: false,
error
}
});
mainSessionRecoveryLog.warn(`failed to mark startup-orphaned main sessions for ${target.agentId}: ${String(error)}`);
}
}
if (result.marked > 0) mainSessionRecoveryLog.warn(`marked ${result.marked} startup-orphaned main session(s) for restart recovery`);
return {
...result,
...failedTargets.length > 0 ? { failedTargets } : {}
};
}
//#endregion
export { createCurrentProcessOwnerLookup as i, markRestartAbortedMainSessions as n, markStartupOrphanedMainSessionsForRecovery as r, markOrphanedMainSessionForRecovery as t };