Skip to content

feat(whatsapp): implement unified reaction contract on Baileys adapter - #50670

Closed
aldoeliacim wants to merge 2 commits into
NousResearch:mainfrom
aldoeliacim:feat/whatsapp-reactions
Closed

aldoeliacim wants to merge 2 commits into
NousResearch:mainfrom
aldoeliacim:feat/whatsapp-reactions

Conversation

@aldoeliacim

@aldoeliacim aldoeliacim commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

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_action tool react through one
store and one /react path
.

#50661 normalized add_reaction / remove_reaction across Signal, Telegram,
and Photon and added the SUPPORTS_REACTIONS capability flag to
BasePlatformAdapter. It left WhatsApp at the default
SUPPORTS_REACTIONS = False — even though Baileys supports reactions natively.
This PR closes that gap.

Depends on #50661 (the base contract lives there). This branch is stacked
on top of it; please merge #50661 first. Until then the diff shows both
commits; once #50661 merges it collapses to just the WhatsApp commit.

Why

Reactions are a participation primitive (👀 / 😂 / 👍 — "seen / amused / agreed"
without composing a reply). The send_message tool's react action gates on
SUPPORTS_REACTIONS, so without this the agent reports "not supported here" on
WhatsApp. Baileys exposes reactions via
sock.sendMessage(jid, { react: { text, key } }), but it needs the original
message's key
— including the group participant, which chatId + id alone
cannot recover. Hence a small message store.

How

Bridge (scripts/whatsapp-bridge/bridge.js)

  • Bounded, TTL'd messageStore with rememberMessage / 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 /react resolves the target via resolveStoredMessage
    sock.sendMessage(jid, { react: { text, key } }); 404 when uncached, empty
    emoji retracts. This is the exact contract the whatsapp_action tool already
    calls
    , so both reaction paths share one store rather than diverging.
  • Server startup guarded behind isMain so the store helpers can be imported by
    the node:test harness without binding a port.

Adapter (plugins/platforms/whatsapp/adapter.py)

  • SUPPORTS_REACTIONS = True.
  • add_reaction / remove_reaction POST to /react, mirroring the existing
    edit_message HTTP pattern and connection guards.

Testing

  • scripts/whatsapp-bridge/bridge.test.mjs (new): store remember/resolve, group
    participant retention, TTL eviction, keyless-message rejection.
  • tests/gateway/test_whatsapp_reactions.py (new): payload shape, empty-emoji
    clear, missing-message_id guard, 404 uncached, bridge-error propagation,
    not-connected short-circuit.
  • tests/gateway/test_unified_reactions_contract.py: extended to assert the
    WhatsApp adapter advertises the flag and overrides both coroutines.
556 passed, 6 skipped   # pytest tests/gateway tests/tools -k "whatsapp or reaction or send_message"
10 passed               # node --test bridge.test.mjs allowlist.test.mjs
ruff clean; node --check clean

Notes

@alt-glitch alt-glitch added type/feature New feature or request platform/whatsapp WhatsApp Business adapter comp/gateway Gateway runner, session dispatch, delivery P2 Medium — degraded but workaround exists labels Jun 22, 2026
@aldoeliacim
aldoeliacim force-pushed the feat/whatsapp-reactions branch from ed3ecdc to 97802c3 Compare June 22, 2026 08:00
@aldoeliacim
aldoeliacim force-pushed the feat/whatsapp-reactions branch from 97802c3 to 2a55889 Compare June 22, 2026 14:47
@aldoeliacim
aldoeliacim force-pushed the feat/whatsapp-reactions branch 2 times, most recently from 408d58f to 7ad1747 Compare June 30, 2026 11:21
Copilot AI review requested due to automatic review settings July 1, 2026 20:43
@aldoeliacim
aldoeliacim force-pushed the feat/whatsapp-reactions branch from 7ad1747 to 62061af Compare July 1, 2026 20:43

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_REACTIONS plus default add_reaction / remove_reaction coroutines to BasePlatformAdapter, and updates the send_message tool 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 thread scripts/whatsapp-bridge/bridge.js Outdated
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
aldoeliacim force-pushed the feat/whatsapp-reactions branch 3 times, most recently from 48e2a5f to 38ae617 Compare July 4, 2026 21:37
…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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/gateway Gateway runner, session dispatch, delivery P2 Medium — degraded but workaround exists platform/whatsapp WhatsApp Business adapter type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants