diff --git a/src/plugins/loader.runtime-registry.test.ts b/src/plugins/loader.runtime-registry.test.ts index 752b550ab31..4e6be841509 100644 --- a/src/plugins/loader.runtime-registry.test.ts +++ b/src/plugins/loader.runtime-registry.test.ts @@ -18,6 +18,7 @@ import { } from "./memory-state.js"; import { createEmptyPluginRegistry } from "./registry.js"; import { setActivePluginRegistry } from "./runtime.js"; +import type { CreatePluginRuntimeOptions } from "./runtime/index.js"; afterEach(() => { resetPluginLoaderTestStateForTest(); @@ -39,7 +40,7 @@ describe("getCompatibleActivePluginRegistry", () => { }, }; const { cacheKey } = __testing.resolvePluginLoadCacheContext(loadOptions); - setActivePluginRegistry(registry, cacheKey); + setActivePluginRegistry(registry, cacheKey, "gateway-bindable"); expect(__testing.getCompatibleActivePluginRegistry(loadOptions)).toBe(registry); expect( @@ -59,6 +60,38 @@ describe("getCompatibleActivePluginRegistry", () => { ...loadOptions, runtimeOptions: undefined, }), + ).toBe(registry); + expect( + __testing.getCompatibleActivePluginRegistry({ + ...loadOptions, + runtimeOptions: { + subagent: {} as CreatePluginRuntimeOptions["subagent"], + }, + }), + ).toBeUndefined(); + }); + + it("does not treat a default-mode active registry as compatible with gateway binding", () => { + const registry = createEmptyPluginRegistry(); + const loadOptions = { + config: { + plugins: { + allow: ["demo"], + load: { paths: ["/tmp/demo.js"] }, + }, + }, + workspaceDir: "/tmp/workspace-a", + }; + const { cacheKey } = __testing.resolvePluginLoadCacheContext(loadOptions); + setActivePluginRegistry(registry, cacheKey, "default"); + + expect( + __testing.getCompatibleActivePluginRegistry({ + ...loadOptions, + runtimeOptions: { + allowGatewaySubagentBinding: true, + }, + }), ).toBeUndefined(); }); diff --git a/src/plugins/loader.test.ts b/src/plugins/loader.test.ts index 61c810ae615..2235bbb87ac 100644 --- a/src/plugins/loader.test.ts +++ b/src/plugins/loader.test.ts @@ -43,6 +43,7 @@ import { listImportedRuntimePluginIds, setActivePluginRegistry, } from "./runtime.js"; +import type { PluginSdkResolutionPreference } from "./sdk-alias.js"; let cachedBundledTelegramDir = ""; let cachedBundledMemoryDir = ""; const BUNDLED_TELEGRAM_PLUGIN_BODY = `module.exports = { @@ -1779,7 +1780,41 @@ module.exports = { id: "throws-after-import", register() {} };`, loadVariant: () => loadOpenClawPlugins({ ...options, - pluginSdkResolution: "workspace" as const, + pluginSdkResolution: "workspace" as PluginSdkResolutionPreference, + }), + }; + }, + }, + { + name: "does not reuse cached registries across gateway subagent binding modes", + setup: () => { + useNoBundledPlugins(); + const plugin = writePlugin({ + id: "cache-gateway-shared", + filename: "cache-gateway-shared.cjs", + body: `module.exports = { id: "cache-gateway-shared", register() {} };`, + }); + + const options = { + workspaceDir: plugin.dir, + config: { + plugins: { + allow: ["cache-gateway-shared"], + load: { + paths: [plugin.file], + }, + }, + }, + }; + + return { + loadFirst: () => loadOpenClawPlugins(options), + loadVariant: () => + loadOpenClawPlugins({ + ...options, + runtimeOptions: { + allowGatewaySubagentBinding: true, + }, }), }; }, @@ -1788,34 +1823,6 @@ module.exports = { id: "throws-after-import", register() {} };`, expectCacheMissThenHit(setup()); }); - it("reuses cached registry across gateway subagent binding modes", () => { - useNoBundledPlugins(); - const plugin = writePlugin({ - id: "cache-gateway-shared", - filename: "cache-gateway-shared.cjs", - body: `module.exports = { id: "cache-gateway-shared", register() {} };`, - }); - - const options = { - workspaceDir: plugin.dir, - config: { - plugins: { - allow: ["cache-gateway-shared"], - load: { - paths: [plugin.file], - }, - }, - }, - }; - - const first = loadOpenClawPlugins(options); - const second = loadOpenClawPlugins({ - ...options, - runtimeOptions: { allowGatewaySubagentBinding: true }, - }); - expect(second).toBe(first); - }); - it("evicts least recently used registries when the loader cache exceeds its cap", () => { useNoBundledPlugins(); const plugin = writePlugin({ diff --git a/src/plugins/loader.ts b/src/plugins/loader.ts index 562ade0b999..e6a43344a00 100644 --- a/src/plugins/loader.ts +++ b/src/plugins/loader.ts @@ -49,6 +49,7 @@ import { resolvePluginCacheInputs } from "./roots.js"; import { getActivePluginRegistry, getActivePluginRegistryKey, + getActivePluginRuntimeSubagentMode, recordImportedPluginId, setActivePluginRegistry, } from "./runtime.js"; @@ -285,6 +286,7 @@ function buildCacheKey(params: { includeSetupOnlyChannelPlugins?: boolean; preferSetupRuntimeForChannelPlugins?: boolean; loadModules?: boolean; + runtimeSubagentMode?: "default" | "explicit" | "gateway-bindable"; pluginSdkResolution?: PluginSdkResolutionPreference; coreGatewayMethodNames?: string[]; }): string { @@ -314,13 +316,14 @@ function buildCacheKey(params: { const startupChannelMode = params.preferSetupRuntimeForChannelPlugins === true ? "prefer-setup" : "full"; const moduleLoadMode = params.loadModules === false ? "manifest-only" : "load-modules"; + const runtimeSubagentMode = params.runtimeSubagentMode ?? "default"; const gatewayMethodsKey = JSON.stringify(params.coreGatewayMethodNames ?? []); return `${roots.workspace ?? ""}::${roots.global ?? ""}::${roots.stock ?? ""}::${JSON.stringify({ ...params.plugins, installs, loadPaths, activationMetadataKey: params.activationMetadataKey ?? "", - })}::${scopeKey}::${setupOnlyKey}::${startupChannelMode}::${moduleLoadMode}::${params.pluginSdkResolution ?? "auto"}::${gatewayMethodsKey}`; + })}::${scopeKey}::${setupOnlyKey}::${startupChannelMode}::${moduleLoadMode}::${runtimeSubagentMode}::${params.pluginSdkResolution ?? "auto"}::${gatewayMethodsKey}`; } function normalizeScopedPluginIds(ids?: string[]): string[] | undefined { @@ -419,6 +422,7 @@ function resolvePluginLoadCacheContext(options: PluginLoadOptions = {}) { const onlyPluginIds = normalizeScopedPluginIds(options.onlyPluginIds); const includeSetupOnlyChannelPlugins = options.includeSetupOnlyChannelPlugins === true; const preferSetupRuntimeForChannelPlugins = options.preferSetupRuntimeForChannelPlugins === true; + const runtimeSubagentMode = resolveRuntimeSubagentMode(options.runtimeOptions); const coreGatewayMethodNames = Object.keys(options.coreGatewayHandlers ?? {}).toSorted(); const cacheKey = buildCacheKey({ workspaceDir: options.workspaceDir, @@ -433,6 +437,7 @@ function resolvePluginLoadCacheContext(options: PluginLoadOptions = {}) { includeSetupOnlyChannelPlugins, preferSetupRuntimeForChannelPlugins, loadModules: options.loadModules, + runtimeSubagentMode, pluginSdkResolution: options.pluginSdkResolution, coreGatewayMethodNames, }); @@ -448,7 +453,7 @@ function resolvePluginLoadCacheContext(options: PluginLoadOptions = {}) { preferSetupRuntimeForChannelPlugins, shouldActivate: options.activate !== false, shouldLoadModules: options.loadModules !== false, - runtimeSubagentMode: resolveRuntimeSubagentMode(options.runtimeOptions), + runtimeSubagentMode, cacheKey, }; } @@ -467,9 +472,26 @@ function getCompatibleActivePluginRegistry( if (!activeCacheKey) { return undefined; } - return resolvePluginLoadCacheContext(options).cacheKey === activeCacheKey - ? activeRegistry - : undefined; + const loadContext = resolvePluginLoadCacheContext(options); + if (loadContext.cacheKey === activeCacheKey) { + return activeRegistry; + } + if ( + loadContext.runtimeSubagentMode === "default" && + getActivePluginRuntimeSubagentMode() === "gateway-bindable" + ) { + const gatewayBindableCacheKey = resolvePluginLoadCacheContext({ + ...options, + runtimeOptions: { + ...options.runtimeOptions, + allowGatewaySubagentBinding: true, + }, + }).cacheKey; + if (gatewayBindableCacheKey === activeCacheKey) { + return activeRegistry; + } + } + return undefined; } export function resolveRuntimePluginRegistry(