From ce391dc382e5c61c87bfb537096e0b319d40e128 Mon Sep 17 00:00:00 2001 From: Marcus Castro <7562095+mcaxtr@users.noreply.github.com> Date: Mon, 22 Jun 2026 23:05:35 -0300 Subject: [PATCH] fix(whatsapp): preserve durable reply target (#95914) --- .../monitor/inbound-dispatch.test.ts | 60 +++++++++++++++++++ .../auto-reply/monitor/inbound-dispatch.ts | 1 + 2 files changed, 61 insertions(+) diff --git a/extensions/whatsapp/src/auto-reply/monitor/inbound-dispatch.test.ts b/extensions/whatsapp/src/auto-reply/monitor/inbound-dispatch.test.ts index c62885a40f5f..41cb838c5d59 100644 --- a/extensions/whatsapp/src/auto-reply/monitor/inbound-dispatch.test.ts +++ b/extensions/whatsapp/src/auto-reply/monitor/inbound-dispatch.test.ts @@ -11,6 +11,7 @@ type CapturedReplyPayload = { isError?: boolean; mediaUrl?: string; mediaUrls?: string[]; + replyToId?: string | null; }; type CapturedDispatchParams = { @@ -720,6 +721,7 @@ describe("whatsapp inbound dispatch", () => { agentId: "main", to: "+1000", info: { kind: "final" }, + replyToId: null, }); expectRecordFields(requireRecord(durableParams.payload, "durable payload"), { text: "final payload", @@ -734,6 +736,64 @@ describe("whatsapp inbound dispatch", () => { }); }); + it.each([ + { + name: "uses resolved final payload reply targets", + payload: { text: "final payload", replyToId: "trigger-message" }, + expectedReplyToId: "trigger-message", + }, + { + name: "blocks durable fallback without a final payload reply target", + payload: { text: "final payload" }, + expectedReplyToId: null, + }, + ] satisfies Array<{ + name: string; + payload: CapturedReplyPayload; + expectedReplyToId: string | null; + }>)("$name while preserving quoted WhatsApp context", async ({ payload, expectedReplyToId }) => { + deliverInboundReplyWithMessageSendContextMock.mockResolvedValueOnce({ + status: "handled_visible", + delivery: { + messageIds: ["wa-1"], + visibleReplySent: true, + }, + }); + const deliverReply = vi.fn(async () => acceptedDeliveryResult()); + + await dispatchBufferedReply({ + context: { + Body: "incoming", + ReplyToId: "quoted-bot-message", + ReplyToBody: "Earlier bot reply", + ReplyToSender: "OpenClaw", + }, + deliverReply, + msg: makeMsg({ + event: { id: "trigger-message" }, + }), + }); + + const deliver = getCapturedDeliver(); + await deliver?.(payload, { kind: "final" }); + + const durableParams = requireMockArg( + deliverInboundReplyWithMessageSendContextMock, + 0, + 0, + "durable delivery params", + ); + expectRecordFields(durableParams, { + replyToId: expectedReplyToId, + }); + expectRecordFields(requireRecord(durableParams.ctxPayload, "durable context"), { + ReplyToId: "quoted-bot-message", + ReplyToBody: "Earlier bot reply", + ReplyToSender: "OpenClaw", + }); + expect(deliverReply).not.toHaveBeenCalled(); + }); + it("does not fall back when durable WhatsApp delivery suppresses a send", async () => { deliverInboundReplyWithMessageSendContextMock.mockResolvedValueOnce({ status: "handled_no_send", diff --git a/extensions/whatsapp/src/auto-reply/monitor/inbound-dispatch.ts b/extensions/whatsapp/src/auto-reply/monitor/inbound-dispatch.ts index f080729fcc4c..68e2c6848dde 100644 --- a/extensions/whatsapp/src/auto-reply/monitor/inbound-dispatch.ts +++ b/extensions/whatsapp/src/auto-reply/monitor/inbound-dispatch.ts @@ -691,6 +691,7 @@ export async function dispatchWhatsAppBufferedReply(params: { payload: normalizedDeliveryPayload, info, to: conversationId, + replyToId: normalizedDeliveryPayload.replyToId ?? null, formatting: { textLimit, tableMode,