UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

554 lines (553 loc) • 24.8 kB
import { l as normalizeOptionalString } from "./string-coerce-1VuiLaaR.mjs"; import { c as isRecord } from "./record-coerce-TIsquoVR.mjs"; import { r as resolveStateDir } from "./state-dir-BHJORNly.mjs"; import { n as assertExistingDatabaseIdentity, u as readDatabasePathIdentitySync } from "./sqlite-worker-identity-KSR14F6C.mjs"; import { n as normalizeAgentId } from "./agent-id-D1-Q6vR4.mjs"; import { c as SqliteWorkerError } from "./sqlite-worker-contract-C51cJHzg.mjs"; import { r as cloneEnvWithPlatformSemantics } from "./config-env-vars-DjPHKieF.mjs"; import { r as isIncognitoSessionKey } from "./session-key-CmZ4o9-l.mjs"; import { M as parseAgentSessionKey } from "./session-key-C09ow4h0.mjs"; import { c as resolveOpenClawAgentSqlitePath, i as isIncognitoOpenClawAgentSqlitePath } from "./openclaw-agent-db.paths-DVbbFaej.mjs"; import { r as runOpenClawAgentWriteAdmission } from "./openclaw-agent-write-admission-CKOCIcah.mjs"; import { U as validateChatAbortParams } from "./src-l2dWvEsN.mjs"; import { t as ErrorCodes } from "./gateway-error-details-DIefe7Zy.mjs"; import { f as errorShape } from "./error-codes-Dh56LniQ.mjs"; import { s as assertSessionStoreReadCandidate } from "./session-sqlite-target-Cnk9g9d0.mjs"; import { i as toDatabaseOptions } from "./session-accessor.sqlite-scope-helpers-DOK3pvYX.mjs"; import { f as resolveSqliteWriteAdmissionScope, i as prepareSqliteScope, s as resolveSqliteScope } from "./session-accessor.sqlite-scope-CdpbxR_m.mjs"; import { t as captureSessionStoreReadCandidates } from "./session-store-target-inventory-Dj6Sq0En.mjs"; import { a as withSessionEntryWorker } from "./session-accessor.sqlite-replacement-worker-9fiwLkM3.mjs"; import { a as tryResolveSessionCompatibilityOwnerAgentId, n as resolveRequestedSessionAgentId } from "./session-request-agent-Bxi11ET4.mjs"; import { a as resolveSessionStoreKey } from "./session-store-key-Ci099tm9.mjs"; import { t as chatRunBelongsToAgent } from "./chat-run-owner-Ci8Mz1ow.mjs"; import { r as isChatAbortControllerEntryAbortable, t as abortChatRunById } from "./chat-abort-DsJ_Zmkt.mjs"; import { i as isQueuedChatTurnForSession, t as abortQueuedChatTurnById } from "./chat-queued-turns-Dn_FgmH5.mjs"; import { a as readGatewayRequestMutationAuthority } from "./session-mutation-guards-Cw1VPolZ.mjs"; import { r as loadGatewaySessionEntry } from "./session-utils-store-BAlWJHht.mjs"; import { r as resolveStateContentionPresentation } from "./session-run-error-presentation-Mm1GMV0-.mjs"; import "./session-utils-seGz8XhX.mjs"; import { r as emitSessionsChanged } from "./session-change-event-KJsyN66H.mjs"; import { i as getWorkerInferenceSessionControl } from "./inference-control-internal-h8yXQYae.mjs"; import { n as createChatAbortOps, t as captureWorkerInferenceForSession } from "./chat-abort-ops-BfEknC6Y.mjs"; import { a as withAbortedPartialPersistenceWarning, i as deferAbortedPartialPersistence, n as abortedPartialPersistenceError, r as captureAbortedPartial } from "./chat-aborted-partial-CxQqurtr.mjs"; import { t as assertValidParams } from "./validation-Bu27N0nv.mjs"; import { n as formatStopRequest } from "./control-plane-audit-DhEbrRsy.mjs"; import { n as pendingChatSendDedupeKey } from "./server-shared-C-7Ahu3n.mjs"; import { d as resolveChatAbortRequester, f as writePreRegisteredAgentAbort, i as descendantAbortError, l as readPreRegisteredAgentDedupePayloadForSession, n as abortControlledSubagents, o as canRequesterAbortChatRun, p as writePreRegisteredChatAbort, s as canRequesterAbortPreRegisteredRun, t as abortChatRunsForSessionKeyWithPartials } from "./chat-abort-runtime-DNZEdDuD.mjs"; import { i as persistAbortedPartials } from "./chat-transcript-persistence-Cf86cx_Q.mjs"; //#region src/config/sessions/session-pending-input-withdrawal.ts function readWithdrawalResult(receipt, input) { return isRecord(receipt) && receipt.kind === "session-pending-input-withdrawal" && receipt.sessionKey === input.sessionKey && receipt.sessionId === input.sessionId && receipt.runId === input.runId && typeof receipt.withdrawn === "boolean" ? receipt.withdrawn : void 0; } /** The caller retains live cancellation authority until withdrawal commits. */ async function discardSessionPendingInput(scope, runId, assertCurrent) { assertCurrent(); const env = cloneEnvWithPlatformSemantics(scope.env ?? process.env); env.OPENCLAW_STATE_DIR = resolveStateDir(env); const target = { ...scope, env }; const logical = resolveSqliteScope({ ...target, storePath: void 0 }); const storePath = logical.path ?? target.storePath ?? resolveOpenClawAgentSqlitePath(toDatabaseOptions(logical)); const input = { sessionKey: logical.sessionKey, sessionId: target.sessionId, runId }; if (isIncognitoOpenClawAgentSqlitePath(storePath, toDatabaseOptions(logical))) throw new Error("Queued input removal is unavailable for incognito chats; use Stop to cancel execution"); const candidates = captureSessionStoreReadCandidates(storePath); const identities = new Map(candidates.filter((candidate) => !candidate.scope).map((candidate) => { const identity = readDatabasePathIdentitySync(candidate.path); return [identity.canonicalPath, identity]; })); const writeAdmission = resolveSqliteWriteAdmissionScope(target); const withdraw = async () => { const resolved = await prepareSqliteScope(target); assertCurrent(); const options = toDatabaseOptions(resolved); const path = resolveOpenClawAgentSqlitePath(options); const identity = identities.get(assertSessionStoreReadCandidate(path, candidates)); if (!identity) throw new Error("Pending input withdrawal changed its captured database owner"); if (!identity.key.startsWith("file:")) return false; const assertHeld = () => { assertSessionStoreReadCandidate(path, candidates); assertExistingDatabaseIdentity(path, identity.key, identity.birthtime); assertCurrent(); }; assertHeld(); let admitted; return withSessionEntryWorker({ ...options, path }, void 0, assertHeld, async (execution, source) => { return await execution.runExisting(source, async (worker) => { const outcome = await worker.execute({ type: "session.pendingInputs.withdraw", input }).then(() => ({ ok: true }), (error) => ({ ok: false, error })); if (admitted) { await admitted.retained.settled; const withdrawn = readWithdrawalResult(admitted.admission.committed?.facts, input); if (admitted.admission.settlement?.kind === "completed" && withdrawn !== void 0) return withdrawn; } if (!outcome.ok) throw outcome.error; throw new SqliteWorkerError("Pending input withdrawal has no confirmed native completion and commit receipt", "outcome-unknown"); }) ?? false; }, (admission, retained, facts) => { if (!isRecord(facts) || readWithdrawalResult(facts.publication, input) === void 0) throw new Error("Pending input withdrawal commit omitted its exact receipt"); admitted = { admission, retained }; }); }; return writeAdmission ? runOpenClawAgentWriteAdmission(toDatabaseOptions(writeAdmission), withdraw, true) : withdraw(); } //#endregion //#region src/gateway/server-methods/chat-abort-handler.ts async function handleChatAbortRequestWithLifecycle(options, lifecycle = {}) { const { params, respond, context, client, sessionMutationAuthorization } = options; const authority = readGatewayRequestMutationAuthority(options); const requester = resolveChatAbortRequester(client, sessionMutationAuthorization); const assertCurrent = () => { authority.assertCurrent(); sessionMutationAuthorization?.assertCurrent(); requester.sessionAuthority?.assertCurrent(); }; if (!assertValidParams(params, validateChatAbortParams, "chat.abort", respond)) return; const { sessionKey: rawSessionKey, runId, preserveSideRuns, discardPendingInput } = params; if (discardPendingInput && !runId) { respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, "discardPendingInput requires an exact runId")); return; } const agentIdOverride = normalizeOptionalString(params.agentId); const abortCfg = context.getRuntimeConfig(); const parsedAbortSessionKey = parseAgentSessionKey(rawSessionKey); const compatibilityDefaultAgentId = tryResolveSessionCompatibilityOwnerAgentId(abortCfg, rawSessionKey); const inferredSessionAgentId = !agentIdOverride && parsedAbortSessionKey ? normalizeAgentId(parsedAbortSessionKey.agentId) : void 0; const bareSessionAgentResolution = !parsedAbortSessionKey ? resolveRequestedSessionAgentId(abortCfg, rawSessionKey, agentIdOverride) : void 0; if (bareSessionAgentResolution && !bareSessionAgentResolution.ok) { respond(false, void 0, bareSessionAgentResolution.error); return; } const abortAgentId = parsedAbortSessionKey ? agentIdOverride ?? inferredSessionAgentId : bareSessionAgentResolution?.agentId; if (!abortAgentId) { respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, rawSessionKey.trim().toLowerCase() === "global" ? "agentId is required for global chat.abort when no compatibility owner exists" : "agentId is required for unscoped chat.abort when no compatibility owner exists")); return; } if (agentIdOverride && parsedAbortSessionKey && normalizeAgentId(parsedAbortSessionKey.agentId) !== normalizeAgentId(agentIdOverride)) { respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, `agentId "${agentIdOverride}" does not match session key "${rawSessionKey}"`)); return; } const canonicalAbortSessionKey = resolveSessionStoreKey({ cfg: abortCfg, sessionKey: rawSessionKey, storeAgentId: abortAgentId }); if (discardPendingInput && isIncognitoSessionKey(canonicalAbortSessionKey)) { respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, "Removing accepted queued input is unavailable in incognito sessions. Use Stop to cancel it.")); return; } const narrow = authority.sessionScope === "operator.sessions.write" || requester.sessionAuthority !== void 0; const admittedTarget = sessionMutationAuthorization?.admittedTarget; if (narrow && (!admittedTarget?.sessionId.trim() || admittedTarget.sessionKey !== canonicalAbortSessionKey || admittedTarget.agentId !== normalizeAgentId(abortAgentId))) { respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, "session target is unavailable")); return; } const requiredSessionId = narrow ? admittedTarget?.sessionId : void 0; const ops = createChatAbortOps(context); const abortSession = (() => { try { return { ok: true, value: loadGatewaySessionEntry(canonicalAbortSessionKey, { agentId: abortAgentId }) }; } catch (error) { return { ok: false, error }; } })(); const abortSessionEntry = abortSession.ok ? abortSession.value.entry : void 0; if (!runId) { const res = await abortChatRunsForSessionKeyWithPartials({ context, ops, sessionKey: canonicalAbortSessionKey, sessionKeyAliases: canonicalAbortSessionKey === rawSessionKey ? void 0 : [rawSessionKey], agentId: abortAgentId, sessionId: abortSessionEntry?.sessionId, requiredSessionId, session: abortSession, defaultAgentId: compatibilityDefaultAgentId, abortOrigin: "rpc", stopReason: "rpc", requester, assertCurrent, preserveSideRuns, onAuthorizedAfterQueuedAbort: lifecycle.onAuthorizedAfterQueuedAbort, cascadeDescendants: lifecycle.cascadeDescendants }); if (res.unauthorized) { respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, "unauthorized")); return; } if (res.descendants?.killed) lifecycle.onDescendantsCancelled?.(); const error = res.error ?? descendantAbortError(res.descendants, "Session"); if (error) { respond(false, void 0, withAbortedPartialPersistenceWarning(error, res.warning)); return; } respond(true, { ok: true, aborted: res.aborted, runIds: res.runIds, ...res.warning ? { warning: res.warning } : {} }); return; } const normalizedAgentIdOverride = normalizeAgentId(abortAgentId); const authorizeRunTarget = (target) => { let error; if (discardPendingInput && target.sessionKey !== rawSessionKey && target.sessionKey !== canonicalAbortSessionKey) error = "discarded input runId does not match sessionKey"; else if (narrow && target.sessionId !== requiredSessionId) error = "runId does not match session incarnation"; else if (target.sessionKey !== rawSessionKey && target.sessionKey !== canonicalAbortSessionKey && (narrow || !canRequesterAbortChatRun(target, requester, { requireOwnerMatch: true }))) error = "runId does not match sessionKey"; else if (!chatRunBelongsToAgent({ agentId: target.agentId, sessionKey: target.sessionKey, defaultAgentId: compatibilityDefaultAgentId }, normalizedAgentIdOverride)) error = "runId does not match agentId"; else if (!canRequesterAbortChatRun(target, requester)) error = "unauthorized"; if (error) respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, error)); return error === void 0; }; const active = context.chatAbortControllers.get(runId); const withdrawalQueue = discardPendingInput ? context.chatQueuedTurns.get(runId) : void 0; const withdrawalScope = withdrawalQueue && { sessionKey: withdrawalQueue.sessionKey, sessionId: withdrawalQueue.sessionId, agentId: withdrawalQueue.agentId }; const ownsWithdrawalQueue = () => Boolean(withdrawalQueue && withdrawalScope && context.chatQueuedTurns.get(runId) === withdrawalQueue && isQueuedChatTurnForSession(context.chatQueuedTurns, runId, withdrawalScope)); if (discardPendingInput) { if (active && !authorizeRunTarget(active) || withdrawalQueue && !authorizeRunTarget(withdrawalQueue)) return; if (!ownsWithdrawalQueue() || active && withdrawalQueue?.controller !== active.controller) { respond(true, { ok: true, aborted: false, runIds: [] }); return; } } const workerTarget = getWorkerInferenceSessionControl(context.workerEnvironmentService)?.resolveSessionTargetForRunId(runId); const workerCancellation = captureWorkerInferenceForSession({ context, sessionId: active?.sessionId ?? workerTarget?.sessionId ?? abortSessionEntry?.sessionId, runId }); let inputWithdrawn = false; const holdPendingInputWithdrawal = (controller) => { if (!discardPendingInput) return; const queued = withdrawalQueue?.controller === controller ? withdrawalQueue : void 0; const activeTarget = active && { entry: active, sessionKey: active.sessionKey, sessionId: active.sessionId, agentId: active.agentId }; const queuedTarget = queued && { entry: queued, sessionKey: queued.sessionKey, sessionId: queued.sessionId, agentId: queued.agentId }; const release = queued?.holdPendingInputWithdrawal?.(); if (!queued || !release) throw new Error("Queued input is already starting; refresh before removing it."); return () => { try { if (!inputWithdrawn || controller.signal.aborted) return; if (activeTarget && context.chatAbortControllers.get(runId) === activeTarget.entry && activeTarget.entry.controller === controller && activeTarget.entry.sessionKey === activeTarget.sessionKey && activeTarget.entry.sessionId === activeTarget.sessionId && activeTarget.entry.agentId === activeTarget.agentId) abortChatRunById(ops, { runId, sessionKey: activeTarget.sessionKey, stopReason: "rpc" }); if (!controller.signal.aborted && queuedTarget && context.chatQueuedTurns.get(runId) === queuedTarget.entry && queuedTarget.entry.controller === controller && queuedTarget.entry.sessionKey === queuedTarget.sessionKey && queuedTarget.entry.sessionId === queuedTarget.sessionId && queuedTarget.entry.agentId === queuedTarget.agentId) abortQueuedChatTurnById(context.chatQueuedTurns, { runId, sessionKey: queuedTarget.sessionKey, stopReason: "rpc" }); } finally { release?.(); } }; }; const discardBeforeAbort = async (target, ownsTarget) => { if (!abortSession.ok) throw abortSession.error; const captured = { agentId: target.agentId ?? abortAgentId, sessionKey: target.sessionKey, sessionId: target.sessionId, storePath: abortSession.value.storePath }; inputWithdrawn = await discardSessionPendingInput(captured, runId, () => { assertCurrent(); if (!ownsTarget()) throw new Error("Run changed before input removal; refresh and retry."); }); if (inputWithdrawn) emitSessionsChanged(context, { sessionKey: captured.sessionKey, sessionId: captured.sessionId, agentId: captured.agentId, reason: "agent.input.settled" }, { accessChanged: false }); }; const respondWithWorkerRuns = async (localRunIds, warning) => { const runIds = new Set(localRunIds); if (inputWithdrawn) runIds.add(runId); if (requester.isAdmin) { assertCurrent(); await workerCancellation?.cancel({ assertCurrent, onCancelled: (id) => runIds.add(id) }); } if (!abortSession.ok) throw abortSession.error; respond(true, { ok: true, aborted: runIds.size > 0, runIds: [...runIds], ...warning ? { warning } : {} }); }; if (!active) { const readPendingRunForAbort = (entry) => { for (const sessionKey of /* @__PURE__ */ new Set([canonicalAbortSessionKey, rawSessionKey])) { const payload = readPreRegisteredAgentDedupePayloadForSession({ entry, runId, sessionKey, agentId: abortAgentId, defaultAgentId: compatibilityDefaultAgentId, requiredSessionId }); if (payload) return { sessionKey: normalizeOptionalString(payload.sessionKey) ? sessionKey : void 0, payload }; } }; const pendingChatMatch = !discardPendingInput && readPendingRunForAbort(context.dedupe.get(pendingChatSendDedupeKey(runId))); if (pendingChatMatch) { if (!canRequesterAbortPreRegisteredRun(pendingChatMatch.payload, requester)) { respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, "unauthorized")); return; } assertCurrent(); await respondWithWorkerRuns(writePreRegisteredChatAbort({ context, runId, stopReason: "rpc", attemptId: normalizeOptionalString(pendingChatMatch.payload.attemptId), expectedPayload: pendingChatMatch.payload }) ? [runId] : []); return; } const pendingAgentEntry = context.dedupe.get(`agent:${runId}`); const pendingAgentMatch = !discardPendingInput && readPendingRunForAbort(pendingAgentEntry); if (pendingAgentMatch) { const pendingAgentPayload = pendingAgentMatch.payload; if (!canRequesterAbortPreRegisteredRun(pendingAgentPayload, requester)) { respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, "unauthorized")); return; } let aborted = false; const descendants = await abortControlledSubagents({ cfg: abortCfg, sessionKey: pendingAgentMatch.sessionKey ?? canonicalAbortSessionKey, agentId: abortAgentId, requesterTurnRunId: runId, assertCurrent, beforeKill: () => { assertCurrent(); return aborted = writePreRegisteredAgentAbort({ context, runId, sessionKey: pendingAgentMatch.sessionKey, payload: pendingAgentPayload, expectedPayload: pendingAgentPayload, stopReason: "rpc" }); } }); const error = descendantAbortError(descendants, "Parent run"); if (error) { respond(false, void 0, error); return; } await respondWithWorkerRuns(aborted ? [runId] : []); return; } const chatQueuedTurns = context.chatQueuedTurns; const queued = chatQueuedTurns.get(runId); if (queued) { if (!discardPendingInput && !authorizeRunTarget(queued)) return; const { sessionKey, sessionId, agentId } = queued; const ownsTarget = () => chatQueuedTurns.get(runId) === queued && isQueuedChatTurnForSession(chatQueuedTurns, runId, { sessionKey, sessionId, agentId }); const releaseWithdrawal = holdPendingInputWithdrawal(queued.controller); try { if (discardPendingInput) { await discardBeforeAbort(queued, ownsTarget); if (!inputWithdrawn) { respond(true, { ok: true, aborted: false, runIds: [] }); return; } } assertCurrent(); if (discardPendingInput && !ownsTarget()) { await respondWithWorkerRuns([]); return; } if (chatQueuedTurns.get(runId) !== queued || queued.sessionKey !== sessionKey || queued.sessionId !== sessionId || queued.agentId !== agentId) throw new Error("Run changed before cancellation; retry Stop."); await respondWithWorkerRuns(abortQueuedChatTurnById(chatQueuedTurns, { runId, sessionKey: queued.sessionKey, stopReason: "rpc", allowSessionMismatch: true }).aborted ? [runId] : []); } finally { releaseWithdrawal?.(); } return; } if (!workerCancellation?.runIds.length) { if (!abortSession.ok) throw abortSession.error; respond(true, { ok: true, aborted: false, runIds: [] }); return; } if (!requester.isAdmin) { respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, "unauthorized")); return; } await respondWithWorkerRuns([]); return; } if (!discardPendingInput && !authorizeRunTarget(active)) return; let aborted = false; const { sessionKey, sessionId, agentId, controlUiVisible } = active; const ownsTarget = () => context.chatAbortControllers.get(runId) === active && active.sessionKey === sessionKey && active.sessionId === sessionId && active.agentId === agentId && isChatAbortControllerEntryAbortable(active) && (!discardPendingInput || ownsWithdrawalQueue()); const releaseWithdrawal = holdPendingInputWithdrawal(active.controller); try { if (discardPendingInput) { await discardBeforeAbort(active, ownsTarget); if (!inputWithdrawn) { respond(true, { ok: true, aborted: false, runIds: [] }); return; } } assertCurrent(); if (discardPendingInput && !ownsTarget()) { await respondWithWorkerRuns([]); return; } const partialText = context.chatRunState.resolveBuffer(runId, { final: true }).text; const snapshot = controlUiVisible !== false && partialText?.trim() ? captureAbortedPartial({ runId, sessionKey, sessionId, agentId: agentId ?? abortAgentId, text: partialText, abortOrigin: "rpc", resolveTerminalProducer: active.resolveTerminalProducer, ...sessionKey === rawSessionKey || sessionKey === canonicalAbortSessionKey ? { session: abortSession } : {} }) : void 0; let descendants; let failure; let warning; try { descendants = await abortControlledSubagents({ cfg: abortCfg, sessionKey, agentId, requesterTurnRunId: runId, assertCurrent, beforeKill: () => { assertCurrent(); if (context.chatAbortControllers.get(runId) !== active || active.sessionKey !== sessionKey || active.sessionId !== sessionId || active.agentId !== agentId) { if (inputWithdrawn) return false; throw new Error("Run changed before cancellation; retry Stop."); } return abortChatRunById(ops, { runId, sessionKey, stopReason: "rpc", onAbortCommitted: () => { aborted = true; deferAbortedPartialPersistence(snapshot, context); } }).aborted; } }); } catch (error) { failure = { error }; } if (aborted && snapshot) try { warning = await persistAbortedPartials({ context, snapshots: [snapshot] }); } catch (error) { if (failure) throw new AggregateError([failure.error, error], "Chat cancellation and persistence failed", { cause: error }); throw error; } if (failure) throw abortedPartialPersistenceError(failure.error, warning); if (!abortSession.ok) throw abortedPartialPersistenceError(abortSession.error, warning); const descendantError = descendantAbortError(descendants, "Parent run"); if (descendantError) { respond(false, void 0, withAbortedPartialPersistenceWarning(descendantError, warning)); return; } try { await respondWithWorkerRuns(aborted ? [runId] : [], warning); } catch (error) { throw abortedPartialPersistenceError(error, warning); } } finally { releaseWithdrawal?.(); } } async function handleChatAbortRequest(options) { if (validateChatAbortParams(options.params)) options.context.logGateway.info(formatStopRequest("chat.abort", options.client, options.params)); try { await handleChatAbortRequestWithLifecycle(options); } catch (error) { const contention = resolveStateContentionPresentation(error); if (!contention) throw error; options.respond(false, void 0, errorShape(ErrorCodes.UNAVAILABLE, "The server is busy. Check this turn's status before trying Stop again.\n\nSQLite transaction admission remained busy. Stopping may already have taken effect.", { details: { errorKind: contention.errorKind } })); } } //#endregion export { handleChatAbortRequestWithLifecycle as n, handleChatAbortRequest as t };