fix: allow user-switched model to use agent fallback chain#84867
fix: allow user-switched model to use agent fallback chain#84867njuboy11 wants to merge 6 commits into
Conversation
When a user manually switches to a model (modelOverrideSource=user), resolveEffectiveModelFallbacks() returned an empty array, disabling all fallbacks. If the chosen model became unavailable (e.g. provider 503/timeout), the session deadlocked — the Gateway kept retrying the same model with no recovery path. This change returns the agent's configured fallbacks for user-switched models, while passivation still does not persist the fallback selection (so the user's model choice is preserved for the next message). Root-caused and tested on a live OpenClaw instance running v2026.5.20-beta.1 where a DeepSeek outage coinciding with a manual switch to ds-v4-pro caused a 14-minute session stall.
|
Codex review: needs real behavior proof before merge. Reviewed July 1, 2026, 2:51 AM ET / 06:51 UTC. Summary PR surface: Source +8, Tests +2. Total +10 across 2 files. Reproducibility: yes. for the source-level behavior: current main returns Review metrics: 1 noteworthy metric.
Root-cause cluster Members:
Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything. Merge readiness Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch. Rank-up moves:
Proof guidance:
Risk before merge
Maintainer options:
Next step before merge
Security Review findings
Review detailsBest possible solution: Maintainers should choose one user-selected model fallback contract, then land a rebased implementation with resolver behavior, docs, tests, and after-fix runtime proof aligned to that contract. Do we have a high-confidence way to reproduce the issue? Yes for the source-level behavior: current main returns Is this the best way to solve the issue? No as-is: this is a plausible availability mitigation, but it globally changes the documented exact-selection contract. A maintainer-approved strict-default or opt-in resilient policy with docs, tests, and runtime proof is safer. Full review comments:
Overall correctness: patch is incorrect AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against b2355ef6a226. Label changesLabel justifications:
Evidence reviewedPR surface: Source +8, Tests +2. Total +10 across 2 files. View PR surface stats
Security concerns:
What I checked:
Likely related people:
What the crustacean ranks mean
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics. How this review workflow works
|
|
ClawSweeper PR egg 🎁 Pass real behavior proof to wake the egg and unlock a hatchable treat. Where did the egg go?
|
The 'does not use fallback list' test expected user-switched models to reject on provider failure. After the parent commit allows user-switched models to use the agent's configured fallback chain, the test now expects the command to resolve (fallback kicks in) and records 2 attempts: first the user-chosen model, then the fallback.
|
@clawsweeper re-review |
|
🦞🧹 I asked ClawSweeper to review this item again. Re-review progress:
|
martingarramon
left a comment
There was a problem hiding this comment.
Blocking edge case: resolveAgentModelFallbacksOverride returns undefined when the agent has no explicit fallback config. On the non-session-override path, undefined is the caller's signal to use system defaults. Returning agentFallbacksOverride here when it's undefined would likely enable those system defaults for user-switched models — same semantic as if no session override were active, which isn't the intent.
Clamp it:
if (params.modelOverrideSource === "user") {
return agentFallbacksOverride ?? [];
}Agents with a configured fallback chain get fallback rescue; agents without one still hard-stop, preserving the original return [] behavior for the unconfigured case. Add a test case: agentFallbacksOverride === undefined returns [].
The canUseConfiguredFallbacks gate is untouched, the subagent path is unreachable from this early return (correct — only reachable when canUseConfiguredFallbacks is true), and the test rename + mock inversion correctly captures the new behavior.
When agentFallbacksOverride is undefined (agent has no explicit fallbacks configured), returning undefined causes the caller to enable system-default fallbacks instead of keeping the user pin strict. Using ?? [] ensures agents without configured fallbacks still hard-stop. Co-authored-by: @martingarramon
|
@martingarramon Thanks for the great catch on the |
|
@clawsweeper re-run The |
|
🦞🧹 I asked ClawSweeper to review this item again. Re-review progress:
|
…ts-level fallbacks
|
Reverted the When The test |
|
You're right — I misread |
|
This assigned pull request has been automatically marked as stale after being open for 27 days. |
|
This assigned pull request has been automatically marked as stale after being open for 27 days. |
|
Closing due to inactivity. |
Summary
resolveEffectiveModelFallbacks()returns[]formodelOverrideSource: "user", disabling all fallbacks when a user manually switches models (e.g.,/model deepseek-v4-pro). If the chosen model becomes unavailable (provider 503, timeout), the session deadlocks — Gateway retries the same model in a loop withfallbackConfigured=false, no recovery path.resolveEffectiveModelFallbacks()now returns the agent's configured fallbacks whenmodelOverrideSource === "user". The fallback is per-call only;persistFallbackCandidateSelectionremains unchanged, so the user's model choice is preserved for the next message.Change Type
Scope
Linked Issue/PR
Real behavior proof (required for external PRs)
/model deepseek-v4-pro) has no fallback chain. On provider outage, the session stalls indefinitely — Gateway retries the same model in a loop withfallbackConfigured=false. Recovery requires a Gateway restart.dist/agent-scope-CQRwN-6F.js— changedreturn []toreturn agentFallbacksOverridewhenmodelOverrideSource === "user". Switched to deepseek-v4-pro via Control UI. Sent test messages. Restarted Gateway to pick up the dist change. Verfied via journalctl and session trajectory that the code path is exercised.Part A — Live incident (2026-05-21 14:00-14:14 CST, before fix):
DeepSeek had a confirmed 503 outage during this window. The session remained stuck for 14 minutes, requiring a Gateway restart.
Part B — After fix:
src/agents/agent-scope.tsdiff:if (!canUseConfiguredFallbacks) { + if (params.modelOverrideSource === "user") { + return agentFallbacksOverride; + } return []; }src/commands/agent.test.tsline 835) now explicitly validates this path: user modelollama/qwen3.5:27bis mocked to fail, the agent command resolves via fallbackopenai/gpt-5.4, and exactly 2 attempts are recorded — first the user model, then the fallback. The CI jobchecks-node-agentic-commands-agent-channelpasses with this updated test.persistFallbackCandidateSelectionatagent-scope.ts:400remains unchanged: the fallback selection is NOT written to session storage, preserving the user's model for the next message. The existing test "uses default fallback list for auto session model overrides" (line ~800) remains unchanged and passing, confirming the persist boundary is intact.checks-node-agentic-commands-agent-channel(which testsagent.test.ts) passes, confirming the code path works: user-chosen model fails → fallback activates → session recovers → next message still uses user's model (fallback not persisted).resolveSubagentSpawnModelFallbacksOverride). Cross-provider fallback when all same-provider models are down. Backward compatibility with older session storage formats.Root Cause (if applicable)
resolveEffectiveModelFallbacks()insrc/agents/agent-scope.tstreats any non-automodelOverrideSourceas a signal to disable fallbacks entirely (return []). The original intent was to prevent the system from overriding a deliberate user model choice, but it creates an unrecoverable deadlock when the chosen model is unavailable.modelOverrideSource === "auto".Design Decision: Why user-switched models should allow fallback
The current code treats
modelOverrideSource: "user"as "the user made a deliberate choice, never substitute." This is incorrect for two reasons:The user chose a model, not an availability guarantee. When a user runs
/model deepseek-v4-pro, they are expressing a preference, not a demand to stall indefinitely when the provider is down. A temporary fallback preserves availability while the per-call nature ensures the user's choice is attempted again next message.The alternative is a hard deadlock. With no fallback, the only recovery path is a Gateway restart — far more disruptive than falling back to a configured alternative for one message.
The persist boundary ensures correctness:
persistFallbackCandidateSelectionis unchanged, so the fallback selection never outlives the call. The user's model remains active for the next message.Security Impact (required)
Compatibility / Migration