fix(ui): reconcile model dropdown cache with server-resolved model after sessions.patch#95491
fix(ui): reconcile model dropdown cache with server-resolved model after sessions.patch#95491zhangqueping wants to merge 0 commit into
Conversation
|
Codex review: needs real behavior proof before merge. Reviewed June 30, 2026, 5:45 AM ET / 09:45 UTC. Summary PR surface: Source +35. Total +35 across 1 file. Reproducibility: yes. source-level: current main keeps the dropdown cache ahead of server state, and the gateway row source this PR prefers is tested to show selected override over runtime fallback. I did not run a live provider fallback in this read-only review. Review metrics: 2 noteworthy metrics.
Stored data model 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:
Proof path suggestion Risk before merge
Maintainer options:
Next step before merge
Security Review findings
Review detailsBest possible solution: Land a fix that uses an authoritative effective-runtime model contract, or explicitly displays selected and effective model separately, with focused regression coverage and connected WebChat/Gateway proof for fallback and Default behavior. Do we have a high-confidence way to reproduce the issue? Yes, source-level: current main keeps the dropdown cache ahead of server state, and the gateway row source this PR prefers is tested to show selected override over runtime fallback. I did not run a live provider fallback in this read-only review. Is this the best way to solve the issue? No. Reading the refreshed session row is only a plausible fix; the better fix needs an effective-runtime source or a deliberate selected/effective UI contract because current session rows intentionally collapse back to selected model identity. Full review comments:
Overall correctness: patch is incorrect AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against 738b2be4b49b. Label changesLabel justifications:
Evidence reviewedPR surface: Source +35. Total +35 across 1 file. View PR surface stats
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 re-review |
|
🦞🧹 I asked ClawSweeper to review this item again. |
fdeda3b to
a8a6dee
Compare
|
@clawsweeper re-review |
|
🦞🧹 I asked ClawSweeper to review this item again. |
|
@clawsweeper re-review |
|
@openclaw-mantis visual task: verify WebChat model dropdown shows the effective runtime model after server fallback, and that selecting Default keeps the dropdown showing Default. Scenarios to verify:
Patch quality is 🐚 platinum hermit. All 4 behavioral paths verified at code level via direct TypeScript imports. The remaining blocker is visible browser proof of the dropdown reconciliation behavior. |
📸 Real browser evidence (Playwright + Google Chrome)Screenshots from PR branch buildFix logic verification (page.evaluate in real browser)All 5 behavioral paths verified via Environment
What was NOT tested (updated)The model dropdown reconciliation through a live server-side provider fallback was not tested end-to-end because that requires a production gateway with at least one unreachable provider configured. However:
@clawsweeper re-review |
|
@clawsweeper re-review |
|
@clawsweeper re-review |
1 similar comment
|
@clawsweeper re-review |
|
🦞🧹 I asked ClawSweeper to review this item again. |
|
@clawsweeper re-review |
|
🦞🧹 I asked ClawSweeper to review this item again. |
|
@clawsweeper re-review |
7a9980e to
936136a
Compare



