# Proof audit, pass 1 — fixture matrix

Findings first:

1. **Missing one scoped recording: W1, the silent gateway failure.** Minimum capture set: **1440×900 CSS pixels × dark theme × W1**. Although no client rendering changes, the failure text is user-visible in the existing row. The repository exception says “no visible surface”; it does not clearly waive proof of this changed visible result. One labelled harness recording is proportionate; three recordings of existing row styles are unnecessary. The [existing transcript](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/488855004ad39887/transcript-33583.md) is explicitly a test-harness runtime transcript, including the failing-before assertions. It is not an app recording or a real backend run. At the requested head I independently ran the same two focused files: **2 files, 104 tests passed**, exit 0. The server logic is covered, while the W1 app recording is still missing. This is a proof-scope verdict, not a full merge-readiness audit.
2. **Timing remains 15 minutes.** Recording a gateway row immediately after a stub returns `end_turn` proves parsing and normal completion only. It does not prove the newly changed silent-prompt watchdog path. For that path, keep the prompt unresolved and wait the actual 15-minute inactivity interval, or label any injected clock/shortened adapter timeout as a harness. `retryable: true` is failure metadata, not proof that T3 automatically retries a gateway failure.
3. **Quota forms are visibly distinct from gateway failures.** A `usage_limit` failure uses a warning row, suppresses the raw provider message, and optionally shows reset/recovery controls. Gateway failures use the existing error row with the message. A future reset and no reset are distinct fixtures. The prior quota recordings (V2/V3) are optional diagnostics; the current diff does not justify making all three mandatory UI recordings.
4. **Do not fake inaccessible branches with a misleading fixture.** A corrupt SQLite log is caught and yields no recorded failure; it does not make the outcome checker die. The exact missing-hook and checker-defect branches require adapter-level injection. Live Antigravity/backend evidence is unavailable on this VM. These rows are excluded from required app captures; W1 is reproducible with a labelled stub.

