摘要 (Summary)
cooldownProbeUsedProviders is a module-level Set that lives for the entire gateway process. It is populated by every error classified as rate_limit / overloaded / timeout (including the client-side LLM idle timeout (120s): no response from model, which is not a model failure at all) / empty_response / unknown / unclassified / no_error_details. Once a provider is in the set, every subsequent run within the same process skips it — even across sessions and agents. The /model slash command only mutates the agent primary in config; it does NOT touch this Set, so user-side recovery is impossible. The only escape hatch is a full kill of the gateway process.
This is reproducible, has a clear source-level root cause, and the fix is small.
复现步骤 (Reproduction)
Environment: openclaw v2026.5.x (reproduced on 2026.5.28 and 2026.5.3-1), MiniMax-M3 (1.0M context) as agents.defaults.model.primary, MiniMax-M2.7 as first fallback, Qwen/Qwen3.5-397B-A17B second, exo/Qwen3.5-2B-MLX-8bit third. Repro is provider-agnostic — the issue is in runWithModelFallback, not the providers.
- Configure a primary model that can fail with a
timeout / overloaded / empty_response transient error (or just let the agent run a long-running turn that triggers the 120 s streaming idle timeout).
- Let the primary turn abort / fail with that error.
- Verify with
openclaw gateway status (or any available observability) that the provider's cooldown slot is occupied.
- From the same gateway process, in the Control UI:
- run
/model <another model on the same provider> — observe that the new model is not tried on the next turn, because the provider is still in the cooldown set.
- open a brand-new session via
/new or a fresh dashboard session — observe the same: the provider is still in the cooldown set.
- Run
openclaw gateway restart (or kill -TERM <gateway-pid> and let it respawn).
- From the now-fresh process, retry the same turns — they succeed. The only recovery is process-level.
期望 vs 实际 (Expected vs Actual)
Expected
- A failed primary model turn should be retryable, either automatically after the cooldown window expires or via an explicit user action (e.g.
/model, /reset-cooldown, openclaw models reset-cooldown).
- The user should be able to see the cooldown state of every provider/model so they understand why the agent is "stuck" on a fallback.
- The OpenClaw client should NOT classify its own 120 s
streamWithIdleTimeout as a model-side timeout — that is a client-side measurement and could just as easily be triggered by a slow network or a slow transformers startup.
Actual
cooldownProbeUsedProviders is populated by transient-client-side errors and is never cleared except by a process restart.
/model does not clear it.
- The Control UI does not surface the cooldown state at all.
- Manual recovery requires killing the gateway, which the user only discovers by accident after they
pkill node and find that everything starts working again.
根因(源码定位)— Root cause with source references
All paths are inside the openclaw package install (e.g. /opt/homebrew/lib/node_modules/openclaw/dist/). Bundle names are abbreviated.
1. The cooldown Set is module-level
dist/model-fallback--*.js
// Module top-level — global to the gateway process
const cooldownProbeUsedProviders = new Set();
// Once a provider is in the set, every subsequent run within the
// same process skips it (per-run + cross-session + cross-agent)
if (isTransientCooldownReason && cooldownProbeUsedProviders.has(candidate.provider)) {
const error = `Provider ${candidate.provider} is in cooldown (probe already attempted this run)`;
attempts.push({ provider: candidate.provider, error, reason: decision.reason });
decision: "skip_candidate",
// ...
}
if (!shouldPreserveTransientCooldownProbeSlot(probeFailureReason)) {
cooldownProbeUsedProviders.add(transientProbeProviderForAttempt);
}
2. The reason classifier considers the client-side idle timeout as timeout
dist/selection-*.js (client-side streaming wrapper):
function streamWithIdleTimeout(baseFn, timeoutMs, onIdleTimeout) {
return (model, context, options) => {
const createIdleTimeoutError = () =>
new Error(`LLM idle timeout (${Math.floor(timeoutMs / 1e3)}s): no response from model`);
const createTimeoutPromise = (setTimer) => {
return new Promise((_, reject) => {
const timer = setTimeout(() => {
const error = createIdleTimeoutError();
abortStream(error);
onIdleTimeout?.(error);
reject(error);
}, timeoutMs);
});
};
// ...
};
}
dist/errors-*.js (reason classification):
// classifyFailoverReasonFromCode:
default: return TIMEOUT_ERROR_CODES.has(normalized) ? "timeout" : null;
dist/model-fallback--*.js (cooldown classifier):
function shouldUseTransientCooldownProbeSlot(reason) {
return reason === "rate_limit"
|| reason === "overloaded"
|| reason === "timeout" // <-- LLM idle timeout ends up here
|| reason === "empty_response"
|| reason === "unknown"
|| reason === "unclassified"
|| reason === "no_error_details";
}
The LLM idle timeout (120s): no response from model error is generated by the OpenClaw client. It is not an API response. The provider never had a chance to return overloaded or rate_limit; the socket might have been mid-stream when the 120 s timer fired. Treating it as a provider-side timeout and adding the provider to the cooldown set is a category error: it confuses "the model did not produce anything for 120 s" (which can be slow-but-fine, e.g. a long-thinking turn on MiniMax-M3 with a 32k-token input) with "the provider is degraded and we should back off".
3. /model does not reset the cooldown
dist/get-reply-*.js and dist/agent-scope-*.js:
runAutoFallbackPrimaryProbe reads from the per-session modelOverride, never from the global cooldownProbeUsedProviders.
clearAutoFallbackPrimaryProbeSelection clears the session entry, not the module-level Set.
There is no command path that calls cooldownProbeUsedProviders.clear(). Only a process restart reaches Set initialisation.
4. The set is not observable
There is no Control UI panel, no openclaw models list --cooldown, no openclaw status line for it. Users have no way to know why a model is no longer being attempted.
建议修复(按优先级)— Suggested fixes
P0 — Stop the mis-classification
Either
(a) Exclude the client-side idle timeout from shouldUseTransientCooldownProbeSlot — change the timeout branch to be a finer signal that distinguishes "provider reported timeout / network timed out" from "client-side setTimeout fired". The client's LLM idle timeout (Ns): no response from model should be a separate reason: "client_idle_timeout" that does not poison the provider.
or
(b) Default streamWithIdleTimeout's timeoutMs to a much larger value (e.g. 600 s) for slow / thinking models, AND make the default configurable per-agent so a 32k-token M3 turn doesn't trip it.
P1 — Make the cooldown recoverable from the user side
openclaw models reset-cooldown [provider ...] — clear entries from the set.
/reset-cooldown slash command (session + cross-session).
/model should always re-evaluate the cooldown set, not just change the primary in config.
P2 — Make the cooldown observable
- Add a column to the Models tab in Control UI showing the per-provider cooldown state, time-remaining, and the last error that poisoned it.
- Add a
cooldownUntil line in openclaw status for the running gateway.
- Emit a one-shot log line (and a CLI flag) the first time a provider enters the cooldown set, including the reason and the source call site, so users can grep their gateway log.
P3 — Optional: stop persisting it across session boundaries
If the design intent really is "single-process global cooldown for safety", document it loudly in the model-selection docs and in the warning text. But based on user pain, the cross-session persistence is a foot-gun, not a feature. Consider scoping the Set to the per-run / per-session scope unless the operator opts in to global cooldown via agents.defaults.model.cooldownScope: "global".
影响范围 — Impact
- Severity: High. Locks the user out of the configured primary model for the rest of the gateway's uptime. In practice this means "until I notice and restart", which is hours to days.
- Scope: Every OpenClaw user with a slow / thinking primary model (MiniMax-M3, o1, Claude-with-extended-thinking, large-context DeepSeek, etc.) and a fallback chain.
- First-observed in production: 2026-06-09 on a
MiniMax-M3 + MiniMax-M2.7 + Qwen-397B + Qwen2B setup. Confirmed with MiniMax-M2.7 itself which the user describes as "长期使用稳定" (long-term stable in production) — i.e. the regression is in the OpenClaw client, not in the provider.
复现配置 — Reproducer config
~/.openclaw/openclaw.json (excerpt, sanitised):
实际观察 — Observed evidence
- Trajectory of the affected session shows
model.completed for MiniMax-M3 succeeded, then no further MiniMax-M3 calls in the next 11 minutes despite many user turns. Cooldown set kept the runner off the primary.
MiniMax-M2.7 aborted exactly once with errorMessage: "LLM idle timeout (120s): no response from model" — a 32k-token input + tool-call chain. After that, minimax provider never recovered within the session.
modelscope/Qwen/Qwen3.5-397B-A17B then 401'd (already fixed by the user with a key rotation), then 429'd (limit_burst_rate). Fallback chain exhausted.
openclaw gateway restart immediately restored MiniMax-M3 availability. No config change.
我可以帮的
- 写一个最小复现 script(CLI / one-shot isolated session)让你附在 issue 里
- 提一个对应的 PR patch 给
dist/selection-*.js 把 client_idle_timeout 拆出来
- 提一个对应的 PR patch 给
dist/model-fallback-*.js 暴露 cooldownScope + models.reset-cooldown CLI
需要任何一个就告诉我。
摘要 (Summary)
cooldownProbeUsedProvidersis a module-levelSetthat lives for the entire gateway process. It is populated by every error classified asrate_limit/overloaded/timeout(including the client-sideLLM idle timeout (120s): no response from model, which is not a model failure at all) /empty_response/unknown/unclassified/no_error_details. Once a provider is in the set, every subsequent run within the same process skips it — even across sessions and agents. The/modelslash command only mutates the agent primary in config; it does NOT touch this Set, so user-side recovery is impossible. The only escape hatch is a fullkillof the gateway process.This is reproducible, has a clear source-level root cause, and the fix is small.
复现步骤 (Reproduction)
Environment:
openclaw v2026.5.x(reproduced on2026.5.28and2026.5.3-1),MiniMax-M3(1.0M context) asagents.defaults.model.primary,MiniMax-M2.7as first fallback,Qwen/Qwen3.5-397B-A17Bsecond,exo/Qwen3.5-2B-MLX-8bitthird. Repro is provider-agnostic — the issue is inrunWithModelFallback, not the providers.timeout/overloaded/empty_responsetransient error (or just let the agent run a long-running turn that triggers the 120 s streaming idle timeout).openclaw gateway status(or any available observability) that the provider's cooldown slot is occupied./model <another model on the same provider>— observe that the new model is not tried on the next turn, because the provider is still in the cooldown set./newor a fresh dashboard session — observe the same: the provider is still in the cooldown set.openclaw gateway restart(orkill -TERM <gateway-pid>and let it respawn).期望 vs 实际 (Expected vs Actual)
Expected
/model,/reset-cooldown,openclaw models reset-cooldown).streamWithIdleTimeoutas a model-sidetimeout— that is a client-side measurement and could just as easily be triggered by a slow network or a slowtransformersstartup.Actual
cooldownProbeUsedProvidersis populated by transient-client-side errors and is never cleared except by a process restart./modeldoes not clear it.pkill nodeand find that everything starts working again.根因(源码定位)— Root cause with source references
All paths are inside the
openclawpackage install (e.g./opt/homebrew/lib/node_modules/openclaw/dist/). Bundle names are abbreviated.1. The cooldown Set is module-level
dist/model-fallback--*.js2. The reason classifier considers the client-side idle timeout as
timeoutdist/selection-*.js(client-side streaming wrapper):dist/errors-*.js(reason classification):dist/model-fallback--*.js(cooldown classifier):The
LLM idle timeout (120s): no response from modelerror is generated by the OpenClaw client. It is not an API response. The provider never had a chance to returnoverloadedorrate_limit; the socket might have been mid-stream when the 120 s timer fired. Treating it as a provider-sidetimeoutand adding the provider to the cooldown set is a category error: it confuses "the model did not produce anything for 120 s" (which can be slow-but-fine, e.g. a long-thinking turn on MiniMax-M3 with a 32k-token input) with "the provider is degraded and we should back off".3.
/modeldoes not reset the cooldowndist/get-reply-*.jsanddist/agent-scope-*.js:runAutoFallbackPrimaryProbereads from the per-sessionmodelOverride, never from the globalcooldownProbeUsedProviders.clearAutoFallbackPrimaryProbeSelectionclears the session entry, not the module-levelSet.There is no command path that calls
cooldownProbeUsedProviders.clear(). Only a process restart reachesSetinitialisation.4. The set is not observable
There is no Control UI panel, no
openclaw models list --cooldown, noopenclaw statusline for it. Users have no way to know why a model is no longer being attempted.建议修复(按优先级)— Suggested fixes
P0 — Stop the mis-classification
Either
(a) Exclude the client-side idle timeout from
shouldUseTransientCooldownProbeSlot— change thetimeoutbranch to be a finer signal that distinguishes "provider reported timeout / network timed out" from "client-side setTimeout fired". The client'sLLM idle timeout (Ns): no response from modelshould be a separatereason: "client_idle_timeout"that does not poison the provider.or
(b) Default
streamWithIdleTimeout'stimeoutMsto a much larger value (e.g. 600 s) for slow / thinking models, AND make the default configurable per-agent so a 32k-token M3 turn doesn't trip it.P1 — Make the cooldown recoverable from the user side
openclaw models reset-cooldown [provider ...]— clear entries from the set./reset-cooldownslash command (session + cross-session)./modelshould always re-evaluate the cooldown set, not just change the primary in config.P2 — Make the cooldown observable
cooldownUntilline inopenclaw statusfor the running gateway.P3 — Optional: stop persisting it across session boundaries
If the design intent really is "single-process global cooldown for safety", document it loudly in the model-selection docs and in the warning text. But based on user pain, the cross-session persistence is a foot-gun, not a feature. Consider scoping the Set to the per-run / per-session scope unless the operator opts in to global cooldown via
agents.defaults.model.cooldownScope: "global".影响范围 — Impact
MiniMax-M3+MiniMax-M2.7+Qwen-397B+Qwen2Bsetup. Confirmed withMiniMax-M2.7itself which the user describes as "长期使用稳定" (long-term stable in production) — i.e. the regression is in the OpenClaw client, not in the provider.复现配置 — Reproducer config
~/.openclaw/openclaw.json(excerpt, sanitised):{ "agents": { "defaults": { "model": { "primary": "minimax/MiniMax-M3", "fallbacks": [ "minimax/MiniMax-M2.7", "modelscope/Qwen/Qwen3.5-397B-A17B", "exo/mlx-community/Qwen3.5-2B-MLX-8bit" ] }, "subagents": { "model": { "primary": "minimax/MiniMax-M2.7", "fallbacks": [ "modelscope/Qwen/Qwen3.5-397B-A17B", "exo/mlx-community/Qwen3.5-2B-MLX-8bit" ] } } // stream idle timeout: implicit 120s (default), not currently // surfaced as a top-level agent option } } }实际观察 — Observed evidence
model.completedforMiniMax-M3succeeded, then no furtherMiniMax-M3calls in the next 11 minutes despite many user turns. Cooldown set kept the runner off the primary.MiniMax-M2.7aborted exactly once witherrorMessage: "LLM idle timeout (120s): no response from model"— a 32k-token input + tool-call chain. After that,minimaxprovider never recovered within the session.modelscope/Qwen/Qwen3.5-397B-A17Bthen 401'd (already fixed by the user with a key rotation), then 429'd (limit_burst_rate). Fallback chain exhausted.openclaw gateway restartimmediately restoredMiniMax-M3availability. No config change.我可以帮的
dist/selection-*.js把client_idle_timeout拆出来dist/model-fallback-*.js暴露cooldownScope+models.reset-cooldownCLI需要任何一个就告诉我。