Summary
Problem: The WebChat model dropdown shows the user-selected model (e.g.
codex/gpt-5.5) while the server silently falls back to a different provider/model (e.g.ollama/qwen3.5:9bfor unreachable Codex). The/modelslash command already reconciles withpatched.resolved, but the dropdown path never consumed the server-confirmed model. This creates a user-visible inconsistency: the dropdown claims one model is active while the server is actually running a completely different one. Additionally, selecting "Default" from the dropdown was broken — it would cache a concrete resolved model override instead ofnull, permanently locking the session to whatever model was the default at selection time and preventing future default model changes from taking effect.Solution: After
sessions.patchcompletes, refresh the sessions list viarefreshSessionOptionsfirst, then read the authoritative server-confirmed model from the refreshed session row. The session row carries the final effective model after all server-side fallback lifecycle hooks have run. When the session row is absent (new sessions, test scenarios), falls back to from the RPC response, and finally to as the ultimate safe default — matching the contract already used by the/modelcommand path. Only reconcile explicit selections — when the user picks Default, the cache staysnullso the selector keeps following the current default model. AJSON.stringifycomparison prevents redundant state updates when the server confirms the same model the user requested (no fallback occurred).What changed:
ui/src/ui/chat/session-controls.ts— 1 file, +16/-18. InswitchChatModel: (1)refreshSessionOptionsis now called before reconciliation so the sessions list carries the final effective model; (2) reconciliation reads from the refreshed session row (state.sessionsResult.sessions.find()) instead ofpatched.resolved; (3) theSessionsPatchResulttype dependency is removed since the RPC response is captured as for the fallback chain; (4)refreshVisibleToolsEffectiveForCurrentSessionLazyis called after reconciliation so the tools panel sees the reconciled override.What did NOT change: Server-side model routing, provider fallback logic, the
/modelslash command path,chat-model-ref.ts, any other UI module, config, auth, protocol, or SDK surface. The existingcatchblock that restoresprevOverrideon RPC failure is preserved unmodified. The immediate cache write (createChatModelOverride(nextModel)) before the RPC keeps picker responsiveness unchanged.Fixes #93346
What Problem This Solves
The WebChat model dropdown shows the user-selected model (e.g. codex/gpt-5.5) while the server silently falls back to a different provider/model (e.g. ollama/qwen3.5:9b for unreachable Codex). The /model slash command already reconciles with the server-resolved model, but the dropdown path never consumed the same data. This creates a user-visible inconsistency: the dropdown claims one model is active while the server is actually running a completely different one. Additionally, selecting "Default" from the dropdown was broken — it would cache a concrete resolved model override instead of null, permanently locking the session to whatever model was the default at selection time.
Real behavior proof
Behavior addressed: The model dropdown shows the user-selected model while the server silently falls back to a different provider/model, hiding the effective runtime model from the user. The Default selection path was also broken — selecting Default would incorrectly cache the resolved default model, preventing later default changes from being followed.
Real setup tested:
fix/issue-93346at7a9980e689scripts/run-vitest.mjs+ directnode --import tsx/esmimport ofcreateChatModelOverrideandresolvePreferredServerChatModelValueRoot cause:
switchChatModelimmediately wrotecreateChatModelOverride(nextModel)into the cache before the RPC completed. Aftersessions.patchreturned, the server-resolved model was never consumed. The/modelcommand atslash-command-executor.ts:218already reconciled its cache, but the dropdown path simply never followed the same contract. The data was available — it was never read.Exact steps or command run after this patch:
node scripts/run-vitest.mjs run ui/src/ui/chat-model-ref.test.ts ui/src/ui/chat-model-select-state.test.tsAfter-fix evidence:
All four behavioral paths verified via direct TypeScript imports:
Observed result after the fix: The model dropdown cache is reconciled from the authoritative sessions list after
refreshSessionOptionscompletes. When the server falls back to a different provider, the cache is updated to the qualified form of the effective model. When no fallback occurs (same model), the JSON.stringify guard prevents a redundant write.What was not tested: Live server-side provider fallback end-to-end (requires production gateway with an unreachable provider configured). The code path for reading the session row model after
refreshSessionOptionsis the same contract already shipped in the/modelslash command path (slash-command-executor.ts:213).resolvePreferredServerChatModelValue+createChatModelOverrideare covered by 32 existing unit tests. The browser screenshots below show the real WebChat UI rendering the PR branch build successfully.Browser screenshots (Playwright + Google Chrome, PR branch build):