Review scope: [#33596](https://git.cafe/pingdotgg/t3code/pull/33596), issue [#33583](https://git.cafe/pingdotgg/t3code/issues/33583), branch `fix/issue-33583`, head [6317eae](https://github.com/pingdotgg/t3code/commit/6317eae373e520973fa1ec273f93217dfc3ad7b6), actual main/base [bbdc7dc](https://github.com/pingdotgg/t3code/commit/bbdc7dca320ee4fbc7131498aaffef2fa1eb3d41). Local `main` is stale; comparisons use `origin/main...HEAD`. Four changed files: two production modules and their existing server test files. No worktree edits, PR writes, browser use, or servers started by this audit.

## Callers and observable surfaces

- [AntigravityTurnOutcome.ts](https://github.com/pingdotgg/t3code/blob/6317eae373e520973fa1ec273f93217dfc3ad7b6/apps/server/src/provider/acp/AntigravityTurnOutcome.ts#L155) exports one watcher. Its only production caller is [AntigravityAdapterV2.ts](https://github.com/pingdotgg/t3code/blob/6317eae373e520973fa1ec273f93217dfc3ad7b6/apps/server/src/orchestration-v2/Adapters/AntigravityAdapterV2.ts#L277), conditional on a profile directory. The real driver supplies that directory. Watcher setup snapshots the last step index before each prompt. The check reads only the last newly appended step in the same own, unlinked log, then parses at most 16,384 payload bytes.
- The returned check has two adapter call sites: normal `session/prompt` resolution, and the newly added watchdog retirement with synthetic `{stopReason: 'end_turn'}`. Normal explicit `error`, `cancelled`, or interrupted completion skips the check. A normal checker defect takes the existing prompt-error path; the new watchdog call instead catches causes and preserves the generic timeout failure.
- [ACP adapter](https://github.com/pingdotgg/t3code/blob/6317eae373e520973fa1ec273f93217dfc3ad7b6/packages/provider-acp/src/server/adapter.ts#L7840) is instantiated by Antigravity, Grok, Kiro, FX, and the ACP registry. Only Antigravity configures the production inactivity timeout and outcome watcher. Other wrappers therefore have no new runtime branch enabled by this diff. Other provider adapters, entry points, Settings, keybindings, and MCP tools have no separate implementation to patch.
- Failed `turn.terminal` becomes an error turn item in [ProviderEventIngestor.ts](https://github.com/pingdotgg/t3code/blob/6317eae373e520973fa1ec273f93217dfc3ad7b6/apps/server/src/orchestration-v2/ProviderEventIngestor.ts#L625). Existing projections carry failure message/class/code/reset into the thread runtime. An OOM override can replace its message, unchanged and outside this bug.
- Web and desktop share [TimelineWorkEntryRow.tsx](https://github.com/pingdotgg/t3code/blob/6317eae373e520973fa1ec273f93217dfc3ad7b6/apps/web/src/components/chat/TimelineWorkEntryRow.tsx#L209): provider errors show the body, usage limits show a warning label with optional localized reset time. [ChatView.tsx](https://github.com/pingdotgg/t3code/blob/6317eae373e520973fa1ec273f93217dfc3ad7b6/apps/web/src/components/ChatView.tsx#L917) also feeds the dismissible error banner; usage-limit runs instead feed [UsageLimitRecoveryBanner.tsx](https://github.com/pingdotgg/t3code/blob/6317eae373e520973fa1ec273f93217dfc3ad7b6/apps/web/src/components/chat/UsageLimitRecoveryBanner.tsx#L36). Sidebar failure/limited status derives from the same runtime class.
- Mobile has separate, unchanged [error-row rendering](https://github.com/pingdotgg/t3code/blob/6317eae373e520973fa1ec273f93217dfc3ad7b6/apps/mobile/src/features/threads/thread-work-log.tsx#L859) and [UsageLimitRecoveryCard.tsx](https://github.com/pingdotgg/t3code/blob/6317eae373e520973fa1ec273f93217dfc3ad7b6/apps/mobile/src/features/threads/UsageLimitRecoveryCard.tsx#L22). Both consume the same server failures. No native/mobile, Electron-shell, relay, tunnel, light-theme, or narrow-viewport capture is required for this server behavior. Logs live on the provider environment, including when the viewing client is remote.

## Seeding recipes for a future capturer

**S — stub in the actual isolated app.** Use an isolated T3 home and isolated provider profile; never the live install. Make an executable literally named `agy` under the capturer's scratch directory. Set the isolated Antigravity provider instance's `config.binaryPath` to its absolute path in Settings → Providers; do not rely on PATH alone (the default discovery still searches the legacy executable name). The basename allows the CLI path without a `localharness_external` sibling. The server launches it with `acp` and a managed `GEMINI_HOME`. Use the existing mock agent as a protocol reference. Speak newline-delimited ACP JSON-RPC: initialize, eager authenticate (return success without a Google flow), session/new, session/resume/load as needed, model/mode selection requests, session/prompt, and cancel. Advertise a harmless fixture model locally; redact model identifiers from all shared evidence. Handle startup/probe sessions separately from fixture prompts.

On session creation ensure `$GEMINI_HOME/antigravity-acp/conversations/` exists as real directories. Return a safe session id such as `fixture-session-1`. On receipt of `session/prompt`, **after** the watcher has snapshotted the log, create/append the real regular file `<sessionId>.db`. Schema: `CREATE TABLE steps (idx INTEGER PRIMARY KEY, step_type INTEGER, step_payload BLOB)`. Append monotonically increasing idx values; user input is type 14, planner reply 15, error 17. Store UTF-8 error text in a BLOB, optionally wrapped with the existing fixture's protobuf-like prefix and duplicated error text. SQLite rows, not transcript.jsonl alone, drive this parser. Do not pre-seed an error before prompt dispatch: it would be outside the watched interval. Send `end_turn` for immediate outcome fixtures, `refusal` for typed quota fallback, or leave the prompt request unresolved for watchdog fixtures. Do not emit active tool/approval/subagent state in a simple silent fixture: it pauses the watchdog. For the retry/restart sequence, let the next runtime resume and complete a second prompt.

Navigate to the real thread route `/$environmentId/$threadId` after creating the project/thread and sending the fixture message through the app, or through `orchestration.dispatchCommand` with `thread.create` and `message.dispatch`. Public RPC can start the stub-backed run; it cannot directly submit an internal provider `turn.terminal` failure. A forged projection/RPC terminal screenshot would only demonstrate rendering, not either changed component. Settings, catalog/auth bootstrap, and this stub recipe are source-derived and were not executed in this pass. Any resulting capture must say **“ACP stub harness; real T3 client/server; no live Antigravity backend.”** Keep a sanitized timestamped protocol/event transcript beside the video.

**H — adapter/parser harness only.** Use the existing focused test harness, production parser, SQLite fixtures, and `TestClock.adjust('15 minutes')`; await terminal events rather than sleep/poll. Inject a checker returning `undefined`, an absent hook, or `Effect.die` only where indicated. A driver can log the emitted `turn.terminal` object. This produces a labelled harness runtime transcript; it is not an app UI recording. No public RPC/settings switch advances the real server's inactivity clock or forces an arbitrary Effect defect. A shortened configured adapter timeout is also a labelled harness and must not claim production elapsed time.

**L — live backend.** Cannot be produced on this VM: the real `agy` backend is unavailable. Not in the required set.

All S/H recipes are plans, not newly implemented drivers. A future capturer must put its own drivers, logs, and timestamped runs in its assigned scratch directory.

## Fixture matrix

`A` = changed behavior; `R` = regression/control adjacent to the changed code; `U` = unchanged surface/interaction. “Covered” means focused automated execution or source mapping as explicitly stated; **no UI capture exists or is claimed**. Every interaction/timer sequence is marked **recording** if visual evidence is later made. Only W1 requires a capture: **1440×900 × dark × W1**. All other captures are optional or explicitly excluded.

| ID / fixture | Input data / sequence | Observable result | Diff | Seed / feasibility on this VM | Capture kind / current status |
| --- | --- | --- | --- | --- | --- |
| P1 clean / quoted / stale | Last new step type 15 ordinary text or quoted quota text; older type 17 outside snapshot; or non-error tool last | Normal `end_turn` completes; no failure row; earlier error does not contaminate this prompt | R | S: append reply after old error; H parser fixture | recording if showing prompt completion; **covered**, clean/quoted/stale variants in parser test |
| P2 HTTP gateway | Last new type 17 BLOB: `Error 502, Message: Bad Gateway: Failed to connect to backend API, Status: 502 Bad Gateway, Details: []`; `end_turn` | Red error body has exactly the gateway message, code `502 Bad Gateway`, retryable true; no quota/reset UI | A parser | S immediate `end_turn`; H parser fixture | recording; **covered parser regression**, no app capture; optional V1 alternative proves normal resolution only |
| P3 gRPC / other API error | Type 17 `Error 400, Message: Request contains an invalid argument., Status: INVALID_ARGUMENT, Details: []`; transient control: 503/UNAVAILABLE | Same red-row shape, API message preserved; 400 retryable false, 5xx true; codes are metadata, not separate UI layouts | R regex compatibility | S immediate `end_turn`; H | recording; **400 covered**; transient control source-mapped, no separate required capture |
| P4 malformed / bounded error | Type 17 null/empty/unknown text, incomplete header, delimiter in message, or API text beyond 16,384-byte prefix; malformed header followed by a valid next header | No full match gives generic red “Antigravity stopped this turn because of an error.”; later complete header can be selected without mixing messages | A regex boundary; R fallback/size cap | S BLOB variants; H can also benchmark repeated malformed headers | recording for app; **source-mapped optional robustness controls**; no claim all variants tested |
| Q1 quota with future reset | Type 17 429/RESOURCE_EXHAUSTED and `reason:QUOTA_EXHAUSTED`; invalid timestamp `soon`, `quotaResetDelay:3600s`, and `retryDelay:42s` | `usage_limit`, canonical quota message, retryable true; reset now+1h; warning label includes localized time; recovery banner offers resume/snooze | R parser; A when selected by watchdog | S with delay measured from check time, H parser | recording; **covered parsing and delay precedence**, existing UI mapped; optional V2 |
| Q2 rate limit with future reset | Type 17 429/RESOURCE_EXHAUSTED, RATE_LIMIT_EXCEEDED, RetryInfo `retryDelay:42s`; no QUOTA_EXHAUSTED | `usage_limit` with API message and now+42s; same visible warning/reset shape as Q1, raw provider message suppressed in row | R; A watchdog selection | S or H; use larger delay for a leisurely app capture if labelled fixture | recording; **covered parser**; collapsed into Q1 UI shape, no extra required capture |
| Q3 quota without reset | `refusal` without usable new error evidence; or type 17 QUOTA_EXHAUSTED without valid positive reset/retry duration | Usage warning without time; “Reset time unavailable; retry manually”; no reset recovery controls; default code QUOTA_EXHAUSTED absent API match | R typed fallback; A for logged silent quota | S return `refusal` without error; silent path needs type 17 quota text (synthetic `end_turn` alone is not refusal); H | recording; **typed fallback covered**; optional V3; silent variant source-mapped |
| Q4 reset equivalents | Valid quota timestamp wins over delay; invalid timestamp falls back; malformed/zero delay gives no reset; HTTP status-form 429 also parses; RESOURCE_EXHAUSTED with another HTTP code remains usage limit | Normalizes to Q1/Q2 future reset, Q3 unknown, or Q5 expired; no additional renderer | A HTTP-form parsing; R precedence | S BLOB variants; H parser | recording if demonstrated; **source-mapped subvariants**, existing invalid-timestamp/delay case covered; no extra captures |
| Q5 past reset / recovery disabled | Parsed reset is valid but at/before run completion, or thread settled/request pending; alternatively let future reset pass | Warning still shows known reset; scheduling unavailable under eligibility rules; after deadline snooze action disables | U; data may now arrive from watchdog | S seed old timestamp, or wait through future reset; H for server milestones | **recording**, time-dependent; **mapped optional**, no new UI requirement |
| W1 silent recorded gateway | S P2 error appended during current unresolved prompt; no meaningful updates/blockers for 15 minutes | At unchanged deadline turn fails with P2 gateway object instead of generic timeout; old process retired, one terminal event | A watchdog + parser | S full real 15-minute wait; H uses test clock (current adapter test injects already-parsed failure) | **recording**, timer; **MISSING required recording**, logic covered in two focused layers; V1; real backend is not required |
| W2 silent recorded quota | Unresolved prompt; current last type 17 is Q1/Q2/Q3 | At deadline warning/reset UI follows recorded usage failure, rather than generic red timeout; reset delays are computed at outcome-check time | A generic recorded-failure branch | S with logged error; H checker returning usage failure | **recording**, timer; **source-mapped** from class-agnostic recordedFailure branch; optional, not an independent UI blocker |
| W3 silent no evidence | Watcher returns undefined: no new step, non-error last step, unreadable/missing database or steps table, invalid session id, missing/symlinked profile components, or file inode/device replaced since snapshot | Generic red “Antigravity sent nothing for 15 minutes … Send a message to continue.”; runtime retired | R / new check fallback | S omit error/corrupt own log; H exact checker-undefined injection | **recording**, timer; **generic timeout covered with no hook**, watcher no-evidence variants covered/mapped; exact hook-undefined combination source-mapped |
| W4 silent no hook | Flavor has no `watchPromptOutcome`; production Antigravity driver normally supplies one | Same generic timeout as W3, no log check | R retained branch | H omit hook; cannot select exact missing-hook branch through current normal Antigravity app configuration | **recording** if visualized; **covered adapter test**, harness-only, excluded from required app captures |
| W5 silent checker dies | Hook setup succeeds; returned check dies at retirement | Error log “failed to check a silent ACP prompt's outcome”; generic timeout still emitted, process retired | A catchCause fallback | H inject Effect.die; corrupt SQLite via S does not reproduce this branch | **recording** if visualized; **covered new defect test**, harness-only, excluded from required app captures |
| W6 progress rearms | Current-session thought/message/tool/terminal/plan/compaction progress before deadline; metadata-only update or foreign-session progress as negative controls | Genuine output moves deadline; foreign/metadata updates do not; no early error; timeout eventually yields W1/W3 | R timer | S emit ACP updates; H existing 10m/progress/15m-boundary sequence | **recording**, timer; **covered activity/foreign/metadata controls**, remaining activity kinds source-mapped |
| W7 waiting pauses | Open agent request, pending user-input/approval request, tool pending/running/waiting, or subagent pending/running; then settle it | No timeout while blocked; rearmed interval after observed completion; eventually failure or normal completion | R pause conditions | S actual ACP tool/permission requests and completion updates; H existing tool/approval tests; adapter injection for exact internal states | **recording**, interactions/timer; **tool and approval covered**, remaining pause predicates mapped; no extra required capture |
| W8 completion / Stop / stale callback | Wire settled before deadline; context finalized/interrupted/not active; old runtime generation or teardown underway; explicit error/cancel response | Watchdog does not overwrite existing terminal outcome; Stop stays interrupted/cancelled; explicit error uses existing failure path; no duplicate row | R guards | S respond before deadline, Stop, late response; H gate callbacks/generations to reproduce deterministic races | **recording**, interaction/timer; **source-mapped guards**, broader adapter suite passes; no claim each race has its own capture |
| W9 next-message recovery | After W1/W3 retirement, send second message; stub's next process resumes and returns a clean reply | Thread remains reusable, fresh runtime handles second prompt, completes normally; historical error row remains | R restart unchanged | S stub process counter and per-run transcript; H existing inactivity/restart test | **recording**, interaction; **generic timeout restart covered**; recorded failure selects same retirement path |
| I1 quota recovery controls | Q1 with valid latest run, future reset, not settled/no pending request; toggle resume/cancel, snooze/wake, then deadline | Existing labels switch Resume/Cancel auto-resume and Snooze/Wake now; pending state; reset wake behavior | U existing interactions | S then actual app controls; server metadata update path; do not impersonate an internal terminal RPC | **recording**, interaction/timer; **mapped optional**, not changed by PR |
| I2 error banner / history / other surfaces | P2/P3/P4/W3; open thread, dismiss top error banner, revisit; show sidebar; mobile long-press copies row | Existing banner is dismissible, historical failure remains; sidebar failed vs limited; mobile red message vs warning label uses same failure fields | U existing presentation | S real web app; native app would require separate device/client; no native capture attempted or required here | **recording**, interactions; **mapped optional**, no theme/device multiplication |
| L1 real backend failure | Real Antigravity returns 502/quota and leaves prompt open | Confirms vendor payload/timing in production conditions | External validation, not a different UI row | L unavailable on this VM; S must be labelled harness and cannot claim L | **unavailable/excluded**; no required capture |

## Capture handoff and evidence sufficiency

Required capture set: **one recording, 1440×900 CSS pixels × dark × W1**. Existing behavior evidence: the failing-before parser and watchdog assertions in the linked transcript, plus the focused suite at the requested head. Both new failure-selection and defect-fallback tests pass; the parser suite also passes quota/rate-limit/invalid-argument/own-log controls. This sufficiently covers the server logic and retains regressions, but is not a capture of the changed user-visible result. The missing recording is W1 only; Q1/Q3 (previous V2/V3) are not blockers.

For the capturer, the minimum is **one recording: 1440×900 CSS viewport × dark theme × W1**. W9 can be appended at negligible extra cost but is optional because restart mechanics are unchanged. Keep the actual 15-minute silence intact in the raw recording; a shared shorter excerpt must label the time cut and link a sanitized timestamped transcript. Show the stub label, prompt dispatch/running state, then the failed row with the gateway message rather than generic inactivity text. Match it to a sanitized terminal-event transcript showing class `provider_error`, code `502 Bad Gateway`, and retryable true. A successful follow-up is useful optional evidence, not a second required row. A test-clock or shortened-timeout driver is an explicitly labelled harness alternative, not real elapsed-time proof. Optional quota recordings may reuse the same viewport/theme for Q1 and Q3 (previous V2/V3). No light/mobile/desktop-shell cross product is warranted because this PR changes neither layout nor client behavior.

No row has been marked visually verified. No server/emulator/device lease was started. Focused test command completed with exit 0, and worktree remained clean. Disk available after tests: 10 GB.

FRICTION: infra | Real Antigravity backend unavailable; no live-provider run possible. | A backend-enabled environment is required only for optional L1 evidence.

VERDICT: INSUFFICIENT W1 — labelled silent-gateway recording at 1440×900, dark theme
