Skip to content

fix: a Claude model that's gone switches to the list's default when you send - #27826

Merged
brennanb2025 merged 19 commits into
mainfrom
brennanb2025/claude-gone-model-default
Oct 14, 2026
Merged

brennanb2025 merged 19 commits into
mainfrom
brennanb2025/claude-gone-model-default

Conversation

@brennanb2025

@brennanb2025 brennanb2025 commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 17 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​842 $\color{#cf222e}{\Huge{\mathbf{−}}}$​23 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​819
Prod 45 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​553 $\color{#cf222e}{\Huge{\mathbf{−}}}$​118 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​435

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:

  • the picker kept showing the retired model;
  • the first message failed with "There's an issue with the selected model (…). It may not exist or you may not have access to it", and only then did the chat fall back to Claude's default;
  • the remembered model was never replaced, so every new chat failed its first message the same way.

What you see now. If the model list Orca has loaded for Claude doesn't include the chat's model:

  • The picker shows the list's default model. That is the row Claude's own default entry resolves to, or the first row when Claude names none.
  • If your effort level isn't one that model offers, the picker shows the nearest one it does offer (max → xhigh), and you can pick any level that model offers, on the phone too.
  • Your message runs on exactly what the picker showed when you sent it, even if you change the picker again before the message reaches Claude. It works the first time.

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:

  • While Orca has no finished model list for Claude (none yet, still listing, a failed listing, or signed out), the chat keeps its model.
  • A model that is on the list is never changed.
  • The remembered model for new chats changes only when you pick a model. Each new chat checks it again when you send.
  • A paired server running an older Orca shows and runs the chat's model as main does (see Mixed versions).
  • Messages sent from the command line or by orchestration carry no model, so a model named with --model runs exactly as given.
  • Other agents keep their current behaviour.

How it works: the message carries its model, the common pattern.

  1. The composer resolves from the list it has. The picker and the send read one effective selection: what the picker shows, including a default it shows when nothing names a model, and a model it keeps because the list isn't verified. If a finished listing doesn't name the chat's model, the shown model is the list's default row, as above. Only the listing proves a row is real (verifiedModelIds, kept in memory only); the chat's own unlisted model, which the picker also shows, never counts.
  2. Every send carries it. agentSession.send gains an optional options field with the model and effort. It uses the same shape agentSession.create already 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.
  3. The host keeps it with the message. The accepted message's journal row stores its options, and so does a queued card. A chat's opening message stores the model and effort the chat was created with. A card the host keeps after a restart or close keeps them too.
  4. The host applies it as it hands the message over, in the one place every message passes: the delivery loop. Direct sends, queue drains and Send now all go through it. With no agent running, the values are recorded on the chat before the start the message needs, so the agent starts on them. With an agent running, it switches before it gets the message. Model first, then effort, and a value the chat already holds writes nothing. A pick made after the message was accepted doesn't change what it runs on. The write goes through the same function a picker change uses, which records the value on the chat. If it fails, the failure is logged and the message is still sent.
  5. A chat started or resumed with an opening prompt creates the chat and its first message in one call. That call already carries options, and now carries the same selection, resolved by the same functions the composer uses, whenever this window holds the host's saved list.
  6. The phone reads the same rule, shows the same replacement, checks a pick against what it shows, and carries the model on its sends.

Mixed versions. agentSession.send rejects unknown fields, so clients send options only to a host that advertises the new agent-session.send-options.v1 capability. That is the same negotiation delivery uses. An older host gets exactly today's send, and the picker shows that host's chat as main does, 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.

  • Same: the send carries the model and effort; the message keeps them until it is dispatched (queued ones included); dispatch applies them to the turn and records them on the chat when they differ. The composer resolves from its loaded list: keep the model while the list isn't verified, keep a listed model, otherwise use the list's default row, else its first row. Effort moves to the nearest level offered, the higher one on a tie. The remembered default is changed only by an explicit pick. Claude's default is the model Claude's own default entry resolves to, unchanged from main.
  • Intended: the chat is created when its tab opens, not at its first send. This is Orca-wide. The chat exists so its picker, commands and history work before the first message. The create call's options carry the selection, so a first message sent with create starts on it.
  • Intended: the host writes a carried value only when it differs from the chat's own. A send that changes nothing costs nothing.
  • Intended: a failed write does not refuse the message. The message runs on what the chat holds, and Claude's existing recovery from "model not found" still applies. Recording a model must never block a send.
  • Intended: the phone holds Send while a pick is on its way, as it does on main, so its sends never carry a pick in flight.
  • Intended: an older host gets no model on the send, and its picker shows what main shows (Mixed versions above). The desktop's own host is always the same build.
  • Intended: Orca keeps model and effort in its existing option record, the same shape agentSession.create already takes, rather than separate fields.
  • Temporary: only Claude chats' sends carry a model. The host side is the same for every agent, and every agent's opening message already keeps the model its chat was created with. Follow-up: carry it for every agent whose host list is complete, Codex first.
  • Temporary: orca agent launch --prompt without --model starts on the remembered model, because no composer is involved. If that model is gone, the first message fails once and recovers as on main. Follow-up: resolve against the host's saved list when the host creates the chat.

Other limits:

  • Before Orca has a verified model list (the very first Claude chat on a new setup while the list loads, or while signed out), the chat keeps its model, as the common pattern does. That message can still fail once and recover as on main.
  • For up to about 10 minutes after Claude drops a model, Orca's saved list can still name it. The list is also re-read at startup and whenever a chat runs.
  • A model saved under its full name (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 head 2a804bfb125 adds only an empty commit) in Orca with the real signed-in Claude. The remembered new-chat model was set to claude-qa-gone-1, a model Claude doesn't list, with effort max.

1. New chat, once Claude's list has loaded: the picker shows Claude's default (Opus 1M), not the gone model.

picker shows the default

2. The first message replies on Opus. On main it 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.

first message replies

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 used claude-sonnet-5.

Also passed:

  • a hand-picked Sonnet runs as picked;
  • a stopped chat replies on reopen;
  • a message sent 0.4 s after opening the tab replies.

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):

    • a send to a chat with no agent running records the model and effort before the start, and the agent starts on them;
    • a running agent is switched model first, then effort, as the message is handed over, and a value the chat already holds writes nothing;
    • a message accepted while its agent starts runs on its own model even though a later pick changed the chat's, and that write lands before the message is dispatched;
    • a chat's opening message runs on the model it was created with though a pick made before its handover changed the chat's, and a card kept after a restart keeps them;
    • a failed write still sends the message;
    • the same operation id with other options is refused as a conflict;
    • a queued message keeps its options and applies them when the queue sends it, or when you press Send now;
    • a message kept as a card after a crash keeps its options on the card.
  • 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) and mobile-structured-agent-session-host-support.test.ts.

  • pnpm tc:node, pnpm tc:web, mobile typecheck and 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-checks passes 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:node and tc:web pass 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

  • Not applicable, or this change copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.

