# Proof audit, pass 1 — Cursor catalog replacement

PR [#28018](https://git.cafe/pingdotgg/t3code/pull/28018) ([GitHub mirror](https://github.com/pingdotgg/t3code/pull/28018)).

Reviewed head [ba75e85](https://git.cafe/pingdotgg/t3code/commit/ba75e8569ce10dde44a3459b0fc0b20ce69f36b1), branch `fix/issue-28008`, against the requested base [50c1c5d](https://git.cafe/pingdotgg/t3code/commit/50c1c5dc536493826fc66e220598f38b831d5bde). Worktree: `/home/ubuntu/wt/pr-28018`. The requested base is an ancestor of the head. The local `main` ref resolves to a different merge-base, so pin the supplied base for comparison; do not silently substitute the moving local branch.

## Findings

1. **Capture evidence is missing.** The PR body has a useful runtime transcript, but no captures. Its transcript identifies older base/head revisions ([b823dcf](https://git.cafe/pingdotgg/t3code/commit/b823dcf5c6) / [8b2004b](https://git.cafe/pingdotgg/t3code/commit/8b2004be5f)); it is supporting context, not visual proof of the reviewed head. Pass 1 therefore cannot satisfy the evidence gate.
2. **The changed branch is model membership, with visible consequences.** For Cursor, `installed && status === "ready"` now stops appending missing previous built-in models. The picker and Settings consume this list. A statement that there is no changed rendering code is accurate; a statement that there is no visible surface is not. Both visible surfaces must be shown.
3. **Cache and readiness are essential to the fixture.** The file is `<T3 home>/caches/cursor.json`, normally `/home/ubuntu/wt/pr-28018/.t3/caches/cursor.json`, not a file under `userdata`. It is read during registry construction. Editing it while the server runs does not seed the current registry. The managed provider automatically probes on startup, so the fixed head may have removed stale models before a browser attaches. Matched base/head runs with the same stopped-server seed are the reliable real-app comparison.
4. **Non-ready states must retain inventory without being presented as healthy.** The new unit test covers ready shrink and error retention. The two adjusted existing fixtures cover warning/empty retention, including persisted cache. Those tests were read, not executed in this audit. A real invalid-key run provides a small independent guard capture; it should retain Settings inventory while the web picker offers no selectable Cursor catalog because Cursor is not ready.
5. **Do not use hidden models or saved selections as a proxy for catalog removal.** Settings lists hidden models; the picker filters them. Favorites intersect actual options and do not restore missing catalog rows. Custom models are rebuilt from current settings. Saved thread/default selections are a separate input: web and mobile do not have identical fallback presentation. The core fixture selects a live model and explicitly labels the saved-selection extension below.

No implementation defect was established in this proof-matrix audit. This is not a full merge-readiness review.

## Source and path inventory

All links below are pinned to the reviewed head.

- [ProviderRegistry.ts](https://git.cafe/pingdotgg/t3code/blob/ba75e8569ce10dde44a3459b0fc0b20ce69f36b1/apps/server/src/provider/ProviderRegistry.ts): `shouldRetainMissingProviderModels` is called by `mergeProviderModels` (and by OpenCode-only metadata retention, which cannot be affected by the new Cursor clause). `mergeProviderSnapshot` calls the model merge. Production merges occur in `upsertProviders`; its callers cover initial fallback/cache reconciliation, newly added source snapshots, source `streamChanges`, manual refresh by driver/instance/all, secret reload, post-update refresh, and compatibility reclassification. `setProviderMaintenanceActionState` also upserts the current snapshot. No component or prop definition changed.
- [Cursor status](https://git.cafe/pingdotgg/t3code/blob/ba75e8569ce10dde44a3459b0fc0b20ce69f36b1/packages/provider-cursor/src/server/status.ts), [driver](https://git.cafe/pingdotgg/t3code/blob/ba75e8569ce10dde44a3459b0fc0b20ce69f36b1/packages/provider-cursor/src/server/driver.ts), and [managed provider](https://git.cafe/pingdotgg/t3code/blob/ba75e8569ce10dde44a3459b0fc0b20ce69f36b1/packages/provider-core/src/server/managedProvider.ts): initial checking = warning/unknown; usable nonempty SDK inventory = ready/authenticated; empty discovered built-ins = warning/authenticated; absent/rejected key = error/unauthenticated; SDK failure/timeout = error/unknown; disabled = disabled after provider construction. Current custom rows can accompany these states. Successful catalog reads are cached by API key for 30 minutes in [CursorSdkCatalog](https://git.cafe/pingdotgg/t3code/blob/ba75e8569ce10dde44a3459b0fc0b20ce69f36b1/packages/provider-cursor/src/server/CursorSdkCatalog.ts).
- [Cache hydration/persistence](https://git.cafe/pingdotgg/t3code/blob/ba75e8569ce10dde44a3459b0fc0b20ce69f36b1/apps/server/src/provider/providerStatusCache.ts) and [path derivation](https://git.cafe/pingdotgg/t3code/blob/ba75e8569ce10dde44a3459b0fc0b20ce69f36b1/apps/server/src/config.ts): correlated instance/driver and enabled-state matching are required for hydration. Previous custom rows and update-required exclusions are not ordinary retained inventory. Matching slugs can retain capability descriptors independently of membership. Registry persistence precedes its change publication.
- [WebSocket server](https://git.cafe/pingdotgg/t3code/blob/ba75e8569ce10dde44a3459b0fc0b20ce69f36b1/apps/server/src/ws.ts): `server.refreshProviders` calls the registry; `providerStatuses` broadcasts updated lists. [Client command](https://git.cafe/pingdotgg/t3code/blob/ba75e8569ce10dde44a3459b0fc0b20ce69f36b1/packages/client-runtime/src/state/server.ts) scopes refresh to the destination environment. Local, remote, relay, and tunnel clients share this payload. No wire schema changes.
- [Settings route](https://git.cafe/pingdotgg/t3code/blob/ba75e8569ce10dde44a3459b0fc0b20ce69f36b1/apps/web/src/routes/_chat.settings.providers.tsx) → [ProviderSettingsPanel](https://git.cafe/pingdotgg/t3code/blob/ba75e8569ce10dde44a3459b0fc0b20ce69f36b1/apps/web/src/components/settings/ProviderSettingsPanel.tsx) → [ProviderInstanceCard](https://git.cafe/pingdotgg/t3code/blob/ba75e8569ce10dde44a3459b0fc0b20ce69f36b1/apps/web/src/components/settings/ProviderInstanceCard.tsx) → [ProviderModelsSection](https://git.cafe/pingdotgg/t3code/blob/ba75e8569ce10dde44a3459b0fc0b20ce69f36b1/apps/web/src/components/settings/ProviderModelsSection.tsx). This is the sole production caller of `ProviderModelsSection`. `deriveProviderModelsForDisplay` combines live built-ins and current custom settings.
- [ChatComposer](https://git.cafe/pingdotgg/t3code/blob/ba75e8569ce10dde44a3459b0fc0b20ce69f36b1/apps/web/src/components/chat/ChatComposer.tsx) → [ComposerControlsBar](https://git.cafe/pingdotgg/t3code/blob/ba75e8569ce10dde44a3459b0fc0b20ce69f36b1/apps/web/src/components/chat/ComposerControlsBar.tsx) → `ComposerModelPicker` in [ComposerFooterControls](https://git.cafe/pingdotgg/t3code/blob/ba75e8569ce10dde44a3459b0fc0b20ce69f36b1/apps/web/src/components/chat/ComposerFooterControls.tsx) → [ProviderModelPicker](https://git.cafe/pingdotgg/t3code/blob/ba75e8569ce10dde44a3459b0fc0b20ce69f36b1/apps/web/src/components/chat/ProviderModelPicker.tsx) → [ModelPickerContent](https://git.cafe/pingdotgg/t3code/blob/ba75e8569ce10dde44a3459b0fc0b20ce69f36b1/apps/web/src/components/chat/ModelPickerContent.tsx). `ProviderModelPicker` is the sole caller of `ModelPickerContent`. Composer click and the `modelPicker.toggle` keybinding reach the same popup; no separate command-palette model list was found.
- All other production `ProviderModelPicker` callers: default and fallback model in [ProjectDefaultsSettings](https://git.cafe/pingdotgg/t3code/blob/ba75e8569ce10dde44a3459b0fc0b20ce69f36b1/apps/web/src/components/settings/ProjectDefaultsSettings.tsx), text generation model in [SettingsPanels](https://git.cafe/pingdotgg/t3code/blob/ba75e8569ce10dde44a3459b0fc0b20ce69f36b1/apps/web/src/components/settings/SettingsPanels.tsx), scheduled-task editor in [ScheduledTasksSettings](https://git.cafe/pingdotgg/t3code/blob/ba75e8569ce10dde44a3459b0fc0b20ce69f36b1/apps/web/src/components/settings/ScheduledTasksSettings.tsx), and review-guide / source-control-writer models in [SourceControlWritingSettings](https://git.cafe/pingdotgg/t3code/blob/ba75e8569ce10dde44a3459b0fc0b20ce69f36b1/apps/web/src/components/settings/SourceControlWritingSettings.tsx). They use the same options/popup with different trigger labels, disabled/connected/saving guards, setup callbacks, and selection handlers. A separate capture of each is redundant for this server membership change.
- [Web model selection](https://git.cafe/pingdotgg/t3code/blob/ba75e8569ce10dde44a3459b0fc0b20ce69f36b1/apps/web/src/modelSelection.ts) rebuilds custom rows, applies hidden/order preferences, and appends unavailable dynamic selections only for OpenCode/Antigravity, not Cursor. [Provider readiness](https://git.cafe/pingdotgg/t3code/blob/ba75e8569ce10dde44a3459b0fc0b20ce69f36b1/apps/web/src/providerInstances.ts) requires enabled + available + ready. Provider-native subagent bars and subagent tooltips also derive labels/capabilities from this inventory; they do not offer another catalog editor.
- Desktop wraps the same web components. Mobile has a separate [buildModelOptions](https://git.cafe/pingdotgg/t3code/blob/ba75e8569ce10dde44a3459b0fc0b20ce69f36b1/apps/mobile/src/lib/modelOptions.ts), called by ThreadComposer, new-task-flow-provider, ThreadSettingsSheet, ScheduledTaskPickerScreens, scheduledTaskDraft, and SettingsScheduledTasksRouteScreen. [ThreadSettingsSheet](https://git.cafe/pingdotgg/t3code/blob/ba75e8569ce10dde44a3459b0fc0b20ce69f36b1/apps/mobile/src/features/threads/ThreadSettingsSheet.tsx) is the native picker/list with pull-to-refresh. Native Providers settings manages authentication rather than the web's model catalog table. No native or Electron verification is claimed here.
- Agent-triggered refresh also reaches registry services (for example provider-unavailable rechecking in [OrchestratorMcpService](https://git.cafe/pingdotgg/t3code/blob/ba75e8569ce10dde44a3459b0fc0b20ce69f36b1/apps/server/src/mcp/OrchestratorMcpService.ts)); scheduled-task model choices use the same server inventory. Other provider drivers are unchanged, including the pre-existing Grok branch.

## Fixture collapse and capture matrix

**Shared fixture F:** enabled default Cursor instance `cursor`; real live SDK catalog `L` with `N > 8` built-in models; select a model in `L` in a new draft. While the server is stopped, append built-in cache rows `old-account-only-a` / “Old Account Only A”, `old-account-only-b` / “Old Account Only B”, and `retired-model-x` / “Retired Model X”, each `isCustom:false, capabilities:null`. They must not occur in `L`. Let `S` be these three rows. Optional but recommended consolidation: favorite B, hide X, and add one current custom row `proof-custom-model` / “Proof Custom”; call that set `C`, with `K` current custom rows. Prepare these preferences identically for both runs. A remains visible, ordinary, and unfavorited, so hiding cannot explain its disappearance. No model turn needs to be sent.

**Minimum viewport × theme:** web 1440 × 1000 CSS pixels, dark, 100% zoom for all required rows. Use the same dimensions/theme for base and head. Each row involving navigation, filtering, refresh, or startup is **recording**; still frames are supplementary. Multiple rows may share a recording. One matched pair of walkthroughs covers R1–R3, and one head-only guard recording covers R4: **three recordings total**, plus machine-readable cache/RPC excerpts. There is no CSS, breakpoint, or theme change, so light mode, narrow web, macOS shell, and native permutations are outside the minimum.

| Row | Named fixture / data | Expected base → reviewed head | Diff effect | Capture and real-app seed | Pass-1 status |
|---|---|---|---|---|---|
| R1 | Ready, stale-rich Settings; F; N+3+K cached rows | After completed ready probe, Settings lists N+3+K → N+K. All three stale rows disappear, current custom survives. Counts and optional favorite/hidden groups update. | Direct changed branch | **Required recording**, 1440×1000 dark, base + head. `/settings/providers?environmentId=<dev environment>&instanceId=cursor`; show ready status, total, filter each unique stale name, clear filter, show a live model. Seed F in stopped cache in each run. | **MISSING** |
| R2 | Same ready inventory in composer provider tab, Favorites, and search | Ordinary A and favorite B appear on base and disappear on head; hidden X is already absent from picker on both, but its removal is established by R1. Searching A/B gives results → “No models found”; current live/custom choices remain. | Direct changed membership; unchanged filtering | **Required recording**, same viewport/theme, base + head; may share R1 video. Open a draft in the same environment; open composer picker, explicitly select Cursor tab rather than relying on initial Favorites tab, search unique names, inspect Favorites if seeded. Never infer removal from one scrolled/virtualized viewport. | **MISSING** |
| R3 | Ready cleanup survives explicit refresh, browser reload, and server restart | Base retains S after ready refresh/restart; head stays at L+C without S after ready refresh/restart. Persisted cache matches. | Changed membership plus persistence/publication | **Required recording**, same viewport/theme, base + head, combined with R1–R2. Click “Refresh provider status”, show pending/completed action, revisit both surfaces; reload, then restart using tracked PIDs and revisit. Read cache after completed ready snapshot. RPC alternative: `server.refreshProviders` with `{instanceId:"cursor",refreshModels:true,reloadSecrets:true}`; record UI and sanitized response. | **MISSING** |
| R4 | Seeded catalog, rejected API key; installed true, error/unauthenticated; F | Both retain L+S+C in Settings/cache while reporting error. Web picker does not expose selectable Cursor inventory in error. No false success/deletion. | Retention guard, behavior unchanged | **Required head-only recording**, same viewport/theme. Start a separate isolated run from F with a deliberately invalid process-level Cursor key (not the real injected key); wait for the real authentication error, open Settings, filter A, refresh and show retention. Do not show a key entry or upload raw credentials. | **MISSING** |
| W1 | Initial checking; installed true, warning/unknown; cached inventory exists | Both retain cached built-ins while initial discovery is incomplete; web picker readiness gates them. | Unchanged retention branch | **Recording if captured**. Startup races may be too fast to capture; deterministic paused SDK/registry layer is a **labelled harness capture**, not proof of a real provider delay. Covered structurally by warning-retention fixtures. | Capture waived: same branch as R4, startup not a timing fix |
| W2 | Completed empty SDK built-in inventory; warning/authenticated; cached inventory exists; optional current custom rows | Both retain last known built-ins, show empty-discovery warning, and do not delete cache inventory. Empty discovery is not ready. | Unchanged retention branch | **Recording if captured**; real account cannot be forced to return zero models with the provided key. Use **labelled harness** returning `{user,models:[]}` through CursorSdkCatalog, real status builder, registry, then UI. Status and registry tests are the appropriate primary proof. | Capture waived: status warning + empty retention tests already exist; not rerun here |
| E1 | Transport failure or timeout; error/unknown; no next built-ins | Both retain cached built-ins. Timeout/failure diagnostic differs from R4's auth error; timeout is 15 seconds. | Unchanged retention branch | **Recording if captured**. Do not disrupt host networking to force this. Use **labelled harness** failing or withholding the SDK catalog response. The new registry error assertion uses a partial next catalog; source status code was also checked for fallback-only failure. | Capture waived: same merge decision as R4; no timeout behavior change |
| E2 | No cache/no previous models, no custom rows; key absent or rejected | Both show no Settings models and no ready Cursor picker catalog. There is no previous snapshot to prune. | Unchanged early return | **Recording if captured**. Separate empty home, enabled Cursor, invalid override key. Settings empty label is “No models reported for this provider yet.” | Capture waived: no previous inventory, cannot demonstrate this fix |
| D1 | Disabled Cursor; cached enabled snapshot or stale cache identity mismatch | Both use fallback/ignore mismatched cache according to existing hydration guards; picker omits disabled instances. | Unchanged hydration/visibility | **Recording if captured**. Disable in isolated Settings; wrong instance/driver cache requires a **labelled invalid-cache fixture**, not an account-switch claim. | Capture waived: outside changed branch |
| V1 | Small successful catalogs: 1 row, 2–8 rows, crossing 9→8; optionally zero visible after filtering | Ready success removes missing rows in every count. Settings singular/plural changes at 1, filter exists only above 8; picker height is capped and depends on all tabs; virtualized list/scroll appear for larger catalogs. | Data counts can cross existing visual thresholds; renderer unchanged | **Recording if captured**. Exact live counts cannot be guaranteed for supplied account; **labelled harness** with controlled SDK catalogs if exact boundary evidence is desired. R1–R2 exercise real long-list and search-empty states. A ready zero-built-in Cursor catalog is not reachable through real status code. | Capture waived: no count-specific implementation change |
| S1 | Saved retired selection, single or multiple; web and native | Catalog drops stale entry on head. Web single trigger can fall back to its first option, multi labels can preserve saved slugs. Native `buildModelOptions` can append a missing saved selection; absence from the catalog does not promise removal from every saved-selection label. | Secondary consequence, selection code unchanged | **Recording if captured**. Base: choose synthetic stale slug in an unsent draft or save a default; preserve the isolated draft/settings and open against head. For controlled multi/saved-transition timing use a **labelled harness**. Do not claim stale selections are migrated or repaired by this PR. | Capture waived: primary fixture deliberately selects a live model; explicit limitation |
| A1 | Shared picker callers: default/fallback, text generation, scheduled task, review guide, writer; compact and multi composer triggers | Catalog membership follows R2. Mixed/None/custom trigger labels, provider/continuation locks, connected/saving/send-busy/permission guards remain caller-specific. | Same changed data, shared popup | **Recording if captured**. Open corresponding Settings/editor in same environment; choose Cursor. Do not create/run schedules or paid turns for proof. | Covered by shared path; no separate capture required |
| P1 | Desktop, native iOS/Android, remote/relay/tunnel, second environment | Same server inventory; desktop shares web renderer. Native list filtering and saved-selection fallbacks differ as described above. Environment-scoped data must remain scoped. | Shared data change; no platform/wire change | **Recording if captured**. Attach client to this isolated server. Native picker: ThreadSettingsSheet/new-task/scheduled-task model route; pull-to-refresh. Native device or desktop shell capture is an extension, not a substitute for R1–R4. | Outside minimum; no native/Electron/remote runtime verification claimed |

### Existing UI branches deliberately not multiplied

Settings inputs are `models`, `customModels`, `hiddenModels`, `favoriteModels`, `modelOrder`, driver/instance identity, and management/preference permissions. Branches include empty/nonempty, singular/plural, >8 filter threshold, no search matches, favorite/visible/hidden groups, all built-ins hidden (Enable all), custom add/edit/remove and validation errors, ordered move boundaries, capabilities/no-reasoning/fast labels, different name versus slug, truncated long labels/tooltips, read-only controls, and sm/lg layouts. They are existing presentation branches: R1 exercises changed counts/membership and preference groups without making every editor state a new requirement. Adding/editing/removing a custom row, favorites, hide/unhide/bulk toggle, reorder, filter, and tooltip interactions are recording-only if added to evidence.

Picker inputs include instance entries/readiness, active instance/model, selectedModels/onToggleModel, model options, favorites/preferences, search, provider/continuation locks, setup callback, disabled reason, open/disabled/compact/size/composer-owned/terminal/keybinding props and explicit trigger labels. Branches include closed/open, provider/Favorites/search, no favorites/no results/setup notice, legacy collapsed/expanded, unavailable/update-required models, mixed-provider secondary text, new badge, custom rows, keyboard jump labels, selected/checkmarked, single/multiple triggers (0, 1, 2, >2 labels and >3 icons), disabled provider/model, and capped/virtualized height. Open/close/Escape, search, provider-tab navigation, keyboard selection/jump, legacy expansion, star toggles, shift multiselect, setup navigation, scrolling and refresh are recording-only. No render code in these branches changed. Light/dark colors, narrow/wide layout, Linux/macOS key labels and native iOS/Android treatments do not require a Cartesian product for this fix.

Other providers, command palette navigation to Settings, agent tools, scheduled-task execution, and rollback/turn controls have no new render component in the diff. No real provider turn, account switch, paid-plan downgrade, retired upstream model, or schedule execution needs to be manufactured. The three synthetic built-in rows are explicitly labelled cache fixtures, not evidence of an actual account change.

## Capturer handoff

1. Work only in the lead-assigned isolated run/checkouts. Do not alter a running worktree's branch, another lead's state, or the live T3 home. Use unique timestamped output directories; preserve raw recordings/logs. If using an explicit test home, pass `--home-dir <isolated-home>` and seed `<isolated-home>/caches/cursor.json` instead of assuming the default path. Disable real-data seeding with `T3CODE_DEV_SEED=0`. Keep secrets and pairing URLs out of uploaded evidence.
2. Obtain the real current catalog with the injected key, enable Cursor, and stop the server by its captured PID. Read the cache JSON, preserve its schema/identity/health fields, and append only S to `models` (deduplicate by slug). Save a clean seed copy and a sanitized list of slugs/counts. Do not change a stale row into `isCustom:true`: current custom rows are deliberately retained.
3. Run the pinned base with that seed. Configure optional favorite/hidden/custom controls through Settings or `server.updateSettings` (payload `{patch: ...}`), then freeze the same input settings/cache for the head. Preference patch operations include `setModelFavorites:[{provider:"cursor",model:"old-account-only-b",favorite:true}]` and `setModelsHidden:[{provider:"cursor",model:"retired-model-x",hidden:true}]`; custom rows belong to the instance's `config.customModels`. If obtaining the consolidated preference fixture is inconvenient, omit all optional preferences consistently and still capture R1–R4.
4. Record R1–R3 on base and on the reviewed head from identical inputs. Label recordings **Before — base** and **After — reviewed head** with revision links. Record actual catalog count N rather than assuming the historical 48. Wait for observed ready/authenticated status, not a fixed sleep. Show positive live-model controls alongside negative stale-model searches. Clear filters before quoting totals. Explicitly select the Cursor tab; a Favorites tab or hidden preference can otherwise create a false absence result.
5. R3 manual refresh after startup is an idempotence/no-resurrection check on head; it is not proof that a stale in-memory list shrank specifically when the button was clicked. A strict stale→clean manual-refresh transition needs a controlled SDK source that initially returns S and later omits S: label that **harness**. Neither writing the cache while running nor editing the DOM is a valid substitute. Settings applies credential changes with automatic probing, so changing a key before clicking Refresh also cannot prove that the click caused removal.
6. For R4, start another seeded isolated head run with a deliberately invalid API-key override and record its actual non-ready result. The real key stays private. Capture retained Settings rows/cache and the error state, not a fake successful catalog. No browser sign-in or credential upload is needed.
7. Pair UI proof with sanitized cache/RPC evidence: revision, instance/driver, enabled/installed, status/auth status, checkedAt, total model count, and presence booleans/slugs for S and a positive live/custom control. Do not upload full settings/cache blobs containing account names, email, tokens, or unrelated user data. A capture must show the real app; if a harness is used, label its controlled data source and row ids.

Coverage: complete diff, changed/adjacent tests, Cursor status/driver/catalog, registry cache/merge/persistence/publication paths, both web surfaces and every shared picker caller, native option-builder callers, seeding feasibility, and state/theme/width/platform exclusions. No app was launched, no test suite was run, and no captures were taken. No worktree edits, forge writes, comments, pushes, or server processes were made by this auditor. Available disk remained 12 GB.

FRICTION: none

VERDICT: INSUFFICIENT R1, R2, R3, R4 — recordings and matching cache/RPC excerpts missing
