fix(whatsapp): restore implicit reply mentions for LID identities (#48494)

Threads selfLid from the Baileys socket through the inbound WhatsApp
pipeline and adds LID-format matching to the implicit mention check
in group gating, so reply-to-bot detection works when WhatsApp sends
the quoted sender in @lid format.

Also fixes the device-suffix stripping regex (was a silent no-op).

Closes #23029

Co-authored-by: sparkyrider <sparkyrider@users.noreply.github.com>
Reviewed-by: @ademczuk
This commit is contained in:
sparkyrider
2026-03-16 16:44:35 -05:00
committed by GitHub
parent 2ab25babce
commit 10ef58dd69
7 changed files with 47 additions and 7 deletions

View File

@@ -183,6 +183,7 @@ Docs: https://docs.openclaw.ai
- Auth/login lockout recovery: clear stale `auth_permanent` and `billing` disabled state for all profiles matching the target provider when `openclaw models auth login` is invoked, so users locked out by expired or revoked OAuth tokens can recover by re-authenticating instead of waiting for the cooldown timer to expire. (#43057)
- Auto-reply/context-engine compaction: persist the exact embedded-run metadata compaction count for main and followup runner session accounting, so metadata-only auto-compactions no longer undercount multi-compaction runs. (#42629) thanks @uf-hy.
- Auth/Codex CLI reuse: sync reused Codex CLI credentials into the supported `openai-codex:default` OAuth profile instead of reviving the deprecated `openai-codex:codex-cli` slot, so doctor cleanup no longer loops. (#45353) thanks @Gugu-sugar.
- WhatsApp/group replies: recognize implicit reply-to-bot mentions when WhatsApp sends the quoted sender in `@lid` format, including device-suffixed self identities. (#23029) Thanks @sparkyrider.
## 2026.3.12

View File

@@ -127,14 +127,17 @@ export function applyGroupGating(params: ApplyGroupGatingParams) {
conversationId: params.conversationId,
});
const requireMention = activation !== "always";
const selfJid = params.msg.selfJid?.replace(/:\\d+/, "");
const replySenderJid = params.msg.replyToSenderJid?.replace(/:\\d+/, "");
const selfJid = params.msg.selfJid?.replace(/:\d+/, "");
const selfLid = params.msg.selfLid?.replace(/:\d+/, "");
// replyToSenderJid may carry either a standard JID or an @lid identifier.
const replySenderJid = params.msg.replyToSenderJid?.replace(/:\d+/, "");
const selfE164 = params.msg.selfE164 ? normalizeE164(params.msg.selfE164) : null;
const replySenderE164 = params.msg.replyToSenderE164
? normalizeE164(params.msg.replyToSenderE164)
: null;
const implicitMention = Boolean(
(selfJid && replySenderJid && selfJid === replySenderJid) ||
(selfLid && replySenderJid && selfLid === replySenderJid) ||
(selfE164 && replySenderE164 && selfE164 === replySenderE164),
);
const mentionGate = resolveMentionGating({

View File

@@ -112,7 +112,7 @@ describe("applyGroupGating", () => {
accountId: "default",
body: "following up",
timestamp: Date.now(),
selfJid: "15551234567@s.whatsapp.net",
selfJid: "15551234567:1@s.whatsapp.net",
selfE164: "+15551234567",
replyToId: "m0",
replyToBody: "bot said hi",
@@ -125,6 +125,29 @@ describe("applyGroupGating", () => {
expect(result.shouldProcess).toBe(true);
});
it("treats LID-format reply-to-bot as implicit mention", () => {
const cfg = makeConfig({});
const { result } = runGroupGating({
cfg,
msg: createGroupMessage({
id: "m1-lid",
to: "+15550000",
accountId: "default",
body: "following up",
timestamp: Date.now(),
selfJid: "15551234567@s.whatsapp.net",
selfLid: "1234567890123:1@lid",
selfE164: "+15551234567",
replyToId: "m0",
replyToBody: "bot said hi",
replyToSender: "1234567890123@lid",
replyToSenderJid: "1234567890123@lid",
}),
});
expect(result.shouldProcess).toBe(true);
});
it.each([
{ id: "g-new", command: "/new" },
{ id: "g-status", command: "/status" },

View File

@@ -66,6 +66,7 @@ export async function monitorWebInbox(options: {
}
const selfJid = sock.user?.id;
const selfLid = sock.user?.lid;
const selfE164 = selfJid ? jidToE164(selfJid) : null;
const debouncer = createInboundDebouncer<WebInboundMessage>({
debounceMs: options.debounceMs ?? 0,
@@ -372,6 +373,7 @@ export async function monitorWebInbox(options: {
groupParticipants: inbound.groupParticipants,
mentionedJids: mentionedJids ?? undefined,
selfJid,
selfLid,
selfE164,
fromMe: Boolean(msg.key?.fromMe),
location: enriched.location ?? undefined,

View File

@@ -30,6 +30,7 @@ export type WebInboundMessage = {
groupParticipants?: string[];
mentionedJids?: string[];
selfJid?: string | null;
selfLid?: string | null;
selfE164?: string | null;
fromMe?: boolean;
location?: NormalizedLocation;

View File

@@ -118,7 +118,12 @@ describe("web monitor inbox", () => {
await tick();
expect(onMessage).toHaveBeenCalledWith(
expect.objectContaining({ body: "ping", from: "+999", to: "+123" }),
expect.objectContaining({
body: "ping",
from: "+999",
to: "+123",
selfLid: "123:1@lid",
}),
);
expect(sock.readMessages).toHaveBeenCalledWith([
{
@@ -181,7 +186,12 @@ describe("web monitor inbox", () => {
expect(getPNForLID).toHaveBeenCalledWith("999@lid");
expect(onMessage).toHaveBeenCalledWith(
expect.objectContaining({ body: "ping", from: "+999", to: "+123" }),
expect.objectContaining({
body: "ping",
from: "+999",
to: "+123",
selfLid: "123:1@lid",
}),
);
await listener.close();

View File

@@ -44,7 +44,7 @@ export type MockSock = {
getPNForLID: AnyMockFn;
};
};
user: { id: string };
user: { id: string; lid?: string };
};
function createResolvedMock() {
@@ -66,7 +66,7 @@ function createMockSock(): MockSock {
getPNForLID: vi.fn().mockResolvedValue(null),
},
},
user: { id: "123@s.whatsapp.net" },
user: { id: "123@s.whatsapp.net", lid: "123:1@lid" },
};
}