Summary
The message tool (used by agents for proactive WhatsApp sends) intermittently fails with:
Error: No active WhatsApp Web listener (account: default).
…while the auto-reply path (responding to inbound messages) works perfectly at the same time. This means the WhatsApp connection is alive, but the message tool cannot find it.
Root Cause Analysis
The WhatsApp active-listener registry (src/web/active-listener.ts) maintains an in-memory Map<string, Listener> at module scope. Due to Rollup code-splitting, this module is duplicated across multiple output chunks:
reply-B_4pVbIX.js — contains sendMessageWhatsApp() (used by the message tool via runMessageAction)
extensionAPI.js — contains monitorWebChannel(), deliverWebReply(), heartbeat, and inbound message handling
loader-n6BPnYom.js — another copy of the WhatsApp stack
Each chunk has its own const listeners = new Map(). When setActiveWebListener() is called during connection setup, it populates the Map in whichever chunk's copy of the function is executing. But requireActiveWebListener() (called by the message tool's send path) may resolve to a different chunk's Map that was never populated — or that went stale after a reconnect.
Two different send paths
-
Auto-reply (deliverWebReply): Uses msg.reply() on the inbound message object, which holds a direct socket reference. Bypasses the listener Map entirely. Always works.
-
Message tool (sendMessageWhatsApp → requireActiveWebListener): Looks up the listener from the module-scoped Map. Fails when the Map in its chunk is empty or stale.
Why it's intermittent
After a fresh gateway start, all chunk-local Maps may get populated (both monitorWebChannel copies run). But after a WhatsApp reconnect (connection drop + auto-reconnect), only one chunk's Map gets the new listener reference. The auto-reply path keeps working via msg.reply(), but the message tool's chunk has a stale/empty Map.
Evidence
From gateway logs (openclaw-2026-02-11.log):
5:39 PM PT — User says "I'm talking to you on WA right now, why are you detecting disconnects"
- Inbound message received and auto-reply sent successfully (via
extensionAPI.js:46079 deliverWebReply)
- Simultaneously, message tool fails:
[tools] message failed: Error: No active WhatsApp Web listener
Heartbeat running from extensionAPI.js:46780 shows connection alive (reconnectAttempts: 1, messagesHandled: 4)
Successful proactive sends (earlier in the day) go through reply-B_4pVbIX.js:9436 sendMessageWhatsApp — confirming that when both Maps are populated, sends work.
Suggested Fix
The listeners Map in src/web/active-listener.ts should be a singleton that survives code-splitting — for example:
- Move it to a shared chunk that all other chunks import (ensure Rollup doesn't duplicate it)
- Or attach it to a global/process-level registry (e.g.,
globalThis.__openclaw_wa_listeners)
- Or use a separate tiny module with
sideEffects: true to prevent tree-shaking duplication
Environment
- OpenClaw version: 2026.2.6-3
- Node: v22.22.0
- OS: macOS (arm64)
- Channel: WhatsApp Web (multi-device)
Workaround
Hard-restart the gateway process (not SIGUSR1 soft restart) to repopulate all Maps. This is fragile and doesn't survive the next WhatsApp reconnect cycle.
Summary
The
messagetool (used by agents for proactive WhatsApp sends) intermittently fails with:…while the auto-reply path (responding to inbound messages) works perfectly at the same time. This means the WhatsApp connection is alive, but the message tool cannot find it.
Root Cause Analysis
The WhatsApp active-listener registry (
src/web/active-listener.ts) maintains an in-memoryMap<string, Listener>at module scope. Due to Rollup code-splitting, this module is duplicated across multiple output chunks:reply-B_4pVbIX.js— containssendMessageWhatsApp()(used by themessagetool viarunMessageAction)extensionAPI.js— containsmonitorWebChannel(),deliverWebReply(), heartbeat, and inbound message handlingloader-n6BPnYom.js— another copy of the WhatsApp stackEach chunk has its own
const listeners = new Map(). WhensetActiveWebListener()is called during connection setup, it populates the Map in whichever chunk's copy of the function is executing. ButrequireActiveWebListener()(called by the message tool's send path) may resolve to a different chunk's Map that was never populated — or that went stale after a reconnect.Two different send paths
Auto-reply (
deliverWebReply): Usesmsg.reply()on the inbound message object, which holds a direct socket reference. Bypasses the listener Map entirely. Always works.Message tool (
sendMessageWhatsApp→requireActiveWebListener): Looks up the listener from the module-scoped Map. Fails when the Map in its chunk is empty or stale.Why it's intermittent
After a fresh gateway start, all chunk-local Maps may get populated (both
monitorWebChannelcopies run). But after a WhatsApp reconnect (connection drop + auto-reconnect), only one chunk's Map gets the new listener reference. The auto-reply path keeps working viamsg.reply(), but the message tool's chunk has a stale/empty Map.Evidence
From gateway logs (
openclaw-2026-02-11.log):5:39 PM PT — User says "I'm talking to you on WA right now, why are you detecting disconnects"
extensionAPI.js:46079 deliverWebReply)[tools] message failed: Error: No active WhatsApp Web listenerHeartbeat running from
extensionAPI.js:46780shows connection alive (reconnectAttempts: 1,messagesHandled: 4)Successful proactive sends (earlier in the day) go through
reply-B_4pVbIX.js:9436 sendMessageWhatsApp— confirming that when both Maps are populated, sends work.Suggested Fix
The
listenersMap insrc/web/active-listener.tsshould be a singleton that survives code-splitting — for example:globalThis.__openclaw_wa_listeners)sideEffects: trueto prevent tree-shaking duplicationEnvironment
Workaround
Hard-restart the gateway process (not SIGUSR1 soft restart) to repopulate all Maps. This is fragile and doesn't survive the next WhatsApp reconnect cycle.