Skip to content

fix(plugins): forward setChannelRuntime from non-bundled external setup entries#77799

Merged
openperf merged 1 commit intoopenclaw:mainfrom
openperf:fix/77779-external-plugin-setup-entry-set-channel-runtime
May 7, 2026
Merged

fix(plugins): forward setChannelRuntime from non-bundled external setup entries#77799
openperf merged 1 commit intoopenclaw:mainfrom
openperf:fix/77779-external-plugin-setup-entry-set-channel-runtime

Conversation

@openperf
Copy link
Copy Markdown
Member

@openperf openperf commented May 5, 2026

Root cause

resolveSetupChannelRegistration in src/plugins/loader-channel-setup.ts handles two setup-entry export formats. The bundled-contract path ({kind:"bundled-channel-setup-entry", loadSetupPlugin, setChannelRuntime}) correctly extracted and returned setChannelRuntime. The non-bundled plain-object path ({plugin, setChannelRuntime}) only extracted plugin and silently dropped setChannelRuntime.

The consequence is specific to how setup-runtime mode works: registerChannel is always active in registry.ts (exposed unconditionally, not gated on capabilityHandlers), and with runtimeChannel=true in setup-runtime mode it writes the plugin into registry.channels immediately. The channel provider therefore starts during Phase 1 (before gateway listen), not Phase 2. Any runtime initializer the provider polls for must be satisfied via setChannelRuntime in the setup entry — it cannot wait for Phase 2's register() call.

Without this fix:

  1. Phase 1 loads the setup entry → setChannelRuntime is dropped → runtime initializer never called → pluginRuntime = null
  2. The channel is written into registry.channels in Phase 1 → provider starts immediately
  3. Provider polls for runtime, finds it null, times out → gateway crash loop
  4. Phase 2 eventually calls register() with a valid runtime but the channel has already exited

With 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 extract setChannelRuntime when it is a function, mirroring the existing bundled-contract path.
  • src/plugins/loader.test.ts: regression test covering the exact failure path — configured channel with startupDeferConfiguredChannelFullLoadUntilAfterListen and preferSetupRuntimeForChannelPlugins: true, 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 starts before Phase 2.
  • CHANGELOG.md: added entry under ## Unreleased.

What did NOT change

  • The bundled setup-entry path is unchanged.
  • loader.ts:2218 invocation site is unchanged.
  • No changes to the Phase 2 / reloadDeferredGatewayPlugins / register() flow.

Risk / Mitigation

Low. The change adds extraction of a field that was previously silently ignored. Plugins that do not export setChannelRuntime are unaffected. The regression test exercises the exact failure mode and fails without the fix.

Linked Issue/PR

Fixes #77779

@openperf openperf requested a review from a team as a code owner May 5, 2026 10:26
@openclaw-barnacle openclaw-barnacle Bot added size: S maintainer Maintainer-authored PR labels May 5, 2026
@clawsweeper
Copy link
Copy Markdown
Contributor

clawsweeper Bot commented May 5, 2026

Codex review: needs maintainer review before merge.

Summary
The PR forwards setChannelRuntime from non-bundled plain setup-entry exports, adds a deferred configured external-channel regression test, and adds a changelog entry.

Reproducibility: yes. at source level: current main drops setChannelRuntime from the non-bundled { plugin, setChannelRuntime } setup-entry path while the setup-runtime loader later invokes the preserved setter before channel registration. I did not run the regression test or a live WeChat gateway in this read-only review.

Real behavior proof
Not applicable: This is a MEMBER-authored PR with the protected maintainer label, so the external-contributor real-behavior-proof gate does not apply.

Next step before merge
Protected maintainer PR already contains the narrow code, test, and changelog fix; the remaining action is maintainer review plus required checks, not an automated repair.

Security
Cleared: The diff forwards an existing plugin-loader field and adds test/changelog coverage without dependency, workflow, secret, artifact, package-resolution, or broader code-execution changes.

Review details

Best 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 setChannelRuntime from the non-bundled { plugin, setChannelRuntime } setup-entry path while the setup-runtime loader later invokes the preserved setter before channel registration. I did not run the regression test or a live WeChat gateway in this read-only review.

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:

  • pnpm test src/plugins/loader.test.ts
  • pnpm check:changed
  • Required GitHub checks for 830c46c6d81533bdbbd2725aeaefe9c8290abd5e

