From 9cb1bfa1c12e7fc4e9049210200b1ed22560a470 Mon Sep 17 00:00:00 2001 From: Peter Siska <63866+peschee@users.noreply.github.com> Date: Sat, 10 Jan 2026 22:40:19 +0000 Subject: [PATCH 1/3] fix(whatsapp): pass authDir to jidToE164 for LID mention detection WhatsApp group mentions using the new Linked ID format (@lid) were not being detected because jidToE164() was called without the authDir needed to find the LID reverse mapping files. Now isBotMentioned() and debugMention() accept an optional authDir parameter, which is passed through from account.authDir. --- src/web/auto-reply.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/web/auto-reply.ts b/src/web/auto-reply.ts index ee9786680..8306b8186 100644 --- a/src/web/auto-reply.ts +++ b/src/web/auto-reply.ts @@ -189,6 +189,7 @@ function buildMentionConfig( function isBotMentioned( msg: WebInboundMsg, mentionCfg: MentionConfig, + authDir?: string, ): boolean { const clean = (text: string) => // Remove zero-width and directionality markers WhatsApp injects around display names @@ -198,7 +199,7 @@ function isBotMentioned( if (msg.mentionedJids?.length && !isSelfChat) { const normalizedMentions = msg.mentionedJids - .map((jid) => jidToE164(jid) ?? jid) + .map((jid) => jidToE164(jid, authDir ? { authDir } : undefined) ?? jid) .filter(Boolean); if (msg.selfE164 && normalizedMentions.includes(msg.selfE164)) return true; if (msg.selfJid && msg.selfE164) { @@ -230,8 +231,9 @@ function isBotMentioned( function debugMention( msg: WebInboundMsg, mentionCfg: MentionConfig, + authDir?: string, ): { wasMentioned: boolean; details: Record } { - const result = isBotMentioned(msg, mentionCfg); + const result = isBotMentioned(msg, mentionCfg, authDir); const details = { from: msg.from, body: msg.body, @@ -1584,7 +1586,7 @@ export async function monitorWebProvider( groupHistories.set(groupHistoryKey, history); } - const mentionDebug = debugMention(msg, mentionConfig); + const mentionDebug = debugMention(msg, mentionConfig, account.authDir); replyLogger.debug( { conversationId, From 9984248f51d89d6a64bfa75bb7d3cd3f52a017e9 Mon Sep 17 00:00:00 2001 From: Peter Siska <63866+peschee@users.noreply.github.com> Date: Sat, 10 Jan 2026 23:19:26 +0000 Subject: [PATCH 2/3] fix formatting --- pnpm-lock.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 17d826333..96ea38d3b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,7 +9,7 @@ overrides: patchedDependencies: '@mariozechner/pi-ai@0.42.2': - hash: d8d7c692952a4064a56dc4c67818939d17a48886eb355867de2bcc9118915c97 + hash: 626a71b0c497d0d6200d4fe689ecf8e9261ee8f6b478df1913691d5f85fb8a6a path: patches/@mariozechner__pi-ai@0.42.2.patch importers: @@ -36,7 +36,7 @@ importers: version: 0.42.2(ws@8.19.0)(zod@4.3.5) '@mariozechner/pi-ai': specifier: ^0.42.2 - version: 0.42.2(patch_hash=d8d7c692952a4064a56dc4c67818939d17a48886eb355867de2bcc9118915c97)(ws@8.19.0)(zod@4.3.5) + version: 0.42.2(patch_hash=626a71b0c497d0d6200d4fe689ecf8e9261ee8f6b478df1913691d5f85fb8a6a)(ws@8.19.0)(zod@4.3.5) '@mariozechner/pi-coding-agent': specifier: ^0.42.2 version: 0.42.2(ws@8.19.0)(zod@4.3.5) @@ -3777,7 +3777,7 @@ snapshots: '@mariozechner/pi-agent-core@0.42.2(ws@8.19.0)(zod@4.3.5)': dependencies: - '@mariozechner/pi-ai': 0.42.2(patch_hash=d8d7c692952a4064a56dc4c67818939d17a48886eb355867de2bcc9118915c97)(ws@8.19.0)(zod@4.3.5) + '@mariozechner/pi-ai': 0.42.2(patch_hash=626a71b0c497d0d6200d4fe689ecf8e9261ee8f6b478df1913691d5f85fb8a6a)(ws@8.19.0)(zod@4.3.5) '@mariozechner/pi-tui': 0.42.2 transitivePeerDependencies: - '@modelcontextprotocol/sdk' @@ -3787,7 +3787,7 @@ snapshots: - ws - zod - '@mariozechner/pi-ai@0.42.2(patch_hash=d8d7c692952a4064a56dc4c67818939d17a48886eb355867de2bcc9118915c97)(ws@8.19.0)(zod@4.3.5)': + '@mariozechner/pi-ai@0.42.2(patch_hash=626a71b0c497d0d6200d4fe689ecf8e9261ee8f6b478df1913691d5f85fb8a6a)(ws@8.19.0)(zod@4.3.5)': dependencies: '@anthropic-ai/sdk': 0.71.2(zod@4.3.5) '@google/genai': 1.34.0 @@ -3811,7 +3811,7 @@ snapshots: dependencies: '@mariozechner/clipboard': 0.3.0 '@mariozechner/pi-agent-core': 0.42.2(ws@8.19.0)(zod@4.3.5) - '@mariozechner/pi-ai': 0.42.2(patch_hash=d8d7c692952a4064a56dc4c67818939d17a48886eb355867de2bcc9118915c97)(ws@8.19.0)(zod@4.3.5) + '@mariozechner/pi-ai': 0.42.2(patch_hash=626a71b0c497d0d6200d4fe689ecf8e9261ee8f6b478df1913691d5f85fb8a6a)(ws@8.19.0)(zod@4.3.5) '@mariozechner/pi-tui': 0.42.2 chalk: 5.6.2 cli-highlight: 2.1.11 From 6444258ad3746e33ef173c7183e201a0f4b693fa Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 11 Jan 2026 01:14:57 +0100 Subject: [PATCH 3/3] fix: handle WhatsApp LID mentions (#692) (thanks @peschee) --- CHANGELOG.md | 1 + pnpm-lock.yaml | 10 +-- src/web/auto-reply.test.ts | 145 +++++++++++++++++++++++++++++++++++++ src/web/auto-reply.ts | 60 +++++++++++---- src/web/session.ts | 2 +- 5 files changed, 198 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 663bb2e7b..c854cb80a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Fixes - CLI/Status: expand tables to full terminal width; improve update + daemon summary lines; keep `status --all` gateway log tail pasteable. +- WhatsApp: detect @lid mentions in groups using authDir reverse mapping + resolve self JID E.164 for mention gating. (#692) — thanks @peschee. ## 2026.1.10 diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 96ea38d3b..17d826333 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,7 +9,7 @@ overrides: patchedDependencies: '@mariozechner/pi-ai@0.42.2': - hash: 626a71b0c497d0d6200d4fe689ecf8e9261ee8f6b478df1913691d5f85fb8a6a + hash: d8d7c692952a4064a56dc4c67818939d17a48886eb355867de2bcc9118915c97 path: patches/@mariozechner__pi-ai@0.42.2.patch importers: @@ -36,7 +36,7 @@ importers: version: 0.42.2(ws@8.19.0)(zod@4.3.5) '@mariozechner/pi-ai': specifier: ^0.42.2 - version: 0.42.2(patch_hash=626a71b0c497d0d6200d4fe689ecf8e9261ee8f6b478df1913691d5f85fb8a6a)(ws@8.19.0)(zod@4.3.5) + version: 0.42.2(patch_hash=d8d7c692952a4064a56dc4c67818939d17a48886eb355867de2bcc9118915c97)(ws@8.19.0)(zod@4.3.5) '@mariozechner/pi-coding-agent': specifier: ^0.42.2 version: 0.42.2(ws@8.19.0)(zod@4.3.5) @@ -3777,7 +3777,7 @@ snapshots: '@mariozechner/pi-agent-core@0.42.2(ws@8.19.0)(zod@4.3.5)': dependencies: - '@mariozechner/pi-ai': 0.42.2(patch_hash=626a71b0c497d0d6200d4fe689ecf8e9261ee8f6b478df1913691d5f85fb8a6a)(ws@8.19.0)(zod@4.3.5) + '@mariozechner/pi-ai': 0.42.2(patch_hash=d8d7c692952a4064a56dc4c67818939d17a48886eb355867de2bcc9118915c97)(ws@8.19.0)(zod@4.3.5) '@mariozechner/pi-tui': 0.42.2 transitivePeerDependencies: - '@modelcontextprotocol/sdk' @@ -3787,7 +3787,7 @@ snapshots: - ws - zod - '@mariozechner/pi-ai@0.42.2(patch_hash=626a71b0c497d0d6200d4fe689ecf8e9261ee8f6b478df1913691d5f85fb8a6a)(ws@8.19.0)(zod@4.3.5)': + '@mariozechner/pi-ai@0.42.2(patch_hash=d8d7c692952a4064a56dc4c67818939d17a48886eb355867de2bcc9118915c97)(ws@8.19.0)(zod@4.3.5)': dependencies: '@anthropic-ai/sdk': 0.71.2(zod@4.3.5) '@google/genai': 1.34.0 @@ -3811,7 +3811,7 @@ snapshots: dependencies: '@mariozechner/clipboard': 0.3.0 '@mariozechner/pi-agent-core': 0.42.2(ws@8.19.0)(zod@4.3.5) - '@mariozechner/pi-ai': 0.42.2(patch_hash=626a71b0c497d0d6200d4fe689ecf8e9261ee8f6b478df1913691d5f85fb8a6a)(ws@8.19.0)(zod@4.3.5) + '@mariozechner/pi-ai': 0.42.2(patch_hash=d8d7c692952a4064a56dc4c67818939d17a48886eb355867de2bcc9118915c97)(ws@8.19.0)(zod@4.3.5) '@mariozechner/pi-tui': 0.42.2 chalk: 5.6.2 cli-highlight: 2.1.11 diff --git a/src/web/auto-reply.test.ts b/src/web/auto-reply.test.ts index 7e0c78c6e..a731a13a2 100644 --- a/src/web/auto-reply.test.ts +++ b/src/web/auto-reply.test.ts @@ -1135,6 +1135,151 @@ describe("web auto-reply", () => { expect(payload.Body).toContain("[from: Bob (+222)]"); }); + it("detects LID mentions using authDir mapping", async () => { + const sendMedia = vi.fn(); + const reply = vi.fn().mockResolvedValue(undefined); + const sendComposing = vi.fn(); + const resolver = vi.fn().mockResolvedValue({ text: "ok" }); + + let capturedOnMessage: + | ((msg: import("./inbound.js").WebInboundMessage) => Promise) + | undefined; + const listenerFactory = async (opts: { + onMessage: ( + msg: import("./inbound.js").WebInboundMessage, + ) => Promise; + }) => { + capturedOnMessage = opts.onMessage; + return { close: vi.fn() }; + }; + + const authDir = await fs.mkdtemp( + path.join(os.tmpdir(), "clawdbot-wa-auth-"), + ); + + try { + await fs.writeFile( + path.join(authDir, "lid-mapping-555_reverse.json"), + JSON.stringify("15551234"), + ); + + setLoadConfigMock(() => ({ + whatsapp: { + allowFrom: ["*"], + accounts: { + default: { authDir }, + }, + }, + })); + + await monitorWebProvider(false, listenerFactory, false, resolver); + expect(capturedOnMessage).toBeDefined(); + + await capturedOnMessage?.({ + body: "hello group", + from: "123@g.us", + conversationId: "123@g.us", + chatId: "123@g.us", + chatType: "group", + to: "+2", + id: "g1", + senderE164: "+111", + senderName: "Alice", + selfE164: "+15551234", + sendComposing, + reply, + sendMedia, + }); + + await capturedOnMessage?.({ + body: "@bot ping", + from: "123@g.us", + conversationId: "123@g.us", + chatId: "123@g.us", + chatType: "group", + to: "+2", + id: "g2", + senderE164: "+222", + senderName: "Bob", + mentionedJids: ["555@lid"], + selfE164: "+15551234", + selfJid: "15551234@s.whatsapp.net", + sendComposing, + reply, + sendMedia, + }); + + expect(resolver).toHaveBeenCalledTimes(1); + } finally { + resetLoadConfigMock(); + await rmDirWithRetries(authDir); + } + }); + + it("derives self E.164 from LID selfJid for mention gating", async () => { + const sendMedia = vi.fn(); + const reply = vi.fn().mockResolvedValue(undefined); + const sendComposing = vi.fn(); + const resolver = vi.fn().mockResolvedValue({ text: "ok" }); + + let capturedOnMessage: + | ((msg: import("./inbound.js").WebInboundMessage) => Promise) + | undefined; + const listenerFactory = async (opts: { + onMessage: ( + msg: import("./inbound.js").WebInboundMessage, + ) => Promise; + }) => { + capturedOnMessage = opts.onMessage; + return { close: vi.fn() }; + }; + + const authDir = await fs.mkdtemp( + path.join(os.tmpdir(), "clawdbot-wa-auth-"), + ); + + try { + await fs.writeFile( + path.join(authDir, "lid-mapping-777_reverse.json"), + JSON.stringify("15550077"), + ); + + setLoadConfigMock(() => ({ + whatsapp: { + allowFrom: ["*"], + accounts: { + default: { authDir }, + }, + }, + })); + + await monitorWebProvider(false, listenerFactory, false, resolver); + expect(capturedOnMessage).toBeDefined(); + + await capturedOnMessage?.({ + body: "@bot ping", + from: "123@g.us", + conversationId: "123@g.us", + chatId: "123@g.us", + chatType: "group", + to: "+2", + id: "g3", + senderE164: "+333", + senderName: "Cara", + mentionedJids: ["777@lid"], + selfJid: "777@lid", + sendComposing, + reply, + sendMedia, + }); + + expect(resolver).toHaveBeenCalledTimes(1); + } finally { + resetLoadConfigMock(); + await rmDirWithRetries(authDir); + } + }); + it("sets OriginatingTo to the sender for queued routing", async () => { const sendMedia = vi.fn(); const reply = vi.fn().mockResolvedValue(undefined); diff --git a/src/web/auto-reply.ts b/src/web/auto-reply.ts index 8306b8186..c41f6de11 100644 --- a/src/web/auto-reply.ts +++ b/src/web/auto-reply.ts @@ -178,6 +178,12 @@ type MentionConfig = { allowFrom?: Array; }; +type MentionTargets = { + normalizedMentions: string[]; + selfE164: string | null; + selfJid: string | null; +}; + function buildMentionConfig( cfg: ReturnType, agentId?: string, @@ -186,26 +192,42 @@ function buildMentionConfig( return { mentionRegexes, allowFrom: cfg.whatsapp?.allowFrom }; } -function isBotMentioned( +function resolveMentionTargets( + msg: WebInboundMsg, + authDir?: string, +): MentionTargets { + const jidOptions = authDir ? { authDir } : undefined; + const normalizedMentions = msg.mentionedJids?.length + ? msg.mentionedJids + .map((jid) => jidToE164(jid, jidOptions) ?? jid) + .filter(Boolean) + : []; + const selfE164 = + msg.selfE164 ?? (msg.selfJid ? jidToE164(msg.selfJid, jidOptions) : null); + const selfJid = msg.selfJid ? msg.selfJid.replace(/:\\d+/, "") : null; + return { normalizedMentions, selfE164, selfJid }; +} + +function isBotMentionedFromTargets( msg: WebInboundMsg, mentionCfg: MentionConfig, - authDir?: string, + targets: MentionTargets, ): boolean { const clean = (text: string) => // Remove zero-width and directionality markers WhatsApp injects around display names normalizeMentionText(text); - const isSelfChat = isSelfChatMode(msg.selfE164, mentionCfg.allowFrom); + const isSelfChat = isSelfChatMode(targets.selfE164, mentionCfg.allowFrom); if (msg.mentionedJids?.length && !isSelfChat) { - const normalizedMentions = msg.mentionedJids - .map((jid) => jidToE164(jid, authDir ? { authDir } : undefined) ?? jid) - .filter(Boolean); - if (msg.selfE164 && normalizedMentions.includes(msg.selfE164)) return true; - if (msg.selfJid && msg.selfE164) { + if ( + targets.selfE164 && + targets.normalizedMentions.includes(targets.selfE164) + ) + return true; + if (targets.selfJid && targets.selfE164) { // Some mentions use the bare JID; match on E.164 to be safe. - const bareSelf = msg.selfJid.replace(/:\\d+/, ""); - if (normalizedMentions.includes(bareSelf)) return true; + if (targets.normalizedMentions.includes(targets.selfJid)) return true; } } else if (msg.mentionedJids?.length && isSelfChat) { // Self-chat mode: ignore WhatsApp @mention JIDs, otherwise @mentioning the owner in group chats triggers the bot. @@ -214,8 +236,8 @@ function isBotMentioned( if (mentionCfg.mentionRegexes.some((re) => re.test(bodyClean))) return true; // Fallback: detect body containing our own number (with or without +, spacing) - if (msg.selfE164) { - const selfDigits = msg.selfE164.replace(/\D/g, ""); + if (targets.selfE164) { + const selfDigits = targets.selfE164.replace(/\D/g, ""); if (selfDigits) { const bodyDigits = bodyClean.replace(/[^\d]/g, ""); if (bodyDigits.includes(selfDigits)) return true; @@ -233,14 +255,20 @@ function debugMention( mentionCfg: MentionConfig, authDir?: string, ): { wasMentioned: boolean; details: Record } { - const result = isBotMentioned(msg, mentionCfg, authDir); + const mentionTargets = resolveMentionTargets(msg, authDir); + const result = isBotMentionedFromTargets(msg, mentionCfg, mentionTargets); const details = { from: msg.from, body: msg.body, bodyClean: normalizeMentionText(msg.body), mentionedJids: msg.mentionedJids ?? null, + normalizedMentionedJids: mentionTargets.normalizedMentions.length + ? mentionTargets.normalizedMentions + : null, selfJid: msg.selfJid ?? null, + selfJidBare: mentionTargets.selfJid, selfE164: msg.selfE164 ?? null, + resolvedSelfE164: mentionTargets.selfE164, }; return { wasMentioned: result, details }; } @@ -1586,7 +1614,11 @@ export async function monitorWebProvider( groupHistories.set(groupHistoryKey, history); } - const mentionDebug = debugMention(msg, mentionConfig, account.authDir); + const mentionDebug = debugMention( + msg, + mentionConfig, + account.authDir, + ); replyLogger.debug( { conversationId, diff --git a/src/web/session.ts b/src/web/session.ts index f56fdd006..90e67a1bd 100644 --- a/src/web/session.ts +++ b/src/web/session.ts @@ -405,7 +405,7 @@ export function readWebSelfId(authDir: string = resolveDefaultWebAuthDir()) { const raw = fsSync.readFileSync(credsPath, "utf-8"); const parsed = JSON.parse(raw) as { me?: { id?: string } } | undefined; const jid = parsed?.me?.id ?? null; - const e164 = jid ? jidToE164(jid) : null; + const e164 = jid ? jidToE164(jid, { authDir }) : null; return { e164, jid } as const; } catch { return { e164: null, jid: null } as const;