|
| 1 | +# Batch signoff — stream-swap-follow |
| 2 | + |
| 3 | +**Status: CLOSED** · Signed off 2026-07-09 · Both plans PASS, merged to `main`. |
| 4 | + |
| 5 | +This batch is complete and moved to `.agents/.plans-closed/`. No plan in it |
| 6 | +remains executable; the documents are retained as the record of what was done, |
| 7 | +how it was verified, and — critically for this batch — which reported symptom |
| 8 | +is **not** closed by it (see "What this closure does not claim"). |
| 9 | + |
| 10 | +## Plans |
| 11 | + |
| 12 | +| Plan | Title | Verdict | PR | Merged to main | |
| 13 | +| ---- | ------------------------------------------------------------- | --------------------------------- | ---------------------------------------------------------------- | ------------------------------- | |
| 14 | +| 001 | Keep follow-bottom locked when a streamed message is replaced | PASS (operator-approved, amended) | [#54](https://github.com/humanspeak/svelte-virtual-chat/pull/54) | `46ed850` (v0.1.17, 2026-07-09) | |
| 15 | +| 002 | CSS scroll anchoring as a pre-paint follow-bottom guarantee | PASS | [#55](https://github.com/humanspeak/svelte-virtual-chat/pull/55) | `23538db` (v0.1.18, 2026-07-09) | |
| 16 | + |
| 17 | +## What the batch delivered |
| 18 | + |
| 19 | +Consumer apps that stream an assistant reply into a placeholder and then swap |
| 20 | +in the final document (new id, sometimes remove-then-add) no longer lose |
| 21 | +follow-bottom: |
| 22 | + |
| 23 | +- **001** fixed the message-identity symptoms: height carry-over, in-place |
| 24 | + identity invalidation (`messageShape`), shrink→grow smooth suppression. |
| 25 | +- **002** fixed the structural cause: no JS-timing hook (rAF, ResizeObserver, |
| 26 | + microtask) can guarantee a pre-paint `scrollTop` write, so the fix moved |
| 27 | + into layout — CSS scroll anchoring on the viewport with a bottom sentinel, |
| 28 | + content opt-out, a derived sentinel height (webkit border-top law), and a |
| 29 | + bounded tail-swap height reserve in `chatTailSwapCarry.ts`. |
| 30 | +- Verified as **fixes, not probability reductions**: guard measured the |
| 31 | + user-visible defect directly — 3/25 chromium regrow runs painted a real |
| 32 | + off-bottom frame (worst 408px) without 002's change, 0/25 with it; the |
| 33 | + previously 1-in-10-failing variants repeat-sampled 0-in-30 across webkit, |
| 34 | + mobile-safari, and mobile-chrome. Full browser matrix green at close-out. |
| 35 | + |
| 36 | +## Gate history (full detail in the per-plan `.guard.md` / `.guard-report.md`) |
| 37 | + |
| 38 | +This batch earned its PASS the hard way, and the record should not smooth |
| 39 | +that over: |
| 40 | + |
| 41 | +- **001** initially NO-PASS at `final` (STOP line-budget exceeded, unplanned |
| 42 | + strategy). Operator reviewed and accepted; the plan was amended to authorize |
| 43 | + the work. A post-close-out `trunk check` gap surfaced 3 lint issues, fixed |
| 44 | + with operator authorization. |
| 45 | +- **The batch was reopened 2026-07-09**: 001's single-run-per-browser gate had |
| 46 | + let a ~1-in-20 chromium race through. Measured (`--repeat-each=25`), not |
| 47 | + inferred. That lesson — repeat-sample or a green run means little — is |
| 48 | + institutionalized in 002's done criteria. |
| 49 | +- **002** went through two NO-PASS checkpoints (a forbidden JS-timing hook; a |
| 50 | + scope violation that would have permanently leaked phantom height on tail |
| 51 | + deletion) before passing. Three operator-approved amendments, all |
| 52 | + strengthening; no criterion or STOP was ever weakened. |
| 53 | + |
| 54 | +## What this closure does not claim |
| 55 | + |
| 56 | +**The persistent-strand question stays open.** No failing run in either plan |
| 57 | +ever showed `following=false` or a settled `finalGap > 2` — everything this |
| 58 | +batch fixed was a _transient_. The originally reported real-world Firefox |
| 59 | +symptom (a viewport that stays stranded off-bottom) was never reproduced and |
| 60 | +remains unexplained. Do not close that report on the strength of this batch. |
| 61 | +The tracer for chasing it is committed at `tests/chat/regrow-trace.debug.ts`. |
| 62 | + |
| 63 | +## Residual follow-ups carried forward (none block closure) |
| 64 | + |
| 65 | +- **Highest value: commit a regression test for the tail-deletion leak.** |
| 66 | + Guard proved the fix with a throwaway fixture (permanent tail delete → |
| 67 | + reserve clears after 250ms) and deleted it; nothing in the repo would catch |
| 68 | + a reintroduction. The component's 250ms clear timer is untested. |
| 69 | +- **`TAIL_SWAP_RESERVE_MS = 250` is a magic number.** A store slower than |
| 70 | + 250ms to deliver the replacement reopens the original race. Derive the |
| 71 | + window or document the assumption. |
| 72 | +- **Anchor selection is the load-bearing invariant.** Any new element inside |
| 73 | + the viewport without `overflow-anchor: none` can steal anchor selection and |
| 74 | + silently reintroduce the bug. Reviewers must check every new viewport child. |
| 75 | +- **webkit + `viewportClass` border-top hazard** deserves a line in the prop |
| 76 | + docs — a top border taller than the sentinel disables anchoring in Safari. |
| 77 | +- **`same-id` and `new-id` variants are still single-sampled** in the stress |
| 78 | + suite; twice on this batch a real bug hid behind a single green run. |
| 79 | +- Orphaned height-cache entry pruning (memory growth over long sessions) — |
| 80 | + deferred from 001, still real, still independent. |
0 commit comments