Skip to content

WhatsApp active-listener singleton not shared across bundled chunks (outbound sends fail) #45994

@javicasper

Description

@javicasper

Bug Description

Proactive WhatsApp message sending (via CLI message send, cron announce delivery, or agent message tool) fails with:

Error: No active WhatsApp Web listener (account: default). Start the gateway, then link WhatsApp with: openclaw channels login --channel whatsapp --account default.

Despite WhatsApp being fully connectedchannels status --probe shows linked, running, connected, and inbound auto-replies work correctly.

Root Cause

src/web/active-listener.ts defines a module-scope listeners = new Map() singleton. The bundler (tsdown) emits this module into two separate chunks:

  • model-selection-*.js (used by the gateway WebSocket handler / agent outbound path)
  • reply-*.js (used by channel-web-*.js which initializes the WhatsApp connection)

Each chunk gets its own independent listeners Map. When channel-web calls setActiveWebListener(), it registers the listener in the reply-*.js Map. When the gateway WebSocket handler calls requireActiveWebListener() to send a proactive message, it checks the model-selection-*.js Map — which is always empty.

This is the exact same class of bug fixed by PR #43683 ("Runtime: share singleton state across bundled chunks") for Telegram, Slack, Signal, iMessage, and core pipeline singletons. WhatsApp's active-listener.ts was not included in that fix.

Steps to Reproduce

  1. Start gateway with WhatsApp linked
  2. Verify openclaw channels status --probe shows connected
  3. Send an inbound WhatsApp message → auto-reply works ✅
  4. Run openclaw message send --channel whatsapp --target "+1234567890" --message "test" → fails with "No active WhatsApp Web listener" ❌
  5. Run any cron with WhatsApp delivery → same failure ❌

Expected Behavior

Proactive sends should work when WhatsApp is connected.

Workaround

Patch both chunks to share the Map via globalThis:

// In both model-selection-*.js and reply-*.js, replace:
const listeners = /* @__PURE__ */ new Map();
// With:
const listeners = globalThis.__openclaw_wa_listeners ??= /* @__PURE__ */ new Map();

Proposed Fix

Apply the same resolveGlobalSingleton(Symbol.for("openclaw.wa-listeners")) pattern from PR #43683 to src/web/active-listener.ts.

Environment

  • OpenClaw: 2026.3.13 (also confirmed on 2026.3.12)
  • Node: 22.22.0
  • Platform: Linux (Debian)

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    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