Skip to content

Commit 37de881

Browse files
lurebatmcaxtr
andauthored
fix(whatsapp): ignore self-chat quoted replies in groups (openclaw#60148)
Merged via squash. Prepared head SHA: c51b55e Co-authored-by: lurebat <154669821+lurebat@users.noreply.github.com> Co-authored-by: mcaxtr <7562095+mcaxtr@users.noreply.github.com> Reviewed-by: @mcaxtr
1 parent 2d2fe2b commit 37de881

4 files changed

Lines changed: 120 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,7 @@ Docs: https://docs.openclaw.ai
194194
- Telegram/local Bot API: preserve media MIME types for absolute-path downloads so local audio files still trigger transcription and other MIME-based handling. (#54603) Thanks @jzakirov
195195
- Channels/WhatsApp: pass inbound message timestamp to model context so the AI can see when WhatsApp messages were sent. (#58590) Thanks @Maninae
196196
- QQBot/voice: lazy-load `silk-wasm` in `audio-convert.ts` so qqbot still starts when the optional voice dependency is missing, while voice encode/decode degrades gracefully instead of crashing at module load time. (#58829) Thanks @WideLee.
197+
- WhatsApp/groups: fix bot waking up on self-number quoted replies in groups with `selfChatMode` enabled. (#60148) Thanks @lurebat
197198

198199
## 2026.3.31
199200

extensions/whatsapp/src/auto-reply/monitor/group-gating.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ type ApplyGroupGatingParams = {
4040
groupHistories: Map<string, GroupHistoryEntry[]>;
4141
groupHistoryLimit: number;
4242
groupMemberNames: Map<string, Map<string, string>>;
43+
selfChatMode?: boolean;
4344
logVerbose: (msg: string) => void;
4445
replyLogger: { debug: (obj: unknown, msg: string) => void };
4546
};
@@ -143,10 +144,15 @@ export function applyGroupGating(params: ApplyGroupGatingParams) {
143144
});
144145
const requireMention = activation !== "always";
145146
const replyContext = getReplyContext(params.msg, params.authDir);
147+
const sharedNumberSelfChat = params.selfChatMode === true;
146148
// Detect reply-to-bot: compare JIDs, LIDs, and E.164 numbers.
147149
// WhatsApp may report the quoted message sender as either a phone JID
148150
// (xxxxx@s.whatsapp.net) or a LID (xxxxx@lid), so we compare both.
149-
const implicitMention = identitiesOverlap(self, replyContext?.sender);
151+
// But in shared-number/selfChatMode setups, replies from the same self number
152+
// should not count as implicit bot mentions unless the message explicitly
153+
// mentioned the bot in text.
154+
const implicitReplyToSelf = sharedNumberSelfChat && identitiesOverlap(self, sender);
155+
const implicitMention = !implicitReplyToSelf && identitiesOverlap(self, replyContext?.sender);
150156
const mentionGate = resolveMentionGating({
151157
requireMention,
152158
canDetectMention: true,

extensions/whatsapp/src/auto-reply/monitor/on-message.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ export function createWebOnMessageHandler(params: {
2929
replyResolver: typeof getReplyFromConfig;
3030
replyLogger: ReturnType<(typeof import("openclaw/plugin-sdk/runtime-env"))["getChildLogger"]>;
3131
baseMentionConfig: MentionConfig;
32-
account: { authDir?: string; accountId?: string };
32+
account: { authDir?: string; accountId?: string; selfChatMode?: boolean };
3333
}) {
3434
const processForRoute = async (
3535
msg: WebInboundMsg,
@@ -135,6 +135,7 @@ export function createWebOnMessageHandler(params: {
135135
sessionKey: route.sessionKey,
136136
baseMentionConfig: params.baseMentionConfig,
137137
authDir: params.account.authDir,
138+
selfChatMode: params.account.selfChatMode,
138139
groupHistories: params.groupHistories,
139140
groupHistoryLimit: params.groupHistoryLimit,
140141
groupMemberNames: params.groupMemberNames,

extensions/whatsapp/src/auto-reply/web-auto-reply-monitor.test.ts

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ function runGroupGating(params: {
4040
msg: Record<string, unknown>;
4141
conversationId?: string;
4242
agentId?: string;
43+
selfChatMode?: boolean;
4344
}) {
4445
const groupHistories = new Map<string, GroupHistoryEntry[]>();
4546
const conversationId = params.conversationId ?? "123@g.us";
@@ -55,6 +56,7 @@ function runGroupGating(params: {
5556
agentId,
5657
sessionKey,
5758
baseMentionConfig,
59+
selfChatMode: params.selfChatMode,
5860
groupHistories,
5961
groupHistoryLimit: 10,
6062
groupMemberNames: new Map(),
@@ -125,6 +127,114 @@ describe("applyGroupGating", () => {
125127
expect(result.shouldProcess).toBe(true);
126128
});
127129

130+
it("does not treat self-number quoted replies as implicit mention in selfChatMode groups", () => {
131+
const cfg = makeConfig({
132+
channels: {
133+
whatsapp: {
134+
selfChatMode: true,
135+
groupPolicy: "open",
136+
groups: { "*": { requireMention: true } },
137+
},
138+
},
139+
});
140+
const { result } = runGroupGating({
141+
cfg,
142+
selfChatMode: true,
143+
msg: createGroupMessage({
144+
id: "m-self-reply",
145+
to: "+15550000",
146+
accountId: "default",
147+
body: "following up on my own message",
148+
timestamp: Date.now(),
149+
senderE164: "+15551234567",
150+
senderJid: "15551234567@s.whatsapp.net",
151+
selfJid: "15551234567@s.whatsapp.net",
152+
selfE164: "+15551234567",
153+
replyToId: "m0",
154+
replyToBody: "my earlier message",
155+
replyToSender: "+15551234567",
156+
replyToSenderJid: "15551234567@s.whatsapp.net",
157+
replyToSenderE164: "+15551234567",
158+
}),
159+
});
160+
161+
expect(result.shouldProcess).toBe(false);
162+
});
163+
164+
it("still treats reply-to-bot as implicit mention in selfChatMode when sender is a different user", () => {
165+
const cfg = makeConfig({
166+
channels: {
167+
whatsapp: {
168+
selfChatMode: true,
169+
groupPolicy: "open",
170+
groups: { "*": { requireMention: true } },
171+
},
172+
},
173+
});
174+
const { result } = runGroupGating({
175+
cfg,
176+
selfChatMode: true,
177+
msg: createGroupMessage({
178+
id: "m-other-reply",
179+
to: "+15550000",
180+
accountId: "default",
181+
body: "following up on bot reply",
182+
timestamp: Date.now(),
183+
senderE164: "+15559999999",
184+
senderJid: "15559999999@s.whatsapp.net",
185+
selfJid: "15551234567@s.whatsapp.net",
186+
selfE164: "+15551234567",
187+
replyToId: "m0",
188+
replyToBody: "bot earlier response",
189+
replyToSender: "+15551234567",
190+
replyToSenderJid: "15551234567@s.whatsapp.net",
191+
replyToSenderE164: "+15551234567",
192+
}),
193+
});
194+
195+
expect(result.shouldProcess).toBe(true);
196+
});
197+
198+
it("honors per-account selfChatMode overrides before suppressing implicit mentions", () => {
199+
const cfg = makeConfig({
200+
channels: {
201+
whatsapp: {
202+
selfChatMode: true,
203+
groupPolicy: "open",
204+
groups: { "*": { requireMention: true } },
205+
accounts: {
206+
work: {
207+
selfChatMode: false,
208+
},
209+
},
210+
},
211+
},
212+
});
213+
// Per-account override: work account has selfChatMode: false despite root being true
214+
const { result } = runGroupGating({
215+
cfg,
216+
selfChatMode: false,
217+
msg: createGroupMessage({
218+
id: "m-account-override",
219+
to: "+15550000",
220+
accountId: "work",
221+
body: "following up on bot reply",
222+
timestamp: Date.now(),
223+
senderE164: "+15551234567",
224+
senderJid: "15551234567@s.whatsapp.net",
225+
selfJid: "15551234567@s.whatsapp.net",
226+
selfE164: "+15551234567",
227+
replyToId: "m0",
228+
replyToBody: "bot earlier response",
229+
replyToSender: "+15551234567",
230+
replyToSenderJid: "15551234567@s.whatsapp.net",
231+
replyToSenderE164: "+15551234567",
232+
}),
233+
});
234+
235+
expect(result.shouldProcess).toBe(true);
236+
});
237+
128238
it.each([
129239
{ id: "g-new", command: "/new" },
130240
{ id: "g-status", command: "/status" },

0 commit comments

Comments
 (0)