Skip to content

Commit 42a3229

Browse files
authored
fix(plugins): forward setChannelRuntime from non-bundled external setup entries (#77799)
Merged via squash. Prepared head SHA: 7b7676b Co-authored-by: openperf <80630709+openperf@users.noreply.github.com> Co-authored-by: openperf <80630709+openperf@users.noreply.github.com> Reviewed-by: @openperf
1 parent 1c331a8 commit 42a3229

3 files changed

Lines changed: 132 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -481,6 +481,7 @@ Docs: https://docs.openclaw.ai
481481
- Dependencies: bump transitive `basic-ftp` to 5.3.1 so the runtime lockfile no longer includes the vulnerable 5.3.0 build flagged by the production dependency audit. (#78637) Thanks @sallyom.
482482
- Agents/compaction: clamp compaction summary reserve tokens to each model's output limit so high-context compaction no longer requests invalid `max_tokens` values. (#54392) Thanks @adzendo.
483483
- Agents/subagents: have completed session-mode subagent registry rows honor `agents.defaults.subagents.archiveAfterMinutes` (default 60 minutes; same knob run-mode already uses for `archiveAtMs`) instead of a hardcoded 5-minute TTL, so `subagents list` and other registry-backed surfaces still show recently-completed runs and operators have one consistent retention knob across spawn modes. (#78263) Thanks @arniesaha.
484+
- Plugins/channel setup: fix `setChannelRuntime` being silently dropped from non-bundled external plugin setup entries — external channel plugins that export `{ plugin, setChannelRuntime }` from their setup entry now have the runtime setter invoked, so the runtime initializer the provider polls for is set before the channel starts, preventing a poll timeout and gateway crash loop when the plugin opts into deferred startup loading. Fixes #77779. (#77799) Thanks @openperf.
484485

485486
## 2026.5.3-1
486487

src/plugins/loader-channel-setup.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,12 +174,18 @@ export function resolveSetupChannelRegistration(moduleExport: unknown): {
174174
}
175175
const setup = resolved as {
176176
plugin?: unknown;
177+
setChannelRuntime?: unknown;
177178
};
178179
if (!setup.plugin || typeof setup.plugin !== "object") {
179180
return {};
180181
}
181182
return {
182183
plugin: setup.plugin as ChannelPlugin,
184+
...(typeof setup.setChannelRuntime === "function"
185+
? {
186+
setChannelRuntime: setup.setChannelRuntime as (runtime: PluginRuntime) => void,
187+
}
188+
: {}),
183189
};
184190
}
185191

src/plugins/loader.test.ts

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5080,6 +5080,131 @@ module.exports = {
50805080
expect(fs.existsSync(runtimeMarker)).toBe(false);
50815081
});
50825082

5083+
it("invokes setChannelRuntime from a non-bundled setup entry for a configured deferred channel during startup Phase 1", () => {
5084+
// Regression test for #77779. When a configured external channel plugin opts into
5085+
// deferred full loading (startupDeferConfiguredChannelFullLoadUntilAfterListen) the
5086+
// loader runs in setup-runtime mode during Phase 1 (before gateway listen). In that
5087+
// phase api.registerChannel is active and writes the plugin into registry.channels,
5088+
// so the channel provider starts immediately — before Phase 2 runs register(). Any
5089+
// runtime initializer (e.g. setWeixinRuntime) that the provider polls for must
5090+
// therefore be invoked via setChannelRuntime in the setup entry. Before this fix,
5091+
// resolveSetupChannelRegistration silently dropped setChannelRuntime from non-bundled
5092+
// {plugin, setChannelRuntime} exports, leaving the runtime unset and causing
5093+
// waitForWeixinRuntime() to time out.
5094+
useNoBundledPlugins();
5095+
const pluginDir = makeTempDir();
5096+
const runtimeMarker = path.join(makeTempDir(), "deferred-configured-setup-runtime-applied.txt");
5097+
5098+
fs.writeFileSync(
5099+
path.join(pluginDir, "package.json"),
5100+
JSON.stringify(
5101+
{
5102+
name: "@openclaw/non-bundled-deferred-setup-runtime-test",
5103+
openclaw: {
5104+
extensions: ["./index.cjs"],
5105+
setupEntry: "./setup-entry.cjs",
5106+
startup: {
5107+
deferConfiguredChannelFullLoadUntilAfterListen: true,
5108+
},
5109+
},
5110+
},
5111+
null,
5112+
2,
5113+
),
5114+
"utf-8",
5115+
);
5116+
fs.writeFileSync(
5117+
path.join(pluginDir, "openclaw.plugin.json"),
5118+
JSON.stringify(
5119+
{
5120+
id: "non-bundled-deferred-setup-runtime-test",
5121+
configSchema: { type: "object", properties: {} },
5122+
channels: ["non-bundled-deferred-setup-runtime-test"],
5123+
},
5124+
null,
5125+
2,
5126+
),
5127+
"utf-8",
5128+
);
5129+
fs.writeFileSync(
5130+
path.join(pluginDir, "index.cjs"),
5131+
`module.exports = {
5132+
id: "non-bundled-deferred-setup-runtime-test",
5133+
register(api) {
5134+
api.registerChannel({
5135+
plugin: {
5136+
id: "non-bundled-deferred-setup-runtime-test",
5137+
meta: {
5138+
id: "non-bundled-deferred-setup-runtime-test",
5139+
label: "Non-Bundled Deferred Setup Runtime Test",
5140+
selectionLabel: "Non-Bundled Deferred Setup Runtime Test",
5141+
docsPath: "/channels/non-bundled-deferred-setup-runtime-test",
5142+
blurb: "full channel entry",
5143+
},
5144+
capabilities: { chatTypes: ["direct"] },
5145+
config: { listAccountIds: () => ["default"], resolveAccount: () => ({ accountId: "default", token: "configured" }) },
5146+
outbound: { deliveryMode: "direct" },
5147+
},
5148+
});
5149+
},
5150+
};`,
5151+
"utf-8",
5152+
);
5153+
fs.writeFileSync(
5154+
path.join(pluginDir, "setup-entry.cjs"),
5155+
`module.exports = {
5156+
plugin: {
5157+
id: "non-bundled-deferred-setup-runtime-test",
5158+
meta: {
5159+
id: "non-bundled-deferred-setup-runtime-test",
5160+
label: "Non-Bundled Deferred Setup Runtime Test",
5161+
selectionLabel: "Non-Bundled Deferred Setup Runtime Test",
5162+
docsPath: "/channels/non-bundled-deferred-setup-runtime-test",
5163+
blurb: "setup entry",
5164+
},
5165+
capabilities: { chatTypes: ["direct"] },
5166+
config: { listAccountIds: () => ["default"], resolveAccount: () => ({ accountId: "default", token: "configured" }) },
5167+
outbound: { deliveryMode: "direct" },
5168+
},
5169+
setChannelRuntime: () => {
5170+
require("node:fs").writeFileSync(${JSON.stringify(runtimeMarker)}, "applied", "utf-8");
5171+
},
5172+
};`,
5173+
"utf-8",
5174+
);
5175+
5176+
// Phase 1: preferSetupRuntimeForChannelPlugins=true simulates gateway startup when
5177+
// at least one deferred configured channel plugin is present. The configured channel
5178+
// opts into deferral, so setup-runtime mode is used. setup-entry.cjs is loaded and
5179+
// setChannelRuntime must be invoked. The channel is also written into registry.channels
5180+
// (runtimeChannel=true in setup-runtime), so the provider starts in Phase 1.
5181+
const registry = loadOpenClawPlugins({
5182+
cache: false,
5183+
preferSetupRuntimeForChannelPlugins: true,
5184+
config: {
5185+
channels: {
5186+
"non-bundled-deferred-setup-runtime-test": {
5187+
enabled: true,
5188+
token: "configured",
5189+
},
5190+
},
5191+
plugins: {
5192+
load: { paths: [pluginDir] },
5193+
allow: ["non-bundled-deferred-setup-runtime-test"],
5194+
},
5195+
},
5196+
});
5197+
5198+
// setChannelRuntime must have been called so that any runtime initializer the
5199+
// provider polls for (e.g. waitForWeixinRuntime) is satisfied before the provider
5200+
// times out.
5201+
expect(fs.existsSync(runtimeMarker)).toBe(true);
5202+
// The channel is registered in registry.channels during Phase 1 (not deferred to
5203+
// Phase 2), confirming the provider would start and need setChannelRuntime.
5204+
expect(registry.channels).toHaveLength(1);
5205+
expect(registry.channels[0]?.plugin.id).toBe("non-bundled-deferred-setup-runtime-test");
5206+
});
5207+
50835208
it("isolates loadSetupPlugin errors as per-plugin diagnostics instead of crashing registry load", () => {
50845209
useNoBundledPlugins();
50855210
const pluginDir = makeTempDir();

0 commit comments

Comments
 (0)