# PR #22788 proof audit, pass 2 (fs-08, 2026-10-22, read-only)

Auditor: Claude Opus 5.5 (Claude Code). No edits, commits or pushes in `~/wt/pr-22788`. `~/wt/pr-22943` not touched.

## SHAs

| Check | Result |
|---|---|
| PR head | `0af183bec7`, parent `de48987b4a`, grandparent `6f2d6c63e3` (the driver landed in two commits, not one) |
| `git diff --stat 6f2d6c63e3 0af183bec7` | only `apps/server/scripts/timeline-follow-evidence.ts` (+402). **No production change: PASS** |
| B | `8d3b061121` = merge-base, and the second parent of the merge `6f2d6c63e3` |
| `git diff --stat 8d3b061121 6f2d6c63e3` | 6 files, +449/−177. Runtime files: `useTimelineFollow.ts`, `MessagesTimeline.tsx`, `ChatView.tsx` |
| Manifest | downloaded copy is byte-identical to `/tmp/ev-22788/out/manifest.md`. Fixture sha `c26f312c…b87` is the same on B and A |
| Capture worktrees | `/tmp/ev-22788/run/{A,B}` no longer exist, so the overlay `git status` can only be read from the manifest and S0 text, not re-checked |

## Per-row verdicts

| Row | Capturer | Audit verdict | Basis |
|---|---|---|---|
| S0 transcript | PASS | **PASS** | S0-A shows the phases advancing `entry → … → resumed`, with each `.done` appearing after its release, and a matching fixture sha. I re-ran it on a `git archive` of B plus the overlay: the companion seed and all five releases completed. |
| Hydration hold | PASS | **PASS (deviation OK)** | Every W result.json shows `holdAfterNav`, `loadingAt100`, `loadingBeforeRelease` and `titleInHeader` all true, with 143–158 frames and 2.57–2.79 s. `wsRequestsHeld: 0` in every row: the WS hold never engaged and the HTTP `GET …/bounded` route hold did all the work. That is acceptable but should be stated as the mechanism. |
| W1 PageUp, B | NOT REPRODUCED | **INVALID TEST (driver)**, not evidence against the bug | PageUp ×2 takes the gap to 1484 px. The viewport is 848 px (`client` = 5994−5146), and `maintainScrollAtEndThreshold={1}` (MessagesTimeline.tsx, same on B) limits LegendList's re-pin to within one viewport of the end. At gap 1484 the list cannot snap, even with B's follow flag still on. The B snap needs the reader to stay inside that one-viewport band. |
| W1 PageUp, A | listeners 4/1 | **PARTIAL**: attach PASS, hold UNPROVEN | A's listener counts after release are real (4 on timeline, 1 doc keydown; B 0/0). A's "held" sample is the same as B's (top 3662, P063 on both), so it shows nothing B didn't. |
| W1p pill, A | FAIL (gap 160) | **INCONCLUSIVE (driver timing)** | While `isWorking`, the list uses `TIMELINE_MAINTAIN_SCROLL_AT_END_SMOOTH`, and LegendList's animated maintain path runs for 500 ms. The driver samples exactly 500 ms after `.done`. My B live-clock rerun also ends at gap 160 at `S_res`, so 160 is a sampling artifact, not an A regression. |
| W2 wheel, B | NOT REPRODUCED | **INVALID TEST (driver)** | Wheel ×2 gives gap 1200, which is above the 848 band. Same cause as W1. |
| W2 wheel, A | listeners 4/1 | **PARTIAL** | `handleWheel` is on the timeline on A and absent on B. The hold is indistinguishable from B, same as W1. |
| W3 no-input control | FAIL both | **FIXTURE/DRIVER INVALID, not the app** | (1) The `prose` phase adds P101–P120 in one chunk, +1117 px, which is more than one viewport (848). After that jump the distance from the end exceeds the threshold, so LegendList won't re-pin. (2) `tool-start`/`tool-finish` add 0 px in every row: height stays at 5994, so no tool boundary ever grows the list. (3) The 500 ms sample wait races the 500 ms smooth follow. My rerun of W3 on B (archive of 8d3b061121 + overlay, isolated home, ports 13816/5776) gave a nondeterministic partial follow: fixed clock top 5146→6022→6375 (gap 241, 48); live clock 5146→6176→6263 (gap 87, 160). The capture had 5146 stuck. Follow did run, but the fixture outran it. |
| Frozen clock (`page.clock.setFixedTime`) | not discussed | **Suspect, not shown to be the cause** | It freezes `Date.now`, which LegendList uses for the MVCP anchor-lock TTL, imperative-scroll settle timeouts and velocity windows. In my W3-B rerun, fixed and live clocks behaved alike, so it is not the main cause of W3. It is still a risk and should be dropped for W rows. |
| Phase-range indexing | FAIL | **FAIL, confirmed: narration timestamps don't match the video** | Frame extracts: W1-B @35.2 s shows overlay `pageup t=33.9s` and the pre-PageUp view (P090–P100 at the bottom), while the narration puts `S_up` (P063 on top) at 34.8 s. W3-A @26.5 s shows `prose t=24.7s` when the narration says `resumed` started at 25.4 s. The video lags the clip clock by about 1.2–1.8 s. `videoStartWall` is taken after `newPage()`, but recording started earlier and the lag is not constant. Narration .md tables are consistent with result.json samples, but not with the mp4 times. |
| Recording content vs narration | n/a | **PASS (state), FAIL (timing)** | Frames show the same scroll state that the samples report (W3-A at prose: P090–P101 visible, not following; B and A sidebars as expected). Durations: W1-B 43.2 s, W1-A 45.3 s, W3-A 29.4 s, W3-B 29.2 s. |
| ST1/ST2 1440 stills | UNVERIFIED | **UNVERIFIED** | C-mounted DiffT is 46,591 px (ST1) and 6,390 px (ST2). The B and A homes have different sidebar thread counts (W3-A sidebar shows 8+ long threads), so C-mounted is not comparable. C-held only shows the same non-snap on both. |
| ST3/ST4 390 | not captured | **MISSING** | — |

