UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

1,221 lines (1,220 loc) 49.2 kB
import { r as __exportAll } from "./rolldown-runtime-Dr7-SnC6.js"; import { l as normalizeOptionalString } from "./string-coerce-CIXf7egm.js"; import { n as ok, t as err } from "./result-BQGgYouL.js"; import { t as formatErrorMessage } from "./errors-Db3Ymjlb.js"; import { _ as resolveSessionAgentId } from "./agent-scope-DbtJyKUL.js"; import { t as isPluginJsonValue } from "./host-hook-json-BdcyDerH.js"; import { t as ADMIN_SCOPE } from "./operator-scopes-Dw7Gu2cA.js"; import { t as ErrorCodes } from "./gateway-error-details-w0nAGBBp.js"; import { Di as validateSessionsPatchParams, Ei as validateSessionsPatchManyParams, Ni as validateSessionsResetParams, Oi as validateSessionsPluginPatchParams, Rr as validateSessionsAssignOwnerParams } from "./src-BiL5aQto.js"; import { d as errorShape, f as missingScopeErrorShape } from "./error-codes-Bo8q2D1o.js"; import { Ht as SessionLabelOwnerIndex, Jt as applySessionEntryCanonicalReplacements } from "./session-accessor-YsytfDtG.js"; import { A as isInternalSessionEffectsKey } from "./session-accessor.sqlite-entry-CWk3jL7s.js"; import { h as runExclusiveSessionLifecycleMutation } from "./session-lifecycle-admission-CS8v45tk.js"; import { a as resolveStoredSessionKeyForAgentStore } from "./session-store-key-8xEjWSNi.js"; import { m as resolveMissingAgentHarnessSessionError } from "./agent-harness-session-key-BOz3yx0-.js"; import { t as assignSessionOwner } from "./session-accessor.sqlite-owner-DysLwyNh.js"; import { t as SESSION_LIFECYCLE_CHANGED_ERROR_REASON } from "./lifecycle-BaroCBMc.js"; import { i as patchPluginSessionExtension } from "./host-hook-state-Bk6oeu0I.js"; import { a as resolveCreatorSandbox, f as resolveOperatorSessionCreation, h as gatewayClientSessionCreator, t as authorizeGatewaySessionCreation } from "./operator-role-policy-wsr1DeJv.js"; import { n as resolveSessionModelRef } from "./session-model-ref-CPZiclLt.js"; import { n as projectSessionPatchResult } from "./session-utils-model-BYAclq2V.js"; import { S as disableCronJobsBoundToSessions, h as projectSessionActor, m as projectAssignableSessionOwner } from "./session-utils-list-B0k8KJn5.js"; import { i as tryResolveSessionCompatibilityOwnerAgentId, n as resolveRequestedSessionAgentId } from "./session-request-agent-CCRSEGCB.js"; import { f as resolveGatewaySessionStoreTargetWithStore, o as resolveCanonicalGatewaySessionStoreKey, s as resolveCanonicalSessionEntryFromStoreKeys } from "./session-utils-store-CInT2loy.js"; import "./session-utils-Cai0_C6U.js"; import { t as triggerSessionPatchHook } from "./session-patch-hooks-Bav35gWY.js"; import { t as parseSessionLabel } from "./session-label-DSD-L6TD.js"; import { n as prepareSessionWorkerPlacementMutationCheck } from "./session-placement-lifecycle-7DQHpaT9.js"; import { t as persistStickyModelSelectionBestEffort } from "./sticky-model-selection-BlEz_Ddb.js"; import { H as SessionMutationAuthorizationChangedError, k as authorizeIncognitoSessionTarget, r as createSessionListEntryFilter, y as ensureSessionGroupRegistered, z as resolveSessionSharingTarget } from "./session-sharing-B7MI8hNo.js"; import { t as ModelAccountConnectAuthorityError } from "./model-account-connect-BqlZBB8r.js"; import { n as preparePersonalModelSelection } from "./users-model-account-access-B9iXjlLs.js"; import { t as assertValidParams } from "./validation-pzrlzFvo.js"; import { t as resolveGatewayModelSelectionPolicy } from "./session-model-selection-policy-BiBt__vw.js"; import { t as resolvePluginSessionOwnershipError } from "./session-plugin-ownership-421Ior05.js"; import { n as emitSessionsChanged } from "./session-change-event-DzmH4zlz.js"; import { n as sessionToolOverridesEqual } from "./session-tool-overrides-BnsUT7mq.js"; import { d as sessionLog, l as resolveSessionWorkerPlacementPatchError, n as isAgentMainSessionKey, o as loadSessionsRuntimeModule, s as requireSessionKey } from "./sessions-shared-D_lV07q2.js"; import { n as projectSessionsPatchEntry, t as prepareSessionsPatchEntry } from "./sessions-patch-CCbzG5TT.js"; import { i as synchronizeSessionWorktreeArchive, t as SessionWorktreeLifecycleError } from "./session-worktree-lifecycle-_3Jyj3pK.js"; import { t as prepareSessionLifecycleDrain } from "./sessions-lifecycle-drain-B8GdcZDj.js"; //#region src/gateway/server-methods/session-unread-ack.ts var session_unread_ack_exports = /* @__PURE__ */ __exportAll({ resolveSessionUnreadAck: () => resolveSessionUnreadAck$1, validateSessionUnreadAck: () => validateSessionUnreadAck$1 }); const CONDITIONAL_UNREAD_ACK_ALLOWED_KEYS = /* @__PURE__ */ new Set([ "agentId", "expectedLifecycleRevision", "expectedMarkedUnreadAt", "expectedSessionId", "key", "unread" ]); function hasOtherMutation(patch) { return Object.entries(patch).some(([key, value]) => value !== void 0 && !CONDITIONAL_UNREAD_ACK_ALLOWED_KEYS.has(key)); } function validateSessionUnreadAck$1(patch, target) { if (target.expectedMarkedUnreadAt === void 0) return; if (patch.unread === false && !hasOtherMutation(patch)) return; return "expectedMarkedUnreadAt requires unread=false as the only mutation."; } function resolveSessionUnreadAck$1(entry, patch) { const { expectedMarkedUnreadAt } = patch; if (patch.unread !== false || hasOtherMutation(patch) || expectedMarkedUnreadAt === void 0) return { kind: "apply" }; if (!entry) return { kind: "missing" }; return (entry.markedUnreadAt ?? null) === expectedMarkedUnreadAt ? { kind: "apply" } : { kind: "stale", entry }; } //#endregion //#region src/gateway/server-methods/sessions-patch-errors.ts function invalidSessionPatchOutcome(message) { return { ok: false, error: errorShape(ErrorCodes.INVALID_REQUEST, message) }; } function unexpectedPatchError(key, error) { if (error instanceof ModelAccountConnectAuthorityError) return errorShape(ErrorCodes.FORBIDDEN, error.message); if (error instanceof SessionMutationAuthorizationChangedError) return error.error; if (error instanceof SessionWorktreeLifecycleError) return error.reason === "session-changed" ? sessionChangedError(key) : errorShape(ErrorCodes.UNAVAILABLE, error.message, { retryable: true }); sessionLog.warn(`sessions.patch: target failed for ${key}: ${formatErrorMessage(error)}`); return errorShape(ErrorCodes.UNAVAILABLE, "Session patch failed unexpectedly. Retry the request.", { retryable: true }); } function sessionChangedError(key) { return errorShape(ErrorCodes.INVALID_REQUEST, `Session ${key} changed before patch. Retry.`, { details: { reason: SESSION_LIFECYCLE_CHANGED_ERROR_REASON } }); } function createCommitGuard(key, assertCurrent) { return () => { try { assertCurrent?.(); return; } catch (error) { return error instanceof SessionMutationAuthorizationChangedError ? error.error : unexpectedPatchError(key, error); } }; } //#endregion //#region src/gateway/server-methods/sessions-patch-archive.ts function releaseSessionPatchArchive(preparation) { try { preparation?.drain.release(); } catch (error) { sessionLog.warn(`sessions.patch: archive drain release failed for ${preparation?.canonicalKey}: ${formatErrorMessage(error)}`); } } function archiveUnavailableError(key, message) { return errorShape(ErrorCodes.UNAVAILABLE, message === "active" ? `Session ${key} is still active; retry the archive.` : `Session ${key} did not finish stopping; retry the archive.`, { retryable: true }); } function protectedArchiveError(cfg, canonicalKey) { if (canonicalKey === "unknown") return errorShape(ErrorCodes.INVALID_REQUEST, "Cannot archive the unknown session sentinel."); if (canonicalKey === "global" || isAgentMainSessionKey(cfg, canonicalKey)) return errorShape(ErrorCodes.INVALID_REQUEST, "Cannot archive an agent's main session."); } function archiveTargetChanged(params) { const { baselineEntry, currentEntry, patch } = params; const expectedSessionChanged = patch.expectedSessionId !== void 0 && currentEntry?.sessionId !== patch.expectedSessionId || patch.expectedLifecycleRevision !== void 0 && currentEntry?.lifecycleRevision !== patch.expectedLifecycleRevision; const generationChanged = baselineEntry !== void 0 && currentEntry !== void 0 && (currentEntry.sessionId !== baselineEntry.sessionId || currentEntry.lifecycleRevision !== baselineEntry.lifecycleRevision); return expectedSessionChanged || baselineEntry !== void 0 && currentEntry === void 0 || baselineEntry === void 0 && currentEntry !== void 0 || generationChanged; } async function prepareSessionPatchArchive(params) { const { cfg, target } = params; const resolveCurrent = () => { const freshResolved = resolveGatewaySessionStoreTargetWithStore({ cfg, key: target.key, ...target.requestedAgentId ? { agentId: target.requestedAgentId } : {}, exactRead: true }); if (freshResolved.storePath !== target.storePath) return err(sessionChangedError(target.key)); const fresh = resolveCanonicalGatewaySessionStoreKey({ cfg, key: target.key, store: freshResolved.store, agentId: target.requestedAgentId }); const freshCanonicalKey = fresh.target.canonicalKey ?? target.key; const ownershipError = resolvePluginSessionOwnershipError({ action: "patch", entry: fresh.entry, key: freshCanonicalKey, pluginOwnerId: params.pluginOwnerId }); if (ownershipError) return err(ownershipError); if (freshCanonicalKey !== target.canonicalKey || archiveTargetChanged({ currentEntry: fresh.entry, baselineEntry: target.initialEntry, patch: target.fullPatch })) return err(sessionChangedError(target.key)); const missingHarnessSessionError = resolveMissingAgentHarnessSessionError(freshCanonicalKey, fresh.entry); if (missingHarnessSessionError) return err(errorShape(ErrorCodes.INVALID_REQUEST, missingHarnessSessionError)); const protectedError = protectedArchiveError(cfg, freshCanonicalKey); if (protectedError) return err(protectedError); const placementError = resolveSessionWorkerPlacementPatchError({ agentId: freshResolved.agentId, cfg, context: params.context, entry: fresh.entry, key: target.key, patch: target.fullPatch, sessionKey: freshCanonicalKey, validateModelRuntime: false }); if (placementError) return err(errorShape(ErrorCodes.INVALID_REQUEST, placementError)); return ok({ freshResolved, fresh, freshCanonicalKey }); }; const resolved = resolveCurrent(); if (!resolved.ok) return resolved; const { freshResolved, fresh, freshCanonicalKey } = resolved.value; const assertCurrent = () => { params.personalModelSelection?.assertCurrent(); const authorizationError = params.commitGuard(); if (authorizationError) throw new SessionMutationAuthorizationChangedError(authorizationError); const current = resolveCurrent(); if (!current.ok) throw new SessionMutationAuthorizationChangedError(current.error); }; const freshCandidateKeys = new Set(fresh.target.storeKeys); const preview = await projectSessionsPatchEntry({ cfg, existingEntry: fresh.entry, isLabelInUse: (label) => Object.entries(freshResolved.store).some(([sessionKey, entry]) => !freshCandidateKeys.has(sessionKey) && entry.label === label), storeKey: fresh.primaryKey, agentId: target.requestedAgentId, patch: target.fullPatch, archivedBy: target.archiveActor, loadGatewayModelCatalog: params.loadGatewayModelCatalog, personalModelSelection: params.personalModelSelection }); if (!preview.ok) return err(preview.error); const previewPlacementError = resolveSessionWorkerPlacementPatchError({ agentId: freshResolved.agentId, cfg, context: params.context, entry: preview.entry, key: target.key, patch: target.fullPatch, sessionKey: freshCanonicalKey, validateModelRuntime: true }); if (previewPlacementError) return err(errorShape(ErrorCodes.INVALID_REQUEST, previewPlacementError)); try { const drain = await prepareSessionLifecycleDrain({ action: "archive", authorize: assertCurrent, context: params.context, storePath: target.storePath, sessionKeys: Array.from(/* @__PURE__ */ new Set([ target.key, target.canonicalKey, ...target.initialStoreKeys, freshCanonicalKey, ...fresh.target.storeKeys ])), sessionId: fresh.entry?.sessionId, sessionKey: freshCanonicalKey, agentId: freshResolved.agentId, defaultAgentId: tryResolveSessionCompatibilityOwnerAgentId(cfg, freshCanonicalKey), lifecycleIdentities: target.lifecycleIdentities.filter((identity) => Boolean(identity)) }); return ok({ canonicalKey: freshCanonicalKey, drain, ...fresh.entry ? { entry: fresh.entry } : {} }); } catch (error) { if (error instanceof SessionMutationAuthorizationChangedError || error instanceof ModelAccountConnectAuthorityError) return err(unexpectedPatchError(target.key, error)); sessionLog.warn(`sessions.patch: archive drain failed for ${target.canonicalKey}: ${formatErrorMessage(error)}`); return err(archiveUnavailableError(target.key, "stopping")); } } function validateSessionPatchArchiveProjection(params) { if (params.preparation.drain.hasAuthoritativeWork()) return archiveUnavailableError(params.key, "active"); if (params.primaryKey !== params.preparation.canonicalKey || archiveTargetChanged({ currentEntry: params.existingEntry, baselineEntry: params.preparation.entry, patch: params.fullPatch })) return sessionChangedError(params.key); return protectedArchiveError(params.cfg, params.primaryKey) ?? resolvePluginSessionOwnershipError({ action: "patch", entry: params.existingEntry, key: params.primaryKey, pluginOwnerId: params.pluginOwnerId }); } /** Restore before opening admission; remove only after archive metadata is durable. */ async function prepareSessionPatchWorktreeTransition(params) { const assertPlacementCurrent = prepareSessionWorkerPlacementMutationCheck({ context: params.context, sessionId: params.entry.sessionId }); const commitGuard = () => { const authorizationError = params.authorize(); if (authorizationError) throw new SessionMutationAuthorizationChangedError(authorizationError); assertPlacementCurrent(); if (params.preparation?.drain.hasAuthoritativeWork()) throw new SessionWorktreeLifecycleError("Session worktree is still active; retry the archive after work settles.", "busy"); }; const synchronize = (entry) => synchronizeSessionWorktreeArchive({ archived: params.archived, entry, scope: params.scope, commitGuard }); if (!params.archived) await synchronize(params.entry); return { assertCommitAllowed: commitGuard, afterCommit: params.archived ? async (entry) => { try { await synchronize(entry); return; } catch (error) { const cleanupError = unexpectedPatchError(params.scope.sessionKey, error); return errorShape(ErrorCodes.UNAVAILABLE, `Session archived, but worktree cleanup did not finish. ${cleanupError.message} Retry archive after resolving the cleanup condition; garbage collection will also retry.`, { retryable: false }); } } : void 0 }; } //#endregion //#region src/gateway/server-methods/sessions-patch-catalog-preparation.ts function createSessionPatchCatalogPreparation(loadCatalog) { const preparations = /* @__PURE__ */ new Map(); const prepare = (agentId) => { let promise = preparations.get(agentId); if (!promise) { promise = (async () => { try { const catalog = await loadCatalog(agentId); return ok(Array.isArray(catalog) ? catalog : []); } catch (error) { return err(error); } })(); preparations.set(agentId, promise); } return promise; }; const load = async (agentId) => { const catalog = await prepare(agentId); if (!catalog.ok) throw catalog.error; return catalog.value; }; return { prepare, load, available: async (agentId) => { const catalog = await preparations.get(agentId); return catalog?.ok ? catalog.value : void 0; }, project: async (params) => { if (params.mode === "ordered") return { kind: "complete", result: await projectSessionsPatchEntry({ ...params.projection, loadGatewayModelCatalog: () => load(params.agentId) }) }; const projection = prepareSessionsPatchEntry(params.projection); if (projection.kind === "complete") return projection; if (!params.catalog) return { kind: "model-catalog" }; if (!params.catalog.ok) throw params.catalog.error; return { kind: "complete", result: projection.finish(params.catalog.value) }; } }; } //#endregion //#region src/gateway/server-methods/sessions-patch-model-selection.ts function persistSessionPatchModelSelection(params) { if (typeof params.patch.model !== "string") return; const agentId = resolveSessionAgentId({ config: params.cfg, sessionKey: params.sessionKey, agentId: params.targetAgentId }); const policy = resolveGatewayModelSelectionPolicy({ agentId, callerScopes: params.callerScopes, cfg: params.cfg }); if (policy.target === "session" || policy.scope === "effective" && (params.entry.modelOverrideSource !== "user" || !params.entry.providerOverride || !params.entry.modelOverride)) return; const resolved = resolveSessionModelRef(params.cfg, params.entry, agentId); persistStickyModelSelectionBestEffort({ agentId, model: `${resolved.provider}/${resolved.model}`, target: policy.target === "agent" ? "agent" : "defaults" }); } //#endregion //#region src/gateway/server-methods/sessions-patch-effects.ts /** Publish committed patch effects even when active-runtime application later reports an error. */ async function publishSessionPatchEffects(params) { const archivedSessionKeys = /* @__PURE__ */ new Set(); for (const { target, entry } of params.targets) { triggerSessionPatchHook({ cfg: params.cfg, sessionEntry: entry, sessionKey: target.canonicalKey, patch: target.fullPatch }); persistSessionPatchModelSelection({ cfg: params.cfg, callerScopes: params.callerScopes, entry, patch: target.fullPatch, sessionKey: target.canonicalKey, targetAgentId: target.targetAgentId }); emitSessionsChanged(params.context, { sessionKey: target.canonicalKey, ...target.requestedAgentId ? { agentId: target.requestedAgentId } : {}, reason: "patch" }); if (target.fullPatch.archived === true) archivedSessionKeys.add(target.canonicalKey); } const category = params.category; if (params.targets.length > 0 && typeof category === "string" && category.trim()) { if (ensureSessionGroupRegistered(category)) emitSessionsChanged(params.context, { reason: "groups" }); } if (params.callerCanManageCron && archivedSessionKeys.size > 0) try { const disabledBySession = await disableCronJobsBoundToSessions({ cron: params.context.cron, cfg: params.cfg, sessionKeys: [...archivedSessionKeys] }); for (const [sessionKey, disabledJobIds] of disabledBySession) if (disabledJobIds.length > 0) sessionLog.info(`sessions.patch: disabled cron jobs bound to archived session ${sessionKey}: ${disabledJobIds.join(", ")}`); } catch (error) { sessionLog.warn(`sessions.patch: failed to disable cron jobs for archived sessions: ${formatErrorMessage(error)}`); } } //#endregion //#region src/gateway/server-methods/sessions-patch-expectations.ts function resolveSessionPatchExpectationError(patch) { if (patch.expectedPermissionMode !== void 0 && patch.permissionMode === void 0) return "expectedPermissionMode requires a permissionMode replacement."; if (patch.expectedToolOverrides !== void 0 && patch.toolOverrides === void 0) return "expectedToolOverrides requires a toolOverrides replacement."; } function sessionPatchExpectationsChanged(entry, patch) { return patch.expectedPermissionMode !== void 0 && (entry?.permissionMode ?? null) !== patch.expectedPermissionMode || patch.expectedToolOverrides !== void 0 && !sessionToolOverridesEqual(entry?.toolOverrides, patch.expectedToolOverrides); } function sessionPatchTargetIdentity(patch) { return { key: patch.key, ...patch.agentId ? { agentId: patch.agentId } : {}, ...patch.expectedSessionId !== void 0 ? { expectedSessionId: patch.expectedSessionId } : {}, ...patch.expectedLifecycleRevision !== void 0 ? { expectedLifecycleRevision: patch.expectedLifecycleRevision } : {}, ...patch.expectedPermissionMode !== void 0 ? { expectedPermissionMode: patch.expectedPermissionMode } : {}, ...patch.expectedToolOverrides !== void 0 ? { expectedToolOverrides: patch.expectedToolOverrides } : {}, expectedMarkedUnreadAt: patch.expectedMarkedUnreadAt }; } //#endregion //#region src/gateway/server-methods/sessions-patch-engine.ts const { resolveSessionUnreadAck, validateSessionUnreadAck } = session_unread_ack_exports; async function executeSessionPatchMutations(params) { const { client } = params; let personalModelSelection; try { personalModelSelection = preparePersonalModelSelection(params, params.patch.model); } catch (error) { return { ok: false, error: unexpectedPatchError(params.targets[0]?.key ?? "", error) }; } const cfg = params.context.getRuntimeConfig(); const operatorCreation = resolveOperatorSessionCreation(client); const sandbox = resolveCreatorSandbox(cfg, operatorCreation); const creation = { ...operatorCreation, ...sandbox ? { sandbox } : {} }; const archiveActor = gatewayClientSessionCreator(client); const callerScopes = Array.isArray(client?.connect?.scopes) ? client.connect.scopes : []; const callerCanManageCron = client === null || callerScopes.includes("operator.admin"); const pluginOwnerId = client?.internal?.pluginRuntimeOwnerId; const permissionRuntime = "permissionMode" in params.patch ? await import("./sessions-patch-permissions.runtime.js") : void 0; const targetDiscoveryCache = /* @__PURE__ */ new Map(); const preflightTargets = params.targets.map((input) => { const key = input.key.trim(); const requestedAgent = resolveRequestedSessionAgentId(cfg, key, input.agentId); return { input, key, requestedAgent, resolved: requestedAgent.ok ? resolveGatewaySessionStoreTargetWithStore({ cfg, key, agentId: requestedAgent.agentId, exactRead: true, targetDiscoveryCache }) : void 0 }; }); const logicalTargets = /* @__PURE__ */ new Set(); for (const { key, resolved } of preflightTargets) { if (!resolved) continue; const logicalId = `${resolved.storePath}\0${resolved.canonicalKey ?? key}`; if (logicalTargets.has(logicalId)) return invalidSessionPatchOutcome("Duplicate target."); logicalTargets.add(logicalId); } const outcomes = Array.from({ length: params.targets.length }); const permissionErrors = /* @__PURE__ */ new Map(); const prepared = []; const preparedByIndex = Array.from({ length: params.targets.length }); for (const [index, { input, key, requestedAgent, resolved }] of preflightTargets.entries()) { const unreadAckError = validateSessionUnreadAck(params.patch, input); if (unreadAckError) { outcomes[index] = invalidSessionPatchOutcome(unreadAckError); continue; } if (!requestedAgent.ok) { outcomes[index] = requestedAgent; continue; } if (!resolved) { outcomes[index] = invalidSessionPatchOutcome("Session target could not be resolved."); continue; } const requestedAgentId = requestedAgent.agentId; const canonicalKey = resolved.canonicalKey ?? key; const candidateKeys = resolved.storeKeys; let initialEntry; try { initialEntry = resolveCanonicalSessionEntryFromStoreKeys(resolved.store, [...candidateKeys]); } catch (error) { outcomes[index] = { ok: false, error: unexpectedPatchError(key, error) }; continue; } const creationError = !initialEntry && authorizeGatewaySessionCreation({ cfg, client, agentId: resolved.agentId }); if (creationError) { outcomes[index] = { ok: false, error: creationError }; continue; } const ownershipError = resolvePluginSessionOwnershipError({ action: "patch", entry: initialEntry, key: canonicalKey, pluginOwnerId }); if (ownershipError) { outcomes[index] = { ok: false, error: ownershipError }; continue; } const missingHarnessSessionError = resolveMissingAgentHarnessSessionError(canonicalKey, initialEntry); if (missingHarnessSessionError) { outcomes[index] = invalidSessionPatchOutcome(missingHarnessSessionError); continue; } const { commitGuard: _commitGuard, ...identity } = input; const fullPatch = { ...params.patch, ...identity }; const expectationError = resolveSessionPatchExpectationError(fullPatch); if (expectationError) { outcomes[index] = invalidSessionPatchOutcome(expectationError); continue; } let initialPlacementPatchError; try { initialPlacementPatchError = resolveSessionWorkerPlacementPatchError({ agentId: resolved.agentId, cfg, context: params.context, entry: initialEntry, key, patch: fullPatch, sessionKey: canonicalKey, validateModelRuntime: false }); } catch (error) { outcomes[index] = { ok: false, error: unexpectedPatchError(key, error) }; continue; } if (initialPlacementPatchError) { outcomes[index] = invalidSessionPatchOutcome(initialPlacementPatchError); continue; } const lifecycleIdentities = Array.from(/* @__PURE__ */ new Set([ key, canonicalKey, ...candidateKeys, initialEntry?.sessionId ])); const preparedTarget = { archiveActor, canonicalKey, fullPatch, index, ...initialEntry ? { initialEntry } : {}, initialStoreKeys: [...candidateKeys], key, lifecycleIdentities, ...requestedAgentId ? { requestedAgentId } : {}, storePath: resolved.storePath, targetAgentId: resolved.agentId }; prepared.push(preparedTarget); preparedByIndex[index] = preparedTarget; } const catalogs = createSessionPatchCatalogPreparation((agentId) => params.context.loadGatewayModelCatalog({ agentId })); if (prepared.length > 0) { const releaseArchiveDrains = async () => prepared.forEach((target) => releaseSessionPatchArchive(target.archivePreparation)); try { await Promise.all(prepared.filter((target) => target.fullPatch.archived === true).map(async (target) => { try { const result = await prepareSessionPatchArchive({ cfg, commitGuard: params.targets[target.index].commitGuard, context: params.context, loadGatewayModelCatalog: () => catalogs.load(target.targetAgentId), personalModelSelection, ...pluginOwnerId ? { pluginOwnerId } : {}, target }); if (result.ok) target.archivePreparation = result.value; else outcomes[target.index] = result; } catch (error) { outcomes[target.index] = { ok: false, error: unexpectedPatchError(target.key, error) }; } })); await runExclusiveSessionLifecycleMutation({ targets: prepared.map((target) => ({ scope: target.storePath, identities: target.lifecycleIdentities })), prepare: async () => { for (const target of prepared) target.archivePreparation?.drain.handoffToMutation(); }, finalize: releaseArchiveDrains, run: async () => { const groups = /* @__PURE__ */ new Map(); for (const target of prepared) { if (target.fullPatch.archived === true && !target.archivePreparation) continue; const groupKey = `${target.storePath}\0${target.targetAgentId}`; const group = groups.get(groupKey) ?? []; group.push(target); groups.set(groupKey, group); } await Promise.all([...groups.values()].map(async (group) => { const first = group[0]; try { const selectedSessionKeys = group.flatMap((target) => [ target.key, target.canonicalKey, ...target.initialStoreKeys ]); const requestedLabel = parseSessionLabel(first.fullPatch.label); const worktreeTransitions = /* @__PURE__ */ new Map(); const applyGroup = (catalogPreparation) => applySessionEntryCanonicalReplacements({ assertCommitAllowed: () => { personalModelSelection?.assertCurrent(); for (const transition of worktreeTransitions.values()) transition.assertCommitAllowed(); }, agentId: first.targetAgentId, sessionKeys: selectedSessionKeys, ...requestedLabel.ok ? { includeLabelOwners: requestedLabel.label } : {}, storePath: first.storePath, skipMaintenance: true, update: async (entries) => { const workingStore = Object.fromEntries(entries.flatMap(({ entry, sessionKey }) => isInternalSessionEffectsKey(sessionKey) ? [] : [[sessionKey, entry]])); const labelOwners = new SessionLabelOwnerIndex(workingStore); const replacements = []; const projectedOutcomes = []; for (const target of group) try { const { entry: existingEntry, primaryKey, target: currentTarget } = resolveCanonicalGatewaySessionStoreKey({ cfg, key: target.key, store: workingStore, ...target.requestedAgentId ? { agentId: target.requestedAgentId } : {} }); const creationError = !existingEntry && authorizeGatewaySessionCreation({ cfg, client, agentId: target.targetAgentId }); if (creationError) { projectedOutcomes.push({ ok: false, error: creationError }); continue; } const candidateKeys = currentTarget.storeKeys; const ownershipError = resolvePluginSessionOwnershipError({ action: "patch", entry: existingEntry, key: primaryKey, pluginOwnerId }); if (ownershipError) { projectedOutcomes.push({ ok: false, error: ownershipError }); continue; } const expectedSessionChanged = target.fullPatch.expectedSessionId !== void 0 && existingEntry?.sessionId !== target.fullPatch.expectedSessionId || target.fullPatch.expectedLifecycleRevision !== void 0 && existingEntry?.lifecycleRevision !== target.fullPatch.expectedLifecycleRevision || sessionPatchExpectationsChanged(existingEntry, target.fullPatch); const lifecycleEntryRemoved = target.initialEntry !== void 0 && existingEntry === void 0; const archiveTargetChanged = target.fullPatch.archived === true && (target.initialEntry === void 0 ? existingEntry !== void 0 : existingEntry !== void 0 && (existingEntry.sessionId !== target.initialEntry.sessionId || existingEntry.lifecycleRevision !== target.initialEntry.lifecycleRevision)); if (expectedSessionChanged || lifecycleEntryRemoved || archiveTargetChanged) { projectedOutcomes.push({ ok: false, error: sessionChangedError(target.key) }); continue; } if (target.fullPatch.archived === true) { const archiveError = validateSessionPatchArchiveProjection({ cfg, existingEntry, fullPatch: target.fullPatch, key: target.key, ...pluginOwnerId ? { pluginOwnerId } : {}, preparation: target.archivePreparation, primaryKey }); if (archiveError) { projectedOutcomes.push({ ok: false, error: archiveError }); continue; } } const unreadAck = resolveSessionUnreadAck(existingEntry, target.fullPatch); if (unreadAck.kind === "missing") { projectedOutcomes.push({ ok: false, error: sessionChangedError(target.key) }); continue; } if (unreadAck.kind === "stale") { const authorizationFailure = params.targets[target.index].commitGuard(); if (authorizationFailure) { projectedOutcomes.push({ ok: false, error: authorizationFailure }); continue; } projectedOutcomes.push({ ok: true, applied: false, entry: unreadAck.entry }); continue; } const projection = await catalogs.project({ agentId: target.targetAgentId, mode: group.length === 1 ? "prepare" : "ordered", catalog: catalogPreparation, projection: { cfg, creation, existingEntry, isLabelInUse: (label) => labelOwners.isLabelInUse(label, candidateKeys), storeKey: primaryKey, agentId: target.requestedAgentId, patch: target.fullPatch, archivedBy: archiveActor, personalModelSelection } }); if (projection.kind === "model-catalog") return { result: projection }; const projected = projection.result; if (!projected.ok) { projectedOutcomes.push(projected); continue; } const placementPatchError = resolveSessionWorkerPlacementPatchError({ agentId: target.targetAgentId, cfg, context: params.context, entry: projected.entry, key: target.key, patch: target.fullPatch, sessionKey: primaryKey, validateModelRuntime: true }); if (placementPatchError) { projectedOutcomes.push(invalidSessionPatchOutcome(placementPatchError)); continue; } const authorizationFailure = params.targets[target.index].commitGuard(); if (authorizationFailure) { projectedOutcomes.push({ ok: false, error: authorizationFailure }); continue; } if (existingEntry?.worktree && typeof target.fullPatch.archived === "boolean") { const transition = await prepareSessionPatchWorktreeTransition({ archived: target.fullPatch.archived, entry: existingEntry, context: params.context, scope: { agentId: target.targetAgentId, sessionKey: primaryKey, storePath: target.storePath }, authorize: params.targets[target.index].commitGuard, preparation: target.archivePreparation }); worktreeTransitions.set(target.index, transition); } if (permissionRuntime && existingEntry?.sessionId) { const permission = permissionRuntime.prepareSessionPatchPermissionChange({ context: params.context, sessionId: existingEntry.sessionId, sessionKey: target.canonicalKey, agentId: target.targetAgentId, assertCurrent: params.targets[target.index].commitGuard }); if (!permission.ok) { projectedOutcomes.push(permission); continue; } target.permissionChange = permission.change; } const previousSessionKeys = candidateKeys.filter((sessionKey) => sessionKey !== primaryKey && workingStore[sessionKey]); replacements.push({ entry: projected.entry, previousSessionKeys, sessionKey: primaryKey }); const cloned = labelOwners.replaceEntry(candidateKeys, primaryKey, projected.entry); projectedOutcomes.push({ ok: true, applied: true, entry: cloned }); } catch (error) { projectedOutcomes.push({ ok: false, error: unexpectedPatchError(target.key, error) }); } return { replacements, result: { kind: "complete", outcomes: projectedOutcomes } }; } }); let groupResult = await applyGroup(); if (groupResult.kind === "model-catalog") groupResult = await applyGroup(await catalogs.prepare(first.targetAgentId)); if (groupResult.kind !== "complete") throw new Error("Session patch catalog preparation did not complete"); const groupOutcomes = groupResult.outcomes; for (const [groupIndex, target] of group.entries()) { const outcome = groupOutcomes[groupIndex]; outcomes[target.index] = outcome; const afterCommit = worktreeTransitions.get(target.index)?.afterCommit; if (outcome.ok && outcome.applied && afterCommit) outcome.cleanupError = await afterCommit(outcome.entry); } } catch (error) { for (const target of group) outcomes[target.index] = { ok: false, error: unexpectedPatchError(target.key, error) }; } })); for (const target of prepared) { const outcome = outcomes[target.index]; if (!target.permissionChange || !outcome?.ok || !outcome.applied) continue; const error = await target.permissionChange.apply(outcome.entry.permissionMode ?? null); if (error) permissionErrors.set(target.index, error); } } }); } finally { for (const target of prepared) target.permissionChange?.finish(); await releaseArchiveDrains(); } } await publishSessionPatchEffects({ cfg, context: params.context, callerScopes, callerCanManageCron, category: params.patch.category, targets: prepared.flatMap((target) => { const outcome = outcomes[target.index]; return outcome?.ok && outcome.applied ? [{ target, entry: outcome.entry }] : []; }) }); for (const [index, error] of permissionErrors) outcomes[index] = { ok: false, error }; return { ok: true, cfg, outcomes: outcomes.map((outcome) => outcome?.ok && outcome.cleanupError ? { ok: false, error: outcome.cleanupError } : outcome), preparedByIndex, catalogs }; } async function executeSessionPatchMany(params) { const executed = await executeSessionPatchMutations({ client: params.client, context: params.context, patch: params.patch, targets: params.targets.map((target) => ({ ...target, commitGuard: createCommitGuard(target.key.trim(), () => params.sessionMutationAuthorization?.assertTargetCurrent({ sessionKey: target.key.trim(), ...target.agentId ? { agentId: target.agentId } : {} })) })) }); if (!executed.ok) return executed; const outcomes = []; for (const [index, outcome] of executed.outcomes.entries()) { const target = params.targets[index]; if (outcome.ok) { outcomes.push(target.agentId ? { ok: true, key: target.key, agentId: target.agentId } : { ok: true, key: target.key }); continue; } outcomes.push(target.agentId ? { ok: false, key: target.key, agentId: target.agentId, error: outcome.error } : { ok: false, key: target.key, error: outcome.error }); } return { ok: true, outcomes }; } async function executeSessionPatch(params) { const target = sessionPatchTargetIdentity(params.patch); const executed = await executeSessionPatchMutations({ client: params.client, context: params.context, patch: params.patch, targets: [{ ...target, commitGuard: createCommitGuard(target.key, params.sessionMutationAuthorization?.assertCurrent) }] }); if (!executed.ok) return executed; const outcome = executed.outcomes[0]; if (!outcome.ok) return outcome; const prepared = executed.preparedByIndex[0]; return { ok: true, result: projectSessionPatchResult({ ...prepared, cfg: executed.cfg, entry: outcome.entry, modelCatalog: await executed.catalogs.available(prepared.targetAgentId) }) }; } //#endregion //#region src/gateway/server-methods/sessions-mutations.ts const sessionMutationHandlers = { "sessions.patchMany": async ({ params, respond, context, client, sessionMutationAuthorization }) => { if (!assertValidParams(params, validateSessionsPatchManyParams, "sessions.patchMany", respond)) return; const scopes = Array.isArray(client?.connect.scopes) ? client.connect.scopes : []; if (params.patch.permissionMode === "full" && client !== null && !scopes.includes("operator.admin")) { respond(false, void 0, missingScopeErrorShape({ missingScope: ADMIN_SCOPE, requiredScopes: [ADMIN_SCOPE] })); return; } const executed = await executeSessionPatchMany({ client, context, patch: params.patch, sessionMutationAuthorization, targets: params.targets }); if (!executed.ok) { respond(false, void 0, executed.error); return; } respond(true, { outcomes: executed.outcomes }, void 0); }, "sessions.patch": async ({ params, respond, context, client, sessionMutationAuthorization }) => { if (!assertValidParams(params, validateSessionsPatchParams, "sessions.patch", respond)) return; const scopes = Array.isArray(client?.connect.scopes) ? client.connect.scopes : []; if (params.permissionMode === "full" && client !== null && !scopes.includes("operator.admin")) { respond(false, void 0, missingScopeErrorShape({ missingScope: ADMIN_SCOPE, requiredScopes: [ADMIN_SCOPE] })); return; } const key = requireSessionKey(params.key, respond); if (!key) return; const executed = await executeSessionPatch({ client, context, patch: { ...params, key }, sessionMutationAuthorization }); if (!executed.ok) { respond(false, void 0, executed.error); return; } respond(true, executed.result, void 0); }, "sessions.assignOwner": async ({ params, respond, context, client }) => { if (!assertValidParams(params, validateSessionsAssignOwnerParams, "sessions.assignOwner", respond)) return; const key = requireSessionKey(params.key, respond); if (!key) return; const runtimeAgentId = normalizeOptionalString(client?.internal?.agentRuntimeIdentity?.agentId); const agentToolCallerId = client?.internal?.syntheticClient === true ? normalizeOptionalString(client.internal.agentToolCaller?.agentId) : void 0; const trustedAgentId = runtimeAgentId ?? agentToolCallerId; const humanActor = gatewayClientSessionCreator(client); const assignedBy = trustedAgentId ? { type: "agent", id: trustedAgentId } : humanActor ? { type: "human", id: humanActor.id } : null; if (!assignedBy) { respond(false, void 0, errorShape(ErrorCodes.FORBIDDEN, "sessions.assignOwner requires an identified caller")); return; } const cfg = context.getRuntimeConfig(); const requestedAgent = resolveRequestedSessionAgentId(cfg, key, params.agentId); if (!requestedAgent.ok) { respond(false, void 0, requestedAgent.error); return; } const target = resolveSessionSharingTarget({ cfg, sessionKey: key, agentId: requestedAgent.agentId }); if (!target) { respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, `unknown session: ${key}`)); return; } const authorizeView = (candidate) => authorizeIncognitoSessionTarget({ client, sessionKey: key, target: candidate }) ?? (createSessionListEntryFilter({ client, cfg })?.(candidate.storeKey, candidate.entry) === false ? errorShape(ErrorCodes.FORBIDDEN, "session is not visible to this connection") : null); const visibilityError = authorizeView(target); if (visibilityError) { respond(false, void 0, visibilityError); return; } const ownerIdentityById = /* @__PURE__ */ new Map(); const projectedOwner = projectAssignableSessionOwner(params.owner, ownerIdentityById, cfg); if (!projectedOwner) { respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, `unknown session owner "${params.owner.id}"`)); return; } const owner = { type: projectedOwner.type, id: projectedOwner.id }; const assignment = assignSessionOwner({ agentId: target.agentId, sessionKey: target.storeKey, storePath: target.storePath }, { owner, assignedBy, assertCurrent: () => { const current = resolveSessionSharingTarget({ cfg: context.getRuntimeConfig(), sessionKey: target.canonicalKey, agentId: target.agentId }); const currentError = current ? authorizeView(current) : null; if (!current || current.entry.sessionId !== target.entry.sessionId || current.storeKey !== target.storeKey || currentError) throw new SessionMutationAuthorizationChangedError(currentError ?? errorShape(ErrorCodes.INVALID_REQUEST, "session changed before sessions.assignOwner; retry the request")); } }); const projectedActor = assignment ? projectAssignableSessionOwner(assignment.actor, ownerIdentityById, cfg) : null; const projectedAssignedBy = assignment?.assignedBy ? projectSessionActor(assignment.assignedBy, /* @__PURE__ */ new Map(), cfg) : void 0; const projected = assignment && projectedActor ? { actor: projectedActor, ...projectedAssignedBy ? { assignedBy: projectedAssignedBy } : {}, ...assignment.assignedAt !== void 0 ? { assignedAt: assignment.assignedAt } : {} } : void 0; if (!projected) { respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, `unknown session: ${key}`)); return; } respond(true, { ok: true, key: target.canonicalKey, owner: projected }, void 0); emitSessionsChanged(context, { sessionKey: target.canonicalKey, agentId: target.agentId, reason: "owner" }); }, "sessions.pluginPatch": async ({ params, respond, context, client, sessionMutationAuthorization }) => { if (!assertValidParams(params, validateSessionsPluginPatchParams, "sessions.pluginPatch", respond)) return; const key = requireSessionKey(params.key, respond); if (!key) return; if (!(Array.isArray(client?.connect.scopes) ? client.connect.scopes : []).includes("operator.admin")) { respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, `sessions.pluginPatch requires gateway scope: ${ADMIN_SCOPE}`)); return; } const pluginId = normalizeOptionalString(params.pluginId); const namespace = normalizeOptionalString(params.namespace); if (!pluginId || !namespace) { respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, "pluginId and namespace are required")); return; } if (params.unset === true && params.value !== void 0) { respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, "sessions.pluginPatch cannot specify both unset and value")); return; } if (params.value !== void 0 && !isPluginJsonValue(params.value)) { respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, "sessions.pluginPatch value must be JSON-compatible")); return; } const requestedAgent = resolveRequestedSessionAgentId(context.getRuntimeConfig(), key, params.agentId); if (!requestedAgent.ok) { respond(false, void 0, requestedAgent.error); return; } const canonicalKey = resolveStoredSessionKeyForAgentStore({ cfg: context.getRuntimeConfig(), agentId: requestedAgent.agentId, sessionKey: key }); const patched = await patchPluginSessionExtension({ cfg: context.getRuntimeConfig(), sessionKey: canonicalKey, agentId: requestedAgent.agentId, pluginId, namespace, value: params.value, unset: params.unset === true, assertCurrent: sessionMutationAuthorization?.assertCurrent }); if (!patched.ok) { respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, patched.error)); return; } respond(true, { ok: true, key: patched.key, value: patched.value }, void 0); emitSessionsChanged(context, { sessionKey: patched.key, agentId: requestedAgent.agentId, reason: "plugin-patch" }); }, "sessions.reset": async ({ params, respond, context, client, sessionMutationAuthorization }) => { if (!assertValidParams(params, validateSessionsResetParams, "sessions.reset", respond)) return; const p = params; const key = requireSessionKey(p.key, respond); if (!key) return; const reason = p.reason === "new" ? "new" : "reset"; const { performGatewaySessionReset } = await loadSessionsRuntimeModule(); const result = await performGatewaySessionReset({ key, ...p.agentId ? { agentId: p.agentId } : {}, reason, commandSource: "gateway:sessions.reset", creation: resolveOperatorSessionCreation(client), ...client?.authenticatedUserProfile ? { requestingOperatorProfileId: client.authenticatedUserProfile.profileId } : {}, ...client?.internal?.operatorRoleActor ? { operatorRoleActor: client.internal.operatorRoleActor } : {}, authorizedPluginId: normalizeOptionalString(client?.internal?.pluginRuntimeOwnerId), armSessionDiffBaselineCapture: true, workerPlacementContext: context, assertAuthorizedInstance: sessionMutationAuthorization?.assertCurrent }); if (!result.ok) { respond(false, void 0, result.error); return; } if ("incognitoDeleted" in result) { respond(true, { ok: true, key: result.key, deleted: true }, void 0); emitSessionsChanged(context, { sessionKey: result.key, agentId: result.agentId, sessionId: result.deletedSessionId, reason: "delete" }); return; } respond(true, { ok: true, key: result.key, entry: result.entry, resolved: result.resolved }, void 0); emitSessionsChanged(context, { sessionKey: result.key, agentId: result.agentId, reason }); } }; //#endregion export { sessionMutationHandlers };