fix(channels): use pinned channel registry for outbound adapter resolution

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
This commit is contained in:
GodsBoy
2026-03-26 16:15:38 +02:00
committed by Ayaan Zaidi
parent b29e180ef4
commit bc9c074b2c
2 changed files with 25 additions and 2 deletions

View File

@@ -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<TValue> = (
@@ -13,7 +13,7 @@ export function createChannelRegistryLoader<TValue>(
let lastRegistry: PluginRegistry | null = null;
return async (id: ChannelId): Promise<TValue | undefined> => {
const registry = getActivePluginRegistry();
const registry = getActivePluginChannelRegistry();
if (registry !== lastRegistry) {
cache.clear();
lastRegistry = registry;

View File

@@ -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);
});
});