From 0a6584a231b445dacb378d7012634d4697972bb8 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 2 May 2026 10:33:22 +0100 Subject: [PATCH] fix(plugins): reuse gateway-bindable loader cache --- CHANGELOG.md | 1 + src/plugins/loader.test.ts | 39 +++++++++++++++++ src/plugins/loader.ts | 85 ++++++++++++++++++++++++++++++-------- 3 files changed, 108 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c2abfbb3978..53f3c6dd542 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/src/plugins/loader.test.ts b/src/plugins/loader.test.ts index a6a7598d0c9..816cbc8f4f7 100644 --- a/src/plugins/loader.test.ts +++ b/src/plugins/loader.test.ts @@ -3119,6 +3119,45 @@ module.exports = { id: "throws-after-import", register() {} };`, delete (globalThis as Record)[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)[marker]).toBe(1); + delete (globalThis as Record)[marker]; + }); + it("re-initializes global hook runner when serving registry from cache", () => { useNoBundledPlugins(); const plugin = writePlugin({ diff --git a/src/plugins/loader.ts b/src/plugins/loader.ts index 0cf33530ad6..5b6c4cc936a 100644 --- a/src/plugins/loader.ts +++ b/src/plugins/loader.ts @@ -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);