fix: preserve gateway-bindable loader compatibility

This commit is contained in:
Peter Steinberger
2026-04-06 15:25:22 +01:00
parent c78defdc2f
commit fa67ab2358
3 changed files with 97 additions and 35 deletions

View File

@@ -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();
});

View File

@@ -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({

View File

@@ -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(