# PR #22788 evidence round 2: proof audit pass 2

Auditor: Claude Opus 5.5 (Claude Code), 2026-10-22 ~13:15–13:25 UTC. Adversarial review of the round-2 capture ([manifest](https://gh-file-drop-api-prod-mi5fy3sowv63ufte.pinglabs.workers.dev/f/e1441f1152536c59/ev22788-manifest.md)) against the pass-1 matrix (`/tmp/ev-22788/audit1/matrix.md`). Nothing was committed, pushed, or rebased. I touched no capture artifacts and did not touch `~/.t3/userdata`.

## Summary

The core claim holds up and **I reproduced it independently**. With hydration delayed, main (`ed3f965e07`) never attaches the follow listeners (0/0) and snaps a 0.4-viewport wheel-up back to the bottom at every streamed boundary. The PR merge (`c4f9d0e114`) attaches 4/1 and holds the reader's position to the pixel. My W1 rebuild ran on fresh homes and fresh servers, and its numbers are identical to the capturer's.

One material caveat (C1 below): the fixture's "tool" phases are not real tool calls on either build. The ACP `tool_call` update falls into the server's *unsupported update* path, so every "tool boundary" is an `Unsupported ACP update: tool_call` dynamic-tool row. That is symmetric across builds, and the row still flows through the timeline's tool-row and data-change path. So it does not void the comparison, but the evidence should not be described as "real tool calls".

## 3. Validity

| check | result |
|---|---|
| BEFORE tree SHA | `/tmp/ev-22788/base` HEAD = `ed3f965e0768f9a77729a17c5145a90ff3ab6210` ✔ |
| AFTER tree SHA | `~/wt/pr-22788` HEAD = `c4f9d0e1144151d14da7e8a3612e0a43af3ef141`, parents `9ec682fce8d5…` (PR head) + `ed3f965e07…` ✔ |
| AFTER vs BEFORE diff | exactly the PR's 6 files (`acp-mock-agent.ts`, `timeline-follow-fixture.ts`, `ChatView.tsx`, `MessagesTimeline.browser.test.tsx`, `MessagesTimeline.tsx`, `useTimelineFollow.ts`) ✔ |
| Working trees | `git status --short`: only the two `?? apps/server/scripts/ev22788-*` overlays in each ✔ |
| Overlay sha256 (both trees = `/tmp/ev-22788/overlay`) | mock `504242bd…38b8`, fixture `9400062b…8cc8`, byte-identical in all three places ✔ |
| Driver sha256 | `driver/` and `upload/` copies = `49c57bfdf6d423c76aad464f23468c3d57a3843366f085cae04304b6405ddf43` ✔ |
| BEFORE lacked listeners (H1) | capture: 0/0 on BEFORE R0, W1, W2, W3 (held). My rebuild: 0/0, empty follow-add log. AFTER: 4/1 (scroll, wheel, touchmove, pointerdown + keydown), all adds after release (rebuild: adds at 12881.9 ms perf > release at 12447.8 ms) ✔ |
| Snap caused by missing listeners, not by the fixture | Same build, same fixture, same overlay. BEFORE **with** the hold → 0/0 and snap (R0, W1, W2). BEFORE **without** the hold (W5) → 4/1 and an exact hold (gaps 339/339/395/395/395/450, scrollTop Δ 0, anchor P084). The only variable that flips the outcome on BEFORE is whether the listeners attached ✔ |
| Node | servers v26.12.1 (repo-pinned via vite-plus) on both builds; driver v24.22.1 on both. Symmetric, no effect ✔ |

## 1. Row-by-row deviations

| row | as specified? | deviation | weakens? |
|---|---|---|---|
| R0 gate | yes: 0/0, 75 frames, loading seen, S_up 339 = 0.40 ch, gap 0 at S1/S2/S3 | none | no |
| H1 | yes: BEFORE 0/0 vs AFTER 4/1, frames 72/79 ≥ 60, loading seen on both | hold measured 3.0–3.2 s instead of 2.5 s (extra `page.evaluate` in the handler). Same on both builds, and longer only makes the hold stricter | no |
| W1 | yes, numbers match matrix criteria: BEFORE gap ≤ 40 at all 6 phases, pill never; AFTER scrollTop Δ 0, anchor P084 ΔanchorTop 0, gap 339→395→450 = ΣΔ (56+55) exactly, pill true | tool phases add 0 px (see C1); W1's "Δ > 0" phases are `p1`/`p2` only. The BEFORE snap at `t1-start`/`t1-finish` (Δ 0) still demonstrates the data-change re-pin | slightly (C1 framing only) |
| W2 | yes: activeElement BODY on every press, no PageUp, control follow OK, gate 360 = 0.42 ch; BEFORE snaps, AFTER holds 360→416 | none | no |
| W3 | yes: gap 0 after all 10 phases on both builds, pill never | ran with the hold (as matrix requires) | no |
| W4 | yes: BEFORE wheel path (pill never appears, matching corrections §0.1), AFTER pill click → gap 0 and re-follows through `resumed` | none | no |
| W5 | yes, valid: BEFORE 4/1 without a hold, both hold | mount took 394 frames even without a hold, yet the listeners attached. This is consistent with main's retry loop being keyed on hydration, not on navigation | no |
| P-probe | Δ bounds met for `p*` / `resumed` | ran with the hold on (matrix says no hold); W5 (no hold) reproduces the same Δ values (56/55), so this is moot. Tool Δ = 0 (C1) | no |
| S0 | transcript present | not re-audited in depth (budget) | — |
| R-opt | skipped (optional) | — | no (optional) |
| Stills | see §5 | one home per build means the sidebar differs; 390 opened by direct URL, not the thread switcher | no for timeline; the stills prove only "no at-rest visual change in the main pane" |
| All rows | — | **one home per build** instead of per row. My W1 rebuild used a brand-new home per build and gave identical numbers, so for W1 this is neutralized. For the other rows the shared home only adds sidebar threads | no |

## 2. Narration vs recordings (frames extracted with ffmpeg at narrated times)

Contact sheet: `/tmp/ev-22788/audit2/frames/sheet.png`.

| clip | video t | narrated state | on screen | match |
|---|---|---|---|---|
| before-R0 | 17.27 | after wheel, gap 339, gate ok | overlay "after wheel: gap 339 px (0.40 ch) gate ok", top paragraph P084, no pill | ✔ |
| before-R0 | 18.2 | after t1-start: snapped | overlay "release t1-start", top P090, last row `Unsupported ACP update: tool_call` at the bottom, no pill | ✔ (snap visible) |
| before-R0 | 20.5 | after p1: snapped | overlay "release p1", P101 + `Tool` row at bottom | ✔ |
| before-W1 | 16.1 | after wheel | P084 top, no pill | ✔ |
| before-W1 | 17.1 | after t1-start: gap 0 | P090 top, bottom of thread visible | ✔ |
| before-W1 | 22.3 | after p2: gap 0 | P092 top, P102 + `Tool` row at bottom | ✔ |
| after-W1 | 14.42 | after wheel, pill | P084 top, `Scroll to end` pill | ✔ |
| after-W1 | 15.4 | after t1-start: held | P084 top, pill | ✔ |
| after-W1 | 20.2 | after p2: held (gap 450) | P084 top, pill | ✔ |
| after-W1 | 21.3 | W4 pill click → end | P092 top, P102 + `Tool` row at bottom, no pill | ✔ |

All ten frames match the narration and result.json.

## 4. Independent rebuild: W1 on both builds

My own dev servers (`node scripts/dev-runner.ts dev --home-dir /tmp/ev-22788/homes/audit2-<build>`, T3CODE_* unset), fresh homes, a fresh workspace `/tmp/ev-22788/ws/audit2-harbor`, a fresh barrier root `/tmp/ev-22788/barriers/audit2-<build>`, and a fresh pairing. The driver is the verified `49c57bfd…` with only three path constants changed (state file, seed script, out dir → `audit2/`); the seed script changed only its project ID, home, and barrier root. Diffs are recorded in the audit log. Output: `/tmp/ev-22788/audit2/out/{before,after}-W1/` (video + result.json).

| sample | capturer BEFORE gap | **audit2 BEFORE gap** | capturer AFTER gap / scrollTop | **audit2 AFTER gap / scrollTop** |
|---|---|---|---|---|
| listeners | 0/0 | **0/0** | 4/1 | **4/1** |
| hold ms / frames / loading | 3200 / 72 / T | **3125 / 81 / T** | 3045 / 79 / T | **3013 / 78 / T** |
| S_up | 339 (pill F) | **339 (pill F)** | 339 / 4806 (pill T) | **339 / 4806 (pill T)** |
| t1-start | 0 | **0** | 339 / 4806 | **339 / 4806** |
| t1-finish | 0 | **0** | 339 / 4806 | **339 / 4806** |
| p1 | 0 | **0** | 395 / 4806 | **395 / 4806** |
| t2-start | 0 | **0** | 395 / 4806 | **395 / 4806** |
| t2-finish | 0 | **0** | 395 / 4806 | **395 / 4806** |
| p2 | 0 | **0** | 450 / 4806 | **450 / 4806** |
| W4 path → gap | wheel → 0 | **wheel → 0** | pill → 0 | **pill → 0** |
| t3-start…resumed | 0,0,0,0 | **0,0,0,0** | 0,0,0,0 | **0,0,0,0** |
| anchor (AFTER pre-resume) | — | — | P084, ΔanchorTop 0 | **P084, ΔanchorTop 0** |

Exact reproduction. Every process I started was stopped: two dev-runner PIDs, then their eight children, identified by PPID chain plus `T3CODE_HOME=/tmp/ev-22788/homes/audit2-*` and killed by explicit PID (recorded in `pids/audit2-*.pid`). The ports are free and no process with an audit2 environment remains.

## 5. Stills

Recomputed with PIL:
- 390×844 light/dark: PNGs are **byte-identical** (sha256 `21861317…` light, `a5d579fb…` dark on both builds); diff bbox None, 0 px ✔
- 1440×900 light: 7946 non-zero px, bbox (18, 364, 237, 860) exclusive; dark 7877 px, same bbox. Entirely inside the sidebar (x < 256). Main pane 0 px ✔ The manifest's [18, 364, 236, 858] is the inclusive form of the same box. Explained by the differing sidebar thread lists (one home per build).
- All stills: gap 0, pill false, same scrollHeight per size on both builds.

## Caveats found by this audit

**C1 (material, symmetric): the fixture's tool calls are rejected as unsupported on both builds.** Both capture DBs (read-only) persist the tool rows as `"title":"Unsupported ACP update: tool_call","type":"dynamic_tool","input":{}` (BEFORE 7 items, AFTER 5 items; my audit2 homes have 1 each). No item titled "Check the harbor log N" exists in any home. The ACP decoder routes the fixture's `tool_call` to `_t3_unknown` → `UnknownUpdate` → `adapter.ts:5082` synthetic tool. I did not find the schema field that fails (budget). Consequences:
- This, not row derivation, is most likely why the tool phases add 0 px. The pass-1 matrix's §0.2 explanation is unverified.
- The "tool boundary" rows exercise a dynamic-tool row's start/complete data change, not a real `execute` tool with title/command. The listener mechanism is content-agnostic and the AFTER hold also covers real prose growth (`p1`, `p2`), so the fix is still demonstrated. But the PR body should say "streamed tool rows and prose" or the fixture should be fixed to emit a schema-valid `tool_call` before claiming real tool calls.

**C2 (cosmetic, unexplained):** the running row's label differs while a tool is live. The BEFORE probe and video show `Unsupported ACP update: tool_call`; the AFTER probe shows `Tool`, and both show `Tool` later. Persisted payloads are identical, and the PR's web diff (ref callback + `onScrollNodeChange`) doesn't touch labels, so this is most likely live-row label timing (title vs fallback while streaming). It doesn't affect any height or gap measurement (Δ identical on both builds).

**C3 (minor):** the P-probe ran with the hold; the 390 stills opened by direct URL; one home per build; the hold lasted ~3.0–3.2 s rather than 2.5 s. None of these changes a verdict: W5 covers no-hold Δ, the rebuild covers fresh homes, and the longer hold applies to both builds.

## Verdict

All required rows are captured on both builds with the matrix's numeric criteria met. The narration matches the frames, the SHAs and overlays are verified, the causality is isolated by W5, and W1 reproduces exactly in an independent rebuild. The evidence is sufficient to prove the fix (listeners attach after a late scroll-node mount, and the reader's position holds through streamed rows), provided the PR evidence wording reflects C1.

VERDICT: SUFFICIENT
