Repository navigation
fix(native-chat): a Claude chat keeps a model its CLI stopped listing - #30436
Conversation
Claude Code 2.1.302 stopped listing opus[1m], which still runs, and chats on it moved to the list's default on the next send, effort remapped. The picker no longer derives a replacement from the list: the chat's own model shows as a selected-only entry, its effort read-only, and every send carries both. A real model_not_found reply still recovers: the options answer shows what the reset child runs, so the next send records it. A reopened chat's catalog read lists its own model so older clients that replace one keep it too. Tracked in STA-10947.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info
📝 Walkthrough
Priority: ⬇️ Low Merge Risk: ⚪ Minimal · up to
|
There was a problem hiding this comment.
Actionable comments posted: 2
ℹ️ Review info
⚙️ Run configuration
- Configuration used: Repository UI
- Review profile: CHILL
- Plan: Advanced
- Run ID:
6edb82fa-4bbf-49d3-9611-dac8553f5cce
📒 Files selected for processing: 26
mobile/src/session/MobileNativeChatSessionOptionPickers.tsxmobile/src/session/MobileNativeChatSessionOptionRows.tsxmobile/src/session/use-mobile-structured-agent-options.first-frame.test.tsmobile/src/session/use-mobile-structured-agent-options.permission-mode.test.tsmobile/src/session/use-mobile-structured-agent-options.test.tsmobile/src/session/use-mobile-structured-agent-options.tsmobile/src/session/use-mobile-structured-agent-session-send.test.tsxmobile/src/session/use-mobile-structured-option-surface.tssrc/main/native-chat/agent-session-wire/structured-agent-session-options-read.tssrc/main/runtime/claude-structured-retired-saved-model.test.tssrc/main/runtime/rpc/methods/structured-agent-session-options-read.test.tssrc/main/runtime/rpc/methods/structured-agent-session-options-read.tssrc/renderer/src/components/native-chat/NativeChatSessionOptionRows.tsxsrc/renderer/src/components/native-chat/use-structured-agent-session-option-state.tssrc/renderer/src/components/native-chat/use-structured-agent-session-send-options.test.tsxsrc/renderer/src/components/native-chat/use-structured-agent-session-send-options.tssrc/renderer/src/lib/structured-agent-session-launch-create-message.test.tssrc/renderer/src/lib/structured-agent-session-launch-create-message.tssrc/shared/native-chat-session-options.tssrc/shared/structured-agent-session-model-catalog-application.test.tssrc/shared/structured-agent-session-option-view.test.tssrc/shared/structured-agent-session-option-view.tssrc/shared/structured-agent-session-options.test.tssrc/shared/structured-agent-session-options.tssrc/shared/structured-agent-session-permission-option.test.tssrc/shared/structured-agent-session-unlisted-effort.ts
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 0 remain after this review.
…ve, for every agent
…red model falls back as on main - The read-only value predicate keys on the effort row, so the phone's legacy host Permissions row opens as before. - The desktop read-only effort row honours a row kept from the previous model. - A live child with no listing of its own answers with the saved listing's ids, so the read-only effort reads the same live and at rest. - The read-only effort borrows listed rows' effort label and level names. - The host seeds a new Claude chat from a remembered default the saved listing no longer names on that listing's default, effort nearest, as main did; a chat's own saved or running model is never replaced. Tracked in STA-10947.
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to GitHub limitations.
🟡 Minor · Drop the effort when the model restore fails. · structured-agent-session-options-read.ts:271-281
src/main/native-chat/agent-session-wire/structured-agent-session-options-read.ts:271-281
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winDrop the effort when the model restore fails.
When Claude reports
model_not_found, the child switches to its default model, butshownSaved('effort')remains true. The reader therefore returns the replacement model with the old model's effort. The option view records that pair, and Claude sends both values on later messages. Claude's own option code states that an effort can remain from the old model even when the new model does not support it.Treat a model restore failure as invalidating the saved effort and omit the live effort before building
current.Suggested fix
const saved = record?.options const shownSaved = (key: 'model' | 'effort'): boolean => - saved?.[key] !== undefined && !refused.includes(key) + saved?.[key] !== undefined && + !refused.includes(key) && + !(key === 'effort' && refused.includes('model')) const selected = live && (shownSaved('model') || shownSaved('effort')) ? await atRest() : options + const { effort: _staleEffort, ...liveWithoutEffort } = options.current const current = { - ...options.current, + ...(refused.includes('model') ? liveWithoutEffort : options.current), ...(shownSaved('model') ? { model: selected.current.model } : {}), ...(shownSaved('effort') ? { effort: selected.current.effort } : {}) }
ℹ️ Review info
⚙️ Run configuration
- Configuration used: Repository UI
- Review profile: CHILL
- Plan: Advanced
- Run ID:
1083e5da-bb57-4a30-9d8d-f81e804478a3
📒 Files selected for processing: 10
src/main/claude/claude-structured-effort-default-at-rest.test.tssrc/main/native-chat/agent-session-wire/structured-agent-session-option-models.tssrc/main/native-chat/agent-session-wire/structured-agent-session-options-read.test.tssrc/main/native-chat/agent-session-wire/structured-agent-session-options-read.tssrc/main/runtime/rpc/methods/orchestration/worker/structured-worker-launch-seed-options.test.tssrc/main/runtime/rpc/methods/structured-agent-session-create.tssrc/main/runtime/session-tabs/structured-launch-seed-listing.tssrc/renderer/src/components/native-chat/NativeChatSessionOptionRows.tsxsrc/shared/native-chat-session-options.tssrc/shared/structured-agent-session-unlisted-effort.ts
🚧 Files skipped from review as they are similar to previous changes: 2
- src/shared/native-chat-session-options.ts
- src/renderer/src/components/native-chat/NativeChatSessionOptionRows.tsx
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 0 remain after this review.
…model; one guard - An at-rest create carrying an effort but no model, on a remembered model the saved listing no longer names, now moves that effort to the nearest level the fallback model offers, as main did. - A create whose own options name no model, and whose merged options carry none, reads no listing. - The seed fallback keeps one guard and no unreachable branch. - The read-only effort finds its level's name in any listed model's choices. Tracked in STA-10947.
… ids and never throws
Review loop status: ready on df5a3d3The problem. Claude Code auto-updated from 2.1.280 to 2.1.302, and the new version no longer lists What changes for the user. A chat keeps the model and effort it was on. The picker shows that model as an entry only that chat has, and shows its effort as read-only text labelled like a listed model's effort row. Every message runs on them, on desktop and phone. A new chat whose remembered model the list no longer names starts on the list's default, as on Review. Three rounds plus a readiness check, all with Claude reviewers.
Deferred, P3 or unproven: one extra saved-list read per options call when nothing is saved; a stopped chat's saved list could disagree with a running child after a failed listing; the new-chat fallback test covers the helper, not the create step around it. Labelled in the PR body: the read-only effort and the "show what runs" answer apply to every agent (intended); the new-chat fallback now covers Verified.
Not verified. The phone app and a 1.4.224-or-older host (no rig; covered by named tests only), a Codex chat on an unlisted model in the real app (couldn't be set up; covered by the stopped-equals-running host test), SSH and WSL. For Brennan's reviewIncident chat after the CLI update, before Send, and its reply: Where a human judgment matters:
|
…#30436) * fix(native-chat): a Claude chat keeps a model its CLI stopped listing Claude Code 2.1.302 stopped listing opus[1m], which still runs, and chats on it moved to the list's default on the next send, effort remapped. The picker no longer derives a replacement from the list: the chat's own model shows as a selected-only entry, its effort read-only, and every send carries both. A real model_not_found reply still recovers: the options answer shows what the reset child runs, so the next send records it. A reopened chat's catalog read lists its own model so older clients that replace one keep it too. Tracked in STA-10947. * test(mobile): drop the send-options capability the options hook no longer reads * test: an unlisted model's saved effort shows read-only at rest, as live, for every agent * fix(native-chat): round-1 review fixes; a new chat's unlisted remembered model falls back as on main - The read-only value predicate keys on the effort row, so the phone's legacy host Permissions row opens as before. - The desktop read-only effort row honours a row kept from the previous model. - A live child with no listing of its own answers with the saved listing's ids, so the read-only effort reads the same live and at rest. - The read-only effort borrows listed rows' effort label and level names. - The host seeds a new Claude chat from a remembered default the saved listing no longer names on that listing's default, effort nearest, as main did; a chat's own saved or running model is never replaced. Tracked in STA-10947. * test: pin the new-chat seed fallback on its typed helper, with no casts * fix(native-chat): round-2: a carried effort moves with a fallen-back model; one guard - An at-rest create carrying an effort but no model, on a remembered model the saved listing no longer names, now moves that effort to the nearest level the fallback model offers, as main did. - A create whose own options name no model, and whose merged options carry none, reads no listing. - The seed fallback keeps one guard and no unreachable branch. - The read-only effort finds its level's name in any listed model's choices. Tracked in STA-10947. * fix(native-chat): a live options read with no record borrows no saved ids and never throws (cherry picked from commit 0322185)
Resolution groups: 1. Revert-caused conflicts (29 files under acp, ai-vault, native-chat, pi, claude, codex, runtime/rpc, renderer ai-vault, shared ai-vault, mobile picker): upstream side taken. Our side carried only upstream release-branch commits there (2213562, c361d26, bb20d8f, 42e1469), none of ours. Files deleted on one side follow upstream (3 restored, 3 removed). Also aligned to v1.4.232: 40 files that auto-merged but still carried the stablyai#29977 revert (c361d26) or a duplicated stablyai#30436 test block (bb20d8f); no fork or PR commit touches them. 2. package.json: upstream (version 1.4.232); no own change on our side. 3. cloud-operate-relay-production-reserve-watch.yml: upstream. 5974258 is an upstream "match main's workflow files" commit; .github now equals v1.4.232. 4. src/main/updater/updater-setup.ts: upstream structure (macOS ShipIt scheduling, stablyai#30944) plus our kill-switch import; the early returns in both check entry points and setupAutoUpdater auto-merged unchanged.









ELI5
When Claude Code updated itself, it stopped listing
opus[1m]in its model menu, though that model still runs. Orca read "not in the list" as "can't run", so on your next message it quietly moved your Claude chat to another model (Sonnet or plain Opus), sometimes with a different effort. Plain Opus here has a 200k context instead of 1M. Now a chat keeps the model and effort it was on. The picker shows them even when Claude's list no longer has them, and every message runs on them.What Changed
The problem. Claude Code 2.1.280 auto-updated to 2.1.302, which lists
opusbut no longeropus[1m]. Chats in the native Chat UI that were onopus[1m], running or reopened, switched to the list's default model on the next Send, and their effort moved to the "nearest" level. Nothing told the user.opus[1m]still runs fine on the newer CLI: an off-list run succeeds, and the switch shrinks the context from 1M to 200k.Why it happened. The picker treated the model list as authority over the chat's own saved choice. When the list didn't name the chat's model, the shared picker logic derived a replacement: the list's default row with the effort moved to the nearest level. The picker showed it, every send carried it, and the host saved it. #30022 already narrowed this once for the same incident, but a reopened chat still hit it. A list that lacks a model is not evidence the model can't run.
What you see now.
main. With nothing saved, a new chat starts on the list's default. A new Claude chat whose remembered default model the host's saved list no longer names starts on the list's default, with its effort moved to the nearest level offered. That'smain's fallback, now applied by the host when it seeds the chat, so desktop, phone and CLI launches all get it. It never applies to a chat's own saved or running model.model_not_foundreply from Claude, or an option ACP agents or Pi couldn't take), the failed message shows as it does today, the running chat moves to what the agent now runs, and the picker shows that. The next message saves it. Nothing is resent automatically.Unchanged: listed models with no effort control (Haiku) still show no effort. A model you pick still runs as picked. Codex, ACP agents and Pi never had their model replaced, and what they send is unchanged.
Intended differences for every agent (Codex, ACP agents, Pi included):
mainsuch a chat showed no effort. The text is the effort the chat already launches with, so nothing it sends changes.Intended: the new-chat fallback now runs on the host, so it applies to every way of creating a chat. That covers desktop, phone, the CLI's
orca agent launchand orchestration'sworker-start --agent claudewhen they don't pass--model. Onmainonly desktop and phone moved such a chat off the remembered unlisted model; CLI and worker creates kept it and failed or ran on it. To launch on a specific model anyway, pass--model.How (the mechanism).
structuredAgentSessionListedPicksandnearestAgentEffortare deleted). The client-only "verified ids" state that fed it goes too, except the ids a chat's own options answer lists. Those now only tell an unlisted model from a listed one, for the read-only effort.model_not_foundreply fills), so the picker shows what actually runs.agentSession.modelCatalogread also lists that chat's own saved model as a row. Released clients (desktop v1.4.226–229, the shipped phone app) verify against this answer, so they keep the model too.Why
The bug class is "the list decides the chat's model". A second guard on the replacement (#30022 was the first) would leave the class in place, so this removes the replacement from the chat's own selection instead. What remains is only the new-chat seeding fallback
mainalready had, now in the one place the host seeds. The common pattern replaces an absent selection with the default unless the selection is kept as a selected-only entry. Intended difference: we always keep the chat's own selection, because a real CLI update produced exactly this state, the model still ran, and the replacement cost real context. The common pattern also keeps a preferred effort the model's row says nothing about, which matches the read-only effort.Considered and rejected:
listedModelIdsfor older clients: that would break the field's meaning, and no released client reads it.#30107 is the broader follow-up redesign of this area. This PR fixes only the incident and lands first.
Mixed versions. There is no new wire field, opcode or saved field.
Linked Issue
Tracked in STA-10947.
Visual Proof
Electron QA on another machine with real signed-in Claude Code, branch compared with
main(full list in the review status comment).Chat on


opus[1m]after the CLI stopped listing it: the model is kept, effort is read-only, and the reply runs on them.New chat from a remembered


opus[1m], branch thenmain(same fallback):Testing
structured-agent-session-option-view.test.ts: the incident as a flow. A chat onopus[1m]atmax, under a list naming only plainopus, showsopus[1m]and a read-onlymaxthrough a refresh and a live answer, and every send carries{ model: 'opus[1m]', effort: 'max' }. A listed Haiku shows no effort. A new chat with an unlisted remembered model names no other model.use-mobile-structured-agent-session-send.test.tsx: the same on the phone, through the real hooks, ending in a send carrying the literal model and effort.structured-agent-session-options-read.test.ts(RPC): a reopened chat's catalog read lists its own model. A new chat's read and the account's read list only the CLI's.claude-structured-retired-saved-model.test.ts: after a realmodel_not_foundreply, the options answer no longer shows the retired model. I confirmed this assertion fails without the change.Updated the tests that asserted the replacement (send options, launch create, catalog application).
Ran locally:
pnpm tc:node,pnpm tc:web, the mobiletypecheck, and the named test files above plus the related option, permission, picker, send-options and retired-model files. All passed.Not run: full lint (CI), the full suites (CI), and the real Claude CLI tests (no
src/main/claude/claude-structured-*file changed).I manually tested these changes locally
Automated tests added/updated, or explained why not below
AI Disclosure
Review
Agent skill upstream boundary
Notes
Checklist
N/Awith reasonpnpm lint,pnpm typecheck,pnpm test, andpnpm buildpass (or CI will cover; local preferred)