fix(outbound): preserve channel registry during provider snapshots

This commit is contained in:
Peter Steinberger
2026-03-15 21:45:09 -07:00
parent 7a6be3d531
commit 0c2ae71366
8 changed files with 48 additions and 10 deletions

View File

@@ -18,7 +18,10 @@ import {
pickGatewaySelfPresence,
resolveGatewayProbeAuthResolution,
} from "./status.gateway-probe.js";
import type { buildChannelsTable, collectChannelStatusIssues } from "./status.scan.runtime.js";
import type {
buildChannelsTable as buildChannelsTableFn,
collectChannelStatusIssues as collectChannelStatusIssuesFn,
} from "./status.scan.runtime.js";
import { getStatusSummary } from "./status.summary.js";
import { getUpdateCheckResult } from "./status.update.js";
@@ -164,9 +167,9 @@ export type StatusScanResult = {
gatewayProbe: Awaited<ReturnType<typeof probeGateway>> | null;
gatewayReachable: boolean;
gatewaySelf: ReturnType<typeof pickGatewaySelfPresence>;
channelIssues: ChannelStatusIssues;
channelIssues: ReturnType<typeof collectChannelStatusIssuesFn>;
agentStatus: Awaited<ReturnType<typeof getAgentLocalStatuses>>;
channels: ChannelsTable;
channels: Awaited<ReturnType<typeof buildChannelsTableFn>>;
summary: Awaited<ReturnType<typeof getStatusSummary>>;
memory: MemoryStatusSnapshot | null;
memoryPlugin: MemoryPluginStatus;

View File

@@ -133,6 +133,23 @@ describe("outbound channel resolution", () => {
expect(loadOpenClawPluginsMock).toHaveBeenCalledTimes(1);
});
it("bootstraps when the active registry has other channels but not the requested one", async () => {
const plugin = { id: "telegram" };
getChannelPluginMock.mockReturnValueOnce(undefined).mockReturnValueOnce(plugin);
getActivePluginRegistryMock.mockReturnValue({
channels: [{ plugin: { id: "discord" } }],
});
const channelResolution = await importChannelResolution("bootstrap-missing-target");
expect(
channelResolution.resolveOutboundChannelPlugin({
channel: "telegram",
cfg: { channels: {} } as never,
}),
).toBe(plugin);
expect(loadOpenClawPluginsMock).toHaveBeenCalledTimes(1);
});
it("retries bootstrap after a transient load failure", async () => {
getChannelPluginMock.mockReturnValue(undefined);
loadOpenClawPluginsMock.mockImplementationOnce(() => {

View File

@@ -33,7 +33,10 @@ function maybeBootstrapChannelPlugin(params: {
}
const activeRegistry = getActivePluginRegistry();
if ((activeRegistry?.channels?.length ?? 0) > 0) {
const activeHasRequestedChannel = activeRegistry?.channels?.some(
(entry) => entry?.plugin?.id === params.channel,
);
if (activeHasRequestedChannel) {
return;
}

View File

@@ -34,6 +34,7 @@ import { createSubsystemLogger } from "../../logging/subsystem.js";
import { getAgentScopedMediaLocalRoots } from "../../media/local-roots.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";
@@ -113,6 +114,13 @@ 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);
const handler = createPluginHandler({ ...params, outbound });
if (!handler) {

View File

@@ -38,6 +38,8 @@ describe("resolvePluginProviders", () => {
expect.objectContaining({
workspaceDir: "/workspace/explicit",
env,
cache: false,
activate: false,
}),
);
});
@@ -59,6 +61,8 @@ describe("resolvePluginProviders", () => {
allow: expect.arrayContaining(["openrouter", "google", "kilocode", "moonshot"]),
}),
}),
cache: false,
activate: false,
}),
);
});
@@ -76,6 +80,8 @@ describe("resolvePluginProviders", () => {
allow: expect.arrayContaining(["openai", "moonshot", "zai"]),
}),
}),
cache: false,
activate: false,
}),
);
});

View File

@@ -35,6 +35,7 @@ const BUNDLED_PROVIDER_ALLOWLIST_COMPAT_PLUGIN_IDS = [
"venice",
"vercel-ai-gateway",
"volcengine",
"xai",
"vllm",
"xiaomi",
"zai",
@@ -142,8 +143,8 @@ export function resolvePluginProviders(params: {
workspaceDir: params.workspaceDir,
env: params.env,
onlyPluginIds: params.onlyPluginIds,
activate: params.activate,
cache: params.cache,
cache: params.cache ?? false,
activate: params.activate ?? false,
logger: createPluginLoaderLogger(log),
});