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

@@ -34,6 +34,7 @@ Docs: https://docs.openclaw.ai
- Plugins/startup: carry the Gateway `PluginLookUpTable` into deferred channel full-runtime reloads so post-listen startup does not rebuild manifest metadata after the provisional setup-runtime load. Thanks @shakkernerd.
- Gateway/startup: extend `OPENCLAW_GATEWAY_STARTUP_TRACE=1` with per-phase event-loop delay plus plugin lookup-table timing and count metrics for installed-index, manifest, startup-plan, and owner-map work, and include the new timing fields in startup benchmark summaries. Thanks @shakkernerd.
- Plugins/channels: resolve read-only channel command defaults from one plugin index plus manifest pass instead of reloading plugin metadata while checking candidate plugin enablement. Thanks @shakkernerd.
- Plugins/capabilities: cache manifest-derived capability provider plugin IDs per config snapshot so repeated TTS, media, realtime, memory, image, video, and music provider resolution avoids redundant manifest scans. Thanks @shakkernerd.
- Plugins/contracts: resolve runtime manifest-contract plugin owners from one plugin index plus manifest pass instead of rebuilding manifest metadata separately for all owners and enabled owners. Thanks @shakkernerd.
- Plugins/extractors: reuse one manifest registry pass while resolving bundled document and web-content extractor plugins instead of rereading manifests for compatibility and enablement filtering. Thanks @shakkernerd.
- Plugins/registry: resolve lookup-table owner maps for providers, CLI backends, setup providers, command aliases, model catalogs, channel configs, and manifest contracts while preserving setup-only CLI backend ownership. Thanks @shakkernerd.

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: {