diff --git a/CHANGELOG.md b/CHANGELOG.md index 81c49997cd6..f3b1745b4f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -306,6 +306,7 @@ Docs: https://docs.openclaw.ai - Security/Windows: route the `.cmd`/`.bat` process wrapper through the shared Windows install-root resolver instead of `process.env.ComSpec`, so workspace dotenv-blocked `SystemRoot`/`WINDIR` overrides and unsafe values like UNC paths or path-lists cannot redirect `cmd.exe` selection on Windows. (#77472) Thanks @drobison00. - Agents/bootstrap: honor `BOOTSTRAP.md` content injected by `agent:bootstrap` hooks when deciding whether bootstrap is pending, so hook-provided required setup instructions are included in the system prompt. (#77501) Thanks @ificator. - Agents/replay-history: drop trailing assistant turns whose content is empty or carries only the stream-error sentinel before sending the transcript to the provider, so prefill-strict providers (such as github-copilot/claude-opus-4.6) no longer reject the request with `400 The conversation must end with a user message` after a session whose last turn errored before producing content. Refs #77228. (#77287) Thanks @openperf. +- Gateway/sessions: cache selected model override resolution while building session-list rows so `openclaw sessions` and Control UI session lists stay responsive on model-heavy stores. (#77650) Thanks @ragesaq. ## 2026.5.3-1 diff --git a/src/gateway/session-utils.ts b/src/gateway/session-utils.ts index 1a3ac6664a2..56ba9ddaa19 100644 --- a/src/gateway/session-utils.ts +++ b/src/gateway/session-utils.ts @@ -372,6 +372,7 @@ function shouldKeepStoreOnlyChildLink(entry: SessionEntry, now: number): boolean type SessionListRowContext = { subagentRuns: ReturnType; storeChildSessionsByKey: Map; + selectedModelByOverrideRef: Map>; thinkingLevelsByModelRef: Map>; }; @@ -489,6 +490,7 @@ function buildSessionListRowContext(params: { return { subagentRuns, storeChildSessionsByKey: buildStoreChildSessionIndex(params.store, params.now, subagentRuns), + selectedModelByOverrideRef: new Map(), thinkingLevelsByModelRef: new Map(), }; } @@ -497,6 +499,36 @@ function createSessionRowModelCacheKey(provider: string | undefined, model: stri return `${normalizeLowercaseStringOrEmpty(provider)}\0${normalizeOptionalString(model) ?? ""}`; } +function resolveSessionSelectedModelRef(params: { + cfg: OpenClawConfig; + entry?: SessionEntry; + agentId: string; + rowContext?: SessionListRowContext; +}): ReturnType | null { + const override = normalizeStoredOverrideModel({ + providerOverride: params.entry?.providerOverride, + modelOverride: params.entry?.modelOverride, + }); + if (!override.modelOverride) { + return null; + } + if (!params.rowContext) { + return resolveSessionModelRef(params.cfg, params.entry, params.agentId); + } + const key = [ + normalizeAgentId(params.agentId), + override.providerOverride ?? "", + override.modelOverride, + ].join("\0"); + const cached = params.rowContext.selectedModelByOverrideRef.get(key); + if (cached) { + return cached; + } + const selected = resolveSessionModelRef(params.cfg, params.entry, params.agentId); + params.rowContext.selectedModelByOverrideRef.set(key, selected); + return selected; +} + function resolveSessionRowThinkingLevels(params: { provider: string; model: string; @@ -1540,9 +1572,12 @@ export function buildGatewaySessionRow(params: { ? resolveSessionRuntimeMs(subagentRun, now) : undefined)) : undefined; - const selectedModel = entry?.modelOverride?.trim() - ? resolveSessionModelRef(cfg, entry, sessionAgentId) - : null; + const selectedModel = resolveSessionSelectedModelRef({ + cfg, + entry, + agentId: sessionAgentId, + rowContext, + }); const resolvedModel = resolveSessionModelIdentityRef( cfg, entry,