#22399: fix(web): use globalThis singleton for active-listener state
channel: whatsapp-web
size: XS
experienced-contributor
Cluster:
GlobalThis Integration Fixes
## Summary
- **Problem:** The Rollup/Vite build system code-splits `src/web/active-listener.ts` into multiple chunks (4 in current builds), each with its own module-level `listeners` Map. `monitorWebChannel` calls `setActiveWebListener()` which writes to one chunk's Map instance, but other code paths (e.g. the WhatsApp extension plugin's `react` action) resolve through a different chunk with an empty Map — causing `"No active WhatsApp Web listener"` errors even though the connection is healthy and text sends work.
- **Root cause:** Module-level mutable state (`const listeners = new Map()`) assumes singleton semantics, but code-splitting violates this by creating independent copies of the module scope per chunk.
- **Fix:** Store the `listeners` Map and `_currentListener` reference on `globalThis` with namespaced keys, so all chunks share the same instance regardless of how the build system splits the module.
> **Note to maintainers:** The ideal long-term fix is a build config change (e.g. Rollup `manualChunks`) to ensure `active-listener.ts` is never code-split. The `globalThis` approach is a correct and resilient workaround that also protects against future accidental code-splitting regressions.
## Change Type (select all)
- [x] Bug fix
## Scope (select all touched areas)
- [x] Integrations
## Linked Issue/PR
- Related #22367 (separate fix for abort-during-reconnect-backoff — a different bug with the same symptom)
## User-visible / Behavior Changes
WhatsApp reactions (`message react`) now work reliably. Previously failed with "No active WhatsApp Web listener" on every attempt, even on fresh gateway starts with no reconnect events.
## Security Impact (required)
- New permissions/capabilities? `No`
- Secrets/tokens handling changed? `No`
- New/changed network calls? `No`
- Command/tool execution surface changed? `No`
- Data access scope changed? `No`
## Repro + Verification
### Environment
- OS: Ubuntu 24.04 (ARM64, OCI)
- Runtime/container: Node.js, OpenClaw gateway
- Integration/channel: WhatsApp Web
### Steps
1. Start gateway with WhatsApp channel
2. Send a WhatsApp message to trigger inbound handling (text reply works)
3. Attempt `message react` with an emoji → fails with "No active WhatsApp Web listener"
### Evidence
4 separate `active-listener-*.js` chunks in `dist/`, each with independent `listeners = new Map()`:
```
dist/active-listener-CQFe2moz.js ← used by monitor (setActiveWebListener)
dist/active-listener-QaB3KeD2.js ← used by different outbound/embedded paths
dist/active-listener-DgvMdw4N.js
dist/active-listener-DJAL1z1W.js
```
### Verified
- Deployed locally, gateway restarted, WhatsApp react confirmed working after fix
## Human Verification (required)
- Verified scenarios: Built, deployed, and tested on live gateway — react works after fix
- Edge cases checked: Multiple accounts (Map keyed by accountId), listener removal (delete from shared Map), deliberate shutdown (listener set to null propagates through globalThis)
- What you did **not** verify: Whether other modules with module-level mutable state have similar code-splitting issues
## Compatibility / Migration
- Backward compatible? `Yes`
- Config/env changes? `No`
- Migration needed? `No`
## Failure Recovery (if this breaks)
- How to disable/revert: Revert single commit, restart gateway
- Known bad symptoms: None expected — the globalThis keys are namespaced and only accessed by this module
<!-- greptile_comment -->
<h3>Greptile Summary</h3>
Correctly fixes code-splitting bug by storing singleton state in `globalThis`. When tsdown/Rollup splits `active-listener.ts` into multiple chunks, each chunk gets its own module-level scope - breaking the singleton pattern for the `listeners` Map. The fix ensures all chunks share the same Map instance by storing it on `globalThis` with namespaced keys.
The implementation:
- Stores the `listeners` Map in `globalThis.__openclaw_web_listeners__`
- Stores `_currentListener` reference in `globalThis.__openclaw_web_current_listener__`
- Uses proper TypeScript typing for the global singleton pattern
- Maintains backward compatibility with existing code
- Follows the correct pattern: initialize from `globalThis` if present, otherwise create and store
The fix is focused and minimal - only touching the state initialization and the setter that updates `_currentListener`. The PR description mentions this is a workaround and the ideal fix would be build config changes to prevent code-splitting of this module.
<h3>Confidence Score: 5/5</h3>
- Safe to merge - clean fix for a well-diagnosed code-splitting bug with no breaking changes
- The fix correctly addresses the root cause (module-level state not being shared across code-split chunks) with a standard `globalThis` singleton pattern. The implementation is clean, properly typed, and maintains backward compatibility. Existing tests continue to work since they use the same module chunk in the test environment. The namespaced global keys prevent collisions.
- No files require special attention
<sub>Last reviewed commit: 0d8b160</sub>
<!-- greptile_other_comments_section -->
<!-- /greptile_comment -->
Most Similar PRs
#22367: fix(whatsapp): prevent permanent listener loss after abort during r...
by mcinteerj · 2026-02-21
81.8%
#23019: fix(hooks): use globalThis singleton for internal hooks handlers Map
by karmafeast · 2026-02-21
77.1%
#9727: fix(whatsapp): retry reconnect loop on initial connection failure
by luizlf · 2026-02-05
76.6%
#16923: fix(web): resolve stale socket race condition in WhatsApp auto-reply
by dorukardahan · 2026-02-15
75.3%
#20554: fix(googlechat): prevent infinite restart loop in startAccount
by Gitjay11 · 2026-02-19
74.2%
#22478: fix(diagnostics-otel): wire OTLP exporter to emit traffic to config...
by LuffySama-Dev · 2026-02-21
74.2%
#20471: fix: share relay auth state across bundler chunks via globalThis
by MisterGuy420 · 2026-02-19
74.2%
#22143: Fix memory leak in WhatsApp channel reconnection loop
by lancejames221b · 2026-02-20
73.4%
#17326: fix(whatsapp): group composing indicator, echo prevention, and pres...
by globalcaos · 2026-02-15
72.6%
#22469: fix(gateway): avoid stale whatsapp labels on direct sessions
by loganprit · 2026-02-21
72.6%