diff --git a/CHANGELOG.md b/CHANGELOG.md index daff7f3a1..083967884 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,7 @@ ### Fixes - Sub-agents: normalize announce delivery origin + queue bucketing by accountId to keep multi-account routing stable. (#1061, #1058) — thanks @adam91holt. +- Sessions: include deliveryContext in sessions.list and reuse normalized delivery routing for announce/restart fallbacks. (#1058) - Gateway: honor explicit delivery targets without implicit accountId fallback; preserve lastAccountId for implicit routing. - Gateway: avoid reusing last-to/accountId when the requested channel differs; sync deliveryContext with last route fields. - Repo: fix oxlint config filename and move ignore pattern into config. (#1064) — thanks @connorshea. diff --git a/src/agents/tools/sessions-announce-target.test.ts b/src/agents/tools/sessions-announce-target.test.ts index bc34eecd0..c405d03a6 100644 --- a/src/agents/tools/sessions-announce-target.test.ts +++ b/src/agents/tools/sessions-announce-target.test.ts @@ -26,9 +26,11 @@ describe("resolveAnnounceTarget", () => { sessions: [ { key: "agent:main:whatsapp:group:123@g.us", - lastChannel: "whatsapp", - lastTo: "123@g.us", - lastAccountId: "work", + deliveryContext: { + channel: "whatsapp", + to: "123@g.us", + accountId: "work", + }, }, ], }); diff --git a/src/agents/tools/sessions-announce-target.ts b/src/agents/tools/sessions-announce-target.ts index 06cc4965a..ed993788f 100644 --- a/src/agents/tools/sessions-announce-target.ts +++ b/src/agents/tools/sessions-announce-target.ts @@ -33,9 +33,19 @@ export async function resolveAnnounceTarget(params: { sessions.find((entry) => entry?.key === params.sessionKey) ?? sessions.find((entry) => entry?.key === params.displayKey); - const channel = typeof match?.lastChannel === "string" ? match.lastChannel : undefined; - const to = typeof match?.lastTo === "string" ? match.lastTo : undefined; - const accountId = typeof match?.lastAccountId === "string" ? match.lastAccountId : undefined; + const deliveryContext = + match?.deliveryContext && typeof match.deliveryContext === "object" + ? (match.deliveryContext as Record) + : undefined; + const channel = + (typeof deliveryContext?.channel === "string" ? deliveryContext.channel : undefined) ?? + (typeof match?.lastChannel === "string" ? match.lastChannel : undefined); + const to = + (typeof deliveryContext?.to === "string" ? deliveryContext.to : undefined) ?? + (typeof match?.lastTo === "string" ? match.lastTo : undefined); + const accountId = + (typeof deliveryContext?.accountId === "string" ? deliveryContext.accountId : undefined) ?? + (typeof match?.lastAccountId === "string" ? match.lastAccountId : undefined); if (channel && to) return { channel, to, accountId }; } catch { // ignore diff --git a/src/agents/tools/sessions-list-tool.ts b/src/agents/tools/sessions-list-tool.ts index dc8bae6de..ad50fc0b0 100644 --- a/src/agents/tools/sessions-list-tool.ts +++ b/src/agents/tools/sessions-list-tool.ts @@ -167,9 +167,21 @@ export function createSessionsListTool(opts?: { }); const entryChannel = typeof entry.channel === "string" ? entry.channel : undefined; - const lastChannel = typeof entry.lastChannel === "string" ? entry.lastChannel : undefined; + const deliveryContext = + entry.deliveryContext && typeof entry.deliveryContext === "object" + ? (entry.deliveryContext as Record) + : undefined; + const deliveryChannel = + typeof deliveryContext?.channel === "string" ? deliveryContext.channel : undefined; + const deliveryTo = + typeof deliveryContext?.to === "string" ? deliveryContext.to : undefined; + const deliveryAccountId = + typeof deliveryContext?.accountId === "string" ? deliveryContext.accountId : undefined; + const lastChannel = + deliveryChannel ?? (typeof entry.lastChannel === "string" ? entry.lastChannel : undefined); const lastAccountId = - typeof entry.lastAccountId === "string" ? entry.lastAccountId : undefined; + deliveryAccountId ?? + (typeof entry.lastAccountId === "string" ? entry.lastAccountId : undefined); const derivedChannel = deriveChannel({ key, kind, @@ -201,7 +213,7 @@ export function createSessionsListTool(opts?: { typeof entry.abortedLastRun === "boolean" ? entry.abortedLastRun : undefined, sendPolicy: typeof entry.sendPolicy === "string" ? entry.sendPolicy : undefined, lastChannel, - lastTo: typeof entry.lastTo === "string" ? entry.lastTo : undefined, + lastTo: deliveryTo ?? (typeof entry.lastTo === "string" ? entry.lastTo : undefined), lastAccountId, transcriptPath, }; diff --git a/src/auto-reply/reply/session.ts b/src/auto-reply/reply/session.ts index 3e6ff9f16..d6751b135 100644 --- a/src/auto-reply/reply/session.ts +++ b/src/auto-reply/reply/session.ts @@ -201,9 +201,10 @@ export async function initSessionState(params: { const baseEntry = !isNewSession && freshEntry ? entry : undefined; // Track the originating channel/to for announce routing (subagent announce-back). const lastChannelRaw = - (ctx.OriginatingChannel as string | undefined)?.trim() || baseEntry?.lastChannel; - const lastToRaw = ctx.OriginatingTo?.trim() || ctx.To?.trim() || baseEntry?.lastTo; - const lastAccountIdRaw = ctx.AccountId?.trim() || baseEntry?.lastAccountId; + (ctx.OriginatingChannel as string | undefined) || baseEntry?.lastChannel; + const lastToRaw = (ctx.OriginatingTo as string | undefined) || ctx.To || baseEntry?.lastTo; + const lastAccountIdRaw = + (ctx.AccountId as string | undefined) || baseEntry?.lastAccountId; const deliveryFields = normalizeSessionDeliveryFields({ deliveryContext: { channel: lastChannelRaw, diff --git a/src/commands/agent.delivery.test.ts b/src/commands/agent.delivery.test.ts index 8eabd336a..273fbb984 100644 --- a/src/commands/agent.delivery.test.ts +++ b/src/commands/agent.delivery.test.ts @@ -20,9 +20,15 @@ vi.mock("../infra/outbound/deliver.js", () => ({ deliverOutboundPayloads: mocks.deliverOutboundPayloads, })); -vi.mock("../infra/outbound/targets.js", () => ({ - resolveOutboundTarget: mocks.resolveOutboundTarget, -})); +vi.mock("../infra/outbound/targets.js", async () => { + const actual = await vi.importActual( + "../infra/outbound/targets.js", + ); + return { + ...actual, + resolveOutboundTarget: mocks.resolveOutboundTarget, + }; +}); describe("deliverAgentCommandResult", () => { beforeEach(() => { @@ -178,4 +184,39 @@ describe("deliverAgentCommandResult", () => { expect.objectContaining({ accountId: undefined, channel: "whatsapp" }), ); }); + + it("uses session last channel when none is provided", async () => { + const cfg = {} as ClawdbotConfig; + const deps = {} as CliDeps; + const runtime = { + log: vi.fn(), + error: vi.fn(), + } as unknown as RuntimeEnv; + const sessionEntry = { + lastChannel: "telegram", + lastTo: "123", + } as SessionEntry; + const result = { + payloads: [{ text: "hi" }], + meta: {}, + }; + + const { deliverAgentCommandResult } = await import("./agent/delivery.js"); + await deliverAgentCommandResult({ + cfg, + deps, + runtime, + opts: { + message: "hello", + deliver: true, + }, + sessionEntry, + result, + payloads: result.payloads, + }); + + expect(mocks.resolveOutboundTarget).toHaveBeenCalledWith( + expect.objectContaining({ channel: "telegram", to: "123" }), + ); + }); }); diff --git a/src/commands/agent/delivery.ts b/src/commands/agent/delivery.ts index 5fb97c3a2..32335c902 100644 --- a/src/commands/agent/delivery.ts +++ b/src/commands/agent/delivery.ts @@ -1,6 +1,4 @@ import { getChannelPlugin, normalizeChannelId } from "../../channels/plugins/index.js"; -import type { ChannelOutboundTargetMode } from "../../channels/plugins/types.js"; -import { DEFAULT_CHAT_CHANNEL } from "../../channels/registry.js"; import { createOutboundSendDeps, type CliDeps } from "../../cli/outbound-send-deps.js"; import type { ClawdbotConfig } from "../../config/config.js"; import type { SessionEntry } from "../../config/sessions.js"; @@ -12,34 +10,16 @@ import { normalizeOutboundPayloads, normalizeOutboundPayloadsForJson, } from "../../infra/outbound/payloads.js"; +import { resolveAgentDeliveryPlan } from "../../infra/outbound/agent-delivery.js"; import { resolveOutboundTarget } from "../../infra/outbound/targets.js"; import type { RuntimeEnv } from "../../runtime.js"; -import { - isInternalMessageChannel, - resolveGatewayMessageChannel, -} from "../../utils/message-channel.js"; -import { normalizeAccountId } from "../../utils/account-id.js"; -import { deliveryContextFromSession } from "../../utils/delivery-context.js"; +import { isInternalMessageChannel } from "../../utils/message-channel.js"; import type { AgentCommandOpts } from "./types.js"; type RunResult = Awaited< ReturnType<(typeof import("../../agents/pi-embedded.js"))["runEmbeddedPiAgent"]> >; -function resolveDeliveryAccountId(params: { - opts: AgentCommandOpts; - sessionEntry?: SessionEntry; - targetMode: ChannelOutboundTargetMode; - deliveryChannel?: string; -}) { - const sessionOrigin = deliveryContextFromSession(params.sessionEntry); - const explicit = normalizeAccountId(params.opts.accountId); - if (explicit || params.targetMode !== "implicit") return explicit; - if (!params.deliveryChannel || isInternalMessageChannel(params.deliveryChannel)) return undefined; - if (sessionOrigin?.channel !== params.deliveryChannel) return undefined; - return normalizeAccountId(sessionOrigin?.accountId); -} - export async function deliverAgentCommandResult(params: { cfg: ClawdbotConfig; deps: CliDeps; @@ -52,7 +32,14 @@ export async function deliverAgentCommandResult(params: { const { cfg, deps, runtime, opts, sessionEntry, payloads, result } = params; const deliver = opts.deliver === true; const bestEffortDeliver = opts.bestEffortDeliver === true; - const deliveryChannel = resolveGatewayMessageChannel(opts.channel) ?? DEFAULT_CHAT_CHANNEL; + const deliveryPlan = resolveAgentDeliveryPlan({ + sessionEntry, + requestedChannel: opts.channel, + explicitTo: opts.to, + accountId: opts.accountId, + wantsDelivery: deliver, + }); + const deliveryChannel = deliveryPlan.resolvedChannel; // Channel docking: delivery channels are resolved via plugin registry. const deliveryPlugin = !isInternalMessageChannel(deliveryChannel) ? getChannelPlugin(normalizeChannelId(deliveryChannel) ?? deliveryChannel) @@ -61,19 +48,14 @@ export async function deliverAgentCommandResult(params: { const isDeliveryChannelKnown = isInternalMessageChannel(deliveryChannel) || Boolean(deliveryPlugin); - const targetMode: ChannelOutboundTargetMode = - opts.deliveryTargetMode ?? (opts.to ? "explicit" : "implicit"); - const resolvedAccountId = resolveDeliveryAccountId({ - opts, - sessionEntry, - targetMode, - deliveryChannel, - }); + const targetMode = + opts.deliveryTargetMode ?? deliveryPlan.deliveryTargetMode ?? (opts.to ? "explicit" : "implicit"); + const resolvedAccountId = deliveryPlan.resolvedAccountId; const resolvedTarget = deliver && isDeliveryChannelKnown && deliveryChannel ? resolveOutboundTarget({ channel: deliveryChannel, - to: opts.to, + to: deliveryPlan.resolvedTo, cfg, accountId: resolvedAccountId, mode: targetMode, diff --git a/src/config/sessions/store.ts b/src/config/sessions/store.ts index b2639addf..3544cdcf0 100644 --- a/src/config/sessions/store.ts +++ b/src/config/sessions/store.ts @@ -332,24 +332,19 @@ export async function updateLastRoute(params: { const store = loadSessionStore(storePath); const existing = store[sessionKey]; const now = Date.now(); - const trimmedAccountId = accountId?.trim(); - const resolvedAccountId = - trimmedAccountId && trimmedAccountId.length > 0 - ? trimmedAccountId - : existing?.lastAccountId ?? existing?.deliveryContext?.accountId; const normalized = normalizeSessionDeliveryFields({ deliveryContext: { - channel: channel ?? existing?.lastChannel, - to, - accountId: resolvedAccountId, + channel: channel ?? existing?.lastChannel ?? existing?.deliveryContext?.channel, + to: to ?? existing?.lastTo ?? existing?.deliveryContext?.to, + accountId: accountId ?? existing?.lastAccountId ?? existing?.deliveryContext?.accountId, }, }); const next = mergeSessionEntry(existing, { updatedAt: Math.max(existing?.updatedAt ?? 0, now), deliveryContext: normalized.deliveryContext, - lastChannel: normalized.lastChannel ?? channel, - lastTo: normalized.lastTo ?? (to?.trim() ? to.trim() : undefined), - lastAccountId: normalized.lastAccountId ?? resolvedAccountId, + lastChannel: normalized.lastChannel, + lastTo: normalized.lastTo, + lastAccountId: normalized.lastAccountId, }); store[sessionKey] = next; await saveSessionStoreUnlocked(storePath, store); diff --git a/src/gateway/server-methods/agent.ts b/src/gateway/server-methods/agent.ts index 37373a1af..33c2d7b10 100644 --- a/src/gateway/server-methods/agent.ts +++ b/src/gateway/server-methods/agent.ts @@ -1,5 +1,4 @@ import { randomUUID } from "node:crypto"; -import { DEFAULT_CHAT_CHANNEL } from "../../channels/registry.js"; import { agentCommand } from "../../commands/agent.js"; import { loadConfig } from "../../config/config.js"; import { @@ -9,10 +8,10 @@ import { updateSessionStore, } from "../../config/sessions.js"; import { registerAgentRunContext } from "../../infra/agent-events.js"; -import { resolveOutboundTarget, resolveSessionDeliveryTarget } from "../../infra/outbound/targets.js"; +import { resolveAgentDeliveryPlan } from "../../infra/outbound/agent-delivery.js"; +import { resolveOutboundTarget } from "../../infra/outbound/targets.js"; import { defaultRuntime } from "../../runtime.js"; import { resolveSendPolicy } from "../../sessions/send-policy.js"; -import { normalizeAccountId } from "../../utils/account-id.js"; import { normalizeSessionDeliveryFields } from "../../utils/delivery-context.js"; import { INTERNAL_MESSAGE_CHANNEL, @@ -202,53 +201,21 @@ export const agentHandlers: GatewayRequestHandlers = { const runId = idem; const wantsDelivery = request.deliver === true; - const requestedChannel = normalizeMessageChannel(request.channel) ?? "last"; const explicitTo = typeof request.to === "string" && request.to.trim() ? request.to.trim() : undefined; - - const baseDelivery = resolveSessionDeliveryTarget({ - entry: sessionEntry, - requestedChannel: requestedChannel === INTERNAL_MESSAGE_CHANNEL ? "last" : requestedChannel, + const deliveryPlan = resolveAgentDeliveryPlan({ + sessionEntry, + requestedChannel: request.channel, explicitTo, + accountId: request.accountId, + wantsDelivery, }); - const resolvedChannel = (() => { - if (requestedChannel === INTERNAL_MESSAGE_CHANNEL) return INTERNAL_MESSAGE_CHANNEL; - if (requestedChannel === "last") { - // WebChat is not a deliverable surface. Treat it as "unset" for routing, - // so VoiceWake and CLI callers don't get stuck with deliver=false. - if (baseDelivery.channel && baseDelivery.channel !== INTERNAL_MESSAGE_CHANNEL) { - return baseDelivery.channel; - } - return wantsDelivery ? DEFAULT_CHAT_CHANNEL : INTERNAL_MESSAGE_CHANNEL; - } + const resolvedChannel = deliveryPlan.resolvedChannel; + const deliveryTargetMode = deliveryPlan.deliveryTargetMode; + const resolvedAccountId = deliveryPlan.resolvedAccountId; + let resolvedTo = deliveryPlan.resolvedTo; - if (isGatewayMessageChannel(requestedChannel)) return requestedChannel; - - if (baseDelivery.channel && baseDelivery.channel !== INTERNAL_MESSAGE_CHANNEL) { - return baseDelivery.channel; - } - return wantsDelivery ? DEFAULT_CHAT_CHANNEL : INTERNAL_MESSAGE_CHANNEL; - })(); - - const deliveryTargetMode = explicitTo - ? "explicit" - : isDeliverableMessageChannel(resolvedChannel) - ? "implicit" - : undefined; - const resolvedAccountId = - normalizeAccountId(request.accountId) ?? - (deliveryTargetMode === "implicit" && resolvedChannel === baseDelivery.lastChannel - ? baseDelivery.lastAccountId - : undefined); - let resolvedTo = explicitTo; - if ( - !resolvedTo && - isDeliverableMessageChannel(resolvedChannel) && - resolvedChannel === baseDelivery.lastChannel - ) { - resolvedTo = baseDelivery.lastTo; - } if (!resolvedTo && isDeliverableMessageChannel(resolvedChannel)) { const cfg = cfgForAgent ?? loadConfig(); const fallback = resolveOutboundTarget({ diff --git a/src/gateway/server-restart-sentinel.ts b/src/gateway/server-restart-sentinel.ts index 723cad067..d0661c063 100644 --- a/src/gateway/server-restart-sentinel.ts +++ b/src/gateway/server-restart-sentinel.ts @@ -11,6 +11,7 @@ import { } from "../infra/restart-sentinel.js"; import { enqueueSystemEvent } from "../infra/system-events.js"; import { defaultRuntime } from "../runtime.js"; +import { deliveryContextFromSession, mergeDeliveryContext } from "../utils/delivery-context.js"; import { loadSessionEntry } from "./session-utils.js"; export async function scheduleRestartSentinelWake(params: { deps: CliDeps }) { @@ -28,12 +29,11 @@ export async function scheduleRestartSentinelWake(params: { deps: CliDeps }) { } const { cfg, entry } = loadSessionEntry(sessionKey); - const lastChannel = entry?.lastChannel; - const lastTo = entry?.lastTo?.trim(); const parsedTarget = resolveAnnounceTargetFromKey(sessionKey); - const channelRaw = lastChannel ?? parsedTarget?.channel; + const origin = mergeDeliveryContext(deliveryContextFromSession(entry), parsedTarget ?? undefined); + const channelRaw = origin?.channel; const channel = channelRaw ? normalizeChannelId(channelRaw) : null; - const to = lastTo || parsedTarget?.to; + const to = origin?.to; if (!channel || !to) { enqueueSystemEvent(message, { sessionKey }); return; @@ -43,7 +43,7 @@ export async function scheduleRestartSentinelWake(params: { deps: CliDeps }) { channel, to, cfg, - accountId: parsedTarget?.accountId ?? entry?.lastAccountId, + accountId: origin?.accountId, mode: "implicit", }); if (!resolved.ok) { diff --git a/src/gateway/server.sessions.gateway-server-sessions-a.test.ts b/src/gateway/server.sessions.gateway-server-sessions-a.test.ts index 9396c50fd..83f1c136b 100644 --- a/src/gateway/server.sessions.gateway-server-sessions-a.test.ts +++ b/src/gateway/server.sessions.gateway-server-sessions-a.test.ts @@ -128,6 +128,7 @@ describe("gateway server sessions", () => { thinkingLevel?: string; verboseLevel?: string; lastAccountId?: string; + deliveryContext?: { channel?: string; to?: string; accountId?: string }; }>; }>(ws, "sessions.list", { includeGlobal: false, includeUnknown: false }); @@ -140,6 +141,11 @@ describe("gateway server sessions", () => { expect(main?.thinkingLevel).toBe("low"); expect(main?.verboseLevel).toBe("on"); expect(main?.lastAccountId).toBe("work"); + expect(main?.deliveryContext).toEqual({ + channel: "whatsapp", + to: "+1555", + accountId: "work", + }); const active = await rpcReq<{ sessions: Array<{ key: string }>; diff --git a/src/gateway/session-utils.ts b/src/gateway/session-utils.ts index 099de7eeb..9382edc05 100644 --- a/src/gateway/session-utils.ts +++ b/src/gateway/session-utils.ts @@ -21,6 +21,7 @@ import { normalizeMainKey, parseAgentSessionKey, } from "../routing/session-key.js"; +import { normalizeSessionDeliveryFields } from "../utils/delivery-context.js"; import type { GatewayAgentRow, GatewaySessionRow, @@ -401,6 +402,7 @@ export function listSessionsFromStore(params: { key, }) : undefined); + const deliveryFields = normalizeSessionDeliveryFields(entry); return { key, kind: classifySessionKey(key, entry), @@ -427,9 +429,10 @@ export function listSessionsFromStore(params: { modelProvider: entry?.modelProvider, model: entry?.model, contextTokens: entry?.contextTokens, - lastChannel: entry?.lastChannel, - lastTo: entry?.lastTo, - lastAccountId: entry?.lastAccountId, + deliveryContext: deliveryFields.deliveryContext, + lastChannel: deliveryFields.lastChannel ?? entry?.lastChannel, + lastTo: deliveryFields.lastTo ?? entry?.lastTo, + lastAccountId: deliveryFields.lastAccountId ?? entry?.lastAccountId, } satisfies GatewaySessionRow; }) .sort((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0)); diff --git a/src/gateway/session-utils.types.ts b/src/gateway/session-utils.types.ts index d69c7b342..cb11db95a 100644 --- a/src/gateway/session-utils.types.ts +++ b/src/gateway/session-utils.types.ts @@ -1,4 +1,5 @@ import type { SessionEntry } from "../config/sessions.js"; +import type { DeliveryContext } from "../utils/delivery-context.js"; export type GatewaySessionsDefaults = { modelProvider: string | null; @@ -32,6 +33,7 @@ export type GatewaySessionRow = { modelProvider?: string; model?: string; contextTokens?: number; + deliveryContext?: DeliveryContext; lastChannel?: SessionEntry["lastChannel"]; lastTo?: string; lastAccountId?: string; diff --git a/src/infra/outbound/agent-delivery.ts b/src/infra/outbound/agent-delivery.ts new file mode 100644 index 000000000..adde8ee24 --- /dev/null +++ b/src/infra/outbound/agent-delivery.ts @@ -0,0 +1,88 @@ +import { DEFAULT_CHAT_CHANNEL } from "../../channels/registry.js"; +import type { ChannelOutboundTargetMode } from "../../channels/plugins/types.js"; +import type { SessionEntry } from "../../config/sessions.js"; +import { normalizeAccountId } from "../../utils/account-id.js"; +import { + INTERNAL_MESSAGE_CHANNEL, + isDeliverableMessageChannel, + isGatewayMessageChannel, + normalizeMessageChannel, + type GatewayMessageChannel, +} from "../../utils/message-channel.js"; +import { resolveSessionDeliveryTarget, type SessionDeliveryTarget } from "./targets.js"; + +export type AgentDeliveryPlan = { + baseDelivery: SessionDeliveryTarget; + resolvedChannel: GatewayMessageChannel; + resolvedTo?: string; + resolvedAccountId?: string; + deliveryTargetMode?: ChannelOutboundTargetMode; +}; + +export function resolveAgentDeliveryPlan(params: { + sessionEntry?: SessionEntry; + requestedChannel?: string; + explicitTo?: string; + accountId?: string; + wantsDelivery: boolean; +}): AgentDeliveryPlan { + const requestedRaw = + typeof params.requestedChannel === "string" ? params.requestedChannel.trim() : ""; + const normalizedRequested = requestedRaw ? normalizeMessageChannel(requestedRaw) : undefined; + const requestedChannel = normalizedRequested || "last"; + + const explicitTo = + typeof params.explicitTo === "string" && params.explicitTo.trim() + ? params.explicitTo.trim() + : undefined; + + const baseDelivery = resolveSessionDeliveryTarget({ + entry: params.sessionEntry, + requestedChannel: requestedChannel === INTERNAL_MESSAGE_CHANNEL ? "last" : requestedChannel, + explicitTo, + }); + + const resolvedChannel = (() => { + if (requestedChannel === INTERNAL_MESSAGE_CHANNEL) return INTERNAL_MESSAGE_CHANNEL; + if (requestedChannel === "last") { + if (baseDelivery.channel && baseDelivery.channel !== INTERNAL_MESSAGE_CHANNEL) { + return baseDelivery.channel; + } + return params.wantsDelivery ? DEFAULT_CHAT_CHANNEL : INTERNAL_MESSAGE_CHANNEL; + } + + if (isGatewayMessageChannel(requestedChannel)) return requestedChannel; + + if (baseDelivery.channel && baseDelivery.channel !== INTERNAL_MESSAGE_CHANNEL) { + return baseDelivery.channel; + } + return params.wantsDelivery ? DEFAULT_CHAT_CHANNEL : INTERNAL_MESSAGE_CHANNEL; + })(); + + const deliveryTargetMode = explicitTo + ? "explicit" + : isDeliverableMessageChannel(resolvedChannel) + ? "implicit" + : undefined; + + const resolvedAccountId = + normalizeAccountId(params.accountId) ?? + (deliveryTargetMode === "implicit" ? baseDelivery.accountId : undefined); + + let resolvedTo = explicitTo; + if ( + !resolvedTo && + isDeliverableMessageChannel(resolvedChannel) && + resolvedChannel === baseDelivery.lastChannel + ) { + resolvedTo = baseDelivery.lastTo; + } + + return { + baseDelivery, + resolvedChannel, + resolvedTo, + resolvedAccountId, + deliveryTargetMode, + }; +}