UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

302 lines (301 loc) • 13 kB
import { E as createDiagnosticTraceContextFromActiveScope, P as runWithDiagnosticTraceContext } from "./diagnostic-events-DT4ejwoa.mjs"; import { t as createSubsystemLogger } from "./subsystem-4NzR43uz.mjs"; import "./session-key-DY3MOX4N.mjs"; import { h as parseAgentSessionKey } from "./session-key-utils-CDyBC9Gn.mjs"; import { l as deliveryContextFromSession } from "./store-entry-ClyG-tia.mjs"; import { n as deliveryContextKey, s as normalizeDeliveryContext } from "./delivery-context.shared-BYdFKp9k.mjs"; import { t as getPluginServiceSchedulerBinding } from "./service-scheduler-binding-CXvCAS6L.mjs"; import { t as getAgentDatabaseStartupAdmission } from "./agent-database-startup-BYk3phjn.mjs"; import { f as clearChannelHistoryIfEnabled } from "./history-C_lABG5e.mjs"; import { s as loadSessionEntryReadOnly } from "./session-accessor.sqlite-entry-DLa7_9ui.mjs"; import "./session-accessor-C3ljJ974.mjs"; import { l as updateSessionEntry } from "./session-accessor.entry-mutation-telsi3Ig.mjs"; import { t as appendAssistantMessageToSessionTranscript } from "./transcript-CpqWjSbD.mjs"; import { c as findDeliveryIntentOwner } from "./delivery-queue-storage-DyY4NlIw.mjs"; import { r as getGatewayRecoveryRuntime } from "./server-recovery-runtime-context-DEgRJSCI.mjs"; import { n as withDispatchProcessedOutcomeSink } from "./dispatch-processed-outcome-BSdYjymX.mjs"; import { t as isRecentOutboundMessageIdentity } from "./outbound-echo-Br9oQQVA.mjs"; import { i as resolvePairLoopGuardSettings, n as createPairLoopGuard } from "./pair-loop-guard-runtime-Bkkb_K7u.mjs"; //#region src/channels/turn/bot-loop-protection.ts const channelBotPairLoopGuard = createPairLoopGuard({ pruneIntervalMs: 6e4 }); 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/dispatch-result.ts const hasFinalSignal = (signals) => signals.fallbackDelivered === true || signals.deliverySummaryDelivered === true; const hasVisibleSignal = (result, signals) => result?.observedReplyDelivery === true || signals.observedReplyDelivery === true || hasFinalSignal(signals); const EMPTY_CHANNEL_TURN_DISPATCH_COUNTS = { tool: 0, block: 0, final: 0 }; function hasVisibleChannelTurnDispatchFromReceipt(result, signals = {}) { return result?.settledReceipt?.anyVisibleDelivered === true || hasVisibleSignal(result, signals); } function resolveChannelTurnDispatchCounts(result) { const counts = result?.settledReceipt?.counts; return counts ? { tool: counts.tool?.delivered ?? 0, block: counts.block?.delivered ?? 0, final: counts.final?.delivered ?? 0 } : { ...EMPTY_CHANNEL_TURN_DISPATCH_COUNTS, ...result?.counts }; } function hasVisibleChannelTurnDispatch(result, signals = {}) { if (result?.settledReceipt) return hasVisibleChannelTurnDispatchFromReceipt(result, signals); return hasVisibleSignal(result, signals) || result?.queuedFinal === true || Object.values(resolveChannelTurnDispatchCounts(result)).some((count) => count > 0); } function hasFinalChannelTurnDispatch(result, signals = {}) { const finalCounts = result?.settledReceipt?.counts.final; return hasFinalSignal(signals) || (result?.settledReceipt ? (finalCounts?.delivered ?? 0) > 0 || (finalCounts?.failedAfterSend ?? 0) > 0 : result?.queuedFinal === true || resolveChannelTurnDispatchCounts(result).final > 0); } //#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."; 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 = `main-session-restart-recovery:pending-final:${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 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 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)) 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 dropPreparedChannelTurn(params, reason, messageId = params.messageId) { const admission = { kind: "drop", reason }; emit(params, { stage: "authorize", event: "drop", messageId, 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; return dropPreparedChannelTurn(params, "outbound-echo", params.messageId ?? matchedMessageId); } 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; } if (params.botLoopProtection && recordChannelBotPairLoopAndCheckSuppression(params.botLoopProtection).suppressed) { const botLoopDrop = dropPreparedChannelTurn(params, "bot-loop-protection"); clearPendingHistoryAfterTurn(params.history); await params.runDispatchLifecycle?.onDispatchSkipped("botLoopProtection"); return botLoopDrop; } try { const recordSessionKey = resolveRecordSessionKey(params); const emitStage = (stage, event, error) => emit(params, { stage, event, ...stage === "record" ? { sessionKey: recordSessionKey } : {}, admission: admission.kind, ...event === "error" ? { error } : {} }); try { const agentId = params.ctxPayload.AgentId ?? parseAgentSessionKey(params.routeSessionKey)?.agentId; if (agentId) await getAgentDatabaseStartupAdmission()?.waitForAgentPreparation(agentId, { signal: getPluginServiceSchedulerBinding()?.().signal }); 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 }); } emitStage("record", "start"); 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 }); emitStage("record", "done"); await params.afterRecord?.(); await deliverPendingDeliveryNotice(recordSessionKey, params.storePath); } catch (err) { emitStage("record", "error", err); try { await params.onPreDispatchFailure?.(err); } catch {} throw err; } emitStage("dispatch", "start"); let dispatchResult; try { let processedOutcome; if (admission.kind === "observeOnly") { if (options.suppressObserveOnlyDispatch) await params.runDispatchLifecycle?.onDispatchSkipped("observeOnly"); else await params.runDispatch(); dispatchResult = params.observeOnlyDispatchResult ?? { queuedFinal: false, counts: EMPTY_CHANNEL_TURN_DISPATCH_COUNTS }; } else ({result: dispatchResult, processedOutcome} = await withDispatchProcessedOutcomeSink(() => params.runDispatch())); maybeWarnZeroCountVisibleDispatch({ ...params, admission, dispatchResult, processedOutcome }); } catch (err) { emitStage("dispatch", "error", err); throw err; } emitStage("dispatch", "done"); 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 { resolveChannelTurnDispatchCounts as a, hasVisibleChannelTurnDispatch as i, runPreparedChannelTurnCore as n, recordChannelBotPairLoopAndCheckSuppression as o, hasFinalChannelTurnDispatch as r, runPreparedChannelTurn as t };