# Browser timeout evidence

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

## Environment for every capture

Both sides were independently installed and run from separate Git worktrees with `T3CODE_DEV_SEED=0 vp run dev`. Each server created its own fresh database. No production database or credentials were copied into the fixture. Surface: real web app, Linux, Node 26.12.1, server-owned Chrome 157.0.8103.38, outer browser viewport 1600 × 1000 CSS pixels, app light theme. Recordings were scaled from 3200 × 2000 to 1600 × 1000 without changing timing. Each capture was opened and visually inspected before upload.

| Capture | Commit | State / environment |
| --- | --- | --- |
| before-main.mp4 | 871b00907d0bcf2705224de6c6e90de925c7cbb9 | Base environment above; new synthetic thread and Tab-A/Tab-B |
| after-main.mp4 | b55ddf9a2b1fa4f6be0ef62cca3d8dacdc6268a1 | Head environment above; same seed and actions |
| before-dialog.mp4 | 871b00907d0bcf2705224de6c6e90de925c7cbb9 | Same base server; new Dialog tab |
| after-dialog.mp4 | b55ddf9a2b1fa4f6be0ef62cca3d8dacdc6268a1 | Same head server; new Dialog tab |
| before-queue.mp4 | 871b00907d0bcf2705224de6c6e90de925c7cbb9 | Same base server; new Expiry tab |
| after-queue.mp4 | b55ddf9a2b1fa4f6be0ef62cca3d8dacdc6268a1 | Same head server; new Expiry tab |
| before-recovery.mp4 | 871b00907d0bcf2705224de6c6e90de925c7cbb9 | Same base server; close Tab-A and open Recovery |
| after-recovery.mp4 | b55ddf9a2b1fa4f6be0ef62cca3d8dacdc6268a1 | Same head server; same recovery |
| before-closed.mp4 | 871b00907d0bcf2705224de6c6e90de925c7cbb9 | Restarted base with another fresh database; Closed-control/Healthy-control |
| after-closed.mp4 | b55ddf9a2b1fa4f6be0ef62cca3d8dacdc6268a1 | Restarted head with another fresh database; same seed and actions |

## Driver and fault injection

`instrument.py <worktree>` installs the fixture and the scripted ACP agent. The agent runs as the development Replay provider's actual subprocess, in real persisted agent threads. It uses the real MCP endpoint and thread-scoped authorization supplied by T3 to that subprocess. No synthetic thread scopes, replacement broker, or loopback bridge are used. Authentication, MCP handlers, broker, manager, tab queue, Playwright browser, UI RPC and manager event mirror remain real. The app's Browser panel drives Tab-B appearance, zoom and close; these do not bypass the panel.

To deterministically hold the dark appearance operation, the injected gate sits immediately before the real `page.emulateMedia` call in `ServerBrowser.applyRendering`. `fault=never` never resolves. `fault=dialog` schedules a real browser alert, then never resolves. `fault=release` releases after 120650 ms and lets the original call run. The fixture is the same on both sides and shows its applied theme and click count.

The uploaded agent changes only its default private log destination to the portable `agent-results.ndjson`. The uploaded instrument includes the fixture title adjustment used in the runs. The driver is deterministic and does not call a paid model.

Create the real persisted thread through the checkout CLI, with the isolated home selected by `--base-dir .t3` and the development Replay provider. Send the commands below with `thread send`; the same JSON text appears in the videos. `port` is that side's actual Vite port (base 5868, head 6148). The second thread is titled Independent status thread and sends `status`.

## Same interaction on both sides

1. Send `EVIDENCE {"action":"setup","port":<port>}`. Open the Browser panel. Send `hang`: default-deadline dark appearance on Tab-A, then same-tab resize and status. In the Browser panel select Tab-B; choose Preview menu → Appearance → Dark, use Zoom in, then close Tab-B. From the independent real thread send `status`.
2. Send `dialog`: open a fresh tab, start its recording, request dark appearance, then resize and recording stop while its alert is pending. Read status, accept the alert, close the tab.
3. Send `queue`: open a fresh Expiry tab and start recording. Request dark appearance. After 250 ms queue a click with a 1000 ms deadline, then after another 250 ms queue recording stop (120000 ms deadline). Wait for the injected action to release, then send `inspect` to read the counter and verify no recording remains active. Videos retain the wait; the visible counter stays zero.
4. Send `recover`: close the owned stuck Tab-A, open a fresh tab with `reuseExistingTab=false`, resize it.
5. Restart each side on fresh state, additionally apply `closed-control-instrument.py <worktree>` before launch. Send `closedSetup`. In the Browser panel set Closed-control to Dark, then set Healthy-control to Dark. This injection closes the first tab's actual SessionControl immediately before the existing mirror `system().catch` path. Neither system() nor its caller is replaced.

Interaction order and fixture are the same; navigation and menu opening take different amounts of wall time. No test results are offered as capture evidence.

## What happened

- Main: base's FIRST request timed out at 16108 ms and evicted the host. Follow-up resize/status returned no-host errors at 65/24 ms; they did not themselves wait for broker timeouts. Head timed out at 15671 ms, then resize returned actionable busy guidance in 55 ms and status succeeded in 158 ms. Tab-B stayed light on base despite panel settings; on head its appearance and zoom applied. The panel close event was processed on head; base's later tab list still included Tab-B after its panel was closed.
- The independent thread's status returned promptly on both sides (107/185 ms). It had no owned tab, so both reported available=false; this is a real authenticated cross-thread request, not proof that an already-owned second thread tab survives eviction.
- Dialog: head resize and recording-stop returned dialogPending guidance (77/135 ms), ahead of busy guidance. Status exposed the actual alert; accepting it and closing the tab succeeded. Base's follow-ups had no host and could not resolve the alert through MCP.
- Queue: head's queued click timed out and never clicked (counter zero after release). Its recording-stop request finished successfully at 120332 ms, saving a 65281-byte MP4 after the request's 120000 ms deadline, demonstrating cleanup still runs. A later stop said no recording was active. Base's 1-second queued click evicted the host, interrupting the held appearance and recording-stop callers. Base also ended at zero clicks: this experiment does NOT claim the existing pre-start deadline check on base allowed the click to run. The new signal-aborted guard is exercised through the same expired queued request, but the video cannot attribute protection to that individual line.
- Recovery: closing Tab-A and opening/resizing a fresh tab succeeded on both sides after reconnection. Head does this while retaining the shared host.
- Closed control: after the forced close-before-system race, base's Healthy-control remained light; head's changed dark. Head caught the closed-control rejection and continued the mirror. Supplemental `system-probe.ts` runs the real source directly (no test runner or replacement control): base throws synchronously, head returns a rejected Promise, and neither callback executes. Its terminal output is in runtime-transcripts.txt.

## Limits

The deterministic faults are injected; an organic Playwright hang or naturally occurring close race was not awaited. Native Electron and mobile surfaces were not run. Continuity of another thread's already-open owned tab was not captured. Recording data is synthetic. The recording cleanup video shows successful saving and later no-active-recording, rather than playing the saved inner fixture recording. The explicit signal.aborted branch cannot be distinguished visually from base's existing deadline check in this case; the behavior (expired queued work never starts) is shown on both sides.
