fix(whatsapp): preserve live connections across reloads (#110762)

This commit is contained in:
Marcus Castro
2026-07-18 14:18:11 -03:00
committed by GitHub
parent f1a8c04aea
commit f705f69fc2
6 changed files with 59 additions and 14 deletions
@@ -12,7 +12,7 @@ vi.mock("openclaw/plugin-sdk/channel-runtime-context", () => ({
}));
vi.mock("./runtime.js", () => ({
getOptionalWhatsAppRuntime: () => ({ channel: runtimeContextMocks.channelRuntime }),
getOptionalWhatsAppChannelRuntime: () => runtimeContextMocks.channelRuntime,
}));
const WHATSAPP_ACTIVE_LISTENER_TEST_CFG = {
@@ -3,7 +3,7 @@ import type { WASocket } from "baileys";
import { getChannelRuntimeContext } from "openclaw/plugin-sdk/channel-runtime-context";
import type { WhatsAppSelfIdentity } from "./identity.js";
import type { ActiveWebListener } from "./inbound/types.js";
import { getOptionalWhatsAppRuntime } from "./runtime.js";
import { getOptionalWhatsAppChannelRuntime } from "./runtime.js";
export const WHATSAPP_CONNECTION_CONTROLLER_CAPABILITY = "connection-controller";
export const WHATSAPP_CONNECTION_OWNER_PENDING_CAPABILITY = "connection-owner-pending";
@@ -18,7 +18,7 @@ export function getWhatsAppConnectionController(
accountId: string,
): WhatsAppConnectionControllerHandle | null {
const context = getChannelRuntimeContext({
channelRuntime: getOptionalWhatsAppRuntime()?.channel,
channelRuntime: getOptionalWhatsAppChannelRuntime() ?? undefined,
channelId: "whatsapp",
accountId,
capability: WHATSAPP_CONNECTION_CONTROLLER_CAPABILITY,
@@ -29,7 +29,7 @@ export function getWhatsAppConnectionController(
export function hasPendingWhatsAppConnectionOwner(accountId: string): boolean {
return Boolean(
getChannelRuntimeContext({
channelRuntime: getOptionalWhatsAppRuntime()?.channel,
channelRuntime: getOptionalWhatsAppChannelRuntime() ?? undefined,
channelId: "whatsapp",
accountId,
capability: WHATSAPP_CONNECTION_OWNER_PENDING_CAPABILITY,
@@ -51,7 +51,7 @@ vi.mock("openclaw/plugin-sdk/channel-runtime-context", () => {
});
vi.mock("./runtime.js", () => ({
getWhatsAppRuntime: () => ({ channel: runtimeContextMocks.channelRuntime }),
getWhatsAppChannelRuntime: () => runtimeContextMocks.channelRuntime,
}));
vi.mock("./connection-owner.js", () => ({
@@ -14,7 +14,7 @@ import {
import { resolveComparableIdentity, type WhatsAppSelfIdentity } from "./identity.js";
import type { ActiveWebListener, WebListenerCloseReason } from "./inbound/types.js";
import { computeBackoff, sleepWithAbort, type ReconnectPolicy } from "./reconnect.js";
import { getWhatsAppRuntime } from "./runtime.js";
import { getWhatsAppChannelRuntime } from "./runtime.js";
import {
createWaSocket,
formatError,
@@ -692,7 +692,7 @@ export class WhatsAppConnectionController {
// Publish only after the listener is ready. Lease tokens make disposal of this
// controller's previous registration unable to remove a newer replacement.
this.runtimeContextLease = registerChannelRuntimeContext({
channelRuntime: getWhatsAppRuntime().channel,
channelRuntime: getWhatsAppChannelRuntime(),
channelId: "whatsapp",
accountId: this.accountId,
capability: WHATSAPP_CONNECTION_CONTROLLER_CAPABILITY,
@@ -1011,7 +1011,7 @@ export class WhatsAppConnectionController {
// Keep a pending marker separate from the ready controller capability. This
// blocks a second socket without replacing a still-working controller on reload.
this.pendingOwnerContextLease = registerChannelRuntimeContext({
channelRuntime: getWhatsAppRuntime().channel,
channelRuntime: getWhatsAppChannelRuntime(),
channelId: "whatsapp",
accountId: this.accountId,
capability: WHATSAPP_CONNECTION_OWNER_PENDING_CAPABILITY,
+23
View File
@@ -0,0 +1,23 @@
import type { PluginRuntime } from "openclaw/plugin-sdk/core";
// Whatsapp tests cover runtime injection across plugin registry reloads.
import { describe, expect, it } from "vitest";
import {
getOptionalWhatsAppChannelRuntime,
getWhatsAppChannelRuntime,
getWhatsAppRuntime,
setWhatsAppRuntime,
} from "./runtime.js";
describe("WhatsApp runtime", () => {
it("preserves the channel context owner when the injected runtime changes", () => {
const originalChannelRuntime = getOptionalWhatsAppChannelRuntime();
const first = { channel: { runtimeContexts: { id: "first" } } } as unknown as PluginRuntime;
const second = { channel: { runtimeContexts: { id: "second" } } } as unknown as PluginRuntime;
setWhatsAppRuntime(first);
setWhatsAppRuntime(second);
expect(getWhatsAppRuntime()).toBe(second);
expect(getWhatsAppChannelRuntime()).toBe(originalChannelRuntime ?? first.channel);
});
});
+28 -6
View File
@@ -2,12 +2,34 @@
import type { PluginRuntime } from "openclaw/plugin-sdk/core";
import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store";
const {
setRuntime: setWhatsAppRuntime,
getRuntime: getWhatsAppRuntime,
tryGetRuntime: getOptionalWhatsAppRuntime,
} = createPluginRuntimeStore<PluginRuntime>({
const runtimeStore = createPluginRuntimeStore<PluginRuntime>({
pluginId: "whatsapp",
errorMessage: "WhatsApp runtime not initialized",
});
export { getOptionalWhatsAppRuntime, getWhatsAppRuntime, setWhatsAppRuntime };
const channelRuntimeStore = createPluginRuntimeStore<PluginRuntime["channel"]>({
key: "plugin-runtime:whatsapp:channel-context-owner",
errorMessage: "WhatsApp channel runtime not initialized",
});
/** Injects current helpers while preserving the process-lifetime channel context owner. */
function setWhatsAppRuntime(next: PluginRuntime): void {
// Plugin registry reloads create fresh runtime objects. Live connection leases must remain
// readable by outbound sends until their account task explicitly disposes them.
if (!channelRuntimeStore.tryGetRuntime()) {
channelRuntimeStore.setRuntime(next.channel);
}
runtimeStore.setRuntime(next);
}
const getWhatsAppRuntime = runtimeStore.getRuntime;
const getOptionalWhatsAppRuntime = runtimeStore.tryGetRuntime;
const getWhatsAppChannelRuntime = channelRuntimeStore.getRuntime;
const getOptionalWhatsAppChannelRuntime = channelRuntimeStore.tryGetRuntime;
export {
getOptionalWhatsAppChannelRuntime,
getOptionalWhatsAppRuntime,
getWhatsAppChannelRuntime,
getWhatsAppRuntime,
setWhatsAppRuntime,
};