From acbdafc4f4c01960de6c20c829454a714f55032a Mon Sep 17 00:00:00 2001 From: karesansui Date: Mon, 16 Mar 2026 01:45:00 +0900 Subject: [PATCH] fix: propagate webhook mode to health monitor snapshot Webhook channels (LINE, Zalo, Nextcloud Talk, BlueBubbles) are incorrectly flagged as stale-socket during quiet periods because snapshot.mode is always undefined, making the mode !== "webhook" guard in evaluateChannelHealth dead code. Add mode: "webhook" to each webhook plugin's describeAccount and propagate described.mode in getRuntimeSnapshot. Co-Authored-By: Claude Opus 4.6 --- CHANGELOG.md | 1 + extensions/bluebubbles/src/channel-shared.ts | 1 + extensions/line/src/channel-shared.ts | 1 + extensions/zalo/src/channel.ts | 1 + src/gateway/server-channels.test.ts | 30 ++++++++++++++++---- src/gateway/server-channels.ts | 3 ++ 6 files changed, 32 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 346b22c4040..642bedc1e5c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ Docs: https://docs.openclaw.ai - Memory/QMD: resolve slugified `memory_search` file hints back to the indexed filesystem path before returning search hits, so `memory_get` works again for mixed-case and spaced paths. (#50313) Thanks @erra9x. - Security/LINE: make webhook signature validation run the timing-safe compare even when the supplied signature length is wrong, closing a small timing side-channel. (#55663) Thanks @gavyngong. - LINE/status: stop `openclaw status` from warning about missing credentials when sanitized LINE snapshots are already configured, while still surfacing whether the missing field is the token or secret. (#45701) Thanks @tamaosamu. +- Gateway/health: carry webhook-vs-polling account mode from channel descriptors into runtime snapshots so passive channels like LINE and BlueBubbles skip false stale-socket health failures. (#47488) Thanks @karesansui-u. ## 2026.3.28-beta.1 diff --git a/extensions/bluebubbles/src/channel-shared.ts b/extensions/bluebubbles/src/channel-shared.ts index 69f782208e5..066ce57e81d 100644 --- a/extensions/bluebubbles/src/channel-shared.ts +++ b/extensions/bluebubbles/src/channel-shared.ts @@ -63,6 +63,7 @@ export function describeBlueBubblesAccount(account: ResolvedBlueBubblesAccount) configured: account.configured, extra: { baseUrl: account.baseUrl, + mode: "webhook", }, }); } diff --git a/extensions/line/src/channel-shared.ts b/extensions/line/src/channel-shared.ts index ce0f455ae4d..70fa3b38a7f 100644 --- a/extensions/line/src/channel-shared.ts +++ b/extensions/line/src/channel-shared.ts @@ -43,6 +43,7 @@ export const lineChannelPluginCommon = { enabled: account.enabled, configured: hasLineCredentials(account), tokenSource: account.tokenSource ?? undefined, + mode: "webhook", }), }, } satisfies Pick< diff --git a/extensions/zalo/src/channel.ts b/extensions/zalo/src/channel.ts index 084936ad1bd..754a5db1bab 100644 --- a/extensions/zalo/src/channel.ts +++ b/extensions/zalo/src/channel.ts @@ -177,6 +177,7 @@ export const zaloPlugin: ChannelPlugin = configured: Boolean(account.token?.trim()), extra: { tokenSource: account.tokenSource, + mode: account.config.webhookUrl ? "webhook" : "polling", }, }), }, diff --git a/src/gateway/server-channels.test.ts b/src/gateway/server-channels.test.ts index 1f2e218afaa..c96810dfa5a 100644 --- a/src/gateway/server-channels.test.ts +++ b/src/gateway/server-channels.test.ts @@ -46,6 +46,7 @@ function createTestPlugin(params?: { account?: TestAccount; startAccount?: NonNullable["gateway"]>["startAccount"]; includeDescribeAccount?: boolean; + describeAccount?: ChannelPlugin["config"]["describeAccount"]; resolveAccount?: ChannelPlugin["config"]["resolveAccount"]; isConfigured?: ChannelPlugin["config"]["isConfigured"]; }): ChannelPlugin { @@ -59,11 +60,13 @@ function createTestPlugin(params?: { ...(params?.isConfigured ? { isConfigured: params.isConfigured } : {}), }; if (includeDescribeAccount) { - config.describeAccount = (resolved) => ({ - accountId: DEFAULT_ACCOUNT_ID, - enabled: resolved.enabled !== false, - configured: resolved.configured !== false, - }); + config.describeAccount = + params?.describeAccount ?? + ((resolved) => ({ + accountId: DEFAULT_ACCOUNT_ID, + enabled: resolved.enabled !== false, + configured: resolved.configured !== false, + })); } const gateway: NonNullable["gateway"]> = {}; if (params?.startAccount) { @@ -198,6 +201,23 @@ describe("server-channels auto restart", () => { expect(account?.configured).toBe(true); }); + it("forwards described mode into runtime snapshots", () => { + installTestRegistry( + createTestPlugin({ + describeAccount: (resolved) => ({ + accountId: DEFAULT_ACCOUNT_ID, + enabled: resolved.enabled !== false, + configured: resolved.configured !== false, + mode: "webhook", + }), + }), + ); + const manager = createManager(); + const snapshot = manager.getRuntimeSnapshot(); + const account = snapshot.channelAccounts.discord?.[DEFAULT_ACCOUNT_ID]; + expect(account?.mode).toBe("webhook"); + }); + it("passes channelRuntime through channel gateway context when provided", async () => { const channelRuntime = { marker: "channel-runtime" } as unknown as PluginRuntime["channel"]; const startAccount = vi.fn(async (ctx) => { diff --git a/src/gateway/server-channels.ts b/src/gateway/server-channels.ts index 29328ade445..3165d4c4aec 100644 --- a/src/gateway/server-channels.ts +++ b/src/gateway/server-channels.ts @@ -555,6 +555,9 @@ export function createChannelManager(opts: ChannelManagerOptions): ChannelManage const next = { ...current, accountId: id }; next.enabled = enabled; next.configured = typeof configured === "boolean" ? configured : (next.configured ?? true); + if (described?.mode !== undefined) { + next.mode = described.mode; + } if (!next.running) { if (!enabled) { next.lastError ??= plugin.config.disabledReason?.(account, cfg) ?? "disabled";