Skip to content

fix(provider-setup): name the stalled enable write instead of blaming the model picker (#1649) - #1672

Merged
rafaelgiln merged 1 commit into
mainfrom
fix/issue-1649-attribute-stalled-enable-write
Sep 1, 2026
Merged

fix(provider-setup): name the stalled enable write instead of blaming the model picker (#1649)#1672
rafaelgiln merged 1 commit into
mainfrom
fix/issue-1649-attribute-stalled-enable-write

Conversation

@rafaelgiln

@rafaelgiln rafaelgiln commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

Closes #1649.

Problem

MODEL_PICKER_DEFECT fired 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:

⚠️  provider panel: the model-toggle batch did not settle in time —
    30 toggle(s) clicked, 1 write(s) started, 0 finished.

POST /api/v1/models/enabled_models was issued and had not answered 90 s later. The listener counts requestfailed too, 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 calls refreshAllModelInputs, and the picker keeps the pre-toggle enabled set, which on a freshly configured provider is MIN_DEFAULT_MODELS = 5.

The picker is correct there; the panel is what lies. aria-checked is useModelToggleQueue'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 by collect-models' own server-side read (0 of 30/36/8 enable writes confirmed, on all four shards) and by 5 being MIN_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_signature and 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_model wait exhausts its 60 s and dies as a bare locator.waitFor: Timeout 60000ms exceeded (agent-current-date-tool.spec.ts:260 attempt 1, agent-context-id-continuity.spec.ts:407 attempt 0).

Verdict: test-defect — in attribution. Reproduced causally rather than by re-running, since re-running cannot falsify a timing defect: on a healthy 1.12.0.dev45 container a scout held that POST for 120 s with page.route, everything else unchanged, and produced the daily's evidence byte for byte (36 toggle(s) clicked, 1 write(s) started, 0 finishedMODEL_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.tsToggleBatchResult now returns writesStarted/writesFinished (the give-up message already printed them; the caller could not read them, which is the whole of the reopen). New MODEL_TOGGLE_WRITE_STALLED, plus two pure functions: writeStallReason() and modelTriggerStallMessage().
  • model-option.tsResolveContext.toggleWrite is the third source; a new write-stalled verdict is returned inside the existing checked.includes(requested) branch, after empty/match/unmatchable. openModelPickerAfterPanelClose() shares the post-close open across the three setups and re-throws a model_model failure 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 as toggleWrite, 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.json entry is not included — none of its five patterns matches a locator timeout or PROVIDER_LIST_STALLED either; 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 a test.skip, and write-stalled deliberately does not return through absentBehavior: "return" — that hatch exists for a stale pin from models.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 still MODEL_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-models confirming 0 of 74 enable writes) is #1666, untouched here.

No .spec.ts is created or edited, so no tag changes and no QA-CHECKLIST.md bullet moves.

Validation (nightly 1.12.0.dev45, --workers=1 --retries=0)

  • typecheck ✅ (0) · eslint ✅ (0 errors) · QA-CHECKLIST guard ✅ · coverage guard ✅

  • npm run test:units870 passed, 0 failed (11 new)

  • agent-current-date-tool.spec.ts3/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.ts3/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_DEFECT before this change and MODEL_TOGGLE_WRITE_STALLED after.

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

    • M1 removed the gave-up guard in writeStallReason → 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 was
    • M2 made the carrier never recognise a stall → red: a picker miss after a STALLED write blames the write · a gave-up batch yields a reason naming the write · the model_model message blames the instance
    • M3 consulted the stall before the picker's own verdicts → red: a stall never rewrites a verdict about a picker that DOES offer the model · a listed model with its toggle OFF stays a setup failure · a picker miss after a STALLED write blames the write

    A 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-MUTATION markers, 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:260 is also listed on #1666, which owns the run-level cold-path cause; what changes here is only how its attempt-1 model_model timeout is reported.

Out of scope, found on the way

🤖 Generated with Claude Code

… 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>
@rafaelgiln

Copy link
Copy Markdown
Collaborator Author

CI: green, and the two flakies read faithfully

All 7 checks passed. The E2E lane really ran — Running 45 tests using 2 workers38 passed, 2 flaky, 5 skipped (11.0m) — so this is not a green that executed nothing. Both flakies recovered on retry; neither is caused by this change, and one of them will look like it is, which is why it is written down here.

1. agent-context-id-continuity.spec.ts:407 — failed at waitForProviderRow (provider-list-state.ts:237, from setup-openai.ts:40). That is #1648's PROVIDER_LIST_STALLED path: the provider list never arrived. It happens before anything this PR touches (line 40 vs line 103).

2. language-model-regression.spec.ts:246 — failed at openModelPickerAfterPanelClose (model-option.ts:518):

TimeoutError: locator.waitFor: Timeout 60000ms exceeded.
Call log:
  - waiting for getByTestId('model_model') to be visible

The stack frame is new, the behaviour is not. Before this PR the identical wait lived inline at setup-openai.ts:104 with the identical 60 s budget and the identical locator; the refactor moved it into a shared helper and changed nothing about it. What the log shows is in fact the branch this PR adds working as designed: the catch consults writeStallReason, which returned null because the batch did not give up, so Playwright's own error is re-thrown untouched, call log included — "No stall to blame: a trigger that never returns on a healthy flush is a real defect and must keep surfacing as Playwright's own error". Had the batch stalled, the same run would have printed MODEL_TOGGLE_WRITE_STALLED naming the write instead.

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 model_model timeout that has existed since #1651 raised that budget from 15 s, unchanged by this PR and deliberately not widened by it.

Local validation on 1.12.0.dev45 was --retries=0: 3/3 clean on agent-current-date-tool.spec.ts and 3/3 clean on model-provider-model-toggle.spec.ts, plus 870/870 units.

🤖 Generated with Claude Code

@rafaelgiln
rafaelgiln merged commit da16fd1 into main Sep 1, 2026
7 checks passed
@rafaelgiln
rafaelgiln deleted the fix/issue-1649-attribute-stalled-enable-write branch September 1, 2026 22:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant