UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

383 lines (382 loc) 16.1 kB
import { n as resolveGlobalMap } from "./global-singleton-Dc_stLtU.js"; import { l as normalizeOptionalString } from "./string-coerce-CIXf7egm.js"; import { t as formatErrorMessage } from "./errors-Db3Ymjlb.js"; import { t as ErrorCodes } from "./gateway-error-details-w0nAGBBp.js"; import { Mi as validateSessionsRecoverParams } from "./src-BiL5aQto.js"; import { d as errorShape } from "./error-codes-Bo8q2D1o.js"; import "./users-CPWrgxrZ.js"; import { l as normalizeSessionDeliveryState } from "./delivery-context.shared-CXmRgetN.js"; import { Ut as inheritSessionSelection, gt as recoverSessionEntryFromRestartTombstone } from "./session-accessor-YsytfDtG.js"; import { u as runQueuedStoreWrite } from "./store-writer-state-C4OG_EQ4.js"; import { h as runExclusiveSessionLifecycleMutation, i as closeSessionWorkAdmissions, m as isSessionWorkAdmissionActive, y as normalizeSessionIdentities } from "./session-lifecycle-admission-CS8v45tk.js"; import { n as buildSessionCreationStamp, r as inheritSessionCreationPolicy } from "./session-entry-provenance-jzrCUpdQ.js"; import { r as mergeSessionEntry } from "./types-BHV0IaPK.js"; import { u as recordSessionCreated } from "./session-state-events-DNCKmH78.js"; import { s as createAgentRunDirectAbortError } from "./run-termination-jabvTOp5.js"; import { u as isEmbeddedAgentRunActive } from "./runs-Cb42qain.js"; import { n as buildMainSessionRecoveryClearPatch } from "./main-session-recovery-clear-H7IP1700.js"; import { n as inspectMainRestartRecoveryRolloverEligibility } from "./main-session-recovery-state-DD1DvBOq.js"; import { a as resolveCreatorSandbox, f as resolveOperatorSessionCreation, t as authorizeGatewaySessionCreation } from "./operator-role-policy-wsr1DeJv.js"; import { d as resolveGatewaySessionStoreTarget, i as loadGatewaySessionEntryReadOnly } from "./session-utils-store-CInT2loy.js"; import "./session-utils-Cai0_C6U.js"; import { t as formatSystemTurnPrompt } from "./system-turn-prompt-CqPm0DzY.js"; import { i as prepareSessionWorkerPlacementStop, n as prepareSessionWorkerPlacementMutationCheck } from "./session-placement-lifecycle-7DQHpaT9.js"; import "./embedded-agent-D9hkv0XX.js"; import { i as handleTrustedInternalChatSend } from "./chat-send-handler-Dpw17Gmy.js"; import { t as assertValidParams } from "./validation-pzrlzFvo.js"; import { t as resolvePluginSessionOwnershipError } from "./session-plugin-ownership-421Ior05.js"; import { n as emitSessionsChanged, t as emitSessionArchived } from "./session-change-event-DzmH4zlz.js"; import { n as createAgentRuntimeAuthorityGuard } from "./agent-runtime-authority-BkcmfdKo.js"; import { t as resolveSessionWorkerPlacementContext } from "./session-worker-placement-context-BPME1dHp.js"; import { t as buildDashboardSessionKey } from "./session-create-service-CM4MxLMO.js"; import { randomUUID } from "node:crypto"; //#region src/gateway/session-recovery-entry.ts /** Builds the fresh runtime identity paired with a recovered transcript. */ function buildRestartRecoverySuccessorEntry(params) { const source = params.source; const entry = mergeSessionEntry(void 0, { ...inheritSessionSelection(source), ...buildSessionCreationStamp({ via: "operator", ...params.creation }), delivery: normalizeSessionDeliveryState(), sessionId: params.sessionId, previousSessionId: source.sessionId, spawnDepth: 0, ...source.agentHarnessId ? { agentHarnessId: source.agentHarnessId } : {}, ...source.modelSelectionLocked === true ? { modelSelectionLocked: true } : {}, ...source.pluginOwnerId ? { pluginOwnerId: source.pluginOwnerId } : {}, ...source.visibility ? { visibility: source.visibility } : {}, ...source.spawnedCwd ? { spawnedCwd: source.spawnedCwd } : {}, ...source.execHost ? { execHost: source.execHost } : {}, ...source.execNode ? { execNode: source.execNode } : {}, ...source.execCwd ? { execCwd: source.execCwd } : {} }); return { ...entry, ...buildMainSessionRecoveryClearPatch(entry), sessionId: params.sessionId }; } //#endregion //#region src/gateway/session-recovery-service.ts const recoveryQueues = resolveGlobalMap(Symbol.for("openclaw.sessionRecoveryQueues")); function recoveryConflictError(reason) { const unavailable = reason === "successor-missing" || reason === "transcript-missing"; return errorShape(unavailable ? ErrorCodes.UNAVAILABLE : ErrorCodes.INVALID_REQUEST, unavailable ? "Session recovery state is incomplete." : "Session changed before recovery; refresh and retry.", { details: { reason } }); } /** Owns explicit restart recovery from authorization through continuation launch. */ async function recoverGatewaySession(params) { const sourceTarget = resolveGatewaySessionStoreTarget({ cfg: params.cfg, key: params.key, ...params.agentId ? { agentId: params.agentId } : {} }); const readSource = () => loadGatewaySessionEntryReadOnly(sourceTarget.canonicalKey, { agentId: sourceTarget.agentId }).entry; const initialSource = readSource(); if (!initialSource?.sessionId) return { ok: false, error: errorShape(ErrorCodes.INVALID_REQUEST, "Session recovery source was not found.") }; const initialEligibility = inspectMainRestartRecoveryRolloverEligibility(initialSource); if (!initialEligibility.eligible && initialEligibility.reason !== "already_recovered") return { ok: false, error: errorShape(ErrorCodes.INVALID_REQUEST, "Session recovery requires a restart-tombstoned session.") }; const recovery = initialSource.mainRestartRecovery; if (!recovery?.tombstone) return { ok: false, error: errorShape(ErrorCodes.INVALID_REQUEST, "Session is not recoverable.") }; const generatedSuccessorKey = buildDashboardSessionKey(sourceTarget.agentId); const successorTarget = resolveGatewaySessionStoreTarget({ cfg: params.cfg, key: generatedSuccessorKey, agentId: sourceTarget.agentId }); const successorSessionId = randomUUID(); const resolveCurrentSource = () => { params.commitGuard?.(); const currentSource = readSource(); const currentOwnershipError = resolvePluginSessionOwnershipError({ action: "recover", entry: currentSource, key: sourceTarget.canonicalKey, pluginOwnerId: params.authorizedPluginId }); if (currentOwnershipError) return { ok: false, error: currentOwnershipError }; if (!currentSource?.sessionId || currentSource.sessionId !== initialSource.sessionId || currentSource.lifecycleRevision !== initialSource.lifecycleRevision || currentSource.mainRestartRecovery?.cycleId !== recovery.cycleId || !currentSource.mainRestartRecovery.tombstone?.recoveredSessionKey && currentSource.mainRestartRecovery.revision !== recovery.revision) return { ok: false, error: recoveryConflictError("source-changed") }; if (!currentSource.mainRestartRecovery?.tombstone?.recoveredSessionKey) { const creationError = authorizeGatewaySessionCreation({ cfg: params.cfg, agentId: sourceTarget.agentId, ...params.operatorRoleActor ? { actor: params.operatorRoleActor } : { profileId: params.requestingOperatorProfileId } }); if (creationError) return { ok: false, error: creationError }; } if (isEmbeddedAgentRunActive(currentSource.sessionId) || isSessionWorkAdmissionActive(sourceTarget.storePath, [sourceTarget.canonicalKey, currentSource.sessionId])) return { ok: false, error: errorShape(ErrorCodes.INVALID_REQUEST, "Session recovery is unavailable while the source still has active work.") }; return { ok: true, source: currentSource }; }; const assertCurrent = () => { const current = resolveCurrentSource(); if (!current.ok) throw new Error(current.error.message); }; const sourceIdentities = [ ...sourceTarget.storeKeys, sourceTarget.canonicalKey, initialSource.sessionId ]; const stopFailure = (error) => errorShape(ErrorCodes.UNAVAILABLE, `Session recovery cannot safely stop/reclaim its cloud worker: ${formatErrorMessage(error)} Stop cloud worker or call sessions.reclaim, then retry recovery.`, { retryable: true }); const commitRecovery = async () => { let release = () => {}; try { const prepared = await runExclusiveSessionLifecycleMutation({ scope: sourceTarget.storePath, identities: sourceIdentities, run: async () => { const current = resolveCurrentSource(); if (!current.ok) return current; let stop; try { if (!current.source.mainRestartRecovery?.tombstone?.recoveredSessionKey) stop = prepareSessionWorkerPlacementStop({ action: "recover", agentId: sourceTarget.agentId, authorize: assertCurrent, context: params.workerPlacementContext, sessionId: initialSource.sessionId, sessionKey: sourceTarget.canonicalKey }); } catch (error) { return { ok: false, error: stopFailure(error) }; } release = closeSessionWorkAdmissions({ scope: sourceTarget.storePath, identities: sourceIdentities, reason: createAgentRunDirectAbortError() }); return { ...current, stop }; } }); if (!prepared.ok) return prepared; let assertPlacementCurrent; if (prepared.stop) try { await prepared.stop(); assertPlacementCurrent = prepareSessionWorkerPlacementMutationCheck({ context: params.workerPlacementContext, sessionId: initialSource.sessionId }); } catch (error) { const current = resolveCurrentSource(); return current.ok ? { ok: false, error: stopFailure(error) } : current; } return await runExclusiveSessionLifecycleMutation({ targets: [{ scope: sourceTarget.storePath, identities: sourceIdentities }, { scope: successorTarget.storePath, identities: [successorTarget.canonicalKey, successorSessionId] }], prepare: async () => release(), run: async () => { const settled = resolveCurrentSource(); if (!settled.ok) return settled; const currentSource = settled.source; const commitGuard = () => { assertCurrent(); assertPlacementCurrent?.(); }; commitGuard(); const successorEntry = buildRestartRecoverySuccessorEntry({ sessionId: successorSessionId, source: currentSource, creation: params.actor ? { actor: params.actor, sandbox: params.actor.id === "gateway-owner" ? currentSource.sandbox : resolveCreatorSandbox(params.cfg, params) } : inheritSessionCreationPolicy(currentSource) }); const result = await recoverSessionEntryFromRestartTombstone({ agentId: sourceTarget.agentId, ...params.actor ? { archivedBy: params.actor } : {}, commitGuard, expected: { cycleId: recovery.cycleId, lifecycleRevision: initialSource.lifecycleRevision, revision: recovery.revision, sessionId: initialSource.sessionId, ...normalizeOptionalString(initialSource.pluginOwnerId) ? { pluginOwnerId: initialSource.pluginOwnerId } : {} }, sourceTarget, storePath: sourceTarget.storePath, successorEntry, successorTarget }); if (result.status === "conflict") return { ok: false, error: recoveryConflictError(result.reason) }; return { ok: true, created: result.status === "created", successorEntry: result.successorEntry, successorKey: result.successorKey }; } }); } finally { release(); } }; const committed = await runQueuedStoreWrite({ queues: recoveryQueues, storePath: normalizeSessionIdentities(sourceTarget.storePath, [sourceTarget.canonicalKey])[0], label: "recoverGatewaySession", fn: commitRecovery }); if (!committed.ok) return committed; if (committed.created) recordSessionCreated({ sessionKey: committed.successorKey, entry: committed.successorEntry, agentId: sourceTarget.agentId }); const continuation = await params.launchContinuation({ agentId: sourceTarget.agentId, idempotencyKey: `restart-recovery-rollover:${committed.successorEntry.sessionId}`, sessionId: committed.successorEntry.sessionId, sessionKey: committed.successorKey }); return { ok: true, agentId: sourceTarget.agentId, created: committed.created, sourceKey: sourceTarget.canonicalKey, successorEntry: committed.successorEntry, successorKey: committed.successorKey, continuation }; } //#endregion //#region src/gateway/server-methods/session-recovery-continuation.ts const RECOVERY_CONTINUATION_TEXT = "Continue from the recovered transcript and finish the interrupted work."; /** Starts the fixed recovery continuation as trusted system input. */ async function launchSessionRecoveryContinuation(params) { let outcome; try { await handleTrustedInternalChatSend({ req: params.req, params: { sessionKey: params.sessionKey, agentId: params.agentId, sessionId: params.sessionId, message: formatSystemTurnPrompt(RECOVERY_CONTINUATION_TEXT), idempotencyKey: params.idempotencyKey, deliver: false, suppressCommandInterpretation: true, systemInputProvenance: { kind: "internal_system", sourceSessionKey: params.sessionKey, sourceTool: "sessions.recover" } }, respond: (ok, payload, error) => { const response = payload; const runId = ok && response && typeof response.runId === "string" ? response.runId.trim() : ""; outcome = ok && runId ? { status: "started", runId } : { status: "rejected", error: error ?? errorShape(ErrorCodes.UNAVAILABLE, "Continuation was not started.") }; }, context: params.context, client: params.client, isWebchatConnect: () => false }, params.commitGuard ? async () => { params.commitGuard?.(); return true; } : void 0); } catch (error) { outcome = { status: "rejected", error: errorShape(ErrorCodes.INVALID_REQUEST, error instanceof Error ? error.message : "Continuation authority check failed.") }; } return outcome ?? { status: "rejected", error: errorShape(ErrorCodes.UNAVAILABLE, "Continuation returned no outcome.") }; } //#endregion //#region src/gateway/server-methods/sessions-recover.ts const sessionRecoverHandlers = { "sessions.recover": async ({ req, params, respond, client, context, sessionMutationAuthorization }) => { if (!assertValidParams(params, validateSessionsRecoverParams, "sessions.recover", respond)) return; const authority = createAgentRuntimeAuthorityGuard(client, context, respond); const commitGuard = authority.commitGuard || sessionMutationAuthorization ? () => { authority.commitGuard?.(); sessionMutationAuthorization?.assertCurrent(); } : void 0; const creation = resolveOperatorSessionCreation(client); const recovered = await recoverGatewaySession({ cfg: context.getRuntimeConfig(), key: params.key, ...params.agentId ? { agentId: params.agentId } : {}, ...creation.actor ? { actor: creation.actor } : {}, ...client?.authenticatedUserProfile ? { requestingOperatorProfileId: client.authenticatedUserProfile.profileId } : {}, ...client?.internal?.operatorRoleActor ? { operatorRoleActor: client.internal.operatorRoleActor } : {}, authorizedPluginId: client?.internal?.pluginRuntimeOwnerId, ...commitGuard ? { commitGuard } : {}, workerPlacementContext: resolveSessionWorkerPlacementContext(context), launchContinuation: async (continuation) => await launchSessionRecoveryContinuation({ ...continuation, client, ...commitGuard ? { commitGuard } : {}, context, req }) }).catch((error) => authority.handleClosedError(error)); if (!recovered) return; if (!recovered.ok) { respond(false, void 0, recovered.error); return; } emitSessionArchived(context, recovered.sourceKey, recovered.sourceKey === "global" ? recovered.agentId : void 0); emitSessionsChanged(context, { sessionKey: recovered.successorKey, reason: recovered.created ? "create" : "recovery", ...recovered.successorKey === "global" ? { agentId: recovered.agentId } : {} }); respond(true, { ok: true, key: recovered.successorKey, sessionId: recovered.successorEntry.sessionId, continuation: recovered.continuation }, void 0); } }; //#endregion export { sessionRecoverHandlers };