Notes

  • Security: options uses the same validation as agentSession.create's options. The host writes it only through the existing pick path, which already checks the key per agent.
  • Cross-platform: pure logic.
  • SSH / paired servers: the host that owns the chat applies the model, as it does for every pick. Older servers get today's send.
  • Folder workspaces: unaffected.
  • Stored data: queued message cards gain one nullable column, options_json, added in place the same way the table's other later columns were, and a message's journal row gains one optional options field. An older build ignores both.
  • Performance: one record lookup per handed-over message. It writes only when a value changed.

Checklist

  • This PR is small and focused
  • I explained what changed and why (ELI5, the user-facing before/after, the mechanism, and why over the alternatives)
  • Before/after screenshots or videos attached for UI changes, or N/A with reason
  • Self-reviewed for correctness, security, and performance
  • Cross-platform, SSH/remote, and path/shortcut impact considered (or N/A)
  • pnpm lint, pnpm typecheck, pnpm test, and pnpm build pass (or CI will cover; local preferred)

…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.
@brennanb2025 brennanb2025 changed the title fix: start a new Claude chat on the default model when its remembered model is gone fix: a new Claude chat switches off a remembered model Claude no longer offers before its first message Oct 12, 2026
…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.
@brennanb2025 brennanb2025 changed the title fix: a new Claude chat switches off a remembered model Claude no longer offers before its first message fix: a Claude model that's gone switches to the list's default when you send Oct 12, 2026
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.
@brennanb2025

Copy link
Copy Markdown
Contributor Author

Review summary (head 8c4516b95bf)

What this fixes: when Claude stops offering the model a chat is set to (or the model remembered for new chats), the first message failed with "There's an issue with the selected model", and every later new chat failed the same way. Now the picker shows Claude's default instead, and the send uses it.

How it was reviewed

  • Five rounds of independent code review on successive designs. The final pass on 7951fa088be approved with no blocking issues and found nothing removable. The only later change is a merge of main with no conflicts.
  • Two earlier designs (decide when the chat is created on the host; pick automatically when the tab opens) failed the live check with the real Claude, because a chat exists before Claude's model list has loaded. The final design decides at send time from the list the picker has, which is the common pattern. It has no wire change and no stored state.
  • Findings fixed along the way: picker and send disagreeing, a list that's loading, failed or malformed being trusted, an explicit pick being overwritten, and a double apply across quick sends.
  • Deliberate limits, listed in the body: a prompt sent while the chat is being created; single-choice effort; the phone; stale lists for up to about 10 minutes; a send before the list loads.

