UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

312 lines (311 loc) • 11.8 kB
import { E as createDiagnosticTraceContextFromActiveScope, P as runWithDiagnosticTraceContext } from "./diagnostic-events-BflWr4hm.mjs"; import { t as createSubsystemLogger } from "./subsystem-BlrgtxyM.mjs"; import { n as deliveryContextKey, s as normalizeDeliveryContext } from "./delivery-context.shared-ZxnjUaKl.mjs"; import { t as deliveryContextFromSession } from "./delivery-context.read-CR06zOJ4.mjs"; import { c as loadSessionEntryReadOnly } from "./session-accessor.sqlite-entry-BuCmtpuj.mjs"; import { f as clearChannelHistoryIfEnabled } from "./history-mAfsnQBJ.mjs"; import "./session-accessor-BM3Wa1Bx.mjs"; import { c as updateSessionEntry } from "./session-accessor.reset-BDT6VRzw.mjs"; import { t as appendAssistantMessageToSessionTranscript } from "./transcript-D08D0EMY.mjs"; import { c as findDeliveryIntentOwner } from "./delivery-queue-storage-egXRkEGm.mjs"; import { r as getGatewayRecoveryRuntime } from "./server-recovery-runtime-context-DjDoE_dq.mjs"; import { n as withDispatchProcessedOutcomeSink } from "./dispatch-processed-outcome-CM-0_qe5.mjs"; import { t as isRecentOutboundMessageIdentity } from "./outbound-echo-D5b8Q9mA.mjs"; import { r as hasVisibleChannelTurnDispatch, t as EMPTY_CHANNEL_TURN_DISPATCH_COUNTS } from "./dispatch-result-B75usq__.mjs"; import { a as resolvePairLoopGuardSettings, r as createPairLoopGuard } from "./pair-loop-guard-runtime-Df7mJxV6.mjs"; //#region src/channels/turn/bot-loop-protection.ts const channelBotPairLoopGuard = createPairLoopGuard({ pruneIntervalMs: 6e4 }); /** Records a bot pair interaction and returns whether the loop guard should suppress it. */ function recordChannelBotPairLoopAndCheckSuppression(params) { return channelBotPairLoopGuard.recordAndCheck({ scopeId: params.scopeId, conversationId: params.conversationId, senderId: params.senderId, receiverId: params.receiverId, eventId: params.eventId, settings: resolvePairLoopGuardSettings({ config: params.config, defaultsConfig: params.defaultsConfig, defaultEnabled: params.defaultEnabled }), nowMs: params.nowMs }); } //#endregion //#region src/channels/turn/pending-delivery-notice.ts const PENDING_DELIVERY_NOTICE = "I couldn’t confirm whether my previous reply reached this chat, so I won’t resend it automatically. Please ask for any missing remainder."; function noticeId(intentId) { return `main-session-restart-recovery:pending-final:${intentId}`; } async function deliverPendingDeliveryNotice(sessionKey, storePath) { const entry = loadSessionEntryReadOnly({ sessionKey, storePath, readConsistency: "latest", hydrateSkillPromptRefs: false }); const notice = entry?.pendingDeliveryNotice; const context = normalizeDeliveryContext(notice?.context); const runtime = getGatewayRecoveryRuntime(); if (!entry || !runtime || !notice || notice.state !== "owed" || !context?.channel || !context.to || deliveryContextKey(context) !== deliveryContextKey(deliveryContextFromSession(entry))) return; const idempotencyKey = noticeId(notice.intentId); let delivered; try { delivered = !(await runtime.sendRecoveryNotice({ channel: context.channel, to: context.to, accountId: context.accountId, threadId: context.threadId, text: PENDING_DELIVERY_NOTICE, idempotencyKey })).suppressed; } catch { const owner = await findDeliveryIntentOwner(idempotencyKey); if (owner?.status !== "completed" && owner?.status !== "failed") return; delivered = owner.status === "completed"; } if (delivered && !(await appendAssistantMessageToSessionTranscript({ sessionKey, storePath, expectedSessionId: entry.sessionId, text: PENDING_DELIVERY_NOTICE, idempotencyKey })).ok) return; await updateSessionEntry({ sessionKey, storePath }, (current) => current.sessionId === entry.sessionId && current.pendingDeliveryNotice?.intentId === notice.intentId && current.pendingDeliveryNotice.state !== "acknowledged" ? { pendingDeliveryNotice: { ...current.pendingDeliveryNotice, state: delivered ? "acknowledged" : "unresolved" }, updatedAt: Date.now() } : null, { skipMaintenance: true, takeCacheOwnership: true }); } //#endregion //#region src/channels/turn/execution.ts const NO_ADDITIONAL_DELIVERY_SIGNALS = {}; const log = createSubsystemLogger("channels/turn/execution"); function emit(params, event) { params.log?.({ channel: params.channel, accountId: params.accountId, messageId: params.messageId, sessionKey: params.ctxPayload.SessionKey ?? params.routeSessionKey, admission: params.admission?.kind ?? "dispatch", ...event }); } function clearPendingHistoryAfterTurn(params) { if (!params?.isGroup || !params.historyKey || !params.historyMap || params.limit === void 0) return; clearChannelHistoryIfEnabled({ historyMap: params.historyMap, historyKey: params.historyKey, limit: params.limit }); } function resolveObserveOnlyDispatchResult(params) { return params.observeOnlyDispatchResult ?? { queuedFinal: false, counts: EMPTY_CHANNEL_TURN_DISPATCH_COUNTS }; } function resolveRecordSessionKey(params) { const explicitSessionKey = params.record?.sessionKey; if (explicitSessionKey === void 0) return params.ctxPayload.SessionKey ?? params.routeSessionKey; const normalizedSessionKey = explicitSessionKey.trim(); if (!normalizedSessionKey) throw new Error("Channel turn record.sessionKey must be non-empty."); if (normalizedSessionKey !== explicitSessionKey) throw new Error("Channel turn record.sessionKey must not include surrounding whitespace."); return explicitSessionKey; } function maybeWarnZeroCountVisibleDispatch(params) { if (params.admission?.kind === "observeOnly" || params.ctxPayload.InternalTurnSource !== void 0) return; const dispatchResult = params.dispatchResult; if (dispatchResult?.deferredToActiveRun) return; if (hasVisibleChannelTurnDispatch(dispatchResult, NO_ADDITIONAL_DELIVERY_SIGNALS)) return; const processed = params.processedOutcome; const cause = processed ? `${processed.outcome}${processed.reason ? `:${processed.reason}` : ""}` : void 0; log.warn(`visible channel turn dispatched with no queued reply payloads: channel=${params.channel} messageId=${params.messageId ?? "unknown"} sessionKey=${params.ctxPayload.SessionKey ?? params.routeSessionKey} cause=${cause ?? "unknown"}`); emit(params, { stage: "dispatch", event: "warning", reason: "zero-count-visible-dispatch" }); } function resolveBotLoopProtectionDrop(params) { if (!params.botLoopProtection) return; if (!recordChannelBotPairLoopAndCheckSuppression(params.botLoopProtection).suppressed) return; const admission = { kind: "drop", reason: "bot-loop-protection" }; emit(params, { stage: "authorize", event: "drop", admission: admission.kind, reason: admission.reason }); return { admission, dispatched: false, ctxPayload: params.ctxPayload, routeSessionKey: params.routeSessionKey }; } function resolveOutboundEchoDrop(params) { const conversationId = [params.ctxPayload.NativeChannelId, params.ctxPayload.ChatId].find((value) => typeof value === "string" && value.trim().length > 0); if (!conversationId) return; const matchedMessageId = [ params.messageId, params.ctxPayload.MessageSidFull, params.ctxPayload.MessageSid ].find((messageId) => typeof messageId === "string" && isRecentOutboundMessageIdentity({ channel: params.channel, accountId: params.accountId, conversationId, messageId })); const sourceId = params.outboundEchoSourceId?.trim(); const matchesSource = sourceId ? isRecentOutboundMessageIdentity({ channel: params.channel, accountId: params.accountId, conversationId, sourceId }) : false; if (!matchedMessageId && !matchesSource) return; const admission = { kind: "drop", reason: "outbound-echo" }; emit(params, { stage: "authorize", event: "drop", messageId: params.messageId ?? matchedMessageId, admission: admission.kind, reason: admission.reason }); return { admission, dispatched: false, ctxPayload: params.ctxPayload, routeSessionKey: params.routeSessionKey }; } async function runPreparedChannelTurnCore(params, options) { const trace = createDiagnosticTraceContextFromActiveScope(); return await runWithDiagnosticTraceContext(trace, () => runPreparedChannelTurnCoreInTrace(params, options)); } async function runPreparedChannelTurnCoreInTrace(params, options) { const admission = params.admission ?? { kind: "dispatch" }; const outboundEchoDrop = resolveOutboundEchoDrop(params); if (outboundEchoDrop) { clearPendingHistoryAfterTurn(params.history); await params.runDispatchLifecycle?.onDispatchSkipped("outboundEcho"); return outboundEchoDrop; } const botLoopDrop = resolveBotLoopProtectionDrop(params); if (botLoopDrop) { clearPendingHistoryAfterTurn(params.history); await params.runDispatchLifecycle?.onDispatchSkipped("botLoopProtection"); return botLoopDrop; } try { const recordSessionKey = resolveRecordSessionKey(params); if (params.ctxPayload.SessionTranscriptContext) { const { mergeSessionTranscriptContext } = await import("./session-transcript-context.runtime.js"); await mergeSessionTranscriptContext({ agentId: params.ctxPayload.AgentId, ctx: params.ctxPayload, sessionKey: recordSessionKey, storePath: params.storePath }); } emit(params, { stage: "record", event: "start", sessionKey: recordSessionKey, admission: admission.kind }); try { await params.recordInboundSession({ storePath: params.storePath, sessionKey: recordSessionKey, ctx: params.ctxPayload, groupResolution: params.record?.groupResolution, createIfMissing: params.record?.createIfMissing, updateLastRoute: params.record?.updateLastRoute, onRecordError: params.record?.onRecordError ?? (() => void 0), trackSessionMetaTask: params.record?.trackSessionMetaTask }); emit(params, { stage: "record", event: "done", sessionKey: recordSessionKey, admission: admission.kind }); await params.afterRecord?.(); await deliverPendingDeliveryNotice(recordSessionKey, params.storePath); } catch (err) { emit(params, { stage: "record", event: "error", sessionKey: recordSessionKey, admission: admission.kind, error: err }); try { await params.onPreDispatchFailure?.(err); } catch {} throw err; } emit(params, { stage: "dispatch", event: "start", admission: admission.kind }); let dispatchResult; try { if (admission.kind === "observeOnly" && !options.suppressObserveOnlyDispatch) await params.runDispatch(); else if (admission.kind === "observeOnly") await params.runDispatchLifecycle?.onDispatchSkipped("observeOnly"); let processedOutcome; if (admission.kind === "observeOnly") dispatchResult = resolveObserveOnlyDispatchResult(params); else ({result: dispatchResult, processedOutcome} = await withDispatchProcessedOutcomeSink(() => params.runDispatch())); maybeWarnZeroCountVisibleDispatch({ ...params, admission, dispatchResult, processedOutcome }); } catch (err) { emit(params, { stage: "dispatch", event: "error", admission: admission.kind, error: err }); throw err; } emit(params, { stage: "dispatch", event: "done", admission: admission.kind }); return { admission, dispatched: true, ctxPayload: params.ctxPayload, routeSessionKey: params.routeSessionKey, dispatchResult }; } finally { clearPendingHistoryAfterTurn(params.history); } } async function runPreparedChannelTurn(params) { return await runPreparedChannelTurnCore(params, { suppressObserveOnlyDispatch: true }); } //#endregion export { runPreparedChannelTurnCore as n, recordChannelBotPairLoopAndCheckSuppression as r, runPreparedChannelTurn as t };