diff --git a/src/auto-reply/reply/dispatch-acp-delivery.test.ts b/src/auto-reply/reply/dispatch-acp-delivery.test.ts index bfca2a4ba27..73c36847ab2 100644 --- a/src/auto-reply/reply/dispatch-acp-delivery.test.ts +++ b/src/auto-reply/reply/dispatch-acp-delivery.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; import { createAcpDispatchDeliveryCoordinator } from "./dispatch-acp-delivery.js"; import type { ReplyDispatcher } from "./reply-dispatcher.js"; import { buildTestCtx } from "./test-ctx.js"; @@ -11,10 +11,23 @@ const ttsMocks = vi.hoisted(() => ({ }), })); +const deliveryMocks = vi.hoisted(() => ({ + routeReply: vi.fn(async (_params: unknown) => ({ ok: true, messageId: "mock-message" })), + runMessageAction: vi.fn(async (_params: unknown) => ({ ok: true as const })), +})); + vi.mock("../../tts/tts.js", () => ({ maybeApplyTtsToPayload: (params: unknown) => ttsMocks.maybeApplyTtsToPayload(params), })); +vi.mock("./route-reply.js", () => ({ + routeReply: (params: unknown) => deliveryMocks.routeReply(params), +})); + +vi.mock("../../infra/outbound/message-action-runner.js", () => ({ + runMessageAction: (params: unknown) => deliveryMocks.runMessageAction(params), +})); + function createDispatcher(): ReplyDispatcher { return { sendToolResult: vi.fn(() => true), @@ -43,6 +56,13 @@ function createCoordinator(onReplyStart?: (...args: unknown[]) => Promise) } describe("createAcpDispatchDeliveryCoordinator", () => { + beforeEach(() => { + deliveryMocks.routeReply.mockClear(); + deliveryMocks.routeReply.mockResolvedValue({ ok: true, messageId: "mock-message" }); + deliveryMocks.runMessageAction.mockClear(); + deliveryMocks.runMessageAction.mockResolvedValue({ ok: true as const }); + }); + it("bypasses TTS when skipTts is requested", async () => { const dispatcher = createDispatcher(); const coordinator = createAcpDispatchDeliveryCoordinator({ @@ -221,4 +241,36 @@ describe("createAcpDispatchDeliveryCoordinator", () => { expect(coordinator.getAccumulatedBlockText()).toBe("working on it"); expect(coordinator.hasDeliveredVisibleText()).toBe(false); }); + + it("routes ACP replies through the configured default account when AccountId is omitted", async () => { + const coordinator = createAcpDispatchDeliveryCoordinator({ + cfg: createAcpTestConfig({ + channels: { + discord: { + defaultAccount: "work", + }, + }, + }), + ctx: buildTestCtx({ + Provider: "discord", + Surface: "discord", + SessionKey: "agent:codex-acp:session-1", + }), + dispatcher: createDispatcher(), + inboundAudio: false, + shouldRouteToOriginating: true, + originatingChannel: "discord", + originatingTo: "channel:thread-1", + }); + + await coordinator.deliver("block", { text: "hello" }, { skipTts: true }); + + expect(deliveryMocks.routeReply).toHaveBeenCalledWith( + expect.objectContaining({ + channel: "discord", + to: "channel:thread-1", + accountId: "work", + }), + ); + }); }); diff --git a/src/auto-reply/reply/dispatch-acp-delivery.ts b/src/auto-reply/reply/dispatch-acp-delivery.ts index de863975699..877c04f4b43 100644 --- a/src/auto-reply/reply/dispatch-acp-delivery.ts +++ b/src/auto-reply/reply/dispatch-acp-delivery.ts @@ -29,6 +29,28 @@ function normalizeDeliveryChannel(value: string | undefined): string | undefined return normalized || undefined; } +function resolveDeliveryAccountId(params: { + cfg: OpenClawConfig; + channel: string | undefined; + accountId: string | undefined; +}): string | undefined { + const explicit = params.accountId?.trim(); + if (explicit) { + return explicit; + } + const channelId = normalizeDeliveryChannel(params.channel); + if (!channelId) { + return undefined; + } + const channelCfg = (params.cfg.channels as Record)[ + channelId + ]; + const configuredDefault = channelCfg?.defaultAccount; + return typeof configuredDefault === "string" && configuredDefault.trim() + ? configuredDefault.trim() + : undefined; +} + function shouldTreatDeliveredTextAsVisible(params: { channel: string | undefined; kind: ReplyDispatchKind; @@ -113,6 +135,11 @@ export function createAcpDispatchDeliveryCoordinator(params: { }; const directChannel = normalizeDeliveryChannel(params.ctx.Provider ?? params.ctx.Surface); const routedChannel = normalizeDeliveryChannel(params.originatingChannel); + const resolvedAccountId = resolveDeliveryAccountId({ + cfg: params.cfg, + channel: routedChannel ?? directChannel, + accountId: params.ctx.AccountId, + }); const settleDirectVisibleText = async () => { if (state.settledDirectVisibleText || state.queuedDirectVisibleTextDeliveries === 0) { @@ -229,7 +256,7 @@ export function createAcpDispatchDeliveryCoordinator(params: { channel: params.originatingChannel, to: params.originatingTo, sessionKey: params.ctx.SessionKey, - accountId: params.ctx.AccountId, + accountId: resolvedAccountId, threadId: params.ctx.MessageThreadId, cfg: params.cfg, }); @@ -245,7 +272,7 @@ export function createAcpDispatchDeliveryCoordinator(params: { if (kind === "tool" && meta?.toolCallId && result.messageId) { state.toolMessageByCallId.set(meta.toolCallId, { channel: params.originatingChannel, - accountId: params.ctx.AccountId, + accountId: resolvedAccountId, to: params.originatingTo, ...(params.ctx.MessageThreadId != null ? { threadId: params.ctx.MessageThreadId } : {}), messageId: result.messageId,