Live check: real signed-in Claude on this head (see Visual Proof).

  • A remembered gone model shows Claude's default once the list loads.
  • The first message replies, and the remembered model updates.
  • A hand-picked model runs as picked.
  • A stopped chat works.

CI: green on 8c4516b95bf. Local: 436 test files pass (4,529 tests); tc:web and tc:node pass.

Replaces #26459.

@coderabbitai

coderabbitai Bot commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 03876b82-6e5d-4788-943e-1069a1d65963
📥 Commits

Reviewing files that changed from the base of the PR and between 2a804bf and b9fcc03.

📒 Files selected for processing: 12
  • config/scripts/vitest-sqlite-runtime-files.mjs
  • 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-agent-session.ts
  • mobile/src/session/use-mobile-structured-turn-indicator.test.tsx
  • src/main/native-chat/agent-session-wire/structured-agent-session-create.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-mutation-plans.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-turns.ts
  • src/shared/agent-session-create-capabilities.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 8 remain after this review.

📝 Walkthrough

Structured agent sessions derive model and effort values from displayed options and include them in sends when the host advertises send-options support. Send options are validated and included in operation fingerprints. Journal and queued-message records retain the options, and host delivery applies them before handing submissions to a provider child. Model views track verified host catalog entries and can select a listed fallback model and supported effort.

Priority: ➖ Normal

Merge Risk: ⚪ Minimal · up to b9fcc

Queued sends retain their selected options through handover. If the host rejects an option change, delivery intentionally uses the chat’s existing option; no actionable merge-blocking risk is established.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 44.78% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 67 functions across 56 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Title check Passed The title clearly describes the primary behavior change: replacing an unavailable Claude model with the model-list default when sending.
Description check Passed The description is detailed and covers the required explanation, changes, rationale, linked issue, visual proof, testing, compatibility, and checklist information. Some checklist items remain unchecke…
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
  • Fix all pre-merge checks with AI
✨ Finishing Touches: 💡 1
📝 Generate docstrings: 💡
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@brennanb2025
brennanb2025 marked this pull request as draft October 13, 2026 07:31
…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).
…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.
@brennanb2025

Copy link
Copy Markdown
Contributor Author

Review summary (head 2a804bfb125; code at e0eda1379c5)

This supersedes my earlier summary. The PR was rebuilt on the common pattern.

What it fixes: when Claude stops offering a chat's model (or the model remembered for new chats), the first message failed with "There's an issue with the selected model", and so did every later new chat. Now the composer shows Claude's default, and the message runs on it.

Mechanism (the common pattern):

  • The composer works out the model from the list it has loaded. It keeps your model while the list isn't verified, keeps a listed model, and otherwise uses the default row (else the first), with effort moved to the nearest level offered.
  • Every send carries that model and effort to the host, as an optional field the host must advertise support for. Older hosts are unchanged.
  • The message keeps them until it is handed to Claude: direct sends, queued messages, Send-now, the opening prompt, and across a restart. At handover they are applied through the same writer a picker change uses.
  • The remembered new-chat model changes only on a hand pick.
  • The PR body labels every remaining difference as intended or temporary.

Reviews: several rounds of independent review against both the code and the references. Fixed along the way:

  • the selection lost between acceptance and dispatch;
  • the opening prompt's model not stored;
  • an in-flight pick skipping resolution;
  • the picker shown to older hosts;
  • phone effort validation;
  • a label heuristic;
  • queue-only plumbing, now removed.

The final pass on e0eda1379c5 approved, with no blocking issues.

Live check: real signed-in Claude on e0eda1379c5, all rows passed (see Visual Proof).

CI: fully green on 2a804bfb125.

Follow-ups, not in this PR:

  • Main bug found during QA: a model pick while Claude runs drops the chat's permission mode. The host replaces the record's options with Claude's answer, which has no mode.
  • Temporary differences listed in the body: only Claude sends carry a model; a CLI --prompt without --model keeps today's fallback.

@brennanb2025
brennanb2025 marked this pull request as ready for review October 13, 2026 12:21
…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
@brennanb2025
brennanb2025 merged commit 04b4e96 into main Oct 14, 2026
32 checks passed
@brennanb2025
brennanb2025 deleted the brennanb2025/claude-gone-model-default branch October 14, 2026 04:22
brennanb2025 added a commit that referenced this pull request Oct 14, 2026
…H and headless hosts (#28558)

Picked from PR head fb40193 before merge to main (main's typecheck is broken by #28268 + #27826; fix #28570).
nwparker added a commit that referenced this pull request Oct 14, 2026
#27826 and #28268 crossed: the option-intent writer now needs `now`, and the
message-options context didn't supply it, breaking main's typecheck.

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant