openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
447 lines (446 loc) • 20.4 kB
JavaScript
import { S as isCronSessionKey, b as isAcpSessionKey, w as isSubagentSessionKey } from "./session-key-C09ow4h0.mjs";
import { a as hasRestartRecoveryTerminalRun } from "./restart-recovery-state-IiHiWzPq.mjs";
import { i as removeMainSessionRecoveryForegroundClaim, n as buildMainSessionRecoveryClearPatch } from "./main-session-recovery-clear-BczELu-B.mjs";
import { r as sanitizePendingFinalDeliveryText, t as PENDING_FINAL_DELIVERY_CLEAR_PATCH } from "./pending-final-delivery-state-Cmhed7ry.mjs";
import { i as resolveRestartRecoveryTerminalClientRunId } from "./main-session-restart-recovery-shared-D4YztC_w.mjs";
//#region src/agents/main-session-recovery/main-session-recovery-admitted-interruption.ts
function interruptAdmittedMainSessionRecovery(entry, command) {
const state = entry.mainRestartRecovery;
if (entry.sessionId !== command.sessionId) return {
kind: "rejected",
reason: "session_replaced"
};
if (!state || state.cycleId !== command.cycleId || state.chargedAttempts !== command.attempt || state.reservation || state.foregroundClaims || !entry.restartRecoveryRuns?.some((run) => run.runId === command.runId && run.lifecycleGeneration === command.lifecycleGeneration)) return {
kind: "rejected",
reason: "stale_reservation"
};
if (entry.lifecycleRunId !== command.runId) return entry.status === "running" && entry.abortedLastRun === true && entry.lifecycleRunId === void 0 && entry.restartRecoveryDeliveryRunId === void 0 ? { kind: "no_change" } : {
kind: "rejected",
reason: "stale_reservation"
};
entry.status = "running";
entry.lifecycleRunId = void 0;
entry.lastRunId = void 0;
entry.abortedLastRun = true;
entry.startedAt = void 0;
entry.endedAt = void 0;
entry.runtimeMs = void 0;
if (entry.restartRecoveryDeliveryRunId === command.runId) entry.restartRecoveryDeliveryRunId = void 0;
entry.updatedAt = command.now;
return { kind: "applied" };
}
//#endregion
//#region src/agents/main-session-recovery/main-session-recovery-state.ts
function updateRecoveryState(entry, state, patch) {
return entry.mainRestartRecovery = {
...state,
revision: state.revision + 1,
...patch
};
}
function createCycle(cycleId) {
return {
cycleId,
revision: 1,
chargedAttempts: 0
};
}
function getMainSessionRecoveryRetryCount(state) {
return state ? state.chargedAttempts - (state.startedAttempt ?? 0) : 0;
}
function matchesObservation(entry, observation) {
if (entry.sessionId !== observation.sessionId) return "session_replaced";
if (entry.mainRestartRecovery?.cycleId !== observation.cycleId) return "stale_cycle";
return entry.mainRestartRecovery.revision === observation.revision ? null : "stale_revision";
}
function hasCurrentForegroundClaim(state, lifecycleGeneration) {
return state.foregroundClaims?.lifecycleGeneration === lifecycleGeneration && state.foregroundClaims.tokens.length > 0;
}
function ownsForegroundClaim(state, claim) {
return state?.cycleId === claim.cycleId && state.foregroundClaims?.lifecycleGeneration === claim.lifecycleGeneration && state.foregroundClaims.tokens.includes(claim.claimId);
}
function validateRecoveryAdmission(entry, command) {
const state = entry.mainRestartRecovery;
if (entry.sessionId !== command.sessionId) return "session_replaced";
if (entry.status !== "running" || entry.abortedLastRun !== true || !state) return "not_interrupted";
if (state.reservation?.runId !== command.runId || state.reservation.lifecycleGeneration !== command.lifecycleGeneration) return "stale_reservation";
return hasCurrentForegroundClaim(state, command.lifecycleGeneration) ? "foreground_active" : null;
}
/** Keeps distinct concurrent runs while transferring each run id to its newest lifecycle owner. */
function normalizeMainSessionRecoveryRunFences(runs) {
return [...new Map([...runs].map((run) => [run.runId, run])).values()].toSorted((left, right) => left.runId.localeCompare(right.runId));
}
function recordLifecycleFence(entry, run) {
entry.restartRecoveryRuns = normalizeMainSessionRecoveryRunFences([...entry.restartRecoveryRuns ?? [], run]);
}
function isMainRestartRecoveryCandidate(entry, sessionKey) {
if (typeof entry.spawnDepth === "number" && entry.spawnDepth > 0) return false;
if (entry.subagentRole != null) return false;
return !isSubagentSessionKey(sessionKey) && !isCronSessionKey(sessionKey) && !isAcpSessionKey(sessionKey);
}
function isMainSessionRecoveryPending(entry, sessionKey) {
const state = entry.mainRestartRecovery;
return entry.status === "running" && entry.abortedLastRun === true && isMainRestartRecoveryCandidate(entry, sessionKey) && !state?.foregroundClaims && !state?.reservation && !state?.tombstone;
}
/** Failed foreground admission can leave an unfinished recovery cycle behind. */
function isMainSessionRecoveryReconciliationCandidate(entry) {
return (entry.status === void 0 || entry.status === "running" || entry.status === "failed") && entry.abortedLastRun !== true && entry.mainRestartRecovery !== void 0 && !entry.mainRestartRecovery.tombstone;
}
function inspectMainRestartRecoveryRolloverEligibility(entry) {
if (!entry.mainRestartRecovery?.tombstone) return {
eligible: false,
reason: "not_tombstoned"
};
const recoveredSessionId = entry.mainRestartRecovery.tombstone.recoveredSessionId;
const recoveredSessionKey = entry.mainRestartRecovery.tombstone.recoveredSessionKey;
if (recoveredSessionId || recoveredSessionKey) return {
eligible: false,
reason: "already_recovered",
...recoveredSessionId ? { recoveredSessionId } : {},
...recoveredSessionKey ? { recoveredSessionKey } : {}
};
return { eligible: true };
}
function isMainRestartRecoveryAggregateTerminalOnly(entry) {
const state = entry.mainRestartRecovery;
if (!state || state.tombstone || state.reservation || state.foregroundClaims) return false;
if (entry.restartRecoveryDeliveryRunId !== void 0) return false;
const runs = entry.restartRecoveryRuns;
return runs !== void 0 && runs.length > 0 && runs.every((run) => hasRestartRecoveryTerminalRun(entry, run.runId));
}
function hasOrphanedMainRestartRecoveryFences(entry, sessionKey) {
if (!isMainRestartRecoveryCandidate(entry, sessionKey)) return false;
return entry.status === "running" && entry.abortedLastRun !== true && entry.restartRecoveryDeliveryRunId === void 0 && (entry.restartRecoveryRuns !== void 0 && entry.mainRestartRecovery === void 0 || isMainRestartRecoveryAggregateTerminalOnly(entry)) || entry.status !== "running" && entry.mainRestartRecovery === void 0 && (entry.restartRecoveryRuns !== void 0 || entry.abortedLastRun === true);
}
function inspectMainSessionRecovery(params) {
const { entry } = params;
const state = entry.mainRestartRecovery;
if (state?.tombstone) return { status: "tombstoned" };
if (state && hasCurrentForegroundClaim(state, params.lifecycleGeneration)) return { status: "blocked" };
if (entry.status === "running" && entry.abortedLastRun !== true && state && entry.restartRecoveryRuns?.some((run) => run.lifecycleGeneration === params.lifecycleGeneration)) return { status: "blocked" };
if (entry.status !== "running" || entry.abortedLastRun !== true || !isMainRestartRecoveryCandidate(entry, params.sessionKey) || !state) return { status: "inactive" };
const observation = {
sessionId: entry.sessionId,
cycleId: state.cycleId,
revision: state.revision
};
if (state.reservation) return { status: "blocked" };
const retryCount = getMainSessionRecoveryRetryCount(state);
if (retryCount >= 3) return {
status: "exhausted",
observation,
reason: `main-session restart recovery blocked after ${retryCount} automatic attempts without a started runtime turn; inspect the failed main session and use /new or reset to start a replacement session`
};
return {
status: "recoverable",
observation,
nextAttempt: state.chargedAttempts + 1
};
}
function inspectMainSessionRecoveryForAdmission(params) {
if (params.entry.status === "running" && params.entry.abortedLastRun !== true && params.entry.mainRestartRecovery && params.entry.restartRecoveryRuns?.length && !isMainRestartRecoveryAggregateTerminalOnly(params.entry)) return { status: "blocked" };
if (params.entry.status === "running" && params.entry.abortedLastRun === true && isMainRestartRecoveryCandidate(params.entry, params.sessionKey) && !params.entry.mainRestartRecovery) return { status: "blocked" };
return inspectMainSessionRecovery(params);
}
function transitionMainSessionRecovery(entry, command) {
switch (command.kind) {
case "mark_interrupted": {
const state = entry.mainRestartRecovery;
if (!state) entry.mainRestartRecovery = createCycle(command.cycleId);
else if (state.foregroundClaims || state.reservation) updateRecoveryState(entry, state, {
foregroundClaims: void 0,
reservation: void 0
});
entry.status = "running";
entry.activeWriterRunId = void 0;
entry.lifecycleRunId = void 0;
entry.lastRunId = void 0;
entry.abortedLastRun = true;
if (command.resetRuntime) {
entry.startedAt = void 0;
entry.endedAt = void 0;
entry.runtimeMs = void 0;
}
for (const run of command.runs ?? []) recordLifecycleFence(entry, run);
entry.updatedAt = command.now;
return { kind: "applied" };
}
case "inspect": return {
kind: "observed",
view: inspectMainSessionRecoveryForAdmission({
entry,
lifecycleGeneration: command.lifecycleGeneration,
sessionKey: command.sessionKey
})
};
case "observe": {
if (entry.status === "running" && entry.abortedLastRun === true && isMainRestartRecoveryCandidate(entry, command.sessionKey) && !entry.mainRestartRecovery) entry.mainRestartRecovery = createCycle(command.cycleId);
let state = entry.mainRestartRecovery;
if (state?.foregroundClaims && state.foregroundClaims.lifecycleGeneration !== command.lifecycleGeneration) {
if (entry.abortedLastRun !== true) {
Object.assign(entry, buildMainSessionRecoveryClearPatch(entry));
state = void 0;
} else state = updateRecoveryState(entry, state, { foregroundClaims: void 0 });
}
if (state?.reservation && state.reservation.lifecycleGeneration !== command.lifecycleGeneration) updateRecoveryState(entry, state, { reservation: void 0 });
if (entry.abortedLastRun !== true && isMainRestartRecoveryAggregateTerminalOnly(entry)) Object.assign(entry, buildMainSessionRecoveryClearPatch(entry));
return {
kind: "observed",
view: inspectMainSessionRecovery({
entry,
lifecycleGeneration: command.lifecycleGeneration,
sessionKey: command.sessionKey
})
};
}
case "prepare_attempt": {
const conflict = matchesObservation(entry, command.observation);
if (conflict) return {
kind: "rejected",
reason: conflict
};
const state = entry.mainRestartRecovery;
if (entry.status !== "running" || entry.abortedLastRun !== true) return {
kind: "rejected",
reason: "not_interrupted"
};
if (state.tombstone) return {
kind: "rejected",
reason: "already_tombstoned"
};
if (state.reservation) return {
kind: "rejected",
reason: "reservation_active"
};
if (command.attempt !== state.chargedAttempts + 1) return {
kind: "rejected",
reason: "stale_revision"
};
const retryExecutionIdentity = command.executionIdentity.state === "enabled" && state.executionIdentity ? state.executionIdentity : void 0;
const executionIdentityAdmission = retryExecutionIdentity ? {
kind: "retry-reference",
token: retryExecutionIdentity
} : void 0;
updateRecoveryState(entry, state, {
executionIdentity: retryExecutionIdentity,
chargedAttempts: command.attempt,
reservation: {
runId: command.runId,
attempt: command.attempt,
lifecycleGeneration: command.lifecycleGeneration
}
});
entry.updatedAt = command.now;
return {
kind: "reserved",
reservation: {
sessionId: entry.sessionId,
cycleId: state.cycleId,
lifecycleGeneration: command.lifecycleGeneration,
runId: command.runId,
attempt: command.attempt,
...executionIdentityAdmission ? { executionIdentityAdmission } : {}
}
};
}
case "bind_admitted_execution_identity":
case "register_recovery_turn": {
const state = entry.mainRestartRecovery;
if (!state || state.cycleId !== command.cycleId || state.chargedAttempts !== command.attempt || entry.sessionId !== command.sessionId || entry.lifecycleRunId !== command.runId || !entry.restartRecoveryRuns?.some((run) => run.runId === command.runId && run.lifecycleGeneration === command.lifecycleGeneration)) return {
kind: "rejected",
reason: "stale_reservation"
};
if (command.kind === "register_recovery_turn") {
if (state.startedAttempt === command.attempt) return { kind: "no_change" };
updateRecoveryState(entry, state, { startedAttempt: command.attempt });
} else {
if (state.executionIdentity) return JSON.stringify(state.executionIdentity) === JSON.stringify(command.token) ? { kind: "no_change" } : {
kind: "rejected",
reason: "stale_reservation"
};
if (command.token.runId !== command.runId) return {
kind: "rejected",
reason: "stale_reservation"
};
updateRecoveryState(entry, state, { executionIdentity: command.token });
}
return { kind: "applied" };
}
case "cancel_reservation":
case "abandon_reservation": {
const state = entry.mainRestartRecovery;
const reserved = state?.reservation;
if (!state || entry.sessionId !== command.reservation.sessionId || state.cycleId !== command.reservation.cycleId || reserved?.runId !== command.reservation.runId || reserved.attempt !== command.reservation.attempt || reserved.lifecycleGeneration !== command.reservation.lifecycleGeneration) return {
kind: "rejected",
reason: "stale_reservation"
};
updateRecoveryState(entry, state, {
chargedAttempts: command.kind === "cancel_reservation" ? Math.max(0, command.reservation.attempt - 1) : state.chargedAttempts,
reservation: void 0
});
return { kind: "applied" };
}
case "validate_recovery": {
const conflict = validateRecoveryAdmission(entry, command);
return conflict ? {
kind: "rejected",
reason: conflict
} : { kind: "recovery_validated" };
}
case "admit_recovery": {
const conflict = validateRecoveryAdmission(entry, command);
if (conflict) return {
kind: "rejected",
reason: conflict
};
const state = entry.mainRestartRecovery;
updateRecoveryState(entry, state, {
reservation: void 0,
foregroundClaims: void 0
});
entry.abortedLastRun = false;
entry.lifecycleRunId = command.runId;
entry.lastRunId = void 0;
recordLifecycleFence(entry, {
runId: command.runId,
lifecycleGeneration: command.lifecycleGeneration
});
if (entry.pendingFinalDelivery?.kind === "replayable") {
const pendingText = sanitizePendingFinalDeliveryText(entry.pendingFinalDelivery.text);
if (pendingText) entry.pendingFinalDelivery = {
...entry.pendingFinalDelivery,
text: pendingText
};
else Object.assign(entry, PENDING_FINAL_DELIVERY_CLEAR_PATCH);
}
return {
kind: "admitted_recovery",
admission: {
cycleId: state.cycleId,
attempt: state.chargedAttempts,
lifecycleGeneration: command.lifecycleGeneration,
runId: command.runId,
sessionId: command.sessionId
}
};
}
case "mark_admitted_recovery_interrupted": return interruptAdmittedMainSessionRecovery(entry, command);
case "claim_foreground": {
if (entry.sessionId === command.sessionId && hasOrphanedMainRestartRecoveryFences(entry, command.sessionKey)) {
Object.assign(entry, buildMainSessionRecoveryClearPatch(entry));
return { kind: "applied" };
}
if (entry.sessionId !== command.sessionId || entry.status !== "running" || entry.abortedLastRun !== true || !isMainRestartRecoveryCandidate(entry, command.sessionKey)) return { kind: "no_change" };
const state = entry.mainRestartRecovery ?? createCycle(command.cycleId);
if (state.tombstone) return {
kind: "rejected",
reason: "already_tombstoned"
};
if (getMainSessionRecoveryRetryCount(state) >= 3) return {
kind: "rejected",
reason: "recovery_exhausted"
};
const currentClaims = state.foregroundClaims?.lifecycleGeneration === command.lifecycleGeneration ? state.foregroundClaims : void 0;
const tokens = [.../* @__PURE__ */ new Set([...currentClaims?.tokens ?? [], command.claimId])].toSorted();
const runIdsByClaimId = command.runId ? {
...currentClaims?.runIdsByClaimId,
[command.claimId]: command.runId
} : currentClaims?.runIdsByClaimId;
if (command.runId) recordLifecycleFence(entry, {
lifecycleGeneration: command.lifecycleGeneration,
runId: command.runId
});
updateRecoveryState(entry, state, {
reservation: state.reservation?.lifecycleGeneration === command.lifecycleGeneration ? state.reservation : void 0,
foregroundClaims: {
lifecycleGeneration: command.lifecycleGeneration,
tokens,
...runIdsByClaimId ? { runIdsByClaimId } : {}
}
});
return {
kind: "foreground_claimed",
claim: {
cycleId: state.cycleId,
lifecycleGeneration: command.lifecycleGeneration,
claimId: command.claimId,
sessionId: entry.sessionId,
sessionKey: command.sessionKey,
...command.runId ? { runId: command.runId } : {}
}
};
}
case "bind_foreground_run": {
const state = entry.mainRestartRecovery;
const claims = state?.foregroundClaims;
if (!state || !claims || !ownsForegroundClaim(state, command.claim)) return { kind: "no_change" };
recordLifecycleFence(entry, {
lifecycleGeneration: command.claim.lifecycleGeneration,
runId: command.runId
});
updateRecoveryState(entry, state, { foregroundClaims: {
...claims,
runIdsByClaimId: {
...claims.runIdsByClaimId,
[command.claim.claimId]: command.runId
}
} });
return { kind: "applied" };
}
case "validate_foreground": {
const state = entry.mainRestartRecovery;
return entry.sessionId === command.claim.sessionId && ownsForegroundClaim(state, command.claim) ? { kind: "foreground_validated" } : { kind: "no_change" };
}
case "release_foreground": {
const state = entry.mainRestartRecovery;
const claims = state?.foregroundClaims;
if (!state || !claims || !ownsForegroundClaim(state, command.claim)) return { kind: "no_change" };
const foregroundClaims = removeMainSessionRecoveryForegroundClaim(claims, command.claim.claimId);
if (!foregroundClaims && entry.abortedLastRun !== true) {
Object.assign(entry, buildMainSessionRecoveryClearPatch(entry));
return { kind: "applied" };
}
updateRecoveryState(entry, state, { foregroundClaims });
return { kind: "applied" };
}
case "tombstone": {
const conflict = matchesObservation(entry, command.observation);
if (conflict) return {
kind: "rejected",
reason: conflict
};
const state = entry.mainRestartRecovery;
if (state.reservation) return {
kind: "rejected",
reason: "reservation_active"
};
if (state.tombstone) return {
kind: "rejected",
reason: "already_tombstoned"
};
updateRecoveryState(entry, state, { tombstone: { reason: command.reason } });
entry.abortedLastRun = false;
entry.status = "failed";
entry.lifecycleRunId = void 0;
entry.lastRunId = resolveRestartRecoveryTerminalClientRunId(entry);
entry.endedAt = command.now;
entry.runtimeMs = Math.max(0, command.now - (entry.startedAt ?? command.now));
entry.updatedAt = command.now;
return { kind: "tombstoned" };
}
case "doctor_repair":
if (!entry.mainRestartRecovery?.tombstone || entry.abortedLastRun !== true) return { kind: "no_change" };
entry.abortedLastRun = false;
entry.updatedAt = command.now;
return { kind: "doctor_repaired" };
case "clear": {
const patch = buildMainSessionRecoveryClearPatch(entry);
if (Object.keys(patch).length === 0) return { kind: "no_change" };
Object.assign(entry, patch);
return { kind: "applied" };
}
default: return command;
}
}
//#endregion
export { isMainSessionRecoveryPending as a, transitionMainSessionRecovery as c, isMainRestartRecoveryCandidate as i, inspectMainRestartRecoveryRolloverEligibility as n, isMainSessionRecoveryReconciliationCandidate as o, isMainRestartRecoveryAggregateTerminalOnly as r, normalizeMainSessionRecoveryRunFences as s, getMainSessionRecoveryRetryCount as t };