fix: cache capability provider manifest ids

This commit is contained in:
Shakker
2026-04-27 09:18:05 +01:00
parent e21c909bd0
commit e792f96a84
3 changed files with 111 additions and 2 deletions

View File

@@ -502,6 +502,27 @@ describe("resolvePluginCapabilityProviders", () => {
});
});
it("reuses manifest-derived capability plugin ids for the same config snapshot", () => {
const { cfg, enablementCompat } = createCompatChainConfig();
setBundledCapabilityFixture("mediaUnderstandingProviders");
mocks.withBundledPluginEnablementCompat.mockReturnValue(enablementCompat);
mocks.withBundledPluginVitestCompat.mockReturnValue(enablementCompat);
expectNoResolvedCapabilityProviders(
resolvePluginCapabilityProviders({ key: "mediaUnderstandingProviders", cfg }),
);
expectNoResolvedCapabilityProviders(
resolvePluginCapabilityProviders({ key: "mediaUnderstandingProviders", cfg }),
);
expect(mocks.loadPluginManifestRegistry).toHaveBeenCalledOnce();
expect(mocks.withBundledPluginAllowlistCompat).toHaveBeenCalledTimes(2);
expect(mocks.withBundledPluginAllowlistCompat).toHaveBeenCalledWith({
config: cfg,
pluginIds: ["openai"],
});
});
it("reuses a compatible active registry even when the capability list is empty", () => {
const active = createEmptyPluginRegistry();
mocks.resolveRuntimePluginRegistry.mockReturnValue(active);

View File

@@ -4,6 +4,11 @@ import {
withBundledPluginEnablementCompat,
withBundledPluginVitestCompat,
} from "./bundled-compat.js";
import {
buildPluginSnapshotCacheEnvKey,
resolvePluginSnapshotCacheTtlMs,
shouldUsePluginSnapshotCache,
} from "./cache-controls.js";
import { hasExplicitPluginConfig } from "./config-policy.js";
import { resolveRuntimePluginRegistry } from "./loader.js";
import { loadPluginManifestRegistryForPluginRegistry } from "./plugin-registry.js";
@@ -32,6 +37,11 @@ type CapabilityContractKey =
type CapabilityProviderForKey<K extends CapabilityProviderRegistryKey> =
PluginRegistry[K][number] extends { provider: infer T } ? T : never;
type CapabilityProviderPluginIdCacheEntry = {
expiresAt: number;
pluginIds: string[];
};
const CAPABILITY_CONTRACT_KEY: Record<CapabilityProviderRegistryKey, CapabilityContractKey> = {
memoryEmbeddingProviders: "memoryEmbeddingProviders",
speechProviders: "speechProviders",
@@ -43,15 +53,86 @@ const CAPABILITY_CONTRACT_KEY: Record<CapabilityProviderRegistryKey, CapabilityC
musicGenerationProviders: "musicGenerationProviders",
};
const capabilityProviderPluginIdCache = new WeakMap<
OpenClawConfig,
WeakMap<NodeJS.ProcessEnv, Map<string, CapabilityProviderPluginIdCacheEntry>>
>();
function buildCapabilityProviderPluginIdCacheKey(params: {
key: CapabilityProviderRegistryKey;
env: NodeJS.ProcessEnv;
providerId?: string;
}): string {
return JSON.stringify({
key: params.key,
providerId: params.providerId ?? "",
env: buildPluginSnapshotCacheEnvKey(params.env),
});
}
function getCachedCapabilityProviderPluginIds(params: {
key: CapabilityProviderRegistryKey;
cfg?: OpenClawConfig;
env: NodeJS.ProcessEnv;
providerId?: string;
}): string[] | undefined {
if (!params.cfg || !shouldUsePluginSnapshotCache(params.env)) {
return undefined;
}
const envCache = capabilityProviderPluginIdCache.get(params.cfg)?.get(params.env);
const cached = envCache?.get(buildCapabilityProviderPluginIdCacheKey(params));
if (!cached || cached.expiresAt <= Date.now()) {
return undefined;
}
return [...cached.pluginIds];
}
function memoizeCapabilityProviderPluginIds(params: {
key: CapabilityProviderRegistryKey;
cfg?: OpenClawConfig;
env: NodeJS.ProcessEnv;
providerId?: string;
pluginIds: string[];
}): void {
if (!params.cfg || !shouldUsePluginSnapshotCache(params.env)) {
return;
}
let configCache = capabilityProviderPluginIdCache.get(params.cfg);
if (!configCache) {
configCache = new WeakMap<
NodeJS.ProcessEnv,
Map<string, CapabilityProviderPluginIdCacheEntry>
>();
capabilityProviderPluginIdCache.set(params.cfg, configCache);
}
let envCache = configCache.get(params.env);
if (!envCache) {
envCache = new Map<string, CapabilityProviderPluginIdCacheEntry>();
configCache.set(params.env, envCache);
}
envCache.set(buildCapabilityProviderPluginIdCacheKey(params), {
expiresAt: Date.now() + resolvePluginSnapshotCacheTtlMs(params.env),
pluginIds: [...params.pluginIds],
});
}
function resolveBundledCapabilityCompatPluginIds(params: {
key: CapabilityProviderRegistryKey;
cfg?: OpenClawConfig;
providerId?: string;
}): string[] {
const env = process.env;
const cached = getCachedCapabilityProviderPluginIds({
...params,
env,
});
if (cached) {
return cached;
}
const contractKey = CAPABILITY_CONTRACT_KEY[params.key];
return loadPluginManifestRegistryForPluginRegistry({
const pluginIds = loadPluginManifestRegistryForPluginRegistry({
config: params.cfg,
env: process.env,
env,
includeDisabled: true,
})
.plugins.filter(
@@ -62,6 +143,12 @@ function resolveBundledCapabilityCompatPluginIds(params: {
)
.map((plugin) => plugin.id)
.toSorted((left, right) => left.localeCompare(right));
memoizeCapabilityProviderPluginIds({
...params,
env,
pluginIds,
});
return pluginIds;
}
function resolveCapabilityProviderConfig(params: {