diff --git a/CHANGELOG.md b/CHANGELOG.md index f8753f65170..1f9add36e12 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,7 @@ Docs: https://docs.openclaw.ai - Agents/status: use provider-aware context window lookup for fresh Anthropic 4.6 model overrides so `/status` shows the correct 1.0m window instead of an underreported shared-cache minimum. (#54796) Thanks @neeravmakwana. - CLI/message send: write manual `openclaw message send` deliveries into the resolved agent session transcript again by always threading the default CLI agent through outbound mirroring. (#54187) Thanks @KevInTheCloud5617. - CLI/onboarding: show the Kimi Code API key option again in the Moonshot setup menu so the interactive picker includes all Kimi setup paths together. Fixes #54412 Thanks @sparkyrider +- WhatsApp: fix infinite echo loop in self-chat DM mode where the bot's own outbound replies were re-processed as new inbound user messages. (#54570) Thanks @joelnishanth ## 2026.3.24 diff --git a/extensions/whatsapp/src/inbound/monitor.ts b/extensions/whatsapp/src/inbound/monitor.ts index 5f829d9c4ed..6760cf598cd 100644 --- a/extensions/whatsapp/src/inbound/monitor.ts +++ b/extensions/whatsapp/src/inbound/monitor.ts @@ -218,8 +218,10 @@ export async function monitorWebInbox(options: { } const group = isGroupJid(remoteJid); + // Drop echoes of messages the gateway itself sent (tracked by sendTrackedMessage). + // Applies to both groups and DMs/self-chat — without this, self-chat mode + // re-processes the bot's own replies as new inbound user messages. if ( - group && Boolean(msg.key?.fromMe) && id && isRecentOutboundMessage({ @@ -228,7 +230,7 @@ export async function monitorWebInbox(options: { messageId: id, }) ) { - logVerbose(`Skipping recent outbound WhatsApp group echo ${id} for ${remoteJid}`); + logVerbose(`Skipping recent outbound WhatsApp echo ${id} for ${remoteJid}`); return null; } if (id) { diff --git a/extensions/whatsapp/src/monitor-inbox.allows-messages-from-senders-allowfrom-list.test.ts b/extensions/whatsapp/src/monitor-inbox.allows-messages-from-senders-allowfrom-list.test.ts index e0b022fe82f..99f4e989e11 100644 --- a/extensions/whatsapp/src/monitor-inbox.allows-messages-from-senders-allowfrom-list.test.ts +++ b/extensions/whatsapp/src/monitor-inbox.allows-messages-from-senders-allowfrom-list.test.ts @@ -298,6 +298,45 @@ describe("web monitor inbox", () => { await listener.close(); }); + it("filters self-chat DM fromMe echoes when the gateway sent the matching message id", async () => { + mockLoadConfig.mockReturnValue({ + channels: { + whatsapp: { + selfChatMode: true, + allowFrom: ["+123"], + }, + }, + messages: DEFAULT_MESSAGES_CFG, + }); + + const onMessage = vi.fn(); + const { listener, sock } = await startInboxMonitor(onMessage); + + sock.sendMessage.mockResolvedValueOnce({ key: { id: "bot-dm-echo-1" } }); + await listener.sendMessage("123@s.whatsapp.net", "self-chat reply"); + + sock.ev.emit("messages.upsert", { + type: "notify", + messages: [ + { + key: { + id: "bot-dm-echo-1", + fromMe: true, + remoteJid: "123@s.whatsapp.net", + }, + message: { conversation: "self-chat reply" }, + messageTimestamp: nowSeconds(), + pushName: "Owner", + }, + ], + }); + await settleInboundWork(); + + expect(onMessage).not.toHaveBeenCalled(); + + await listener.close(); + }); + it("handles append messages by marking them read but skipping auto-reply", async () => { const { onMessage, listener, sock } = await openInboxMonitor(); const staleTs = Math.floor(Date.now() / 1000) - 300;