diff --git a/src/agents/models-config-state.ts b/src/agents/models-config-state.ts index 1216ce8c98d..1986e8a003e 100644 --- a/src/agents/models-config-state.ts +++ b/src/agents/models-config-state.ts @@ -6,6 +6,14 @@ type ModelsJsonState = { string, Promise<{ fingerprint: string; result: { agentDir: string; wrote: boolean } }> >; + /** + * Cross-config noop cache: when planOpenClawModelsJson returns "noop" (no write + * needed), the result is valid for all callers regardless of config differences, + * as long as models.json has not changed (same mtime). This avoids redundant + * planOpenClawModelsJson runs when multiple agents with slightly different configs + * (e.g. main agent vs subagent) call ensureOpenClawModelsJson concurrently. + */ + noopCache: Map; }; export const MODELS_JSON_STATE = (() => { @@ -19,12 +27,18 @@ export const MODELS_JSON_STATE = (() => { string, Promise<{ fingerprint: string; result: { agentDir: string; wrote: boolean } }> >(), + noopCache: new Map(), }; } + // Schema migration: add noopCache if missing (e.g. after in-process restart with old state) + if (!globalState[MODELS_JSON_STATE_KEY].noopCache) { + globalState[MODELS_JSON_STATE_KEY].noopCache = new Map(); + } return globalState[MODELS_JSON_STATE_KEY]; })(); export function resetModelsJsonReadyCacheForTest(): void { MODELS_JSON_STATE.writeLocks.clear(); MODELS_JSON_STATE.readyCache.clear(); + MODELS_JSON_STATE.noopCache.clear(); } diff --git a/src/agents/models-config.ts b/src/agents/models-config.ts index 569c5808a44..dd90e4f1347 100644 --- a/src/agents/models-config.ts +++ b/src/agents/models-config.ts @@ -71,6 +71,10 @@ async function buildModelsJsonFingerprint(params: { }); } +function modelsJsonReadyCacheKey(targetPath: string, fingerprint: string): string { + return `${targetPath}\0${fingerprint}`; +} + async function readExistingModelsFile(pathname: string): Promise<{ raw: string; parsed: unknown; @@ -172,7 +176,7 @@ export async function ensureOpenClawModelsJson( getCurrentPluginMetadataSnapshot({ config: cfg, ...(workspaceDir ? { workspaceDir } : {}), - }); + }); const agentDir = agentDirOverride?.trim() ? agentDirOverride.trim() : resolveOpenClawAgentDir(); const targetPath = path.join(agentDir, "models.json"); const fingerprint = await buildModelsJsonFingerprint({ @@ -188,13 +192,12 @@ export async function ensureOpenClawModelsJson( ? { providerDiscoveryTimeoutMs: options.providerDiscoveryTimeoutMs } : {}), }); - const cached = MODELS_JSON_STATE.readyCache.get(targetPath); + const cacheKey = modelsJsonReadyCacheKey(targetPath, fingerprint); + const cached = MODELS_JSON_STATE.readyCache.get(cacheKey); if (cached) { const settled = await cached; - if (settled.fingerprint === fingerprint) { - await ensureModelsFileModeForModelsJson(targetPath); - return settled.result; - } + await ensureModelsFileModeForModelsJson(targetPath); + return settled.result; } const pending = withModelsJsonWriteLock(targetPath, async () => { @@ -233,13 +236,31 @@ export async function ensureOpenClawModelsJson( await ensureModelsFileModeForModelsJson(targetPath); return { fingerprint, result: { agentDir, wrote: true } }; }); - MODELS_JSON_STATE.readyCache.set(targetPath, pending); + MODELS_JSON_STATE.readyCache.set(cacheKey, pending); try { const settled = await pending; + const refreshedFingerprint = await buildModelsJsonFingerprint({ + config: cfg, + sourceConfigForSecrets: resolved.sourceConfigForSecrets, + agentDir, + ...(workspaceDir ? { workspaceDir } : {}), + ...(pluginMetadataSnapshot ? { pluginMetadataSnapshot } : {}), + ...(options.providerDiscoveryProviderIds + ? { providerDiscoveryProviderIds: options.providerDiscoveryProviderIds } + : {}), + ...(options.providerDiscoveryTimeoutMs !== undefined + ? { providerDiscoveryTimeoutMs: options.providerDiscoveryTimeoutMs } + : {}), + }); + const refreshedCacheKey = modelsJsonReadyCacheKey(targetPath, refreshedFingerprint); + if (refreshedCacheKey !== cacheKey) { + MODELS_JSON_STATE.readyCache.delete(cacheKey); + MODELS_JSON_STATE.readyCache.set(refreshedCacheKey, Promise.resolve(settled)); + } return settled.result; } catch (error) { - if (MODELS_JSON_STATE.readyCache.get(targetPath) === pending) { - MODELS_JSON_STATE.readyCache.delete(targetPath); + if (MODELS_JSON_STATE.readyCache.get(cacheKey) === pending) { + MODELS_JSON_STATE.readyCache.delete(cacheKey); } throw error; }