Repository navigation
fix: a Claude model that's gone switches to the list's default when you send - #27826
Conversation
…d model is gone The remembered new-chat model was launched as-is even after Claude stopped offering it, so every new chat failed its first message. The host create and the desktop new-chat picker now derive the same replacement from the host's saved model list: the listed default, effort moved to the nearest level it offers. Explicit models (--model, held picks) run as given; with no saved list the remembered model is kept.
The check read host.deps before ruling out other agents and explicit models, so creates whose host carries no deps (and every non-Claude create) paid for a lookup they never use.
A held model runs as picked on the host, so the picker no longer derives an effort beside it. A cached list whose default marker was dropped for this workspace keeps the placeholder instead of naming its first row.
The host-side check read the saved list separately from the picker, so the two disagreed and a cold start never fired it. The desktop new-chat picker now picks the replacement it shows, once, through the existing pick path, so the first send runs it and the remembered selection heals. The host is back to main.
…in hand The open-time auto-pick raced the chat's create and the list's arrival, so the first send could still launch the gone model. Now, like the common pattern, the picker shows the replacement for a model the host's loaded list lacks, and the send step applies it through the existing pick call, awaited, right before the message goes out. A failed pick never holds the send back.
The picker, the send step and the option setter now all read the list the picker shows, less the row a session adds for its own unlisted model, so what is shown is what applies and an explicit pick from that list stays. A listing that is still running, failed, or absent keeps the model. The send step re-derives from the chat's state when it runs, so a second send behind one that already moved the model applies nothing.
A single verified flag outlived the catalog it described: live options built from the built-in fallback list, or a malformed or empty host answer, still authorized a replacement the host would refuse. The state now keeps the ids a finished, well-formed host listing names, and a replacement must be both shown in the picker and named there.
…one-model-default
Review summary (head
|
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info
📝 Walkthrough
Priority: ➖ Normal Merge Risk: ⚪ Minimal · up to
|
…fort Every composer send (desktop and phone) carries the model and effort the picker shows, as optional agentSession.send options, only to a host that advertises agent-session.send-options.v1. The host writes a carried value that differs from the chat's own through the existing pick path before accepting the message, so the turn starts on it and the chat records it. A queued card keeps its options and applies them when it goes out; a launch's create call carries the replacement for its first message. Replaces the separate awaited pick before each send.
…with The carried selection is now stored with the message (its submission row, and a queued card's row) and applied through the shared option writer as the delivery loop hands the message over, or before the start it needs. A pick made after the message was accepted no longer changes the model it runs on. Direct sends, queue drains and Send now share that one apply point; the early apply before acceptance and the queue-only apply are gone. The picker and the send read one effective selection: the replacement for a model the host's finished listing lacks shows only for a host that takes a send's selection, the phone validates picks against what it shows, a send carries the default the composer shows, a resumed launch's first message gets the same resolution, and only the listing proves a row (no label heuristic).
…one-model-default
…resolves like the picker The opening message a create sends now stores the model and effort the chat was created with, so a pick made before its handover no longer changes what it runs on, and a card kept after a restart keeps them. A pick on its way to the host resolves through the same option view the picker shows before a send carries it, so a gone model it names is replaced too; the send selection reads only the shown snapshot. The option writer builds its own record-and-publish context, so the message path no longer duplicates it, and the client and host share one function for a send's fingerprint fields.
Review summary (head
|
…one-model-default # Conflicts: # mobile/src/session/mobile-native-chat-merged-snapshot-parity-hooks.test.tsx # mobile/src/session/mobile-structured-agent-session-host-support.test.ts # mobile/src/session/mobile-structured-agent-session-host-support.ts # mobile/src/session/use-mobile-structured-agent-session-prompt-cancel.test.tsx # mobile/src/session/use-mobile-structured-agent-session-queued.test-fixture.ts # mobile/src/session/use-mobile-structured-turn-indicator.test.tsx # src/shared/agent-session-create-capabilities.ts
ELI5
You can pick a Claude model for a chat, and Orca remembers it for new chats. When Claude stops offering that model, Orca kept sending it, so the next message failed with "model not found". Now every message you send carries the model and effort the picker shows, and runs on exactly that. If the chat's model is no longer on Claude's list, the picker already shows the list's default model, and that is the model your message runs on.
What Changed
The problem. Anthropic sometimes retires Claude models. When a chat's model, or the model Orca remembers for new chats, was retired:
What you see now. If the model list Orca has loaded for Claude doesn't include the chat's model:
defaultentry resolves to, or the first row when Claude names none.max→xhigh), and you can pick any level that model offers, on the phone too.This is the same for a new chat, a chat you reopen, a running chat, a chat started or resumed with an opening prompt, a message queued while Claude is working, and the phone.
Unchanged:
maindoes (see Mixed versions).--modelruns exactly as given.How it works: the message carries its model, the common pattern.
verifiedModelIds, kept in memory only); the chat's own unlisted model, which the picker also shows, never counts.agentSession.sendgains an optionaloptionsfield with the model and effort. It uses the same shapeagentSession.createalready takes for its options, and it is part of the operation's fingerprint, not the message's (client and host compute those fields with one shared function). A pick still on its way to the host is resolved through the same view the picker shows before the send carries it, so a gone model it names is replaced too, and the pick and the send agree.Mixed versions.
agentSession.sendrejects unknown fields, so clients sendoptionsonly to a host that advertises the newagent-session.send-options.v1capability. That is the same negotiationdeliveryuses. An older host gets exactly today's send, and the picker shows that host's chat asmaindoes, so it never shows a model the send won't carry. Rows written by this build keep one new optional field, which an older build ignores.This replaces #26459.
Why
#26459 solved this with a stored "who chose this model" field, a forced fresh listing with time limits, a new wire field, and extra re-reads, about 900 production lines.
Earlier cuts of this PR first applied the switch through a separate pick call before each send, then applied the carried values before accepting the message. Review showed the second still let a pick made after acceptance change which model the message ran on, a chat's opening message included. In the common pattern, the message keeps its own selection until it is dispatched, and dispatch applies it. This version does that, with one apply point instead of three.
How this compares with the common pattern. Each difference is labelled.
defaultentry resolves to, unchanged frommain.main, so its sends never carry a pick in flight.mainshows (Mixed versions above). The desktop's own host is always the same build.agentSession.createalready takes, rather than separate fields.orca agent launch --promptwithout--modelstarts on the remembered model, because no composer is involved. If that model is gone, the first message fails once and recovers as onmain. Follow-up: resolve against the host's saved list when the host creates the chat.Other limits:
main.claude-sonnet-5) is not matched to its short picker name (sonnet). In practice this only affects chats started with an explicit--model.Linked Issue
Replaces #26459. No separate issue.
Visual Proof
Live check on
e0eda1379c5(CI head2a804bfb125adds only an empty commit) in Orca with the real signed-in Claude. The remembered new-chat model was set toclaude-qa-gone-1, a model Claude doesn't list, with effortmax.1. New chat, once Claude's list has loaded: the picker shows Claude's default (Opus 1M), not the gone model.
2. The first message replies on Opus. On
mainit fails with "There's an issue with the selected model". The remembered new-chat model is not rewritten by the send; it changes only when you pick a model yourself.3. A message keeps the model it was sent with (checked in the chat's journal, no screenshot). I sent a message on Opus, then picked Sonnet while it ran. That message's turn used
claude-opus-5-5[1m], and the next message's turn usedclaude-sonnet-5.Also passed:
Seen during the check and not caused by this PR: picking a model while Claude is running resets the chat's permission mode to "Ask for approval". The same probe reproduces it on
main; it's a separate follow-up.Testing
structured-agent-session-send-options.test.ts(real host, store and journal):structured-agent-session-send-mutation.test.ts: options join the operation fingerprint only, and a send without them is byte-for-byte today's request (older-host shape).structured-agent-session-option-view.test.ts: the replacement rule (new chat, reopened chat, held pick, unverified lists, built-in fallback, malformed answers); a listed row whose label is its own id counts; a model only the chat itself lists is moved; an older host shows the chat's own model; a send carries the shown default and a kept unverified model; a pick on its way resolves as the picker shows it, a gone model included; the phone's picker.use-structured-agent-session-send-options.test.tsx: every send carries the shown model and effort, and the shown default; a pick on its way rides along, resolved as the picker shows it; nothing is carried to a host without the capability.structured-agent-session-launch-create-message.test.ts: a launch with an opening prompt carries the replacement, new and resumed.structured-agent-session-message-sender.test.ts: the request carries the options read at the send, and nothing without them.Phone:
use-mobile-structured-agent-session-send.test.tsx(carries the shown model to a capable host, sends the old shape to an older one, takes an effort the replacement offers and sends on it) andmobile-structured-agent-session-host-support.test.ts.pnpm tc:node,pnpm tc:web, mobiletypecheckand the mobile test typecheck ratchet pass. The structured-session host, journal, shared and renderer native-chat tests pass (the SQLite-backed ones on Node), and all 253 phone session test files pass.orca-ci-checkspasses except for two failures this PR doesn't cause: the native lint reports import cycles in phone files this PR doesn't touch (mobile/src/transport,mobile/src/terminal), and the typecheck step fails locally with "Script not found check".tc:nodeandtc:webpass when run on their own.I manually tested these changes locally
Automated tests added/updated, or explained why not below
AI Disclosure
Review
Agent skill upstream boundary
Notes
optionsuses the same validation asagentSession.create's options. The host writes it only through the existing pick path, which already checks the key per agent.options_json, added in place the same way the table's other later columns were, and a message's journal row gains one optionaloptionsfield. An older build ignores both.Checklist
N/Awith reasonpnpm lint,pnpm typecheck,pnpm test, andpnpm buildpass (or CI will cover; local preferred)