diff --git a/src/infra/outbound/channel-bootstrap.runtime.test.ts b/src/infra/outbound/channel-bootstrap.runtime.test.ts index 9a4c44046417..12d961a89acb 100644 --- a/src/infra/outbound/channel-bootstrap.runtime.test.ts +++ b/src/infra/outbound/channel-bootstrap.runtime.test.ts @@ -76,4 +76,94 @@ describe("bootstrapOutboundChannelPlugin", () => { expect(loaderMocks.resolveRuntimePluginRegistry).not.toHaveBeenCalled(); }); + + it("skips bootstrap when the active replacement registry can send for a pinned setup shell", () => { + const setup = createEmptyPluginRegistry(); + setup.channels = [ + { + pluginId: "discord", + plugin: { id: "discord", meta: {} }, + source: "setup", + }, + ] as never; + const runtime = createEmptyPluginRegistry(); + runtime.channels = [ + { + pluginId: "discord", + plugin: { + id: "discord", + meta: {}, + outbound: { sendText: async () => ({ messageId: "1" }) }, + }, + source: "runtime", + }, + ] as never; + setActivePluginRegistry(setup); + pinActivePluginChannelRegistry(setup); + setActivePluginRegistry(runtime); + + bootstrapOutboundChannelPlugin({ + channel: "discord", + cfg: discordConfig, + }); + + expect(loaderMocks.resolveRuntimePluginRegistry).not.toHaveBeenCalled(); + }); + + it("skips bootstrap when the active replacement registry has a message send surface", () => { + const setup = createEmptyPluginRegistry(); + setup.channels = [ + { + pluginId: "discord", + plugin: { id: "discord", meta: {} }, + source: "setup", + }, + ] as never; + const runtime = createEmptyPluginRegistry(); + runtime.channels = [ + { + pluginId: "discord", + plugin: { + id: "discord", + meta: {}, + message: { send: { text: async () => ({ messageId: "1" }) } }, + }, + source: "runtime", + }, + ] as never; + setActivePluginRegistry(setup); + pinActivePluginChannelRegistry(setup); + setActivePluginRegistry(runtime); + + bootstrapOutboundChannelPlugin({ + channel: "discord", + cfg: discordConfig, + }); + + expect(loaderMocks.resolveRuntimePluginRegistry).not.toHaveBeenCalled(); + }); + + it("retries when bootstrap returns without making the channel send-capable", () => { + const registry = createEmptyPluginRegistry(); + registry.channels = [ + { + pluginId: "discord", + plugin: { id: "discord", meta: {} }, + source: "setup", + }, + ] as never; + setActivePluginRegistry(registry); + pinActivePluginChannelRegistry(registry); + + bootstrapOutboundChannelPlugin({ + channel: "discord", + cfg: discordConfig, + }); + bootstrapOutboundChannelPlugin({ + channel: "discord", + cfg: discordConfig, + }); + + expect(loaderMocks.resolveRuntimePluginRegistry).toHaveBeenCalledTimes(2); + }); }); diff --git a/src/infra/outbound/channel-bootstrap.runtime.ts b/src/infra/outbound/channel-bootstrap.runtime.ts index 03fd8fc062b4..92a6e5f577cd 100644 --- a/src/infra/outbound/channel-bootstrap.runtime.ts +++ b/src/infra/outbound/channel-bootstrap.runtime.ts @@ -8,6 +8,8 @@ import type { PluginChannelRegistration } from "../../plugins/registry-types.js" import { getActivePluginChannelRegistry, getActivePluginChannelRegistryVersion, + getActivePluginRegistry, + getActivePluginRegistryVersion, } from "../../plugins/runtime.js"; import type { DeliverableMessageChannel } from "../../utils/message-channel.js"; @@ -22,6 +24,27 @@ function channelEntryCanSend(entry: PluginChannelRegistration | undefined): bool return Boolean(entry?.plugin?.outbound?.sendText ?? entry?.plugin?.message?.send?.text); } +function findChannelEntry( + registry: ReturnType, + channel: DeliverableMessageChannel, +): PluginChannelRegistration | undefined { + return registry?.channels?.find((entry) => entry?.plugin?.id === channel); +} + +function canResolveSendCapableChannel(channel: DeliverableMessageChannel): boolean { + const activeChannelRegistry = getActivePluginChannelRegistry(); + const channelEntry = findChannelEntry(activeChannelRegistry, channel); + if (channelEntryCanSend(channelEntry)) { + return true; + } + + const activeRegistry = getActivePluginRegistry(); + if (activeRegistry && activeRegistry !== activeChannelRegistry) { + return channelEntryCanSend(findChannelEntry(activeRegistry, channel)); + } + return false; +} + /** Loads runtime plugins on demand when a selected outbound channel has only a setup shell. */ export function bootstrapOutboundChannelPlugin(params: { channel: DeliverableMessageChannel; @@ -32,15 +55,11 @@ export function bootstrapOutboundChannelPlugin(params: { return; } - const activeChannelRegistry = getActivePluginChannelRegistry(); - const activeChannelEntry = activeChannelRegistry?.channels?.find( - (entry) => entry?.plugin?.id === params.channel, - ); - if (channelEntryCanSend(activeChannelEntry)) { + if (canResolveSendCapableChannel(params.channel)) { return; } - const attemptKey = `${getActivePluginChannelRegistryVersion()}:${params.channel}`; + const attemptKey = `${getActivePluginChannelRegistryVersion()}:${getActivePluginRegistryVersion()}:${params.channel}`; if (bootstrapAttempts.has(attemptKey)) { return; } @@ -61,6 +80,9 @@ export function bootstrapOutboundChannelPlugin(params: { allowGatewaySubagentBinding: true, }, }); + if (!canResolveSendCapableChannel(params.channel)) { + bootstrapAttempts.delete(attemptKey); + } } catch { bootstrapAttempts.delete(attemptKey); } diff --git a/src/infra/outbound/channel-resolution.test.ts b/src/infra/outbound/channel-resolution.test.ts index 42c1c0f757d9..6d3ede5dc7cb 100644 --- a/src/infra/outbound/channel-resolution.test.ts +++ b/src/infra/outbound/channel-resolution.test.ts @@ -9,6 +9,7 @@ const getChannelPluginMock = vi.hoisted(() => vi.fn()); const applyPluginAutoEnableMock = vi.hoisted(() => vi.fn()); const resolveRuntimePluginRegistryMock = vi.hoisted(() => vi.fn()); const getActivePluginRegistryMock = vi.hoisted(() => vi.fn()); +const getActivePluginRegistryVersionMock = vi.hoisted(() => vi.fn()); const getActivePluginChannelRegistryMock = vi.hoisted(() => vi.fn()); const getActivePluginChannelRegistryVersionMock = vi.hoisted(() => vi.fn()); const normalizeMessageChannelMock = vi.hoisted(() => vi.fn()); @@ -34,6 +35,8 @@ vi.mock("../../plugins/loader.js", () => ({ vi.mock("../../plugins/runtime.js", () => ({ getActivePluginRegistry: (...args: unknown[]) => getActivePluginRegistryMock(...args), + getActivePluginRegistryVersion: (...args: unknown[]) => + getActivePluginRegistryVersionMock(...args), getActivePluginChannelRegistry: (...args: unknown[]) => getActivePluginChannelRegistryMock(...args), getActivePluginChannelRegistryVersion: (...args: unknown[]) => @@ -75,6 +78,7 @@ describe("outbound channel resolution", () => { applyPluginAutoEnableMock.mockReset(); resolveRuntimePluginRegistryMock.mockReset(); getActivePluginRegistryMock.mockReset(); + getActivePluginRegistryVersionMock.mockReset(); getActivePluginChannelRegistryMock.mockReset(); getActivePluginChannelRegistryVersionMock.mockReset(); normalizeMessageChannelMock.mockReset(); @@ -87,6 +91,7 @@ describe("outbound channel resolution", () => { ["alpha", "beta", "gamma"].includes(String(value)), ); getActivePluginRegistryMock.mockReturnValue({ channels: [] }); + getActivePluginRegistryVersionMock.mockReturnValue(1); getActivePluginChannelRegistryMock.mockReturnValue({ channels: [] }); getActivePluginChannelRegistryVersionMock.mockReturnValue(1); applyPluginAutoEnableMock.mockReturnValue({ @@ -110,7 +115,7 @@ describe("outbound channel resolution", () => { }); it("returns the already-registered plugin without bootstrapping", async () => { - const plugin = { id: "alpha" }; + const plugin = { id: "alpha", outbound: { sendText: vi.fn() } }; getLoadedChannelPluginMock.mockReturnValueOnce(plugin); const channelResolution = await importChannelResolution("existing-plugin"); @@ -124,7 +129,7 @@ describe("outbound channel resolution", () => { }); it("returns a bundled plugin without bootstrapping", async () => { - const plugin = { id: "alpha" }; + const plugin = { id: "alpha", outbound: { sendText: vi.fn() } }; getLoadedChannelPluginMock.mockReturnValue(undefined); getChannelPluginMock.mockReturnValue(plugin); const channelResolution = await importChannelResolution("bundled-plugin"); @@ -158,8 +163,52 @@ describe("outbound channel resolution", () => { ).toBe(plugin); }); + it("resolves message adapters from the pinned channel registry after active registry replacement", async () => { + const message = { send: { text: vi.fn() } }; + const plugin = { id: "alpha", message }; + getLoadedChannelPluginMock.mockReturnValue(undefined); + getChannelPluginMock.mockReturnValue(undefined); + getActivePluginChannelRegistryMock.mockReturnValue({ + channels: [{ plugin }], + }); + getActivePluginRegistryMock.mockReturnValue({ channels: [] }); + const channelResolution = await importChannelResolution("pinned-message-registry"); + + expect( + channelResolution.resolveOutboundChannelMessageAdapter({ + channel: "alpha", + cfg: {} as never, + }), + ).toBe(message); + }); + + it("skips metadata-only loaded message shells for active send-capable message adapters", async () => { + const setupMessage = { receive: { defaultAckPolicy: "manual" } }; + const runtimeMessage = { send: { text: vi.fn() } }; + const setupPlugin = { id: "alpha", message: setupMessage }; + const runtimePlugin = { id: "alpha", message: runtimeMessage }; + getLoadedChannelPluginMock.mockReturnValue(setupPlugin); + getChannelPluginMock.mockReturnValue(undefined); + getActivePluginChannelRegistryMock.mockReturnValue({ + channels: [{ plugin: setupPlugin }], + }); + getActivePluginRegistryMock.mockReturnValue({ + channels: [{ plugin: runtimePlugin }], + }); + const channelResolution = await importChannelResolution("message-metadata-shell"); + + expect( + channelResolution.resolveOutboundChannelMessageAdapter({ + channel: "alpha", + cfg: { channels: {} } as never, + allowBootstrap: true, + }), + ).toBe(runtimeMessage); + expect(resolveRuntimePluginRegistryMock).not.toHaveBeenCalled(); + }); + it("bootstraps configured channel plugins when the active registry is missing the target", async () => { - const plugin = { id: "alpha" }; + const plugin = { id: "alpha", outbound: { sendText: vi.fn() } }; getLoadedChannelPluginMock.mockReturnValueOnce(undefined).mockReturnValueOnce(plugin); const channelResolution = await importChannelResolution("bootstrap-missing-target"); @@ -182,6 +231,161 @@ describe("outbound channel resolution", () => { }); }); + it("bootstraps instead of returning a pinned setup shell as the outbound plugin", async () => { + const setupPlugin = { id: "alpha" }; + const runtimePlugin = { id: "alpha", outbound: { sendText: vi.fn() } }; + getLoadedChannelPluginMock.mockReturnValueOnce(setupPlugin).mockReturnValueOnce(runtimePlugin); + getChannelPluginMock.mockReturnValue(undefined); + getActivePluginRegistryMock.mockReturnValue({ channels: [] }); + getActivePluginChannelRegistryMock.mockReturnValue({ + channels: [{ plugin: setupPlugin }], + }); + const channelResolution = await importChannelResolution("bootstrap-setup-shell"); + + expect( + channelResolution.resolveOutboundChannelPlugin({ + channel: "alpha", + cfg: { channels: {} } as never, + allowBootstrap: true, + }), + ).toBe(runtimePlugin); + expect(resolveRuntimePluginRegistryMock).toHaveBeenCalledTimes(1); + }); + + it("does not return a setup shell when bootstrap does not produce a runtime plugin", async () => { + const setupPlugin = { id: "alpha" }; + getLoadedChannelPluginMock.mockReturnValue(setupPlugin); + getChannelPluginMock.mockReturnValue(setupPlugin); + getActivePluginRegistryMock.mockReturnValue({ + channels: [{ plugin: setupPlugin }], + }); + getActivePluginChannelRegistryMock.mockReturnValue({ + channels: [{ plugin: setupPlugin }], + }); + const channelResolution = await importChannelResolution("bootstrap-still-setup-shell"); + + expect( + channelResolution.resolveOutboundChannelPlugin({ + channel: "alpha", + cfg: { channels: {} } as never, + allowBootstrap: true, + }), + ).toBeUndefined(); + expect(resolveRuntimePluginRegistryMock).toHaveBeenCalledTimes(1); + }); + + it("does not treat an actions-only plugin as send-capable after bootstrap", async () => { + const actionsOnlyPlugin = { id: "alpha", actions: { handleAction: vi.fn() } }; + getLoadedChannelPluginMock.mockReturnValue(actionsOnlyPlugin); + getChannelPluginMock.mockReturnValue(actionsOnlyPlugin); + getActivePluginRegistryMock.mockReturnValue({ + channels: [{ plugin: actionsOnlyPlugin }], + }); + getActivePluginChannelRegistryMock.mockReturnValue({ + channels: [{ plugin: actionsOnlyPlugin }], + }); + const channelResolution = await importChannelResolution("actions-only-plugin"); + + expect( + channelResolution.resolveOutboundChannelPlugin({ + channel: "alpha", + cfg: { channels: {} } as never, + allowBootstrap: true, + }), + ).toBeUndefined(); + expect(resolveRuntimePluginRegistryMock).toHaveBeenCalledTimes(1); + }); + + it("prefers an active runtime plugin over a loaded setup shell", async () => { + const setupPlugin = { id: "alpha" }; + const runtimePlugin = { id: "alpha", outbound: { sendText: vi.fn() } }; + getLoadedChannelPluginMock.mockReturnValue(setupPlugin); + getChannelPluginMock.mockReturnValue(undefined); + getActivePluginRegistryMock.mockReturnValue({ + channels: [{ plugin: runtimePlugin }], + }); + getActivePluginChannelRegistryMock.mockReturnValue({ + channels: [{ plugin: setupPlugin }], + }); + const channelResolution = await importChannelResolution("active-runtime-over-setup"); + + expect( + channelResolution.resolveOutboundChannelPlugin({ + channel: "alpha", + cfg: { channels: {} } as never, + allowBootstrap: true, + }), + ).toBe(runtimePlugin); + expect(resolveRuntimePluginRegistryMock).not.toHaveBeenCalled(); + }); + + it("resolves outbound plugins from the selected runtime channel registry", async () => { + const setupPlugin = { id: "alpha" }; + const runtimePlugin = { id: "alpha", outbound: { sendText: vi.fn() } }; + getLoadedChannelPluginMock.mockReturnValue(undefined); + getChannelPluginMock.mockReturnValue(undefined); + getActivePluginRegistryMock.mockReturnValue({ + channels: [{ plugin: setupPlugin }], + }); + getActivePluginChannelRegistryMock.mockReturnValue({ + channels: [{ plugin: runtimePlugin }], + }); + const channelResolution = await importChannelResolution("selected-runtime-registry"); + + expect( + channelResolution.resolveOutboundChannelPlugin({ + channel: "alpha", + cfg: { channels: {} } as never, + allowBootstrap: true, + }), + ).toBe(runtimePlugin); + expect(resolveRuntimePluginRegistryMock).not.toHaveBeenCalled(); + }); + + it("resolves runtime outbound adapters that do not send text directly", async () => { + const setupPlugin = { id: "alpha" }; + const runtimePlugin = { id: "alpha", outbound: { deliveryMode: "gateway" } }; + getLoadedChannelPluginMock.mockReturnValue(setupPlugin); + getChannelPluginMock.mockReturnValue(undefined); + getActivePluginRegistryMock.mockReturnValue({ + channels: [{ plugin: runtimePlugin }], + }); + getActivePluginChannelRegistryMock.mockReturnValue({ + channels: [{ plugin: setupPlugin }], + }); + const channelResolution = await importChannelResolution("runtime-outbound-adapter"); + + expect( + channelResolution.resolveOutboundChannelPlugin({ + channel: "alpha", + cfg: { channels: {} } as never, + allowBootstrap: true, + }), + ).toBe(runtimePlugin); + expect(resolveRuntimePluginRegistryMock).not.toHaveBeenCalled(); + }); + + it("keeps setup shells visible for read-only channel lookup", async () => { + const setupPlugin = { id: "alpha" }; + const runtimePlugin = { id: "alpha", outbound: { sendText: vi.fn() } }; + getLoadedChannelPluginMock.mockReturnValue(setupPlugin); + getChannelPluginMock.mockReturnValue(undefined); + getActivePluginRegistryMock.mockReturnValue({ + channels: [{ plugin: runtimePlugin }], + }); + getActivePluginChannelRegistryMock.mockReturnValue({ + channels: [{ plugin: setupPlugin }], + }); + const channelResolution = await importChannelResolution("read-setup-shell"); + + expect( + channelResolution.resolveOutboundChannelPluginForRead({ + channel: "alpha", + }), + ).toBe(setupPlugin); + expect(resolveRuntimePluginRegistryMock).not.toHaveBeenCalled(); + }); + it("attempts activation when the active registry has other channels but not the requested one", async () => { getLoadedChannelPluginMock.mockReturnValue(undefined); getChannelPluginMock.mockReturnValue(undefined); @@ -203,7 +407,7 @@ describe("outbound channel resolution", () => { expect(resolveRuntimePluginRegistryMock).toHaveBeenCalledTimes(1); }); - it("does not retry registry loads after a missing outbound plugin", async () => { + it("retries registry loads after bootstrap does not make the channel send-capable", async () => { getChannelPluginMock.mockReturnValue(undefined); const channelResolution = await importChannelResolution("bootstrap-retry"); @@ -220,7 +424,7 @@ describe("outbound channel resolution", () => { cfg: { channels: {} } as never, allowBootstrap: true, }); - expect(resolveRuntimePluginRegistryMock).toHaveBeenCalledTimes(1); + expect(resolveRuntimePluginRegistryMock).toHaveBeenCalledTimes(2); }); it("allows another activation attempt when the pinned channel registry version changes", async () => { diff --git a/src/infra/outbound/channel-resolution.ts b/src/infra/outbound/channel-resolution.ts index cea80a185721..3bcb6c5a7dc8 100644 --- a/src/infra/outbound/channel-resolution.ts +++ b/src/infra/outbound/channel-resolution.ts @@ -21,7 +21,7 @@ import type { ChannelThreadingAdapter, } from "../../channels/plugins/types.public.js"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; -import { getActivePluginRegistry } from "../../plugins/runtime.js"; +import { getActivePluginChannelRegistry, getActivePluginRegistry } from "../../plugins/runtime.js"; import { isDeliverableMessageChannel, normalizeMessageChannel, @@ -111,12 +111,14 @@ function maybeBootstrapChannelPlugin(params: { bootstrapOutboundChannelPlugin(params); } -function resolveDirectFromActiveRegistry(channel: string): ChannelPlugin | undefined { - const activeRegistry = getActivePluginRegistry(); - if (!activeRegistry) { +function resolveDirectFromRegistry( + registry: ReturnType, + channel: string, +): ChannelPlugin | undefined { + if (!registry) { return undefined; } - for (const entry of activeRegistry.channels) { + for (const entry of registry.channels) { const plugin = entry?.plugin; if (plugin?.id === channel) { return plugin; @@ -125,6 +127,81 @@ function resolveDirectFromActiveRegistry(channel: string): ChannelPlugin | undef return undefined; } +function messageAdapterCanSendText( + message: ChannelMessageAdapterShape | undefined, +): message is ChannelMessageAdapterShape { + return typeof message?.send?.text === "function"; +} + +function resolveSendCapableMessageAdapter( + plugin: ChannelPlugin | undefined, +): ChannelMessageAdapterShape | undefined { + const message = plugin?.message; + return messageAdapterCanSendText(message) ? message : undefined; +} + +function channelPluginHasRuntimeOutboundSurface(plugin: ChannelPlugin | undefined): boolean { + return Boolean(plugin?.outbound ?? resolveSendCapableMessageAdapter(plugin)); +} + +function resolveRuntimeOutboundPlugin(plugin: ChannelPlugin): ChannelPlugin | undefined { + return channelPluginHasRuntimeOutboundSurface(plugin) ? plugin : undefined; +} + +function resolveRuntimeOutboundPluginCandidate(params: { + loaded?: ChannelPlugin; + runtime?: ChannelPlugin; + setupFallback?: ChannelPlugin; + bundled?: ChannelPlugin; + allowSetupShell?: boolean; +}): ChannelPlugin | undefined { + if (channelPluginHasRuntimeOutboundSurface(params.loaded)) { + return params.loaded; + } + if (params.runtime) { + return params.runtime; + } + if (channelPluginHasRuntimeOutboundSurface(params.bundled)) { + return params.bundled; + } + if (params.allowSetupShell) { + return params.loaded ?? params.setupFallback ?? params.bundled; + } + return undefined; +} + +function resolveValueFromRuntimeRegistries( + channel: string, + resolveValue: (plugin: ChannelPlugin) => TValue | undefined, +): TValue | undefined { + const channelRegistry = getActivePluginChannelRegistry(); + const channelPlugin = resolveDirectFromRegistry(channelRegistry, channel); + if (channelPlugin) { + const value = resolveValue(channelPlugin); + if (value !== undefined) { + return value; + } + } + const activeRegistry = getActivePluginRegistry(); + if (activeRegistry && activeRegistry !== channelRegistry) { + const activePlugin = resolveDirectFromRegistry(activeRegistry, channel); + if (activePlugin) { + return resolveValue(activePlugin); + } + } + return undefined; +} + +function resolveDirectFromRuntimeRegistries(channel: string): ChannelPlugin | undefined { + return resolveValueFromRuntimeRegistries(channel, (plugin) => plugin); +} + +function resolveRuntimeOutboundPluginFromRuntimeRegistries( + channel: string, +): ChannelPlugin | undefined { + return resolveValueFromRuntimeRegistries(channel, resolveRuntimeOutboundPlugin); +} + function toOutboundChannelRuntime(plugin: ChannelPlugin): OutboundChannelRuntime { return { id: plugin.id, @@ -191,17 +268,18 @@ export function resolveOutboundChannelPlugin(params: { const resolveLoaded = () => getLoadedChannelPlugin(normalized); const resolve = () => getChannelPlugin(normalized); const current = resolveLoaded(); - if (current) { - return current; - } - const directCurrent = resolveDirectFromActiveRegistry(normalized); - if (directCurrent) { - return directCurrent; - } - + const runtimeCurrent = resolveRuntimeOutboundPluginFromRuntimeRegistries(normalized); + const setupFallback = resolveDirectFromRuntimeRegistries(normalized); const bundledCurrent = resolve(); - if (bundledCurrent) { - return bundledCurrent; + const candidate = resolveRuntimeOutboundPluginCandidate({ + loaded: current, + runtime: runtimeCurrent, + setupFallback, + bundled: bundledCurrent, + allowSetupShell: params.allowBootstrap !== true, + }); + if (candidate) { + return candidate; } if (params.allowBootstrap !== true) { @@ -209,7 +287,12 @@ export function resolveOutboundChannelPlugin(params: { } maybeBootstrapChannelPlugin({ channel: normalized, cfg: params.cfg }); - return resolveLoaded() ?? resolveDirectFromActiveRegistry(normalized) ?? resolve(); + return resolveRuntimeOutboundPluginCandidate({ + loaded: resolveLoaded(), + runtime: resolveRuntimeOutboundPluginFromRuntimeRegistries(normalized), + setupFallback: resolveDirectFromRuntimeRegistries(normalized), + bundled: resolve(), + }); } /** Resolves the message adapter for a deliverable outbound channel. */ @@ -218,7 +301,23 @@ export function resolveOutboundChannelMessageAdapter(params: { cfg?: OpenClawConfig; allowBootstrap?: boolean; }): ChannelMessageAdapterShape | undefined { - return resolveOutboundChannelPlugin(params)?.message; + const normalized = normalizeDeliverableOutboundChannel(params.channel); + if (!normalized) { + return undefined; + } + const current = + resolveSendCapableMessageAdapter(getLoadedChannelPlugin(normalized)) ?? + resolveValueFromRuntimeRegistries(normalized, resolveSendCapableMessageAdapter) ?? + resolveSendCapableMessageAdapter(getChannelPlugin(normalized)); + if (current || params.allowBootstrap !== true) { + return current; + } + maybeBootstrapChannelPlugin({ channel: normalized, cfg: params.cfg }); + return ( + resolveSendCapableMessageAdapter(getLoadedChannelPlugin(normalized)) ?? + resolveValueFromRuntimeRegistries(normalized, resolveSendCapableMessageAdapter) ?? + resolveSendCapableMessageAdapter(getChannelPlugin(normalized)) + ); } /** Resolves a channel plugin for read-only metadata paths. */ @@ -235,7 +334,7 @@ export function resolveOutboundChannelPluginForRead(params: { if (current) { return current; } - const directCurrent = resolveDirectFromActiveRegistry(normalized); + const directCurrent = resolveDirectFromRuntimeRegistries(normalized); if (directCurrent) { return directCurrent; } @@ -244,7 +343,7 @@ export function resolveOutboundChannelPluginForRead(params: { maybeBootstrapChannelPlugin({ channel: deliverable, cfg: params.cfg }); return ( getLoadedChannelPlugin(deliverable) ?? - resolveDirectFromActiveRegistry(deliverable) ?? + resolveDirectFromRuntimeRegistries(deliverable) ?? getChannelPlugin(deliverable) ); } @@ -270,6 +369,6 @@ export function resolveLoadedOutboundChannelPluginForRead(params: { } return ( getLoadedChannelPlugin(normalized as Parameters[0]) ?? - resolveDirectFromActiveRegistry(normalized) + resolveDirectFromRuntimeRegistries(normalized) ); }