refactor: lazy load outbound channel bootstrap

This commit is contained in:
Shakker
2026-04-01 17:00:38 +01:00
committed by Shakker
parent 4a81771290
commit 4a0905b94b
4 changed files with 85 additions and 56 deletions

View File

@@ -0,0 +1,56 @@
import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../../agents/agent-scope.js";
import type { OpenClawConfig } from "../../config/config.js";
import { applyPluginAutoEnable } from "../../config/plugin-auto-enable.js";
import { resolveRuntimePluginRegistry } from "../../plugins/loader.js";
import {
getActivePluginChannelRegistryVersion,
getActivePluginRegistry,
} from "../../plugins/runtime.js";
import type { DeliverableMessageChannel } from "../../utils/message-channel.js";
const bootstrapAttempts = new Set<string>();
export function resetOutboundChannelBootstrapStateForTests(): void {
bootstrapAttempts.clear();
}
export function bootstrapOutboundChannelPlugin(params: {
channel: DeliverableMessageChannel;
cfg?: OpenClawConfig;
}): void {
const cfg = params.cfg;
if (!cfg) {
return;
}
const activeRegistry = getActivePluginRegistry();
const activeHasRequestedChannel = activeRegistry?.channels?.some(
(entry) => entry?.plugin?.id === params.channel,
);
if (activeHasRequestedChannel) {
return;
}
const attemptKey = `${getActivePluginChannelRegistryVersion()}:${params.channel}`;
if (bootstrapAttempts.has(attemptKey)) {
return;
}
bootstrapAttempts.add(attemptKey);
const autoEnabled = applyPluginAutoEnable({ config: cfg });
const defaultAgentId = resolveDefaultAgentId(autoEnabled.config);
const workspaceDir = resolveAgentWorkspaceDir(autoEnabled.config, defaultAgentId);
try {
resolveRuntimePluginRegistry({
config: autoEnabled.config,
activationSourceConfig: cfg,
autoEnabledReasons: autoEnabled.autoEnabledReasons,
workspaceDir,
runtimeOptions: {
allowGatewaySubagentBinding: true,
},
});
} catch {
bootstrapAttempts.delete(attemptKey);
}
}

View File

