From 9369e36eb2be7e31cfddd410a952c20c24fed06e Mon Sep 17 00:00:00 2001 From: Nick Sullivan Date: Thu, 29 Jan 2026 10:38:02 -0600 Subject: [PATCH 1/6] feat(whatsapp): subscribe to inbound reaction events MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- CHANGELOG.md | 77 ++- docs/channels/whatsapp.md | 91 +++- docs/tools/reactions.md | 2 + ...app-inbound-reaction-system-events.test.ts | 187 +++++++ src/web/auto-reply/monitor.ts | 31 +- src/web/inbound.ts | 6 +- src/web/inbound/monitor.ts | 70 ++- src/web/inbound/types.ts | 21 + .../monitor-inbox.inbound-reactions.test.ts | 460 ++++++++++++++++++ 9 files changed, 918 insertions(+), 27 deletions(-) create mode 100644 src/web/auto-reply.whatsapp-inbound-reaction-system-events.test.ts create mode 100644 src/web/monitor-inbox.inbound-reactions.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c5321870..0c3b519e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,9 +3,12 @@ Docs: https://docs.molt.bot ## 2026.1.27-beta.1 + Status: beta. ### 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. - 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). @@ -70,9 +73,11 @@ Status: beta. - Routing: add per-account DM session scope and document multi-account isolation. (#3095) Thanks @jarvis-sam. ### Breaking + - **BREAKING:** Gateway auth mode "none" is removed; gateway now requires token/password (Tailscale Serve identity still allowed). ### Fixes + - 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. - Discord: restore username directory lookup in target resolution. (#3131) Thanks @bonald. @@ -125,6 +130,7 @@ Status: beta. ## 2026.1.24-3 ### Fixes + - 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. - Security audit: flag loopback Control UI with auth disabled as critical. (#1795) Thanks @orlyjamie. @@ -133,16 +139,19 @@ Status: beta. ## 2026.1.24-2 ### Fixes + - Packaging: include dist/link-understanding output in npm tarball (fixes missing apply.js import on install). ## 2026.1.24-1 ### Fixes + - Packaging: include dist/shared output in npm tarball (fixes missing reasoning-tags import on install). ## 2026.1.24 ### 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 - 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 @@ -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 ### Changes + - 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 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. ### Fixes + - 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: 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 ### Fixes + - Packaging: include dist/tts output in npm tarball (fixes missing dist/tts/tts.js). ## 2026.1.23 ### 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 - 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 @@ -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 ### 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 - 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 @@ -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 ### Fixes + - 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. - 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 ### Changes + - 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. - 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. ### Fixes + - 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. - 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 ### 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 - Slack: remove deprecated `filetype` field from `files.uploadV2` to eliminate API warnings. (#1447) ## 2026.1.21 ### Changes + - 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 - 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. ### 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:** Envelope and system event timestamps now default to host-local time (was UTC) so agents don’t have to constantly convert. ### Fixes + - 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) - 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 ### Changes + - 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 - 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. ### 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. ### Fixes + - 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: 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 ### Changes + - 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. ## 2026.1.16-1 ### Highlights + - 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 - 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 -- 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 ### Breaking + - **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:** 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 ` now copies into `~/.clawdbot/extensions` (use `--link` to keep path-based loading). ### Changes + - 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. - 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. ### Fixes + - macOS: drain subprocess pipes before waiting to avoid deadlocks. (#1081) β€” thanks @thesash. - Verbose: wrap tool summaries/output in markdown only for markdown-capable channels. - 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 ### Highlights + - 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). - 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). ### Breaking + - **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:** Channel auth now prefers config over env for Discord/Telegram/Matrix (env is fallback only). (#1040) β€” thanks @thewilloftheshadow. ### Changes + - UI/Apps: move channel/config settings to schema-driven forms and rename Connections β†’ Channels. (#1040) β€” thanks @thewilloftheshadow. - CLI: set process titles to `moltbot-` for clearer process listings. - 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. ### Fixes + - 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. - 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 ### Highlights + - 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. - 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. ### Changes + - 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. - 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). ### Fixes + - 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. - 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 ### Changes + - Usage: add MiniMax coding plan usage tracking. - Auth: label Claude Code CLI auth options. (#915) β€” thanks @SeanZoR. - 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..configWrites` gating for channel-initiated config writes; migrate Slack channel IDs. ### 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: 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)`. - Gateway/Dev: ensure `pnpm gateway:dev` always uses the dev profile config + state (`~/.clawdbot-dev`). #### Agents / Auth / Tools / Sandbox + - 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: 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. #### macOS / Apps + - 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: 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 ### Fixes + - 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. ## 2026.1.12-2 ### Fixes + - 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: 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 ### Fixes + - Packaging: include `dist/channels/**` in the npm tarball (fixes `ERR_MODULE_NOT_FOUND` for `dist/channels/registry.js`). ## 2026.1.12 ### 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.*`). - 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). @@ -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. ### 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: 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. @@ -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. ### Installer + - Install: run `moltbot doctor --non-interactive` after git installs/updates and nudge daemon restarts when detected. ### Fixes + - 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. - 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. ### Maintenance + - Dependencies: bump Pi packages to 0.45.3 and refresh patched pi-ai. - Testing: update Vitest + browser-playwright to 4.0.17. - 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 ### Highlights + - 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. - 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. ### Changes + - CLI/Onboarding: simplify MiniMax auth choice to a single M2.1 option. - CLI: configure section selection now loops until Continue. - 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. ### Installer + - 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. - 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). ### Fixes + - 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. - 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 ### 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 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. - Gateway: add OpenAI-compatible `/v1/chat/completions` HTTP endpoint (auth, SSE streaming, per-agent routing). (#680). ### Changes + - 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. - 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. ### Fixes + - Auto-reply: suppress draft/typing streaming for `NO_REPLY` (silent system ops) so it doesn’t 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/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). - Tests/Live: reset the gateway session between model runs to avoid cross-provider transcript incompatibilities (notably OpenAI Responses reasoning replay rules). - ## 2026.1.9 ### Highlights + - 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. - 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. ### Breaking + - 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`. ### 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: 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. @@ -1122,6 +1179,7 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic - Apps/Branding: refreshed iOS/Android/macOS icons (#521) β€” thanks @fishfisher. ### Fixes + - 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. - 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. ### 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. - Build: Docker build cache layer (#605) β€” thanks @zknicker. @@ -1148,6 +1207,7 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic ## 2026.1.8 ### Highlights + - Security: DMs locked down by default across providers; pairing-first + allowlist guidance. - Sandbox: per-agent scope defaults + workspace access controls; tool/session isolation tuned. - 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. ### Breaking + - **SECURITY (update ASAP):** inbound DMs are now **locked down by default** on Telegram/WhatsApp/Signal/iMessage/Discord/Slack. - Previously, if you didn’t 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"`). @@ -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. ### 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. - **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. @@ -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. ### Maintenance + - 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. ## 2026.1.5 ### Highlights + - Models: add image-specific model config (`agent.imageModel` + fallbacks) and scan support. - Agent tools: new `image` tool routed to the image model (when configured). - 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`). ### Fixes + - Control UI: render Markdown in tool result cards. - 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 diff --git a/docs/channels/whatsapp.md b/docs/channels/whatsapp.md index b6ae260ce..ae2838ef9 100644 --- a/docs/channels/whatsapp.md +++ b/docs/channels/whatsapp.md @@ -3,45 +3,51 @@ summary: "WhatsApp (web channel) integration: login, inbox, replies, media, and read_when: - 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). ## Quick setup (beginner) -1) Use a **separate phone number** if possible (recommended). -2) Configure WhatsApp in `~/.clawdbot/moltbot.json`. -3) Run `moltbot channels login` to scan the QR code (Linked Devices). -4) Start the gateway. + +1. Use a **separate phone number** if possible (recommended). +2. Configure WhatsApp in `~/.clawdbot/moltbot.json`. +3. Run `moltbot channels login` to scan the QR code (Linked Devices). +4. Start the gateway. Minimal config: + ```json5 { channels: { whatsapp: { dmPolicy: "allowlist", - allowFrom: ["+15551234567"] - } - } + allowFrom: ["+15551234567"], + }, + }, } ``` ## Goals + - Multiple WhatsApp accounts (multi-account) in one Gateway process. - Deterministic routing: replies return to WhatsApp, no model routing. - Model sees enough context to understand quoted replies. ## Config writes + By default, WhatsApp is allowed to write config updates triggered by `/config set|unset` (requires `commands.config: true`). Disable with: + ```json5 { - channels: { whatsapp: { configWrites: false } } + channels: { whatsapp: { configWrites: false } }, } ``` ## Architecture (who owns what) + - **Gateway** owns the Baileys socket and inbox loop. - **CLI / macOS app** talk to the gateway; no direct Baileys use. - **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: ### 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 Wi‑Fi 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. **Sample config (dedicated number, single-user allowlist):** + ```json5 { channels: { whatsapp: { 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 ` ### Personal number (fallback) + Quick fallback: run Moltbot on **your own number**. Message yourself (WhatsApp β€œMessage yourself”) for testing so you don’t 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. **Sample config (personal number, self-chat):** + ```json { "whatsapp": { @@ -91,6 +101,7 @@ if `messages.responsePrefix` is unset. Set it explicitly to customize or disable the prefix (use `""` to remove it). ### Number sourcing tips + - **Local eSIM** from your country's mobile carrier (most reliable) - Austria: [hot.at](https://www.hot.at) - 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`. ## Why Not Twilio? + - Early Moltbot builds supported Twilio’s WhatsApp Business integration. - WhatsApp Business numbers are a poor fit for a personal assistant. - Meta enforces a 24‑hour reply window; if you haven’t responded in the last 24 hours, the business number can’t initiate new messages. @@ -108,6 +120,7 @@ the prefix (use `""` to remove it). - Result: unreliable delivery and frequent blocks, so support was removed. ## Login + credentials + - Login command: `moltbot channels login` (QR via Linked Devices). - Multi-account login: `moltbot channels login --account ` (`` = `accountId`). - 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. ## Inbound flow (DM + group) + - WhatsApp events come from `messages.upsert` (Baileys). - Inbox listeners are detached on shutdown to avoid accumulating event handlers in tests/restarts. - 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. ### Personal-number mode (fallback) + If you run Moltbot on your **personal WhatsApp number**, enable `channels.whatsapp.selfChatMode` (see sample above). Behavior: + - Outbound DMs never trigger pairing replies (prevents spamming contacts). - Inbound unknown senders still follow `channels.whatsapp.dmPolicy`. - 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 + By default, the gateway marks inbound WhatsApp messages as read (blue ticks) once they are accepted. Disable globally: + ```json5 { - channels: { whatsapp: { sendReadReceipts: false } } + channels: { whatsapp: { sendReadReceipts: false } }, } ``` Disable per account: + ```json5 { channels: { whatsapp: { accounts: { - personal: { sendReadReceipts: false } - } - } - } + personal: { sendReadReceipts: false }, + }, + }, + }, } ``` Notes: + - Self-chat mode always skips read receipts. ## 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?** Pairing is a DM gate for unknown senders: + - First DM from a new sender returns a short code (message is not processed). - Approve with: `moltbot pairing approve whatsapp ` (list with `moltbot pairing list whatsapp`). - 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. It’s 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) + - `Body` is the current message body with envelope. - 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 - `` ## Groups + - Groups map to `agent::whatsapp:group:` sessions. - Group policy: `channels.whatsapp.groupPolicy = open|disabled|allowlist` (default `allowlist`). - 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. - Owner = `channels.whatsapp.allowFrom` (or self E.164 if unset). - **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) - Current message under: `[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). ## Reply delivery (threading) + - WhatsApp Web sends standard messages (no quoted reply threading in the current gateway). - 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. **Configuration:** + ```json { "whatsapp": { @@ -232,6 +257,7 @@ WhatsApp can automatically send emoji reactions to incoming messages immediately ``` **Options:** + - `emoji` (string): Emoji to use for acknowledgment (e.g., "πŸ‘€", "βœ…", "πŸ“¨"). Empty or omitted = feature disabled. - `direct` (boolean, default: `true`): Send reactions in direct/DM chats. - `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 **Per-account override:** + ```json { "whatsapp": { @@ -257,6 +284,7 @@ WhatsApp can automatically send emoji reactions to incoming messages immediately ``` **Behavior notes:** + - 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). - 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. ## Agent tool (reactions) + - Tool: `whatsapp` with `react` action (`chatJid`, `messageId`, `emoji`, optional `remove`). - Optional: `participant` (group sender), `fromMe` (reacting to your own message), `accountId` (multi-account). - Reaction removal semantics: see [/tools/reactions](/tools/reactions). - Tool gating: `channels.whatsapp.actions.reactions` (default: enabled). +## Inbound reaction notifications + +When someone reacts to a message in a WhatsApp chat, the gateway emits a system event so the agent sees it in its next prompt. System events appear as lines like: + +``` +WhatsApp reaction added: πŸ‘ by +1234567890 msg BAE5ABC123 +``` + +- 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 + - 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. - 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 send (text + media) + - Uses active web listener; error if gateway not running. - Text chunking: 4k max per message (configurable via `channels.whatsapp.textChunkLimit`, optional `channels.whatsapp.chunkMode`). - Media: @@ -288,17 +333,21 @@ WhatsApp can automatically send emoji reactions to incoming messages immediately - Gateway: `send` params include `gifPlayback: true` ## Voice notes (PTT audio) + WhatsApp sends audio as **voice notes** (PTT bubble). + - 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). ## Media limits + optimization + - Default outbound cap: 5 MB (per media item). - Override: `agents.defaults.mediaMaxMb`. - Images are auto-optimized to JPEG under cap (resize + quality sweep). - Oversize media => error; media reply falls back to text warning. ## Heartbeats + - **Gateway heartbeat** logs connection health (`web.heartbeatSeconds`, default 60s). - **Agent heartbeat** can be configured per agent (`agents.list[].heartbeat`) or globally 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). ## Reconnect behavior + - Backoff policy: `web.reconnect`: - `initialMs`, `maxMs`, `factor`, `jitter`, `maxAttempts`. - If maxAttempts reached, web monitoring stops (degraded). - Logged-out => stop and require re-link. ## Config quick map + - `channels.whatsapp.dmPolicy` (DM policy: pairing/allowlist/open/disabled). - `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). @@ -343,6 +394,7 @@ WhatsApp sends audio as **voice notes** (PTT bubble). - `web.reconnect.*` ## Logs + troubleshooting + - Subsystems: `whatsapp/inbound`, `whatsapp/outbound`, `web-heartbeat`, `web-reconnect`. - Log file: `/tmp/moltbot/moltbot-YYYY-MM-DD.log` (configurable). - Troubleshooting guide: [Gateway troubleshooting](/gateway/troubleshooting). @@ -350,13 +402,16 @@ WhatsApp sends audio as **voice notes** (PTT bubble). ## Troubleshooting (quick) **Not linked / QR login required** + - 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). **Linked but disconnected / reconnect loop** + - 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`. **Bun runtime** + - Bun is **not recommended**. WhatsApp (Baileys) and Telegram are unreliable on Bun. Run the gateway with **Node**. (See Getting Started runtime note.) diff --git a/docs/tools/reactions.md b/docs/tools/reactions.md index 364f38695..e5696b18b 100644 --- a/docs/tools/reactions.md +++ b/docs/tools/reactions.md @@ -3,6 +3,7 @@ summary: "Reaction semantics shared across channels" read_when: - Working on reactions in any channel --- + # Reaction tooling Shared reaction semantics across channels: @@ -18,3 +19,4 @@ Channel notes: - **Telegram**: empty `emoji` removes the bot's reactions; `remove: true` also removes reactions but still requires a non-empty `emoji` for tool validation. - **WhatsApp**: empty `emoji` removes the bot reaction; `remove: true` maps to empty emoji (still requires `emoji`). - **Signal**: inbound reaction notifications emit system events when `channels.signal.reactionNotifications` is enabled. +- **WhatsApp**: inbound reaction notifications are always surfaced as system events (no configuration required). diff --git a/src/web/auto-reply.whatsapp-inbound-reaction-system-events.test.ts b/src/web/auto-reply.whatsapp-inbound-reaction-system-events.test.ts new file mode 100644 index 000000000..d648855c9 --- /dev/null +++ b/src/web/auto-reply.whatsapp-inbound-reaction-system-events.test.ts @@ -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; + 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[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; + 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[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; + 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[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"); + }); +}); diff --git a/src/web/auto-reply/monitor.ts b/src/web/auto-reply/monitor.ts index 791b38967..53ac624e0 100644 --- a/src/web/auto-reply/monitor.ts +++ b/src/web/auto-reply/monitor.ts @@ -28,6 +28,7 @@ import { whatsappHeartbeatLog, whatsappLog } from "./loggers.js"; import { buildMentionConfig } from "./mentions.js"; import { createEchoTracker } from "./monitor/echo.js"; import { createWebOnMessageHandler } from "./monitor/on-message.js"; +import type { WebInboundReaction } from "../inbound.js"; import type { WebChannelStatus, WebInboundMsg, WebMonitorTuning } from "./types.js"; import { isLikelyWhatsAppCryptoError } from "./util.js"; @@ -173,7 +174,10 @@ export async function monitorWebChannel( account, }); - const inboundDebounceMs = resolveInboundDebounceMs({ cfg, channel: "whatsapp" }); + const inboundDebounceMs = resolveInboundDebounceMs({ + cfg, + channel: "whatsapp", + }); const shouldDebounce = (msg: WebInboundMsg) => { if (msg.mediaPath || msg.mediaType) return false; if (msg.location) return false; @@ -198,6 +202,31 @@ export async function monitorWebChannel( _lastInboundMsg = 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; diff --git a/src/web/inbound.ts b/src/web/inbound.ts index 39efe97f4..fc9a8e672 100644 --- a/src/web/inbound.ts +++ b/src/web/inbound.ts @@ -1,4 +1,8 @@ export { resetWebInboundDedupe } from "./inbound/dedupe.js"; export { extractLocationData, extractMediaPlaceholder, extractText } from "./inbound/extract.js"; export { monitorWebInbox } from "./inbound/monitor.js"; -export type { WebInboundMessage, WebListenerCloseReason } from "./inbound/types.js"; +export type { + WebInboundMessage, + WebInboundReaction, + WebListenerCloseReason, +} from "./inbound/types.js"; diff --git a/src/web/inbound/monitor.ts b/src/web/inbound/monitor.ts index 3633cbce9..7a6562799 100644 --- a/src/web/inbound/monitor.ts +++ b/src/web/inbound/monitor.ts @@ -20,13 +20,15 @@ import { } from "./extract.js"; import { downloadInboundMedia } from "./media.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: { verbose: boolean; accountId: string; authDir: string; onMessage: (msg: WebInboundMessage) => Promise; + /** Called when a reaction is received on a message. */ + onReaction?: (reaction: WebInboundReaction) => void; mediaMaxMb?: number; /** Send read receipts for incoming messages (default true). */ sendReadReceipts?: boolean; @@ -313,6 +315,67 @@ export async function monitorWebInbox(options: { }; 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 = ( update: Partial, ) => { @@ -350,14 +413,19 @@ export async function monitorWebInbox(options: { const messagesUpsertHandler = handleMessagesUpsert as unknown as ( ...args: unknown[] ) => void; + const messagesReactionHandler = handleMessagesReaction as unknown as ( + ...args: unknown[] + ) => void; const connectionUpdateHandler = handleConnectionUpdate as unknown as ( ...args: unknown[] ) => void; if (typeof ev.off === "function") { ev.off("messages.upsert", messagesUpsertHandler); + ev.off("messages.reaction", messagesReactionHandler); ev.off("connection.update", connectionUpdateHandler); } else if (typeof ev.removeListener === "function") { ev.removeListener("messages.upsert", messagesUpsertHandler); + ev.removeListener("messages.reaction", messagesReactionHandler); ev.removeListener("connection.update", connectionUpdateHandler); } sock.ws?.close(); diff --git a/src/web/inbound/types.ts b/src/web/inbound/types.ts index 5f861fcc8..54e7a3c12 100644 --- a/src/web/inbound/types.ts +++ b/src/web/inbound/types.ts @@ -40,3 +40,24 @@ export type WebInboundMessage = { mediaUrl?: string; 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; +}; diff --git a/src/web/monitor-inbox.inbound-reactions.test.ts b/src/web/monitor-inbox.inbound-reactions.test.ts new file mode 100644 index 000000000..cb6a6aedf --- /dev/null +++ b/src/web/monitor-inbox.inbound-reactions.test.ts @@ -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(); + 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(); + }); +}); From 6b20d5640b6f045d5875fa9f7ce689592bda5abc Mon Sep 17 00:00:00 2001 From: Nick Sullivan Date: Thu, 29 Jan 2026 10:40:44 -0600 Subject: [PATCH 2/6] revert changelog entry to avoid merge conflicts The inbound reaction feature is internal plumbing, not user-facing enough to warrant a changelog entry that will conflict with active PRs. Co-Authored-By: Claude Opus 4.5 --- CHANGELOG.md | 77 ++++------------------------------------------------ 1 file changed, 6 insertions(+), 71 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c3b519e2..3c5321870 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,12 +3,9 @@ Docs: https://docs.molt.bot ## 2026.1.27-beta.1 - Status: beta. ### 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. - 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). @@ -73,11 +70,9 @@ Status: beta. - Routing: add per-account DM session scope and document multi-account isolation. (#3095) Thanks @jarvis-sam. ### Breaking - - **BREAKING:** Gateway auth mode "none" is removed; gateway now requires token/password (Tailscale Serve identity still allowed). ### Fixes - - 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. - Discord: restore username directory lookup in target resolution. (#3131) Thanks @bonald. @@ -130,7 +125,6 @@ Status: beta. ## 2026.1.24-3 ### Fixes - - 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. - Security audit: flag loopback Control UI with auth disabled as critical. (#1795) Thanks @orlyjamie. @@ -139,19 +133,16 @@ Status: beta. ## 2026.1.24-2 ### Fixes - - Packaging: include dist/link-understanding output in npm tarball (fixes missing apply.js import on install). ## 2026.1.24-1 ### Fixes - - Packaging: include dist/shared output in npm tarball (fixes missing reasoning-tags import on install). ## 2026.1.24 ### 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 - 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 @@ -159,7 +150,6 @@ Status: beta. - Telegram: DM topics as separate sessions + outbound link preview toggle. (#1597, #1700) Thanks @rohannagpal, @zerone0x. https://docs.molt.bot/channels/telegram ### Changes - - 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 auto mode enum (off/always/inbound/tagged) with per-session `/tts` override. (#1667) Thanks @sebslight. https://docs.molt.bot/tts @@ -178,7 +168,6 @@ Status: beta. - Dev: add prek pre-commit hooks + dependabot config for weekly updates. (#1720) Thanks @dguido. ### Fixes - - 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: clear stale disconnect banners on reconnect; allow form saves with unsupported schema paths but block missing schema. (#1707) Thanks @Glucksberg. @@ -222,13 +211,11 @@ Status: beta. ## 2026.1.23-1 ### Fixes - - Packaging: include dist/tts output in npm tarball (fixes missing dist/tts/tts.js). ## 2026.1.23 ### 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 - 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 @@ -236,7 +223,6 @@ Status: beta. - Channels: add Tlon/Urbit channel plugin (DMs, group mentions, thread replies). (#1544) Thanks @wca4a. https://docs.molt.bot/channels/tlon ### 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 - 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 @@ -251,7 +237,6 @@ Status: beta. - Docs: clarify HEARTBEAT.md empty file skips heartbeats, missing file still runs. (#1535) Thanks @JustYannicc. https://docs.molt.bot/gateway/heartbeat ### Fixes - - 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. - Messaging/Sessions: mirror outbound sends into target session keys (threads + dmScope), create session entries on send, and normalize session key casing. (#1520, commit 4b6cdd1d3) @@ -290,7 +275,6 @@ Status: beta. ## 2026.1.22 ### Changes - - 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. - Slack: add chat-type reply threading overrides via `replyToModeByChatType`. (#1442) Thanks @stefangalescu. @@ -298,7 +282,6 @@ Status: beta. - Onboarding: add hatch choice (TUI/Web/Later), token explainer, background dashboard seed on macOS, and showcase link. ### Fixes - - 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. - Auto-reply: only report a model switch when session state is available. (#1465) Thanks @robbyczgw-cla. @@ -329,14 +312,12 @@ Status: beta. ## 2026.1.21-2 ### 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 - Slack: remove deprecated `filetype` field from `files.uploadV2` to eliminate API warnings. (#1447) ## 2026.1.21 ### Changes - - 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 - Heartbeat: allow running heartbeats in an explicit session key. (#1256) Thanks @zknicker. @@ -359,12 +340,10 @@ Status: beta. - Docs: add per-message Gmail search example for gog. (#1220) Thanks @mbelinky. ### 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:** Envelope and system event timestamps now default to host-local time (was UTC) so agents don’t have to constantly convert. ### Fixes - - 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) - Memory: prevent CLI hangs by deferring vector probes, adding sqlite-vec/embedding timeouts, and showing sync progress early. @@ -387,7 +366,6 @@ Status: beta. ## 2026.1.20 ### Changes - - 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 - TUI: add syntax highlighting for code blocks. (#1200) https://docs.molt.bot/tui @@ -466,11 +444,9 @@ Status: beta. - Swabble: use the tagged Commander Swift package release. ### 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. ### Fixes - - 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: emit message-flow diagnostics across channels via shared dispatch. (#1244) @@ -570,23 +546,20 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic ## 2026.1.16-2 ### Changes - - 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. ## 2026.1.16-1 ### Highlights - - 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 - 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 -- 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 ### Breaking - - **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:** Drop legacy `chatType: "room"` support; use `chatType: "channel"`. @@ -595,7 +568,6 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic - **BREAKING:** `moltbot plugins install ` now copies into `~/.clawdbot/extensions` (use `--link` to keep path-based loading). ### Changes - - 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. - Tools: improve `web_fetch` extraction using Readability (with fallback). @@ -631,7 +603,6 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic - Plugins: add zip installs and `--link` to avoid copying local paths. ### Fixes - - macOS: drain subprocess pipes before waiting to avoid deadlocks. (#1081) β€” thanks @thesash. - Verbose: wrap tool summaries/output in markdown only for markdown-capable channels. - Tools: include provider/session context in elevated exec denial errors. @@ -688,20 +659,17 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic ## 2026.1.15 ### Highlights - - 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). - 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). ### Breaking - - **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:** Channel auth now prefers config over env for Discord/Telegram/Matrix (env is fallback only). (#1040) β€” thanks @thewilloftheshadow. ### Changes - - UI/Apps: move channel/config settings to schema-driven forms and rename Connections β†’ Channels. (#1040) β€” thanks @thewilloftheshadow. - CLI: set process titles to `moltbot-` for clearer process listings. - CLI/macOS: sync remote SSH target/identity to config and let `gateway status` auto-infer SSH targets (ssh-config aware). @@ -741,7 +709,6 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic - Discord: allow emoji/sticker uploads + channel actions in config defaults. (#870) β€” thanks @JDIVE. ### Fixes - - 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. - WhatsApp: report linked status consistently in channel status. (#1050) β€” thanks @YuriNachos. @@ -778,14 +745,12 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic ## 2026.1.14-1 ### Highlights - - 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. - 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. ### Changes - - 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. - Agents: add optional auth-profile copy prompt on `agents add` and improve auth error messaging. @@ -802,7 +767,6 @@ 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). ### Fixes - - 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. - Google: downgrade unsigned thinking blocks before send to avoid missing signature errors. @@ -824,7 +788,6 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic ## 2026.1.14 ### Changes - - Usage: add MiniMax coding plan usage tracking. - Auth: label Claude Code CLI auth options. (#915) β€” thanks @SeanZoR. - Docs: standardize Claude Code CLI naming across docs and prompts. (follow-up to #915) @@ -832,16 +795,14 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic - Config: add `channels..configWrites` gating for channel-initiated config writes; migrate Slack channel IDs. ### 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: 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)`. - Gateway/Dev: ensure `pnpm gateway:dev` always uses the dev profile config + state (`~/.clawdbot-dev`). #### Agents / Auth / Tools / Sandbox - - 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: cap tool call IDs for OpenAI/OpenRouter to avoid request rejections. (#875) β€” thanks @j1philli. @@ -854,7 +815,6 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic - Google: downgrade unsigned thinking blocks before send to avoid missing signature errors. #### macOS / Apps - - 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: pass auth token/password to dashboard URL for authenticated access. (#918) β€” thanks @rahthakor. @@ -874,14 +834,12 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic ## 2026.1.13 ### Fixes - - 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. ## 2026.1.12-2 ### Fixes - - 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: strip invalid Gemini thought signatures from OpenRouter history to avoid 400s. (#841, #845) β€” thanks @MatthieuBizien. @@ -889,13 +847,11 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic ## 2026.1.12-1 ### Fixes - - Packaging: include `dist/channels/**` in the npm tarball (fixes `ERR_MODULE_NOT_FOUND` for `dist/channels/registry.js`). ## 2026.1.12 ### 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.*`). - 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). @@ -904,7 +860,6 @@ 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. ### 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: 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. @@ -918,11 +873,9 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic - Heartbeat: default `ackMaxChars` to 300 so short `HEARTBEAT_OK` replies stay internal. ### Installer - - Install: run `moltbot doctor --non-interactive` after git installs/updates and nudge daemon restarts when detected. ### Fixes - - 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. - 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. @@ -947,7 +900,6 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic - Connections UI: polish multi-account account cards. (#816) β€” thanks @steipete. ### Maintenance - - Dependencies: bump Pi packages to 0.45.3 and refresh patched pi-ai. - Testing: update Vitest + browser-playwright to 4.0.17. - Docs: add Amazon Bedrock provider notes and link from models/FAQ. @@ -955,14 +907,12 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic ## 2026.1.11 ### Highlights - - 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. - 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. ### Changes - - CLI/Onboarding: simplify MiniMax auth choice to a single M2.1 option. - CLI: configure section selection now loops until Continue. - Docs: explain MiniMax vs MiniMax Lightning (speed vs cost) and restore LM Studio example. @@ -998,7 +948,6 @@ 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. ### Installer - - 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. - Installer tests: add root+non-root docker smokes, CI workflow to fetch molt.bot scripts and run install sh/cli with onboarding skipped. @@ -1007,7 +956,6 @@ 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). ### Fixes - - 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. - CLI: fix guardCancel typing for configure prompts. (#769) β€” thanks @steipete. @@ -1047,14 +995,12 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic ## 2026.1.10 ### 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 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. - Gateway: add OpenAI-compatible `/v1/chat/completions` HTTP endpoint (auth, SSE streaming, per-agent routing). (#680). ### Changes - - 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. - Agents: add human-delay pacing between block replies (modes: off/natural/custom, per-agent configurable). (#446) β€” thanks @tony-freedomology. @@ -1068,7 +1014,6 @@ 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. ### Fixes - - Auto-reply: suppress draft/typing streaming for `NO_REPLY` (silent system ops) so it doesn’t 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/Gateway: clarify that `moltbot gateway status` reports RPC health (connect + RPC) and shows RPC failures separately from connect failures. @@ -1140,10 +1085,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). - Tests/Live: reset the gateway session between model runs to avoid cross-provider transcript incompatibilities (notably OpenAI Responses reasoning replay rules). + ## 2026.1.9 ### Highlights - - 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. - CLI/Gateway UX: message subcommands, gateway discover/status/SSH, /config + /debug, sandbox CLI. @@ -1152,12 +1097,10 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic - Control UI/TUI: queued messages, session links, reasoning view, mobile polish, logs UX. ### Breaking - - 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`. ### 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: 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. @@ -1179,7 +1122,6 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic - Apps/Branding: refreshed iOS/Android/macOS icons (#521) β€” thanks @fishfisher. ### Fixes - - 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. - 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. @@ -1197,8 +1139,7 @@ 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. ### 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. - Build: Docker build cache layer (#605) β€” thanks @zknicker. @@ -1207,7 +1148,6 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic ## 2026.1.8 ### Highlights - - Security: DMs locked down by default across providers; pairing-first + allowlist guidance. - Sandbox: per-agent scope defaults + workspace access controls; tool/session isolation tuned. - Agent loop: compaction, pruning, streaming, and error handling hardened. @@ -1216,7 +1156,6 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic - CLI/Gateway/Doctor: daemon/logs/status, auth migration, and diagnostics significantly expanded. ### Breaking - - **SECURITY (update ASAP):** inbound DMs are now **locked down by default** on Telegram/WhatsApp/Signal/iMessage/Discord/Slack. - Previously, if you didn’t 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"`). @@ -1231,7 +1170,6 @@ 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. ### 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. - **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. @@ -1243,15 +1181,13 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic - **Docs:** new FAQ/ClawdHub/config examples/showcase entries and clarified auth, sandbox, and systemd docs. ### Maintenance - - 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. ## 2026.1.5 ### Highlights - - Models: add image-specific model config (`agent.imageModel` + fallbacks) and scan support. - Agent tools: new `image` tool routed to the image model (when configured). - Config: default model shorthands (`opus`, `sonnet`, `gpt`, `gpt-mini`, `gemini`, `gemini-flash`). @@ -1259,7 +1195,6 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic - Bun: optional local install/build workflow without maintaining a Bun lockfile (see `docs/bun.md`). ### Fixes - - Control UI: render Markdown in tool result cards. - 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 From a0d3aaf54183394d59f26c63659cfac0151c6936 Mon Sep 17 00:00:00 2001 From: Nick Sullivan Date: Thu, 29 Jan 2026 12:21:09 -0600 Subject: [PATCH 3/6] feat(whatsapp): handle reaction removals and improve sender detection - Emit reaction events with isRemoval flag instead of skipping them - Fall back to chatJid for DM sender when reaction.key is missing - Add chatType and accountId to reaction logs for observability - Update tests to reflect new removal handling behavior Co-Authored-By: Claude Opus 4.5 --- src/web/auto-reply/monitor.ts | 8 +++-- src/web/inbound/monitor.ts | 35 ++++++++++++++----- src/web/inbound/types.ts | 4 ++- .../monitor-inbox.inbound-reactions.test.ts | 16 ++++++--- 4 files changed, 47 insertions(+), 16 deletions(-) diff --git a/src/web/auto-reply/monitor.ts b/src/web/auto-reply/monitor.ts index 53ac624e0..427780846 100644 --- a/src/web/auto-reply/monitor.ts +++ b/src/web/auto-reply/monitor.ts @@ -215,14 +215,16 @@ export async function monitorWebChannel( }, }); const senderLabel = reaction.senderE164 ?? reaction.senderJid ?? "someone"; - const text = `WhatsApp reaction added: ${reaction.emoji} by ${senderLabel} msg ${reaction.messageId}`; + const action = reaction.isRemoval ? "removed" : "added"; + const emojiPart = reaction.isRemoval ? "" : `: ${reaction.emoji}`; + const text = `WhatsApp reaction ${action}${emojiPart} by ${senderLabel} msg ${reaction.messageId}`; const contextKey = [ "whatsapp", "reaction", - "added", + action, reaction.messageId, reaction.senderJid ?? "unknown", - reaction.emoji, + reaction.emoji || "removed", reaction.chatJid, ].join(":"); enqueueSystemEvent(text, { sessionKey: route.sessionKey, contextKey }); diff --git a/src/web/inbound/monitor.ts b/src/web/inbound/monitor.ts index 7a6562799..aabc6d5fc 100644 --- a/src/web/inbound/monitor.ts +++ b/src/web/inbound/monitor.ts @@ -336,23 +336,42 @@ export async function monitorWebInbox(options: { 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 isRemoval = !emoji; 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); + // Determine who sent the reaction: + // - fromMe=true β†’ we reacted (use selfJid) + // - Otherwise: use reaction.key metadata, fallback to chatJid for DMs + let senderJid: string | undefined; + if (reactionKey?.fromMe) { + senderJid = selfJid ?? undefined; + } else { + // Prefer explicit sender info from reaction.key, fall back to chatJid for DMs + senderJid = + reactionKey?.participant ?? reactionKey?.remoteJid ?? (group ? undefined : chatJid); + } const senderE164 = senderJid ? await resolveInboundJid(senderJid) : null; - inboundLogger.info({ emoji, messageId, chatJid, senderJid }, "inbound reaction"); + const chatType = group ? "group" : "direct"; + inboundLogger.info( + { + emoji: emoji || "(removed)", + messageId, + chatJid, + chatType, + senderJid, + isRemoval, + accountId: options.accountId, + }, + isRemoval ? "inbound reaction removed" : "inbound reaction added", + ); options.onReaction({ messageId, emoji, + isRemoval, chatJid, - chatType: group ? "group" : "direct", + chatType, accountId: options.accountId, senderJid: senderJid ?? undefined, senderE164: senderE164 ?? undefined, diff --git a/src/web/inbound/types.ts b/src/web/inbound/types.ts index 54e7a3c12..63c03b2b0 100644 --- a/src/web/inbound/types.ts +++ b/src/web/inbound/types.ts @@ -44,8 +44,10 @@ export type WebInboundMessage = { export type WebInboundReaction = { /** Message ID being reacted to. */ messageId: string; - /** Emoji text (empty string = reaction removed). */ + /** Emoji text (empty string when isRemoval=true). */ emoji: string; + /** True if this is a reaction removal (emoji will be empty). */ + isRemoval?: boolean; /** JID of the chat where the reaction occurred. */ chatJid: string; chatType: "direct" | "group"; diff --git a/src/web/monitor-inbox.inbound-reactions.test.ts b/src/web/monitor-inbox.inbound-reactions.test.ts index cb6a6aedf..60530b199 100644 --- a/src/web/monitor-inbox.inbound-reactions.test.ts +++ b/src/web/monitor-inbox.inbound-reactions.test.ts @@ -142,7 +142,7 @@ describe("web monitor inbox – inbound reactions", () => { await listener.close(); }); - it("skips reaction removals (empty emoji)", async () => { + it("handles reaction removals (empty emoji)", async () => { const onReaction = vi.fn(); const listener = await monitorWebInbox({ @@ -169,7 +169,14 @@ describe("web monitor inbox – inbound reactions", () => { ]); await new Promise((resolve) => setImmediate(resolve)); - expect(onReaction).not.toHaveBeenCalled(); + expect(onReaction).toHaveBeenCalledWith( + expect.objectContaining({ + messageId: "msg-123", + emoji: "", + isRemoval: true, + chatJid: "999@s.whatsapp.net", + }), + ); await listener.close(); }); @@ -416,12 +423,13 @@ describe("web monitor inbox – inbound reactions", () => { ]); await new Promise((resolve) => setImmediate(resolve)); + // In DMs without reaction.key, we use chatJid as the sender expect(onReaction).toHaveBeenCalledWith( expect.objectContaining({ messageId: "msg-123", emoji: "πŸ”₯", - senderJid: undefined, - senderE164: undefined, + senderJid: "999@s.whatsapp.net", + chatType: "direct", }), ); From 734a7a476d6d0a2d380f4e690563f1d3418a27b5 Mon Sep 17 00:00:00 2001 From: Nick Sullivan Date: Thu, 29 Jan 2026 12:42:08 -0600 Subject: [PATCH 4/6] fix(whatsapp): gate reactions by DM/group access controls Address Codex review - reactions now respect the same access controls as messages (dmPolicy, allowlists, etc). Self-reactions bypass the check since they're our own actions, not inbound events. Co-Authored-By: Claude Opus 4.5 --- src/web/inbound/monitor.ts | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/web/inbound/monitor.ts b/src/web/inbound/monitor.ts index aabc6d5fc..e68763468 100644 --- a/src/web/inbound/monitor.ts +++ b/src/web/inbound/monitor.ts @@ -352,6 +352,31 @@ export async function monitorWebInbox(options: { } const senderE164 = senderJid ? await resolveInboundJid(senderJid) : null; + // Gate reactions by the same access controls as messages (skip for our own reactions) + const isOwnReaction = Boolean(reactionKey?.fromMe); + if (!isOwnReaction) { + const from = group ? chatJid : await resolveInboundJid(chatJid); + if (!from) continue; + const access = await checkInboundAccessControl({ + accountId: options.accountId, + from, + selfE164, + senderE164, + group, + isFromMe: false, + connectedAtMs, + sock: { sendMessage: (jid, content) => sock.sendMessage(jid, content) }, + remoteJid: chatJid, + }); + if (!access.allowed) { + inboundLogger.debug( + { chatJid, senderJid, group }, + "reaction blocked by access control", + ); + continue; + } + } + const chatType = group ? "group" : "direct"; inboundLogger.info( { From 273f3ca260d749cd6b3db94bb1a4ad9b69c9c934 Mon Sep 17 00:00:00 2001 From: Nick Sullivan Date: Thu, 29 Jan 2026 12:43:09 -0600 Subject: [PATCH 5/6] chore: remove unused senderName field from reaction type Baileys reaction events don't include push names, so this field was dead interface pollution. (Cursor review) Co-Authored-By: Claude Opus 4.5 --- src/web/inbound/types.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/web/inbound/types.ts b/src/web/inbound/types.ts index 63c03b2b0..eea9905bd 100644 --- a/src/web/inbound/types.ts +++ b/src/web/inbound/types.ts @@ -57,8 +57,6 @@ export type WebInboundReaction = { 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; From 79f799256d0daa2faa41fb5e57e353cd23bb6a77 Mon Sep 17 00:00:00 2001 From: Nick Sullivan Date: Thu, 29 Jan 2026 12:53:31 -0600 Subject: [PATCH 6/6] fix(whatsapp): update reaction docs + suppress pairing for reactions - Update docs: reaction removals now emit events with isRemoval=true - Pass no-op sendMessage to access control to prevent pairing messages being sent when unknown users react (pairing is for messages, not reactions) (Cursor review) Co-Authored-By: Claude Opus 4.5 --- docs/channels/whatsapp.md | 3 ++- src/web/inbound/monitor.ts | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/channels/whatsapp.md b/docs/channels/whatsapp.md index ae2838ef9..34ab16613 100644 --- a/docs/channels/whatsapp.md +++ b/docs/channels/whatsapp.md @@ -304,9 +304,10 @@ When someone reacts to a message in a WhatsApp chat, the gateway emits a system ``` WhatsApp reaction added: πŸ‘ by +1234567890 msg BAE5ABC123 +WhatsApp reaction removed by +1234567890 msg BAE5ABC123 ``` -- Reaction removals (empty emoji) are silently skipped. +- Reaction removals emit events with `isRemoval: true` and no emoji in the message. - Self-reactions in DMs are correctly attributed to the bot's own JID. - Group reactions include the participant who reacted. - Events are deduplicated by message ID, sender, and emoji to avoid repeat notifications. diff --git a/src/web/inbound/monitor.ts b/src/web/inbound/monitor.ts index e68763468..1423d2135 100644 --- a/src/web/inbound/monitor.ts +++ b/src/web/inbound/monitor.ts @@ -357,6 +357,7 @@ export async function monitorWebInbox(options: { if (!isOwnReaction) { const from = group ? chatJid : await resolveInboundJid(chatJid); if (!from) continue; + // No-op sendMessage: reactions shouldn't trigger pairing replies const access = await checkInboundAccessControl({ accountId: options.accountId, from, @@ -365,7 +366,7 @@ export async function monitorWebInbox(options: { group, isFromMe: false, connectedAtMs, - sock: { sendMessage: (jid, content) => sock.sendMessage(jid, content) }, + sock: { sendMessage: async () => ({}) }, remoteJid: chatJid, }); if (!access.allowed) {