perf(plugins): scan-scoped package.json cache in discovery#84302
Conversation
…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).
|
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: Source-level yes, runtime no: current discovery reads package.json through overlapping helper paths before candidate de-dupe, and the PR body cites a profile, but I did not run the TUI startup measurement. 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. Real behavior proof Risk before merge
Maintainer options:
Next step before merge Security Review detailsBest possible solution: Land the scan-local, trust-aware cache only after maintainers are comfortable with the remaining profiling gap or add a small cache-boundary regression test first. Do we have a high-confidence way to reproduce the issue? Source-level yes, runtime no: current discovery reads package.json through overlapping helper paths before candidate de-dupe, and the PR body cites a profile, but I did not run the TUI startup measurement. Is this the best way to solve the issue? Yes with caveats: a scan-local cache keyed by trust mode is a bounded maintainable fix for repeated manifest parses, while cache-specific regression coverage or startup profiling would improve merge confidence. Label justifications:
What I checked:
Likely related people:
Codex review notes: model gpt-5.5, reasoning high; reviewed against 88d8d6af9336. |
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.
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.
c909256 to
949dd68
Compare
|
ClawSweeper's prior findings (against head
Current head
Local gates green: ClawSweeper has since re-reviewed against |
|
ClawSweeper PR egg ✨ Hatched: 🌱 uncommon Gilded Shellbean Rarity: 🌱 uncommon. What is this egg doing here?
|
|
@clawsweeper reevaluate labels, I believe I've mitigated security concern |
|
🦞👀 Reason: freeform assist requires an open issue or PR. Request: reevaluate labels, I believe I've mitigated security concern |
…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
…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
…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
…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
…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
…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
…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
…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
…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
…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
…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
Summary
Investigates and fixes the inner amplifier — why each plugin's
package.jsonwas being read ~1,565 times per TUI startup. Stacked on top of #84283.Root cause:
runPluginDiscoveryruns twotracePluginLifecyclePhase("discovery scan")blocks (scoped + shared), each invoking multiple overlapping helpers (discoverFromPath,discoverInDirectory) that all callreadCandidatePackageManifestper candidate. For bundled plugins reachable through multiple roots (stock root + source-checkout extensions + bundled overlays + installed paths + global root), the samepackage.jsongets read once per code path per scan — and the per-helperseen: Set<string>dedup operates at the candidate level after the manifest read happens. The existingrealpathCacheonly memoizes realpath resolution, not the JSON parse.Fix: add a per-scan
Map<string, PackageManifest | null>threaded through the same chain asrealpathCache, keyed by${trustMode}:${rootRealPath}. Trust mode separatestrusted(bundled, no hardlink check) fromexternal-reject/external-allowso the cache can't reuse a trusted parse for a later external-origin read that needs hardlink rejection. Cache lifetime is one scan — created inrunPluginDiscoveryalongside the existingseen/realpathCache, dies when the scan returns.discoverOpenClawPluginsremains stateless externally.This PR is complementary to #84283:
ensurePluginRegistryLoadedinvocation.Commits
perf(plugins): scan-scoped package.json cache in discovery— the cache + threading.perf(plugins): expose raw parsed package.json on PluginCandidate— keeps the parsed manifest on the candidate so downstream iterators don't have to re-read.perf(plugins): make package-manifest cache key trust-aware— keys the cache by${trustMode}:${realPath}so trusted/external/hardlink-rejecting reads don't cross. (Addresses clawsweeper P1.)Expected impact
From the original profile (~1,565 reads per extension
package.json):Not measured end-to-end yet — single-launch impact depends on root overlap in the user's environment. If most reads were already from a single root (no overlap), savings are smaller.
Test plan
node scripts/run-vitest.mjs src/plugins/discovery.test.ts— 67/67 passpnpm tsgo --noEmit --project tsconfig.core.json— cleannode scripts/run-oxlint.mjs --quiet src/plugins/discovery.ts— 0/0npx oxfmt --check— cleanjson.tryReadJsonSynclines perextensions/<id>/package.jsonpath in/tmp/openclaw-perf.log).