perf(plugins): extend discovery threading to loader, manifest registry, installed-index, and config contracts#84258
Conversation
|
Codex review: needs maintainer review before merge. Workflow note: Future ClawSweeper reviews update this same comment in place. How this review workflow works
Summary Reproducibility: Do we have a high-confidence way to reproduce the issue? Yes for the redundant discovery shape: current main source shows independent discoverOpenClawPlugins calls in the affected helpers and a retry loop that calls the bundled runtime loader twice. I did not run the TUI startup profiler in this read-only review. PR rating Rank-up moves:
What the crustacean ranks mean
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics. PR egg Rarity: 🥚 common. What is this egg doing here?
Real behavior proof Risk before merge Maintainer options:
Next step before merge Security Review detailsBest possible solution: Review and land the explicit function-scoped discovery-threading change if maintainers accept the scoped performance proof, then track broader startup costs such as loader discovery, manifest reads, and jiti load paths as follow-up work. Do we have a high-confidence way to reproduce the issue? Do we have a high-confidence way to reproduce the issue? Yes for the redundant discovery shape: current main source shows independent discoverOpenClawPlugins calls in the affected helpers and a retry loop that calls the bundled runtime loader twice. I did not run the TUI startup profiler in this read-only review. Is this the best way to solve the issue? Is this the best way to solve the issue? Yes for the narrow issue: explicit per-flow discovery threading matches the plugin boundary guidance better than a hidden low-level JSON or metadata cache. The remaining startup freeze sources called out in the PR should be handled as separate follow-ups. Label justifications:
Acceptance criteria:
What I checked:
Likely related people:
Codex review notes: model gpt-5.5, reasoning high; reviewed against 9b97e1ef2fd2. |
Existing work in this area — for reviewer awarenessAfter opening this PR I searched for related issues/PRs and found significant existing work that reviewers should consider: Issue #82308 — "[Perf]: Redundant filesystem walks in plugin discovery during startup" (open, May 15 2026)Direct match for the same root cause. The issue states an explicit design constraint which conflicts with the approach in this PR:
A ClawSweeper review on the issue reinforces:
This PR's PR #75451 — "perf: thread explicit plugin discovery through contracts registry" (open, May 1 2026, author @SebTardif)The architecturally-aligned fix for #82308. Threads an optional How these relate to this PR
Other related-looking reports that may share this root cause:
Recommendation for reviewersGiven the design-constraint conflict, I'd defer to maintainers on the right path:
I have a working CPU profile and per-call instrumentation that reproduces the 212K-reads-from-380-paths pattern from the PR description, including breakdowns of which files are re-read most. Happy to share that data if it helps the decision. |
84fb71f to
d6b8f46
Compare
…y, installed-index, and config contracts Follow-up to #75451. Threads optional discovery?: PluginDiscoveryResult through the remaining helpers that still call discoverOpenClawPlugins internally during startup: - loadOpenClawPlugins / loadOpenClawPluginCliRegistry (src/plugins/loader.ts): add discovery? to PluginLoadOptions and consult it before falling back to an internal scan at both call sites. - loadPluginManifestRegistry (src/plugins/manifest-registry.ts): accept discovery? as a more ergonomic alternative to the existing candidates? / diagnostics? pair; candidates? still wins when both are supplied. - resolveInstalledPluginIndexRegistry (src/plugins/installed-plugin-index-registry.ts): add discovery? to LoadInstalledPluginIndexParams and use it when candidates aren't supplied. - resolvePluginConfigContractsById (src/plugins/config-contracts.ts): add discovery? and thread it into the bundled-fallback discovery call. Add discovery-threading.test.ts asserting each entry point skips its internal discoverOpenClawPlugins call when discovery is supplied, calls it when nothing is supplied, and prefers explicit candidates over discovery when both are present (6 tests, all pass). discoverOpenClawPlugins remains stateless; sharing is function-scoped per src/plugins/CLAUDE.md guidance. Backward compatible: every change is additive (new optional param).
d6b8f46 to
1e076ab
Compare
…y, installed-index, and config contracts (openclaw#84258) Follow-up to openclaw#75451. Threads optional discovery?: PluginDiscoveryResult through the remaining helpers that still call discoverOpenClawPlugins internally during startup: - loadOpenClawPlugins / loadOpenClawPluginCliRegistry (src/plugins/loader.ts): add discovery? to PluginLoadOptions and consult it before falling back to an internal scan at both call sites. - loadPluginManifestRegistry (src/plugins/manifest-registry.ts): accept discovery? as a more ergonomic alternative to the existing candidates? / diagnostics? pair; candidates? still wins when both are supplied. - resolveInstalledPluginIndexRegistry (src/plugins/installed-plugin-index-registry.ts): add discovery? to LoadInstalledPluginIndexParams and use it when candidates aren't supplied. - resolvePluginConfigContractsById (src/plugins/config-contracts.ts): add discovery? and thread it into the bundled-fallback discovery call. Add discovery-threading.test.ts asserting each entry point skips its internal discoverOpenClawPlugins call when discovery is supplied, calls it when nothing is supplied, and prefers explicit candidates over discovery when both are present (6 tests, all pass). discoverOpenClawPlugins remains stateless; sharing is function-scoped per src/plugins/CLAUDE.md guidance. Backward compatible: every change is additive (new optional param).
…y, installed-index, and config contracts (openclaw#84258) Follow-up to openclaw#75451. Threads optional discovery?: PluginDiscoveryResult through the remaining helpers that still call discoverOpenClawPlugins internally during startup: - loadOpenClawPlugins / loadOpenClawPluginCliRegistry (src/plugins/loader.ts): add discovery? to PluginLoadOptions and consult it before falling back to an internal scan at both call sites. - loadPluginManifestRegistry (src/plugins/manifest-registry.ts): accept discovery? as a more ergonomic alternative to the existing candidates? / diagnostics? pair; candidates? still wins when both are supplied. - resolveInstalledPluginIndexRegistry (src/plugins/installed-plugin-index-registry.ts): add discovery? to LoadInstalledPluginIndexParams and use it when candidates aren't supplied. - resolvePluginConfigContractsById (src/plugins/config-contracts.ts): add discovery? and thread it into the bundled-fallback discovery call. Add discovery-threading.test.ts asserting each entry point skips its internal discoverOpenClawPlugins call when discovery is supplied, calls it when nothing is supplied, and prefers explicit candidates over discovery when both are present (6 tests, all pass). discoverOpenClawPlugins remains stateless; sharing is function-scoped per src/plugins/CLAUDE.md guidance. Backward compatible: every change is additive (new optional param).
…y, installed-index, and config contracts (openclaw#84258) Follow-up to openclaw#75451. Threads optional discovery?: PluginDiscoveryResult through the remaining helpers that still call discoverOpenClawPlugins internally during startup: - loadOpenClawPlugins / loadOpenClawPluginCliRegistry (src/plugins/loader.ts): add discovery? to PluginLoadOptions and consult it before falling back to an internal scan at both call sites. - loadPluginManifestRegistry (src/plugins/manifest-registry.ts): accept discovery? as a more ergonomic alternative to the existing candidates? / diagnostics? pair; candidates? still wins when both are supplied. - resolveInstalledPluginIndexRegistry (src/plugins/installed-plugin-index-registry.ts): add discovery? to LoadInstalledPluginIndexParams and use it when candidates aren't supplied. - resolvePluginConfigContractsById (src/plugins/config-contracts.ts): add discovery? and thread it into the bundled-fallback discovery call. Add discovery-threading.test.ts asserting each entry point skips its internal discoverOpenClawPlugins call when discovery is supplied, calls it when nothing is supplied, and prefers explicit candidates over discovery when both are present (6 tests, all pass). discoverOpenClawPlugins remains stateless; sharing is function-scoped per src/plugins/CLAUDE.md guidance. Backward compatible: every change is additive (new optional param).
…y, installed-index, and config contracts (openclaw#84258) Follow-up to openclaw#75451. Threads optional discovery?: PluginDiscoveryResult through the remaining helpers that still call discoverOpenClawPlugins internally during startup: - loadOpenClawPlugins / loadOpenClawPluginCliRegistry (src/plugins/loader.ts): add discovery? to PluginLoadOptions and consult it before falling back to an internal scan at both call sites. - loadPluginManifestRegistry (src/plugins/manifest-registry.ts): accept discovery? as a more ergonomic alternative to the existing candidates? / diagnostics? pair; candidates? still wins when both are supplied. - resolveInstalledPluginIndexRegistry (src/plugins/installed-plugin-index-registry.ts): add discovery? to LoadInstalledPluginIndexParams and use it when candidates aren't supplied. - resolvePluginConfigContractsById (src/plugins/config-contracts.ts): add discovery? and thread it into the bundled-fallback discovery call. Add discovery-threading.test.ts asserting each entry point skips its internal discoverOpenClawPlugins call when discovery is supplied, calls it when nothing is supplied, and prefers explicit candidates over discovery when both are present (6 tests, all pass). discoverOpenClawPlugins remains stateless; sharing is function-scoped per src/plugins/CLAUDE.md guidance. Backward compatible: every change is additive (new optional param).
…y, installed-index, and config contracts (openclaw#84258) Follow-up to openclaw#75451. Threads optional discovery?: PluginDiscoveryResult through the remaining helpers that still call discoverOpenClawPlugins internally during startup: - loadOpenClawPlugins / loadOpenClawPluginCliRegistry (src/plugins/loader.ts): add discovery? to PluginLoadOptions and consult it before falling back to an internal scan at both call sites. - loadPluginManifestRegistry (src/plugins/manifest-registry.ts): accept discovery? as a more ergonomic alternative to the existing candidates? / diagnostics? pair; candidates? still wins when both are supplied. - resolveInstalledPluginIndexRegistry (src/plugins/installed-plugin-index-registry.ts): add discovery? to LoadInstalledPluginIndexParams and use it when candidates aren't supplied. - resolvePluginConfigContractsById (src/plugins/config-contracts.ts): add discovery? and thread it into the bundled-fallback discovery call. Add discovery-threading.test.ts asserting each entry point skips its internal discoverOpenClawPlugins call when discovery is supplied, calls it when nothing is supplied, and prefers explicit candidates over discovery when both are present (6 tests, all pass). discoverOpenClawPlugins remains stateless; sharing is function-scoped per src/plugins/CLAUDE.md guidance. Backward compatible: every change is additive (new optional param).
…y, installed-index, and config contracts (openclaw#84258) Follow-up to openclaw#75451. Threads optional discovery?: PluginDiscoveryResult through the remaining helpers that still call discoverOpenClawPlugins internally during startup: - loadOpenClawPlugins / loadOpenClawPluginCliRegistry (src/plugins/loader.ts): add discovery? to PluginLoadOptions and consult it before falling back to an internal scan at both call sites. - loadPluginManifestRegistry (src/plugins/manifest-registry.ts): accept discovery? as a more ergonomic alternative to the existing candidates? / diagnostics? pair; candidates? still wins when both are supplied. - resolveInstalledPluginIndexRegistry (src/plugins/installed-plugin-index-registry.ts): add discovery? to LoadInstalledPluginIndexParams and use it when candidates aren't supplied. - resolvePluginConfigContractsById (src/plugins/config-contracts.ts): add discovery? and thread it into the bundled-fallback discovery call. Add discovery-threading.test.ts asserting each entry point skips its internal discoverOpenClawPlugins call when discovery is supplied, calls it when nothing is supplied, and prefers explicit candidates over discovery when both are present (6 tests, all pass). discoverOpenClawPlugins remains stateless; sharing is function-scoped per src/plugins/CLAUDE.md guidance. Backward compatible: every change is additive (new optional param).
…y, installed-index, and config contracts (openclaw#84258) Follow-up to openclaw#75451. Threads optional discovery?: PluginDiscoveryResult through the remaining helpers that still call discoverOpenClawPlugins internally during startup: - loadOpenClawPlugins / loadOpenClawPluginCliRegistry (src/plugins/loader.ts): add discovery? to PluginLoadOptions and consult it before falling back to an internal scan at both call sites. - loadPluginManifestRegistry (src/plugins/manifest-registry.ts): accept discovery? as a more ergonomic alternative to the existing candidates? / diagnostics? pair; candidates? still wins when both are supplied. - resolveInstalledPluginIndexRegistry (src/plugins/installed-plugin-index-registry.ts): add discovery? to LoadInstalledPluginIndexParams and use it when candidates aren't supplied. - resolvePluginConfigContractsById (src/plugins/config-contracts.ts): add discovery? and thread it into the bundled-fallback discovery call. Add discovery-threading.test.ts asserting each entry point skips its internal discoverOpenClawPlugins call when discovery is supplied, calls it when nothing is supplied, and prefers explicit candidates over discovery when both are present (6 tests, all pass). discoverOpenClawPlugins remains stateless; sharing is function-scoped per src/plugins/CLAUDE.md guidance. Backward compatible: every change is additive (new optional param).
…y, installed-index, and config contracts (openclaw#84258) Follow-up to openclaw#75451. Threads optional discovery?: PluginDiscoveryResult through the remaining helpers that still call discoverOpenClawPlugins internally during startup: - loadOpenClawPlugins / loadOpenClawPluginCliRegistry (src/plugins/loader.ts): add discovery? to PluginLoadOptions and consult it before falling back to an internal scan at both call sites. - loadPluginManifestRegistry (src/plugins/manifest-registry.ts): accept discovery? as a more ergonomic alternative to the existing candidates? / diagnostics? pair; candidates? still wins when both are supplied. - resolveInstalledPluginIndexRegistry (src/plugins/installed-plugin-index-registry.ts): add discovery? to LoadInstalledPluginIndexParams and use it when candidates aren't supplied. - resolvePluginConfigContractsById (src/plugins/config-contracts.ts): add discovery? and thread it into the bundled-fallback discovery call. Add discovery-threading.test.ts asserting each entry point skips its internal discoverOpenClawPlugins call when discovery is supplied, calls it when nothing is supplied, and prefers explicit candidates over discovery when both are present (6 tests, all pass). discoverOpenClawPlugins remains stateless; sharing is function-scoped per src/plugins/CLAUDE.md guidance. Backward compatible: every change is additive (new optional param).
…y, installed-index, and config contracts (openclaw#84258) Follow-up to openclaw#75451. Threads optional discovery?: PluginDiscoveryResult through the remaining helpers that still call discoverOpenClawPlugins internally during startup: - loadOpenClawPlugins / loadOpenClawPluginCliRegistry (src/plugins/loader.ts): add discovery? to PluginLoadOptions and consult it before falling back to an internal scan at both call sites. - loadPluginManifestRegistry (src/plugins/manifest-registry.ts): accept discovery? as a more ergonomic alternative to the existing candidates? / diagnostics? pair; candidates? still wins when both are supplied. - resolveInstalledPluginIndexRegistry (src/plugins/installed-plugin-index-registry.ts): add discovery? to LoadInstalledPluginIndexParams and use it when candidates aren't supplied. - resolvePluginConfigContractsById (src/plugins/config-contracts.ts): add discovery? and thread it into the bundled-fallback discovery call. Add discovery-threading.test.ts asserting each entry point skips its internal discoverOpenClawPlugins call when discovery is supplied, calls it when nothing is supplied, and prefers explicit candidates over discovery when both are present (6 tests, all pass). discoverOpenClawPlugins remains stateless; sharing is function-scoped per src/plugins/CLAUDE.md guidance. Backward compatible: every change is additive (new optional param).
…y, installed-index, and config contracts (openclaw#84258) Follow-up to openclaw#75451. Threads optional discovery?: PluginDiscoveryResult through the remaining helpers that still call discoverOpenClawPlugins internally during startup: - loadOpenClawPlugins / loadOpenClawPluginCliRegistry (src/plugins/loader.ts): add discovery? to PluginLoadOptions and consult it before falling back to an internal scan at both call sites. - loadPluginManifestRegistry (src/plugins/manifest-registry.ts): accept discovery? as a more ergonomic alternative to the existing candidates? / diagnostics? pair; candidates? still wins when both are supplied. - resolveInstalledPluginIndexRegistry (src/plugins/installed-plugin-index-registry.ts): add discovery? to LoadInstalledPluginIndexParams and use it when candidates aren't supplied. - resolvePluginConfigContractsById (src/plugins/config-contracts.ts): add discovery? and thread it into the bundled-fallback discovery call. Add discovery-threading.test.ts asserting each entry point skips its internal discoverOpenClawPlugins call when discovery is supplied, calls it when nothing is supplied, and prefers explicit candidates over discovery when both are present (6 tests, all pass). discoverOpenClawPlugins remains stateless; sharing is function-scoped per src/plugins/CLAUDE.md guidance. Backward compatible: every change is additive (new optional param).
…y, installed-index, and config contracts (openclaw#84258) Follow-up to openclaw#75451. Threads optional discovery?: PluginDiscoveryResult through the remaining helpers that still call discoverOpenClawPlugins internally during startup: - loadOpenClawPlugins / loadOpenClawPluginCliRegistry (src/plugins/loader.ts): add discovery? to PluginLoadOptions and consult it before falling back to an internal scan at both call sites. - loadPluginManifestRegistry (src/plugins/manifest-registry.ts): accept discovery? as a more ergonomic alternative to the existing candidates? / diagnostics? pair; candidates? still wins when both are supplied. - resolveInstalledPluginIndexRegistry (src/plugins/installed-plugin-index-registry.ts): add discovery? to LoadInstalledPluginIndexParams and use it when candidates aren't supplied. - resolvePluginConfigContractsById (src/plugins/config-contracts.ts): add discovery? and thread it into the bundled-fallback discovery call. Add discovery-threading.test.ts asserting each entry point skips its internal discoverOpenClawPlugins call when discovery is supplied, calls it when nothing is supplied, and prefers explicit candidates over discovery when both are present (6 tests, all pass). discoverOpenClawPlugins remains stateless; sharing is function-scoped per src/plugins/CLAUDE.md guidance. Backward compatible: every change is additive (new optional param).
Summary
Follow-up to #75451. Extends the function-scoped
discovery?: PluginDiscoveryResultthreading pattern to the remaining four plugin subsystems that still calldiscoverOpenClawPluginsinternally during startup, plus tests asserting the wiring.discoverOpenClawPluginsremains stateless. Sharing is function-scoped persrc/plugins/CLAUDE.mdguidance. Every change is additive (a new optional param) and backward compatible — existing callers are unaffected.What this PR adds
src/plugins/loader.tsdiscovery?toPluginLoadOptions; both call sites (loadOpenClawPluginsandloadOpenClawPluginCliRegistry) consult it before doing their own scan.src/plugins/manifest-registry.tsdiscovery?toloadPluginManifestRegistryparams; used whencandidates?is not supplied.src/plugins/installed-plugin-index-types.ts+installed-plugin-index-registry.tsdiscovery?toLoadInstalledPluginIndexParams;resolveInstalledPluginIndexRegistryuses it whencandidates?is not supplied.src/plugins/config-contracts.tsdiscovery?toresolvePluginConfigContractsById; threaded into the bundled-fallback closure.src/plugins/discovery-threading.test.ts(new)discoverOpenClawPluginscall whendiscoveryis supplied, (b) calls it when nothing is supplied, (c) prefers explicitcandidates?overdiscovery?when both are present.#75451 covered
bundled-capability-runtime,bundled-sources,channel-catalog-registry, and thecontracts/registryretry loop. With this PR, all knowndiscoverOpenClawPluginscallers insrc/plugins/accept an optional shared discovery snapshot. Top-level orchestrators can compute one discovery per CLI/TUI/gateway flow and pass it through.Context: what we measured before #75451 landed
The freeze that motivated this work is a 25–30s TUI startup with the process at
R+ 101% CPU, doing 212,119 synchronous JSON reads against only 379 unique paths per launch — each bundled extension'spackage.jsonre-read ~1,500 times. CPU profile Bottom-Up was dominated bylstat(24%),open(13%),realpathSync(13% total), withjitisource-transform and GC making up the rest. An event-loop heartbeat probe showed the JS event loop blocked for the full freeze duration. The detailed evidence is on #75451: #75451 (comment)This PR is necessary but not sufficient to close the freeze on its own. It is preparatory plumbing that lets callers actually realize the sharing. A separate follow-up should:
PluginDiscoveryResultthere and thread it through each call.require()-drivenpackage.jsonwalks during plugin module loading — those are larger costs than the outer scan dedup addressed here.Test plan
node scripts/run-vitest.mjs src/plugins/discovery-threading.test.ts— 6/6 passpnpm tsgo --noEmit --project tsconfig.core.json— cleannode scripts/run-oxlint.mjs --quieton touched files — 0 warnings, 0 errorsnpx oxfmt --check— cleanVerified the 5 pre-existing failures in
src/plugins/manifest-registry.test.ts("suppresses duplicate warnings..." etc.) reproduce on clean main without this PR's changes — unrelated regression.Behavior addressed: each of the four remaining plugin subsystem helpers in
src/plugins/independently re-invokesdiscoverOpenClawPluginsduring the same startup flow when not explicitly handed a shared discovery snapshot. After #75451 only three helpers + one retry loop had the option to skip the redundant scan; with this PR, all known call sites can.Real environment tested: macOS, Node 22.22, source checkout. Same setup that produced the 212K-read profile referenced above.
Exact steps or command run after this patch:
node scripts/run-vitest.mjs src/plugins/discovery-threading.test.ts;pnpm tsgo --noEmit --project tsconfig.core.json;node scripts/run-oxlint.mjs --quiet src/plugins/{loader,manifest-registry,installed-plugin-index-registry,installed-plugin-index-types,config-contracts,discovery-threading.test}.ts;npx oxfmt --check.Evidence after fix: 6 new tests pass; lint/format/typecheck clean.
Observed result after fix: each entry point demonstrably skips its internal discovery call when
discoveryis supplied (asserted by the new test suite via avi.mockspy ondiscoverOpenClawPlugins).What was not tested: end-to-end TUI startup timing on this PR alone. The sharing is preparatory — orchestrator-level threading that supplies the same discovery to multiple helpers in one flow is a separate follow-up. This PR is the structural prerequisite for that work.