Conversation
…pen-mercato#906) The GitHub tab's hand-off panel and the Inbox card held their engine pick in plain `useState`, so every mount reset it to "never touched" — and that is precisely the state `resolveModel` resolves to `defaultModels[runner]`, which `configAnswer` seeds from the coding agent's OWN settings file. A user who picked `auto` on every hand-off still got every run pinned to whatever `~/.claude/settings.json` names, and `engineBody` sent it as an explicit `model`, so the pin was real rather than cosmetic. The pick now lives in the `cez-followup-selection` store that already remembers the workflow and skills pick, behind two hooks in `engine-pills.tsx` — the file that exists so these surfaces cannot drift from the composer. `null` still means "never touched" and an explicit auto is `''`; collapsing the two is the bug, so the store round-trips `''` verbatim and `writeFollowupSelection` became a merge write, letting the engine and the workflow pick share one key without clobbering each other. The GitHub tab persists; the Inbox seeds from the store without writing back, which fixes the same symptom there while preserving open-mercato#401's rule that a pick on one card must not re-aim the card below it. A validity sweep drops a remembered runner this host cannot honour, or a model belonging to another backend, rather than letting it ride a POST — the pick is remembered in a localStorage key that cockpits for different repos share. Settings -> Agents could not express an explicit auto at all: clearing the preset was indistinguishable from never setting one, so the native default showed straight back through and the select snapped back to it. A new optional `defaultModelsAuto` key fixes that additively — `configAnswer` answers `''` for an overridden runner, which every model picker already reads as auto, so no response shape changes and older readers are unaffected. It is deliberately its own key rather than an "auto" sentinel inside `defaultModels`, which an older cezar would pass straight to --model.
|
🤖 Updated in place by I do not have label permissions on this repository, so none of these could be applied — they are recorded here for a maintainer, and this single comment is the current intended set.
Not applied and why: no |
|
🤖 The assignee and the |
|
🤖 |
open-mercato#906) Review follow-up. "Nothing is connected" and "provider status has not answered yet" are indistinguishable inside the sweep, so a version that treats an empty runner list as evidence clears the remembered pick on every mount — and, because the GitHub surface persists, writes the `null` back to the store. That destroys the choice permanently and quietly reintroduces open-mercato#906. Removing the `loaded` guard did fail a test already, but only obliquely: it tripped the foreign-backend-model case, whose name tells a future maintainer nothing about why the guard exists. AGENTS.md asks for exactly this pairing — a fail-open pass-through needs the empty/absent-input case pinned, or it lies. Also type `autoModelOverrides` explicitly. `Object.fromEntries` answers with an index signature, and `configAnswer`'s return type is asserted `Exact` against `configResponseSchema`; the current spread order happens not to leak it, but reordering would widen `defaultModels` past the contract.
sapersky
left a comment
There was a problem hiding this comment.
🔍 Code Review
🎯 Summary
PR #907 fixes #906 on two fronts. The primary defect: the GitHub tab's hand-off panel and the Inbox card held their engine pick in unpersisted useState, so every mount reset it to null — and null is precisely the state resolveModel resolves to defaultModels[runner], which configAnswer seeds from the coding agent's own settings file. The pick was not merely forgotten; engineBody sent the resolved native model as an explicit model, so a user who chose auto every time still had every run pinned. The secondary defect: Settings → Agents could not express an explicit auto at all, because clearing the preset was indistinguishable from never setting one.
The fix is well-placed. The pick moves into the cez-followup-selection store that already remembers the workflow and skills pick, behind hooks in engine-pills.tsx — the file whose own doc block says it exists so these surfaces "cannot drift from the composer", and "is the pick remembered at all" was exactly the axis it did not own. The null vs '' distinction is preserved deliberately and tested explicitly, which matters because collapsing the two would leave the bug in place. writeFollowupSelection becoming a read-modify-write merge is the right call now that two owners share one key.
Two things I verified rather than assumed, because they are this repository's load-bearing invariants. First, the contract-parity guard: configAnswer's return type is asserted Exact against configResponseSchema, and I confirmed the guard is live by adding a stray response field and watching contract-parity.workspace.test.ts fail with "schema-is-wider" — so the new '' answer genuinely does not widen the response. Second, defaultModelsAuto needed to reach the contract (not just server.ts) because SetConfigInput is the type the cockpit's putConfig and agents-section.tsx compile against; it does.
Verdict: approve — no blockers and no remaining majors. Two findings surfaced during review and both were fixed in-review in 58fe2ef1 (this PR is the automation's own, run with --autofix), so neither blocks. Note that GitHub does not permit approving one's own pull request, so this is submitted as a review comment: a maintainer's approval is still required, and the merge-queue / needs-qa labels could not be applied for lack of write permission.
🧪 Validation Gate
| Command | Result |
|---|---|
npm run typecheck |
✅ pass — 0 errors across contract, api-client, server and web |
npm test |
✅ pass — 6122 passed / 321 files (see the environment note below) |
npm run test:unit |
✅ pass — 35 passed, 0 failed |
npm run build |
✅ pass — check:pack ok — 475 files, 85 under web/dist |
npm run test:package |
✅ pass — 15 passed, 0 failed |
Environment note, stated plainly because the gate rule allows no excuses and I therefore did not offer one. A first npm test run on the contributor's machine showed 12 failures. Rather than wave them off as pre-existing, I identified and demonstrated the cause: TMPDIR was …/cezar/.ai/cezar/tmp/<taskId>, which git rev-parse --show-toplevel resolves to inside the repository — so every assertion of the form "outside a git repository" / "not a git worktree" / "non-repo dir" cannot hold — and LANG was pl_PL.UTF-8, which formats 20000 as 20 000 and breaks a toContain('20,000') assertion. Re-running the four affected files with TMPDIR=/tmp LANG=en_US.UTF-8 turned all 137 of their tests green, and the full suite dropped from 12 failures to 3. Those last 3 are load-related timeouts (~4–5s each) whose membership changes between runs; all pass in isolation. None of the 12 touches a file in this diff, and all reproduce identically on the unmodified base commit. CI remains the authority — only license/cla (pass) has reported so far.
🟠 Major
1. packages/web/src/components/engine-pills.tsx:198 — the sweep's fail-open guard had no test naming it. Fixed in-review (58fe2ef1).
useEnginePickValidity cannot distinguish "no backend is connected" from "provider status has not answered yet" — both are an empty runners array. The loaded guard is what stops the second case being treated as evidence, and it is load-bearing: without it a remembered pick is cleared on every mount and, because the GitHub surface persists, the null is written straight back to the store. That destroys the user's choice permanently and quietly reintroduces #906, and it also turns an unreachable agent CLI — a documented degradation path, priority 2 in CODE_REVIEW.md — into silent state loss.
Removing the guard did already fail one test, but only incidentally: it tripped the foreign-backend-model case, whose name would tell a future maintainer nothing about why the guard exists. AGENTS.md is explicit here — "a fail-open helper needs a populated-input guarantee, or it lies… pair every silent pass-through with a test that pins the empty/absent input case."
Fix applied: a test asserting that with PROVIDERS_NONE the remembered runner and model both survive, gated on the Run button becoming disabled so it cannot pass on a pre-query render. Verified to fail when the loaded guard is removed.
🔵 Minor
2. packages/cezar/src/server/server.ts:5032 — autoModelOverrides relied on inference where the contract is asserted Exact. Fixed in-review (58fe2ef1).
The helper returned Object.fromEntries(...), whose type carries an index signature. It is spread into configAnswer's defaultModels, and that object's type is asserted Exact against configResponseSchema. The current spread order happens not to leak the index signature — I confirmed the assertion passes — but that is a property of the ordering, not of the code: moving autoModelOverrides after the preset spread could widen defaultModels past the contract, and the failure would surface as an opaque type error in a parity test far from the change. Now annotated : AgentModelDefaults, which is the shape it actually produces.
⚪ Nits
3. packages/web/src/components/engine-pills.tsx:15 — a shared component imports from a route directory. @/routes/github/hand-to-agent-draft is route-local by path, and engine-pills.tsx is a shared component. I checked before flagging: the file already imports @/routes/new-task-form for the whole resolution quartet, so the direction is established precedent rather than something new, and hand-to-agent-draft.ts is a React-free pure store, so nothing route-shaped is pulled into the shared chunk. The alternative — duplicating the store or hoisting it to @/lib — would either fork the cez-followup-selection key or churn a file with its own passing test suite for no behavioral gain. Recording it so the next reader knows it was a decision.
4. packages/web/src/components/engine-pills.tsx:141 — the persistence effect writes the seed back on first mount, a no-op localStorage write of values just read from it. Harmless, and guarding it would cost more code than it saves.
5. packages/web/src/components/engine-pills.tsx:198 — the sweep still evaluates while modelsLocked is on, so it can clear a remembered model that the locked host was ignoring anyway. The user's stored preference would otherwise return when the lock is lifted. Genuinely trivial; worth a thought only if modelsLocked ever becomes common.
💥 Breaking Changes
- Exported APIs — no export removed or renamed.
FollowupSelectiongains two fields andwriteFollowupSelectionwidens its parameter toPartial<…>, which is accepting-more, not breaking. Both are internal to the cockpit bundle, not a published entry point. - HTTP routes and response shapes —
GET/PUT /api/v1/configis listed as a protected surface inBACKWARD_COMPATIBILITY.md§2, and the change is strictly additive: one new optional request key. The response shape is unchanged —runnerModelsSchemaalready accepted'', so answering it needed no schema edit, verified by the liveExactparity assertion. No route added, so nobc-route-inventoryentry is due. - Semantics of existing keys —
defaultModelsand the meaning ofnull/''on it are untouched. The override is deliberately a separate key rather than an"auto"sentinel insidedefaultModels, because an older cezar would pass such a value straight to--model. An older cockpit reading a newer server sees'', which already means auto in every picker; a newer cockpit against an older server simply fails to persist the override, with no error. - Config formats —
config.jsongains one optional key with.optional().catch(undefined), matching its siblings, so a malformed value degrades per-key and old files parse unchanged.BACKWARD_COMPATIBILITY.mdis updated in the same commit, including the rationale for a separate key. - Persisted client state — the
cez-followup-selectionlocalStorage key gains fields;normalizeSelectionstill accepts the old two-field shape, pinned by a test, so an existing install upgrades silently. - Run-time behavior of
auto— confirmed by search that no run-launch path readsdefaultModels; it is purely a cockpit preselect. Whatautomeans when a run starts (no--modelflag, the agent CLI decides) is unchanged, as #906 required. - Graceful degradation —
localStorageunavailable still degrades to "nothing remembered" on both read and write, including the new merge-read insidewriteFollowupSelection.
🧪 Test Coverage
Coverage is strong, and unusually well-targeted for this kind of fix. Nine cases across five files, and — following AGENTS.md's "prove the regression test fails without the fix" rule — I verified each one bites in the right direction rather than trusting that it does:
hand-to-agent-draft.test.ts— the engine round-trip;''kept distinct fromnullincluding in the stored JSON; the pre-#906 two-field shape reading as never-touched; an unknown runner and a non-string model degrading tonull; and the merge write, so the engine and workflow picks cannot clobber each other.github.test.tsx— an explicitautosurviving a full cold remount on a host whose config reports a native default, with nomodelon the POST; both sweep branches; and the new unanswered-provider guard.inbox.test.tsx— a rememberedautobeating the configured default with no body on the wire, and the#401guard that a card's pick never reaches the store.config-api.test.ts— the override beating a native default and answering''; the per-runner merge storing only a real opinion; themodelsLockedrefusal writing nothing at all; and an old-shaped config round-tripping untouched.agents-section.test.tsx— picking auto claims the override, naming a model releases it.
Two details worth crediting, because both are the failure mode this repo warns about. The Inbox assertion is deliberately gated on a post-config signal (the default runner arriving) — an earlier draft asserted "the pill shows auto" and passed on the pre-load render, green either way, which is exactly the green-either-way test AGENTS.md calls out. And the #401 no-write-back guard was verified to fail if the Inbox is switched to the persisting hook, so the asymmetry between the two surfaces is pinned as a decision rather than left to a comment.
The one deliberate departure from #906's written plan — the Inbox seeds from the store but never writes back — is argued in the PR body and in the code, and is the right call: writing back would break #401's recorded rule that a pick on one card must not re-aim the card below it, and persisting would make that leak global and survive reloads, which is strictly worse than the route state #401 already rejected. Seeding alone still removes the reported symptom there.
🧪 Manual QA instructions (
|
|
🤖 Lock retained — chain continues ( |
|
🤖 No CI has run, and none can until a maintainer releases it. As of 2026-08-19T08:10:39Z there are zero workflow runs for the head SHA I waited a bounded interval and re-checked rather than assume; the result was unchanged. No further CI follow-up will come from this agent. Standing in for CI, here is the full
One caveat stated openly, because it affected the numbers. On this machine |
|
🤖 |
📸 UI verification — PASS
The precondition matters, so I verified it rather than assumed it. This host's P1 — GitHub hand-off: the pick is remembered
Checkpoint 05 is the one that proves the fix is real rather than cosmetic. #906's evidence was that P1 — Settings → Agents:
|
| # | Action | Observed | |
|---|---|---|---|
| 06 | Open Settings → Agents | claude = opus[1m], codex = gpt-5.6-sol, opencode/pi = auto; no config.json |
✅ defect 2's starting state |
| 07 | Choose auto (default) for claude | Select stays on auto; config.json = {"defaultModelsAuto":{"claude":true}}; API → defaultModels: {"claude":"","codex":"gpt-5.6-sol"} |
✅ |
| 08 | Reload the page | Still auto | ✅ |
| 09 | Choose sonnet instead |
config.json = {"defaultModels":{"claude":"sonnet"}}, defaultModelsAuto removed |
✅ the two are never both set |
Two details worth naming. The stored shape is the additive defaultModelsAuto key — there is no "auto" sentinel inside defaultModels, which is the shape an older cezar would have passed straight to --model. And codex kept its own native default (gpt-5.6-sol) throughout, so the per-runner merge really is per-runner.
P1 — Inbox: seeded from the remembered pick, still per-card
| # | Action | Observed | |
|---|---|---|---|
| 10 | Open the Inbox with the native default at opus[1m] and config.json = {} |
Both cards show auto |
✅ the remembered pick beats the native default here too |
| 11 | Re-aim card 1 to codex | card 1 = codex / GPT-5.6-Sol; card 2 unchanged (claude / auto); store untouched (runner:null, model:"") |
✅ #401 holds |
Checkpoint 11 is the deliberate asymmetry this PR argues for, confirmed in a browser: the Inbox reads the remembered pick but never writes to it, so re-aiming one card cannot re-aim its neighbour — and nothing that happened on a card leaked into storage for the next reload to inherit.
P2 — Perceived performance and the untouched-behavior guard
| # | Action | Observed | |
|---|---|---|---|
| 12 | Cold-load the changed route | domContentLoaded 29ms, loadEvent 29ms; pill reads auto on first paint with no flash of the native model |
✅ |
| 13 | Open in a brand-new browser profile (empty localStorage) | Pill = opus[1m] |
✅ a new user with no remembered pick still gets the configured default |
Checkpoint 13 is a guard rather than a fix: it pins the behavior this PR deliberately did not change. Remembering a pick must not stop an untouched surface honouring the configured default.
⚠️ Not exercised — stated plainly, not folded into the pass
- Mobile viewport smoke. Not attempted-and-passed; not attempted.
agent-browser 0.33.2has noviewportordevicecommand — it answersUnknown command: viewport, and an earlier attempt that appeared to resize in fact leftinnerWidthat 1280, so the screenshot it produced was desktop-width and I deleted it rather than present it as mobile evidence. A mobile pass on these surfaces still needs a human or a provider that can set a viewport. - The remembered pick surviving an unanswered provider status. This needs
/api/v1/providers/statusstalled, which the live env cannot do. It is covered by the unit test added in58fe2ef1during review, verified to fail when theloadedguard is removed — but it is test coverage, not browser evidence, and I am not claiming otherwise.
Environment
Booted via .ai/scripts/test-env-up.sh (production build, CEZ_DRY_RUN=1, agent CLIs mocked) at http://127.0.0.1:54811. One deviation to disclose: the Inbox is capability-gated off by default (/api/v1/health reported followups: false, and /api/v1/todos returns [] regardless of file contents while it is off), so for checkpoints 10–11 I restarted the app with the descriptor's own start command plus CEZ_FOLLOWUPS=1 and seeded two runnable todos.json fixtures. Everything else ran on the stock environment. Both the fixtures and the environment are torn down with this run; source files were not touched.
Evidence hosting
The screenshots are hosted on a dedicated, slash-free qa-evidence-pr907 branch of the fork (sapersky/cezar) rather than of this repository, because I have no write access here. The fork is public, so the raw.githubusercontent.com URLs render inline exactly as they would otherwise — a better outcome than the links-only fallback, and it keeps evidence off this change's own branch, which is the rule that matters. The full artifact set (13 PNGs plus report.json and report.md) is also on disk at worktrees/09c4c396-f394-451d-ae3e-a3170287b2c9/.ai/qa/artifacts_pr907.
Screenshots
step-01-untouched-shows-native-opus
step-02-picked-auto
step-03-auto-survives-item-hop
step-04-auto-survives-full-reload
step-05-run-started-with-auto
step-06-settings-shows-native-opus
step-07-settings-auto-sticks
step-08-settings-auto-survives-reload
step-09-real-model-releases-override
step-10-inbox-cards-seeded-with-auto
step-11-inbox-per-card-pick-not-shared
step-12-cold-load-desktop
step-13-fresh-profile-still-honors-native-default
🧪 Follow-up: a browser-level test this change does not yet ship
Suggested file: Fixture requirement — the part that makes or breaks the test. The whole defect only exists when Scenario
Also worth covering, and not exercisable in this QA pass: stall or fail |
|
🤖 🤖 Where this stands, and what needs a human. The work itself is done and self-consistent: two commits, the full local validation gate green, an approve verdict on review with both findings raised there fixed in-review, and browser evidence that the reported symptom is gone (a run started from the GitHub tab with
Not merging and not adding |
One conflict, in `packages/web/src/components/engine-pills.tsx`, and only in its import block. open-mercato#924 moved the agent-account lookup out of `useAgentProfiles`/`useRepo` into the shared `@/api/agent-accounts` hook, while open-mercato#906 added the `useEffect`/`useState` the remembered-pick hooks need. Both sides kept: main's account imports, plus the react import this branch introduced. The bodies merged without overlap — the account logic and the remembered-pick hooks touch different parts of the file. typecheck, build and check:pack clean. The 13 `npm test` failures are the environmental ones already documented in the PR body (TMPDIR resolves inside the repo here, so the "outside a git repository" assertions cannot hold; two numbers formatted under pl-PL), plus one from main that fails for the same locale reason.













Closes #906
🎯 Goal
An explicit engine pick made on the GitHub tab's hand-off panel or an Inbox card is remembered, exactly like the composer's, so choosing
autostaysautoacross reloads and item hops. The symptom this removes: a user who pickedautoon every single hand-off still had every run pinned to the model named in their coding agent's own settings file — silently the most expensive one available, at real quota and money cost.🔍 Problem
defaultModelsis seeded server-side from the coding agent's native settings (~/.claude/settings.jsonand its siblings), so "the user has not chosen a model" is not a neutral state — it hands the choice to that file. Both start surfaces outside/newheld their engine pick in unpersisteduseState, so every mount put them back into exactly that state. The pick was not merely forgotten in the UI:engineBodysent the resolved native model as an explicitmodelfield, so the pin was real. Separately, Settings → Agents could not express an explicitautoeither, so the same override could not be neutralized there — the select snapped straight back to the native model.🔍 Root Cause
Two defects, one cause:
null("never touched") was the only state these surfaces could be in after a mount, andnullis the state that lets the native default win.github.tsxhelduseState<EnginePick>({runner: null, model: null, account: null})while its sibling workflow/skills state hydrated from and wrote back tohand-to-agent-draft.ts— whoseFollowupSelectioncarried no engine fields.inbox.tsxhad the identical unpersisted state.useResolvedEnginethen calledresolveModel(null, …, defaultModels, …), which by its own documented contract ("onlynullmeans never touched") returnsdefaults[runner], andengineBodyput that on the wire. The composer escaped this only because its pick lives in a persisted draft (new-task-draft.ts).agents-section.tsxsentevent.target.value || null,PUT /api/configtreatsnull/''as delete the key, andconfigAnswerthen let the native value show through again. There was no way to say "ignore the native default" short of hand-editing the vendor's file.What Changed
packages/web/src/routes/github/hand-to-agent-draft.ts—FollowupSelectiongainedrunnerandmodel, normalized per-key in the store's existing tolerant stance (an unknown runner or a non-string model degrades tonull, and the pre-Fix: GitHub/Inbox hand-off forgets the model pick and silently pins the native default model #906 two-field shape still reads as "engine never touched").''round-trips as'': keeping an explicit auto distinct from never-touched is the fix, so collapsing them would leave the bug in place.writeFollowupSelectionbecame a read-modify-write merge — the same stancePUT /api/configtakes on the rawconfig.json— because the workflow pick and the engine pick now share one key under separate owners, and a full overwrite would let whichever effect ran last erase the other's choice.packages/web/src/components/engine-pills.tsx— the newuseRememberedEnginePick/useSeededEnginePickhooks, plus the validity sweep. They live here because this file already exists so these two surfaces "cannot drift from the composer", and "is the pick remembered at all" was the one axis the shared component did not own — which is exactly where they drifted. The sweep drops a remembered runner that is not connected on this host, or a modelmodelConflictsWithRunnerrejects for the resolved runner, rather than letting it ride a POST; it waits for provider status to load first, since an in-flight fetch is not evidence of absence. This is not theoretical — cockpits for different repos share onelocalhost:<port>origin and therefore this localStorage key. An explicit auto ('') never conflicts and always survives.packages/web/src/routes/github/github.tsx— the hand-off panel's pick is now remembered on the same footing as its workflow pick.packages/web/src/routes/inbox.tsx— the card seeds from the remembered pick without writing back. This is a deliberate asymmetry rather than an oversight, and the one place this PR departs from the issue's written plan: writing back would break#401's recorded rule that "run this one on codex must not silently re-aim the card below it", and persisting would make that leak global and survive reloads — strictly worse than the route state#401already rejected. Seeding alone still removes the reported symptom here, because an untouched card no longer falls through to the native default. Both halves are pinned by tests so the asymmetry stays a decision.packages/cezar/src/config.ts,packages/contract/src/workspace.ts,packages/cezar/src/server/server.ts— a new optionaldefaultModelsAuto(per-runner boolean).configAnsweranswers''for an overridden runner, layered over the native seed and under a repo preset, so naming a preset later simply wins. The fixed-model policy (modelsLocked) refuses the override on exactly the same terms as a preset, and onlytrueis ever stored —false/nulldeletes the key rather than persisting something that means nothing.packages/web/src/routes/settings/agents-section.tsx— picking auto now clears the preset and claims the override; picking a real model releases it.BACKWARD_COMPATIBILITY.md— documents the new key and why it is a separate key rather than a sentinel.🧪 Tests
npm run typecheck— clean.npm test— 6112 passed / 324 files.npm run test:unit— 35 passed.npm run build— ok (check:packok).npm run test:package— 15 passed.npm testare pre-existing and environmental on the contributor's machine, verified identical on a clean checkout of this branch's base:TMPDIRresolves inside the repository here, so the "outside a git repository" / "non-repo dir" assertions cannot hold, plus one number formatted by a non-en-USlocale and two load-related timeouts. None involve the files in this PR.hand-to-agent-draft.test.ts— the engine round-trip;''vsnullkept distinct (including in the stored JSON); the pre-Fix: GitHub/Inbox hand-off forgets the model pick and silently pins the native default model #906 two-field shape reading as never-touched; an unknown runner and a non-string model degrading tonull; the merge write, so the engine and workflow picks cannot clobber each other.github.test.tsx— an explicitly pickedautosurviving a full cold remount on a host whose config reports a native default, with nomodelon the POST; both sweep branches (a dead runner omitted from the request and cleared from the store; a foreign-backend model cleared from the store).inbox.test.tsx— a remembered explicit auto beating the configured default and leaving no body on the wire (gated on a post-config signal, so it cannot pass on the pre-load state); and the#401guard, which fails if the Inbox is made to persist.config-api.test.ts— the auto override beating a native default and answering''; the per-runner merge storing only a real opinion; themodelsLockedrefusal writing nothing at all; a config written before this change round-tripping untouched.agents-section.test.tsx— picking auto claims the override, naming a model releases it.💥 Breaking Changes
GET/PUT /api/v1/configis a protected surface (BACKWARD_COMPATIBILITY.md§2) and this change is strictly additive: one new optional key, absent by default, ignored by older readers, with no change to the meaning ofdefaultModelsor ofnull/''on it. Answering''needed no response-schema change becauserunnerModelsSchemaalready accepts it, and''already is auto in every model picker — so an older cockpit reading a newer server shows auto rather than meeting an unknown sentinel. A newer cockpit against an older server simply fails to persist the override, with no error.cez-followup-selectionlocalStorage key gains fields;normalizeSelectionstill accepts the old two-field shape, so an existing install upgrades silently.defaultModels, so whatautomeans at run time (no--modelflag; the agent CLI decides) is untouched.