feat(whatsapp): inbound reaction events (#1)

* feat(whatsapp): subscribe to inbound reaction events

Subscribe to Baileys `messages.reaction` events and surface them as
system events for the agent session. Follows the same pattern used by
Signal and Slack: listen → parse → route → enqueueSystemEvent.

- Add `WebInboundReaction` type and `onReaction` callback to monitor
- Parse emoji, sender JID, target message ID from Baileys event
- Route via `resolveAgentRoute` and enqueue as system event
- Handle self-reaction attribution (reactionKey.fromMe → selfJid)
- Skip reaction removals (empty emoji) and status/broadcast JIDs
- Outer + inner try/catch for resilience against malformed events
- 13 unit tests (monitor-level + system event wiring)
- Changelog entry and docs updates

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* revert changelog entry to avoid merge conflicts

The inbound reaction feature is internal plumbing, not user-facing enough
to warrant a changelog entry that will conflict with active PRs.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat(whatsapp): handle reaction removals and improve sender detection

- Emit reaction events with isRemoval flag instead of skipping them
- Fall back to chatJid for DM sender when reaction.key is missing
- Add chatType and accountId to reaction logs for observability
- Update tests to reflect new removal handling behavior

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(whatsapp): gate reactions by DM/group access controls

Address Codex review - reactions now respect the same access controls
as messages (dmPolicy, allowlists, etc). Self-reactions bypass the
check since they're our own actions, not inbound events.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* chore: remove unused senderName field from reaction type

Baileys reaction events don't include push names, so this field
was dead interface pollution. (Cursor review)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(whatsapp): update reaction docs + suppress pairing for reactions

- Update docs: reaction removals now emit events with isRemoval=true
- Pass no-op sendMessage to access control to prevent pairing messages
  being sent when unknown users react (pairing is for messages, not reactions)

(Cursor review)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Nick Sullivan <nick@technick.ai>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
authored and Nick Sullivan committed 2026-01-30 10:44:00 -06:00
1 parent 09be5d45d5
commit 5571967ebd
8 files changed
+844 -3

No files matched your search

+15
View File
@@ -269,6 +269,21 @@ WhatsApp can automatically send emoji reactions to incoming messages immediately
- Reaction removal semantics: see [/tools/reactions](/tools/reactions).
- Tool gating: `channels.whatsapp.actions.reactions` (default: enabled).
## Inbound reaction notifications
When someone reacts to a message in a WhatsApp chat, the gateway emits a system event so the agent sees it in its next prompt. System events appear as lines like:
```
WhatsApp reaction added: 👍 by +1234567890 msg BAE5ABC123
WhatsApp reaction removed by +1234567890 msg BAE5ABC123
```
- Reaction removals emit events with `isRemoval: true` and no emoji in the message.
- Self-reactions in DMs are correctly attributed to the bot's own JID.
- Group reactions include the participant who reacted.
- Events are deduplicated by message ID, sender, and emoji to avoid repeat notifications.
- No configuration required; inbound reactions are always surfaced when the gateway is running.
## Limits
- Outbound text is chunked to `channels.whatsapp.textChunkLimit` (default 4000).
- Optional newline chunking: set `channels.whatsapp.chunkMode="newline"` to split on blank lines (paragraph boundaries) before length chunking.
+2
View File
@@ -3,6 +3,7 @@ summary: "Reaction semantics shared across channels"
read_when:
- Working on reactions in any channel
---
# Reaction tooling
Shared reaction semantics across channels:
@@ -18,3 +19,4 @@ Channel notes:
- **Telegram**: empty `emoji` removes the bot's reactions; `remove: true` also removes reactions but still requires a non-empty `emoji` for tool validation.
- **WhatsApp**: empty `emoji` removes the bot reaction; `remove: true` maps to empty emoji (still requires `emoji`).
- **Signal**: inbound reaction notifications emit system events when `channels.signal.reactionNotifications` is enabled.
- **WhatsApp**: inbound reaction notifications are always surfaced as system events (no configuration required).