perf: thread explicit plugin discovery through contracts registry#75451
Conversation
|
Codex review: needs real behavior proof before merge. Workflow note: Future ClawSweeper reviews update this same comment in place. How this review workflow works
Summary Reproducibility: yes. by source inspection: current main's scoped contracts retry loop can call the bundled capability runtime helper twice, and that helper discovers plugins internally. I did not establish a runtime benchmark 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 Where did the egg go?
Real behavior proof Risk before merge
Maintainer options:
Next step before merge Security Review detailsBest possible solution: Land one rebased version of the explicit function-scoped discovery threading with redacted runtime or perf proof, then track broader startup-read reductions separately. Do we have a high-confidence way to reproduce the issue? Yes by source inspection: current main's scoped contracts retry loop can call the bundled capability runtime helper twice, and that helper discovers plugins internally. I did not establish a runtime benchmark in this read-only review. Is this the best way to solve the issue? Mostly yes: explicit function-scoped discovery threading matches the plugin metadata freshness rule better than a hidden cache. The remaining blockers are proof and branch coordination, not a different core approach. Label justifications:
What I checked:
Likely related people:
Codex review notes: model gpt-5.5, reasoning high; reviewed against 9b97e1ef2fd2. |
ef7f5a9 to
c578f18
Compare
e4c53d9 to
dc6be2b
Compare
dc6be2b to
72c24d9
Compare
72c24d9 to
4499137
Compare
|
Hi @SebTardif — thank you for this fix. I've been profiling a TUI startup freeze (25–30s with the process pegged at 101% CPU, ~212K sync JSON reads against ~380 unique paths) and found that the redundant filesystem walks you addressed here are part of the picture. I've reproduced your patch verbatim with attribution in #84258 so it can land on a fresher base alongside the profiling evidence and follow-up discussion of which call sites still need the same treatment. Your authorship is preserved via Closing this in favor of #84258. Happy to credit further if there are details about the original investigation you'd like included. |
|
EDIT, that was AI driven, it's a partial fix considering just merging this and building on it |
Real behavior proof — profiling evidencePosting this to address the ClawSweeper review's "needs real behavior proof before merge" block. I profiled a TUI startup freeze on CPU profile (Node
|
| % self time | Activity |
|---|---|
| 24% | lstat (Node module resolution) |
| 13% | open |
| 13% (total) | realpathSync |
| 5% | jiti runtime TS transpile (node_modules/jiti/dist/jiti.cjs) |
| 3% | (garbage collector) |
| 9.5% (total) | tryReadJsonSync from dist/json-files-*.js |
Per-call instrumentation (temporary perfMark around tryReadJsonSync)
One TUI startup against a running gateway:
| Metric | Value |
|---|---|
| Total sync JSON reads | 212,119 |
| Unique JSON paths read | 379 |
| Average reads per file | ~560× |
| Plugin module loads | 248 (62 unique modules) |
Loaded via jiti (slow path) |
119 (~48%) |
Top read offenders in one run:
3838 ~/.openclaw/npm/node_modules/@openclaw/codex/package.json
2166 ~/.openclaw/plugins/installs.json
1565 extensions/whatsapp/package.json
1565 extensions/anthropic-vertex/package.json
1565 extensions/amazon-bedrock/package.json
... (50+ extensions, all ~1565× each)
Event-loop heartbeat
A 500 ms setInterval probe missed every tick during the freeze (heartbeat gaps of 5 s and 25 s logged on a single run), confirming the freeze is synchronous JS work, not I/O wait. Wall-clock duration varied 3.7 s → 27 s → 30 s across runs, tracking OS file-cache temperature.
How this PR fits
This PR directly addresses one source of the redundancy: subsystems independently invoking discoverOpenClawPlugins, and the retry loop in loadScopedCapabilityRuntimeRegistryEntries re-discovering on each attempt. The function-scoped sharing matches the explicit design constraint from src/plugins/CLAUDE.md (no persistent metadata caches).
It is unlikely to fully resolve the freeze by itself — the manifest-read amplifier (~1,565× per extension) suggests the dominant cost is inside the loader / Node require() walks during plugin module loading, not in outer discovery scans. Estimated impact of this PR is a partial improvement; the remaining callers in loader.ts (2 sites), manifest-registry.ts, installed-plugin-index-registry.ts, and config-contracts.ts would benefit from the same threading pattern in follow-up PRs.
Methodology
- Source-checkout branch, macOS, Node 22.22.
- Per-call instrumentation: temporary
perfMarkcalls appended to/tmp/openclaw-perf.logviaappendFileSync. Available on the audit branch if useful. - CPU profile:
NODE_OPTIONS="--cpu-prof --cpu-prof-dir=/tmp/openclaw-prof --cpu-prof-interval=200" node ./openclaw.mjs tui, opened in Chrome DevTools / VS Code.
…m walks Add optional discovery parameter to loadBundledCapabilityRuntimeRegistry, resolveBundledPluginSources, and listChannelCatalogEntries so callers that already hold a PluginDiscoveryResult can skip redundant filesystem walks. In contracts/registry.ts, the retry loop in loadScopedCapabilityRuntimeRegistryEntries computes discovery once and shares it across retry attempts (function-scoped, not module-scoped). discoverOpenClawPlugins() itself remains stateless with no hidden cache. Closes openclaw#82308 Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>
4499137 to
e9f2cb0
Compare
|
@clawsweeper re-review |
…m walks (openclaw#75451) Add optional discovery parameter to loadBundledCapabilityRuntimeRegistry, resolveBundledPluginSources, and listChannelCatalogEntries so callers that already hold a PluginDiscoveryResult can skip redundant filesystem walks. In contracts/registry.ts, the retry loop in loadScopedCapabilityRuntimeRegistryEntries computes discovery once and shares it across retry attempts (function-scoped, not module-scoped). discoverOpenClawPlugins() itself remains stateless with no hidden cache. Closes openclaw#82308 Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>
…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#84283) * perf(plugins): extend discovery threading to loader, manifest registry, installed-index, and config contracts 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). * perf(plugins): drop verbose JSDoc from discovery? params
…84302) * perf(plugins): extend discovery threading to loader, manifest registry, installed-index, and config contracts 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). * perf(plugins): drop verbose JSDoc from discovery? params * perf(plugins): scan-scoped package.json cache in discovery Adds a per-scan Map<string, PackageManifest | null> threaded through discoverFromPath/discoverInDirectory/readCandidatePackageManifest, keyed by the directory's resolved real path. Within one discovery scan, a plugin's package.json is now read from disk once and reused across the overlapping discovery code paths (bundled overlay scan, stock-root scan, source-checkout extensions scan, installed-path scan, global-root scan) that previously each fired their own read. The cache lifetime is one scan (created in runPluginDiscovery alongside the existing realpathCache and seen Set, dies when the scan returns). discoverOpenClawPlugins remains stateless externally; no persistent metadata cache. * perf(plugins): expose raw parsed package.json on PluginCandidate Discovery already reads each plugin's package.json once and produces a parsed PackageManifest object before distilling it into metadata via getPackageManifestMetadata. Currently only the distilled metadata is kept on the candidate; the full parsed manifest is discarded. Store the full parsed manifest on rawPackageManifest so downstream consumers iterating candidates can use it instead of re-reading from disk. This is the candidate-side groundwork for the scenario-C followup that routes consumers (bundled-plugin-metadata, bundle-* helpers, etc.) through the cached field; those consumers currently do their own directory scans and would need to be refactored to iterate PluginCandidate arrays before they can benefit. The field is a frozen-at-discovery-time snapshot, same lifetime semantics as the existing packageManifest / packageName / packageVersion fields on PluginCandidate. No new staleness window introduced. * perf(plugins): make package-manifest cache key trust-aware
…m walks (openclaw#75451) Add optional discovery parameter to loadBundledCapabilityRuntimeRegistry, resolveBundledPluginSources, and listChannelCatalogEntries so callers that already hold a PluginDiscoveryResult can skip redundant filesystem walks. In contracts/registry.ts, the retry loop in loadScopedCapabilityRuntimeRegistryEntries computes discovery once and shares it across retry attempts (function-scoped, not module-scoped). discoverOpenClawPlugins() itself remains stateless with no hidden cache. Closes openclaw#82308 Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>
…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#84283) * perf(plugins): extend discovery threading to loader, manifest registry, installed-index, and config contracts 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). * perf(plugins): drop verbose JSDoc from discovery? params
…84302) * perf(plugins): extend discovery threading to loader, manifest registry, installed-index, and config contracts 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). * perf(plugins): drop verbose JSDoc from discovery? params * perf(plugins): scan-scoped package.json cache in discovery Adds a per-scan Map<string, PackageManifest | null> threaded through discoverFromPath/discoverInDirectory/readCandidatePackageManifest, keyed by the directory's resolved real path. Within one discovery scan, a plugin's package.json is now read from disk once and reused across the overlapping discovery code paths (bundled overlay scan, stock-root scan, source-checkout extensions scan, installed-path scan, global-root scan) that previously each fired their own read. The cache lifetime is one scan (created in runPluginDiscovery alongside the existing realpathCache and seen Set, dies when the scan returns). discoverOpenClawPlugins remains stateless externally; no persistent metadata cache. * perf(plugins): expose raw parsed package.json on PluginCandidate Discovery already reads each plugin's package.json once and produces a parsed PackageManifest object before distilling it into metadata via getPackageManifestMetadata. Currently only the distilled metadata is kept on the candidate; the full parsed manifest is discarded. Store the full parsed manifest on rawPackageManifest so downstream consumers iterating candidates can use it instead of re-reading from disk. This is the candidate-side groundwork for the scenario-C followup that routes consumers (bundled-plugin-metadata, bundle-* helpers, etc.) through the cached field; those consumers currently do their own directory scans and would need to be refactored to iterate PluginCandidate arrays before they can benefit. The field is a frozen-at-discovery-time snapshot, same lifetime semantics as the existing packageManifest / packageName / packageVersion fields on PluginCandidate. No new staleness window introduced. * perf(plugins): make package-manifest cache key trust-aware
…m walks (openclaw#75451) Add optional discovery parameter to loadBundledCapabilityRuntimeRegistry, resolveBundledPluginSources, and listChannelCatalogEntries so callers that already hold a PluginDiscoveryResult can skip redundant filesystem walks. In contracts/registry.ts, the retry loop in loadScopedCapabilityRuntimeRegistryEntries computes discovery once and shares it across retry attempts (function-scoped, not module-scoped). discoverOpenClawPlugins() itself remains stateless with no hidden cache. Closes openclaw#82308 Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>
…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#84283) * perf(plugins): extend discovery threading to loader, manifest registry, installed-index, and config contracts 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). * perf(plugins): drop verbose JSDoc from discovery? params
…84302) * perf(plugins): extend discovery threading to loader, manifest registry, installed-index, and config contracts 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). * perf(plugins): drop verbose JSDoc from discovery? params * perf(plugins): scan-scoped package.json cache in discovery Adds a per-scan Map<string, PackageManifest | null> threaded through discoverFromPath/discoverInDirectory/readCandidatePackageManifest, keyed by the directory's resolved real path. Within one discovery scan, a plugin's package.json is now read from disk once and reused across the overlapping discovery code paths (bundled overlay scan, stock-root scan, source-checkout extensions scan, installed-path scan, global-root scan) that previously each fired their own read. The cache lifetime is one scan (created in runPluginDiscovery alongside the existing realpathCache and seen Set, dies when the scan returns). discoverOpenClawPlugins remains stateless externally; no persistent metadata cache. * perf(plugins): expose raw parsed package.json on PluginCandidate Discovery already reads each plugin's package.json once and produces a parsed PackageManifest object before distilling it into metadata via getPackageManifestMetadata. Currently only the distilled metadata is kept on the candidate; the full parsed manifest is discarded. Store the full parsed manifest on rawPackageManifest so downstream consumers iterating candidates can use it instead of re-reading from disk. This is the candidate-side groundwork for the scenario-C followup that routes consumers (bundled-plugin-metadata, bundle-* helpers, etc.) through the cached field; those consumers currently do their own directory scans and would need to be refactored to iterate PluginCandidate arrays before they can benefit. The field is a frozen-at-discovery-time snapshot, same lifetime semantics as the existing packageManifest / packageName / packageVersion fields on PluginCandidate. No new staleness window introduced. * perf(plugins): make package-manifest cache key trust-aware
…m walks (openclaw#75451) Add optional discovery parameter to loadBundledCapabilityRuntimeRegistry, resolveBundledPluginSources, and listChannelCatalogEntries so callers that already hold a PluginDiscoveryResult can skip redundant filesystem walks. In contracts/registry.ts, the retry loop in loadScopedCapabilityRuntimeRegistryEntries computes discovery once and shares it across retry attempts (function-scoped, not module-scoped). discoverOpenClawPlugins() itself remains stateless with no hidden cache. Closes openclaw#82308 Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>
…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#84283) * perf(plugins): extend discovery threading to loader, manifest registry, installed-index, and config contracts 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). * perf(plugins): drop verbose JSDoc from discovery? params
…84302) * perf(plugins): extend discovery threading to loader, manifest registry, installed-index, and config contracts 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). * perf(plugins): drop verbose JSDoc from discovery? params * perf(plugins): scan-scoped package.json cache in discovery Adds a per-scan Map<string, PackageManifest | null> threaded through discoverFromPath/discoverInDirectory/readCandidatePackageManifest, keyed by the directory's resolved real path. Within one discovery scan, a plugin's package.json is now read from disk once and reused across the overlapping discovery code paths (bundled overlay scan, stock-root scan, source-checkout extensions scan, installed-path scan, global-root scan) that previously each fired their own read. The cache lifetime is one scan (created in runPluginDiscovery alongside the existing realpathCache and seen Set, dies when the scan returns). discoverOpenClawPlugins remains stateless externally; no persistent metadata cache. * perf(plugins): expose raw parsed package.json on PluginCandidate Discovery already reads each plugin's package.json once and produces a parsed PackageManifest object before distilling it into metadata via getPackageManifestMetadata. Currently only the distilled metadata is kept on the candidate; the full parsed manifest is discarded. Store the full parsed manifest on rawPackageManifest so downstream consumers iterating candidates can use it instead of re-reading from disk. This is the candidate-side groundwork for the scenario-C followup that routes consumers (bundled-plugin-metadata, bundle-* helpers, etc.) through the cached field; those consumers currently do their own directory scans and would need to be refactored to iterate PluginCandidate arrays before they can benefit. The field is a frozen-at-discovery-time snapshot, same lifetime semantics as the existing packageManifest / packageName / packageVersion fields on PluginCandidate. No new staleness window introduced. * perf(plugins): make package-manifest cache key trust-aware
…m walks (openclaw#75451) Add optional discovery parameter to loadBundledCapabilityRuntimeRegistry, resolveBundledPluginSources, and listChannelCatalogEntries so callers that already hold a PluginDiscoveryResult can skip redundant filesystem walks. In contracts/registry.ts, the retry loop in loadScopedCapabilityRuntimeRegistryEntries computes discovery once and shares it across retry attempts (function-scoped, not module-scoped). discoverOpenClawPlugins() itself remains stateless with no hidden cache. Closes openclaw#82308 Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>
…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#84283) * perf(plugins): extend discovery threading to loader, manifest registry, installed-index, and config contracts 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). * perf(plugins): drop verbose JSDoc from discovery? params
…84302) * perf(plugins): extend discovery threading to loader, manifest registry, installed-index, and config contracts 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). * perf(plugins): drop verbose JSDoc from discovery? params * perf(plugins): scan-scoped package.json cache in discovery Adds a per-scan Map<string, PackageManifest | null> threaded through discoverFromPath/discoverInDirectory/readCandidatePackageManifest, keyed by the directory's resolved real path. Within one discovery scan, a plugin's package.json is now read from disk once and reused across the overlapping discovery code paths (bundled overlay scan, stock-root scan, source-checkout extensions scan, installed-path scan, global-root scan) that previously each fired their own read. The cache lifetime is one scan (created in runPluginDiscovery alongside the existing realpathCache and seen Set, dies when the scan returns). discoverOpenClawPlugins remains stateless externally; no persistent metadata cache. * perf(plugins): expose raw parsed package.json on PluginCandidate Discovery already reads each plugin's package.json once and produces a parsed PackageManifest object before distilling it into metadata via getPackageManifestMetadata. Currently only the distilled metadata is kept on the candidate; the full parsed manifest is discarded. Store the full parsed manifest on rawPackageManifest so downstream consumers iterating candidates can use it instead of re-reading from disk. This is the candidate-side groundwork for the scenario-C followup that routes consumers (bundled-plugin-metadata, bundle-* helpers, etc.) through the cached field; those consumers currently do their own directory scans and would need to be refactored to iterate PluginCandidate arrays before they can benefit. The field is a frozen-at-discovery-time snapshot, same lifetime semantics as the existing packageManifest / packageName / packageVersion fields on PluginCandidate. No new staleness window introduced. * perf(plugins): make package-manifest cache key trust-aware
…m walks (openclaw#75451) Add optional discovery parameter to loadBundledCapabilityRuntimeRegistry, resolveBundledPluginSources, and listChannelCatalogEntries so callers that already hold a PluginDiscoveryResult can skip redundant filesystem walks. In contracts/registry.ts, the retry loop in loadScopedCapabilityRuntimeRegistryEntries computes discovery once and shares it across retry attempts (function-scoped, not module-scoped). discoverOpenClawPlugins() itself remains stateless with no hidden cache. Closes openclaw#82308 Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>
…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#84283) * perf(plugins): extend discovery threading to loader, manifest registry, installed-index, and config contracts 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). * perf(plugins): drop verbose JSDoc from discovery? params
…84302) * perf(plugins): extend discovery threading to loader, manifest registry, installed-index, and config contracts 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). * perf(plugins): drop verbose JSDoc from discovery? params * perf(plugins): scan-scoped package.json cache in discovery Adds a per-scan Map<string, PackageManifest | null> threaded through discoverFromPath/discoverInDirectory/readCandidatePackageManifest, keyed by the directory's resolved real path. Within one discovery scan, a plugin's package.json is now read from disk once and reused across the overlapping discovery code paths (bundled overlay scan, stock-root scan, source-checkout extensions scan, installed-path scan, global-root scan) that previously each fired their own read. The cache lifetime is one scan (created in runPluginDiscovery alongside the existing realpathCache and seen Set, dies when the scan returns). discoverOpenClawPlugins remains stateless externally; no persistent metadata cache. * perf(plugins): expose raw parsed package.json on PluginCandidate Discovery already reads each plugin's package.json once and produces a parsed PackageManifest object before distilling it into metadata via getPackageManifestMetadata. Currently only the distilled metadata is kept on the candidate; the full parsed manifest is discarded. Store the full parsed manifest on rawPackageManifest so downstream consumers iterating candidates can use it instead of re-reading from disk. This is the candidate-side groundwork for the scenario-C followup that routes consumers (bundled-plugin-metadata, bundle-* helpers, etc.) through the cached field; those consumers currently do their own directory scans and would need to be refactored to iterate PluginCandidate arrays before they can benefit. The field is a frozen-at-discovery-time snapshot, same lifetime semantics as the existing packageManifest / packageName / packageVersion fields on PluginCandidate. No new staleness window introduced. * perf(plugins): make package-manifest cache key trust-aware
…m walks (openclaw#75451) Add optional discovery parameter to loadBundledCapabilityRuntimeRegistry, resolveBundledPluginSources, and listChannelCatalogEntries so callers that already hold a PluginDiscoveryResult can skip redundant filesystem walks. In contracts/registry.ts, the retry loop in loadScopedCapabilityRuntimeRegistryEntries computes discovery once and shares it across retry attempts (function-scoped, not module-scoped). discoverOpenClawPlugins() itself remains stateless with no hidden cache. Closes openclaw#82308 Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>
…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#84283) * perf(plugins): extend discovery threading to loader, manifest registry, installed-index, and config contracts 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). * perf(plugins): drop verbose JSDoc from discovery? params
…84302) * perf(plugins): extend discovery threading to loader, manifest registry, installed-index, and config contracts 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). * perf(plugins): drop verbose JSDoc from discovery? params * perf(plugins): scan-scoped package.json cache in discovery Adds a per-scan Map<string, PackageManifest | null> threaded through discoverFromPath/discoverInDirectory/readCandidatePackageManifest, keyed by the directory's resolved real path. Within one discovery scan, a plugin's package.json is now read from disk once and reused across the overlapping discovery code paths (bundled overlay scan, stock-root scan, source-checkout extensions scan, installed-path scan, global-root scan) that previously each fired their own read. The cache lifetime is one scan (created in runPluginDiscovery alongside the existing realpathCache and seen Set, dies when the scan returns). discoverOpenClawPlugins remains stateless externally; no persistent metadata cache. * perf(plugins): expose raw parsed package.json on PluginCandidate Discovery already reads each plugin's package.json once and produces a parsed PackageManifest object before distilling it into metadata via getPackageManifestMetadata. Currently only the distilled metadata is kept on the candidate; the full parsed manifest is discarded. Store the full parsed manifest on rawPackageManifest so downstream consumers iterating candidates can use it instead of re-reading from disk. This is the candidate-side groundwork for the scenario-C followup that routes consumers (bundled-plugin-metadata, bundle-* helpers, etc.) through the cached field; those consumers currently do their own directory scans and would need to be refactored to iterate PluginCandidate arrays before they can benefit. The field is a frozen-at-discovery-time snapshot, same lifetime semantics as the existing packageManifest / packageName / packageVersion fields on PluginCandidate. No new staleness window introduced. * perf(plugins): make package-manifest cache key trust-aware
…m walks (openclaw#75451) Add optional discovery parameter to loadBundledCapabilityRuntimeRegistry, resolveBundledPluginSources, and listChannelCatalogEntries so callers that already hold a PluginDiscoveryResult can skip redundant filesystem walks. In contracts/registry.ts, the retry loop in loadScopedCapabilityRuntimeRegistryEntries computes discovery once and shares it across retry attempts (function-scoped, not module-scoped). discoverOpenClawPlugins() itself remains stateless with no hidden cache. Closes openclaw#82308 Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>
…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).
Problem
discoverOpenClawPlugins()is called independently by multiple plugin subsystems during startup. When callers in the same flow independently calldiscoverOpenClawPluginswith identical params, the filesystem walk repeats unnecessarily.Approach
Add an optional
discovery?: PluginDiscoveryResultparameter to the three functions that calldiscoverOpenClawPluginsinternally:loadBundledCapabilityRuntimeRegistryresolveBundledPluginSourceslistChannelCatalogEntriesWhen provided, the pre-computed result is used directly; when absent, the function calls
discoverOpenClawPluginsas before (fully backward compatible).In
contracts/registry.ts, the retry loop inloadScopedCapabilityRuntimeRegistryEntriescomputes discovery once and passes it to both retry attempts, eliminating one redundant scan per retried load. The discovery is function-scoped, not module-scoped.discoverOpenClawPlugins()itself remains stateless.Files changed
src/plugins/bundled-capability-runtime.tsdiscovery?paramsrc/plugins/bundled-sources.tsdiscovery?paramsrc/plugins/channel-catalog-registry.tsdiscovery?paramsrc/plugins/contracts/registry.tsCHANGELOG.mdCloses #82308
Real behavior proof
Behavior addressed: plugin subsystem functions that call discoverOpenClawPlugins() internally now accept an optional pre-computed discovery result, allowing callers in the same startup flow to share one filesystem walk instead of repeating it per subsystem.
Real environment tested: local source checkout of openclaw/openclaw at commit 5d81c29 (current upstream/main) on Linux x86_64, Node v26.0.0, vitest 4.1.6. Used git worktree from the local fork at ~/openclaw/. Cherry-picked the feature commit onto upstream/main (zero source file conflicts).
Exact steps or command run after this patch: pnpm tsgo:core (type check); node scripts/run-vitest.mjs src/plugins/bundled-capability-runtime.test.ts src/plugins/bundled-sources.test.ts src/plugins/channel-catalog-registry.test.ts src/plugins/contracts/registry.ts (focused test suite covering all changed modules).
Evidence after fix: terminal output copied below.
Type check:
No errors.
Test suite (14 tests across 4 changed modules):
Observed result after fix: all 14 tests pass across the 4 changed modules. Type check is clean. The change is fully backward compatible: all existing callers continue working without the optional param. The only behavioral change is when the optional
discoveryparam is provided, the internaldiscoverOpenClawPluginscall is skipped. The retry loop in contracts/registry.ts is the first caller to use this, computing discovery once and sharing it across retry attempts.Runtime profiling (independent confirmation by @RomneyDa)
@RomneyDa independently profiled a TUI startup freeze (25-30s, process at 101% CPU) and confirmed the redundant discovery walks this PR targets are part of the measured problem. Full profiling methodology and data in this comment.
CPU profile (Node
--cpu-prof, Bottom-Up):lstat(Node module resolution)openrealpathSynctryReadJsonSyncfromdist/json-files-*.jsPer-call instrumentation (temporary
perfMarkaroundtryReadJsonSync):An event-loop heartbeat probe (500ms
setInterval) missed every tick during the freeze (gaps of 5s and 25s), confirming the freeze is synchronous JS work, not I/O wait.Methodology: source-checkout branch, macOS, Node 22.22. Per-call instrumentation via temporary
perfMarkcalls. CPU profile viaNODE_OPTIONS="--cpu-prof --cpu-prof-dir=/tmp/openclaw-prof --cpu-prof-interval=200".This PR directly addresses one source of the redundancy: subsystems independently invoking
discoverOpenClawPlugins, and the retry loop re-discovering on each attempt. The function-scoped sharing matches the explicit design constraint fromsrc/plugins/AGENTS.md(no persistent metadata caches). Remaining callers inloader.ts,manifest-registry.ts,installed-plugin-index-registry.ts, andconfig-contracts.tswould benefit from the same threading pattern in follow-up PRs.