@@ -61,7 +61,7 @@ function expectBootstrapArgs() {
}
describe("outbound channel resolution", () => {
beforeEach(() => {
beforeEach(async () => {
resolveDefaultAgentIdMock.mockReset();
resolveAgentWorkspaceDirMock.mockReset();
getChannelPluginMock.mockReset();
@@ -86,6 +86,9 @@ describe("outbound channel resolution", () => {
});
resolveDefaultAgentIdMock.mockReturnValue("main");
resolveAgentWorkspaceDirMock.mockReturnValue("/tmp/workspace");
const channelResolution = await importChannelResolution("reset");
channelResolution.resetOutboundChannelResolutionStateForTest();
});
it.each([

View File

@@ -1,23 +1,19 @@
import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../../agents/agent-scope.js";
import { getChannelPlugin } from "../../channels/plugins/index.js";
import type { ChannelPlugin } from "../../channels/plugins/types.js";
import type { OpenClawConfig } from "../../config/config.js";
import { applyPluginAutoEnable } from "../../config/plugin-auto-enable.js";
import { resolveRuntimePluginRegistry } from "../../plugins/loader.js";
import {
getActivePluginRegistry,
getActivePluginChannelRegistryVersion,
} from "../../plugins/runtime.js";
import { getActivePluginRegistry } from "../../plugins/runtime.js";
import {
isDeliverableMessageChannel,
normalizeMessageChannel,
type DeliverableMessageChannel,
} from "../../utils/message-channel.js";
const bootstrapAttempts = new Set<string>();
import {
bootstrapOutboundChannelPlugin,
resetOutboundChannelBootstrapStateForTests,
} from "./channel-bootstrap.runtime.js";
export function resetOutboundChannelResolutionStateForTest(): void {
bootstrapAttempts.clear();
resetOutboundChannelBootstrapStateForTests();
}
export function normalizeDeliverableOutboundChannel(
@@ -34,42 +30,7 @@ function maybeBootstrapChannelPlugin(params: {
channel: DeliverableMessageChannel;
cfg?: OpenClawConfig;
}): void {
const cfg = params.cfg;
if (!cfg) {
return;
}
const activeRegistry = getActivePluginRegistry();
const activeHasRequestedChannel = activeRegistry?.channels?.some(
(entry) => entry?.plugin?.id === params.channel,
);
if (activeHasRequestedChannel) {
return;
}
const attemptKey = `${getActivePluginChannelRegistryVersion()}:${params.channel}`;
if (bootstrapAttempts.has(attemptKey)) {
return;
}
bootstrapAttempts.add(attemptKey);
const autoEnabled = applyPluginAutoEnable({ config: cfg });
const defaultAgentId = resolveDefaultAgentId(autoEnabled.config);
const workspaceDir = resolveAgentWorkspaceDir(autoEnabled.config, defaultAgentId);
try {
resolveRuntimePluginRegistry({
config: autoEnabled.config,
activationSourceConfig: cfg,
autoEnabledReasons: autoEnabled.autoEnabledReasons,
workspaceDir,
runtimeOptions: {
allowGatewaySubagentBinding: true,
},
});
} catch {
// Allow a follow-up resolution attempt if bootstrap failed transiently.
bootstrapAttempts.delete(attemptKey);
}
bootstrapOutboundChannelPlugin(params);
}
function resolveDirectFromActiveRegistry(

View File

@@ -30,7 +30,6 @@ import type { OutboundMediaAccess } from "../../media/load-options.js";
import { resolveAgentScopedOutboundMediaAccess } from "../../media/read-capability.js";
import { getGlobalHookRunner } from "../../plugins/hook-runner-global.js";
import { throwIfAborted } from "./abort.js";
import { resolveOutboundChannelPlugin } from "./channel-resolution.js";
import { ackDelivery, enqueueDelivery, failDelivery } from "./delivery-queue.js";
import type { OutboundIdentity } from "./identity.js";
import type { DeliveryMirror } from "./mirror.js";
@@ -55,6 +54,15 @@ async function loadTranscriptRuntime() {
return await transcriptRuntimePromise;
}
let channelBootstrapRuntimePromise:
| Promise<typeof import("./channel-bootstrap.runtime.js")>
| undefined;
async function loadChannelBootstrapRuntime() {
channelBootstrapRuntimePromise ??= import("./channel-bootstrap.runtime.js");
return await channelBootstrapRuntimePromise;
}
export type OutboundDeliveryResult = {
channel: Exclude<OutboundChannel, "none">;
messageId: string;
@@ -141,14 +149,15 @@ type ChannelHandlerParams = {
// Channel docking: outbound delivery delegates to plugin.outbound adapters.
async function createChannelHandler(params: ChannelHandlerParams): Promise<ChannelHandler> {
// Recover channel plugins the same way target resolution does so direct cron
// delivery still works when a prior test or lazy path left the active plugin
// registry empty.
resolveOutboundChannelPlugin({
channel: params.channel,
cfg: params.cfg,
});
const outbound = await loadChannelOutboundAdapter(params.channel);
let outbound = await loadChannelOutboundAdapter(params.channel);
if (!outbound) {
const { bootstrapOutboundChannelPlugin } = await loadChannelBootstrapRuntime();
bootstrapOutboundChannelPlugin({
channel: params.channel,
cfg: params.cfg,
});
outbound = await loadChannelOutboundAdapter(params.channel);
}
const handler = createPluginHandler({ ...params, outbound });
if (!handler) {
throw new Error(`Outbound not configured for channel: ${params.channel}`);