fix(plugins): forward setChannelRuntime from non-bundled external setup entries#77799
Conversation
|
Codex review: needs maintainer review before merge. Summary Reproducibility: yes. at source level: current main drops Real behavior proof Next step before merge Security Review detailsBest possible solution: Land this narrow loader, test, and changelog fix after maintainer review and required checks, then let it close #77779. Do we have a high-confidence way to reproduce the issue? Yes, at source level: current main drops Is this the best way to solve the issue? Yes: forwarding the already-supported runtime setter through the plain setup-object path is the narrowest maintainable fix and matches the existing bundled setup-entry behavior. The added regression test covers the configured deferred channel Phase 1 path. Acceptance criteria:
What I checked:
Likely related people:
Remaining risk / open question:
Codex review notes: model gpt-5.5, reasoning high; reviewed against f2458d8828de. Re-review progress:
|
ed4b199 to
830c46c
Compare
…up entries
resolveSetupChannelRegistration handled the non-bundled setup-entry format
({plugin, setChannelRuntime}) by only extracting `plugin`, silently dropping
`setChannelRuntime`.
Root cause: in setup-runtime mode (Phase 1 of deferred gateway startup),
registerChannel is always active (runtimeChannel=true) and writes the channel
plugin into registry.channels immediately. This means the channel provider
starts in Phase 1, before Phase 2's register() call. Any runtime initializer
the provider polls for (e.g. waitForWeixinRuntime) must therefore be set via
setChannelRuntime in the setup entry — it cannot wait for Phase 2.
For external plugins using the plain-object setup entry format the setter was
silently discarded, leaving the runtime uninitialized when the provider started.
waitForWeixinRuntime() would time out after 10 s and the gateway entered a
crash loop. Phase 2 eventually ran register() with a valid api.runtime but by
then the channel had already exited. Fixes openclaw#77779.
Mirror the existing bundled-entry handling: extract setChannelRuntime from the
non-bundled path and include it in the return value so loader.ts:2218 can
invoke it before the channel is registered.
Regression test covers the exact failure path: configured channel with
startupDeferConfiguredChannelFullLoadUntilAfterListen and
preferSetupRuntimeForChannelPlugins, non-bundled setup entry exporting
{plugin, setChannelRuntime}. Asserts both that the setter is invoked and that
the channel lands in registry.channels in Phase 1 (confirming the provider
would start before Phase 2).
830c46c to
7b7676b
Compare
|
Merged via squash.
Thanks @openperf! |
|
Landed in 42a3229. Fix is minimal and surgical — adds |
…up entries (openclaw#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
…up entries (openclaw#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
Root cause
resolveSetupChannelRegistrationinsrc/plugins/loader-channel-setup.tshandles two setup-entry export formats. The bundled-contract path ({kind:"bundled-channel-setup-entry", loadSetupPlugin, setChannelRuntime}) correctly extracted and returnedsetChannelRuntime. The non-bundled plain-object path ({plugin, setChannelRuntime}) only extractedpluginand silently droppedsetChannelRuntime.The consequence is specific to how setup-runtime mode works:
registerChannelis always active inregistry.ts(exposed unconditionally, not gated oncapabilityHandlers), and withruntimeChannel=truein setup-runtime mode it writes the plugin intoregistry.channelsimmediately. The channel provider therefore starts during Phase 1 (before gateway listen), not Phase 2. Any runtime initializer the provider polls for must be satisfied viasetChannelRuntimein the setup entry — it cannot wait for Phase 2'sregister()call.Without this fix:
setChannelRuntimeis dropped → runtime initializer never called →pluginRuntime = nullregistry.channelsin Phase 1 → provider starts immediatelyregister()with a valid runtime but the channel has already exitedWith this fix:
setChannelRuntime(api.runtime)is invoked in Phase 1, the runtime is set before the provider starts, and the poll returns immediately.What changed
src/plugins/loader-channel-setup.ts: extend the non-bundled return path to also extractsetChannelRuntimewhen it is a function, mirroring the existing bundled-contract path.src/plugins/loader.test.ts: regression test covering the exact failure path — configured channel withstartupDeferConfiguredChannelFullLoadUntilAfterListenandpreferSetupRuntimeForChannelPlugins: true, non-bundled setup entry exporting{plugin, setChannelRuntime}. Asserts both that the setter is invoked and that the channel lands inregistry.channelsin Phase 1, confirming the provider starts before Phase 2.CHANGELOG.md: added entry under## Unreleased.What did NOT change
loader.ts:2218invocation site is unchanged.reloadDeferredGatewayPlugins/register()flow.Risk / Mitigation
Low. The change adds extraction of a field that was previously silently ignored. Plugins that do not export
setChannelRuntimeare unaffected. The regression test exercises the exact failure mode and fails without the fix.Linked Issue/PR
Fixes #77779