From 5c8761976ceb9fc2ff4dc262429ae05cb17bd922 Mon Sep 17 00:00:00 2001
From: Peter Lee
Date: Fri, 19 Jun 2026 07:41:26 -0500
Subject: [PATCH] fix(whatsapp): restart listener on selfChatMode config change
(#93873)
Merged via squash.
Prepared head SHA: d85f604f01111d2105b8d716a011ad536766a294
Co-authored-by: xialonglee <22994703+xialonglee@users.noreply.github.com>
Co-authored-by: steipete <58493+steipete@users.noreply.github.com>
Reviewed-by: @steipete
---
extensions/whatsapp/index.test.ts | 2 +-
extensions/whatsapp/src/shared.ts | 2 +-
src/gateway/config-reload.test.ts | 10 +++-
src/gateway/server-reload-handlers.test.ts | 53 ++++++++++++++++++++--
4 files changed, 61 insertions(+), 6 deletions(-)
diff --git a/extensions/whatsapp/index.test.ts b/extensions/whatsapp/index.test.ts
index 81e697d8eaa1..b56b22e3b17f 100644
--- a/extensions/whatsapp/index.test.ts
+++ b/extensions/whatsapp/index.test.ts
@@ -15,7 +15,7 @@ describe("whatsapp bundled entries", () => {
it("declares account config as channel-restart reload metadata", () => {
expect(whatsappPlugin.reload).toEqual({
- configPrefixes: ["web", "channels.whatsapp.accounts"],
+ configPrefixes: ["web", "channels.whatsapp.accounts", "channels.whatsapp.selfChatMode"],
noopPrefixes: ["channels.whatsapp"],
});
});
diff --git a/extensions/whatsapp/src/shared.ts b/extensions/whatsapp/src/shared.ts
index c0b8591be2de..441adcac3394 100644
--- a/extensions/whatsapp/src/shared.ts
+++ b/extensions/whatsapp/src/shared.ts
@@ -181,7 +181,7 @@ export function createWhatsAppPluginBase(params: {
// the broad `channels.whatsapp` noop prefix below otherwise swallows it as a
// hot no-op and leaves the account connected until a full restart.
reload: {
- configPrefixes: ["web", "channels.whatsapp.accounts"],
+ configPrefixes: ["web", "channels.whatsapp.accounts", "channels.whatsapp.selfChatMode"],
noopPrefixes: ["channels.whatsapp"],
},
gatewayMethodDescriptors: [{ name: "web.login.start" }, { name: "web.login.wait" }],
diff --git a/src/gateway/config-reload.test.ts b/src/gateway/config-reload.test.ts
index aba0ccaff74e..048a1d8efbb0 100644
--- a/src/gateway/config-reload.test.ts
+++ b/src/gateway/config-reload.test.ts
@@ -160,7 +160,7 @@ describe("buildGatewayReloadPlan", () => {
resolveAccount: () => ({}),
},
reload: {
- configPrefixes: ["web", "channels.whatsapp.accounts"],
+ configPrefixes: ["web", "channels.whatsapp.accounts", "channels.whatsapp.selfChatMode"],
noopPrefixes: ["channels.whatsapp"],
},
};
@@ -235,6 +235,14 @@ describe("buildGatewayReloadPlan", () => {
expect(plan.noopPaths).toStrictEqual([]);
});
+ it("restarts the WhatsApp channel when selfChatMode changes (configPrefix wins over broad noop prefix)", () => {
+ const plan = buildGatewayReloadPlan(["channels.whatsapp.selfChatMode"]);
+ expect(plan.restartGateway).toBe(false);
+ expect(plan.restartChannels).toEqual(new Set(["whatsapp"]));
+ expect(plan.hotReasons).toContain("channels.whatsapp.selfChatMode");
+ expect(plan.noopPaths).toStrictEqual([]);
+ });
+
it("keeps other channels.whatsapp.* changes as hot no-ops", () => {
const plan = buildGatewayReloadPlan(["channels.whatsapp.replyToMode"]);
expect(plan.restartGateway).toBe(false);
diff --git a/src/gateway/server-reload-handlers.test.ts b/src/gateway/server-reload-handlers.test.ts
index 8232fb86ebaf..c92b0eb50005 100644
--- a/src/gateway/server-reload-handlers.test.ts
+++ b/src/gateway/server-reload-handlers.test.ts
@@ -5,8 +5,13 @@ import { afterEach, describe, expect, it, vi } from "vitest";
import type { ConfigWriteNotification } from "../config/config.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { consumeGatewaySigusr1RestartIntent } from "../infra/restart.js";
+import {
+ pinActivePluginChannelRegistry,
+ releasePinnedPluginChannelRegistry,
+} from "../plugins/runtime.js";
import { createEmptyRuntimeWebToolsMetadata } from "../secrets/runtime-fast-path.js";
import { activateSecretsRuntimeSnapshot, clearSecretsRuntimeSnapshot } from "../secrets/runtime.js";
+import { createChannelTestPluginBase, createTestRegistry } from "../test-utils/channel-plugins.js";
import { diffConfigPaths } from "./config-diff.js";
import {
buildGatewayReloadPlan,
@@ -141,7 +146,13 @@ vi.mock("../agents/agent-bundle-mcp-tools.js", () => ({
disposeAllSessionMcpRuntimes: hoisted.disposeAllSessionMcpRuntimes,
}));
-function createReloadHandlersForTest(logReload = { info: vi.fn(), warn: vi.fn() }) {
+function createReloadHandlersForTest(
+ logReload = { info: vi.fn(), warn: vi.fn() },
+ channels?: {
+ start: (channel: ChannelKind) => Promise;
+ stop: (channel: ChannelKind) => Promise;
+ },
+) {
const cron = { start: vi.fn(async () => {}), stop: vi.fn() };
const heartbeatRunner = {
stop: vi.fn(),
@@ -158,8 +169,8 @@ function createReloadHandlersForTest(logReload = { info: vi.fn(), warn: vi.fn()
channelHealthMonitor: null,
}),
setState: vi.fn(),
- startChannel: vi.fn(async () => {}),
- stopChannel: vi.fn(async () => {}),
+ startChannel: channels?.start ?? vi.fn(async () => {}),
+ stopChannel: channels?.stop ?? vi.fn(async () => {}),
stopPostReadySidecars: vi.fn(),
reloadPlugins: vi.fn(
async (): Promise => ({
@@ -889,6 +900,42 @@ describe("gateway channel hot reload handlers", () => {
}
}
+ it("restarts WhatsApp when the planner receives a selfChatMode change", async () => {
+ const whatsappPlugin = {
+ ...createChannelTestPluginBase({ id: "whatsapp" }),
+ reload: {
+ configPrefixes: ["web", "channels.whatsapp.accounts", "channels.whatsapp.selfChatMode"],
+ noopPrefixes: ["channels.whatsapp"],
+ },
+ };
+ const registry = createTestRegistry([
+ { pluginId: "whatsapp", plugin: whatsappPlugin, source: "test" },
+ ]);
+ const events: string[] = [];
+ const channels = {
+ stop: vi.fn(async (channel: ChannelKind) => {
+ events.push(`stop:${channel}`);
+ }),
+ start: vi.fn(async (channel: ChannelKind) => {
+ events.push(`start:${channel}`);
+ }),
+ };
+
+ pinActivePluginChannelRegistry(registry);
+ try {
+ const plan = buildGatewayReloadPlan(["channels.whatsapp.selfChatMode"]);
+ const { applyHotReload } = createReloadHandlersForTest(undefined, channels);
+
+ expect(plan.restartGateway).toBe(false);
+ expect(plan.restartChannels).toEqual(new Set(["whatsapp"]));
+ await withChannelReloadsEnabled(() => applyHotReload(plan, {}));
+
+ expect(events).toEqual(["stop:whatsapp", "start:whatsapp"]);
+ } finally {
+ releasePinnedPluginChannelRegistry(registry);
+ }
+ });
+
it("continues restarting later channels after a hot-reload stop failure", async () => {
const events: string[] = [];
const setState = vi.fn();