# Evidence capture procedure

Issue [#33247](https://git.cafe/pingdotgg/t3code/issues/33247). Before [c6ca605630](https://git.cafe/pingdotgg/t3code/commit/c6ca6056309904f89b90b6055d797973be0a0939), after [107c62505b](https://git.cafe/pingdotgg/t3code/commit/107c62505bbaa56b1f7d2c9cdd73a5fd66fc0831). No source edits or commits were made for this capture.

Both checkouts were installed with `vp install` and started with `T3CODE_DEV_SEED=0 vp run dev`. Fresh development databases were created at each checkout's migration level. No live database was read or copied, and neither VITE_HTTP_URL nor VITE_WS_URL was set. Real ports from dev-runner: before web 8477 / server 16517; after web 7759 / server 15799.

`stub.mjs` starts an ephemeral loopback HTTP endpoint and generates scratch Pi models/settings with synthetic credentials. Do not publish the generated credential files. `rpc.mjs` authenticates to the development server and implements the server's WebSocket RPC envelope. `seed.mjs` creates a tiny synthetic git repository, project, Pi provider instances and one thread for each fixture, dispatches prompts, and reads persisted projections through RPC. `T3_CAPTURE_APP_ROOT` points to an installed checkout for the ws dependency. `seed` creates U/T/D; `addR` adds R. A deleted exploratory U thread could not reuse its ID, so the selected U thread uses the `-capture` suffix. The exploratory thread is not used in any selected evidence.

The real Pi 1.4.0 binary was resolved with `command -v pi`. Each instance uses driver pi, model default, launchArgs `--no-approve`, and PI_CODING_AGENT_DIR under the timestamped scratch run. The stub rejects WebSocket upgrades; Pi falls back to SSE. Both sides have the same synthetic prompts and model. Pi retries are enabled with maxRetries 1 and baseDelayMs/maxAgentDelayMs 100.

## Rendering environment (every selected capture)

T3 built-in preview, Chrome 156.0.0.0 on Linux x86_64, incognito profile; no device emulation, freeform CSS viewport; DPR 2; font scaling 100% (root 16px); timezone UTC. Sidebar and details panel closed, short thread at the top, no cropping. The preview screenshot API scales desktop full-surface images to 1280×889 from the 1440×1000 CSS viewport. Narrow screenshots are 780×1688 from 390×844 CSS. Recordings are native 2880×2000. Preview tools do not expose prefers-reduced-motion emulation, so it was not overridden.

| Pair | CSS viewport | Theme | Browser clock |
|---|---|---|---|
| U | 1440×1000 | dark | frozen 2026-11-09 09:24:00 UTC |
| T desktop | 1440×1000 | light | frozen 2026-11-09 09:29:00 UTC |
| T narrow | 390×844 | dark | frozen 2026-11-09 09:29:00 UTC |
| R | 1440×1000 | dark | frozen 2026-11-09 09:30:00 UTC |
| D recording | 1440×1000 | dark | live UTC |
| D settled comparison | 1440×1000 | dark | frozen 2026-11-09 09:40:00 UTC |

For each still, wait for app synchronization/network activity to settle and `document.fonts.ready`; dismiss any floating error notice, close overlays, move the pointer off the conversation, and blur the composer. Freeze the browser clock only after load and failure persistence (Date subclass overriding empty construction and Date.now). Take two preview_snapshot(save:true) screenshots at least 500 ms apart. Decode pixels and require exact equality (ImageChops.getbbox() null / changed-pixel count zero). Retake unsettled frames. Inspect the selected full images and both diff images. No DOM text, styles, failures, or model labels were fabricated for the screenshots.

`diffs.py` uses full images without alignment or masks. Diff0 marks any nonzero max RGB channel delta white (so even a one-level change is visible). DiffT marks a max RGB channel delta greater than 5/255 white. ImageMagick compare AE cross-checks the zero-threshold count. The D diffs use supplementary settled screenshots after the deadline; D's primary evidence remains its recording pair.

## Fixture actions and timestamps

U: streamed usage_limit_reached with no reset. Prompt dispatch before 09:21:55.043, after 09:21:55.143 UTC. Auto-resume off.

T: identical HTTP 429 payloads with resets_at 1794224127. Prompt dispatch before 09:24:47.616, after 09:24:47.587 UTC. Auto-resume off. Pi rendered ~131 min; fixed adapter persisted resetAt 11:36:18.557 UTC. Both desktop and narrow use this same thread state.

R: first SSE error server_error / stream disconnected before completion; second SSE error usage_limit_reached / The usage limit has been reached. Prompt dispatch before 09:27:08.147, after 09:27:08.110 UTC. Persisted retry attempt 1, maxAttempts 1, retryDelayMs 100 on both sides. The fixed current failure row is hidden in favor of the usage-limit banner, so the requested visible after-retry wording was not observed.

D, same sequence on both builds:

1. Enable autoResumeLimitedThreads before dispatch. Open the empty D thread at 1440×1000 dark and start recording: before 09:29:29.609, after 09:29:30.351 UTC.
2. Dispatch `Reply with the synthetic recovery confirmation.`: before 09:29:30.621, after 09:29:30.571 UTC.
3. First stub request receives HTTP 429: before 09:29:31.961, after 09:29:31.987 UTC. Both resets_at values are 1794216636 (09:30:36 UTC), about 65 s ahead. Pi says ~1 min. Fixed adapter persists resetAt 09:31:02.032 UTC, about receipt +90 s.
4. Observe failure; dismiss the floating failure notice if present. Before needed this at roughly recording +30 s; after showed the usage-limit banner instead. Pointer/focus were not perfectly matched during the recording: before exposes prompt hover controls and a composer caret for part of the clip. These are harness differences, unrelated to classification. Supplementary settled stills match pointer/focus state.
5. Wait without sending a manual continuation. After automatically starts run 2 at 09:31:02.274 and completes at 09:31:02.728 UTC. The stub's successful SSE request is 09:31:02.604 UTC. Main has only failed run 1 when read again after the deadline.
6. Stop recording: before 09:31:31.586, after 09:31:31.820 UTC. Raw MP4 durations are 122.264 s before and 94.207 s after: the preview recorder ends the after clip at its last painted frame. Both include the relevant deadline and resulting state. These files are unedited; they are not equal-duration encodes.
7. Later take matching settled comparison stills (two pixel-identical frames each) after the deadline. The tiny marks at the left in the recovered view are the app's actual timeline minimap, not duplicated or ghosted text. A magnified timestamp inspection confirmed the timestamp is fully rendered.

## Coverage and limitations

U, T (both viewports), D and R captured: 4/4 capture rows, 5/5 required pairs. C is test-only and has no capture requirement. Mobile is out of this trimmed matrix's scope. The visible wording `Usage limit reached after N retries` on the current R failure is unverified because the fixed UI shows only the recovery banner; the persisted retry and usage_limit classification are verified in the RPC transcript.

AI-assisted; verified with the tests and tools listed above.
