# Read-only proof audit, pass 1

Issue [#20883](https://git.cafe/pingdotgg/t3code/issues/20883); PR [#24313](https://github.com/pingdotgg/t3code/pull/24313) (GitCafe same number).
Reviewed head [70fbe839eb](https://github.com/pingdotgg/t3code/commit/70fbe839eb1cf8d151101e3c39066132ee1b1a4f). PR observed main base [26c3625f48](https://github.com/pingdotgg/t3code/commit/26c3625f484eadd4d5b6ca5a3dd1f8f8bd5533b4). This is a source-derived capture prescription, not captured acceptance proof. No browser, capture, tests, edits to the checkout, push, PR comment, or merge performed. Only this external report was written and uploaded as requested.

## Verdict and scope

INSUFFICIENT PROOF. Existing five focused browser tests were reported passing; read here, not rerun. They mock toast calls and route parameters, so do not establish real toast timing, sidebar/keyboard input, rendering, or client/environment integration. No valid complete final-head acceptance pair exists. PR body agrees. Cleanup completed during this audit at [4fbf29c294](https://github.com/pingdotgg/t3code/commit/4fbf29c29477a4dbb4c176f254fb6b95c1b6e2c2). Final read-only comparison confirms only the seven proof files and five knip registration lines were removed, with no changes to either web file; working tree is clean. Use full head 4fbf29c29477a4dbb4c176f254fb6b95c1b6e2c2 for acceptance unless it changes again. GitCafe reports 31 successful checks at the audited head, but current-main conflicts; neither changes this proof verdict.

Production diff: 22 added lines in one web coordinator: an environment-local toastId -> threadId map, matching-route close effect, unmount cleanup, top-level onClose ownership removal, and registration after add. No changed contract, server, provider adapter, mobile code, or toast renderer. Audit the final cleanup diff to ensure only proof files and their knip entries disappear. Do not preserve proof scripts in production merely to run evidence.

## Render paths and callers

- [Authenticated root](https://github.com/pingdotgg/t3code/blob/70fbe839eb/apps/web/src/routes/__root.tsx#L219) mounts ToastProvider / AnchoredToastProvider and ThreadNotificationCoordinator outside the route outlet. Web and Electron share this path. Native mobile does not.
- [Coordinator](https://github.com/pingdotgg/t3code/blob/70fbe839eb/apps/web/src/components/ThreadNotificationCoordinator.tsx#L32) maps connected environment IDs to keyed EnvironmentNotifications. It returns null only when mode=off AND in-app=false. Removing an environment unmounts only its owner; losing a live shell merely clears prior observations and does not unmount it.
- [New ownership effects](https://github.com/pingdotgg/t3code/blob/70fbe839eb/apps/web/src/components/ThreadNotificationCoordinator.tsx#L122): environment mismatch -> retain all; environment match/thread mismatch -> retain unrelated; both match -> close every matching ID; cleanup -> close every remaining owned ID.
- [Creation path](https://github.com/pingdotgg/t3code/blob/70fbe839eb/apps/web/src/components/ThreadNotificationCoordinator.tsx#L142): only changed observed nonarchived top-level threads. First snapshot, unchanged state, archived threads, subagent rows, stopped completion, and non-events do not create a new notice. Completion is success/check; approval is warning/shield; input is warning/question; failure is error/alert with long-description clamp; limited is warning/question. All five variants enter the changed ownership path. Child attention can surface on its parent through existing presentation logic; no new adapter-specific branches.
- In-app gate requires enabled + visible + focused + a different environment/thread. The desktop Notification branch and sound/badge machinery are separate and unchanged. No browser Notification permission is needed for the in-app proof.
- [Sidebar row](https://github.com/pingdotgg/t3code/blob/70fbe839eb/apps/web/src/components/sidebar/SidebarThreadRow.tsx#L744) pointer / Enter / Space delegates scoped thread selection; [current sidebar keyboard handler](https://github.com/pingdotgg/t3code/blob/70fbe839eb/apps/web/src/components/Sidebar.tsx#L2539) handles previous/next and indexed jump. [Legacy handler](https://github.com/pingdotgg/t3code/blob/70fbe839eb/apps/web/src/components/LegacySidebar.tsx#L3450) has the equivalent route behavior. AppSidebarLayout selects legacySidebarEnabled, default false. [Command palette](https://github.com/pingdotgg/t3code/blob/70fbe839eb/apps/web/src/components/CommandPalette.tsx#L1640) thread search also navigates to the same scoped route. Route changes, not click-handler patches, trigger the fix. Settings has notification enable/mode controls and Connections removal, not another thread-opening implementation.
- [Toast renderer](https://github.com/pingdotgg/t3code/blob/70fbe839eb/apps/web/src/components/ui/toast.tsx#L511): regular top-right global portal, stacked/collapsed/hover-expanded, responsive inset; leading icon, title, thread-title description, Open thread action, corner dismiss. Notifications omit threadRef/threadId, so generic scoped-toast filtering does not hide them on navigation. Anchored, expandable-detail, copy-error, and visible-time timeout paths are not used by these notices.
- Installed Base UI 1.5.0 defaults to timeout=5000ms, limit=3. closeToast invokes top-level toast.onClose after marking ending. Corner dismiss calls manager.close, so its data.onClose lookup does NOT by itself show an ownership leak. Date freezing leaves timers live. Exit opacity/transform transition is 500ms.

## Minimal scaled fixture matrix

Four required cells: 1440x900 light, 1440x900 dark, 390x844 light, 390x844 dark, CSS pixels. Narrow is responsive web, not native mobile. Every interaction row below needs Before and After recordings in all four cells. A still cannot prove closure timing or selectivity. At most THREE live notices at once; split chapters instead of hiding a fourth witness. A/B denote separate connected environments, T/U/V/W/L distinct thread IDs except explicitly equal T across A and B. Use visibly distinct public titles with environment prefixes.

| Row | Minimal real app sequence / branch | Expected After (Before retains route/unmount-owned notices until normal expiry) | Affected? | Medium |
|---|---|---|---|---|
| R1 Selective route closure | On quiet route create A/T completion, A/U long-title input, B/T approval. Open A/T by sidebar; then B/T by configured keyboard jump; finally A/U by previous/next or jump. | First removes only A/T; A/U and B/T remain. Second changes environment with SAME T ID and removes only B/T. Last removes A/U. Both environment and thread mismatch branches are witnessed. | Yes, route effect | Recording, 3 action checkpoints |
| R2 Repeated ownership + remaining variants | Fresh state: A/V complete, then a later approval on A/V, plus A/W failed with >180-char title. Open V by sidebar: both V notices close, W stays. Open W through its toast action. Return quiet, emit A/L limited, open L by keyboard. | All matching IDs close, not just latest; error remains selective, action still works; limited closes on navigation. Existing success/warning/error/icon/wrap/clamp paths are visible. | Yes; action is existing control | Recording, 3 action checkpoints |
| R3 Lifetime + unmount | Fresh chapters: dismiss a completion with corner X; let an input expire naturally off-hover; use fresh notices for the following. With A and B notices live, remove B using Connections and return: B notice closes, A stays. Then in-app=false while mode=sound: A remains (owner mounted); mode=off: A closes (owner unmounted). Re-enable, emit fresh notice, open target. | Manual/expiry stay closed on later route changes, unrelated notice survives single-owner removal, global unmount clears remainder, remount creates and routes normally. While target already active, another completed turn produces no in-app toast. | Yes, onClose/cleanup; guards are controls | Recording; chapter snapshots |
| C0 Comparable appearance | Extract pre-action frames above; show expanded stack, success/input/approval/error/limited states across chapters. Same app state, viewport, theme, pointer/focus and fonts on both SHAs. | No intended pre-action visual change. Report actual differences. | Unchanged render control | Still pairs extracted from recordings; no separate capture run |

This is branch/interaction coverage, not status x entry-point x lifecycle Cartesian expansion. R1 covers the issue's three requested statuses; R2 covers the shared path's other visual variants and loop multiplicity; R3 covers all new ownership lifetime paths. Do not duplicate all statuses for both sidebar and keyboard. A separate native mobile/provider matrix is unjustified by this diff. Electron shell integration remains unverified unless separately exercised; shared web evidence is not a claim of desktop execution.

Additional caller coverage without another four-cell fixture: in one 1440-light chapter, select a pending notice's thread via command palette; switch legacySidebarEnabled and exercise one real row plus its keyboard handler. Record these if claiming those callers verified. They can reuse R1/R2 fixtures after fresh emission. Source tracing alone must be labeled as such. Settings toggles are already R3.

## Minimum deliverables and acceptance gate

- Eight continuous recordings minimum: Before/After x four cells, each with clearly timestamped R1/R2/R3 chapters and deterministic resets between chapters. Alternatively 24 row-level recordings (3 rows x 4 cells x 2 phases). Video packaging does not reduce the required interactions.
- For each decisive action retain matched pre/post PNGs from each phase. R1 and R2 have three navigation checkpoints each; R3 needs manual-dismiss, natural-expiry, B removal, mounted-disable, global-unmount and remount-navigation checkpoints. Retain running -> toast appearance in video. Reuse pre frames as C0 stills.
- For EACH paired checkpoint generate diff0 and diffT (T=8/255 maximum RGB-channel delta). Include counts, full images and interpretation. diff0 is absolute RGB delta amplified 16x/clipped; diff8 is magenta above threshold, black otherwise. No crop, resize, registration, recolor or hand cleanup. Post frames intentionally differ where notices disappear and exposed background appears. Focus/hover/stack displacement and any unrelated residual differences must be disclosed.
- Report exact full Before/After SHAs, clean or dirty state, production diff identity after cleanup, startup command, app/backend ports without auth URLs, UTC timeline plus video offsets, browser/OS, CSS viewport, actual PNG/video raster dimensions, DPR, visualViewport.scale, browser zoom, root font, text scaling, theme, pointer/focus/visibility and Date/timer treatment. Previous measured environment is historical, not a measurement of the future run.
- Route post frame: >500ms after actual route commit, ideally 650-1000ms, with demonstrable timeout budget remaining on the Before notice. Use monotonic elapsed time/video timestamps; frozen Date is not timing evidence. Hover can pause timers while building fixtures, but after leaving the stack count all unpaused time. If Before expired, the pair is invalid. If tooling stalls, discard/retry that chapter, not the entire unrelated matrix.
- Multi-toast witnesses must visibly exist together BEFORE the action and remain identifiable afterward. Do not infer a hidden/fourth toast survived. Do not count a DOM text search, pending request state, or toast-manager mock as visual acceptance.

## Exact feasible setup (prescription only; not run in this audit)

Use the scripts from the audited commit, which remain recoverable after removal. Externalize ALL seven files, including capture.mjs. Example from the existing worktree:

```sh
mkdir -p /tmp/issue-20883-evidence/fixture
for name in README.md browser.js capture.mjs clock.mjs diff.py provider.mjs wire.json; do
  git show 70fbe839eb1cf8d151101e3c39066132ee1b1a4f:apps/server/scripts/issue-20883-proof/$name > /tmp/issue-20883-evidence/fixture/$name
done
chmod +x /tmp/issue-20883-evidence/fixture/provider.mjs
mkdir -p /tmp/issue-20883-public-project
git init /tmp/issue-20883-public-project
git -C /tmp/issue-20883-public-project -c user.name=Proof -c user.email=proof@example.invalid commit --allow-empty -m 'Public fixture'
printf '1792670400000' > /tmp/issue-20883-clock
```

Do setup on a capture checkout, not the implementation agent's active checkout. For a comparable pair use observed PR main base 26c3625f484eadd4d5b6ca5a3dd1f8f8bd5533b4 and the final cleanup head, verifying their production coordinator diff. Current origin/main has advanced and conflicts; a current-main pairing needs a separately integrated head and a fresh audit. Never switch the shared active worktree under its implementation agent.

For each phase, start real dev app with a fresh isolated public home; retain its terminal session/PID. Example, replacing the home suffix per environment/phase:

```sh
T3_PROOF_CLOCK=/tmp/issue-20883-clock NODE_OPTIONS='--import=/tmp/issue-20883-evidence/fixture/clock.mjs' T3CODE_AUTO_BOOTSTRAP_PROJECT_FROM_CWD=false ./node_modules/.bin/vp run dev --home-dir /tmp/issue-20883-proof-home-A
```

Run a second isolated dev instance for B with a different home. Read actual shifted ports from dev-runner output. Connect B to A's browser using Settings > Connections > Add environment against B's backend. Use the built-in T3 preview workflow once capture is authorized. Pair privately; never publish startup/auth URLs. Do not set VITE_HTTP_URL/VITE_WS_URL; never use the live ~/.t3 home. No provider subscription or real command execution is required.

Evaluate external browser.js in the real Vite app, correcting its hardcoded /@fs/.../packages/client-runtime/src/state/runtime.ts import for the capture checkout. The helper captures primary env in its closure. For B, adapt the EXTERNAL helper to take an explicit environmentId (replace the `const env = registry.get(primaryEnvironmentIdAtom)` assignment with the connected B ID), run it in the SAME main browser and retain separate proofA/proofB objects. All project/thread/settings operations remain createEnvironmentRpcCommand and normal environment commands. This avoids emitting while a separate seeding tab has stolen focus. Do not just call proof.seed twice against A.

For both environments set providerInstances.proof = {driver:'codex',displayName:'Public fixture',config:{binaryPath:'/tmp/issue-20883-evidence/fixture/provider.mjs'}} via server.updateSettings. Set inAppNotificationsEnabled=true, notificationMode=off. Run each helper's seed once; await live six-thread shell before releasing anything. Use distinct A/B public titles, same `proof-complete` ID across environments for T. Long input title already exists. R2 can use proof-complete as V, proof-failed as W, proof-limited as L in a fresh baseline; send complete then approval to V, no need to invent a mixed protocol request.

The existing peer supports complete/input/approval/failed/limited through PROOF:<kind>. proof.send(threadId,kind) dispatches a real user turn. It writes /tmp/issue-20883-gate.ready, waits for /tmp/issue-20883-gate, then emits Codex wire events through the production adapter/orchestrator/projections/WebSocket into the real client. Clear both gate files before EACH dispatch; wait for the ready file and running UI; start recording before release. Serialize ALL releases across both environments because the peer's gate is global. Advance clock file and browser freeze epoch by 1000ms per later completion; use the identical sequence on both phases. Do not dispatch a later turn on V until its completion/checkpoint has actually settled. No projection/database/toast injection.

Build R1/R2 stacks with the pointer over the real stack to pause expiry after first toast. Three live toasts maximum. Observe every expected toast in the real UI; protocol acceptance is not enough. Capture expanded pre, then perform genuine sidebar/native keyboard input. Narrow web requires drawer opening. Use snapshot locator role=button and the public row title/project, not button-only queries. Resolve the actual ordered scoped sidebar row and bind ctrl+alt+shift+9 to thread.jump.N using server.upsertKeybinding; N is the actual visible order (1-based), not a thread ID. If duplicate labels exist, disambiguate by environment. Do not use JS router navigation as the tested action.

Keep reset data outside the checkout. Stop only retained server sessions before copying/restoring the public fixture homes. Homes contain auth: NEVER upload them. Fresh baselines per row avoid unresolved input/approval sessions contaminating another chapter. A baseline can be seeded separately per SHA through commands; copying persisted homes between versions requires compatible schema and is not assumed here. Deterministic content/IDs must match even though local environment IDs and ports can differ; disclose and interpret resulting visible differences.

For R3, mode=sound + in-app=false deliberately retains EnvironmentNotifications; mode=off then causes the actual unmount. Simply toggling in-app=false while sound/desktop mode remains active does not exercise cleanup. A socket disconnect is not necessarily environment removal; use the Connections remove operation and verify the keyed owner is absent. Re-enable and emit a new event rather than expecting a historical snapshot to re-toast.

Existing capture.mjs is only a starting point: it clears every toast at start, emits one event, has unscoped Open thread selectors, and its unmount action rapidly re-enables before navigating. It cannot execute R1/R2, retain unrelated witnesses, or expose the actual unmount checkpoint without EXTERNAL orchestration changes. Its final text-presence check is supplemental only. Use one recording with real action markers and actual checkpoints instead of blindly running this helper per kind. diff.py already implements the required diff0/diff8 pair and counts.

## FRICTION

- Working-copy evidence files disappeared during initial inspection as expected from the concurrent cleanup; recovered read-only with git show at the supplied full SHA. Their external copy location is not yet established in this environment.
- Local main was stale and yielded thousands of unrelated changes. PR-observed base and origin/main comparison isolate the same ten-file audited PR scope; current origin/main has advanced. No ref edits/fetch or conflict resolution attempted.
- `cafe pr diff --stat` is unsupported (INVALID_INPUT); used read-only git diff instead. Cafe returned JSON envelopes under this environment's configured output mode.
- Existing capture helper's singleton design, global gate, five-second timer, three-toast limit, duplicate environment/thread labels, hardcoded checkout import, and rapid unmount/remount are concrete setup constraints. Previous late post checkpoint cannot be rehabilitated.
- PR registration succeeded. No browser or capture was started under this read-only authorization. Report upload is the only requested external write.

Final acceptance remains pending valid recordings and matched checkpoints on the final integrated head.
