diff --git a/CHANGELOG.md b/CHANGELOG.md index 2daf64f2fd4..90447e9b3ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -90,6 +90,7 @@ Docs: https://docs.openclaw.ai - Agents/context engines: preserve the child agent's configured `agentDir` when subagent cleanup re-resolves a context engine, so `onSubagentEnded` hooks keep operating on the correct per-agent state. (#67243) Thanks @jarimustonen. - Channels/WhatsApp: restrict pairing verification replies to real inbound user content, preventing unsolicited prompts from receipts, typing indicators, presence updates, and other non-message Baileys upserts. Fixes #73797. (#73823) Thanks @hclsys. - Configure/Ollama: show the configured Ollama model allowlist after Cloud only or Cloud + Local setup and skip slow per-model cloud metadata fetches. (#73995) Thanks @obviyus. +- Channels/WhatsApp: detect explicit group `@mentions` again when the bot's own E.164 is in `allowFrom`, so shared-number setups no longer skip group pings that directly mention the bot. Fixes #49317. (#73453) Thanks @juan-flores077. ## 2026.4.27 diff --git a/extensions/whatsapp/src/auto-reply/mentions.ts b/extensions/whatsapp/src/auto-reply/mentions.ts index 1c5af35f895..5cb8699fd58 100644 --- a/extensions/whatsapp/src/auto-reply/mentions.ts +++ b/extensions/whatsapp/src/auto-reply/mentions.ts @@ -10,6 +10,7 @@ import { identitiesOverlap, type WhatsAppIdentity, } from "../identity.js"; +import { isWhatsAppGroupJid } from "../normalize-target.js"; import { isSelfChatMode, normalizeE164 } from "../text-runtime.js"; import type { WebInboundMsg } from "./types.js"; @@ -44,10 +45,21 @@ export function isBotMentionedFromTargets( // Remove zero-width and directionality markers WhatsApp injects around display names normalizeMentionText(text); - const isSelfChat = - typeof mentionCfg.isSelfChat === "boolean" - ? mentionCfg.isSelfChat - : isSelfChatMode(targets.self.e164, mentionCfg.allowFrom); + const explicitSelfChatOverride = typeof mentionCfg.isSelfChat === "boolean"; + // `isSelfChatMode` is a config-shaped check ("is the bot's own E.164 in + // allowFrom?"), not a conversation-shaped check, so it returns true even + // for group conversations whenever the operator put their own number in + // allowFrom — which is the common config. The original mention-skip path + // was designed to prevent owner-mentioning-self in a true 1:1 self DM + // from falsely triggering the bot, so when we derive the flag implicitly + // from `allowFrom`, confine the suppression to non-group conversations + // and let real group @mentions go through the identity-overlap check + // (#49317). Explicit `mentionCfg.isSelfChat` overrides from the caller + // are honored as-is so multi-account / precomputed paths keep working. + const isGroupConversation = isWhatsAppGroupJid(msg.from); + const isSelfChat = explicitSelfChatOverride + ? Boolean(mentionCfg.isSelfChat) + : isSelfChatMode(targets.self.e164, mentionCfg.allowFrom) && !isGroupConversation; const hasMentions = targets.normalizedMentions.length > 0; if (hasMentions && !isSelfChat) { @@ -59,7 +71,7 @@ export function isBotMentionedFromTargets( // If the message explicitly mentions someone else, do not fall back to regex matches. return false; } else if (hasMentions && isSelfChat) { - // Self-chat mode: ignore WhatsApp @mention JIDs, otherwise @mentioning the owner in group chats triggers the bot. + // Self-chat mode: ignore WhatsApp @mention JIDs, otherwise @mentioning the owner in self-chat triggers the bot. } const bodyClean = clean(msg.body); if (mentionCfg.mentionRegexes.some((re) => re.test(bodyClean))) { diff --git a/extensions/whatsapp/src/auto-reply/web-auto-reply-monitor.test.ts b/extensions/whatsapp/src/auto-reply/web-auto-reply-monitor.test.ts index 1c0fd061048..70ba1f2e8b0 100644 --- a/extensions/whatsapp/src/auto-reply/web-auto-reply-monitor.test.ts +++ b/extensions/whatsapp/src/auto-reply/web-auto-reply-monitor.test.ts @@ -6,6 +6,7 @@ import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { buildMentionConfig } from "./mentions.js"; import { applyGroupGating, type GroupHistoryEntry } from "./monitor/group-gating.js"; import { buildInboundLine, formatReplyContext } from "./monitor/message-line.js"; +import type { WebInboundMsg } from "./types.js"; let sessionDir: string | undefined; let sessionStorePath: string; @@ -37,10 +38,11 @@ const makeConfig = (overrides: Record) => async function runGroupGating(params: { cfg: import("openclaw/plugin-sdk/config-types").OpenClawConfig; - msg: Record; + msg: WebInboundMsg; conversationId?: string; agentId?: string; selfChatMode?: boolean; + authDir?: string; }) { const groupHistories = new Map(); const conversationId = params.conversationId ?? "123@g.us"; @@ -49,12 +51,13 @@ async function runGroupGating(params: { const baseMentionConfig = buildMentionConfig(params.cfg, undefined); const result = await applyGroupGating({ cfg: params.cfg, - msg: params.msg as any, + msg: params.msg, conversationId, groupHistoryKey: `whatsapp:default:group:${conversationId}`, agentId, sessionKey, baseMentionConfig, + authDir: params.authDir, selfChatMode: params.selfChatMode, groupHistories, groupHistoryLimit: 10, @@ -65,7 +68,7 @@ async function runGroupGating(params: { return { result, groupHistories }; } -function createGroupMessage(overrides: Record = {}) { +function createGroupMessage(overrides: Partial = {}): WebInboundMsg { return { id: "g1", from: "123@g.us", @@ -73,13 +76,14 @@ function createGroupMessage(overrides: Record = {}) { chatId: "123@g.us", chatType: "group", to: "+2", + accountId: "default", body: "hello group", senderE164: "+111", senderName: "Alice", selfE164: "+999", sendComposing: async () => {}, - reply: async () => {}, - sendMedia: async () => {}, + reply: async (_text, _options) => {}, + sendMedia: async (_payload, _options) => {}, ...overrides, }; } @@ -194,6 +198,45 @@ describe("applyGroupGating", () => { expect(result.shouldProcess).toBe(true); }); + it("processes explicit group @mentions when self is in allowFrom (#49317)", async () => { + if (!sessionDir) { + throw new Error("sessionDir not initialized"); + } + await fs.writeFile( + path.join(sessionDir, "lid-mapping-216372600647751_reverse.json"), + JSON.stringify("+15551234567"), + ); + const cfg = makeConfig({ + channels: { + whatsapp: { + allowFrom: ["+15551234567"], + groupPolicy: "open", + groups: { "*": { requireMention: true } }, + }, + }, + }); + const msg = createGroupMessage({ + id: "g-self-lid-mention", + accountId: "default", + body: "@216372600647751 can you see this?", + mentionedJids: ["216372600647751@lid"], + senderE164: "+15550001111", + senderName: "Alice", + selfE164: "+15551234567", + selfJid: "15551234567@s.whatsapp.net", + }); + + const { result, groupHistories } = await runGroupGating({ + cfg, + authDir: sessionDir, + msg, + }); + + expect(result.shouldProcess).toBe(true); + expect(msg.wasMentioned).toBe(true); + expect(groupHistories.get("whatsapp:default:group:123@g.us")).toBeUndefined(); + }); + it("honors per-account selfChatMode overrides before suppressing implicit mentions", async () => { const cfg = makeConfig({ channels: { diff --git a/extensions/whatsapp/src/auto-reply/web-auto-reply-utils.test.ts b/extensions/whatsapp/src/auto-reply/web-auto-reply-utils.test.ts index 06211c1a355..4e4ce176ddc 100644 --- a/extensions/whatsapp/src/auto-reply/web-auto-reply-utils.test.ts +++ b/extensions/whatsapp/src/auto-reply/web-auto-reply-utils.test.ts @@ -70,9 +70,15 @@ describe("isBotMentionedFromTargets", () => { expectMentioned(msg, mentionCfg, true); }); - it("ignores JID mentions in self-chat mode", () => { + it("ignores JID mentions in a true 1:1 self-chat (not a group)", () => { const cfg = { mentionRegexes: [/\bopenclaw\b/i], allowFrom: ["+999"] }; const msg = makeMsg({ + // Direct chat with self, not a group — the original "ignore mentions + // in self-chat" suppression still applies here so that mentioning the + // owner in their own DM does not falsely trigger the bot. + from: "999@s.whatsapp.net", + conversationId: "999@s.whatsapp.net", + chatType: "direct", body: "@owner ping", mentionedJids: ["999@s.whatsapp.net"], selfE164: "+999", @@ -81,6 +87,9 @@ describe("isBotMentionedFromTargets", () => { expectMentioned(msg, cfg, false); const msgTextMention = makeMsg({ + from: "999@s.whatsapp.net", + conversationId: "999@s.whatsapp.net", + chatType: "direct", body: "openclaw ping", selfE164: "+999", selfJid: "999@s.whatsapp.net", @@ -88,6 +97,25 @@ describe("isBotMentionedFromTargets", () => { expectMentioned(msgTextMention, cfg, true); }); + it("detects an explicit group @mention even when self is in allowFrom (#49317)", () => { + // Operator config commonly puts their own E.164 in allowFrom so they can + // run owner-only commands in groups; previously, that flipped the gate + // to "self-chat mode" and silently dropped mention detection in groups, + // including LID-style WhatsApp mentions that resolve to the bot's own + // E.164. After the fix, group conversations honor the identity-overlap + // check regardless of allowFrom. + const cfg = { mentionRegexes: [/\bopenclaw\b/i], allowFrom: ["+15551234567"] }; + const msg = makeMsg({ + // Default `from` is the @g.us group JID from `makeMsg`. + body: "@216372600647751 can you see this?", + mentionedJids: ["216372600647751@lid"], + selfE164: "+15551234567", + selfJid: "15551234567@s.whatsapp.net", + selfLid: "216372600647751@lid", + }); + expectMentioned(msg, cfg, true); + }); + it("honors explicit self-chat overrides without recomputing from allowFrom", () => { const cfg = { mentionRegexes: [/\bopenclaw\b/i],