fix(composer): a degraded profile must not wipe the saved model

↗ view on GitHub · Amal · 2026-08-18 · e414b530

Found by adversarial review of this stack before it was pushed: one
dropped GET /user/profile permanently cleared the user's composer model
selection.

WHY THIS MATTERS
The stale-selection reset added earlier in this branch is destructive by
design - when a stored `openrouter/*` / `vercel/*` id is absent from the
user's saved router lists, useSelectedModel rewrites localStorage to the
default so the composer never sends an id the backend would reject. That
is right when the lists are TRUE. It is wrong when they are UNKNOWN.

HOW THE WIPE HAPPENED
UserProfileContext answers a failed profile fetch (after its one retry)
with a local fallback profile: `apiKeysDegraded` goes true and every
field is a placeholder, including `openRouterModels: []` and
`vercelModels: []`. ChatInput passed those empty arrays into
useSelectedModel exactly as if they had loaded, the reset effect saw the
stored router id "missing from the saved lists", and persisted the
default over it. The user's pick was gone for good - reconnecting could
not bring it back, because the evidence had been overwritten.

THE FIX
ChatInput passes `null` (the hook's "still loading, leave it alone"
signal) whenever the profile is degraded, so the reset runs only on a
profile the server actually answered. A healthy profile that genuinely
reports no saved router models still resets, unchanged. The
`apiKeysDegraded` doc comment is widened to say what it really means:
NOTHING on a degraded profile is an answer, so no normalization may key
off it.

Not fixed here, by design: a database that predates the
20260818_01 migration answers with real empty lists (the deploy-before-
migrate tolerance in getUserRouterModels), so a router selection does
reset there. That is a genuine "you have no saved router models" state
from the server's point of view, and the selection would not work
anyway until the migration lands.

Tests: ChatInput.modelSelection.test.tsx renders the real hook behind a
degraded and a healthy profile. Pre-fix the degraded case fails with
`Expected "openrouter/pricy/frontier" / Received "gemini-3-flash-preview"`
in localStorage; the healthy-empty case passes before and after, pinning
that the reset itself was not weakened.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Repository open-legal-products/mike
Author Amal <mamalanand3@gmail.com>
Authored
Committed
Parents c03145f3
Stats 3 files changed , +134 , -4
Part of Harden model-router selection and failure handling

Capture this commit into my fork

Download a Markdown prompt that tells Claude how to port this exact commit into your working tree. Run it via claude -p < capture-commit-e414b530.md from inside the repo you want the change in.

⬇ Download capture-commit-e414b530.md