UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

432 lines (431 loc) • 20.8 kB
import { F as isRetryableUnadoptedChatClaim, N as hasRestartRecoveryTerminalRun, P as isMainRestartRecoveryCandidate, V as recordLifecycleFence, j as hasMainSessionRecoveryClaim } from "./session-accessor.sqlite-entry-worker-publication-BNeFwCVE.mjs"; import { n as isTerminalSessionStatus } from "./types-BGDhC4_W.mjs"; import { a as removeMainSessionRecoveryForegroundClaim, n as buildMainSessionRecoveryClearPatch, r as buildMainSessionRecoverySettlementPatch } from "./main-session-recovery-clear-B0tByn8u.mjs"; import { r as sanitizePendingFinalDeliveryText, t as PENDING_FINAL_DELIVERY_CLEAR_PATCH } from "./pending-final-delivery-state-Dt4W-G2W.mjs"; import { a as resolveRestartRecoveryTerminalClientRunId, r as isKilledMainSessionRecoveryFenceOnly } from "./main-session-restart-recovery-shared-BcZUyAFd.mjs"; //#region src/agents/main-session-recovery/main-session-recovery-empty-aggregate.ts function isMainRestartRecoveryAggregateEmptyAndUnowned(entry) { const state = entry.mainRestartRecovery; return entry.abortedLastRun !== true && state !== void 0 && state.chargedAttempts === 0 && state.startedAttempt === void 0 && state.executionIdentity === void 0 && state.reservation === void 0 && state.foregroundClaims === void 0 && state.tombstone === void 0 && entry.restartRecoveryRuns === void 0 && entry.restartRecoveryDeliveryRunId === void 0 && entry.pendingFinalDelivery === void 0; } //#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.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; } function isMainSessionRecoveryPending(entry, sessionKey) { const state = entry.mainRestartRecovery; return hasMainSessionRecoveryClaim(entry) && entry.abortedLastRun === true && isMainRestartRecoveryCandidate(entry, sessionKey) && !state?.foregroundClaims && !state?.reservation && !state?.tombstone; } /** Select reconciliation before new input adds custody; the store owner checks run liveness. */ function isMainSessionRecoveryReconciliationCandidate(entry) { const killedFenceOnly = isKilledMainSessionRecoveryFenceOnly(entry); return (entry.status === void 0 || entry.status === "failed" || killedFenceOnly || isMainRestartRecoveryTerminalOnly(entry)) && (entry.abortedLastRun !== true || killedFenceOnly) && hasMainSessionRecoveryClaim(entry) && !isRetryableUnadoptedChatClaim(entry) && !entry.mainRestartRecovery?.tombstone; } /** A later foreground outcome cannot settle a different run's recovery fence. */ function hasCompletedMainSessionRecoveryOutcome(entry) { return isTerminalSessionStatus(entry.status) && entry.status !== "interrupted" && !isRetryableUnadoptedChatClaim(entry) && !entry.pendingFinalDelivery && !entry.restartRecoveryHarnessCompletion && (entry.restartRecoveryRuns ?? []).every((run) => hasRestartRecoveryTerminalRun(entry, run.runId)); } 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 isMainRestartRecoveryTerminalOnly(entry) { const state = entry.mainRestartRecovery; if (state?.tombstone || state?.reservation || state?.foregroundClaims) return false; if (entry.restartRecoveryDeliveryRunId !== void 0 || entry.pendingFinalDelivery) return false; const runs = entry.restartRecoveryRuns; return runs !== void 0 && runs.length > 0 && runs.every((run) => hasRestartRecoveryTerminalRun(entry, run.runId)); } 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.abortedLastRun !== true && state && entry.restartRecoveryRuns?.some((run) => run.lifecycleGeneration === params.lifecycleGeneration)) return { status: "blocked" }; if (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.abortedLastRun !== true && params.entry.mainRestartRecovery && params.entry.restartRecoveryRuns?.length && !isMainRestartRecoveryTerminalOnly(params.entry)) return { status: "blocked" }; if (hasMainSessionRecoveryClaim(params.entry) && 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 startedAt = entry.lifecycleRunId ? entry.startedAt : void 0; const preserveOutcome = entry.status === void 0 && entry.endedAt !== void 0 || isTerminalSessionStatus(entry.status) && entry.status !== "interrupted" && Boolean(entry.restartRecoveryDeliveryRunId || entry.pendingFinalDelivery); entry.restartRecoveryDeliverySourceRunId ??= entry.restartRecoveryDeliveryRunId ?? entry.lifecycleRunId; 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.activeWriterRunId = void 0; entry.lifecycleRunId = void 0; entry.abortedLastRun = true; if (!preserveOutcome) { entry.status = "interrupted"; entry.lastRunId = void 0; entry.startedAt = startedAt; entry.endedAt = command.now; entry.runtimeMs = typeof entry.startedAt === "number" ? Math.max(0, command.now - entry.startedAt) : void 0; entry.lastRunError = "Run interrupted by Gateway restart or loss."; } 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 (hasMainSessionRecoveryClaim(entry) && 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) state = updateRecoveryState(entry, state, { reservation: void 0 }); if (isMainRestartRecoveryAggregateEmptyAndUnowned(entry) || isMainRestartRecoveryTerminalOnly(entry) || hasCompletedMainSessionRecoveryOutcome(entry) && !state?.tombstone && !state?.reservation && !(state && hasCurrentForegroundClaim(state, command.lifecycleGeneration))) Object.assign(entry, buildMainSessionRecoverySettlementPatch({ entry, recordTerminalSource: true })); 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.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" }; updateRecoveryState(entry, state, { executionIdentity: command.executionIdentity.state === "enabled" && state.executionIdentity ? state.executionIdentity : void 0, 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 } }; } 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 }); if (isMainRestartRecoveryAggregateEmptyAndUnowned(entry)) Object.assign(entry, buildMainSessionRecoveryClearPatch(entry)); return { kind: "applied" }; } case "validate_recovery": case "admit_recovery": { const conflict = validateRecoveryAdmission(entry, command); if (conflict) return { kind: "rejected", reason: conflict }; if (command.kind === "validate_recovery") return { kind: "recovery_validated" }; const state = entry.mainRestartRecovery; updateRecoveryState(entry, state, { reservation: void 0, foregroundClaims: void 0 }); entry.abortedLastRun = false; entry.status = void 0; entry.endedAt = void 0; entry.runtimeMs = void 0; entry.lastRunError = void 0; 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": { 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.abortedLastRun === true && entry.lifecycleRunId === void 0 && entry.restartRecoveryDeliveryRunId === void 0 ? { kind: "no_change" } : { kind: "rejected", reason: "stale_reservation" }; entry.status = "interrupted"; entry.lifecycleRunId = void 0; entry.lastRunId = void 0; entry.abortedLastRun = true; entry.startedAt = void 0; entry.endedAt = command.now; entry.lastRunError = "Run interrupted before restart recovery could start."; entry.runtimeMs = void 0; if (entry.restartRecoveryDeliveryRunId === command.runId) entry.restartRecoveryDeliveryRunId = void 0; entry.updatedAt = command.now; return { kind: "applied" }; } case "claim_foreground": { if (entry.sessionId === command.sessionId && isMainRestartRecoveryCandidate(entry, command.sessionKey) && (isMainRestartRecoveryTerminalOnly(entry) || isMainRestartRecoveryAggregateEmptyAndUnowned(entry))) { Object.assign(entry, buildMainSessionRecoveryClearPatch(entry)); return { kind: "applied" }; } if (entry.sessionId !== command.sessionId || !hasMainSessionRecoveryClaim(entry) || 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": case "release_foreground": { const state = entry.mainRestartRecovery; const claims = state?.foregroundClaims; if (!state || !claims || !ownsForegroundClaim(state, command.claim)) return { kind: "no_change" }; if (command.kind === "bind_foreground_run") recordLifecycleFence(entry, { lifecycleGeneration: command.claim.lifecycleGeneration, runId: command.runId }); const foregroundClaims = command.kind === "bind_foreground_run" ? { ...claims, runIdsByClaimId: { ...claims.runIdsByClaimId, [command.claim.claimId]: command.runId } } : removeMainSessionRecoveryForegroundClaim(claims, command.claim.claimId); if (!foregroundClaims && entry.abortedLastRun !== true) Object.assign(entry, buildMainSessionRecoveryClearPatch(entry)); else updateRecoveryState(entry, state, { foregroundClaims }); 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 "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, isMainRestartRecoveryTerminalOnly as i, hasCompletedMainSessionRecoveryOutcome as n, isMainSessionRecoveryReconciliationCandidate as o, inspectMainRestartRecoveryRolloverEligibility as r, transitionMainSessionRecovery as s, getMainSessionRecoveryRetryCount as t };