From c976fc21990fc69d497f2ee785697db2ecac4de7 Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Tue, 21 Apr 2026 23:16:55 -0400 Subject: [PATCH] fix(regression): preserve disabled channel doctor opt-out --- CHANGELOG.md | 2 +- .../doctor/shared/channel-doctor.test.ts | 16 ++++++++++++++++ src/commands/doctor/shared/channel-doctor.ts | 14 +++++++++++++- 3 files changed, 30 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e3a29e18bfb..c263b81c319 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,7 +24,7 @@ Docs: https://docs.openclaw.ai - Plugins/discovery: reject package plugin source entries that escape the package directory before explicit runtime entries or inferred built JavaScript peers can be used. (#69868) thanks @gumadeiras. - CLI/channels: resolve channel presence through a shared policy that keeps ambient env vars and stale persisted auth from surfacing disabled bundled plugins in status, doctor, security audit, and cron delivery validation unless the channel or plugin is effectively enabled or explicitly configured. (#69862) Thanks @gumadeiras. - Control UI/config: preserve intentionally empty raw config snapshots when clearing pending updates so reset restores the original bytes instead of synthesizing JSON for blank config files. (#68178) Thanks @BunsDev. -- Doctor/channels: merge configured-channel doctor hooks across read-only, loaded, setup, and runtime plugin discovery so partial adapters no longer hide runtime-only compatibility repair or allowlist warnings, and ignore malformed hook values before they can mask valid fallbacks. (#69919) Thanks @gumadeiras. +- Doctor/channels: merge configured-channel doctor hooks across read-only, loaded, setup, and runtime plugin discovery so partial adapters no longer hide runtime-only compatibility repair or allowlist warnings, preserve disabled-channel opt-outs, and ignore malformed hook values before they can mask valid fallbacks. (#69919) Thanks @gumadeiras. - memory-core/dreaming: surface a `Dreaming status: blocked` line in `openclaw memory status` when dreaming is enabled but the heartbeat that drives the managed cron is not firing for the default agent, and add a Troubleshooting section to the dreaming docs covering the two common causes (per-agent `heartbeat` blocks excluding `main`, and `heartbeat.every` set to `0`/empty/invalid), so the silent failure described in #69843 becomes legible on the status surface. - Cron/run-log: report generic `message` tool sends under the resolved delivery channel when they match the cron target, while preserving account-specific mismatch checks for delivery traces. (#69940) Thanks @davehappyminion. diff --git a/src/commands/doctor/shared/channel-doctor.test.ts b/src/commands/doctor/shared/channel-doctor.test.ts index d20670dec74..b8f75bbbec5 100644 --- a/src/commands/doctor/shared/channel-doctor.test.ts +++ b/src/commands/doctor/shared/channel-doctor.test.ts @@ -67,6 +67,22 @@ describe("channel doctor compatibility mutations", () => { expect(mocks.getBundledChannelPlugin).not.toHaveBeenCalled(); }); + it("skips plugin discovery for explicitly disabled channels", () => { + const result = collectChannelDoctorCompatibilityMutations({ + channels: { + mattermost: { + enabled: false, + }, + }, + } as never); + + expect(result).toEqual([]); + expect(mocks.resolveReadOnlyChannelPluginsForConfig).not.toHaveBeenCalled(); + expect(mocks.getLoadedChannelPlugin).not.toHaveBeenCalled(); + expect(mocks.getBundledChannelSetupPlugin).not.toHaveBeenCalled(); + expect(mocks.getBundledChannelPlugin).not.toHaveBeenCalled(); + }); + it("uses read-only doctor adapters for configured channel ids", () => { const normalizeCompatibilityConfig = vi.fn(({ cfg }: { cfg: unknown }) => ({ config: cfg, diff --git a/src/commands/doctor/shared/channel-doctor.ts b/src/commands/doctor/shared/channel-doctor.ts index 0f591f3d26e..4bdd3c5524a 100644 --- a/src/commands/doctor/shared/channel-doctor.ts +++ b/src/commands/doctor/shared/channel-doctor.ts @@ -61,8 +61,20 @@ function collectConfiguredChannelIds(cfg: OpenClawConfig): string[] { if (!channels) { return []; } + const channelEntries = channels as Record; return Object.keys(channels) - .filter((channelId) => channelId !== "defaults") + .filter((channelId) => { + if (channelId === "defaults") { + return false; + } + const entry = channelEntries[channelId]; + return ( + !entry || + typeof entry !== "object" || + Array.isArray(entry) || + (entry as { enabled?: unknown }).enabled !== false + ); + }) .toSorted(); }