Files
openclaw/src/plugins/loader-cache.ts
Peter Steinberger 9ff855db05 fix(plugins): canonicalize plugin entry path per process to prevent duplicate register() (#108508)
On git-channel installs where both extensions/<id>/index.ts and
dist/extensions/<id>/index.js exist, two registry assemblies resolved the same
plugin id to different physical files (preferBuiltPluginArtifacts drives both the
artifact choice and the load-cache key), so the path-keyed module cache
evaluated the entry twice — two live plugin instances with separate state,
binding hooks to one and registerTool to the other. Memoize the first resolved
runtime entry path per plugin-id + real root + entry-kind so both assemblies
converge on one module instance; clear the memo on activated-runtime-state and
registry-load-cache resets so reload/install/doctor re-resolve. No-op on
dist-only installs.

Closes #107933
2026-07-15 21:58:13 -07:00

85 lines
2.7 KiB
TypeScript

import { pluginLoaderCacheInstances, type CachedPluginState } from "./loader-cache-instances.js";
import { resolvePluginLoadCacheContext } from "./loader-load-context.js";
import type { PluginLoadOptions, PluginRuntimeSubagentMode } from "./loader-types.js";
import { clearPluginRuntimeArtifactResolutionMemo } from "./plugin-runtime-artifact-resolution.js";
export const pluginLoaderCacheState = pluginLoaderCacheInstances.scoped;
const fullWorkspacePluginLoaderCacheState = pluginLoaderCacheInstances.fullWorkspace;
function getPluginRegistryCache(onlyPluginIds?: string[]) {
return onlyPluginIds ? pluginLoaderCacheState : fullWorkspacePluginLoaderCacheState;
}
function getCachedPluginRegistry(
cacheKey: string,
onlyPluginIds?: string[],
): CachedPluginState | undefined {
return getPluginRegistryCache(onlyPluginIds).get(cacheKey);
}
export function setCachedPluginRegistry(
cacheKey: string,
state: CachedPluginState,
onlyPluginIds?: string[],
): void {
getPluginRegistryCache(onlyPluginIds).set(cacheKey, state);
}
export function getReusableCachedPluginRegistry(params: {
cacheKey: string;
onlyPluginIds: string[] | undefined;
runtimeSubagentMode: PluginRuntimeSubagentMode;
options: PluginLoadOptions;
}):
| {
state: CachedPluginState;
cacheKey: string;
runtimeSubagentMode: PluginRuntimeSubagentMode;
}
| undefined {
const exact = getCachedPluginRegistry(params.cacheKey, params.onlyPluginIds);
if (exact) {
return {
state: exact,
cacheKey: params.cacheKey,
runtimeSubagentMode: params.runtimeSubagentMode,
};
}
if (params.runtimeSubagentMode !== "default") {
return undefined;
}
const gatewayBindableContext = resolvePluginLoadCacheContext({
...params.options,
runtimeOptions: {
...params.options.runtimeOptions,
allowGatewaySubagentBinding: true,
},
});
const gatewayBindable = getCachedPluginRegistry(
gatewayBindableContext.cacheKey,
gatewayBindableContext.onlyPluginIds,
);
if (!gatewayBindable) {
return undefined;
}
return {
state: gatewayBindable,
cacheKey: gatewayBindableContext.cacheKey,
runtimeSubagentMode: gatewayBindableContext.runtimeSubagentMode,
};
}
export function clearPluginRegistryLoadCache(): void {
clearPluginRuntimeArtifactResolutionMemo();
pluginLoaderCacheState.clearCachedRegistries();
fullWorkspacePluginLoaderCacheState.clearCachedRegistries();
}
export function resolvePluginRegistryLoadCacheKey(options: PluginLoadOptions = {}): string {
return resolvePluginLoadCacheContext(options).cacheKey;
}
export function isPluginRegistryLoadInFlight(options: PluginLoadOptions = {}): boolean {
return pluginLoaderCacheState.isLoadInFlight(resolvePluginRegistryLoadCacheKey(options));
}