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>
This commit is contained in:
Nick Sullivan 2026-01-29 10:38:02 -06:00
parent 5f4715acfc
commit 9369e36eb2
9 changed files with 918 additions and 27 deletions

View File

@ -3,9 +3,12 @@
Docs: https://docs.molt.bot Docs: https://docs.molt.bot
## 2026.1.27-beta.1 ## 2026.1.27-beta.1
Status: beta. Status: beta.
### Changes ### Changes
- WhatsApp: subscribe to inbound reaction events and surface them as system events for the agent (e.g., "user reacted 👍 to message X").
- Rebrand: rename the npm package/CLI to `moltbot`, add a `moltbot` compatibility shim, and move extensions to the `@moltbot/*` scope. - Rebrand: rename the npm package/CLI to `moltbot`, add a `moltbot` compatibility shim, and move extensions to the `@moltbot/*` scope.
- Commands: group /help and /commands output with Telegram paging. (#2504) Thanks @hougangdev. - Commands: group /help and /commands output with Telegram paging. (#2504) Thanks @hougangdev.
- macOS: limit project-local `node_modules/.bin` PATH preference to debug builds (reduce PATH hijacking risk). - macOS: limit project-local `node_modules/.bin` PATH preference to debug builds (reduce PATH hijacking risk).
@ -70,9 +73,11 @@ Status: beta.
- Routing: add per-account DM session scope and document multi-account isolation. (#3095) Thanks @jarvis-sam. - Routing: add per-account DM session scope and document multi-account isolation. (#3095) Thanks @jarvis-sam.
### Breaking ### Breaking
- **BREAKING:** Gateway auth mode "none" is removed; gateway now requires token/password (Tailscale Serve identity still allowed). - **BREAKING:** Gateway auth mode "none" is removed; gateway now requires token/password (Tailscale Serve identity still allowed).
### Fixes ### Fixes
- Telegram: avoid silent empty replies by tracking normalization skips before fallback. (#3796) - Telegram: avoid silent empty replies by tracking normalization skips before fallback. (#3796)
- Mentions: honor mentionPatterns even when explicit mentions are present. (#3303) Thanks @HirokiKobayashi-R. - Mentions: honor mentionPatterns even when explicit mentions are present. (#3303) Thanks @HirokiKobayashi-R.
- Discord: restore username directory lookup in target resolution. (#3131) Thanks @bonald. - Discord: restore username directory lookup in target resolution. (#3131) Thanks @bonald.
@ -125,6 +130,7 @@ Status: beta.
## 2026.1.24-3 ## 2026.1.24-3
### Fixes ### Fixes
- Slack: fix image downloads failing due to missing Authorization header on cross-origin redirects. (#1936) Thanks @sanderhelgesen. - Slack: fix image downloads failing due to missing Authorization header on cross-origin redirects. (#1936) Thanks @sanderhelgesen.
- Gateway: harden reverse proxy handling for local-client detection and unauthenticated proxied connects. (#1795) Thanks @orlyjamie. - Gateway: harden reverse proxy handling for local-client detection and unauthenticated proxied connects. (#1795) Thanks @orlyjamie.
- Security audit: flag loopback Control UI with auth disabled as critical. (#1795) Thanks @orlyjamie. - Security audit: flag loopback Control UI with auth disabled as critical. (#1795) Thanks @orlyjamie.
@ -133,16 +139,19 @@ Status: beta.
## 2026.1.24-2 ## 2026.1.24-2
### Fixes ### Fixes
- Packaging: include dist/link-understanding output in npm tarball (fixes missing apply.js import on install). - Packaging: include dist/link-understanding output in npm tarball (fixes missing apply.js import on install).
## 2026.1.24-1 ## 2026.1.24-1
### Fixes ### Fixes
- Packaging: include dist/shared output in npm tarball (fixes missing reasoning-tags import on install). - Packaging: include dist/shared output in npm tarball (fixes missing reasoning-tags import on install).
## 2026.1.24 ## 2026.1.24
### Highlights ### Highlights
- Providers: Ollama discovery + docs; Venice guide upgrades + cross-links. (#1606) Thanks @abhaymundhara. https://docs.molt.bot/providers/ollama https://docs.molt.bot/providers/venice - Providers: Ollama discovery + docs; Venice guide upgrades + cross-links. (#1606) Thanks @abhaymundhara. https://docs.molt.bot/providers/ollama https://docs.molt.bot/providers/venice
- Channels: LINE plugin (Messaging API) with rich replies + quick replies. (#1630) Thanks @plum-dawg. - Channels: LINE plugin (Messaging API) with rich replies + quick replies. (#1630) Thanks @plum-dawg.
- TTS: Edge fallback (keyless) + `/tts` auto modes. (#1668, #1667) Thanks @steipete, @sebslight. https://docs.molt.bot/tts - TTS: Edge fallback (keyless) + `/tts` auto modes. (#1668, #1667) Thanks @steipete, @sebslight. https://docs.molt.bot/tts
@ -150,6 +159,7 @@ Status: beta.
- Telegram: DM topics as separate sessions + outbound link preview toggle. (#1597, #1700) Thanks @rohannagpal, @zerone0x. https://docs.molt.bot/channels/telegram - Telegram: DM topics as separate sessions + outbound link preview toggle. (#1597, #1700) Thanks @rohannagpal, @zerone0x. https://docs.molt.bot/channels/telegram
### Changes ### Changes
- Channels: add LINE plugin (Messaging API) with rich replies, quick replies, and plugin HTTP registry. (#1630) Thanks @plum-dawg. - Channels: add LINE plugin (Messaging API) with rich replies, quick replies, and plugin HTTP registry. (#1630) Thanks @plum-dawg.
- TTS: add Edge TTS provider fallback, defaulting to keyless Edge with MP3 retry on format failures. (#1668) Thanks @steipete. https://docs.molt.bot/tts - TTS: add Edge TTS provider fallback, defaulting to keyless Edge with MP3 retry on format failures. (#1668) Thanks @steipete. https://docs.molt.bot/tts
- TTS: add auto mode enum (off/always/inbound/tagged) with per-session `/tts` override. (#1667) Thanks @sebslight. https://docs.molt.bot/tts - TTS: add auto mode enum (off/always/inbound/tagged) with per-session `/tts` override. (#1667) Thanks @sebslight. https://docs.molt.bot/tts
@ -168,6 +178,7 @@ Status: beta.
- Dev: add prek pre-commit hooks + dependabot config for weekly updates. (#1720) Thanks @dguido. - Dev: add prek pre-commit hooks + dependabot config for weekly updates. (#1720) Thanks @dguido.
### Fixes ### Fixes
- Web UI: fix config/debug layout overflow, scrolling, and code block sizing. (#1715) Thanks @saipreetham589. - Web UI: fix config/debug layout overflow, scrolling, and code block sizing. (#1715) Thanks @saipreetham589.
- Web UI: show Stop button during active runs, swap back to New session when idle. (#1664) Thanks @ndbroadbent. - Web UI: show Stop button during active runs, swap back to New session when idle. (#1664) Thanks @ndbroadbent.
- Web UI: clear stale disconnect banners on reconnect; allow form saves with unsupported schema paths but block missing schema. (#1707) Thanks @Glucksberg. - Web UI: clear stale disconnect banners on reconnect; allow form saves with unsupported schema paths but block missing schema. (#1707) Thanks @Glucksberg.
@ -211,11 +222,13 @@ Status: beta.
## 2026.1.23-1 ## 2026.1.23-1
### Fixes ### Fixes
- Packaging: include dist/tts output in npm tarball (fixes missing dist/tts/tts.js). - Packaging: include dist/tts output in npm tarball (fixes missing dist/tts/tts.js).
## 2026.1.23 ## 2026.1.23
### Highlights ### Highlights
- TTS: move Telegram TTS into core + enable model-driven TTS tags by default for expressive audio replies. (#1559) Thanks @Glucksberg. https://docs.molt.bot/tts - TTS: move Telegram TTS into core + enable model-driven TTS tags by default for expressive audio replies. (#1559) Thanks @Glucksberg. https://docs.molt.bot/tts
- Gateway: add `/tools/invoke` HTTP endpoint for direct tool calls (auth + tool policy enforced). (#1575) Thanks @vignesh07. https://docs.molt.bot/gateway/tools-invoke-http-api - Gateway: add `/tools/invoke` HTTP endpoint for direct tool calls (auth + tool policy enforced). (#1575) Thanks @vignesh07. https://docs.molt.bot/gateway/tools-invoke-http-api
- Heartbeat: per-channel visibility controls (OK/alerts/indicator). (#1452) Thanks @dlauer. https://docs.molt.bot/gateway/heartbeat - Heartbeat: per-channel visibility controls (OK/alerts/indicator). (#1452) Thanks @dlauer. https://docs.molt.bot/gateway/heartbeat
@ -223,6 +236,7 @@ Status: beta.
- Channels: add Tlon/Urbit channel plugin (DMs, group mentions, thread replies). (#1544) Thanks @wca4a. https://docs.molt.bot/channels/tlon - Channels: add Tlon/Urbit channel plugin (DMs, group mentions, thread replies). (#1544) Thanks @wca4a. https://docs.molt.bot/channels/tlon
### Changes ### Changes
- Channels: allow per-group tool allow/deny policies across built-in + plugin channels. (#1546) Thanks @adam91holt. https://docs.molt.bot/multi-agent-sandbox-tools - Channels: allow per-group tool allow/deny policies across built-in + plugin channels. (#1546) Thanks @adam91holt. https://docs.molt.bot/multi-agent-sandbox-tools
- Agents: add Bedrock auto-discovery defaults + config overrides. (#1553) Thanks @fal3. https://docs.molt.bot/bedrock - Agents: add Bedrock auto-discovery defaults + config overrides. (#1553) Thanks @fal3. https://docs.molt.bot/bedrock
- CLI: add `moltbot system` for system events + heartbeat controls; remove standalone `wake`. (commit 71203829d) https://docs.molt.bot/cli/system - CLI: add `moltbot system` for system events + heartbeat controls; remove standalone `wake`. (commit 71203829d) https://docs.molt.bot/cli/system
@ -237,6 +251,7 @@ Status: beta.
- Docs: clarify HEARTBEAT.md empty file skips heartbeats, missing file still runs. (#1535) Thanks @JustYannicc. https://docs.molt.bot/gateway/heartbeat - Docs: clarify HEARTBEAT.md empty file skips heartbeats, missing file still runs. (#1535) Thanks @JustYannicc. https://docs.molt.bot/gateway/heartbeat
### Fixes ### Fixes
- Sessions: accept non-UUID sessionIds for history/send/status while preserving agent scoping. (#1518) - Sessions: accept non-UUID sessionIds for history/send/status while preserving agent scoping. (#1518)
- Heartbeat: accept plugin channel ids for heartbeat target validation + UI hints. - Heartbeat: accept plugin channel ids for heartbeat target validation + UI hints.
- Messaging/Sessions: mirror outbound sends into target session keys (threads + dmScope), create session entries on send, and normalize session key casing. (#1520, commit 4b6cdd1d3) - Messaging/Sessions: mirror outbound sends into target session keys (threads + dmScope), create session entries on send, and normalize session key casing. (#1520, commit 4b6cdd1d3)
@ -275,6 +290,7 @@ Status: beta.
## 2026.1.22 ## 2026.1.22
### Changes ### Changes
- Highlight: Compaction safeguard now uses adaptive chunking, progressive fallback, and UI status + retries. (#1466) Thanks @dlauer. - Highlight: Compaction safeguard now uses adaptive chunking, progressive fallback, and UI status + retries. (#1466) Thanks @dlauer.
- Providers: add Antigravity usage tracking to status output. (#1490) Thanks @patelhiren. - Providers: add Antigravity usage tracking to status output. (#1490) Thanks @patelhiren.
- Slack: add chat-type reply threading overrides via `replyToModeByChatType`. (#1442) Thanks @stefangalescu. - Slack: add chat-type reply threading overrides via `replyToModeByChatType`. (#1442) Thanks @stefangalescu.
@ -282,6 +298,7 @@ Status: beta.
- Onboarding: add hatch choice (TUI/Web/Later), token explainer, background dashboard seed on macOS, and showcase link. - Onboarding: add hatch choice (TUI/Web/Later), token explainer, background dashboard seed on macOS, and showcase link.
### Fixes ### Fixes
- BlueBubbles: stop typing indicator on idle/no-reply. (#1439) Thanks @Nicell. - BlueBubbles: stop typing indicator on idle/no-reply. (#1439) Thanks @Nicell.
- Message tool: keep path/filePath as-is for send; hydrate buffers only for sendAttachment. (#1444) Thanks @hopyky. - Message tool: keep path/filePath as-is for send; hydrate buffers only for sendAttachment. (#1444) Thanks @hopyky.
- Auto-reply: only report a model switch when session state is available. (#1465) Thanks @robbyczgw-cla. - Auto-reply: only report a model switch when session state is available. (#1465) Thanks @robbyczgw-cla.
@ -312,12 +329,14 @@ Status: beta.
## 2026.1.21-2 ## 2026.1.21-2
### Fixes ### Fixes
- Control UI: ignore bootstrap identity placeholder text for avatar values and fall back to the default avatar. https://docs.molt.bot/cli/agents https://docs.molt.bot/web/control-ui - Control UI: ignore bootstrap identity placeholder text for avatar values and fall back to the default avatar. https://docs.molt.bot/cli/agents https://docs.molt.bot/web/control-ui
- Slack: remove deprecated `filetype` field from `files.uploadV2` to eliminate API warnings. (#1447) - Slack: remove deprecated `filetype` field from `files.uploadV2` to eliminate API warnings. (#1447)
## 2026.1.21 ## 2026.1.21
### Changes ### Changes
- Highlight: Lobster optional plugin tool for typed workflows + approval gates. https://docs.molt.bot/tools/lobster - Highlight: Lobster optional plugin tool for typed workflows + approval gates. https://docs.molt.bot/tools/lobster
- Lobster: allow workflow file args via `argsJson` in the plugin tool. https://docs.molt.bot/tools/lobster - Lobster: allow workflow file args via `argsJson` in the plugin tool. https://docs.molt.bot/tools/lobster
- Heartbeat: allow running heartbeats in an explicit session key. (#1256) Thanks @zknicker. - Heartbeat: allow running heartbeats in an explicit session key. (#1256) Thanks @zknicker.
@ -340,10 +359,12 @@ Status: beta.
- Docs: add per-message Gmail search example for gog. (#1220) Thanks @mbelinky. - Docs: add per-message Gmail search example for gog. (#1220) Thanks @mbelinky.
### Breaking ### Breaking
- **BREAKING:** Control UI now rejects insecure HTTP without device identity by default. Use HTTPS (Tailscale Serve) or set `gateway.controlUi.allowInsecureAuth: true` to allow token-only auth. https://docs.molt.bot/web/control-ui#insecure-http - **BREAKING:** Control UI now rejects insecure HTTP without device identity by default. Use HTTPS (Tailscale Serve) or set `gateway.controlUi.allowInsecureAuth: true` to allow token-only auth. https://docs.molt.bot/web/control-ui#insecure-http
- **BREAKING:** Envelope and system event timestamps now default to host-local time (was UTC) so agents dont have to constantly convert. - **BREAKING:** Envelope and system event timestamps now default to host-local time (was UTC) so agents dont have to constantly convert.
### Fixes ### Fixes
- Nodes/macOS: prompt on allowlist miss for node exec approvals, persist allowlist decisions, and flatten node invoke errors. (#1394) Thanks @ngutman. - Nodes/macOS: prompt on allowlist miss for node exec approvals, persist allowlist decisions, and flatten node invoke errors. (#1394) Thanks @ngutman.
- Gateway: keep auto bind loopback-first and add explicit tailnet binding to avoid Tailscale taking over local UI. (#1380) - Gateway: keep auto bind loopback-first and add explicit tailnet binding to avoid Tailscale taking over local UI. (#1380)
- Memory: prevent CLI hangs by deferring vector probes, adding sqlite-vec/embedding timeouts, and showing sync progress early. - Memory: prevent CLI hangs by deferring vector probes, adding sqlite-vec/embedding timeouts, and showing sync progress early.
@ -366,6 +387,7 @@ Status: beta.
## 2026.1.20 ## 2026.1.20
### Changes ### Changes
- Control UI: add copy-as-markdown with error feedback. (#1345) https://docs.molt.bot/web/control-ui - Control UI: add copy-as-markdown with error feedback. (#1345) https://docs.molt.bot/web/control-ui
- Control UI: drop the legacy list view. (#1345) https://docs.molt.bot/web/control-ui - Control UI: drop the legacy list view. (#1345) https://docs.molt.bot/web/control-ui
- TUI: add syntax highlighting for code blocks. (#1200) https://docs.molt.bot/tui - TUI: add syntax highlighting for code blocks. (#1200) https://docs.molt.bot/tui
@ -444,9 +466,11 @@ Status: beta.
- Swabble: use the tagged Commander Swift package release. - Swabble: use the tagged Commander Swift package release.
### Breaking ### Breaking
- **BREAKING:** Reject invalid/unknown config entries and refuse to start the gateway for safety. Run `moltbot doctor --fix` to repair, then update plugins (`moltbot plugins update`) if you use any. - **BREAKING:** Reject invalid/unknown config entries and refuse to start the gateway for safety. Run `moltbot doctor --fix` to repair, then update plugins (`moltbot plugins update`) if you use any.
### Fixes ### Fixes
- Discovery: shorten Bonjour DNS-SD service type to `_moltbot-gw._tcp` and update discovery clients/docs. - Discovery: shorten Bonjour DNS-SD service type to `_moltbot-gw._tcp` and update discovery clients/docs.
- Diagnostics: export OTLP logs, correct queue depth tracking, and document message-flow telemetry. - Diagnostics: export OTLP logs, correct queue depth tracking, and document message-flow telemetry.
- Diagnostics: emit message-flow diagnostics across channels via shared dispatch. (#1244) - Diagnostics: emit message-flow diagnostics across channels via shared dispatch. (#1244)
@ -546,20 +570,23 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
## 2026.1.16-2 ## 2026.1.16-2
### Changes ### Changes
- CLI: stamp build commit into dist metadata so banners show the commit in npm installs. - CLI: stamp build commit into dist metadata so banners show the commit in npm installs.
- CLI: close memory manager after memory commands to avoid hanging processes. (#1127) — thanks @NicholasSpisak. - CLI: close memory manager after memory commands to avoid hanging processes. (#1127) — thanks @NicholasSpisak.
## 2026.1.16-1 ## 2026.1.16-1
### Highlights ### Highlights
- Hooks: add hooks system with bundled hooks, CLI tooling, and docs. (#1028) — thanks @ThomsenDrake. https://docs.molt.bot/hooks - Hooks: add hooks system with bundled hooks, CLI tooling, and docs. (#1028) — thanks @ThomsenDrake. https://docs.molt.bot/hooks
- Media: add inbound media understanding (image/audio/video) with provider + CLI fallbacks. https://docs.molt.bot/nodes/media-understanding - Media: add inbound media understanding (image/audio/video) with provider + CLI fallbacks. https://docs.molt.bot/nodes/media-understanding
- Plugins: add Zalo Personal plugin (`@moltbot/zalouser`) and unify channel directory for plugins. (#1032) — thanks @suminhthanh. https://docs.molt.bot/plugins/zalouser - Plugins: add Zalo Personal plugin (`@moltbot/zalouser`) and unify channel directory for plugins. (#1032) — thanks @suminhthanh. https://docs.molt.bot/plugins/zalouser
- Models: add Vercel AI Gateway auth choice + onboarding updates. (#1016) — thanks @timolins. https://docs.molt.bot/providers/vercel-ai-gateway - Models: add Vercel AI Gateway auth choice + onboarding updates. (#1016) — thanks @timolins. https://docs.molt.bot/providers/vercel-ai-gateway
- Sessions: add `session.identityLinks` for cross-platform DM session li nking. (#1033) — thanks @thewilloftheshadow. https://docs.molt.bot/concepts/session - Sessions: add `session.identityLinks` for cross-platform DM session li nking. (#1033) — thanks @thewilloftheshadow. https://docs.molt.bot/concepts/session
- Web search: add `country`/`language` parameters (schema + Brave API) and docs. (#1046) — thanks @YuriNachos. https://docs.molt.bot/tools/web - Web search: add `country`/`language` parameters (schema + Brave API) and docs. (#1046) — thanks @YuriNachos. https://docs.molt.bot/tools/web
### Breaking ### Breaking
- **BREAKING:** `moltbot message` and message tool now require `target` (dropping `to`/`channelId` for destinations). (#1034) — thanks @tobalsan. - **BREAKING:** `moltbot message` and message tool now require `target` (dropping `to`/`channelId` for destinations). (#1034) — thanks @tobalsan.
- **BREAKING:** Channel auth now prefers config over env for Discord/Telegram/Matrix (env is fallback only). (#1040) — thanks @thewilloftheshadow. - **BREAKING:** Channel auth now prefers config over env for Discord/Telegram/Matrix (env is fallback only). (#1040) — thanks @thewilloftheshadow.
- **BREAKING:** Drop legacy `chatType: "room"` support; use `chatType: "channel"`. - **BREAKING:** Drop legacy `chatType: "room"` support; use `chatType: "channel"`.
@ -568,6 +595,7 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
- **BREAKING:** `moltbot plugins install <path>` now copies into `~/.clawdbot/extensions` (use `--link` to keep path-based loading). - **BREAKING:** `moltbot plugins install <path>` now copies into `~/.clawdbot/extensions` (use `--link` to keep path-based loading).
### Changes ### Changes
- Plugins: ship bundled plugins disabled by default and allow overrides by installed versions. (#1066) — thanks @ItzR3NO. - Plugins: ship bundled plugins disabled by default and allow overrides by installed versions. (#1066) — thanks @ItzR3NO.
- Plugins: add bundled Antigravity + Gemini CLI OAuth + Copilot Proxy provider plugins. (#1066) — thanks @ItzR3NO. - Plugins: add bundled Antigravity + Gemini CLI OAuth + Copilot Proxy provider plugins. (#1066) — thanks @ItzR3NO.
- Tools: improve `web_fetch` extraction using Readability (with fallback). - Tools: improve `web_fetch` extraction using Readability (with fallback).
@ -603,6 +631,7 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
- Plugins: add zip installs and `--link` to avoid copying local paths. - Plugins: add zip installs and `--link` to avoid copying local paths.
### Fixes ### Fixes
- macOS: drain subprocess pipes before waiting to avoid deadlocks. (#1081) — thanks @thesash. - macOS: drain subprocess pipes before waiting to avoid deadlocks. (#1081) — thanks @thesash.
- Verbose: wrap tool summaries/output in markdown only for markdown-capable channels. - Verbose: wrap tool summaries/output in markdown only for markdown-capable channels.
- Tools: include provider/session context in elevated exec denial errors. - Tools: include provider/session context in elevated exec denial errors.
@ -659,17 +688,20 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
## 2026.1.15 ## 2026.1.15
### Highlights ### Highlights
- Plugins: add provider auth registry + `moltbot models auth login` for plugin-driven OAuth/API key flows. - Plugins: add provider auth registry + `moltbot models auth login` for plugin-driven OAuth/API key flows.
- Browser: improve remote CDP/Browserless support (auth passthrough, `wss` upgrade, timeouts, clearer errors). - Browser: improve remote CDP/Browserless support (auth passthrough, `wss` upgrade, timeouts, clearer errors).
- Heartbeat: per-agent configuration + 24h duplicate suppression. (#980) — thanks @voidserf. - Heartbeat: per-agent configuration + 24h duplicate suppression. (#980) — thanks @voidserf.
- Security: audit warns on weak model tiers; app nodes store auth tokens encrypted (Keychain/SecurePrefs). - Security: audit warns on weak model tiers; app nodes store auth tokens encrypted (Keychain/SecurePrefs).
### Breaking ### Breaking
- **BREAKING:** iOS minimum version is now 18.0 to support Textual markdown rendering in native chat. (#702) - **BREAKING:** iOS minimum version is now 18.0 to support Textual markdown rendering in native chat. (#702)
- **BREAKING:** Microsoft Teams is now a plugin; install `@moltbot/msteams` via `moltbot plugins install @moltbot/msteams`. - **BREAKING:** Microsoft Teams is now a plugin; install `@moltbot/msteams` via `moltbot plugins install @moltbot/msteams`.
- **BREAKING:** Channel auth now prefers config over env for Discord/Telegram/Matrix (env is fallback only). (#1040) — thanks @thewilloftheshadow. - **BREAKING:** Channel auth now prefers config over env for Discord/Telegram/Matrix (env is fallback only). (#1040) — thanks @thewilloftheshadow.
### Changes ### Changes
- UI/Apps: move channel/config settings to schema-driven forms and rename Connections → Channels. (#1040) — thanks @thewilloftheshadow. - UI/Apps: move channel/config settings to schema-driven forms and rename Connections → Channels. (#1040) — thanks @thewilloftheshadow.
- CLI: set process titles to `moltbot-<command>` for clearer process listings. - CLI: set process titles to `moltbot-<command>` for clearer process listings.
- CLI/macOS: sync remote SSH target/identity to config and let `gateway status` auto-infer SSH targets (ssh-config aware). - CLI/macOS: sync remote SSH target/identity to config and let `gateway status` auto-infer SSH targets (ssh-config aware).
@ -709,6 +741,7 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
- Discord: allow emoji/sticker uploads + channel actions in config defaults. (#870) — thanks @JDIVE. - Discord: allow emoji/sticker uploads + channel actions in config defaults. (#870) — thanks @JDIVE.
### Fixes ### Fixes
- Messages: make `/stop` clear queued followups and pending session lane work for a hard abort. - Messages: make `/stop` clear queued followups and pending session lane work for a hard abort.
- Messages: make `/stop` abort active sub-agent runs spawned from the requester session and report how many were stopped. - Messages: make `/stop` abort active sub-agent runs spawned from the requester session and report how many were stopped.
- WhatsApp: report linked status consistently in channel status. (#1050) — thanks @YuriNachos. - WhatsApp: report linked status consistently in channel status. (#1050) — thanks @YuriNachos.
@ -745,12 +778,14 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
## 2026.1.14-1 ## 2026.1.14-1
### Highlights ### Highlights
- Web search: `web_search`/`web_fetch` tools (Brave API) + first-time setup in onboarding/configure. - Web search: `web_search`/`web_fetch` tools (Brave API) + first-time setup in onboarding/configure.
- Browser control: Chrome extension relay takeover mode + remote browser control support. - Browser control: Chrome extension relay takeover mode + remote browser control support.
- Plugins: channel plugins (gateway HTTP hooks) + Zalo plugin + onboarding install flow. (#854) — thanks @longmaba. - Plugins: channel plugins (gateway HTTP hooks) + Zalo plugin + onboarding install flow. (#854) — thanks @longmaba.
- Security: expanded `moltbot security audit` (+ `--fix`), detect-secrets CI scan, and a `SECURITY.md` reporting policy. - Security: expanded `moltbot security audit` (+ `--fix`), detect-secrets CI scan, and a `SECURITY.md` reporting policy.
### Changes ### Changes
- Docs: clarify per-agent auth stores, sandboxed skill binaries, and elevated semantics. - Docs: clarify per-agent auth stores, sandboxed skill binaries, and elevated semantics.
- Docs: add FAQ entries for missing provider auth after adding agents and Gemini thinking signature errors. - Docs: add FAQ entries for missing provider auth after adding agents and Gemini thinking signature errors.
- Agents: add optional auth-profile copy prompt on `agents add` and improve auth error messaging. - Agents: add optional auth-profile copy prompt on `agents add` and improve auth error messaging.
@ -767,6 +802,7 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
- Browser: add Chrome extension relay takeover mode (toolbar button), plus `moltbot browser extension install/path` and remote browser control (standalone server + token auth). - Browser: add Chrome extension relay takeover mode (toolbar button), plus `moltbot browser extension install/path` and remote browser control (standalone server + token auth).
### Fixes ### Fixes
- Sessions: refactor session store updates to lock + mutate per-entry, add chat.inject, and harden subagent cleanup flow. (#944) — thanks @tyler6204. - Sessions: refactor session store updates to lock + mutate per-entry, add chat.inject, and harden subagent cleanup flow. (#944) — thanks @tyler6204.
- Browser: add tests for snapshot labels/efficient query params and labeled image responses. - Browser: add tests for snapshot labels/efficient query params and labeled image responses.
- Google: downgrade unsigned thinking blocks before send to avoid missing signature errors. - Google: downgrade unsigned thinking blocks before send to avoid missing signature errors.
@ -788,6 +824,7 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
## 2026.1.14 ## 2026.1.14
### Changes ### Changes
- Usage: add MiniMax coding plan usage tracking. - Usage: add MiniMax coding plan usage tracking.
- Auth: label Claude Code CLI auth options. (#915) — thanks @SeanZoR. - Auth: label Claude Code CLI auth options. (#915) — thanks @SeanZoR.
- Docs: standardize Claude Code CLI naming across docs and prompts. (follow-up to #915) - Docs: standardize Claude Code CLI naming across docs and prompts. (follow-up to #915)
@ -795,14 +832,16 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
- Config: add `channels.<provider>.configWrites` gating for channel-initiated config writes; migrate Slack channel IDs. - Config: add `channels.<provider>.configWrites` gating for channel-initiated config writes; migrate Slack channel IDs.
### Fixes ### Fixes
- Mac: pass auth token/password to dashboard URL for authenticated access. (#918) — thanks @rahthakor.
- UI: use application-defined WebSocket close code (browser compatibility). (#918) — thanks @rahthakor. - Mac: pass auth token/password to dashboard URL for authenticated access. (#918) — thanks @rahthakor.
- UI: use application-defined WebSocket close code (browser compatibility). (#918) — thanks @rahthakor.
- TUI: render picker overlays via the overlay stack so /models and /settings display. (#921) — thanks @grizzdank. - TUI: render picker overlays via the overlay stack so /models and /settings display. (#921) — thanks @grizzdank.
- TUI: add a bright spinner + elapsed time in the status line for send/stream/run states. - TUI: add a bright spinner + elapsed time in the status line for send/stream/run states.
- TUI: show LLM error messages (rate limits, auth, etc.) instead of `(no output)`. - TUI: show LLM error messages (rate limits, auth, etc.) instead of `(no output)`.
- Gateway/Dev: ensure `pnpm gateway:dev` always uses the dev profile config + state (`~/.clawdbot-dev`). - Gateway/Dev: ensure `pnpm gateway:dev` always uses the dev profile config + state (`~/.clawdbot-dev`).
#### Agents / Auth / Tools / Sandbox #### Agents / Auth / Tools / Sandbox
- Agents: make user time zone and 24-hour time explicit in the system prompt. (#859) — thanks @CashWilliams. - Agents: make user time zone and 24-hour time explicit in the system prompt. (#859) — thanks @CashWilliams.
- Agents: strip downgraded tool call text without eating adjacent replies and filter thinking-tag leaks. (#905) — thanks @erikpr1994. - Agents: strip downgraded tool call text without eating adjacent replies and filter thinking-tag leaks. (#905) — thanks @erikpr1994.
- Agents: cap tool call IDs for OpenAI/OpenRouter to avoid request rejections. (#875) — thanks @j1philli. - Agents: cap tool call IDs for OpenAI/OpenRouter to avoid request rejections. (#875) — thanks @j1philli.
@ -815,6 +854,7 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
- Google: downgrade unsigned thinking blocks before send to avoid missing signature errors. - Google: downgrade unsigned thinking blocks before send to avoid missing signature errors.
#### macOS / Apps #### macOS / Apps
- macOS: ensure launchd log directory exists with a test-only override. (#909) — thanks @roshanasingh4. - macOS: ensure launchd log directory exists with a test-only override. (#909) — thanks @roshanasingh4.
- macOS: format ConnectionsStore config to satisfy SwiftFormat lint. (#852) — thanks @mneves75. - macOS: format ConnectionsStore config to satisfy SwiftFormat lint. (#852) — thanks @mneves75.
- macOS: pass auth token/password to dashboard URL for authenticated access. (#918) — thanks @rahthakor. - macOS: pass auth token/password to dashboard URL for authenticated access. (#918) — thanks @rahthakor.
@ -834,12 +874,14 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
## 2026.1.13 ## 2026.1.13
### Fixes ### Fixes
- Postinstall: treat already-applied pnpm patches as no-ops to avoid npm/bun install failures. - Postinstall: treat already-applied pnpm patches as no-ops to avoid npm/bun install failures.
- Packaging: pin `@mariozechner/pi-ai` to 0.45.7 and refresh patched dependency to match npm resolution. - Packaging: pin `@mariozechner/pi-ai` to 0.45.7 and refresh patched dependency to match npm resolution.
## 2026.1.12-2 ## 2026.1.12-2
### Fixes ### Fixes
- Packaging: include `dist/memory/**` in the npm tarball (fixes `ERR_MODULE_NOT_FOUND` for `dist/memory/index.js`). - Packaging: include `dist/memory/**` in the npm tarball (fixes `ERR_MODULE_NOT_FOUND` for `dist/memory/index.js`).
- Agents: persist sub-agent registry across gateway restarts and resume announce flow safely. (#831) — thanks @roshanasingh4. - Agents: persist sub-agent registry across gateway restarts and resume announce flow safely. (#831) — thanks @roshanasingh4.
- Agents: strip invalid Gemini thought signatures from OpenRouter history to avoid 400s. (#841, #845) — thanks @MatthieuBizien. - Agents: strip invalid Gemini thought signatures from OpenRouter history to avoid 400s. (#841, #845) — thanks @MatthieuBizien.
@ -847,11 +889,13 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
## 2026.1.12-1 ## 2026.1.12-1
### Fixes ### Fixes
- Packaging: include `dist/channels/**` in the npm tarball (fixes `ERR_MODULE_NOT_FOUND` for `dist/channels/registry.js`). - Packaging: include `dist/channels/**` in the npm tarball (fixes `ERR_MODULE_NOT_FOUND` for `dist/channels/registry.js`).
## 2026.1.12 ## 2026.1.12
### Highlights ### Highlights
- **BREAKING:** rename chat “providers” (Slack/Telegram/WhatsApp/…) to **channels** across CLI/RPC/config; legacy config keys auto-migrate on load (and are written back as `channels.*`). - **BREAKING:** rename chat “providers” (Slack/Telegram/WhatsApp/…) to **channels** across CLI/RPC/config; legacy config keys auto-migrate on load (and are written back as `channels.*`).
- Memory: add vector search for agent memories (Markdown-only) with SQLite index, chunking, lazy sync + file watch, and per-agent enablement/fallback. - Memory: add vector search for agent memories (Markdown-only) with SQLite index, chunking, lazy sync + file watch, and per-agent enablement/fallback.
- Plugins: restore full voice-call plugin parity (Telnyx/Twilio, streaming, inbound policies, tools/CLI). - Plugins: restore full voice-call plugin parity (Telnyx/Twilio, streaming, inbound policies, tools/CLI).
@ -860,6 +904,7 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
- Agents: add compaction mode config with optional safeguard summarization and per-agent model fallbacks. (#700) — thanks @thewilloftheshadow; (#583) — thanks @mitschabaude-bot. - Agents: add compaction mode config with optional safeguard summarization and per-agent model fallbacks. (#700) — thanks @thewilloftheshadow; (#583) — thanks @mitschabaude-bot.
### New & Improved ### New & Improved
- Memory: add custom OpenAI-compatible embedding endpoints; support OpenAI/local `node-llama-cpp` embeddings with per-agent overrides and provider metadata in tools/CLI. (#819) — thanks @mukhtharcm. - Memory: add custom OpenAI-compatible embedding endpoints; support OpenAI/local `node-llama-cpp` embeddings with per-agent overrides and provider metadata in tools/CLI. (#819) — thanks @mukhtharcm.
- Memory: new `moltbot memory` CLI plus `memory_search`/`memory_get` tools with snippets + line ranges; index stored under `~/.clawdbot/memory/{agentId}.sqlite` with watch-on-by-default. - Memory: new `moltbot memory` CLI plus `memory_search`/`memory_get` tools with snippets + line ranges; index stored under `~/.clawdbot/memory/{agentId}.sqlite` with watch-on-by-default.
- Agents: strengthen memory recall guidance; make workspace bootstrap truncation configurable (default 20k) with warnings; add default sub-agent model config. - Agents: strengthen memory recall guidance; make workspace bootstrap truncation configurable (default 20k) with warnings; add default sub-agent model config.
@ -873,9 +918,11 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
- Heartbeat: default `ackMaxChars` to 300 so short `HEARTBEAT_OK` replies stay internal. - Heartbeat: default `ackMaxChars` to 300 so short `HEARTBEAT_OK` replies stay internal.
### Installer ### Installer
- Install: run `moltbot doctor --non-interactive` after git installs/updates and nudge daemon restarts when detected. - Install: run `moltbot doctor --non-interactive` after git installs/updates and nudge daemon restarts when detected.
### Fixes ### Fixes
- Doctor: warn on pnpm workspace mismatches, missing Control UI assets, and missing tsx binaries; offer UI rebuilds. - Doctor: warn on pnpm workspace mismatches, missing Control UI assets, and missing tsx binaries; offer UI rebuilds.
- Tools: apply global tool allow/deny even when agent-specific tool policy is set. - Tools: apply global tool allow/deny even when agent-specific tool policy is set.
- Models/Providers: treat credential validation failures as auth errors to trigger fallback; normalize `${ENV_VAR}` apiKey values and auto-fill missing provider keys; preserve explicit GitHub Copilot provider config + agent-dir auth profiles. (#822) — thanks @sebslight; (#705) — thanks @TAGOOZ. - Models/Providers: treat credential validation failures as auth errors to trigger fallback; normalize `${ENV_VAR}` apiKey values and auto-fill missing provider keys; preserve explicit GitHub Copilot provider config + agent-dir auth profiles. (#822) — thanks @sebslight; (#705) — thanks @TAGOOZ.
@ -900,6 +947,7 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
- Connections UI: polish multi-account account cards. (#816) — thanks @steipete. - Connections UI: polish multi-account account cards. (#816) — thanks @steipete.
### Maintenance ### Maintenance
- Dependencies: bump Pi packages to 0.45.3 and refresh patched pi-ai. - Dependencies: bump Pi packages to 0.45.3 and refresh patched pi-ai.
- Testing: update Vitest + browser-playwright to 4.0.17. - Testing: update Vitest + browser-playwright to 4.0.17.
- Docs: add Amazon Bedrock provider notes and link from models/FAQ. - Docs: add Amazon Bedrock provider notes and link from models/FAQ.
@ -907,12 +955,14 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
## 2026.1.11 ## 2026.1.11
### Highlights ### Highlights
- Plugins are now first-class: loader + CLI management, plus the new Voice Call plugin. - Plugins are now first-class: loader + CLI management, plus the new Voice Call plugin.
- Config: modular `$include` support for split config files. (#731) — thanks @pasogott. - Config: modular `$include` support for split config files. (#731) — thanks @pasogott.
- Agents/Pi: reserve compaction headroom so pre-compaction memory writes can run before auto-compaction. - Agents/Pi: reserve compaction headroom so pre-compaction memory writes can run before auto-compaction.
- Agents: automatic pre-compaction memory flush turn to store durable memories before compaction. - Agents: automatic pre-compaction memory flush turn to store durable memories before compaction.
### Changes ### Changes
- CLI/Onboarding: simplify MiniMax auth choice to a single M2.1 option. - CLI/Onboarding: simplify MiniMax auth choice to a single M2.1 option.
- CLI: configure section selection now loops until Continue. - CLI: configure section selection now loops until Continue.
- Docs: explain MiniMax vs MiniMax Lightning (speed vs cost) and restore LM Studio example. - Docs: explain MiniMax vs MiniMax Lightning (speed vs cost) and restore LM Studio example.
@ -948,6 +998,7 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
- macOS: remove the attach-only gateway setting; local mode now always manages launchd while still attaching to an existing gateway if present. - macOS: remove the attach-only gateway setting; local mode now always manages launchd while still attaching to an existing gateway if present.
### Installer ### Installer
- Postinstall: replace `git apply` with builtin JS patcher (works npm/pnpm/bun; no git dependency) plus regression tests. - Postinstall: replace `git apply` with builtin JS patcher (works npm/pnpm/bun; no git dependency) plus regression tests.
- Postinstall: skip pnpm patch fallback when the new patcher is active. - Postinstall: skip pnpm patch fallback when the new patcher is active.
- Installer tests: add root+non-root docker smokes, CI workflow to fetch molt.bot scripts and run install sh/cli with onboarding skipped. - Installer tests: add root+non-root docker smokes, CI workflow to fetch molt.bot scripts and run install sh/cli with onboarding skipped.
@ -956,6 +1007,7 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
- Installer UX: add `--install-method git|npm` and auto-detect source checkouts (prompt to update git checkout vs migrate to npm). - Installer UX: add `--install-method git|npm` and auto-detect source checkouts (prompt to update git checkout vs migrate to npm).
### Fixes ### Fixes
- Models/Onboarding: configure MiniMax (minimax.io) via Anthropic-compatible `/anthropic` endpoint by default (keep `minimax-api` as a legacy alias). - Models/Onboarding: configure MiniMax (minimax.io) via Anthropic-compatible `/anthropic` endpoint by default (keep `minimax-api` as a legacy alias).
- Models: normalize Gemini 3 Pro/Flash IDs to preview names for live model lookups. (#769) — thanks @steipete. - Models: normalize Gemini 3 Pro/Flash IDs to preview names for live model lookups. (#769) — thanks @steipete.
- CLI: fix guardCancel typing for configure prompts. (#769) — thanks @steipete. - CLI: fix guardCancel typing for configure prompts. (#769) — thanks @steipete.
@ -995,12 +1047,14 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
## 2026.1.10 ## 2026.1.10
### Highlights ### Highlights
- CLI: `moltbot status` now table-based + shows OS/update/gateway/daemon/agents/sessions; `status --all` adds a full read-only debug report (tables, log tails, Tailscale summary, and scan progress via OSC-9 + spinner). - CLI: `moltbot status` now table-based + shows OS/update/gateway/daemon/agents/sessions; `status --all` adds a full read-only debug report (tables, log tails, Tailscale summary, and scan progress via OSC-9 + spinner).
- CLI Backends: add Codex CLI fallback with resume support (text output) and JSONL parsing for new runs, plus a live CLI resume probe. - CLI Backends: add Codex CLI fallback with resume support (text output) and JSONL parsing for new runs, plus a live CLI resume probe.
- CLI: add `moltbot update` (safe-ish git checkout update) + `--update` shorthand. (#673) — thanks @fm1randa. - CLI: add `moltbot update` (safe-ish git checkout update) + `--update` shorthand. (#673) — thanks @fm1randa.
- Gateway: add OpenAI-compatible `/v1/chat/completions` HTTP endpoint (auth, SSE streaming, per-agent routing). (#680). - Gateway: add OpenAI-compatible `/v1/chat/completions` HTTP endpoint (auth, SSE streaming, per-agent routing). (#680).
### Changes ### Changes
- Onboarding/Models: add first-class Z.AI (GLM) auth choice (`zai-api-key`) + `--zai-api-key` flag. - Onboarding/Models: add first-class Z.AI (GLM) auth choice (`zai-api-key`) + `--zai-api-key` flag.
- CLI/Onboarding: add OpenRouter API key auth option in configure/onboard. (#703) — thanks @mteam88. - CLI/Onboarding: add OpenRouter API key auth option in configure/onboard. (#703) — thanks @mteam88.
- Agents: add human-delay pacing between block replies (modes: off/natural/custom, per-agent configurable). (#446) — thanks @tony-freedomology. - Agents: add human-delay pacing between block replies (modes: off/natural/custom, per-agent configurable). (#446) — thanks @tony-freedomology.
@ -1014,6 +1068,7 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
- Docker: allow optional home volume + extra bind mounts in `docker-setup.sh`. (#679) — thanks @gabriel-trigo. - Docker: allow optional home volume + extra bind mounts in `docker-setup.sh`. (#679) — thanks @gabriel-trigo.
### Fixes ### Fixes
- Auto-reply: suppress draft/typing streaming for `NO_REPLY` (silent system ops) so it doesnt leak partial output. - Auto-reply: suppress draft/typing streaming for `NO_REPLY` (silent system ops) so it doesnt leak partial output.
- CLI/Status: expand tables to full terminal width; clarify provider setup vs runtime warnings; richer per-provider detail; token previews in `status` while keeping `status --all` redacted; add troubleshooting link footer; keep log tails pasteable; show gateway auth used when reachable; surface provider runtime errors (Signal/iMessage/Slack); harden `tailscale status --json` parsing; make `status --all` scan progress determinate; and replace the footer with a 3-line “Next steps” recommendation (share/debug/probe). - CLI/Status: expand tables to full terminal width; clarify provider setup vs runtime warnings; richer per-provider detail; token previews in `status` while keeping `status --all` redacted; add troubleshooting link footer; keep log tails pasteable; show gateway auth used when reachable; surface provider runtime errors (Signal/iMessage/Slack); harden `tailscale status --json` parsing; make `status --all` scan progress determinate; and replace the footer with a 3-line “Next steps” recommendation (share/debug/probe).
- CLI/Gateway: clarify that `moltbot gateway status` reports RPC health (connect + RPC) and shows RPC failures separately from connect failures. - CLI/Gateway: clarify that `moltbot gateway status` reports RPC health (connect + RPC) and shows RPC failures separately from connect failures.
@ -1085,10 +1140,10 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
- Agents: repair session transcripts by dropping duplicate tool results across the whole history (unblocks Anthropic-compatible APIs after retries). - Agents: repair session transcripts by dropping duplicate tool results across the whole history (unblocks Anthropic-compatible APIs after retries).
- Tests/Live: reset the gateway session between model runs to avoid cross-provider transcript incompatibilities (notably OpenAI Responses reasoning replay rules). - Tests/Live: reset the gateway session between model runs to avoid cross-provider transcript incompatibilities (notably OpenAI Responses reasoning replay rules).
## 2026.1.9 ## 2026.1.9
### Highlights ### Highlights
- Microsoft Teams provider: polling, attachments, outbound CLI send, per-channel policy. - Microsoft Teams provider: polling, attachments, outbound CLI send, per-channel policy.
- Models/Auth expansion: OpenCode Zen + MiniMax API onboarding; token auth profiles + auth order; OAuth health in doctor/status. - Models/Auth expansion: OpenCode Zen + MiniMax API onboarding; token auth profiles + auth order; OAuth health in doctor/status.
- CLI/Gateway UX: message subcommands, gateway discover/status/SSH, /config + /debug, sandbox CLI. - CLI/Gateway UX: message subcommands, gateway discover/status/SSH, /config + /debug, sandbox CLI.
@ -1097,10 +1152,12 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
- Control UI/TUI: queued messages, session links, reasoning view, mobile polish, logs UX. - Control UI/TUI: queued messages, session links, reasoning view, mobile polish, logs UX.
### Breaking ### Breaking
- CLI: `moltbot message` now subcommands (`message send|poll|...`) and requires `--provider` unless only one provider configured. - CLI: `moltbot message` now subcommands (`message send|poll|...`) and requires `--provider` unless only one provider configured.
- Commands/Tools: `/restart` and gateway restart tool disabled by default; enable with `commands.restart=true`. - Commands/Tools: `/restart` and gateway restart tool disabled by default; enable with `commands.restart=true`.
### New Features and Changes ### New Features and Changes
- Models/Auth: OpenCode Zen onboarding (#623) — thanks @magimetal; MiniMax Anthropic-compatible API + hosted onboarding (#590, #495) — thanks @mneves75, @tobiasbischoff. - Models/Auth: OpenCode Zen onboarding (#623) — thanks @magimetal; MiniMax Anthropic-compatible API + hosted onboarding (#590, #495) — thanks @mneves75, @tobiasbischoff.
- Models/Auth: setup-token + token auth profiles; `moltbot models auth order {get,set,clear}`; per-agent auth candidates in `/model status`; OAuth expiry checks in doctor/status. - Models/Auth: setup-token + token auth profiles; `moltbot models auth order {get,set,clear}`; per-agent auth candidates in `/model status`; OAuth expiry checks in doctor/status.
- Agent/System: claude-cli runner; `session_status` tool (and sandbox allow); adaptive context pruning default; system prompt messaging guidance + no auto self-update; eligible skills list injection; sub-agent context trimmed. - Agent/System: claude-cli runner; `session_status` tool (and sandbox allow); adaptive context pruning default; system prompt messaging guidance + no auto self-update; eligible skills list injection; sub-agent context trimmed.
@ -1122,6 +1179,7 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
- Apps/Branding: refreshed iOS/Android/macOS icons (#521) — thanks @fishfisher. - Apps/Branding: refreshed iOS/Android/macOS icons (#521) — thanks @fishfisher.
### Fixes ### Fixes
- Packaging: include MS Teams send module in npm tarball. - Packaging: include MS Teams send module in npm tarball.
- Sandbox/Browser: auto-start CDP endpoint; proxy CDP out of container for attachOnly; relax Bun fetch typing; align sandbox list output with config images. - Sandbox/Browser: auto-start CDP endpoint; proxy CDP out of container for attachOnly; relax Bun fetch typing; align sandbox list output with config images.
- Agents/Runtime: gate heartbeat prompt to default sessions; /stop aborts between tool calls; require explicit system-event session keys; guard small context windows; fix model fallback stringification; sessions_spawn inherits provider; failover on billing/credits; respect auth cooldown ordering; restore Anthropic OAuth tool dispatch + tool-name bypass; avoid OpenAI invalid reasoning replay; harden Gmail hook model defaults. - Agents/Runtime: gate heartbeat prompt to default sessions; /stop aborts between tool calls; require explicit system-event session keys; guard small context windows; fix model fallback stringification; sessions_spawn inherits provider; failover on billing/credits; respect auth cooldown ordering; restore Anthropic OAuth tool dispatch + tool-name bypass; avoid OpenAI invalid reasoning replay; harden Gmail hook model defaults.
@ -1139,7 +1197,8 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
- Onboarding/Configure: QuickStart single-select provider picker; avoid Codex CLI false-expiry warnings; clarify WhatsApp owner prompt; fix Minimax hosted onboarding (agents.defaults + msteams heartbeat target); remove configure Control UI prompt; honor gateway --dev flag. - Onboarding/Configure: QuickStart single-select provider picker; avoid Codex CLI false-expiry warnings; clarify WhatsApp owner prompt; fix Minimax hosted onboarding (agents.defaults + msteams heartbeat target); remove configure Control UI prompt; honor gateway --dev flag.
### Maintenance ### Maintenance
- Dependencies: bump pi-* stack to 0.42.2.
- Dependencies: bump pi-\* stack to 0.42.2.
- Dependencies: Pi 0.40.0 bump (#543) — thanks @mcinteerj. - Dependencies: Pi 0.40.0 bump (#543) — thanks @mcinteerj.
- Build: Docker build cache layer (#605) — thanks @zknicker. - Build: Docker build cache layer (#605) — thanks @zknicker.
@ -1148,6 +1207,7 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
## 2026.1.8 ## 2026.1.8
### Highlights ### Highlights
- Security: DMs locked down by default across providers; pairing-first + allowlist guidance. - Security: DMs locked down by default across providers; pairing-first + allowlist guidance.
- Sandbox: per-agent scope defaults + workspace access controls; tool/session isolation tuned. - Sandbox: per-agent scope defaults + workspace access controls; tool/session isolation tuned.
- Agent loop: compaction, pruning, streaming, and error handling hardened. - Agent loop: compaction, pruning, streaming, and error handling hardened.
@ -1156,6 +1216,7 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
- CLI/Gateway/Doctor: daemon/logs/status, auth migration, and diagnostics significantly expanded. - CLI/Gateway/Doctor: daemon/logs/status, auth migration, and diagnostics significantly expanded.
### Breaking ### Breaking
- **SECURITY (update ASAP):** inbound DMs are now **locked down by default** on Telegram/WhatsApp/Signal/iMessage/Discord/Slack. - **SECURITY (update ASAP):** inbound DMs are now **locked down by default** on Telegram/WhatsApp/Signal/iMessage/Discord/Slack.
- Previously, if you didnt configure an allowlist, your bot could be **open to anyone** (especially discoverable Telegram bots). - Previously, if you didnt configure an allowlist, your bot could be **open to anyone** (especially discoverable Telegram bots).
- New default: DM pairing (`dmPolicy="pairing"` / `discord.dm.policy="pairing"` / `slack.dm.policy="pairing"`). - New default: DM pairing (`dmPolicy="pairing"` / `discord.dm.policy="pairing"` / `slack.dm.policy="pairing"`).
@ -1170,6 +1231,7 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
- CLI: remove `update`, `gateway-daemon`, `gateway {install|uninstall|start|stop|restart|daemon status|wake|send|agent}`, and `telegram` commands; move `login/logout` to `providers login/logout` (top-level aliases hidden); use `daemon` for service control, `send`/`agent`/`wake` for RPC, and `nodes canvas` for canvas ops. - CLI: remove `update`, `gateway-daemon`, `gateway {install|uninstall|start|stop|restart|daemon status|wake|send|agent}`, and `telegram` commands; move `login/logout` to `providers login/logout` (top-level aliases hidden); use `daemon` for service control, `send`/`agent`/`wake` for RPC, and `nodes canvas` for canvas ops.
### Fixes ### Fixes
- **CLI/Gateway/Doctor:** daemon runtime selection + improved logs/status/health/errors; auth/password handling for local CLI; richer close/timeout details; auto-migrate legacy config/sessions/state; integrity checks + repair prompts; `--yes`/`--non-interactive`; `--deep` gateway scans; better restart/service hints. - **CLI/Gateway/Doctor:** daemon runtime selection + improved logs/status/health/errors; auth/password handling for local CLI; richer close/timeout details; auto-migrate legacy config/sessions/state; integrity checks + repair prompts; `--yes`/`--non-interactive`; `--deep` gateway scans; better restart/service hints.
- **Agent loop + compaction:** compaction/pruning tuning, overflow handling, safer bootstrap context, and per-provider threading/confirmations; opt-in tool-result pruning + compact tracking. - **Agent loop + compaction:** compaction/pruning tuning, overflow handling, safer bootstrap context, and per-provider threading/confirmations; opt-in tool-result pruning + compact tracking.
- **Sandbox + tools:** per-agent sandbox overrides, workspaceAccess controls, session tool visibility, tool policy overrides, process isolation, and tool schema/timeout/reaction unification. - **Sandbox + tools:** per-agent sandbox overrides, workspaceAccess controls, session tool visibility, tool policy overrides, process isolation, and tool schema/timeout/reaction unification.
@ -1181,13 +1243,15 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
- **Docs:** new FAQ/ClawdHub/config examples/showcase entries and clarified auth, sandbox, and systemd docs. - **Docs:** new FAQ/ClawdHub/config examples/showcase entries and clarified auth, sandbox, and systemd docs.
### Maintenance ### Maintenance
- Skills additions (Himalaya email, CodexBar, 1Password). - Skills additions (Himalaya email, CodexBar, 1Password).
- Dependency refreshes (pi-* stack, Slack SDK, discord-api-types, file-type, zod, Biome, Vite). - Dependency refreshes (pi-\* stack, Slack SDK, discord-api-types, file-type, zod, Biome, Vite).
- Refactors: centralized group allowlist/mention policy; lint/import cleanup; switch tsx → bun for TS execution. - Refactors: centralized group allowlist/mention policy; lint/import cleanup; switch tsx → bun for TS execution.
## 2026.1.5 ## 2026.1.5
### Highlights ### Highlights
- Models: add image-specific model config (`agent.imageModel` + fallbacks) and scan support. - Models: add image-specific model config (`agent.imageModel` + fallbacks) and scan support.
- Agent tools: new `image` tool routed to the image model (when configured). - Agent tools: new `image` tool routed to the image model (when configured).
- Config: default model shorthands (`opus`, `sonnet`, `gpt`, `gpt-mini`, `gemini`, `gemini-flash`). - Config: default model shorthands (`opus`, `sonnet`, `gpt`, `gpt-mini`, `gemini`, `gemini-flash`).
@ -1195,6 +1259,7 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
- Bun: optional local install/build workflow without maintaining a Bun lockfile (see `docs/bun.md`). - Bun: optional local install/build workflow without maintaining a Bun lockfile (see `docs/bun.md`).
### Fixes ### Fixes
- Control UI: render Markdown in tool result cards. - Control UI: render Markdown in tool result cards.
- Control UI: prevent overlapping action buttons in Discord guild rules on narrow layouts. - Control UI: prevent overlapping action buttons in Discord guild rules on narrow layouts.
- Android: tapping the foreground service notification brings the app to the front. (#179) — thanks @Syhids - Android: tapping the foreground service notification brings the app to the front. (#179) — thanks @Syhids

View File

@ -3,45 +3,51 @@ summary: "WhatsApp (web channel) integration: login, inbox, replies, media, and
read_when: read_when:
- Working on WhatsApp/web channel behavior or inbox routing - Working on WhatsApp/web channel behavior or inbox routing
--- ---
# WhatsApp (web channel)
# WhatsApp (web channel)
Status: WhatsApp Web via Baileys only. Gateway owns the session(s). Status: WhatsApp Web via Baileys only. Gateway owns the session(s).
## Quick setup (beginner) ## Quick setup (beginner)
1) Use a **separate phone number** if possible (recommended).
2) Configure WhatsApp in `~/.clawdbot/moltbot.json`. 1. Use a **separate phone number** if possible (recommended).
3) Run `moltbot channels login` to scan the QR code (Linked Devices). 2. Configure WhatsApp in `~/.clawdbot/moltbot.json`.
4) Start the gateway. 3. Run `moltbot channels login` to scan the QR code (Linked Devices).
4. Start the gateway.
Minimal config: Minimal config:
```json5 ```json5
{ {
channels: { channels: {
whatsapp: { whatsapp: {
dmPolicy: "allowlist", dmPolicy: "allowlist",
allowFrom: ["+15551234567"] allowFrom: ["+15551234567"],
} },
} },
} }
``` ```
## Goals ## Goals
- Multiple WhatsApp accounts (multi-account) in one Gateway process. - Multiple WhatsApp accounts (multi-account) in one Gateway process.
- Deterministic routing: replies return to WhatsApp, no model routing. - Deterministic routing: replies return to WhatsApp, no model routing.
- Model sees enough context to understand quoted replies. - Model sees enough context to understand quoted replies.
## Config writes ## Config writes
By default, WhatsApp is allowed to write config updates triggered by `/config set|unset` (requires `commands.config: true`). By default, WhatsApp is allowed to write config updates triggered by `/config set|unset` (requires `commands.config: true`).
Disable with: Disable with:
```json5 ```json5
{ {
channels: { whatsapp: { configWrites: false } } channels: { whatsapp: { configWrites: false } },
} }
``` ```
## Architecture (who owns what) ## Architecture (who owns what)
- **Gateway** owns the Baileys socket and inbox loop. - **Gateway** owns the Baileys socket and inbox loop.
- **CLI / macOS app** talk to the gateway; no direct Baileys use. - **CLI / macOS app** talk to the gateway; no direct Baileys use.
- **Active listener** is required for outbound sends; otherwise send fails fast. - **Active listener** is required for outbound sends; otherwise send fails fast.
@ -51,19 +57,21 @@ Disable with:
WhatsApp requires a real mobile number for verification. VoIP and virtual numbers are usually blocked. There are two supported ways to run Moltbot on WhatsApp: WhatsApp requires a real mobile number for verification. VoIP and virtual numbers are usually blocked. There are two supported ways to run Moltbot on WhatsApp:
### Dedicated number (recommended) ### Dedicated number (recommended)
Use a **separate phone number** for Moltbot. Best UX, clean routing, no self-chat quirks. Ideal setup: **spare/old Android phone + eSIM**. Leave it on WiFi and power, and link it via QR. Use a **separate phone number** for Moltbot. Best UX, clean routing, no self-chat quirks. Ideal setup: **spare/old Android phone + eSIM**. Leave it on WiFi and power, and link it via QR.
**WhatsApp Business:** You can use WhatsApp Business on the same device with a different number. Great for keeping your personal WhatsApp separate — install WhatsApp Business and register the Moltbot number there. **WhatsApp Business:** You can use WhatsApp Business on the same device with a different number. Great for keeping your personal WhatsApp separate — install WhatsApp Business and register the Moltbot number there.
**Sample config (dedicated number, single-user allowlist):** **Sample config (dedicated number, single-user allowlist):**
```json5 ```json5
{ {
channels: { channels: {
whatsapp: { whatsapp: {
dmPolicy: "allowlist", dmPolicy: "allowlist",
allowFrom: ["+15551234567"] allowFrom: ["+15551234567"],
} },
} },
} }
``` ```
@ -72,10 +80,12 @@ If you want pairing instead of allowlist, set `channels.whatsapp.dmPolicy` to `p
`moltbot pairing approve whatsapp <code>` `moltbot pairing approve whatsapp <code>`
### Personal number (fallback) ### Personal number (fallback)
Quick fallback: run Moltbot on **your own number**. Message yourself (WhatsApp “Message yourself”) for testing so you dont spam contacts. Expect to read verification codes on your main phone during setup and experiments. **Must enable self-chat mode.** Quick fallback: run Moltbot on **your own number**. Message yourself (WhatsApp “Message yourself”) for testing so you dont spam contacts. Expect to read verification codes on your main phone during setup and experiments. **Must enable self-chat mode.**
When the wizard asks for your personal WhatsApp number, enter the phone you will message from (the owner/sender), not the assistant number. When the wizard asks for your personal WhatsApp number, enter the phone you will message from (the owner/sender), not the assistant number.
**Sample config (personal number, self-chat):** **Sample config (personal number, self-chat):**
```json ```json
{ {
"whatsapp": { "whatsapp": {
@ -91,6 +101,7 @@ if `messages.responsePrefix` is unset. Set it explicitly to customize or disable
the prefix (use `""` to remove it). the prefix (use `""` to remove it).
### Number sourcing tips ### Number sourcing tips
- **Local eSIM** from your country's mobile carrier (most reliable) - **Local eSIM** from your country's mobile carrier (most reliable)
- Austria: [hot.at](https://www.hot.at) - Austria: [hot.at](https://www.hot.at)
- UK: [giffgaff](https://www.giffgaff.com) — free SIM, no contract - UK: [giffgaff](https://www.giffgaff.com) — free SIM, no contract
@ -101,6 +112,7 @@ the prefix (use `""` to remove it).
**Tip:** The number only needs to receive one verification SMS. After that, WhatsApp Web sessions persist via `creds.json`. **Tip:** The number only needs to receive one verification SMS. After that, WhatsApp Web sessions persist via `creds.json`.
## Why Not Twilio? ## Why Not Twilio?
- Early Moltbot builds supported Twilios WhatsApp Business integration. - Early Moltbot builds supported Twilios WhatsApp Business integration.
- WhatsApp Business numbers are a poor fit for a personal assistant. - WhatsApp Business numbers are a poor fit for a personal assistant.
- Meta enforces a 24hour reply window; if you havent responded in the last 24 hours, the business number cant initiate new messages. - Meta enforces a 24hour reply window; if you havent responded in the last 24 hours, the business number cant initiate new messages.
@ -108,6 +120,7 @@ the prefix (use `""` to remove it).
- Result: unreliable delivery and frequent blocks, so support was removed. - Result: unreliable delivery and frequent blocks, so support was removed.
## Login + credentials ## Login + credentials
- Login command: `moltbot channels login` (QR via Linked Devices). - Login command: `moltbot channels login` (QR via Linked Devices).
- Multi-account login: `moltbot channels login --account <id>` (`<id>` = `accountId`). - Multi-account login: `moltbot channels login --account <id>` (`<id>` = `accountId`).
- Default account (when `--account` is omitted): `default` if present, otherwise the first configured account id (sorted). - Default account (when `--account` is omitted): `default` if present, otherwise the first configured account id (sorted).
@ -118,6 +131,7 @@ the prefix (use `""` to remove it).
- Logged-out socket => error instructs re-link. - Logged-out socket => error instructs re-link.
## Inbound flow (DM + group) ## Inbound flow (DM + group)
- WhatsApp events come from `messages.upsert` (Baileys). - WhatsApp events come from `messages.upsert` (Baileys).
- Inbox listeners are detached on shutdown to avoid accumulating event handlers in tests/restarts. - Inbox listeners are detached on shutdown to avoid accumulating event handlers in tests/restarts.
- Status/broadcast chats are ignored. - Status/broadcast chats are ignored.
@ -128,38 +142,44 @@ the prefix (use `""` to remove it).
- Your linked WhatsApp number is implicitly trusted, so self messages skip `channels.whatsapp.dmPolicy` and `channels.whatsapp.allowFrom` checks. - Your linked WhatsApp number is implicitly trusted, so self messages skip `channels.whatsapp.dmPolicy` and `channels.whatsapp.allowFrom` checks.
### Personal-number mode (fallback) ### Personal-number mode (fallback)
If you run Moltbot on your **personal WhatsApp number**, enable `channels.whatsapp.selfChatMode` (see sample above). If you run Moltbot on your **personal WhatsApp number**, enable `channels.whatsapp.selfChatMode` (see sample above).
Behavior: Behavior:
- Outbound DMs never trigger pairing replies (prevents spamming contacts). - Outbound DMs never trigger pairing replies (prevents spamming contacts).
- Inbound unknown senders still follow `channels.whatsapp.dmPolicy`. - Inbound unknown senders still follow `channels.whatsapp.dmPolicy`.
- Self-chat mode (allowFrom includes your number) avoids auto read receipts and ignores mention JIDs. - Self-chat mode (allowFrom includes your number) avoids auto read receipts and ignores mention JIDs.
- Read receipts sent for non-self-chat DMs. - Read receipts sent for non-self-chat DMs.
## Read receipts ## Read receipts
By default, the gateway marks inbound WhatsApp messages as read (blue ticks) once they are accepted. By default, the gateway marks inbound WhatsApp messages as read (blue ticks) once they are accepted.
Disable globally: Disable globally:
```json5 ```json5
{ {
channels: { whatsapp: { sendReadReceipts: false } } channels: { whatsapp: { sendReadReceipts: false } },
} }
``` ```
Disable per account: Disable per account:
```json5 ```json5
{ {
channels: { channels: {
whatsapp: { whatsapp: {
accounts: { accounts: {
personal: { sendReadReceipts: false } personal: { sendReadReceipts: false },
} },
} },
} },
} }
``` ```
Notes: Notes:
- Self-chat mode always skips read receipts. - Self-chat mode always skips read receipts.
## WhatsApp FAQ: sending messages + pairing ## WhatsApp FAQ: sending messages + pairing
@ -169,6 +189,7 @@ No. Default DM policy is **pairing**, so unknown senders only get a pairing code
**How does pairing work on WhatsApp?** **How does pairing work on WhatsApp?**
Pairing is a DM gate for unknown senders: Pairing is a DM gate for unknown senders:
- First DM from a new sender returns a short code (message is not processed). - First DM from a new sender returns a short code (message is not processed).
- Approve with: `moltbot pairing approve whatsapp <code>` (list with `moltbot pairing list whatsapp`). - Approve with: `moltbot pairing approve whatsapp <code>` (list with `moltbot pairing list whatsapp`).
- Codes expire after 1 hour; pending requests are capped at 3 per channel. - Codes expire after 1 hour; pending requests are capped at 3 per channel.
@ -180,6 +201,7 @@ Yes, by routing each sender to a different agent via `bindings` (peer `kind: "dm
The wizard uses it to set your **allowlist/owner** so your own DMs are permitted. Its not used for auto-sending. If you run on your personal WhatsApp number, use that same number and enable `channels.whatsapp.selfChatMode`. The wizard uses it to set your **allowlist/owner** so your own DMs are permitted. Its not used for auto-sending. If you run on your personal WhatsApp number, use that same number and enable `channels.whatsapp.selfChatMode`.
## Message normalization (what the model sees) ## Message normalization (what the model sees)
- `Body` is the current message body with envelope. - `Body` is the current message body with envelope.
- Quoted reply context is **always appended**: - Quoted reply context is **always appended**:
``` ```
@ -195,6 +217,7 @@ The wizard uses it to set your **allowlist/owner** so your own DMs are permitted
- `<media:image|video|audio|document|sticker>` - `<media:image|video|audio|document|sticker>`
## Groups ## Groups
- Groups map to `agent:<agentId>:whatsapp:group:<jid>` sessions. - Groups map to `agent:<agentId>:whatsapp:group:<jid>` sessions.
- Group policy: `channels.whatsapp.groupPolicy = open|disabled|allowlist` (default `allowlist`). - Group policy: `channels.whatsapp.groupPolicy = open|disabled|allowlist` (default `allowlist`).
- Activation modes: - Activation modes:
@ -203,7 +226,7 @@ The wizard uses it to set your **allowlist/owner** so your own DMs are permitted
- `/activation mention|always` is owner-only and must be sent as a standalone message. - `/activation mention|always` is owner-only and must be sent as a standalone message.
- Owner = `channels.whatsapp.allowFrom` (or self E.164 if unset). - Owner = `channels.whatsapp.allowFrom` (or self E.164 if unset).
- **History injection** (pending-only): - **History injection** (pending-only):
- Recent *unprocessed* messages (default 50) inserted under: - Recent _unprocessed_ messages (default 50) inserted under:
`[Chat messages since your last reply - for context]` (messages already in the session are not re-injected) `[Chat messages since your last reply - for context]` (messages already in the session are not re-injected)
- Current message under: - Current message under:
`[Current message - respond to this]` `[Current message - respond to this]`
@ -211,6 +234,7 @@ The wizard uses it to set your **allowlist/owner** so your own DMs are permitted
- Group metadata cached 5 min (subject + participants). - Group metadata cached 5 min (subject + participants).
## Reply delivery (threading) ## Reply delivery (threading)
- WhatsApp Web sends standard messages (no quoted reply threading in the current gateway). - WhatsApp Web sends standard messages (no quoted reply threading in the current gateway).
- Reply tags are ignored on this channel. - Reply tags are ignored on this channel.
@ -219,6 +243,7 @@ The wizard uses it to set your **allowlist/owner** so your own DMs are permitted
WhatsApp can automatically send emoji reactions to incoming messages immediately upon receipt, before the bot generates a reply. This provides instant feedback to users that their message was received. WhatsApp can automatically send emoji reactions to incoming messages immediately upon receipt, before the bot generates a reply. This provides instant feedback to users that their message was received.
**Configuration:** **Configuration:**
```json ```json
{ {
"whatsapp": { "whatsapp": {
@ -232,6 +257,7 @@ WhatsApp can automatically send emoji reactions to incoming messages immediately
``` ```
**Options:** **Options:**
- `emoji` (string): Emoji to use for acknowledgment (e.g., "👀", "✅", "📨"). Empty or omitted = feature disabled. - `emoji` (string): Emoji to use for acknowledgment (e.g., "👀", "✅", "📨"). Empty or omitted = feature disabled.
- `direct` (boolean, default: `true`): Send reactions in direct/DM chats. - `direct` (boolean, default: `true`): Send reactions in direct/DM chats.
- `group` (string, default: `"mentions"`): Group chat behavior: - `group` (string, default: `"mentions"`): Group chat behavior:
@ -240,6 +266,7 @@ WhatsApp can automatically send emoji reactions to incoming messages immediately
- `"never"`: Never react in groups - `"never"`: Never react in groups
**Per-account override:** **Per-account override:**
```json ```json
{ {
"whatsapp": { "whatsapp": {
@ -257,6 +284,7 @@ WhatsApp can automatically send emoji reactions to incoming messages immediately
``` ```
**Behavior notes:** **Behavior notes:**
- Reactions are sent **immediately** upon message receipt, before typing indicators or bot replies. - Reactions are sent **immediately** upon message receipt, before typing indicators or bot replies.
- In groups with `requireMention: false` (activation: always), `group: "mentions"` will react to all messages (not just @mentions). - In groups with `requireMention: false` (activation: always), `group: "mentions"` will react to all messages (not just @mentions).
- Fire-and-forget: reaction failures are logged but don't prevent the bot from replying. - Fire-and-forget: reaction failures are logged but don't prevent the bot from replying.
@ -264,18 +292,35 @@ WhatsApp can automatically send emoji reactions to incoming messages immediately
- WhatsApp ignores `messages.ackReaction`; use `channels.whatsapp.ackReaction` instead. - WhatsApp ignores `messages.ackReaction`; use `channels.whatsapp.ackReaction` instead.
## Agent tool (reactions) ## Agent tool (reactions)
- Tool: `whatsapp` with `react` action (`chatJid`, `messageId`, `emoji`, optional `remove`). - Tool: `whatsapp` with `react` action (`chatJid`, `messageId`, `emoji`, optional `remove`).
- Optional: `participant` (group sender), `fromMe` (reacting to your own message), `accountId` (multi-account). - Optional: `participant` (group sender), `fromMe` (reacting to your own message), `accountId` (multi-account).
- Reaction removal semantics: see [/tools/reactions](/tools/reactions). - Reaction removal semantics: see [/tools/reactions](/tools/reactions).
- Tool gating: `channels.whatsapp.actions.reactions` (default: enabled). - 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
```
- Reaction removals (empty emoji) are silently skipped.
- 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 ## Limits
- Outbound text is chunked to `channels.whatsapp.textChunkLimit` (default 4000). - 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. - Optional newline chunking: set `channels.whatsapp.chunkMode="newline"` to split on blank lines (paragraph boundaries) before length chunking.
- Inbound media saves are capped by `channels.whatsapp.mediaMaxMb` (default 50 MB). - Inbound media saves are capped by `channels.whatsapp.mediaMaxMb` (default 50 MB).
- Outbound media items are capped by `agents.defaults.mediaMaxMb` (default 5 MB). - Outbound media items are capped by `agents.defaults.mediaMaxMb` (default 5 MB).
## Outbound send (text + media) ## Outbound send (text + media)
- Uses active web listener; error if gateway not running. - Uses active web listener; error if gateway not running.
- Text chunking: 4k max per message (configurable via `channels.whatsapp.textChunkLimit`, optional `channels.whatsapp.chunkMode`). - Text chunking: 4k max per message (configurable via `channels.whatsapp.textChunkLimit`, optional `channels.whatsapp.chunkMode`).
- Media: - Media:
@ -288,17 +333,21 @@ WhatsApp can automatically send emoji reactions to incoming messages immediately
- Gateway: `send` params include `gifPlayback: true` - Gateway: `send` params include `gifPlayback: true`
## Voice notes (PTT audio) ## Voice notes (PTT audio)
WhatsApp sends audio as **voice notes** (PTT bubble). WhatsApp sends audio as **voice notes** (PTT bubble).
- Best results: OGG/Opus. Moltbot rewrites `audio/ogg` to `audio/ogg; codecs=opus`. - Best results: OGG/Opus. Moltbot rewrites `audio/ogg` to `audio/ogg; codecs=opus`.
- `[[audio_as_voice]]` is ignored for WhatsApp (audio already ships as voice note). - `[[audio_as_voice]]` is ignored for WhatsApp (audio already ships as voice note).
## Media limits + optimization ## Media limits + optimization
- Default outbound cap: 5 MB (per media item). - Default outbound cap: 5 MB (per media item).
- Override: `agents.defaults.mediaMaxMb`. - Override: `agents.defaults.mediaMaxMb`.
- Images are auto-optimized to JPEG under cap (resize + quality sweep). - Images are auto-optimized to JPEG under cap (resize + quality sweep).
- Oversize media => error; media reply falls back to text warning. - Oversize media => error; media reply falls back to text warning.
## Heartbeats ## Heartbeats
- **Gateway heartbeat** logs connection health (`web.heartbeatSeconds`, default 60s). - **Gateway heartbeat** logs connection health (`web.heartbeatSeconds`, default 60s).
- **Agent heartbeat** can be configured per agent (`agents.list[].heartbeat`) or globally - **Agent heartbeat** can be configured per agent (`agents.list[].heartbeat`) or globally
via `agents.defaults.heartbeat` (fallback when no per-agent entries are set). via `agents.defaults.heartbeat` (fallback when no per-agent entries are set).
@ -306,12 +355,14 @@ WhatsApp sends audio as **voice notes** (PTT bubble).
- Delivery defaults to the last used channel (or configured target). - Delivery defaults to the last used channel (or configured target).
## Reconnect behavior ## Reconnect behavior
- Backoff policy: `web.reconnect`: - Backoff policy: `web.reconnect`:
- `initialMs`, `maxMs`, `factor`, `jitter`, `maxAttempts`. - `initialMs`, `maxMs`, `factor`, `jitter`, `maxAttempts`.
- If maxAttempts reached, web monitoring stops (degraded). - If maxAttempts reached, web monitoring stops (degraded).
- Logged-out => stop and require re-link. - Logged-out => stop and require re-link.
## Config quick map ## Config quick map
- `channels.whatsapp.dmPolicy` (DM policy: pairing/allowlist/open/disabled). - `channels.whatsapp.dmPolicy` (DM policy: pairing/allowlist/open/disabled).
- `channels.whatsapp.selfChatMode` (same-phone setup; bot uses your personal WhatsApp number). - `channels.whatsapp.selfChatMode` (same-phone setup; bot uses your personal WhatsApp number).
- `channels.whatsapp.allowFrom` (DM allowlist). WhatsApp uses E.164 phone numbers (no usernames). - `channels.whatsapp.allowFrom` (DM allowlist). WhatsApp uses E.164 phone numbers (no usernames).
@ -343,6 +394,7 @@ WhatsApp sends audio as **voice notes** (PTT bubble).
- `web.reconnect.*` - `web.reconnect.*`
## Logs + troubleshooting ## Logs + troubleshooting
- Subsystems: `whatsapp/inbound`, `whatsapp/outbound`, `web-heartbeat`, `web-reconnect`. - Subsystems: `whatsapp/inbound`, `whatsapp/outbound`, `web-heartbeat`, `web-reconnect`.
- Log file: `/tmp/moltbot/moltbot-YYYY-MM-DD.log` (configurable). - Log file: `/tmp/moltbot/moltbot-YYYY-MM-DD.log` (configurable).
- Troubleshooting guide: [Gateway troubleshooting](/gateway/troubleshooting). - Troubleshooting guide: [Gateway troubleshooting](/gateway/troubleshooting).
@ -350,13 +402,16 @@ WhatsApp sends audio as **voice notes** (PTT bubble).
## Troubleshooting (quick) ## Troubleshooting (quick)
**Not linked / QR login required** **Not linked / QR login required**
- Symptom: `channels status` shows `linked: false` or warns “Not linked”. - Symptom: `channels status` shows `linked: false` or warns “Not linked”.
- Fix: run `moltbot channels login` on the gateway host and scan the QR (WhatsApp → Settings → Linked Devices). - Fix: run `moltbot channels login` on the gateway host and scan the QR (WhatsApp → Settings → Linked Devices).
**Linked but disconnected / reconnect loop** **Linked but disconnected / reconnect loop**
- Symptom: `channels status` shows `running, disconnected` or warns “Linked but disconnected”. - Symptom: `channels status` shows `running, disconnected` or warns “Linked but disconnected”.
- Fix: `moltbot doctor` (or restart the gateway). If it persists, relink via `channels login` and inspect `moltbot logs --follow`. - Fix: `moltbot doctor` (or restart the gateway). If it persists, relink via `channels login` and inspect `moltbot logs --follow`.
**Bun runtime** **Bun runtime**
- Bun is **not recommended**. WhatsApp (Baileys) and Telegram are unreliable on Bun. - Bun is **not recommended**. WhatsApp (Baileys) and Telegram are unreliable on Bun.
Run the gateway with **Node**. (See Getting Started runtime note.) Run the gateway with **Node**. (See Getting Started runtime note.)

View File

@ -3,6 +3,7 @@ summary: "Reaction semantics shared across channels"
read_when: read_when:
- Working on reactions in any channel - Working on reactions in any channel
--- ---
# Reaction tooling # Reaction tooling
Shared reaction semantics across channels: 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. - **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`). - **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. - **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).

View File

@ -0,0 +1,187 @@
import "./test-helpers.js";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
vi.mock("../agents/pi-embedded.js", () => ({
abortEmbeddedPiRun: vi.fn().mockReturnValue(false),
isEmbeddedPiRunActive: vi.fn().mockReturnValue(false),
isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false),
runEmbeddedPiAgent: vi.fn(),
queueEmbeddedPiMessage: vi.fn().mockReturnValue(false),
resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`,
}));
import { resetInboundDedupe } from "../auto-reply/reply/inbound-dedupe.js";
import {
drainSystemEvents,
peekSystemEvents,
resetSystemEventsForTest,
} from "../infra/system-events.js";
import { resetLogger, setLoggerOverride } from "../logging.js";
import { resolveAgentRoute } from "../routing/resolve-route.js";
import { monitorWebChannel } from "./auto-reply.js";
import type { WebInboundReaction } from "./inbound.js";
import { resetBaileysMocks, resetLoadConfigMock, setLoadConfigMock } from "./test-helpers.js";
let previousHome: string | undefined;
let tempHome: string | undefined;
beforeEach(async () => {
resetInboundDedupe();
resetSystemEventsForTest();
previousHome = process.env.HOME;
tempHome = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-web-home-"));
process.env.HOME = tempHome;
});
afterEach(async () => {
process.env.HOME = previousHome;
if (tempHome) {
await fs.rm(tempHome, { recursive: true, force: true }).catch(() => {});
tempHome = undefined;
}
});
describe("web auto-reply inbound reaction system events", () => {
beforeEach(() => {
vi.clearAllMocks();
resetBaileysMocks();
resetLoadConfigMock();
});
afterEach(() => {
resetLogger();
setLoggerOverride(null);
vi.useRealTimers();
});
it("enqueues a system event when a reaction is received", async () => {
setLoadConfigMock(() => ({
channels: { whatsapp: { allowFrom: ["*"] } },
messages: {},
}));
let capturedOnReaction: ((reaction: WebInboundReaction) => void) | undefined;
const listenerFactory = async (opts: {
onMessage: (...args: unknown[]) => Promise<void>;
onReaction?: (reaction: WebInboundReaction) => void;
}) => {
capturedOnReaction = opts.onReaction;
return { close: vi.fn() };
};
await monitorWebChannel(false, listenerFactory, false);
expect(capturedOnReaction).toBeDefined();
const cfg = { channels: { whatsapp: { allowFrom: ["*"] } }, messages: {} };
const route = resolveAgentRoute({
cfg: cfg as Parameters<typeof resolveAgentRoute>[0]["cfg"],
channel: "whatsapp",
accountId: "default",
peer: { kind: "dm", id: "999@s.whatsapp.net" },
});
// Drain the "WhatsApp gateway connected" event so we only check reaction events
drainSystemEvents(route.sessionKey);
capturedOnReaction!({
messageId: "msg-abc",
emoji: "👍",
chatJid: "999@s.whatsapp.net",
chatType: "direct",
accountId: "default",
senderJid: "999@s.whatsapp.net",
senderE164: "+999",
timestamp: Date.now(),
});
const events = peekSystemEvents(route.sessionKey);
expect(events).toHaveLength(1);
expect(events[0]).toBe("WhatsApp reaction added: 👍 by +999 msg msg-abc");
});
it("uses senderJid when senderE164 is unavailable", async () => {
setLoadConfigMock(() => ({
channels: { whatsapp: { allowFrom: ["*"] } },
messages: {},
}));
let capturedOnReaction: ((reaction: WebInboundReaction) => void) | undefined;
const listenerFactory = async (opts: {
onMessage: (...args: unknown[]) => Promise<void>;
onReaction?: (reaction: WebInboundReaction) => void;
}) => {
capturedOnReaction = opts.onReaction;
return { close: vi.fn() };
};
await monitorWebChannel(false, listenerFactory, false);
const cfg = { channels: { whatsapp: { allowFrom: ["*"] } }, messages: {} };
const route = resolveAgentRoute({
cfg: cfg as Parameters<typeof resolveAgentRoute>[0]["cfg"],
channel: "whatsapp",
accountId: "default",
peer: { kind: "dm", id: "999@s.whatsapp.net" },
});
drainSystemEvents(route.sessionKey);
capturedOnReaction!({
messageId: "msg-xyz",
emoji: "❤️",
chatJid: "999@s.whatsapp.net",
chatType: "direct",
accountId: "default",
senderJid: "999@s.whatsapp.net",
timestamp: Date.now(),
});
const events = peekSystemEvents(route.sessionKey);
expect(events).toHaveLength(1);
expect(events[0]).toBe("WhatsApp reaction added: ❤️ by 999@s.whatsapp.net msg msg-xyz");
});
it("falls back to 'someone' when no sender info available", async () => {
setLoadConfigMock(() => ({
channels: { whatsapp: { allowFrom: ["*"] } },
messages: {},
}));
let capturedOnReaction: ((reaction: WebInboundReaction) => void) | undefined;
const listenerFactory = async (opts: {
onMessage: (...args: unknown[]) => Promise<void>;
onReaction?: (reaction: WebInboundReaction) => void;
}) => {
capturedOnReaction = opts.onReaction;
return { close: vi.fn() };
};
await monitorWebChannel(false, listenerFactory, false);
const cfg = { channels: { whatsapp: { allowFrom: ["*"] } }, messages: {} };
const route = resolveAgentRoute({
cfg: cfg as Parameters<typeof resolveAgentRoute>[0]["cfg"],
channel: "whatsapp",
accountId: "default",
peer: { kind: "dm", id: "999@s.whatsapp.net" },
});
drainSystemEvents(route.sessionKey);
capturedOnReaction!({
messageId: "msg-noid",
emoji: "🔥",
chatJid: "999@s.whatsapp.net",
chatType: "direct",
accountId: "default",
timestamp: Date.now(),
});
const events = peekSystemEvents(route.sessionKey);
expect(events).toHaveLength(1);
expect(events[0]).toBe("WhatsApp reaction added: 🔥 by someone msg msg-noid");
});
});

View File

@ -28,6 +28,7 @@ import { whatsappHeartbeatLog, whatsappLog } from "./loggers.js";
import { buildMentionConfig } from "./mentions.js"; import { buildMentionConfig } from "./mentions.js";
import { createEchoTracker } from "./monitor/echo.js"; import { createEchoTracker } from "./monitor/echo.js";
import { createWebOnMessageHandler } from "./monitor/on-message.js"; import { createWebOnMessageHandler } from "./monitor/on-message.js";
import type { WebInboundReaction } from "../inbound.js";
import type { WebChannelStatus, WebInboundMsg, WebMonitorTuning } from "./types.js"; import type { WebChannelStatus, WebInboundMsg, WebMonitorTuning } from "./types.js";
import { isLikelyWhatsAppCryptoError } from "./util.js"; import { isLikelyWhatsAppCryptoError } from "./util.js";
@ -173,7 +174,10 @@ export async function monitorWebChannel(
account, account,
}); });
const inboundDebounceMs = resolveInboundDebounceMs({ cfg, channel: "whatsapp" }); const inboundDebounceMs = resolveInboundDebounceMs({
cfg,
channel: "whatsapp",
});
const shouldDebounce = (msg: WebInboundMsg) => { const shouldDebounce = (msg: WebInboundMsg) => {
if (msg.mediaPath || msg.mediaType) return false; if (msg.mediaPath || msg.mediaType) return false;
if (msg.location) return false; if (msg.location) return false;
@ -198,6 +202,31 @@ export async function monitorWebChannel(
_lastInboundMsg = msg; _lastInboundMsg = msg;
await onMessage(msg); await onMessage(msg);
}, },
onReaction: (reaction: WebInboundReaction) => {
status.lastEventAt = Date.now();
emitStatus();
const route = resolveAgentRoute({
cfg,
channel: "whatsapp",
accountId: reaction.accountId,
peer: {
kind: reaction.chatType === "group" ? "group" : "dm",
id: reaction.chatJid,
},
});
const senderLabel = reaction.senderE164 ?? reaction.senderJid ?? "someone";
const text = `WhatsApp reaction added: ${reaction.emoji} by ${senderLabel} msg ${reaction.messageId}`;
const contextKey = [
"whatsapp",
"reaction",
"added",
reaction.messageId,
reaction.senderJid ?? "unknown",
reaction.emoji,
reaction.chatJid,
].join(":");
enqueueSystemEvent(text, { sessionKey: route.sessionKey, contextKey });
},
}); });
status.connected = true; status.connected = true;

View File

@ -1,4 +1,8 @@
export { resetWebInboundDedupe } from "./inbound/dedupe.js"; export { resetWebInboundDedupe } from "./inbound/dedupe.js";
export { extractLocationData, extractMediaPlaceholder, extractText } from "./inbound/extract.js"; export { extractLocationData, extractMediaPlaceholder, extractText } from "./inbound/extract.js";
export { monitorWebInbox } from "./inbound/monitor.js"; export { monitorWebInbox } from "./inbound/monitor.js";
export type { WebInboundMessage, WebListenerCloseReason } from "./inbound/types.js"; export type {
WebInboundMessage,
WebInboundReaction,
WebListenerCloseReason,
} from "./inbound/types.js";

View File

@ -20,13 +20,15 @@ import {
} from "./extract.js"; } from "./extract.js";
import { downloadInboundMedia } from "./media.js"; import { downloadInboundMedia } from "./media.js";
import { createWebSendApi } from "./send-api.js"; import { createWebSendApi } from "./send-api.js";
import type { WebInboundMessage, WebListenerCloseReason } from "./types.js"; import type { WebInboundMessage, WebInboundReaction, WebListenerCloseReason } from "./types.js";
export async function monitorWebInbox(options: { export async function monitorWebInbox(options: {
verbose: boolean; verbose: boolean;
accountId: string; accountId: string;
authDir: string; authDir: string;
onMessage: (msg: WebInboundMessage) => Promise<void>; onMessage: (msg: WebInboundMessage) => Promise<void>;
/** Called when a reaction is received on a message. */
onReaction?: (reaction: WebInboundReaction) => void;
mediaMaxMb?: number; mediaMaxMb?: number;
/** Send read receipts for incoming messages (default true). */ /** Send read receipts for incoming messages (default true). */
sendReadReceipts?: boolean; sendReadReceipts?: boolean;
@ -313,6 +315,67 @@ export async function monitorWebInbox(options: {
}; };
sock.ev.on("messages.upsert", handleMessagesUpsert); sock.ev.on("messages.upsert", handleMessagesUpsert);
// Baileys emits messages.reaction when someone reacts to a message.
const handleMessagesReaction = async (
reactions: Array<{
key: proto.IMessageKey;
reaction: { text?: string; key?: proto.IMessageKey };
}>,
) => {
if (!options.onReaction) return;
try {
for (const entry of reactions) {
try {
const targetKey = entry.key;
const reactionKey = entry.reaction?.key;
const messageId = targetKey?.id;
if (!messageId) continue;
const chatJid = targetKey.remoteJid;
if (!chatJid) continue;
if (chatJid.endsWith("@status") || chatJid.endsWith("@broadcast")) continue;
const emoji = entry.reaction?.text ?? "";
// Empty emoji = reaction removed; skip removals (matches Signal behavior)
if (!emoji) continue;
const group = isJidGroup(chatJid) === true;
// In DMs, reactionKey.fromMe means we reacted (remoteJid is the partner, not us)
const senderJid = reactionKey?.fromMe
? (selfJid ?? undefined)
: (reactionKey?.participant ?? reactionKey?.remoteJid ?? undefined);
const senderE164 = senderJid ? await resolveInboundJid(senderJid) : null;
inboundLogger.info({ emoji, messageId, chatJid, senderJid }, "inbound reaction");
options.onReaction({
messageId,
emoji,
chatJid,
chatType: group ? "group" : "direct",
accountId: options.accountId,
senderJid: senderJid ?? undefined,
senderE164: senderE164 ?? undefined,
reactedToFromMe: targetKey.fromMe ?? undefined,
timestamp: Date.now(),
});
} catch (err) {
inboundLogger.error(
{
error: String(err),
messageId: entry.key?.id,
chatJid: entry.key?.remoteJid,
},
"failed handling inbound reaction",
);
}
}
} catch (outerErr) {
inboundLogger.error({ error: String(outerErr) }, "reaction handler crashed");
}
};
sock.ev.on("messages.reaction", handleMessagesReaction as (...args: unknown[]) => void);
const handleConnectionUpdate = ( const handleConnectionUpdate = (
update: Partial<import("@whiskeysockets/baileys").ConnectionState>, update: Partial<import("@whiskeysockets/baileys").ConnectionState>,
) => { ) => {
@ -350,14 +413,19 @@ export async function monitorWebInbox(options: {
const messagesUpsertHandler = handleMessagesUpsert as unknown as ( const messagesUpsertHandler = handleMessagesUpsert as unknown as (
...args: unknown[] ...args: unknown[]
) => void; ) => void;
const messagesReactionHandler = handleMessagesReaction as unknown as (
...args: unknown[]
) => void;
const connectionUpdateHandler = handleConnectionUpdate as unknown as ( const connectionUpdateHandler = handleConnectionUpdate as unknown as (
...args: unknown[] ...args: unknown[]
) => void; ) => void;
if (typeof ev.off === "function") { if (typeof ev.off === "function") {
ev.off("messages.upsert", messagesUpsertHandler); ev.off("messages.upsert", messagesUpsertHandler);
ev.off("messages.reaction", messagesReactionHandler);
ev.off("connection.update", connectionUpdateHandler); ev.off("connection.update", connectionUpdateHandler);
} else if (typeof ev.removeListener === "function") { } else if (typeof ev.removeListener === "function") {
ev.removeListener("messages.upsert", messagesUpsertHandler); ev.removeListener("messages.upsert", messagesUpsertHandler);
ev.removeListener("messages.reaction", messagesReactionHandler);
ev.removeListener("connection.update", connectionUpdateHandler); ev.removeListener("connection.update", connectionUpdateHandler);
} }
sock.ws?.close(); sock.ws?.close();

View File

@ -40,3 +40,24 @@ export type WebInboundMessage = {
mediaUrl?: string; mediaUrl?: string;
wasMentioned?: boolean; wasMentioned?: boolean;
}; };
export type WebInboundReaction = {
/** Message ID being reacted to. */
messageId: string;
/** Emoji text (empty string = reaction removed). */
emoji: string;
/** JID of the chat where the reaction occurred. */
chatJid: string;
chatType: "direct" | "group";
/** Account that received the reaction. */
accountId: string;
/** JID of the person who reacted. */
senderJid?: string;
/** E.164 of the person who reacted. */
senderE164?: string;
/** Push name of the person who reacted. */
senderName?: string;
/** Whether the reacted message was sent by us. */
reactedToFromMe?: boolean;
timestamp?: number;
};

View File

@ -0,0 +1,460 @@
import { vi } from "vitest";
vi.mock("../media/store.js", () => ({
saveMediaBuffer: vi.fn().mockResolvedValue({
id: "mid",
path: "/tmp/mid",
size: 1,
contentType: "image/jpeg",
}),
}));
const mockLoadConfig = vi.fn().mockReturnValue({
channels: {
whatsapp: {
allowFrom: ["*"],
},
},
messages: {
messagePrefix: undefined,
responsePrefix: undefined,
},
});
const readAllowFromStoreMock = vi.fn().mockResolvedValue([]);
const upsertPairingRequestMock = vi.fn().mockResolvedValue({ code: "PAIRCODE", created: true });
vi.mock("../config/config.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../config/config.js")>();
return {
...actual,
loadConfig: () => mockLoadConfig(),
};
});
vi.mock("../pairing/pairing-store.js", () => ({
readChannelAllowFromStore: (...args: unknown[]) => readAllowFromStoreMock(...args),
upsertChannelPairingRequest: (...args: unknown[]) => upsertPairingRequestMock(...args),
}));
vi.mock("./session.js", () => {
const { EventEmitter } = require("node:events");
const ev = new EventEmitter();
const sock = {
ev,
ws: { close: vi.fn() },
sendPresenceUpdate: vi.fn().mockResolvedValue(undefined),
sendMessage: vi.fn().mockResolvedValue(undefined),
readMessages: vi.fn().mockResolvedValue(undefined),
updateMediaMessage: vi.fn(),
logger: {},
signalRepository: {
lidMapping: {
getPNForLID: vi.fn().mockResolvedValue(null),
},
},
user: { id: "123@s.whatsapp.net" },
groupMetadata: vi.fn().mockResolvedValue({ subject: "Test Group", participants: [] }),
};
return {
createWaSocket: vi.fn().mockResolvedValue(sock),
waitForWaConnection: vi.fn().mockResolvedValue(undefined),
getStatusCode: vi.fn(() => 500),
};
});
const { createWaSocket } = await import("./session.js");
import fsSync from "node:fs";
import os from "node:os";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { resetLogger, setLoggerOverride } from "../logging.js";
import { monitorWebInbox, resetWebInboundDedupe } from "./inbound.js";
const ACCOUNT_ID = "default";
let authDir: string;
describe("web monitor inbox inbound reactions", () => {
beforeEach(() => {
vi.clearAllMocks();
readAllowFromStoreMock.mockResolvedValue([]);
resetWebInboundDedupe();
authDir = fsSync.mkdtempSync(path.join(os.tmpdir(), "moltbot-auth-"));
});
afterEach(() => {
resetLogger();
setLoggerOverride(null);
vi.useRealTimers();
fsSync.rmSync(authDir, { recursive: true, force: true });
});
it("calls onReaction for inbound reaction events", async () => {
const onMessage = vi.fn(async () => {});
const onReaction = vi.fn();
const listener = await monitorWebInbox({
verbose: false,
onMessage,
onReaction,
accountId: ACCOUNT_ID,
authDir,
});
const sock = await createWaSocket();
const reactionEvent = [
{
key: {
id: "msg-123",
remoteJid: "999@s.whatsapp.net",
fromMe: false,
},
reaction: {
text: "👍",
key: {
remoteJid: "888@s.whatsapp.net",
participant: "888@s.whatsapp.net",
},
},
},
];
sock.ev.emit("messages.reaction", reactionEvent);
await new Promise((resolve) => setImmediate(resolve));
expect(onReaction).toHaveBeenCalledTimes(1);
expect(onReaction).toHaveBeenCalledWith(
expect.objectContaining({
messageId: "msg-123",
emoji: "👍",
chatJid: "999@s.whatsapp.net",
chatType: "direct",
accountId: ACCOUNT_ID,
senderJid: "888@s.whatsapp.net",
reactedToFromMe: false,
}),
);
expect(onMessage).not.toHaveBeenCalled();
await listener.close();
});
it("skips reaction removals (empty emoji)", async () => {
const onReaction = vi.fn();
const listener = await monitorWebInbox({
verbose: false,
onMessage: vi.fn(async () => {}),
onReaction,
accountId: ACCOUNT_ID,
authDir,
});
const sock = await createWaSocket();
sock.ev.emit("messages.reaction", [
{
key: {
id: "msg-123",
remoteJid: "999@s.whatsapp.net",
fromMe: false,
},
reaction: {
text: "",
key: { remoteJid: "888@s.whatsapp.net" },
},
},
]);
await new Promise((resolve) => setImmediate(resolve));
expect(onReaction).not.toHaveBeenCalled();
await listener.close();
});
it("skips reactions on status/broadcast JIDs", async () => {
const onReaction = vi.fn();
const listener = await monitorWebInbox({
verbose: false,
onMessage: vi.fn(async () => {}),
onReaction,
accountId: ACCOUNT_ID,
authDir,
});
const sock = await createWaSocket();
sock.ev.emit("messages.reaction", [
{
key: {
id: "msg-123",
remoteJid: "status@status",
fromMe: false,
},
reaction: {
text: "👍",
key: { remoteJid: "888@s.whatsapp.net" },
},
},
]);
await new Promise((resolve) => setImmediate(resolve));
expect(onReaction).not.toHaveBeenCalled();
await listener.close();
});
it("identifies group reactions by chatType", async () => {
const onReaction = vi.fn();
const listener = await monitorWebInbox({
verbose: false,
onMessage: vi.fn(async () => {}),
onReaction,
accountId: ACCOUNT_ID,
authDir,
});
const sock = await createWaSocket();
sock.ev.emit("messages.reaction", [
{
key: {
id: "msg-456",
remoteJid: "120363@g.us",
fromMe: true,
},
reaction: {
text: "❤️",
key: {
remoteJid: "120363@g.us",
participant: "777@s.whatsapp.net",
},
},
},
]);
await new Promise((resolve) => setImmediate(resolve));
expect(onReaction).toHaveBeenCalledWith(
expect.objectContaining({
messageId: "msg-456",
emoji: "❤️",
chatJid: "120363@g.us",
chatType: "group",
senderJid: "777@s.whatsapp.net",
reactedToFromMe: true,
}),
);
await listener.close();
});
it("resolves self-reactions in DMs to own JID", async () => {
const onReaction = vi.fn();
const listener = await monitorWebInbox({
verbose: false,
onMessage: vi.fn(async () => {}),
onReaction,
accountId: ACCOUNT_ID,
authDir,
});
const sock = await createWaSocket();
// Self-reaction: reactionKey.fromMe = true, remoteJid is the partner
sock.ev.emit("messages.reaction", [
{
key: {
id: "msg-789",
remoteJid: "999@s.whatsapp.net",
fromMe: false,
},
reaction: {
text: "👍",
key: {
remoteJid: "999@s.whatsapp.net",
fromMe: true,
},
},
},
]);
await new Promise((resolve) => setImmediate(resolve));
expect(onReaction).toHaveBeenCalledWith(
expect.objectContaining({
messageId: "msg-789",
emoji: "👍",
// senderJid should be our own JID, not the chat partner
senderJid: "123@s.whatsapp.net",
}),
);
await listener.close();
});
it("continues processing remaining reactions when callback throws", async () => {
const onReaction = vi.fn().mockImplementationOnce(() => {
throw new Error("boom");
});
const listener = await monitorWebInbox({
verbose: false,
onMessage: vi.fn(async () => {}),
onReaction,
accountId: ACCOUNT_ID,
authDir,
});
const sock = await createWaSocket();
sock.ev.emit("messages.reaction", [
{
key: { id: "msg-1", remoteJid: "999@s.whatsapp.net", fromMe: false },
reaction: { text: "👍", key: { remoteJid: "888@s.whatsapp.net" } },
},
{
key: { id: "msg-2", remoteJid: "999@s.whatsapp.net", fromMe: false },
reaction: { text: "❤️", key: { remoteJid: "888@s.whatsapp.net" } },
},
]);
await new Promise((resolve) => setImmediate(resolve));
// Both reactions should be attempted despite first throwing
expect(onReaction).toHaveBeenCalledTimes(2);
await listener.close();
});
it("skips reactions with missing messageId", async () => {
const onReaction = vi.fn();
const listener = await monitorWebInbox({
verbose: false,
onMessage: vi.fn(async () => {}),
onReaction,
accountId: ACCOUNT_ID,
authDir,
});
const sock = await createWaSocket();
sock.ev.emit("messages.reaction", [
{
key: {
id: undefined,
remoteJid: "999@s.whatsapp.net",
fromMe: false,
},
reaction: {
text: "👍",
key: { remoteJid: "888@s.whatsapp.net" },
},
},
]);
await new Promise((resolve) => setImmediate(resolve));
expect(onReaction).not.toHaveBeenCalled();
await listener.close();
});
it("skips reactions with missing remoteJid", async () => {
const onReaction = vi.fn();
const listener = await monitorWebInbox({
verbose: false,
onMessage: vi.fn(async () => {}),
onReaction,
accountId: ACCOUNT_ID,
authDir,
});
const sock = await createWaSocket();
sock.ev.emit("messages.reaction", [
{
key: {
id: "msg-123",
remoteJid: undefined,
fromMe: false,
},
reaction: {
text: "👍",
key: { remoteJid: "888@s.whatsapp.net" },
},
},
]);
await new Promise((resolve) => setImmediate(resolve));
expect(onReaction).not.toHaveBeenCalled();
await listener.close();
});
it("handles missing reaction.key gracefully (senderJid undefined)", async () => {
const onReaction = vi.fn();
const listener = await monitorWebInbox({
verbose: false,
onMessage: vi.fn(async () => {}),
onReaction,
accountId: ACCOUNT_ID,
authDir,
});
const sock = await createWaSocket();
sock.ev.emit("messages.reaction", [
{
key: {
id: "msg-123",
remoteJid: "999@s.whatsapp.net",
fromMe: false,
},
reaction: {
text: "🔥",
// no key — sender unknown
},
},
]);
await new Promise((resolve) => setImmediate(resolve));
expect(onReaction).toHaveBeenCalledWith(
expect.objectContaining({
messageId: "msg-123",
emoji: "🔥",
senderJid: undefined,
senderE164: undefined,
}),
);
await listener.close();
});
it("works without onReaction callback (no-op)", async () => {
const onMessage = vi.fn(async () => {});
const listener = await monitorWebInbox({
verbose: false,
onMessage,
accountId: ACCOUNT_ID,
authDir,
});
const sock = await createWaSocket();
// Should not throw when no onReaction is provided
sock.ev.emit("messages.reaction", [
{
key: {
id: "msg-123",
remoteJid: "999@s.whatsapp.net",
fromMe: false,
},
reaction: {
text: "👍",
key: { remoteJid: "888@s.whatsapp.net" },
},
},
]);
await new Promise((resolve) => setImmediate(resolve));
await listener.close();
});
});