fix(plugins): reuse gateway-bindable loader cache

This commit is contained in:
Peter Steinberger
2026-05-02 10:33:22 +01:00
parent f458a9f17f
commit 0a6584a231
3 changed files with 108 additions and 17 deletions

View File

@@ -30,6 +30,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- Plugins: reuse gateway-bindable plugin loader cache entries for later default-mode loads without serving default-built registries to gateway-bound requests, reducing repeated plugin registration during dispatch. Refs #61756. Thanks @DmitryPogodaev.
- Gateway/secrets: include the caught error message in `secrets.reload` and `secrets.resolve` warning logs while keeping RPC errors generic, so operators can diagnose reload and permission failures. Thanks @davidangularme.
- fix(infra): block workspace state-directory env override [AI]. (#75940) Thanks @pgondhi987.
- MCP/OpenAI: normalize parameter-free tool schemas whose top-level object `properties` is missing, null, or invalid before sending tools to OpenAI, so MCP tools without params stay usable. Fixes #75362. Thanks @tolkonepiu and @SymbolStar.

View File

@@ -3119,6 +3119,45 @@ module.exports = { id: "throws-after-import", register() {} };`,
delete (globalThis as Record<string, unknown>)[marker];
});
it("reuses a gateway-bindable cache entry for later default-mode loads", () => {
useNoBundledPlugins();
const marker = "__openclawGatewayBindableCacheRegisterCount";
const plugin = writePlugin({
id: "gateway-bindable-cache",
filename: "gateway-bindable-cache.cjs",
body: `module.exports = {
id: "gateway-bindable-cache",
register() {
globalThis.${marker} = (globalThis.${marker} || 0) + 1;
},
};`,
});
const options = {
workspaceDir: plugin.dir,
config: {
plugins: {
load: { paths: [plugin.file] },
allow: ["gateway-bindable-cache"],
entries: {
"gateway-bindable-cache": { enabled: true },
},
},
},
};
const gatewayBindable = loadOpenClawPlugins({
...options,
runtimeOptions: {
allowGatewaySubagentBinding: true,
},
});
const defaultMode = loadOpenClawPlugins(options);
expect(defaultMode).toBe(gatewayBindable);
expect((globalThis as Record<string, unknown>)[marker]).toBe(1);
delete (globalThis as Record<string, unknown>)[marker];
});
it("re-initializes global hook runner when serving registry from cache", () => {
useNoBundledPlugins();
const plugin = writePlugin({

View File

@@ -552,6 +552,50 @@ function setCachedPluginRegistry(
getPluginRegistryCache(onlyPluginIds).set(cacheKey, state);
}
function getReusableCachedPluginRegistry(params: {
cacheKey: string;
onlyPluginIds: string[];
runtimeSubagentMode: "default" | "explicit" | "gateway-bindable";
options: PluginLoadOptions;
}):
| {
state: CachedPluginState;
cacheKey: string;
runtimeSubagentMode: "default" | "explicit" | "gateway-bindable";
}
| 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,
};
}
function resolveBundledPackageRootForCache(stockRoot?: string): string | undefined {
if (!stockRoot) {
return undefined;
@@ -1332,31 +1376,38 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
const cacheEnabled = options.cache !== false;
if (cacheEnabled) {
const cached = getCachedPluginRegistry(cacheKey, onlyPluginIds);
const cached = getReusableCachedPluginRegistry({
cacheKey,
onlyPluginIds,
runtimeSubagentMode,
options,
});
if (cached) {
if (shouldActivate) {
restoreRegisteredAgentHarnesses(cached.agentHarnesses);
restorePluginCommands(cached.commands ?? []);
restoreRegisteredCompactionProviders(cached.compactionProviders);
restoreDetachedTaskLifecycleRuntimeRegistration(cached.detachedTaskRuntimeRegistration);
restorePluginInteractiveHandlers(cached.interactiveHandlers ?? []);
restoreRegisteredMemoryEmbeddingProviders(cached.memoryEmbeddingProviders);
restoreRegisteredAgentHarnesses(cached.state.agentHarnesses);
restorePluginCommands(cached.state.commands ?? []);
restoreRegisteredCompactionProviders(cached.state.compactionProviders);
restoreDetachedTaskLifecycleRuntimeRegistration(
cached.state.detachedTaskRuntimeRegistration,
);
restorePluginInteractiveHandlers(cached.state.interactiveHandlers ?? []);
restoreRegisteredMemoryEmbeddingProviders(cached.state.memoryEmbeddingProviders);
restoreMemoryPluginState({
capability: cached.memoryCapability,
corpusSupplements: cached.memoryCorpusSupplements,
promptBuilder: cached.memoryPromptBuilder,
promptSupplements: cached.memoryPromptSupplements,
flushPlanResolver: cached.memoryFlushPlanResolver,
runtime: cached.memoryRuntime,
capability: cached.state.memoryCapability,
corpusSupplements: cached.state.memoryCorpusSupplements,
promptBuilder: cached.state.memoryPromptBuilder,
promptSupplements: cached.state.memoryPromptSupplements,
flushPlanResolver: cached.state.memoryFlushPlanResolver,
runtime: cached.state.memoryRuntime,
});
activatePluginRegistry(
cached.registry,
cacheKey,
runtimeSubagentMode,
cached.state.registry,
cached.cacheKey,
cached.runtimeSubagentMode,
options.workspaceDir,
);
}
return cached.registry;
return cached.state.registry;
}
}
pluginLoaderCacheState.beginLoad(cacheKey);