feat(whatsapp): implement unified reaction contract on Baileys adapter - #50670
Closed
aldoeliacim wants to merge 2 commits into
Closed
aldoeliacim wants to merge 2 commits into
aldoeliacim wants to merge 2 commits into
Conversation
aldoeliacim
force-pushed
the
feat/whatsapp-reactions
branch
from
June 22, 2026 08:00
ed3ecdc to
97802c3
Compare
12 tasks
aldoeliacim
force-pushed
the
feat/whatsapp-reactions
branch
from
June 22, 2026 14:47
97802c3 to
2a55889
Compare
aldoeliacim
force-pushed
the
feat/whatsapp-reactions
branch
2 times, most recently
from
June 30, 2026 11:21
408d58f to
7ad1747
Compare
aldoeliacim
force-pushed
the
feat/whatsapp-reactions
branch
from
July 1, 2026 20:43
7ad1747 to
62061af
Compare
Contributor
There was a problem hiding this comment.
Pull request overview
Implements the unified cross-platform reaction contract across adapters and wires the Baileys WhatsApp adapter into it by routing reactions through a bounded, TTL’d message cache in the WhatsApp bridge and a single /react HTTP path.
Changes:
- Adds
SUPPORTS_REACTIONSplus defaultadd_reaction/remove_reactioncoroutines toBasePlatformAdapter, and updates thesend_messagetool to gate on the capability flag. - Implements the unified reaction contract for Signal, Telegram, Photon, and WhatsApp (WhatsApp via bridge
/react). - Adds unit/integration tests covering the unified contract and WhatsApp reaction behavior (Python + Node).
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| tools/send_message_tool.py | Gates react/unreact on SUPPORTS_REACTIONS instead of method presence. |
| tests/tools/test_send_message_react.py | Updates test fakes to advertise the capability flag. |
| tests/gateway/test_whatsapp_reactions.py | New WhatsApp adapter reaction tests (bridge payloads, guards, error propagation). |
| tests/gateway/test_unified_reactions_contract.py | New invariant tests for the base reaction contract + adapter opt-in. |
| scripts/whatsapp-bridge/bridge.test.mjs | New Node tests for the bridge message store behavior (TTL, participant retention). |
| scripts/whatsapp-bridge/bridge.js | Adds bounded/TTL’d message cache and /react endpoint for native Baileys reactions. |
| plugins/platforms/whatsapp/adapter.py | Implements add_reaction/remove_reaction via bridge /react; flips capability flag. |
| plugins/platforms/telegram/adapter.py | Implements unified reaction coroutines; flips capability flag. |
| plugins/platforms/photon/adapter.py | Flips capability flag to match existing unified reaction implementation. |
| gateway/platforms/signal.py | Normalizes Signal reactions onto unified contract; flips capability flag. |
| gateway/platforms/base.py | Introduces unified reaction contract + default structured “not supported” behavior. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+243
to
+244
| const MAX_STORED_MESSAGES = parseInt(process.env.WHATSAPP_MESSAGE_STORE_MAX || '5000', 10); | ||
| const MESSAGE_STORE_TTL_MS = parseInt(process.env.WHATSAPP_MESSAGE_STORE_TTL_MS || String(24 * 60 * 60 * 1000), 10); |
Comment on lines
+238
to
+242
| // Full WAMessage cache used for native WhatsApp reactions. Baileys needs the | ||
| // original message's key (incl. group `participant`) to attach a reaction, so | ||
| // we remember inbound + sent messages as they flow through. Bounded by size and | ||
| // TTL. Exported so the bridge's node:test harness can exercise it directly. | ||
| export const messageStore = new Map(); |
aldoeliacim
force-pushed
the
feat/whatsapp-reactions
branch
3 times, most recently
from
July 4, 2026 21:37
48e2a5f to
38ae617
Compare
…rmAdapter
Reactions are the lightweight "social acknowledgement" output channel — a
participation primitive distinct from sending a message (a thumbs/eyes/laugh
that says "seen / agreed / amused" without composing a reply). Most chat
platforms support them, but each adapter exposed reactions ad-hoc with a
different method name and signature, and BasePlatformAdapter had no contract
at all:
- Signal: send_reaction(chat_id, emoji, target_author, target_timestamp)
- Telegram: private _set_reaction(chat_id, message_id, emoji)
- Photon: add_reaction(chat_id, emoji, message_id=None)
- (others surfaced nothing)
The send_message tool's action="react"/"unreact" calls
add_reaction(chat_id, emoji, message_id) / remove_reaction(chat_id, message_id),
so the agent's reaction channel only actually worked on Photon — every other
platform fell through the getattr() probe with "does not support reactions".
This adds the missing waist:
* BasePlatformAdapter gains SUPPORTS_REACTIONS (default False) plus default
add_reaction/remove_reaction coroutines returning a structured
{"success": False, "error": ...} so callers never probe for method
existence.
* Signal normalizes onto the unified signature: native sendReaction wiring is
renamed to _send_reaction_raw/_remove_reaction_raw (still driving the
processing-lifecycle 👀/✅ hooks), and the public add_reaction/remove_reaction
accept the composite "<author>:<timestamp_ms>" message id Signal needs.
* Telegram promotes its private set/clear-reaction helpers to the public
contract (message_id required — Telegram has no "latest message" affordance).
* Photon already matched the contract; it just advertises SUPPORTS_REACTIONS.
* The send_message tool gates on SUPPORTS_REACTIONS instead of method
presence, giving a clean "platform does not support reactions" answer.
Adapters without reaction support inherit the no-op default and report
SUPPORTS_REACTIONS=False, so behavior is unchanged for them.
Tests: new tests/gateway/test_unified_reactions_contract.py pins the base
contract, the Signal composite-id round-trip, and that each reaction-capable
adapter advertises the flag and overrides both coroutines with the unified
signature. Existing react-dispatch tests updated to the capability-flag gate.
181 reaction-related + 208 signal tests pass.
The unified reaction contract (#50661) normalized add_reaction /
remove_reaction across Signal, Telegram, and Photon, but left the
Baileys WhatsApp adapter at the default SUPPORTS_REACTIONS=False — even
though Baileys supports reactions natively. This wires WhatsApp into the
same contract, reusing the bounded TTL'd message store that the existing
whatsapp_action tool already reacts through (one store, one /react path).
Bridge (scripts/whatsapp-bridge/bridge.js):
- Add a bounded, TTL'd WAMessage store (messageStore + rememberMessage +
resolveStoredMessage), populated as inbound and sent messages flow
through. Baileys needs the original message's key (incl. group
participant) to attach a reaction, which chatId+id alone can't recover.
- POST /react resolves the target via resolveStoredMessage and calls
sock.sendMessage(jid, { react: { text, key } }); 404 when uncached,
empty emoji retracts. This matches the contract the whatsapp_action
tool already calls, so both reaction paths share one store.
- Guard server startup behind isMain so the store helpers can be imported
by the node:test harness without binding a port.
Adapter (plugins/platforms/whatsapp/adapter.py):
- Set SUPPORTS_REACTIONS = True.
- add_reaction / remove_reaction POST to the bridge /react endpoint,
mirroring the existing edit_message HTTP pattern and guards.
Tests:
- bridge.test.mjs: store remember/resolve, group participant retention,
TTL eviction, keyless-message rejection.
- test_whatsapp_reactions.py: payload shape, empty-emoji clear, missing
message_id, 404 uncached, bridge-error propagation, not-connected.
- Extend test_unified_reactions_contract.py to assert the WhatsApp
adapter advertises the flag and overrides both coroutines.
aldoeliacim
force-pushed
the
feat/whatsapp-reactions
branch
from
July 5, 2026 15:27
38ae617 to
43af129
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Wires the Baileys WhatsApp adapter into the unified cross-platform reaction
contract introduced in #50661, reusing a bounded TTL'd message store so the
adapter contract and the existing
whatsapp_actiontool react through onestore and one
/reactpath.#50661 normalized
add_reaction/remove_reactionacross Signal, Telegram,and Photon and added the
SUPPORTS_REACTIONScapability flag toBasePlatformAdapter. It left WhatsApp at the defaultSUPPORTS_REACTIONS = False— even though Baileys supports reactions natively.This PR closes that gap.
Why
Reactions are a participation primitive (👀 / 😂 / 👍 — "seen / amused / agreed"
without composing a reply). The
send_messagetool'sreactaction gates onSUPPORTS_REACTIONS, so without this the agent reports "not supported here" onWhatsApp. Baileys exposes reactions via
sock.sendMessage(jid, { react: { text, key } }), but it needs the originalmessage's key — including the group
participant, whichchatId + idalonecannot recover. Hence a small message store.
How
Bridge (
scripts/whatsapp-bridge/bridge.js)messageStorewithrememberMessage/resolveStoredMessage,populated as inbound and sent messages flow through. Stores the full WAMessage
so the reaction key (with group
participant) survives. Size- and time-bounded(
WHATSAPP_MESSAGE_STORE_MAX,WHATSAPP_MESSAGE_STORE_TTL_MS).POST /reactresolves the target viaresolveStoredMessage→sock.sendMessage(jid, { react: { text, key } });404when uncached, emptyemoji retracts. This is the exact contract the
whatsapp_actiontool alreadycalls, so both reaction paths share one store rather than diverging.
isMainso the store helpers can be imported bythe
node:testharness without binding a port.Adapter (
plugins/platforms/whatsapp/adapter.py)SUPPORTS_REACTIONS = True.add_reaction/remove_reactionPOST to/react, mirroring the existingedit_messageHTTP pattern and connection guards.Testing
scripts/whatsapp-bridge/bridge.test.mjs(new): store remember/resolve, groupparticipantretention, TTL eviction, keyless-message rejection.tests/gateway/test_whatsapp_reactions.py(new): payload shape, empty-emojiclear, missing-
message_idguard,404uncached, bridge-error propagation,not-connected short-circuit.
tests/gateway/test_unified_reactions_contract.py: extended to assert theWhatsApp adapter advertises the flag and overrides both coroutines.
Notes
carry safe defaults). The consumer (the
send_messagereact action) ships infeat(gateway): unified cross-platform reaction contract on BasePlatformAdapter #50661; this PR is the WhatsApp implementation behind the same contract.
default-deny middleware apply).