# A01 native evidence recipe — round 4

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

Task [#33042](https://git.cafe/pingdotgg/t3code/pull/33042), mirror [#33042](https://github.com/pingdotgg/t3code/pull/33042); issue [#33024](https://git.cafe/pingdotgg/t3code/issues/33024). Before [4c361199f4](https://git.cafe/pingdotgg/t3code/commit/4c361199f47f6ea32a2c14835df392640d0f7c63), after [28ee80fa3a](https://git.cafe/pingdotgg/t3code/commit/28ee80fa3a26b4663950345034f971887d0ad2f8).

## Isolation and fixture

Created /home/user/wt/pr-33042-before and /home/user/wt/pr-33042-after at exactly the requested commits using git worktree add. Ran vp install in each. The existing review worktree was not used. Both native clients were prepared with vp run mobile:install ios --prepare before leasing. Each checkout's own server created a fresh database, using T3CODE_DEV_SEED=0 and an explicit --home-dir below this timestamped scratch directory. No live database or another lead's state was read, copied or seeded.

seed.py creates a tiny temporary Git project and two synthetic Claude-format transcripts under each side's scratch home/.claude. launch.py points CLAUDE_CONFIG_DIR and CODEX_HOME into that scratch home. rpc.mjs calls agentSessions.scan, projects.mutate project.create, then agentSessions.import. Both servers returned importedCount=2, skippedCount=0. clean removes the fresh server's automatically created scratch project/thread and applies the same synthetic display selection. The public script accepts that selection through EVIDENCE_FIXTURE_MODEL, withholding the private fixture identifier; it does not render on the native list. No provider turn ran. Both imported threads have historyOrigin=v1_import, latestRunId=null and activeRunId=null, two visible items and no watch. See synthetic-proof.json.

The same fixture driver served both sides. Actual dev-runner ports: before backend 15443 / web 7403; after backend 14054 / web 6014. Start commands were vp run dev --host 0.0.0.0 --home-dir <scratch>/<side>/t3 with T3CODE_DEV_SEED=0. No VITE_HTTP_URL or VITE_WS_URL was set.

## Device and matching sequence

One session pr33042-native-r4, connected once with agent-device connect proxy --daemon-base-url "$SIMLOCK_AGENT_DEVICE_URL" --session pr33042-native-r4, then agent-device open --platform ios --device "iPhone 17 Pro" --session pr33042-native-r4. The same lease remained open for both sides. Every device command ran from the after checkout, which owned the lease. For before, invoked its absolute mobile-native-client.ts ensure ios --session command from that owning workspace; after used vp run mobile:install ios --session. Both fingerprints were 5fa891c99bf29870cc7d56112e3458d95a948393.

Exactly one Metro at a time, port 8087, all interfaces via Expo --lan, REACT_NATIVE_PACKAGER_HOSTNAME=fs-52.tail131df4.ts.net. Opened the same dev-client URL with disableAutoLaunch=1 and disableFab=1, using --relaunch, then paired each fresh backend with the repository pair-client.sh. Removed before's connection before switching to after.

Settings: iPhone 17 Pro, iOS 27.2, portrait, 402×874 points, scale 3, 1206×2622 pixels, Large dynamic type, fontScale 1, system dark, default app fonts. Settings were applied once to the shared leased simulator. before-environment.json and after-environment.json were read from the running native app and match.

Both starting states show only the same two settled imported threads, Settled expanded, scroll at top. Before take 1 expanded Settled from its initial collapsed state; before take 2 and both after takes explicitly collapsed then expanded it. Each take ran scroll top, which returned Already at top, followed by a 10-second idle request. Both before takes are pixel-identical; both after takes are pixel-identical. The matched collapse/expand/top sequence therefore agrees with the initial before expansion. No row registration or coordinate offset was applied.

install-display-clock.py temporarily changed only the two relative-time default nowMs arguments in apps/mobile/src/lib/time.ts to 2026-11-10T12:00:00Z on both sides before Metro launch. capture-controls.js fixes the display-minute snapshot to that same minute. Runtime Date, server clocks, imported timestamps, guards and list components were unchanged. The unused global Date freeze option in the inherited helper was never called and is omitted from the public helper. Restored both source files before removing the worktrees; provenance.json records clean tracked state and exact SHAs.

## Stills, normalization and diffs

capture.py requests two screenshots with --pixel-density 3 --normalize-status-bar and a 500 ms pause after completion of the first screenshot command. Remote capture/transfer takes additional time: this proves stable captures with a 500 ms request gap, not an exactly measured 500 ms sensor-to-sensor interval. timestamps.json contains the request/completion times. A daemon batch with screenshot → wait 500 → screenshot honored the wait but failed to download the nested files; that experiment is excluded. Exact sensor timing remains unverified.

normalize-stills.py converts to RGB and replaces ONLY rectangle [0,0,1206,180] on every accepted still with the identical top 180 pixels from before-start-take1-a.png. This is the iOS status bar, including clock, battery, network and Dynamic Island. All app pixels below it are original. The home indicator is naturally hidden in every accepted raw still; it is outside the normalization box and was not masked. No bottom-toolbar normalization, cropping, scaling, blur, quantization or registration was used. Raw originals and normalized copies are in the bundle.

measure.py computes max(abs(beforeRGB-afterRGB)) per pixel. Diff0 lights every pixel >0 and DiffT16 every pixel >16, full-intensity magenta on black. Boxes are physical pixels, [left,top,right,bottom), right/bottom exclusive. The two independent starting-state comparisons each have Diff0=173 (max channel delta 2), bbox [87,2373,1137,2559], and DiffT16=0. Every one of those 173 pixels lies on the native bottom toolbar's glass edges. Both starting row regions are exactly identical even at threshold zero. All six normalized repeated A/B pairs (two starts and one endpoint per side) have 0 pixels at both thresholds.

Accepted endpoint pair: before-end-b-public.png and after-end-retry-b-public.png. Diff0=63,733, bbox [53,505,1154,963]. DiffT16=48,562, bbox [53,505,1154,960]. All changed pixels are in the un-settled row, its removed Working shelf and the resulting upward shift of Settled/its remaining row. There are zero endpoint differences outside that region, including native bottom toolbar and home indicator. See verification.json for all counts and provenance.json for source hashes.

## One-pixel finding and what else changed

The round-3 settled-row displacement did not reproduce on either of two matched takes per revision. Treat it as capture/launch/scroll-state variance under the rule in this brief. This run does not isolate which earlier gesture generated the fractional scroll/layout state. It is not a reproducible settled-row behavior change. Both revisions have identical mobile list and row source hashes. Settled selection precedes runtime-based Working selection in [threadListV2.ts:751](https://github.com/pingdotgg/t3code/blob/28ee80fa3a26b4663950345034f971887d0ad2f8/apps/mobile/src/features/threads/threadListV2.ts#L751). Both render the same slim row layout at [thread-list-v2-items.tsx:1472](https://github.com/pingdotgg/t3code/blob/28ee80fa3a26b4663950345034f971887d0ad2f8/apps/mobile/src/features/threads/thread-list-v2-items.tsx#L1472); the card-only faded content is not used there.

What else changed:

- Intended: primary leaves Working, loses the grey Waiting glyph, gains 6h, becomes brighter/heavier, and the Settled shelf/remaining row shift upward. The provider mark within the primary row brightens with its row emphasis.
- Starting-state-only capture variance: 173 native glass-edge pixels differ by 1–2 channel levels. No changed pixels at T16 and no settled-row offset. This native chrome was preserved, not normalized away.
- Status bar is identically normalized as described above. Both raw home indicators are hidden. Endpoints have no unrelated native chrome differences.
- Recordings have different command/animation durations. They show the same long-press → Un-settle path; they are not a performance measurement or pixel-identical animation comparison.

## Recordings and timestamps

Kept new round-4 recordings, not round-3 clips. Before's final recording was retaken with Working already expanded so the endpoint visibly retains the waiting row. A normal RPC thread.settle reset between before rehearsals only changed settlement metadata; it did not create a run. Accepted before endpoint stills came from the preceding completed Un-settle rehearsal, with the same no-run endpoint. After's recording and stills came from its first Un-settle.

| Side | Native recording | Long-press request UTC | Un-settle request UTC | Stop request UTC | Visible clip landmarks |
|---|---|---|---|---|---|
| Before | before-unsettle-public.mp4 (9.43s) | 10:59:31.194 | 10:59:35.346 | 10:59:43.146 | 0–4s settled; 5–7s menu; 8–9s transition then Working/grey glyph |
| After | after-unsettle-public.mp4 (16.97s) | 11:05:05.693 | 11:05:14.556 | 11:05:25.742 | 0–8s settled; 9–15s menu; about 16s main list/6h |

Recorder export omits idle frames/time, so clip durations differ from command wall times. normalize-video.py replaces only the top 180 pixels with the same status-bar image and exports H.264, 30 fps. App content is untouched. Both raw and normalized recording contact sheets and every accepted still/diff were opened before upload. The bundled normalized recording review sheets cover every integer-second frame.

## Verification limits and cleanup

Evidence-only run: no source implementation changes and no tests rerun. Android, other dynamic type sizes/themes/devices, other scenarios and exact sensor-to-sensor 500 ms timing are unverified. No claim about automatic subscription latency or real provider execution.

Pool transport intermittently returned unavailable; first recovery followed the required full-minute wait. During the next endpoint request the scroll and idle commands also failed, while the two screenshot commands succeeded; the batch shell continued after the failures. The accepted endpoint had already been idle since the recorded action, over two minutes earlier, and the recovered snapshot showed the intended endpoint. These failures are not presented as successful scroll/idle actions.

Removed both disposable mobile connections; inspector confirmed remaining catalog entries=0 before agent-device close succeeded. Stopped only the four captured process groups. Verified no owned process and no listener on 15443, 7403, 14054, 6014 or 8087 remains. Restored the display-clock source changes and removed both created worktrees with git worktree remove --force. Did not touch the existing review worktree, PR body or live T3 data. Final disk availability 101 GB. No commits, pushes or PR edits.
