From bc9c074b2c17c9fc5e241d833acb6efea6844417 Mon Sep 17 00:00:00 2001 From: GodsBoy Date: Thu, 26 Mar 2026 16:15:38 +0200 Subject: [PATCH] fix(channels): use pinned channel registry for outbound adapter resolution MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit loadChannelOutboundAdapter (via createChannelRegistryLoader) was reading from getActivePluginRegistry() — the unpinned active registry that gets replaced whenever loadOpenClawPlugins() runs (config schema reads, plugin status queries, tool listings, etc.). After replacement, the active registry may omit channel entries or carry them in setup mode without outbound adapters, causing: Outbound not configured for channel: telegram The channel inbound path already uses the pinned registry (getActivePluginChannelRegistry) which is frozen at gateway startup and survives all subsequent registry replacements. This commit aligns the outbound path to use the same pinned surface. Adds a regression test that pins a registry with a telegram outbound adapter, replaces the active registry with an empty one, then asserts loadChannelOutboundAdapter still resolves the adapter. Fixes #54745 Fixes #54013 --- src/channels/plugins/registry-loader.ts | 4 ++-- src/plugins/runtime.channel-pin.test.ts | 23 +++++++++++++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/channels/plugins/registry-loader.ts b/src/channels/plugins/registry-loader.ts index 9f23c5fa09e..baa20555f46 100644 --- a/src/channels/plugins/registry-loader.ts +++ b/src/channels/plugins/registry-loader.ts @@ -1,5 +1,5 @@ import type { PluginChannelRegistration, PluginRegistry } from "../../plugins/registry.js"; -import { getActivePluginRegistry } from "../../plugins/runtime.js"; +import { getActivePluginChannelRegistry } from "../../plugins/runtime.js"; import type { ChannelId } from "./types.js"; type ChannelRegistryValueResolver = ( @@ -13,7 +13,7 @@ export function createChannelRegistryLoader( let lastRegistry: PluginRegistry | null = null; return async (id: ChannelId): Promise => { - const registry = getActivePluginRegistry(); + const registry = getActivePluginChannelRegistry(); if (registry !== lastRegistry) { cache.clear(); lastRegistry = registry; diff --git a/src/plugins/runtime.channel-pin.test.ts b/src/plugins/runtime.channel-pin.test.ts index 1cce99b0e6c..4271c4d6806 100644 --- a/src/plugins/runtime.channel-pin.test.ts +++ b/src/plugins/runtime.channel-pin.test.ts @@ -1,4 +1,5 @@ import { afterEach, describe, expect, it } from "vitest"; +import { loadChannelOutboundAdapter } from "../channels/plugins/outbound/load.js"; import { getChannelPlugin } from "../channels/plugins/registry.js"; import { createEmptyPluginRegistry } from "./registry-empty.js"; import { @@ -173,4 +174,26 @@ describe("channel registry pinning", () => { freshRegistry: fresh, }); }); + + it("loadChannelOutboundAdapter resolves from pinned registry after active registry replacement", async () => { + const outboundAdapter = { send: async () => ({ messageId: "1" }) }; + const startup = createEmptyPluginRegistry(); + startup.channels = [ + { + pluginId: "telegram", + plugin: { id: "telegram", meta: {}, outbound: outboundAdapter }, + source: "test", + }, + ] as never; + setActivePluginRegistry(startup); + pinActivePluginChannelRegistry(startup); + + // Simulate a post-boot registry replacement (e.g. config-schema load, plugin status query). + const replacement = createEmptyPluginRegistry(); + setActivePluginRegistry(replacement); + + // The outbound loader must still find the telegram adapter from the pinned registry. + const adapter = await loadChannelOutboundAdapter("telegram"); + expect(adapter).toBe(outboundAdapter); + }); });