What I checked:

  • Protected PR context: Live PR API data shows the PR is open, authored by openperf with author_association: MEMBER, labeled maintainer, and currently targets head 830c46c6d81533bdbbd2725aeaefe9c8290abd5e onto current main f2458d8828de71997ffa290aa2dc6e1177d06b57. (830c46c6d815)
  • Current main drops the plain setup setter: resolveSetupChannelRegistration preserves setChannelRuntime for bundled setup entries, but the current non-bundled plain-object path only reads and returns plugin, so { plugin, setChannelRuntime } loses the setter. (src/plugins/loader-channel-setup.ts:175, f2458d8828de)
  • Loader depends on the preserved setter: The setup-runtime loader invokes mergedSetupRegistration.setChannelRuntime?.(api.runtime) before api.registerChannel(mergedSetupPlugin), making the dropped field the direct missing link for setup-entry runtime initialization. (src/plugins/loader.ts:2215, f2458d8828de)
  • Setup-runtime channels register in Phase 1: Registry capability decoding keeps runtimeChannel true for every mode except setup-only and tool-discovery, so setup-runtime channel registration can populate registry.channels before the later full registration pass. (src/plugins/registry.ts:330, f2458d8828de)
  • Patch forwards the missing field: The PR adds setChannelRuntime?: unknown to the non-bundled setup object and returns it when it is a function, mirroring the bundled setup-entry branch. (src/plugins/loader-channel-setup.ts:174, 830c46c6d815)
  • Regression test targets the reported failure path: The PR adds a loader test for a configured external channel with deferred full loading and preferSetupRuntimeForChannelPlugins: true, asserting the setup-entry setter runs and the channel lands in registry.channels during Phase 1. (src/plugins/loader.test.ts:5080, 830c46c6d815)

Likely related people:

  • steipete: GitHub path history shows repeated recent maintenance of the central plugin loader, registry, and the split loader runtime helper files involved in setup-runtime behavior. (role: recent loader/runtime maintainer; confidence: high; commits: 4aedffd37aed, 112dedd0939b, 06056926a099; files: src/plugins/loader-channel-setup.ts, src/plugins/loader.ts, src/plugins/registry.ts)
  • amknight: Recent path history ties amknight to bundled channel-entry profiling and setChannelRuntime handling in the Plugin SDK contract that this PR mirrors for non-bundled setup entries. (role: adjacent Plugin SDK channel-entry owner; confidence: medium; commits: 4407df6c0304; files: src/plugin-sdk/channel-entry-contract.ts, src/plugin-sdk/channel-entry-contract.test.ts, src/plugins/loader.ts)
  • openperf: Beyond authoring this PR, GitHub history shows recent merged work by openperf in plugin runtime-deps handling and external-plugin provider behavior, which is adjacent to this external setup-entry fix. (role: recent adjacent external-plugin maintainer and likely follow-up owner; confidence: medium; commits: 4b98f0952934, 34b67c3f2546; files: src/plugins/bundled-runtime-deps-roots.ts, src/plugins/bundled-runtime-deps.test.ts, src/secrets/runtime-web-tools.shared.ts)

Remaining risk / open question:

  • I did not run the new regression test or a live WeChat gateway in this read-only review.
  • Two latest-head checks were still in progress at inspection time, so merge should wait for required CI to finish.

Codex review notes: model gpt-5.5, reasoning high; reviewed against f2458d8828de.

Re-review progress:

@openperf openperf force-pushed the fix/77779-external-plugin-setup-entry-set-channel-runtime branch 5 times, most recently from ed4b199 to 830c46c Compare May 7, 2026 02:20
…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).
@openperf openperf force-pushed the fix/77779-external-plugin-setup-entry-set-channel-runtime branch from 830c46c to 7b7676b Compare May 7, 2026 02:31
@openperf openperf merged commit 42a3229 into openclaw:main May 7, 2026
102 checks passed
@openperf
Copy link
Copy Markdown
Member Author

openperf commented May 7, 2026

Merged via squash.

Thanks @openperf!

@openperf openperf deleted the fix/77779-external-plugin-setup-entry-set-channel-runtime branch May 7, 2026 02:33
@openperf
Copy link
Copy Markdown
Member Author

openperf commented May 7, 2026

Landed in 42a3229. Fix is minimal and surgical — adds setChannelRuntime?: unknown to the intermediate cast in the non-bundled setup-entry path and uses the same typeof === "function" conditional spread already present in the bundled-contract path. Backward-compatible: plugins without the field are unaffected. Closes #77779.

steipete pushed a commit that referenced this pull request May 7, 2026
…up 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

(cherry picked from commit 42a3229)
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request May 9, 2026
…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
rogerdigital pushed a commit to rogerdigital/openclaw that referenced this pull request May 9, 2026
…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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

maintainer Maintainer-authored PR size: S

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: WeChat plugin fails to start — api.runtime undefined in 2026.5.4 causes runtime initialization timeout

1 participant