Merge pull request #640 from mcinteerj/fix/whatsapp-group-reactions

fix(whatsapp): enable reactions in group chats
This commit is contained in:
Peter Steinberger
2026-01-10 19:44:05 +00:00
committed by GitHub
5 changed files with 67 additions and 13 deletions

View File

@@ -12,6 +12,7 @@
### Fixes ### Fixes
- Auto-reply: prefer `RawBody` for command/directive parsing (WhatsApp + Discord) and prevent fallback runs from clobbering concurrent session updates. (#643) — thanks @mcinteerj. - Auto-reply: prefer `RawBody` for command/directive parsing (WhatsApp + Discord) and prevent fallback runs from clobbering concurrent session updates. (#643) — thanks @mcinteerj.
- WhatsApp: fix group reactions by preserving message IDs and sender JIDs in history; normalize participant phone numbers to JIDs in outbound reactions. (#640) — thanks @mcinteerj.
- Cron: `wakeMode: "now"` waits for heartbeat completion (and retries when the main lane is busy). (#666) — thanks @roshanasingh4. - Cron: `wakeMode: "now"` waits for heartbeat completion (and retries when the main lane is busy). (#666) — thanks @roshanasingh4.
- Agents/OpenAI: fix Responses tool-only → follow-up turn handling (avoid standalone `reasoning` items that trigger 400 “required following item”). - Agents/OpenAI: fix Responses tool-only → follow-up turn handling (avoid standalone `reasoning` items that trigger 400 “required following item”).
- Sandbox: add `clawdbot sandbox explain` (effective policy inspector + fix-it keys); improve “sandbox jail” tool-policy/elevated errors with actionable config key paths; link to docs. - Sandbox: add `clawdbot sandbox explain` (effective policy inspector + fix-it keys); improve “sandbox jail” tool-policy/elevated errors with actionable config key paths; link to docs.
@@ -42,7 +43,6 @@
- Telegram: serialize media-group processing to avoid missed albums under load. - Telegram: serialize media-group processing to avoid missed albums under load.
- Signal: handle `dataMessage.reaction` events (signal-cli SSE) to avoid broken attachment errors. (#637) — thanks @neist. - Signal: handle `dataMessage.reaction` events (signal-cli SSE) to avoid broken attachment errors. (#637) — thanks @neist.
- Docs: showcase entries for ParentPay, R2 Upload, iOS TestFlight, and Oura Health. (#650) — thanks @henrino3. - Docs: showcase entries for ParentPay, R2 Upload, iOS TestFlight, and Oura Health. (#650) — thanks @henrino3.
## 2026.1.9 ## 2026.1.9
### Highlights ### Highlights

View File

@@ -1127,7 +1127,8 @@ describe("web auto-reply", () => {
expect(resolver).toHaveBeenCalledTimes(1); expect(resolver).toHaveBeenCalledTimes(1);
const payload = resolver.mock.calls[0][0]; const payload = resolver.mock.calls[0][0];
expect(payload.Body).toContain("Chat messages since your last reply"); expect(payload.Body).toContain("Chat messages since your last reply");
expect(payload.Body).toContain("Alice: hello group"); expect(payload.Body).toContain("Alice (+111): hello group");
expect(payload.Body).toContain("[message_id: g1]");
expect(payload.Body).toContain("@bot ping"); expect(payload.Body).toContain("@bot ping");
expect(payload.Body).toContain("[from: Bob (+222)]"); expect(payload.Body).toContain("[from: Bob (+222)]");
}); });
@@ -1483,7 +1484,8 @@ describe("web auto-reply", () => {
expect(resolver).toHaveBeenCalledTimes(2); expect(resolver).toHaveBeenCalledTimes(2);
const payload = resolver.mock.calls[1][0]; const payload = resolver.mock.calls[1][0];
expect(payload.Body).toContain("Chat messages since your last reply"); expect(payload.Body).toContain("Chat messages since your last reply");
expect(payload.Body).toContain("Alice: first"); expect(payload.Body).toContain("Alice (+111): first");
expect(payload.Body).toContain("[message_id: g-always-1]");
expect(payload.Body).toContain("Bob: second"); expect(payload.Body).toContain("Bob: second");
expect(reply).toHaveBeenCalledTimes(1); expect(reply).toHaveBeenCalledTimes(1);
@@ -2213,7 +2215,8 @@ describe("broadcast groups", () => {
for (const call of resolver.mock.calls.slice(0, 2)) { for (const call of resolver.mock.calls.slice(0, 2)) {
const payload = call[0] as { Body: string }; const payload = call[0] as { Body: string };
expect(payload.Body).toContain("Chat messages since your last reply"); expect(payload.Body).toContain("Chat messages since your last reply");
expect(payload.Body).toContain("Alice: hello group"); expect(payload.Body).toContain("Alice (+111): hello group");
expect(payload.Body).toContain("[message_id: g1]");
expect(payload.Body).toContain("@bot ping"); expect(payload.Body).toContain("@bot ping");
expect(payload.Body).toContain("[from: Bob (+222)]"); expect(payload.Body).toContain("[from: Bob (+222)]");
} }
@@ -2239,7 +2242,7 @@ describe("broadcast groups", () => {
expect(resolver).toHaveBeenCalledTimes(4); expect(resolver).toHaveBeenCalledTimes(4);
for (const call of resolver.mock.calls.slice(2, 4)) { for (const call of resolver.mock.calls.slice(2, 4)) {
const payload = call[0] as { Body: string }; const payload = call[0] as { Body: string };
expect(payload.Body).not.toContain("Alice: hello group"); expect(payload.Body).not.toContain("Alice (+111): hello group");
expect(payload.Body).not.toContain("Chat messages since your last reply"); expect(payload.Body).not.toContain("Chat messages since your last reply");
} }

View File

@@ -826,7 +826,13 @@ export async function monitorWebProvider(
DEFAULT_GROUP_HISTORY_LIMIT; DEFAULT_GROUP_HISTORY_LIMIT;
const groupHistories = new Map< const groupHistories = new Map<
string, string,
Array<{ sender: string; body: string; timestamp?: number }> Array<{
sender: string;
body: string;
timestamp?: number;
id?: string;
senderJid?: string;
}>
>(); >();
const groupMemberNames = new Map<string, Map<string, string>>(); const groupMemberNames = new Map<string, Map<string, string>>();
const sleep = const sleep =
@@ -1104,6 +1110,8 @@ export async function monitorWebProvider(
sender: string; sender: string;
body: string; body: string;
timestamp?: number; timestamp?: number;
id?: string;
senderJid?: string;
}>; }>;
suppressGroupHistoryClear?: boolean; suppressGroupHistoryClear?: boolean;
}, },
@@ -1123,14 +1131,17 @@ export async function monitorWebProvider(
if (historyWithoutCurrent.length > 0) { if (historyWithoutCurrent.length > 0) {
const lineBreak = "\\n"; const lineBreak = "\\n";
const historyText = historyWithoutCurrent const historyText = historyWithoutCurrent
.map((m) => .map((m) => {
formatAgentEnvelope({ const bodyWithId = m.id
? `${m.body}\n[message_id: ${m.id}]`
: m.body;
return formatAgentEnvelope({
provider: "WhatsApp", provider: "WhatsApp",
from: conversationId, from: conversationId,
timestamp: m.timestamp, timestamp: m.timestamp,
body: `${m.sender}: ${m.body}`, body: `${m.sender}: ${bodyWithId}`,
}), });
) })
.join(lineBreak); .join(lineBreak);
combinedBody = buildHistoryContext({ combinedBody = buildHistoryContext({
historyText, historyText,
@@ -1554,11 +1565,19 @@ export async function monitorWebProvider(
sender: string; sender: string;
body: string; body: string;
timestamp?: number; timestamp?: number;
id?: string;
senderJid?: string;
}>); }>);
const sender =
msg.senderName && msg.senderE164
? `${msg.senderName} (${msg.senderE164})`
: (msg.senderName ?? msg.senderE164 ?? "Unknown");
history.push({ history.push({
sender: msg.senderName ?? msg.senderE164 ?? "Unknown", sender,
body: msg.body, body: msg.body,
timestamp: msg.timestamp, timestamp: msg.timestamp,
id: msg.id,
senderJid: msg.senderJid,
}); });
while (history.length > groupHistoryLimit) history.shift(); while (history.length > groupHistoryLimit) history.shift();
groupHistories.set(groupHistoryKey, history); groupHistories.set(groupHistoryKey, history);

View File

@@ -620,7 +620,7 @@ export async function monitorWebInbox(options: {
remoteJid: jid, remoteJid: jid,
id: messageId, id: messageId,
fromMe, fromMe,
participant, participant: participant ? toWhatsappJid(participant) : undefined,
}, },
}, },
}); });

View File

@@ -1402,4 +1402,36 @@ describe("web monitor inbox", () => {
await listener.close(); await listener.close();
}); });
it("normalizes participant phone numbers to JIDs in sendReaction", async () => {
const listener = await monitorWebInbox({
verbose: false,
onMessage: vi.fn(),
accountId: ACCOUNT_ID,
authDir,
});
const sock = await createWaSocket();
await listener.sendReaction(
"12345@g.us",
"msg123",
"👍",
false,
"+6421000000",
);
expect(sock.sendMessage).toHaveBeenCalledWith("12345@g.us", {
react: {
text: "👍",
key: {
remoteJid: "12345@g.us",
id: "msg123",
fromMe: false,
participant: "6421000000@s.whatsapp.net",
},
},
});
await listener.close();
});
}); });