UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

763 lines (762 loc) 26.9 kB
import { c as normalizeOptionalString, p as readStringValue, s as normalizeOptionalLowercaseString } from "./string-coerce-mnp54Vah.js"; import { a as normalizeOptionalTrimmedStringList } from "./string-normalization-WNUDCpXX.js"; import { v as resolveSessionAgentId } from "./agent-scope-MrLta7Pq.js"; import { o as normalizeSessionKeyPreservingOpaquePeerIds, u as parseThreadSessionSuffix } from "./session-key-utils-Bx3apsJ3.js"; import { _ as selectApplicableRuntimeConfig, i as getRuntimeConfigSnapshot, s as getRuntimeConfigSourceSnapshot } from "./runtime-snapshot-D93_HOsR.js"; import "./operator-scopes-CS3xdS-V.js"; import { a as normalizeChannelId, t as getChannelPlugin } from "./registry-MkxNn1Ue.js"; import "./plugins-t2ejWcVy.js"; import "./sessions-D6zZvoxX.js"; import { t as appendAssistantMessageToSessionTranscript } from "./transcript-Cw-EfeYD.js"; import { r as getAgentScopedMediaLocalRoots } from "./local-roots-BqdtViGS.js"; import { t as buildOutboundSessionContext } from "./session-context-Bz96-75Y.js"; import { r as resolveOutboundChannelPlugin } from "./channel-resolution-azq0u3DK.js"; import { s as projectOutboundPayloadPlanForMirror, t as createOutboundPayloadPlan } from "./payloads-B4DTFTLz.js"; import { t as sendDurableMessageBatch } from "./send-DWoTcOuO.js"; import "./runtime-BiQQNgz2.js"; import { n as normalizePollInput } from "./polls-C-v11_tu.js"; import { r as resolveMessageChannelSelection } from "./channel-selection-MKj8pvsX.js"; import { n as resolveOutboundSessionRoute, t as ensureOutboundSessionEntry } from "./outbound-session-Cm9TjWfF.js"; import { a as maybeResolveIdLikeTarget } from "./target-resolver-CUW6ke8W.js"; import { i as resolveOutboundTarget } from "./targets-C4BNalx6.js"; import { in as errorShape, rn as ErrorCodes } from "./schema-BwaBORnA.js"; import { Lt as validateSendParams, jt as validatePollParams, st as validateMessageActionParams, t as formatValidationErrors } from "./src-oj0IwW6K.js"; import { l as resolveAttachmentMediaPolicy, r as hydrateAttachmentParamsForAction, t as dispatchChannelMessageAction } from "./message-action-dispatch-NCUowurX.js"; import { t as extractToolPayload } from "./tool-payload-DPsIQnf_.js"; import { t as formatForLog } from "./ws-log-DlzuStZb.js"; import { n as createOutboundSendDeps } from "./deps-CWqGdIJS.js"; import { t as resolveGatewayPluginConfig } from "./runtime-plugin-config-DPKmNeph.js"; //#region src/infra/outbound/source-reply-mirror.ts function readStringArray(value) { return normalizeOptionalTrimmedStringList(value); } function readFirstString(params, keys) { for (const key of keys) { const value = normalizeOptionalString(params[key]); if (value) return value; } } function resolveSourceReplyTarget(params) { return readFirstString(params, [ "target", "to", "channelId", "chatId" ]); } function resolveSourceReplyThreadId(params) { return readFirstString(params.actionParams, ["threadId", "messageThreadId"]); } function resolveThreadedSourceTarget(params, requestedTarget) { const threadId = resolveSourceReplyThreadId(params); if (!threadId) return requestedTarget; return normalizeOptionalString(getChannelPlugin(params.channel)?.threading?.resolveCurrentChannelId?.({ to: requestedTarget, threadId })) ?? requestedTarget; } function hasExplicitDeliveryFailure(payload) { if (!payload || typeof payload !== "object" || Array.isArray(payload)) return false; const record = payload; if (record.ok === false) return true; const status = normalizeOptionalLowercaseString(record.status); if (status === "failed" || status === "error") return true; const deliveryStatus = normalizeOptionalLowercaseString(record.deliveryStatus); return deliveryStatus === "failed" || deliveryStatus === "error"; } function isCurrentSourceConversation(params) { if (params.action !== "send") return false; if (!params.sessionKey?.trim()) return false; const currentChannel = normalizeOptionalLowercaseString(params.toolContext?.currentChannelProvider); if (!currentChannel || currentChannel !== normalizeOptionalLowercaseString(params.channel)) return false; const currentTarget = normalizeOptionalString(params.toolContext?.currentChannelId); if (!currentTarget) return false; const requestedTarget = resolveSourceReplyTarget(params.actionParams); if (!requestedTarget) return false; return requestedTarget === currentTarget || resolveThreadedSourceTarget(params, requestedTarget) === currentTarget; } /** Mirrors successful outbound source replies into the owning session transcript. */ async function mirrorDeliveredSourceReplyToTranscript(params) { if (hasExplicitDeliveryFailure(params.deliveredPayload)) return false; if (!isCurrentSourceConversation(params)) return false; const mirror = projectOutboundPayloadPlanForMirror(createOutboundPayloadPlan([{ text: readFirstString(params.actionParams, [ "message", "content", "text", "caption" ]) ?? "", mediaUrl: readFirstString(params.actionParams, [ "mediaUrl", "media", "path", "filePath", "fileUrl" ]), mediaUrls: readStringArray(params.actionParams.mediaUrls), presentation: params.actionParams.presentation, interactive: params.actionParams.interactive, channelData: params.actionParams.channelData }])); if (!mirror.text && mirror.mediaUrls.length === 0) return false; await appendAssistantMessageToSessionTranscript({ agentId: params.agentId, sessionKey: params.sessionKey, text: mirror.text, mediaUrls: mirror.mediaUrls.length ? mirror.mediaUrls : void 0, idempotencyKey: params.idempotencyKey, config: params.cfg }); return true; } //#endregion //#region src/gateway/server-methods/send.ts const inflightByContext = /* @__PURE__ */ new WeakMap(); const getInflightMap = (context) => { let inflight = inflightByContext.get(context); if (!inflight) { inflight = /* @__PURE__ */ new Map(); inflightByContext.set(context, inflight); } return inflight; }; function resolveGatewayInflightMap(params) { const cached = params.context.dedupe.get(params.dedupeKey); if (cached) return { kind: "cached", cached }; const inflightMap = getInflightMap(params.context); const inflight = inflightMap.get(params.dedupeKey); if (inflight) return { kind: "inflight", inflight }; return { kind: "ready", inflightMap }; } function resolveGatewayInflightRequest(params) { const idem = params.idempotencyKey; const dedupeKey = `${params.prefix}:${idem}`; const inflight = resolveGatewayInflightMap({ context: params.context, dedupeKey }); if (inflight.kind === "cached") { params.respond(inflight.cached.ok, inflight.cached.payload, inflight.cached.error, { cached: true }); return { kind: "handled", done: Promise.resolve() }; } if (inflight.kind === "inflight") return { kind: "handled", done: inflight.inflight.then((result) => { const meta = result.meta ? { ...result.meta, cached: true } : { cached: true }; params.respond(result.ok, result.payload, result.error, meta); }) }; return { kind: "ready", idem, dedupeKey, inflightMap: inflight.inflightMap }; } async function runGatewayInflightWork(params) { params.inflightMap.set(params.dedupeKey, params.work); try { const result = await params.work; params.respond(result.ok, result.payload, result.error, result.meta); } finally { params.inflightMap.delete(params.dedupeKey); } } async function resolveRequestedChannel(params) { const channelInput = readStringValue(params.requestChannel); const normalizedChannel = channelInput ? normalizeChannelId(channelInput) : null; if (channelInput && !normalizedChannel) { const normalizedInput = normalizeOptionalLowercaseString(channelInput) ?? ""; if (params.rejectWebchatAsInternalOnly && normalizedInput === "webchat") return { error: errorShape(ErrorCodes.INVALID_REQUEST, "unsupported channel: webchat (internal-only). Use `chat.send` for WebChat UI messages or choose a deliverable channel.") }; return { error: errorShape(ErrorCodes.INVALID_REQUEST, params.unsupportedMessage(channelInput)) }; } const sourceCfg = params.context.getRuntimeConfig(); const cfg = resolveGatewayPluginConfig({ config: sourceCfg }); let channel = normalizedChannel; if (!channel) try { channel = (await resolveMessageChannelSelection({ cfg })).channel; } catch (err) { return { error: errorShape(ErrorCodes.INVALID_REQUEST, String(err)) }; } return { cfg, sourceCfg, channel }; } async function resolveInternalDeliveryChannel(requestChannel, context) { const resolvedChannel = await resolveRequestedChannel({ requestChannel, unsupportedMessage: (input) => `unsupported channel: ${input}`, context, rejectWebchatAsInternalOnly: true }); if ("error" in resolvedChannel) return { kind: "failed", result: { ok: false, error: resolvedChannel.error } }; return { kind: "ready", ...resolvedChannel }; } function resolveGatewayOutboundTarget(params) { const resolved = resolveOutboundTarget({ channel: params.channel, to: params.to, cfg: params.cfg, accountId: params.accountId, mode: "explicit" }); if (!resolved.ok) return { ok: false, error: errorShape(ErrorCodes.INVALID_REQUEST, String(resolved.error)) }; return { ok: true, to: resolved.to }; } function resolveMessageActionRuntimeConfig(params) { const runtimeConfig = getRuntimeConfigSnapshot(); const runtimeSourceConfig = getRuntimeConfigSourceSnapshot(); if (!runtimeConfig || !runtimeSourceConfig) return params.cfg; const selected = selectApplicableRuntimeConfig({ inputConfig: params.sourceCfg, runtimeConfig, runtimeSourceConfig }); if (selected === runtimeConfig && selected !== params.cfg) return resolveGatewayPluginConfig({ config: selected }); return params.cfg; } function buildGatewayDeliveryPayload(params) { const payload = { runId: params.runId, messageId: params.result.messageId, channel: params.channel }; if ("chatId" in params.result) payload.chatId = params.result.chatId; if ("channelId" in params.result) payload.channelId = params.result.channelId; if ("toJid" in params.result) payload.toJid = params.result.toJid; if ("conversationId" in params.result) payload.conversationId = params.result.conversationId; if ("pollId" in params.result) payload.pollId = params.result.pollId; return payload; } function cacheGatewayDedupeSuccess(params) { params.context.dedupe.set(params.dedupeKey, { ts: Date.now(), ok: true, payload: params.payload }); } function cacheGatewayDedupeFailure(params) { params.context.dedupe.set(params.dedupeKey, { ts: Date.now(), ok: false, error: params.error }); } function createGatewayInflightSuccess(params) { cacheGatewayDedupeSuccess({ context: params.context, dedupeKey: params.dedupeKey, payload: params.payload }); return { ok: true, payload: params.payload, meta: { channel: params.channel } }; } function createGatewayDeliveryInflightSuccess(params) { return createGatewayInflightSuccess({ context: params.context, dedupeKey: params.dedupeKey, payload: buildGatewayDeliveryPayload({ runId: params.runId, channel: params.channel, result: params.result }), channel: params.channel }); } function createGatewayInflightUnavailableFailure(params) { const error = errorShape(ErrorCodes.UNAVAILABLE, String(params.err)); cacheGatewayDedupeFailure({ context: params.context, dedupeKey: params.dedupeKey, error }); return { ok: false, error, meta: { channel: params.channel, error: formatForLog(params.err) } }; } async function mirrorDeliveredSourceReplyToTranscriptBestEffort(params) { try { await mirrorDeliveredSourceReplyToTranscript(params.mirror); } catch (err) { params.context.logGateway?.warn?.("Source reply transcript mirror failed after delivery.", { error: formatForLog(err), channel: params.mirror.channel, sessionKey: params.mirror.sessionKey }); } } const sourceReplyTranscriptMirrorQueues = /* @__PURE__ */ new Map(); function resolveSourceReplyTranscriptMirrorQueueKey(mirror) { return mirror.sessionKey?.trim() || "__global__"; } function scheduleDeliveredSourceReplyTranscriptMirror(params) { const queueKey = resolveSourceReplyTranscriptMirrorQueueKey(params.mirror); const previous = sourceReplyTranscriptMirrorQueues.get(queueKey); const queued = (async () => { await previous?.catch(() => void 0); await mirrorDeliveredSourceReplyToTranscriptBestEffort(params); })(); sourceReplyTranscriptMirrorQueues.set(queueKey, queued); queued.finally(() => { if (sourceReplyTranscriptMirrorQueues.get(queueKey) === queued) sourceReplyTranscriptMirrorQueues.delete(queueKey); }).catch(() => void 0); return queued; } const sendHandlers = { "message.action": async ({ params, respond, context, client }) => { const p = params; if (!validateMessageActionParams(p)) { respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, `invalid message.action params: ${formatValidationErrors(validateMessageActionParams.errors)}`)); return; } const request = p; const inflight = resolveGatewayInflightRequest({ context, prefix: "message.action", idempotencyKey: request.idempotencyKey, respond }); if (inflight.kind === "handled") { await inflight.done; return; } const { dedupeKey, inflightMap } = inflight; await runGatewayInflightWork({ inflightMap, dedupeKey, work: (async () => { const resolvedChannel = await resolveRequestedChannel({ requestChannel: request.channel, unsupportedMessage: (input) => `unsupported channel: ${input}`, context, rejectWebchatAsInternalOnly: true }); if ("error" in resolvedChannel) return { ok: false, error: resolvedChannel.error }; const { cfg: selectedCfg, sourceCfg, channel } = resolvedChannel; const cfg = resolveMessageActionRuntimeConfig({ cfg: selectedCfg, sourceCfg }); if (!resolveOutboundChannelPlugin({ channel, cfg })?.actions?.handleAction) return { ok: false, error: errorShape(ErrorCodes.INVALID_REQUEST, `Channel ${channel} does not support action ${request.action}.`) }; try { const sessionKey = normalizeOptionalString(request.sessionKey) ?? void 0; const agentId = normalizeOptionalString(request.agentId) ?? (sessionKey ? resolveSessionAgentId({ sessionKey, config: cfg }) : void 0); const accountId = normalizeOptionalString(request.accountId) ?? void 0; if (request.action === "send") await hydrateAttachmentParamsForAction({ cfg, channel, accountId, args: request.params, action: "send", mediaPolicy: resolveAttachmentMediaPolicy({ mediaLocalRoots: getAgentScopedMediaLocalRoots(cfg, agentId) }) }); const gatewayClientScopes = client?.connect?.scopes ?? []; const handled = await dispatchChannelMessageAction({ channel, action: request.action, cfg, params: request.params, accountId, requesterSenderId: normalizeOptionalString(request.requesterSenderId) ?? void 0, senderIsOwner: gatewayClientScopes.includes("operator.admin") ? request.senderIsOwner === true : false, sessionKey, sessionId: normalizeOptionalString(request.sessionId) ?? void 0, inboundEventKind: request.inboundTurnKind, agentId, mediaLocalRoots: getAgentScopedMediaLocalRoots(cfg, agentId), toolContext: request.toolContext, dryRun: false, gatewayClientScopes }); if (!handled) { const error = errorShape(ErrorCodes.INVALID_REQUEST, `Message action ${request.action} not supported for channel ${channel}.`); cacheGatewayDedupeFailure({ context, dedupeKey, error }); return { ok: false, error, meta: { channel } }; } const payload = extractToolPayload(handled); await scheduleDeliveredSourceReplyTranscriptMirror({ context, mirror: { action: request.action, channel, actionParams: request.params, cfg, sessionKey, agentId, toolContext: request.toolContext, idempotencyKey: request.idempotencyKey, deliveredPayload: payload } }); return createGatewayInflightSuccess({ context, dedupeKey, payload, channel }); } catch (err) { return createGatewayInflightUnavailableFailure({ context, dedupeKey, channel, err }); } })(), respond }); }, send: async ({ params, respond, context, client }) => { const p = params; if (!validateSendParams(p)) { respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, `invalid send params: ${formatValidationErrors(validateSendParams.errors)}`)); return; } const request = p; const inflight = resolveGatewayInflightRequest({ context, prefix: "send", idempotencyKey: request.idempotencyKey, respond }); if (inflight.kind === "handled") { await inflight.done; return; } const { idem, dedupeKey, inflightMap } = inflight; const to = normalizeOptionalString(request.to) ?? ""; const message = normalizeOptionalString(request.message) ?? ""; const mediaUrl = normalizeOptionalString(request.mediaUrl); const mediaUrls = Array.isArray(request.mediaUrls) ? request.mediaUrls.map((entry) => normalizeOptionalString(entry)).filter((entry) => Boolean(entry)) : void 0; const buffer = readStringValue(request.buffer); if (!message && !mediaUrl && (mediaUrls?.length ?? 0) === 0 && !buffer) { respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, "invalid send params: text or media is required")); return; } const accountId = normalizeOptionalString(request.accountId); const replyToId = normalizeOptionalString(request.replyToId); const threadId = normalizeOptionalString(request.threadId); await runGatewayInflightWork({ inflightMap, dedupeKey, work: (async () => { const resolvedChannel = await resolveInternalDeliveryChannel(request.channel, context); if (resolvedChannel.kind !== "ready") return resolvedChannel.result; const { cfg, channel } = resolvedChannel; const outboundChannel = channel; if (!resolveOutboundChannelPlugin({ channel, cfg })) return { ok: false, error: errorShape(ErrorCodes.INVALID_REQUEST, `unsupported channel: ${channel}`) }; try { const resolvedTarget = resolveGatewayOutboundTarget({ channel: outboundChannel, to, cfg, accountId }); if (!resolvedTarget.ok) return { ok: false, error: resolvedTarget.error, meta: { channel } }; const idLikeTarget = await maybeResolveIdLikeTarget({ cfg, channel, input: resolvedTarget.to, accountId }); const deliveryTarget = idLikeTarget?.to ?? resolvedTarget.to; const providedSessionKey = normalizeSessionKeyPreservingOpaquePeerIds(request.sessionKey) || void 0; const explicitAgentId = normalizeOptionalString(request.agentId); const sessionAgentId = providedSessionKey ? resolveSessionAgentId({ sessionKey: providedSessionKey, config: cfg }) : void 0; const defaultAgentId = resolveSessionAgentId({ config: cfg }); const effectiveAgentId = explicitAgentId ?? sessionAgentId ?? defaultAgentId; const sendArgs = { mediaUrl, mediaUrls, buffer, filename: normalizeOptionalString(request.filename) ?? void 0, contentType: normalizeOptionalString(request.contentType) ?? void 0 }; await hydrateAttachmentParamsForAction({ cfg, channel, accountId, args: sendArgs, action: "send", mediaPolicy: resolveAttachmentMediaPolicy({ mediaLocalRoots: getAgentScopedMediaLocalRoots(cfg, effectiveAgentId) }) }); const hydratedMediaUrl = normalizeOptionalString(sendArgs.mediaUrl); const hydratedMediaUrls = Array.isArray(sendArgs.mediaUrls) ? sendArgs.mediaUrls.map((entry) => normalizeOptionalString(entry)).filter((entry) => Boolean(entry)) : void 0; const outboundDeps = context.deps ? createOutboundSendDeps(context.deps) : void 0; const outboundPayloads = [{ text: message, mediaUrl: hydratedMediaUrl, mediaUrls: hydratedMediaUrls, ...request.asVoice === true ? { audioAsVoice: true } : {} }]; const mirrorProjection = projectOutboundPayloadPlanForMirror(createOutboundPayloadPlan(outboundPayloads)); const mirrorText = mirrorProjection.text; const mirrorMediaUrls = mirrorProjection.mediaUrls; const derivedRoute = await resolveOutboundSessionRoute({ cfg, channel, agentId: effectiveAgentId, accountId, target: deliveryTarget, currentSessionKey: providedSessionKey, resolvedTarget: idLikeTarget, replyToId, threadId }); const providedSessionBaseKey = parseThreadSessionSuffix(providedSessionKey).baseSessionKey ?? providedSessionKey; const shouldUseDerivedThreadSessionKey = channel === "slack" && Boolean(providedSessionKey) && Boolean(normalizeOptionalString(derivedRoute?.threadId)) && normalizeOptionalLowercaseString(derivedRoute?.baseSessionKey) === normalizeOptionalLowercaseString(providedSessionBaseKey) && normalizeOptionalLowercaseString(derivedRoute?.sessionKey) !== providedSessionKey; const outboundRoute = derivedRoute ? providedSessionKey ? shouldUseDerivedThreadSessionKey ? { ...derivedRoute, baseSessionKey: derivedRoute.baseSessionKey ?? providedSessionKey } : { ...derivedRoute, sessionKey: providedSessionKey, baseSessionKey: providedSessionKey } : derivedRoute : null; if (outboundRoute) await ensureOutboundSessionEntry({ cfg, channel, accountId, route: outboundRoute }); const outboundSessionKey = outboundRoute?.sessionKey ?? providedSessionKey; const outboundSession = buildOutboundSessionContext({ cfg, agentId: effectiveAgentId, sessionKey: outboundSessionKey, conversationType: outboundRoute?.chatType }); const send = await sendDurableMessageBatch({ cfg, channel: outboundChannel, to: deliveryTarget, accountId, payloads: outboundPayloads, replyToId: replyToId ?? null, session: outboundSession, gifPlayback: request.gifPlayback, forceDocument: request.forceDocument, threadId: outboundRoute?.threadId ?? threadId ?? null, deps: outboundDeps, gatewayClientScopes: client?.connect?.scopes ?? [], silent: request.silent, formatting: request.parseMode ? { parseMode: request.parseMode } : void 0, mirror: outboundSessionKey ? { sessionKey: outboundSessionKey, agentId: effectiveAgentId, text: mirrorText || message, mediaUrls: mirrorMediaUrls.length > 0 ? mirrorMediaUrls : void 0, idempotencyKey: idem } : void 0 }); if (send.status === "failed" || send.status === "partial_failed") throw send.error; const result = (send.status === "sent" ? send.results : []).at(-1); if (!result) throw new Error("No delivery result"); return createGatewayDeliveryInflightSuccess({ context, dedupeKey, runId: idem, channel, result }); } catch (err) { return createGatewayInflightUnavailableFailure({ context, dedupeKey, channel, err }); } })(), respond }); }, poll: async ({ params, respond, context, client }) => { const p = params; if (!validatePollParams(p)) { respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, `invalid poll params: ${formatValidationErrors(validatePollParams.errors)}`)); return; } const request = p; const inflight = resolveGatewayInflightRequest({ context, prefix: "poll", idempotencyKey: request.idempotencyKey, respond }); if (inflight.kind === "handled") { await inflight.done; return; } const { idem, dedupeKey, inflightMap } = inflight; await runGatewayInflightWork({ inflightMap, dedupeKey, work: (async () => { const resolvedChannel = await resolveRequestedChannel({ requestChannel: request.channel, unsupportedMessage: (input) => `unsupported poll channel: ${input}`, context }); if ("error" in resolvedChannel) return { ok: false, error: resolvedChannel.error }; const { cfg, channel } = resolvedChannel; const outbound = resolveOutboundChannelPlugin({ channel, cfg })?.outbound; if (typeof request.durationSeconds === "number" && outbound?.supportsPollDurationSeconds !== true) return { ok: false, error: errorShape(ErrorCodes.INVALID_REQUEST, `durationSeconds is not supported for ${channel} polls`) }; if (typeof request.isAnonymous === "boolean" && outbound?.supportsAnonymousPolls !== true) return { ok: false, error: errorShape(ErrorCodes.INVALID_REQUEST, `isAnonymous is not supported for ${channel} polls`) }; const poll = { question: request.question, options: request.options, maxSelections: request.maxSelections, durationSeconds: request.durationSeconds, durationHours: request.durationHours }; const threadId = normalizeOptionalString(request.threadId); const accountId = normalizeOptionalString(request.accountId); try { if (!outbound?.sendPoll) return { ok: false, error: errorShape(ErrorCodes.INVALID_REQUEST, `unsupported poll channel: ${channel}`) }; const resolvedTarget = resolveGatewayOutboundTarget({ channel, to: request.to.trim(), cfg, accountId }); if (!resolvedTarget.ok) return { ok: false, error: resolvedTarget.error }; const normalized = outbound.pollMaxOptions ? normalizePollInput(poll, { maxOptions: outbound.pollMaxOptions }) : normalizePollInput(poll); const payload = buildGatewayDeliveryPayload({ runId: idem, channel, result: await outbound.sendPoll({ cfg, to: resolvedTarget.to, poll: normalized, accountId, threadId, silent: request.silent, isAnonymous: request.isAnonymous, gatewayClientScopes: client?.connect?.scopes ?? [] }) }); cacheGatewayDedupeSuccess({ context, dedupeKey, payload }); return { ok: true, payload, meta: { channel } }; } catch (err) { const error = errorShape(ErrorCodes.UNAVAILABLE, String(err)); cacheGatewayDedupeFailure({ context, dedupeKey, error }); return { ok: false, error, meta: { channel, error: formatForLog(err) } }; } })(), respond }); } }; //#endregion export { sendHandlers };