# Proof audit pass 1 — provider updater failure reason

Issue [#26280](https://git.cafe/pingdotgg/t3code/issues/26280). Audited branch `fix/issue-26280`, head [9740f8522e](https://github.com/pingdotgg/t3code/commit/9740f8522e76a387bcb1be5775103439339a0041), against merge-base [49a9e80e0b](https://github.com/pingdotgg/t3code/commit/49a9e80e0b). Read-only source audit; no browser, server, emulator, or capture was started.

## Findings

1. **The empty-matrix exemption does not apply.** Although only server source and its test change, the changed `ServerProvider.updateState.message` is visible in existing clients. Unchanged client source does not mean no visible surface. The red test and native CLI transcript establish the parsing problem, but neither shows the changed string in a client. Minimum additional evidence: one before/after pair of the provider settings version-advisory popover. Because opening that popover requires interaction, record each side; labelled still frames can accompany the recordings but cannot replace them under this brief.
2. **One renderer, viewport, theme, and failed-provider scenario suffice.** The change introduces no client layout, theme, breakpoint, platform, count, or interaction logic. Capture the same long native failure in the same web settings popover before and after. Desktop uses this web renderer. Mobile, toasts, sidebar tooltips, and update-all results consume the same message; list them in the matrix but do not multiply required evidence across them. The npm example is a second parser case in the new focused test, not a second required visual flow.
3. **Choose an advisory-visible provider.** In web settings, failure text is inside `versionAdvisoryNode`, not an always-visible row error. With current/unknown/missing advisory and no compatibility warning, this node is absent. An unopened popover, collapsed settings row, or RPC-start error toast would miss the changed string. Seed `behind_latest`, `isUpdating=false`, and `updateState.status=failed`; open “Update available, view details.”
4. **Capture evidence is currently absent, as expected for pass 1.** Existing evidence inspected: the lead's red-test log, native stderr transcript, and successful changed-check summary. I did not independently rerun tests or claim the CLI-only transcript was an integrated server/UI run. This audit concerns proof coverage, not a fresh correctness review of the selection heuristic.

## Changed data and expected text

`failureReason` normalizes each line with `toProgressLine` (strip ANSI, collapse whitespace, trim, limit reason to 200 characters), prefers the last `/error|fail/i` line in stderr, then stdout, then falls back to the final nonempty stderr/stdout line. `failureMessage` keeps timeout and exit-code prefixes. The emitted state remains `failed`; `output` remains the combined raw command output. All providers using this maintenance runner share the change, including native, npm, and other installers. No provider adapter, contract, or client component changes.

Use the real native transcript supplied by the lead as the required fixture input (no private data):

```text
Error: Failed to install native update
TelemetrySafeError: Failed to fetch version from https://downloads.claude.ai/claude-code-releases/latest after 3 attempt(s): connect ECONNREFUSED 127.0.0.1:9
Try running "claude doctor" for diagnostics
```

Before, exit 1:

```text
Update command exited with code 1: Try running "claude doctor" for diagnostics
```

After, same exit and output:

```text
Update command exited with code 1: TelemetrySafeError: Failed to fetch version from https://downloads.claude.ai/claude-code-releases/latest after 3 attempt(s): connect ECONNREFUSED 127.0.0.1:9
```

The selected native reason is 157 characters and the full after message is 190 characters. It wraps in the fixed 320px popover. Also retain the test's HTTP-403 and npm multiline output cases as parser proof. A reason longer than 200 characters is normalized/truncated by unchanged code; it is not another required visual row.

## Render-path inventory and equivalence decisions

- **Web/desktop settings:** `SettingsProvidersRoute` → `ProviderSettingsPanel` → access gate → `EnvironmentProviderSettings` → `ProviderInstanceRow` → `ProviderInstanceCard`. This is the only production caller of the card. `renderProviderInstance` calls it in `list` and `editor` modes. The page supports environment and project/checkout scopes and instance targeting; remote edit/read-only access gates share the card. `versionAdvisoryNode` is shared between both modes. The failure paragraph is `aria-live=polite`, warning colored, and `overflow-wrap:anywhere`. The list summary shows running progress but does not display terminal failure text. Both modes render the same medium popover (`w-80`, viewport constrained); their trigger sizing and surrounding rows differ, but that code is unchanged.
- **Card branches:** advisory absent/current/unknown vs behind latest; compatibility graceful/unsupported/broken overrides; enabled/project effective enabled; live provider absent; update running/queued vs failed/unchanged vs succeeded/idle; message null/empty vs present; permission/read-only controls; recommended-version vs latest update action; manual update command present/absent. Counts, names, auth status, models, selected row, custom setup/runtime content, dirty/default instance, deletion and model editing affect surrounding settings but not the changed paragraph. No full unrelated card-state Cartesian product is warranted. Width stacks/splits the settings list/editor at its existing container breakpoint; it does not select a different message renderer. Dark/light affects existing colors only.
- **Provider-only update toast:** `ProviderUpdatesAction` → `useProviderUpdates.updateAll` → `getProviderUpdateRunToastView` → `getProviderUpdateRunOutcome`. A completed RPC result with a failed state supplies the changed text, prefixed by machine/provider labels. Zero settled runs or all interruptions means no toast; all succeeded gives success; one/all/mixed failures changes the title; multiple failures join lines. The result description is a React span with `whitespace-pre-line`, so the toast's raw-string 180-character clamp does not activate on this path. Settings' “Could not update …” toast handles RPC failure to start, not a completed updater's failed state; it is unaffected.
- **Sidebar provider notice:** `SidebarChrome` → `SidebarProviderUpdatePill` → `getProviderUpdateSidebarPillView`. Active updates take precedence; only recent terminal updates after the visible cutoff are considered, deduplicated by driver; dismissed keys are suppressed; newest terminal candidate wins. A single failed provider's description is the message, visible in the hover/focus tooltip and accessibility label; multiple failed drivers use generic description text. Title/icon do not expose the changed reason. Tooltip interaction, navigation to settings, 180ms exit, and timed success dismissal are unchanged.
- **Update everything:** `SidebarUpdatePill` → `useUpdateEverything` uses the same provider outcomes with quiet provider-only toast mode. Provider failure lines feed `collectUpdateEverythingResult` → `showUpdateEverythingResult`, and machine failure state can feed `SidebarUpdateCapsule`'s popover. Zero/success/mixed/multiple failures, interrupted providers, server/local failures, restart handoff and saved post-restart result affect titles/lists or timing, not reason parsing. The capsule shows a machine failure only while its run remains visible; other machines/server handoff can determine that lifetime. Do not use this complex time-dependent path for minimum evidence.
- **Mobile:** Settings environments list and server-controls environment rows navigate to `SettingsEnvironmentDetail(environmentId)` → `SettingsEnvironmentDetailRouteScreen` → `EnvironmentDetail`. Connected config renders enabled providers only. Non-idle state renders selectable message, falling back to `Update <status>` if null; failed is danger-colored, other states muted. The same provider update RPC is used; permission/busy gates disable actions. Missing environment/config/disconnected, disabled providers, idle/absent states and unrelated server-update text do not show the changed reason. iOS/Android share this component; no native-platform branch changed.
- **Interactions:** relevant ones are opening/closing the settings popover, starting/retrying provider update, observing running→failed, triggering Update all, hovering/focusing/dismissing sidebar notice, update-everything/restart flows, and mobile environment navigation/update. They are marked `recording` below wherever a row would be captured. Theme switches, model/auth editing, copy-command feedback, and unrelated settings actions are outside this diff.

## Fixture matrix

“Excluded” means intentionally outside the minimum capture set, not captured or tested by this audit. Required set is **1280×900 CSS pixels × dark theme × C1/C2**. Use the same client build/route, data, scroll, zoom, and trigger placement on both sides. One pair is sufficient; do not require every affected consumer to be captured.

| ID / named fixture | Data and condition | Diff affects it? | Evidence / viewport × theme | Seed / route / driver | Status |
|---|---|---|---|---|---|
| C1 — native failure, BEFORE, advisory popover open | One enabled installed Claude provider; authenticated/ready; older supported version; `behind_latest`; manual command `claude update`; `failed`; not updating; native transcript above, exit 1; baseline message ends with doctor hint | Yes, baseline for changed selection | **Required recording**, 1280×900 dark; record opening popover, hold readable text; labelled Before frame optional | Web `/settings/providers`, selected environment/instance, click “Update available, view details.” Generate state using baseline runner on identical command output. Deterministic fallback is labelled component harness, see seed instructions below | **MISSING** |
| C2 — native failure, AFTER, advisory popover open | Identical C1 data; only runner revision differs; message contains TelemetrySafeError and ECONNREFUSED; no doctor hint | Yes | **Required recording**, same 1280×900 dark; labelled After frame optional | Same route/trigger or same labelled harness, generate state at audited head | **MISSING** |
| S1 — npm failure, short reason | Same advisory-visible card; stderr from new test: first `Failed to check for updates`, diagnostic/cause list, final `npm whoami` hint; exit 1 | Yes, shorter corrected text | Recording if captured; same viewport/theme; no extra required capture | Same card/RPC or harness; head message ends `Failed to check for updates`; before ends final hint | Excluded: same renderer; covered by focused parser test |
| S2 — card detail hidden / gated | No advisory/current/unknown without compatibility warning; unopened popover; no live provider; loading/disconnected/no environment; missing target; disabled/read-only and project scopes as described above | Stored message can differ, but changed paragraph hidden on no-advisory/closed paths; read-only can still show it when opened | Recording if opening/gating flow is captured; no required capture | Same route, vary advisory/access/instance/effective enabled; harness for deterministic state injection | Excluded: no new visible branch; opening visible branch represented by C1/C2 |
| S3 — other maintenance states and unchanged fallbacks | Queued/running; idle/absent; success/unchanged; null/empty message; timeout; empty output; output with no error/fail; selected last line already error | No change to progress/status; timeout/empty/no-match/already-last outcomes unchanged | Recording for transitions/time; no required capture | Maintenance runner fixtures; matching `updateState` in card/mobile/notification harness | Excluded: unchanged behavior |
| S4 — alternate card shell/action/width/theme | List/editor; read-only/editable; latest/recommended/manual command; compatibility advisory; narrow stack/wide split; light/dark; short/long names and many model rows | Same reason paragraph if advisory visible; no shell/style branch changes | Recording if popover/actions driven; no required capture | Existing settings route/scope and card props; same popover reused in list and editor | Excluded: equivalent message renderer; C1/C2 fixes one shell, width and theme |
| T1 — provider-only toast, one failure | One connected environment and updatable instance; RPC succeeds with failed terminal state/message; machine label + provider prefix | Yes | Recording (Update all → settlement/toast lifetime); no required capture | Settings “Update all” via `ProviderUpdatesAction`; real `server.updateProvider` or labelled RPC-result harness | Excluded: same string; no toast code changed |
| T2 — provider-only toast, multiple/mixed/empty | Several environments/providers; all failed or mixed success/failure → joined failure lines; all succeeded/interrupted/empty branches | Failed lines yes; generic titles/success/empty no | Recording (pending/result/timing); no required capture | Same Update all flow; arrays of `ProviderUpdateRun` results in labelled harness | Excluded: count branches unchanged |
| P1 — single failed sidebar notice, tooltip open | Primary provider; no active update; recent `finishedAt` after visible cutoff; one failed driver; not dismissed; tooltip description=message | Yes, tooltip/accessibility description only | Recording (trigger update, hover/focus); no required capture | Primary environment failure through update RPC; hover/focus sidebar failed notice; harness provider atom if needed | Excluded: same string; static pill title alone is insufficient evidence |
| P2 — sidebar generic / suppressed / other state | Multiple failed drivers; active update priority; older/dismissed terminal state; success timed dismissal; unchanged warning | No changed visible failure description when generic/suppressed; keys may contain message | Recording (dismissal/priority/time); no required capture | Primary provider snapshots + timestamps, dismissed keys; labelled harness for deterministic time | Excluded: unchanged visible branches |
| E1 — update-everything result / capsule | Provider failure plus optional other machine/server/local updates; failure line in final toast, saved result, or live capsule popover; run must remain mounted for capsule | Yes when provider failure line displayed | Recording (run/update/restart/popover/time); no required capture | Sidebar update-everything action; provider-update result feeds normal collector and run state; labelled result/run harness if no eligible updates | Excluded: same reason with extra labels; timing/restart code unchanged |
| M1 — mobile enabled failed provider | Connected environment/config; enabled installed provider; non-idle failed state with native long message; compatible advisory optional; permissions affect action only | Yes, selectable danger text | Recording (navigate/update); no required capture; if added choose one iOS or Android device/theme | Settings → Environments → environment; or Server Controls → environment detail. Same server.updateProvider RPC; labelled component harness for injected config | Excluded: same data; native layout unchanged |
| M2 — mobile absent/idle/disabled/muted | No config/environment; disabled provider filtered; idle/absent no message; queued/running/success/unchanged muted text; null fallback; narrow/large type/theme/platform | No changed failed-reason branch in these cases | Recording if navigation/time captured; no required capture | Same environment detail route; vary config/provider states in labelled harness | Excluded: unchanged branches |

## Seeding and capture instructions

A real isolated app can enter C1/C2 by keeping an installed native Claude provider behind latest, invoking the protected RPC `server.updateProvider` with `{provider: "claudeAgent", instanceId: <that instance>}` (or “Update now”), and making its native update produce the supplied failed command output. The RPC successfully returns `{providers: [...]}` carrying terminal `failed` state; this is distinct from an RPC rejection. Permission requires `providers:manage`. The state also reaches clients through provider snapshots. For baseline, use the baseline server implementation; for head use the audited implementation. Use only isolated dev home/state and isolate any CLI environment modifications; do not alter the user's real install or credentials. Do not use a bare CLI transcript as if it were an app capture.

There is no public RPC in the inspected contracts to set an arbitrary provider terminal state. This audit did not establish a reproducible real-provider environment or launch an app. Therefore, if the capturer cannot reproduce the native updater failure safely and identically in the real isolated app, **C1/C2 must be labelled “component harness seeded from real native updater output.”** Render the unchanged production `ProviderInstanceCard` with real CSS/providers/router context and the above advisory-visible provider; generate `updateState` through the actual baseline/head `ProviderMaintenanceRunner` with the mock registry and spawner pattern in its existing test, using the same native transcript. Supply the resulting terminal state to the card (`isUpdating=false`), rather than hand-writing an asserted after message. This preserves proof of the server selection and production renderer while honestly limiting the capture to a harness. Existing `ProviderSettingsPanel.environment.browser.test.tsx` demonstrates provider-atom/settings fixture injection if a full settings panel harness is preferred. Do not edit tracked source for an evidence-only fixture.

Both recordings should show the closed context, open the advisory popover, and hold the complete terminal text long enough to read. A screenshot that shows only the provider row or generic “Provider update failed” title does not satisfy either row. Reject mismatched outputs (403 on one side, ECONNREFUSED on the other), fabricated after text, or captures whose revision labels are missing. The native reason is below the 200-character limit, so the after capture should include ECONNREFUSED, not an ellipsis before it. No animation-quality or timing claim is being made; recordings are required solely because the brief marks interaction-dependent rows that way.

## Source anchors

- [Runner selection and failed state emission](https://github.com/pingdotgg/t3code/blob/9740f8522e76a387bcb1be5775103439339a0041/apps/server/src/provider/providerMaintenanceRunner.ts#L244), [focused regression cases](https://github.com/pingdotgg/t3code/blob/9740f8522e76a387bcb1be5775103439339a0041/apps/server/src/provider/providerMaintenanceRunner.test.ts#L474).
- [Settings route](https://github.com/pingdotgg/t3code/blob/9740f8522e76a387bcb1be5775103439339a0041/apps/web/src/routes/_chat.settings.providers.tsx), [card caller](https://github.com/pingdotgg/t3code/blob/9740f8522e76a387bcb1be5775103439339a0041/apps/web/src/components/settings/ProviderSettingsPanel.tsx#L1286), [failure paragraph](https://github.com/pingdotgg/t3code/blob/9740f8522e76a387bcb1be5775103439339a0041/apps/web/src/components/settings/ProviderInstanceCard.tsx#L916), [advisory visibility](https://github.com/pingdotgg/t3code/blob/9740f8522e76a387bcb1be5775103439339a0041/apps/web/src/components/settings/providerStatus.ts#L122).
- [Update actions/toast](https://github.com/pingdotgg/t3code/blob/9740f8522e76a387bcb1be5775103439339a0041/apps/web/src/components/ProviderUpdatesAction.tsx), [notification result and sidebar logic](https://github.com/pingdotgg/t3code/blob/9740f8522e76a387bcb1be5775103439339a0041/apps/web/src/components/ProviderUpdateLaunchNotification.logic.ts#L200), [sidebar tooltip](https://github.com/pingdotgg/t3code/blob/9740f8522e76a387bcb1be5775103439339a0041/apps/web/src/components/sidebar/SidebarProviderUpdatePill.tsx#L215).
- [Update-everything consumer](https://github.com/pingdotgg/t3code/blob/9740f8522e76a387bcb1be5775103439339a0041/apps/web/src/components/sidebar/useUpdateEverything.ts#L151), [capsule failure](https://github.com/pingdotgg/t3code/blob/9740f8522e76a387bcb1be5775103439339a0041/apps/web/src/components/sidebar/SidebarUpdateCapsule.tsx#L160), [mobile message](https://github.com/pingdotgg/t3code/blob/9740f8522e76a387bcb1be5775103439339a0041/apps/mobile/src/features/settings/SettingsEnvironmentDetailRouteScreen.tsx#L318).

FRICTION: none

VERDICT: INSUFFICIENT C1/C2 before/after settings-popover recordings missing
