openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
352 lines (351 loc) • 16.5 kB
JavaScript
import "./session-key-BnWWjqNc.js";
import { t as isIncognitoSessionKey } from "./incognito-session-key-BwpD1Lwd.js";
import { t as ErrorCodes } from "./gateway-error-details-w0nAGBBp.js";
import { Br as validateSessionsBranchesSwitchParams, Fi as validateSessionsRewindParams, ci as validateSessionsForkParams, zr as validateSessionsBranchesListParams } from "./src-BiL5aQto.js";
import { d as errorShape } from "./error-codes-Bo8q2D1o.js";
import { t as MEDIA_MAX_BYTES, u as readMediaBuffer } from "./store-Ph57PZ9P.js";
import { r as listRegisteredAgentHarnesses } from "./registry-D1_C7waJ.js";
import { at as listSessionBranches, ct as switchSessionBranch, it as forkSessionAtMessage, st as rewindSessionToMessage } from "./session-accessor-YsytfDtG.js";
import { U as withSessionInitializationSource } from "./session-accessor.sqlite-entry-store-BxYl0nro.js";
import { f as isCompetingSessionWorkAdmissionActive, h as runExclusiveSessionLifecycleMutation } from "./session-lifecycle-admission-CS8v45tk.js";
import { i as ModelSelectionLockedError } from "./model-overrides-Bm7jo7Di.js";
import { u as recordSessionCreated } from "./session-state-events-DNCKmH78.js";
import { r as readSessionUpstreamLink } from "./session-upstream-links-DJQuiStM.js";
import { a as resolveCreatorSandbox, f as resolveOperatorSessionCreation, t as authorizeGatewaySessionCreation } from "./operator-role-policy-wsr1DeJv.js";
import { i as tryResolveSessionCompatibilityOwnerAgentId, n as resolveRequestedSessionAgentId } from "./session-request-agent-CCRSEGCB.js";
import { t as clearSessionQueues } from "./cleanup-DKkilV3a.js";
import { a as resolveSessionWorkerPlacementMutationError } from "./session-placement-lifecycle-7DQHpaT9.js";
import { H as SessionMutationAuthorizationChangedError } from "./session-sharing-B7MI8hNo.js";
import { t as assertValidParams } from "./validation-pzrlzFvo.js";
import { i as resolveVisibleActiveSessionRunState } from "./session-active-runs-CCNuJW7B.js";
import { n as emitSessionsChanged } from "./session-change-event-DzmH4zlz.js";
import { t as asWorkerInferenceControl } from "./inference-control-CDvM08Nt.js";
import { i as loadAccessorSessionEntryForGatewayTarget, u as respondSessionWorkerPlacementMutationError } from "./sessions-shared-D_lV07q2.js";
import { t as buildDashboardSessionKey } from "./session-create-service-CM4MxLMO.js";
import path from "node:path";
//#region src/gateway/server-methods/sessions-rewind.ts
const EXTERNAL_CONVERSATION_ERROR = "Session history changes are unavailable because this session is owned by an external agent harness.";
const EDITOR_MEDIA_REF_LIMIT = 10;
async function resolveEditorMediaAttachments(refs) {
if (!refs) return [];
const seen = /* @__PURE__ */ new Set();
const attachments = [];
for (const ref of refs) {
const id = path.basename(ref.path);
if (seen.has(id)) continue;
seen.add(id);
if (seen.size > EDITOR_MEDIA_REF_LIMIT) break;
try {
const media = await readMediaBuffer(id, "inbound", MEDIA_MAX_BYTES);
attachments.push({
mimeType: ref.contentType,
data: media.buffer.toString("base64")
});
} catch {}
}
return attachments;
}
function resolveUpstreamForkHarness(link) {
const matches = listRegisteredAgentHarnesses().filter((entry) => entry.harness.sessionFork?.upstreamKinds.includes(link.upstreamKind));
return matches.length === 1 ? matches[0]?.harness.sessionFork : void 0;
}
const sessionRewindHandlers = {
"sessions.branches.list": async (options) => {
if (!assertValidParams(options.params, validateSessionsBranchesListParams, "sessions.branches.list", options.respond)) return;
await listBranches(options);
},
"sessions.branches.switch": async (options) => {
if (!assertValidParams(options.params, validateSessionsBranchesSwitchParams, "sessions.branches.switch", options.respond)) return;
await mutateSessionAtMessage(options, "switch");
},
"sessions.rewind": async (options) => {
if (!assertValidParams(options.params, validateSessionsRewindParams, "sessions.rewind", options.respond)) return;
await mutateSessionAtMessage(options, "rewind");
},
"sessions.fork": async (options) => {
if (!assertValidParams(options.params, validateSessionsForkParams, "sessions.fork", options.respond)) return;
await mutateSessionAtMessage(options, "fork");
}
};
async function listBranches(options) {
const { params, respond, context } = options;
const sessionKey = typeof params.sessionKey === "string" ? params.sessionKey.trim() : "";
const cfg = context.getRuntimeConfig();
const requestedAgent = resolveRequestedSessionAgentId(cfg, sessionKey, typeof params.agentId === "string" ? params.agentId : void 0);
if (!requestedAgent.ok) {
respond(false, void 0, requestedAgent.error);
return;
}
const current = loadAccessorSessionEntryForGatewayTarget({
key: sessionKey,
cfg,
agentId: requestedAgent.agentId
});
if (!current.entry?.sessionId) {
respond(true, { branches: [] }, void 0);
return;
}
if (readSessionUpstreamLink(current.canonicalKey, current.target.agentId)) {
respond(true, { branches: [] }, void 0);
return;
}
const result = await listSessionBranches({
agentId: current.target.agentId,
sessionKey: current.canonicalKey,
sessionStoreKey: current.sessionStoreKey,
storePath: current.storePath
});
if (result.status !== "ok") {
respondBranchListError(result, respond);
return;
}
respond(true, { branches: result.branches }, void 0);
}
async function mutateSessionAtMessage(options, action) {
const { params, respond, context, client } = options;
const { sessionMutationCommitGuard, sessionMutationAuthorization } = options;
const commitGuard = () => {
sessionMutationCommitGuard?.();
sessionMutationAuthorization?.assertCurrent();
};
const sessionKey = typeof params.sessionKey === "string" ? params.sessionKey.trim() : "";
const entryId = action === "switch" ? typeof params.leafEntryId === "string" ? params.leafEntryId.trim() : "" : typeof params.entryId === "string" ? params.entryId.trim() : "";
const cfg = context.getRuntimeConfig();
const requestedAgent = resolveRequestedSessionAgentId(cfg, sessionKey, typeof params.agentId === "string" ? params.agentId : void 0);
if (!requestedAgent.ok) {
respond(false, void 0, requestedAgent.error);
return;
}
const initial = loadAccessorSessionEntryForGatewayTarget({
key: sessionKey,
cfg,
agentId: requestedAgent.agentId
});
if (!initial.entry?.sessionId) {
respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, `session not found: ${sessionKey}`));
return;
}
const rejectInitializing = (pending) => {
if (!pending) return false;
respond(false, void 0, errorShape(ErrorCodes.UNAVAILABLE, `Session ${sessionKey} is initializing; retry ${action} later.`));
return true;
};
if (rejectInitializing(initial.entry.initializationPending)) return;
if (action === "fork") {
const creationError = authorizeGatewaySessionCreation({
cfg,
client,
agentId: initial.target.agentId
});
if (creationError) {
respond(false, void 0, creationError);
return;
}
}
const initialSessionId = initial.entry.sessionId;
const initialLifecycleRevision = initial.entry.lifecycleRevision;
if (readSessionUpstreamLink(initial.canonicalKey, initial.target.agentId) && action !== "fork") {
respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, EXTERNAL_CONVERSATION_ERROR));
return;
}
const initialPlacementError = resolveSessionWorkerPlacementMutationError({
action,
context,
key: sessionKey,
sessionId: initial.entry.sessionId
});
if (initialPlacementError) {
respondSessionWorkerPlacementMutationError(initialPlacementError, respond);
return;
}
const lifecycleIdentities = [
sessionKey,
initial.canonicalKey,
initial.sessionStoreKey,
initialSessionId,
initialLifecycleRevision
];
let targetStillCurrent = true;
let blockedByActiveRun = false;
await runExclusiveSessionLifecycleMutation({
scope: initial.storePath,
identities: lifecycleIdentities,
prepare: async () => {
const current = loadAccessorSessionEntryForGatewayTarget({
key: sessionKey,
cfg,
agentId: requestedAgent.agentId
});
targetStillCurrent = current.entry?.sessionId === initialSessionId && current.entry.lifecycleRevision === initialLifecycleRevision;
if (!targetStillCurrent) return;
blockedByActiveRun = isCompetingSessionWorkAdmissionActive(initial.storePath, lifecycleIdentities) || (asWorkerInferenceControl(context.workerEnvironmentService)?.hasInferenceForSession(initialSessionId) ?? false) || resolveVisibleActiveSessionRunState({
context,
requestedKey: sessionKey,
canonicalKey: current.canonicalKey,
sessionId: initialSessionId,
agentId: requestedAgent.agentId,
defaultAgentId: tryResolveSessionCompatibilityOwnerAgentId(cfg, sessionKey)
}).active;
},
run: async () => {
commitGuard();
if (!targetStillCurrent) {
respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, `Session ${sessionKey} changed; retry ${action}.`));
return;
}
if (blockedByActiveRun) {
respond(false, void 0, errorShape(ErrorCodes.UNAVAILABLE, action === "switch" ? "Branch switch is unavailable while the agent is working." : `${action === "fork" ? "Fork" : "Rewind"} is unavailable while the agent is working.`));
return;
}
const current = loadAccessorSessionEntryForGatewayTarget({
key: sessionKey,
cfg,
agentId: requestedAgent.agentId
});
if (current.entry?.sessionId !== initialSessionId || current.entry.lifecycleRevision !== initialLifecycleRevision) {
respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, `Session ${sessionKey} changed; retry ${action}.`));
return;
}
if (rejectInitializing(current.entry.initializationPending)) return;
const upstreamLink = readSessionUpstreamLink(current.canonicalKey, current.target.agentId);
const archived = current.entry.archivedAt !== void 0;
if ((archived || upstreamLink) && action !== "fork") {
const message = archived ? `${action === "switch" ? "Branch switch" : "Rewind"} is unavailable for archived sessions.` : EXTERNAL_CONVERSATION_ERROR;
respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, message));
return;
}
const placementError = resolveSessionWorkerPlacementMutationError({
action,
context,
key: sessionKey,
sessionId: current.entry.sessionId
});
if (placementError) {
respondSessionWorkerPlacementMutationError(placementError, respond);
return;
}
const targetKey = action === "fork" ? buildDashboardSessionKey(current.target.agentId, { incognito: current.entry.incognito === true || isIncognitoSessionKey(current.canonicalKey) }) : current.canonicalKey;
const expectedState = {
sessionId: current.entry.sessionId,
lifecycleRevision: current.entry.lifecycleRevision
};
const upstreamForkHarness = upstreamLink ? resolveUpstreamForkHarness(upstreamLink) : void 0;
if (upstreamLink && !upstreamForkHarness) {
respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, EXTERNAL_CONVERSATION_ERROR));
return;
}
const creation = resolveOperatorSessionCreation(client);
const sandbox = action === "fork" ? resolveCreatorSandbox(cfg, creation) : void 0;
const upstreamFork = upstreamLink && upstreamForkHarness ? await withSessionInitializationSource(() => {
commitGuard();
const source = loadAccessorSessionEntryForGatewayTarget({
key: sessionKey,
cfg,
agentId: requestedAgent.agentId
});
if (source.entry?.sessionId !== initialSessionId || source.entry.lifecycleRevision !== initialLifecycleRevision || source.entry.initializationPending === true) throw new Error(`Session ${sessionKey} changed during fork initialization`);
}, () => upstreamForkHarness.fork({
targetKey,
sandbox,
source: {
agentId: current.target.agentId,
sessionId: initialSessionId,
sessionKey: current.canonicalKey,
storePath: current.storePath,
entryId
},
upstream: {
catalogId: upstreamLink.catalogId,
hostId: upstreamLink.hostId,
kind: upstreamLink.upstreamKind,
threadId: upstreamLink.threadId,
ref: upstreamLink.upstreamRef
}
})) : void 0;
if (upstreamFork?.status === "failed") {
respond(false, void 0, errorShape(upstreamFork.code === "upstream-unavailable" ? ErrorCodes.UNAVAILABLE : ErrorCodes.INVALID_REQUEST, upstreamFork.message, { details: { reason: upstreamFork.code } }));
return;
}
if (upstreamFork?.status === "created") {
respond(true, {
sessionKey: upstreamFork.key,
...upstreamFork.editorText !== void 0 ? { editorText: upstreamFork.editorText } : {}
}, void 0);
emitSessionsChanged(context, {
sessionKey: upstreamFork.key,
agentId: requestedAgent.agentId,
reason: "fork"
});
return;
}
let result;
const mutationParams = {
agentId: current.target.agentId,
commitGuard,
sessionKey: current.canonicalKey,
sessionStoreKey: current.sessionStoreKey,
storePath: current.storePath
};
try {
result = await (action === "fork" ? forkSessionAtMessage({
...mutationParams,
entryId,
targetKey,
creation: {
...creation,
sandbox
}
}, expectedState) : action === "rewind" ? rewindSessionToMessage({
...mutationParams,
entryId
}, expectedState) : switchSessionBranch({
...mutationParams,
leafEntryId: entryId
}, expectedState));
} catch (error) {
if (error instanceof SessionMutationAuthorizationChangedError) throw error;
if (error instanceof ModelSelectionLockedError) {
respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, error.message));
return;
}
respond(false, void 0, errorShape(ErrorCodes.UNAVAILABLE, `Failed to ${action} the local session. Try again.`));
return;
}
if (result.status !== "created") {
respondMessageCutError(result, action, entryId, respond);
return;
}
const editorAttachments = action === "switch" ? [] : [..."editorAttachments" in result ? result.editorAttachments ?? [] : [], ...await resolveEditorMediaAttachments("editorMediaRefs" in result ? result.editorMediaRefs : void 0)];
if (action !== "fork") clearSessionQueues(lifecycleIdentities);
else recordSessionCreated({
sessionKey: result.key,
agentId: current.target.agentId,
entry: result.entry
});
respond(true, action === "fork" ? {
sessionKey: result.key,
..."editorText" in result && result.editorText ? { editorText: result.editorText } : {},
...editorAttachments.length > 0 ? { editorAttachments } : {}
} : action === "rewind" ? {
..."editorText" in result && result.editorText ? { editorText: result.editorText } : {},
...editorAttachments.length > 0 ? { editorAttachments } : {}
} : {}, void 0);
emitSessionsChanged(context, {
sessionKey: action === "fork" ? result.key : current.canonicalKey,
agentId: requestedAgent.agentId,
reason: action === "switch" ? "branch-switch" : action
});
}
});
}
function respondMessageCutError(result, action, entryId, respond) {
const actionLabel = action === "switch" ? "branch switch" : action;
const message = result.status === "conflict" ? `Session changed; retry ${action}.` : result.status === "missing-session" ? "session not found" : result.status === "missing-entry" ? `${action === "switch" ? "branch" : "message"} entry not found: ${entryId}` : result.status === "not-branch-tip" ? `entry is not a branch tip: ${entryId}` : result.status === "already-active" ? `branch is already active: ${entryId}` : result.status === "not-user-message" ? `entry is not a user message: ${entryId}` : result.status === "off-active-path" ? `message entry is not on the active path: ${entryId}` : result.status === "unsupported-storage" ? `session transcript storage does not support ${actionLabel}` : `failed to ${actionLabel} session`;
respond(false, void 0, errorShape(result.status === "failed" ? ErrorCodes.UNAVAILABLE : ErrorCodes.INVALID_REQUEST, message));
}
function respondBranchListError(result, respond) {
const message = result.status === "missing-session" ? "session not found" : result.status === "unsupported-storage" ? "session transcript storage does not support branch listing" : "failed to list session branches";
respond(false, void 0, errorShape(result.status === "failed" ? ErrorCodes.UNAVAILABLE : ErrorCodes.INVALID_REQUEST, message));
}
//#endregion
export { sessionRewindHandlers };