Skip to content

WhatsApp message tool fails with 'No active WhatsApp Web listener' while auto-reply works — duplicate listener Maps across bundled chunks #14406

@talison

Description

@talison

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

  1. Auto-reply (deliverWebReply): Uses msg.reply() on the inbound message object, which holds a direct socket reference. Bypasses the listener Map entirely. Always works.

  2. Message tool (sendMessageWhatsApprequireActiveWebListener): 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:

  1. Move it to a shared chunk that all other chunks import (ensure Rollup doesn't duplicate it)
  2. Or attach it to a global/process-level registry (e.g., globalThis.__openclaw_wa_listeners)
  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingstaleMarked as stale due to inactivity

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions