From e32e0f3f7f3e16dc2daefdbf9ddd1c444833ca29 Mon Sep 17 00:00:00 2001 From: Kaspre <36520309+Kaspre@users.noreply.github.com> Date: Fri, 22 May 2026 01:20:15 -0400 Subject: [PATCH] fix(channels): pass allowBootstrap from channel-selection so in-agent message tool resolves channels in --local processes (#85022) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: - The branch passes `allowBootstrap: true` through outbound channel selection, preserves bundled-plugin resolution before bootstrap, adds focused regression tests, and documents the fix in the changelog. - Reproducibility: yes. source inspection gives a high-confidence reproduction path: current main omits `allow ... run the live current-main failure, but the supplied after-fix terminal proof exercises the implicated path. Automerge notes: - PR branch already contained follow-up commit before automerge: test(channels): cover bootstrap channel selection - PR branch already contained follow-up commit before automerge: fix(channels): avoid unnecessary bootstrap during message sends - PR branch already contained follow-up commit before automerge: fix(channels): pass allowBootstrap from channel-selection so in-agent… Validation: - ClawSweeper review passed for head 44099a80e844c964602a6eb311fd9a9b0552f6d9. - Required merge gates passed before the squash merge. Prepared head SHA: 44099a80e844c964602a6eb311fd9a9b0552f6d9 Review: https://github.com/openclaw/openclaw/pull/85022#issuecomment-4510333662 Co-authored-by: Kaspre Co-authored-by: Claude Opus 4.7 Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com> Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com> Approved-by: takhoffman Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com> --- CHANGELOG.md | 1 + src/infra/outbound/channel-resolution.test.ts | 16 ++++++++++ src/infra/outbound/channel-resolution.ts | 7 ++++- src/infra/outbound/channel-selection.test.ts | 30 +++++++++++++++++++ src/infra/outbound/channel-selection.ts | 10 +++++++ 5 files changed, 63 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a760110686a..45a7e106426 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,7 @@ Docs: https://docs.openclaw.ai - Auto-reply/ACP: wait for same-channel block reply delivery before starting tool work, while still honoring ACP dispatch aborts so stopped turns do not wait on slow channel sends. (#83722) Thanks @IWhatsskill. - Codex/ACP: mark required child-run completions that only report progress, omit a final deliverable, or fail requester delivery as blocked while preserving real final reports. (#85110) Thanks @IWhatsskill. - Channels: treat bare abort messages such as `stop`, `abort`, and `wait` as immediate control commands in inbound debounce paths so stop requests are not delayed behind pending message coalescing. (#83348) Thanks @IWhatsskill. +- Channels/message tool: resolve configured external channel plugins during in-agent channel selection, so `openclaw agent --local` message-tool sends no longer report an available channel as unavailable. (#85022) Thanks @Kaspre. - Agents/subagents: surface blocked child-run completions as errors instead of successful subagent finishes. (#80886) Thanks @TurboTheTurtle. - Agents/Pi: treat accepted embedded `sessions_spawn` child-session handoffs as terminal progress so parent turns no longer report false non-deliverable failures. (#85054) Thanks @samzong. - WhatsApp: update Baileys to `7.0.0-rc13` and drop the obsolete logger type patch. diff --git a/src/infra/outbound/channel-resolution.test.ts b/src/infra/outbound/channel-resolution.test.ts index 98ddc84ffa9..915e1cd8b5c 100644 --- a/src/infra/outbound/channel-resolution.test.ts +++ b/src/infra/outbound/channel-resolution.test.ts @@ -121,6 +121,22 @@ describe("outbound channel resolution", () => { expect(resolveRuntimePluginRegistryMock).not.toHaveBeenCalled(); }); + it("returns a bundled plugin without bootstrapping", async () => { + const plugin = { id: "alpha" }; + getLoadedChannelPluginMock.mockReturnValue(undefined); + getChannelPluginMock.mockReturnValue(plugin); + const channelResolution = await importChannelResolution("bundled-plugin"); + + expect( + channelResolution.resolveOutboundChannelPlugin({ + channel: "alpha", + cfg: {} as never, + allowBootstrap: true, + }), + ).toBe(plugin); + expect(resolveRuntimePluginRegistryMock).not.toHaveBeenCalled(); + }); + it("falls back to the active registry when getChannelPlugin misses", async () => { const plugin = { id: "alpha" }; getChannelPluginMock.mockReturnValue(undefined); diff --git a/src/infra/outbound/channel-resolution.ts b/src/infra/outbound/channel-resolution.ts index 33f897cd223..90e38fb837c 100644 --- a/src/infra/outbound/channel-resolution.ts +++ b/src/infra/outbound/channel-resolution.ts @@ -192,8 +192,13 @@ export function resolveOutboundChannelPlugin(params: { return directCurrent; } + const bundledCurrent = resolve(); + if (bundledCurrent) { + return bundledCurrent; + } + if (params.allowBootstrap !== true) { - return resolve(); + return undefined; } maybeBootstrapChannelPlugin({ channel: normalized, cfg: params.cfg }); diff --git a/src/infra/outbound/channel-selection.test.ts b/src/infra/outbound/channel-selection.test.ts index f831621f77e..b40b028bba4 100644 --- a/src/infra/outbound/channel-selection.test.ts +++ b/src/infra/outbound/channel-selection.test.ts @@ -245,6 +245,36 @@ describe("resolveMessageChannelSelection", () => { verify?.(setupResult as never); }); + it("allows bootstrap while checking explicit and fallback channels", async () => { + const cfg = {} as never; + mocks.resolveOutboundChannelPlugin.mockImplementation(({ channel }: { channel: string }) => + channel === "beta" ? { id: "beta" } : undefined, + ); + + await expect( + expectResolvedSelection({ + cfg, + channel: "alpha", + fallbackChannel: "beta", + }), + ).resolves.toEqual({ + channel: "beta", + configured: [], + source: "tool-context-fallback", + }); + + expect(mocks.resolveOutboundChannelPlugin).toHaveBeenNthCalledWith(1, { + channel: "alpha", + cfg, + allowBootstrap: true, + }); + expect(mocks.resolveOutboundChannelPlugin).toHaveBeenNthCalledWith(2, { + channel: "beta", + cfg, + allowBootstrap: true, + }); + }); + it.each([ { params: { cfg: {} as never, channel: "channel:C123", fallbackChannel: "not-a-channel" }, diff --git a/src/infra/outbound/channel-selection.ts b/src/infra/outbound/channel-selection.ts index 55b97d533ab..2a64ffdb247 100644 --- a/src/infra/outbound/channel-selection.ts +++ b/src/infra/outbound/channel-selection.ts @@ -49,9 +49,19 @@ function resolveAvailableKnownChannel(params: { if (!normalized) { return undefined; } + // Pass `allowBootstrap: true` so the in-agent message tool path can resolve + // outbound channels in processes where external channel adapters have not + // been eagerly loaded (e.g. `openclaw agent --local`). Already-loaded and + // bundled plugins still resolve through side-effect-free fast paths first. + // Without the bootstrap fallback, official external channels can surface as + // the recurring "Channel is unavailable" error on `--local`-routed + // dispatches that the CLI send-path could deliver to. + // Adjacent to #77254 (cron-announce / final-reply paths); this closes the + // remaining in-agent caller in the same family. return resolveOutboundChannelPlugin({ channel: normalized, cfg: params.cfg, + allowBootstrap: true, }) ? normalized : undefined;