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 connected — channels 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
- Start gateway with WhatsApp linked
- Verify
openclaw channels status --probe shows connected
- Send an inbound WhatsApp message → auto-reply works ✅
- Run
openclaw message send --channel whatsapp --target "+1234567890" --message "test" → fails with "No active WhatsApp Web listener" ❌
- 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
Bug Description
Proactive WhatsApp message sending (via CLI
message send, cron announce delivery, or agentmessagetool) fails with:Despite WhatsApp being fully connected —
channels status --probeshowslinked, running, connected, and inbound auto-replies work correctly.Root Cause
src/web/active-listener.tsdefines a module-scopelisteners = 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 bychannel-web-*.jswhich initializes the WhatsApp connection)Each chunk gets its own independent
listenersMap. Whenchannel-webcallssetActiveWebListener(), it registers the listener in thereply-*.jsMap. When the gateway WebSocket handler callsrequireActiveWebListener()to send a proactive message, it checks themodel-selection-*.jsMap — 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.tswas not included in that fix.Steps to Reproduce
openclaw channels status --probeshowsconnectedopenclaw message send --channel whatsapp --target "+1234567890" --message "test"→ fails with "No active WhatsApp Web listener" ❌Expected Behavior
Proactive sends should work when WhatsApp is connected.
Workaround
Patch both chunks to share the Map via
globalThis:Proposed Fix
Apply the same
resolveGlobalSingleton(Symbol.for("openclaw.wa-listeners"))pattern from PR #43683 tosrc/web/active-listener.ts.Environment
Related