@@ -2,12 +2,14 @@ import crypto from "node:crypto";
22import fs from "node:fs" ;
33import path from "node:path" ;
44import { resolveUserPath } from "../utils.js" ;
5+ import { resolveCompatibilityHostVersion } from "../version.js" ;
56import { resolveBundledPluginsDir } from "./bundled-dir.js" ;
67import { getCurrentPluginMetadataSnapshot } from "./current-plugin-metadata-snapshot.js" ;
78import type { PluginDiscoveryResult } from "./discovery.js" ;
8- import { fileSignatureMatches } from "./installed-plugin-index-hash.js" ;
9+ import { fileSignatureMatches , hashJson } from "./installed-plugin-index-hash.js" ;
910import { hasOptionalMissingPluginManifestFile } from "./installed-plugin-index-manifest.js" ;
1011import { loadInstalledPluginIndexInstallRecordsSync } from "./installed-plugin-index-record-reader.js" ;
12+ import { resolveInstalledPluginIndexStorePath } from "./installed-plugin-index-store-path.js" ;
1113import {
1214 inspectPersistedInstalledPluginIndex ,
1315 readPersistedInstalledPluginIndexSync ,
@@ -27,6 +29,7 @@ import {
2729 type LoadInstalledPluginIndexParams ,
2830 type RefreshInstalledPluginIndexParams ,
2931} from "./installed-plugin-index.js" ;
32+ import { registerPluginMetadataProcessMemoLifecycleClear } from "./plugin-metadata-lifecycle.js" ;
3033import type { PluginRegistrySnapshotSource } from "./plugin-registry-snapshot.types.js" ;
3134
3235export type PluginRegistrySnapshot = InstalledPluginIndex ;
@@ -53,6 +56,35 @@ export type PluginRegistrySnapshotResult = {
5356} ;
5457
5558export const DISABLE_PERSISTED_PLUGIN_REGISTRY_ENV = "OPENCLAW_DISABLE_PERSISTED_PLUGIN_REGISTRY" ;
59+ const MAX_PLUGIN_REGISTRY_SNAPSHOT_MEMOS = 8 ;
60+ const REGISTRY_SNAPSHOT_MEMO_ENV_KEYS = [
61+ "APPDATA" ,
62+ "HOME" ,
63+ "OPENCLAW_BUNDLED_PLUGINS_DIR" ,
64+ "OPENCLAW_COMPATIBILITY_HOST_VERSION" ,
65+ "OPENCLAW_CONFIG_PATH" ,
66+ "OPENCLAW_DISABLE_BUNDLED_PLUGINS" ,
67+ "OPENCLAW_DISABLE_BUNDLED_SOURCE_OVERLAYS" ,
68+ DISABLE_PERSISTED_PLUGIN_REGISTRY_ENV ,
69+ "OPENCLAW_HOME" ,
70+ "OPENCLAW_NIX_MODE" ,
71+ "OPENCLAW_STATE_DIR" ,
72+ "USERPROFILE" ,
73+ "XDG_CONFIG_HOME" ,
74+ ] as const ;
75+
76+ type PluginRegistrySnapshotMemo = {
77+ key : string ;
78+ result : PluginRegistrySnapshotResult ;
79+ } ;
80+
81+ let pluginRegistrySnapshotMemos : PluginRegistrySnapshotMemo [ ] = [ ] ;
82+
83+ function clearLoadPluginRegistrySnapshotMemo ( ) : void {
84+ pluginRegistrySnapshotMemos = [ ] ;
85+ }
86+
87+ registerPluginMetadataProcessMemoLifecycleClear ( clearLoadPluginRegistrySnapshotMemo ) ;
5688
5789function formatDeprecatedPersistedRegistryDisableWarning ( ) : string {
5890 return `${ DISABLE_PERSISTED_PLUGIN_REGISTRY_ENV } is a deprecated break-glass compatibility switch; use \`openclaw plugins registry --refresh\` or \`openclaw doctor --fix\` to repair registry state.` ;
@@ -73,6 +105,96 @@ function hasEnvFlag(env: NodeJS.ProcessEnv, name: string): boolean {
73105 return Boolean ( value && value !== "0" && value !== "false" && value !== "no" ) ;
74106}
75107
108+ function pickRegistrySnapshotMemoEnv ( env : NodeJS . ProcessEnv ) : Record < string , string > {
109+ return Object . fromEntries (
110+ REGISTRY_SNAPSHOT_MEMO_ENV_KEYS . flatMap ( ( key ) => {
111+ const value = env [ key ] ;
112+ return value === undefined ? [ ] : [ [ key , value ] ] ;
113+ } ) ,
114+ ) ;
115+ }
116+
117+ function canMemoizePluginRegistrySnapshot ( params : LoadPluginRegistryParams ) : boolean {
118+ return (
119+ params . index === undefined &&
120+ params . candidates === undefined &&
121+ params . diagnostics === undefined &&
122+ params . discovery === undefined &&
123+ params . installRecords === undefined &&
124+ params . now === undefined &&
125+ params . filePath === undefined &&
126+ params . pluginIndexFilePath === undefined
127+ ) ;
128+ }
129+
130+ function resolvePluginRegistrySnapshotMemoKey (
131+ params : LoadPluginRegistryParams ,
132+ env : NodeJS . ProcessEnv ,
133+ ) : string | undefined {
134+ if ( ! canMemoizePluginRegistrySnapshot ( params ) ) {
135+ return undefined ;
136+ }
137+ return hashJson ( {
138+ config : params . config ?? null ,
139+ cwd : process . cwd ( ) ,
140+ env : pickRegistrySnapshotMemoEnv ( env ) ,
141+ hostContractVersion : resolveCompatibilityHostVersion ( env ) ,
142+ preferPersisted : params . preferPersisted ?? null ,
143+ // Plugin manifests are process-stable inside the Gateway, while the persisted
144+ // registry envelope can change through explicit refresh/install flows.
145+ registryFile : fileFingerprint (
146+ resolveInstalledPluginIndexStorePath ( {
147+ env,
148+ ...( params . stateDir ? { stateDir : params . stateDir } : { } ) ,
149+ } ) ,
150+ ) ,
151+ stateDir : params . stateDir ? resolveUserPath ( params . stateDir , env ) : null ,
152+ workspaceDir : params . workspaceDir ? resolveUserPath ( params . workspaceDir , env ) : null ,
153+ } ) ;
154+ }
155+
156+ function fileFingerprint ( filePath : string ) : unknown {
157+ try {
158+ const stat = fs . statSync ( filePath , { bigint : true } ) ;
159+ const kind = stat . isFile ( ) ? "file" : stat . isDirectory ( ) ? "dir" : "other" ;
160+ return [ filePath , kind , stat . size . toString ( ) , stat . mtimeNs . toString ( ) , stat . ctimeNs . toString ( ) ] ;
161+ } catch {
162+ return [ filePath , "missing" ] ;
163+ }
164+ }
165+
166+ function findPluginRegistrySnapshotMemo (
167+ key : string | undefined ,
168+ ) : PluginRegistrySnapshotResult | undefined {
169+ if ( ! key ) {
170+ return undefined ;
171+ }
172+ const index = pluginRegistrySnapshotMemos . findIndex ( ( memo ) => memo . key === key ) ;
173+ if ( index === - 1 ) {
174+ return undefined ;
175+ }
176+ const [ memo ] = pluginRegistrySnapshotMemos . splice ( index , 1 ) ;
177+ if ( ! memo ) {
178+ return undefined ;
179+ }
180+ pluginRegistrySnapshotMemos . unshift ( memo ) ;
181+ return memo . result ;
182+ }
183+
184+ function rememberPluginRegistrySnapshotMemo (
185+ key : string | undefined ,
186+ result : PluginRegistrySnapshotResult ,
187+ ) : PluginRegistrySnapshotResult {
188+ if ( ! key ) {
189+ return result ;
190+ }
191+ pluginRegistrySnapshotMemos = [
192+ { key, result } ,
193+ ...pluginRegistrySnapshotMemos . filter ( ( memo ) => memo . key !== key ) ,
194+ ] . slice ( 0 , MAX_PLUGIN_REGISTRY_SNAPSHOT_MEMOS ) ;
195+ return result ;
196+ }
197+
76198function canReuseCurrentPluginMetadataSnapshot ( params : LoadPluginRegistryParams ) : boolean {
77199 return (
78200 params . preferPersisted !== false &&
@@ -288,6 +410,11 @@ export function loadPluginRegistrySnapshotWithMetadata(
288410 }
289411
290412 const env = params . env ?? process . env ;
413+ const memoKey = resolvePluginRegistrySnapshotMemoKey ( params , env ) ;
414+ const memo = findPluginRegistrySnapshotMemo ( memoKey ) ;
415+ if ( memo ) {
416+ return memo ;
417+ }
291418 const diagnostics : PluginRegistrySnapshotDiagnostic [ ] = [ ] ;
292419 const disabledByCaller = params . preferPersisted === false ;
293420 const disabledByEnv = hasEnvFlag ( env , DISABLE_PERSISTED_PLUGIN_REGISTRY_ENV ) ;
@@ -354,7 +481,7 @@ export function loadPluginRegistrySnapshotWithMetadata(
354481 source : "persisted" ,
355482 diagnostics,
356483 } ;
357- return persistedResult ;
484+ return rememberPluginRegistrySnapshotMemo ( memoKey , persistedResult ) ;
358485 }
359486 } else if ( persistedReadsEnabled ) {
360487 diagnostics . push ( {
@@ -379,12 +506,12 @@ export function loadPluginRegistrySnapshotWithMetadata(
379506 ? params . installRecords
380507 : ( params . installRecords ?? { } ) ,
381508 } ) ;
382- return {
509+ return rememberPluginRegistrySnapshotMemo ( memoKey , {
383510 snapshot : derived . index ,
384511 source : "derived" ,
385512 diagnostics,
386513 discovery : derived . discovery ,
387- } ;
514+ } ) ;
388515}
389516
390517function resolveSnapshot ( params : LoadPluginRegistryParams = { } ) : PluginRegistrySnapshot {
0 commit comments