openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
383 lines (382 loc) • 16.1 kB
JavaScript
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 };