## VERDICT: INSUFFICIENT

- The matrix's central claim was not tested in a reproducing configuration: B snaps, A holds. The driver scrolls the reader beyond `maintainScrollAtEndThreshold={1}` (one viewport), so B cannot snap whatever its listener state. "NOT REPRODUCED" here is a driver artifact, not evidence that the race is irrelevant.
- The W3 control fails on both revisions because of the fixture: growth comes as one chunk larger than a viewport, tool phases add no height, and the 500 ms sample races the 500 ms smooth follow. It does not show an app regression, but no W row is a valid control as captured.
- Narration timestamps are offset from the recordings by about 1.2–1.8 s, and phase indexing fails on every clip.
- What is proven: on A the listeners attach after a ≥140-frame held hydration (4 on timeline + 1 doc keydown, all after `hold-release`), while B has 0/0. That is good causal evidence for the mechanism, but there is no behavioral before/after.

## Next steps to make B reproduce the snap

1. **Stay inside the band.** After `mounted`, scroll up by less than one viewport: one `page.mouse.wheel(0, -300)`, or one `ArrowUp`/a single PageUp on a taller viewport. Assert `2 < gap < 0.8 × client` at `S_up` before releasing any phase. B keeps follow on (no listener, so `liveFollowUserScrollGenerationRef === anchorUserScrollGenerationRef` and `onIsAtEndChange` returns early) and LegendList re-pins. A should flip `liveFollowEnabled` off and hold.
2. **Grow in sub-viewport steps.** Split `prose` into several chunks under about 400 px each, or emit P101–P120 per paragraph with the existing 40 ms pacing. Then the W3 control can follow continuously, and the W1/W2 growth stays inside the threshold on B.
3. **Make the tool boundary grow the list.** `tool-start` and `tool-finish` currently add 0 px. They are folded into the existing work or "Thinking" row. Emit a visible tool row, or expand the work group, so the "next tool boundary snaps" step from #20366 actually happens.
4. **Sample after settle, not after a fixed sleep.** Wait for `.done`, then wait until `scrollTop` has been stable for 10 RAF and at least 700 ms have passed, since the smooth maintain runs for 500 ms. Only then take `m()`. Apply the W3 pass rule `gap ≤ 2` after the settle.
5. **Drop `page.clock.setFixedTime` for W rows.** LegendList's TTLs depend on `Date.now`. Keep the fixed clock only for stills, if label stability is needed there.
6. **Fix indexing.** Derive the clip offset from the first overlay-colour frame (nav), not from `Date.now()` after `newPage()`. Or start the recording via CDP screencast timestamps. Re-verify every mark within 0.2 s.
7. **Stills.** Use fresh homes per cell with identical sidebar contents, by seeding the same companion and long threads only, so C-mounted diffs are attributable. Capture 390×844 with the sidebar collapsed and navigate through the thread switcher or command palette.

## Rerun provenance (optional row, done)

- B source: `git archive 8d3b061121` plus `git show 6f2d6c63e3:` of the two fixture scripts, into `/tmp/ev-22788/audit2/B`, with `vp i`. Home `/tmp/ev-22788/audit2/home`, `T3CODE_PORT_OFFSET=43` → 13816/5776. Ports were checked free beforehand.
- Driver: `git show 0af183bec7:apps/server/scripts/timeline-follow-evidence.ts` copied to `/tmp/ev-22788/audit2/driver/ev.ts`, with one change: an optional `--live-clock` flag. Run with `--row W3 --hold-http`. Results are in `/tmp/ev-22788/audit2/r/W3-B-{fixed,live}/result.json`.
- The dev server was stopped by its spawned process group (PID 1413950). Ports 13816/5776 were confirmed free afterwards.
