From 6ae68faf5fd860ee97fc45ece57684b9f75a133e Mon Sep 17 00:00:00 2001 From: clawdia Date: Thu, 19 Mar 2026 02:16:31 +0100 Subject: [PATCH] fix(whatsapp): use globalThis singleton for active-listener Map (#47433) Merged via squash. Prepared head SHA: 1c43dbff399853fd0bd4132886c3394d6659e85b Co-authored-by: clawdia67 <261743618+clawdia67@users.noreply.github.com> Co-authored-by: mcaxtr <7562095+mcaxtr@users.noreply.github.com> Reviewed-by: @mcaxtr --- CHANGELOG.md | 1 + extensions/whatsapp/src/active-listener.ts | 32 ++++++++++++++++++++-- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b1f0a5d9500..1afd7f318a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -155,6 +155,7 @@ Docs: https://docs.openclaw.ai - Google Chat/runtime API: thin the private runtime barrel onto the curated public SDK surface while keeping public Google Chat exports intact. (#49504) Thanks @scoootscooob. - WhatsApp: stabilize inbound monitor and setup tests (#50007) Thanks @joshavant. - Matrix: make onboarding status runtime-safe (#49995) Thanks @joshavant. +- WhatsApp/active-listener: pin the active listener registry to a `globalThis` singleton so split WhatsApp bundle chunks share one listener map and outbound sends stop missing the registered session. (#47433) Thanks @clawdia67. ### Breaking diff --git a/extensions/whatsapp/src/active-listener.ts b/extensions/whatsapp/src/active-listener.ts index 71b6086f3a0..3315a5775ec 100644 --- a/extensions/whatsapp/src/active-listener.ts +++ b/extensions/whatsapp/src/active-listener.ts @@ -28,9 +28,35 @@ export type ActiveWebListener = { close?: () => Promise; }; -let _currentListener: ActiveWebListener | null = null; +// Use a process-level singleton to survive bundler code-splitting. +// Rolldown duplicates this module across multiple output chunks, each with its +// own module-scoped `listeners` Map. The WhatsApp provider writes to one chunk's +// Map via setActiveWebListener(), but the outbound send path reads from a +// different chunk's Map via requireActiveWebListener() — so the listener is +// never found. Pinning the Map to globalThis ensures all chunks share one +// instance. See: https://github.com/openclaw/openclaw/issues/14406 +const GLOBAL_KEY = "__openclaw_wa_listeners" as const; +const GLOBAL_CURRENT_KEY = "__openclaw_wa_current_listener" as const; -const listeners = new Map(); +type GlobalWithListeners = typeof globalThis & { + [GLOBAL_KEY]?: Map; + [GLOBAL_CURRENT_KEY]?: ActiveWebListener | null; +}; + +const _global = globalThis as GlobalWithListeners; + +_global[GLOBAL_KEY] ??= new Map(); +_global[GLOBAL_CURRENT_KEY] ??= null; + +const listeners = _global[GLOBAL_KEY]; + +function getCurrentListener(): ActiveWebListener | null { + return _global[GLOBAL_CURRENT_KEY] ?? null; +} + +function setCurrentListener(listener: ActiveWebListener | null): void { + _global[GLOBAL_CURRENT_KEY] = listener; +} export function resolveWebAccountId(accountId?: string | null): string { return (accountId ?? "").trim() || DEFAULT_ACCOUNT_ID; @@ -74,7 +100,7 @@ export function setActiveWebListener( listeners.set(id, listener); } if (id === DEFAULT_ACCOUNT_ID) { - _currentListener = listener; + setCurrentListener(listener); } }