fix(whatsapp): use globalThis singleton for active-listener Map#47433
fix(whatsapp): use globalThis singleton for active-listener Map#47433mcaxtr merged 2 commits intoopenclaw:mainfrom
Conversation
Greptile SummaryThis PR correctly fixes the "No active WhatsApp Web listener" error caused by Rolldown code-splitting duplicating the Key observations:
Confidence Score: 4/5
Prompt To Fix All With AIThis is a comment left during a code review.
Path: extensions/whatsapp/src/active-listener.ts
Line: 53-55
Comment:
**`getCurrentListener()` is dead code**
`getCurrentListener()` is defined here and `setCurrentListener()` faithfully writes to `_global[GLOBAL_CURRENT_KEY]`, but `getCurrentListener()` is never called anywhere in the file — neither `getActiveWebListener` nor `requireActiveWebListener` reads from it. Both functions exclusively use `listeners.get(id)` (the `GLOBAL_KEY`-backed Map).
This was true of the old `_currentListener` variable too (it was set but apparently never read), so the PR faithfully preserves the existing pattern — but it does mean `GLOBAL_CURRENT_KEY` is maintained as dead state on `globalThis`.
Consider either removing `getCurrentListener()` and `setCurrentListener()` (and the `GLOBAL_CURRENT_KEY` constant and its initializer), or wiring `getCurrentListener()` into `getActiveWebListener` as a fallback for the `DEFAULT_ACCOUNT_ID` case if that was the original intent:
```typescript
export function getActiveWebListener(accountId?: string | null): ActiveWebListener | null {
const id = resolveWebAccountId(accountId);
return listeners.get(id) ?? (id === DEFAULT_ACCOUNT_ID ? getCurrentListener() : null);
}
```
How can I resolve this? If you propose a fix, please make it concise.Last reviewed commit: a298168 |
| function getCurrentListener(): ActiveWebListener | null { | ||
| return _global[GLOBAL_CURRENT_KEY] ?? null; | ||
| } |
There was a problem hiding this comment.
getCurrentListener() is dead code
getCurrentListener() is defined here and setCurrentListener() faithfully writes to _global[GLOBAL_CURRENT_KEY], but getCurrentListener() is never called anywhere in the file — neither getActiveWebListener nor requireActiveWebListener reads from it. Both functions exclusively use listeners.get(id) (the GLOBAL_KEY-backed Map).
This was true of the old _currentListener variable too (it was set but apparently never read), so the PR faithfully preserves the existing pattern — but it does mean GLOBAL_CURRENT_KEY is maintained as dead state on globalThis.
Consider either removing getCurrentListener() and setCurrentListener() (and the GLOBAL_CURRENT_KEY constant and its initializer), or wiring getCurrentListener() into getActiveWebListener as a fallback for the DEFAULT_ACCOUNT_ID case if that was the original intent:
export function getActiveWebListener(accountId?: string | null): ActiveWebListener | null {
const id = resolveWebAccountId(accountId);
return listeners.get(id) ?? (id === DEFAULT_ACCOUNT_ID ? getCurrentListener() : null);
}Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/whatsapp/src/active-listener.ts
Line: 53-55
Comment:
**`getCurrentListener()` is dead code**
`getCurrentListener()` is defined here and `setCurrentListener()` faithfully writes to `_global[GLOBAL_CURRENT_KEY]`, but `getCurrentListener()` is never called anywhere in the file — neither `getActiveWebListener` nor `requireActiveWebListener` reads from it. Both functions exclusively use `listeners.get(id)` (the `GLOBAL_KEY`-backed Map).
This was true of the old `_currentListener` variable too (it was set but apparently never read), so the PR faithfully preserves the existing pattern — but it does mean `GLOBAL_CURRENT_KEY` is maintained as dead state on `globalThis`.
Consider either removing `getCurrentListener()` and `setCurrentListener()` (and the `GLOBAL_CURRENT_KEY` constant and its initializer), or wiring `getCurrentListener()` into `getActiveWebListener` as a fallback for the `DEFAULT_ACCOUNT_ID` case if that was the original intent:
```typescript
export function getActiveWebListener(accountId?: string | null): ActiveWebListener | null {
const id = resolveWebAccountId(accountId);
return listeners.get(id) ?? (id === DEFAULT_ACCOUNT_ID ? getCurrentListener() : null);
}
```
How can I resolve this? If you propose a fix, please make it concise.|
Confirmed working as a manual patch on 2026.3.13 (macOS arm64) We hit this exact bug today — outbound Applied the
After a gateway restart, outbound Please merge/release ASAP — this is a painful bug that's hard to diagnose. 🙏 Thank you!!! |
|
Confirming that this approach works. I manually patched my local installation (v2026.3.13) by forcing the Environment Details: Before the patch, I was consistently hitting the 'No active WhatsApp Web listener' error when using the Thanks for the fix, looking forward to the official merge! |
|
We've been hit hard by this in production. Our setup runs OpenClaw v2026.3.13 as a WhatsApp automation hub on macOS (launchd-managed), with ~10 cron jobs that send notifications via Since upgrading to v2026.3.13, every programmatic send fails with "No active WhatsApp Web listener (account: default)" while auto-replies continue working — classic symptom of the split-Map issue described here. Impact on our end:
The |
1bd307d to
1c43dbf
Compare
|
Merged via squash.
Thanks @clawdia67! |
…claw#47433) Merged via squash. Prepared head SHA: 1c43dbf Co-authored-by: clawdia67 <[email protected]> Co-authored-by: mcaxtr <[email protected]> Reviewed-by: @mcaxtr
|
I manually (with codex) backported the already-merged fix from PR #47433 into my local With Codex helping map the compiled bundles, I confirmed my install had the same split-state problem described in that PR: the WhatsApp active listener registry had been duplicated across multiple
The local hot patch was just a manual backport of const listeners = /* @__PURE__ */ new Map();
to:
globalThis.__openclaw_wa_listeners = globalThis.__openclaw_wa_listeners ?? /* @__PURE__ */ new Map();
const listeners = globalThis.__openclaw_wa_listeners;
In my install, the patched files were:
- discord-CcCLMjHw.js
- model-selection-46xMp11W.js
- model-selection-CU2b7bN6.js
- auth-profiles-DDVivXkv.js
- auth-profiles-DRjqKE3G.js
- reply-Bm8VrLQh.js
- plugin-sdk/thread-bindings-SYAnWHuW.jsAfter restarting the gateway, WhatsApp re-registered normally and delivery is working again on my system. So this appears to confirm that PR #47433 addresses the issue; I just applied it manually to my installed 2026.3.13 bundles before an official release containing that commit was available. |
…claw#47433) Merged via squash. Prepared head SHA: 1c43dbf Co-authored-by: clawdia67 <[email protected]> Co-authored-by: mcaxtr <[email protected]> Reviewed-by: @mcaxtr
…claw#47433) Merged via squash. Prepared head SHA: 1c43dbf Co-authored-by: clawdia67 <[email protected]> Co-authored-by: mcaxtr <[email protected]> Reviewed-by: @mcaxtr
…claw#47433) Merged via squash. Prepared head SHA: 1c43dbf Co-authored-by: clawdia67 <[email protected]> Co-authored-by: mcaxtr <[email protected]> Reviewed-by: @mcaxtr
…claw#47433) Merged via squash. Prepared head SHA: 1c43dbf Co-authored-by: clawdia67 <[email protected]> Co-authored-by: mcaxtr <[email protected]> Reviewed-by: @mcaxtr
|
I manually backported the merged fix from PR #47433 into my local OpenClaw 2026.3.13 install rather than waiting for the next release. The issue matched the split-state problem described in that PR: the WhatsApp active-listener registry had been duplicated across multiple compiled dist chunks, so one chunk registered the active listener while another later read a different empty Map and threw: No active WhatsApp Web listener (account: default) The local hotfix was to replace the per-bundle listener registry: const listeners = /* @PURE */ new Map(); with the shared global singleton approach: globalThis.__openclaw_wa_listeners = How to locate the build files /home/picoclaw/.npm-global/lib/node_modules/openclaw/dist To find the correct folder on your own system: find ~ -type d -name "dist" 2>/dev/null | grep openclaw Then verify the correct folder with: grep -R "const listeners = /* @PURE */ new Map()" /path/to/openclaw/dist Patch only the main OpenClaw dist folder, not nested dependency dist folders under node_modules. Patch script #!/usr/bin/env bash TARGET_DIR="${1:-.}" SEARCH='const listeners = /* @PURE / new Map();' echo "== OpenClaw WhatsApp listener hot patch ==" if [[ ! -d "$TARGET_DIR" ]]; then mkdir -p "$BACKUP_ROOT" mapfile -t files < <(grep -R -l --fixed-strings "$SEARCH" "$TARGET_DIR" 2>/dev/null || true) if [[ ${#files[@]} -eq 0 ]]; then echo "Found ${#files[@]} candidate file(s):" for f in "${files[@]}"; do python3 - "$f" "$SEARCH" "$REPLACEMENT" <<'PY' file_path = Path(sys.argv[1]) text = file_path.read_text(encoding="utf-8") if "globalThis.__openclaw_wa_listeners" in text: count = text.count(search) new_text = text.replace(search, replacement) if new_text == text: file_path.write_text(new_text, encoding="utf-8") echo echo "Files now containing the singleton:" echo How to use
Example from my Raspberry Pi: How to reverse the hotfix ./backup_openclaw_wa_patch_YYYYMMDD_HHMMSS To undo the patch:
Safer restore command: cd ./backup_openclaw_wa_patch_YYYYMMDD_HHMMSS You can also find patched files with: grep -R "globalThis.__openclaw_wa_listeners" /path/to/openclaw/dist and manually change: globalThis.__openclaw_wa_listeners = back to: const listeners = /* @PURE */ new Map(); Notes |
Summary
listenersMap inextensions/whatsapp/src/active-listener.tsis duplicated across 7+ output chunks by Rolldown code-splittingsetActiveWebListener()populates the Map in one chunk, butrequireActiveWebListener()reads from a different chunk's copymessagetool always fail with "No active WhatsApp Web listener" while auto-replies (which bypass the Map via direct socket references) work finelistenersMap and_currentListenertoglobalThisso all chunk-local copies resolve to the same shared instanceTest plan
send.test.tspassesmessagetool — should succeed instead of throwing "No active WhatsApp Web listener"getActiveWebListener()/requireActiveWebListener()return the listener registered bysetActiveWebListener()even when called from a different chunkFixes #14406
🤖 Generated with Claude Code