fix(provider-setup): name the stalled enable write instead of blaming the model picker (#1649) - #1672
Conversation
… the model picker (#1649) The flush gate added in #1651 gave up eight times on the 2026-09-01 daily, every one reading "30 toggle(s) clicked, 1 write(s) started, 0 finished" — the batched POST /api/v1/models/enabled_models was issued and never answered within 90 s. The gate warns rather than throws, so the panel closes anyway, the close-path flush never refreshes the picker, and the picker keeps the pre-toggle set. The picker is correct there: aria-checked is the optimistic client cache and flips before any request, so the panel is the source that lies. The failure 90 s later nonetheless reported MODEL_PICKER_DEFECT and named two hypotheses nobody had measured. Carry the batch's own observation forward. writeStallReason() and modelTriggerStallMessage() are pure; resolveModelOption gains a write-stalled verdict consulted inside the existing checked-includes-requested branch, after empty/match/unmatchable; the three provider setups capture the batch result they were discarding and open the picker through one shared helper that re-throws a model_model timeout as the same named stall, keeping Playwright's original message. No assertion weakened, no budget raised, nothing became a skip. A picker miss after a settled batch is still MODEL_PICKER_DEFECT, and an unobserved batch leaves every existing verdict byte-identical. This does not make a saturated day green — those specs still fail, naming the instance instead of the picker. The run-level cause that puts a spec on this path (collect-models confirming 0 of 74 enable writes) is #1666. Closes #1649 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CI: green, and the two flakies read faithfullyAll 7 checks passed. The E2E lane really ran — 1. 2. The stack frame is new, the behaviour is not. Before this PR the identical wait lived inline at So the honest reading of #2 is: an instance that did not bring the trigger back within 60 s while the toggle batch had settled — the class of Local validation on 🤖 Generated with Claude Code |
Closes #1649.
Problem
MODEL_PICKER_DEFECTfired 3× again on the first daily after #1651 (33511210195, 2026-09-01), one of them a hard failure, with the numeric fingerprint this issue was opened on intact —15 option(s) enumerated (Anthropic: 5, Google Generative AI: 5, OpenAI: 5). Only the provider moved:gpt-4o-mini/OpenAI →gemini-3.5-flash/Google.The cause is not the picker, and #1651's gate already knew it. That gate gave up eight times on that run (shards 1, 2 and 4), every one reading the same shape:
POST /api/v1/models/enabled_modelswas issued and had not answered 90 s later. The listener countsrequestfailedtoo, so that is a hang, not a network error. The gate then closes the panel anyway — by design: it warns, it does not throw — the close-path flush never callsrefreshAllModelInputs, and the picker keeps the pre-toggle enabled set, which on a freshly configured provider isMIN_DEFAULT_MODELS = 5.The picker is correct there; the panel is what lies.
aria-checkedisuseModelToggleQueue's optimistic cache — it flips at click time, before any request — and it is exactly the source feeding the loud message's "is ENABLED in the provider panel". Corroborated on the same run bycollect-models' own server-side read (0 of 30/36/8enable writes confirmed, on all four shards) and by5beingMIN_DEFAULT_MODELS(lfx/base/models/model_utils.py).So the gate observed the real cause and printed it to a log line that no failure message, no
error_signatureand no triage dataset correlates, while the failure 90 s later named two hypotheses nobody had measured. That is why this issue carried two verdicts and a reopen.Two of the six occurrences are the same cause wearing a different face: after a give-up, the
model_modelwait exhausts its 60 s and dies as a barelocator.waitFor: Timeout 60000ms exceeded(agent-current-date-tool.spec.ts:260attempt 1,agent-context-id-continuity.spec.ts:407attempt 0).Verdict:
test-defect— in attribution. Reproduced causally rather than by re-running, since re-running cannot falsify a timing defect: on a healthy1.12.0.dev45container a scout held that POST for 120 s withpage.route, everything else unchanged, and produced the daily's evidence byte for byte (36 toggle(s) clicked, 1 write(s) started, 0 finished→MODEL_PICKER_DEFECT … 5 option(s) enumerated (OpenAI: 5)), first attempt.Fix
Carry the batch's own observation to whatever fails next, so a stalled write is named instead of the picker.
model-toggle-batch.ts—ToggleBatchResultnow returnswritesStarted/writesFinished(the give-up message already printed them; the caller could not read them, which is the whole of the reopen). NewMODEL_TOGGLE_WRITE_STALLED, plus two pure functions:writeStallReason()andmodelTriggerStallMessage().model-option.ts—ResolveContext.toggleWriteis the third source; a newwrite-stalledverdict is returned inside the existingchecked.includes(requested)branch, afterempty/match/unmatchable.openModelPickerAfterPanelClose()shares the post-close open across the three setups and re-throws amodel_modelfailure as the same named stall, keeping Playwright's original message inside it.setup-{openai,google,anthropic}.ts— capture the batch result they were discarding, pass it astoggleWrite, and open the picker through the shared helper. The three blocks were copy-paste, and had already drifted: fix(provider-setup): let the model-toggle batch flush before closing the panel (#1649) #1651 landed the same budgets in each with three differently-worded comments.Rejected alternatives, both declined deliberately: failing fast on a give-up (skips the picker read that proves the disagreement, and a write landing at 91 s would be mis-reported), and seeding enabled models through the API instead of the UI toggles (a redesign of the setup contract that overlaps #1666's open decisions). A new
scripts/lib/infra-signature-patterns.jsonentry is not included — none of its five patterns matches a locator timeout orPROVIDER_LIST_STALLEDeither; that is #1589's gap, and the new message is shaped to be matchable there.Scope note
No assertion weakened, no budget raised, nothing became a skip. The message never starts with
MODEL_NOT_AVAILABLE, so no caller can turn it into atest.skip, andwrite-stalleddeliberately does not return throughabsentBehavior: "return"— that hatch exists for a stale pin frommodels.json(#606), and degrading on an instance that could not accept the write would hide exactly the state this issue was reopened for. A picker miss after a settled batch is stillMODEL_PICKER_DEFECT, unchanged, and an unobserved batch leaves every existing verdict byte-identical (#1012).This does not make a saturated day green, and is not meant to. On such a day these specs still fail — they name the instance instead of accusing the picker. The run-level cause that puts a spec on this path at all (
collect-modelsconfirming 0 of 74 enable writes) is #1666, untouched here.No
.spec.tsis created or edited, so no tag changes and noQA-CHECKLIST.mdbullet moves.Validation (nightly
1.12.0.dev45,--workers=1 --retries=0)typecheck ✅ (0) · eslint ✅ (0 errors) · QA-CHECKLIST guard ✅ · coverage guard ✅
npm run test:units— 870 passed, 0 failed (11 new)agent-current-date-tool.spec.ts— 3/3 clean,expected=2 skipped=0 flaky=0, zero🚨 Backend Error. Run 1 was the real cold path (fresh container, no Google credential): 173 s against 34 s for the warm ones.model-provider-model-toggle.spec.ts— 3/3 clean,expected=2 skipped=0 flaky=0, zero🚨 Backend Error.Pre-fix baseline: 0/5 (
repro-run,google / gemini-3.5-flash). The defect cannot reproduce on a healthy instance by construction — it requires the write to stall — hence the induced-delay proof above.End-to-end red/green pair on the live instance: the identical 120 s hold printed
MODEL_PICKER_DEFECTbefore this change andMODEL_TOGGLE_WRITE_STALLEDafter.Force-fail — the diff carries no
.spec.ts, so the mechanical gate has nothing to mutate and passes vacuously (the shape fix(provider-setup): let the model-toggle batch flush before closing the panel (#1649) #1651 hit too). It was done behaviourally against the 11 new unit tests, three mutations, each reverted, each red on a distinct set:gave-upguard inwriteStallReason→ red: a picker miss after a SETTLED write is still MODEL_PICKER_DEFECT · a settled batch is never a stall · with no stall the model_model failure is left exactly as it wasA first attempt at M2 was discarded rather than banked: it left unreachable code and failed at file level (828 tests, 2 file failures) — a compile error, not the behavioural red the gate exists for. After revert: 0
FF-MUTATIONmarkers, 870/870, typecheck 0, lint 0.Symptom rows
Every row gets its own verdict; all are
test-defect(attribution).agent-current-date-tool.spec.ts:260is also listed on #1666, which owns the run-level cold-path cause; what changes here is only how its attempt-1model_modeltimeout is reported.Out of scope, found on the way
tests/helpers/provider-setup/setup-language-model-openai.tsstill closes the panel with no flush wait, and its comment asserts the very claim [Daily #1642] MODEL_PICKER_DEFECT — the picker offers exactly 5 options per provider while the panel reports 49 enabled (5 occurrences, all on 2026-08-31) #1649 disproved. Consequence is milder (it ranks rather than pins, so it can silently select a default model instead of failing) — left out to keep this diff to attribution only.model-provider-model-toggle.spec.tshad one hard failure and one flake on the 2026-09-01 daily with no issue opened; itsgetByText('Model Providers')signature (helpers/ui/go-to-settings.ts:15) reproduced once in six local runs, outside this diff and predating it.🤖 Generated with Claude Code