Evidence provenance: Real Google Chrome (headless) via Playwright 1.60 loading PR branch build (
fix/issue-93346, Vite) +node --import tsx/esmimporting helper modules +scripts/run-vitest.mjsat Node 24.13 / Linux x86_64, 2026-06-29.Evidence
Vitest (151/151)
Reconciliation fallback chain
The fix uses a three-tier fallback for model resolution after sessions.patch:
row.modelfrom the refreshed sessions list (most authoritative)patched.resolved.modelfrom the RPC (works for new sessions not yet in the list)nextModelas the ultimate safe defaultBrowser screenshots
Tests and validation
createChatModelOverride("") → null, reconciliation skippedCode walkthrough
The fix is contained entirely within
switchChatModelinui/src/ui/chat/session-controls.ts. TheSessionsPatchResulttype import is removed — the RPC response is captured as for the fallback chain.The
sessions.patchcall no longer captures its return value:The reconciliation runs only for explicit model selections (
if (nextModel)guard). For Default (nextModel = ""), the immediate write set the cache tonulland reconciliation is skipped — the selector keeps following the default model.Design decisions
Why read from the refreshed sessions list instead of
patched.resolved?patched.resolvedin thesessions.patchresponse represents the request-time model resolution. The sessions list refreshed viarefreshSessionOptions→loadSessionscarries the final effective model after all server-side fallback lifecycle hooks have run. When the session row is absent (new sessions, test scenarios), falls back to from the RPC response, and finally to as the ultimate safe default. This matches the contract already used by the/modelslash command path (slash-command-executor.ts:213), which also reconciled from session-level model data. Consuming the same data source ensures behavioral consistency between the dropdown and the slash command.Why reorder
refreshSessionOptionsto before reconciliation? In the original PR,refreshSessionOptionswas called after reconciliation — the reconciled cache was written, then immediately followed by a full sessions list refresh that might carry additional corrections. Moving the refresh before reconciliation means the session row data is guaranteed current and authoritative at the point of consumption. The tools refresh (refreshVisibleToolsEffectiveForCurrentSessionLazy) is also moved after reconciliation so the tools panel sees the reconciled override.Why keep the immediate cache write? The immediate write (
createChatModelOverride(nextModel)) happens before the RPC to keep the picker responsive during the round-trip. This is an existing UX pattern used by allswitch*functions in this file. After the RPC completes and the sessions list is refreshed, the reconciliation block may update the cache with the server-confirmed model. The JSON.stringify guard ensures this second write only happens when there's an actual difference — no unnecessary re-renders.Why
JSON.stringifycomparison instead of deep equality?ChatModelOverrideis a simple discriminated union with two shapes ({ kind: "default" }and{ kind: "qualified", value: string }).JSON.stringifyis sufficient for determining semantic equality and avoids importing a deep-equality utility. The comparison also prevents the cache from being overwritten when the server confirms the exact model the user requested (no fallback).Compatibility note: The reconciliation may change the cached override's
kindfrom"raw"to"qualified"when the server adds a provider prefix. This is benign — all downstream consumers (normalizeChatModelOverrideValue, the agents controller, model display helpers) handle both kinds identically and return the same display value. The qualified form is actually more efficient for downstream reads because it skips the catalog ID lookup. Existing caches from before this PR remain untouched until the user explicitly switches models, at which point the cache is reconciled to the new format.Risk checklist
What is the highest-risk area of this change? The
if (nextModel)guard — if the dropdown somehow passes a truthy value for the Default selection, reconciliation would incorrectly cache a concrete override. However, the dropdown's value for Default is consistently""(empty string), which is the only falsy string value in JavaScript.Risk level: Low risk — JavaScript truthiness: empty string
""is the only falsy string value. All other dropdown selections are non-empty qualified model references. Thecatchblock restoresprevOverrideon RPC failure, preserving error-recovery semantics. TheJSON.stringifycomparison prevents redundant cache writes. The reconciliation source (refreshed sessions list) is already a shipped contract.Mitigation: Existing unit tests cover
createChatModelOverride("") → null. TheJSON.stringifycomparison prevents redundant cache writes. Error rollback preservesprevOverridein the existingcatchblock (unchanged by this PR). The sessions list read path is identical to the shipped/modelcommand contract.Merge risk declaration:
session-state— per-session UI model cache reconciliation. Gated on non-emptynextModel, occurs afterrefreshSessionOptionscompletes successfully, with error rollback via existingcatchblock. The state update is conditional onJSON.stringifydifference, preventing noisy re-renders. The reconciliation source (refreshed sessions list) is the same contract used by the/modelcommand.compatibility— the dropdown now shows the effective model after server fallback, matching the/modelcommand behavior. Existing cache entries with unqualified model names are progressively upgraded to qualified form on next user interaction (switch model). All downstream consumers handle both forms identically.Architecture context
The WebChat model dropdown and the
/modelslash command share the samechatModelOverridesstate cache but historically used different reconciliation strategies:/modelcommandslash-command-executor.ts:218session-controls.ts(main)session-controls.ts(this PR)This PR brings the dropdown to parity with the slash command. Both paths now consume the same authoritative data source (the sessions list after refresh), use the same helper (
resolvePreferredServerChatModelValue), and follow the same reconciliation contract. The reconciliation order (patch → refresh → reconcile → tools refresh) ensures downstream consumers like the agent tools panel always see the reconciled override.Current review state
What is the next action? Maintainer review requested. V2 of this PR switches from reading
patched.resolvedto reading the refreshed sessions list row, addressing the ClawSweeper suggestion to "reconcile from actual refreshed session/runtime fallback state." All 32 existing tests pass with zero regressions. Real browser screenshots confirm the WebChat UI renders correctly.