Skip to content

Stabilize main: consolidate WebKit flake and bottom-follow fixes - #1039

Merged
loganj merged 17 commits into
mainfrom
carl/stabilize-main
Oct 14, 2026
Merged

loganj merged 17 commits into
mainfrom
carl/stabilize-main

Conversation

@wesbillman

@wesbillman wesbillman commented Oct 14, 2026 •

Copy link
Copy Markdown
Collaborator

main has been red on WebKit browser journeys for hours, and the per-test fix PRs kept tripping over each other. This PR replaces all of them. It is the eight existing fixes cherry-picked onto main (authorship and sign-offs preserved), plus the browser case from the #1022 P1.

Replaces Fix
#1021 composer-attachments:562 eager-upload playback and removal layout (test)
#1024 live:234 pause the page clock before the roster cooldown arms (test)
#1029 agent-activity:872 refresh thread typing before the keyboard section (test)
#1032 voice-notes:385 drop in-flight media route reads at teardown (test)
#1033 sidebar-unread:180 release the held alpha EOSE on its first head request, before the 10s live-setup deadline (test; #1033's row-visibility wait still failed 4 of ~120 flake repetitions here)
#1034 inbox-refresh-layout:17 a withdrawn read observation is a cancellation, not a sync error (app)
#1035 Flake check shards its repetitions, so whole-file selections fit the 15 min timeout (CI)
#1022 layout:574 list-height corrections no longer cancel bottom follow, including a correction scroll delivered before the restore frame (app; #1022 alone still failed 121px from bottom here)

#1022 P1

The simultaneous shrink-above / grow-below case is now a permanent journey in layout.spec.mjs. Locally it fails on main (3/3 Chromium, stuck 120px from bottom) and passes with this PR's ChannelTimeline fix (3/3 Chromium + 3/3 WebKit). The full layout.spec.mjs passes 38/38 across both engines.

Validation

  • src/features/messages/ vitest: 1611/1611, including a new reverse-order correction test that fails without the fix
  • unread.test.ts (read-state): passing
  • layout.spec.mjs:619 panel resizing: 15/15 WebKit under 6 workers; sidebar-unread.spec.mjs:180: 30/30 under 6 workers
  • tests/integration/flake-check.test.mjs: 9/9
  • Each component PR's own validation is recorded on that PR
  • Hosted CI on this head: pending

Product behavior changes to confirm: the Inbox no longer shows "Reading observation expired · Retry inbox" after leaving a channel mid-read, and the timeline stays pinned to the bottom when rows above and below change size together.

🤖 Generated with Claude Code

wesbillman and others added 10 commits October 13, 2026 18:32
Co-authored-by: Mordecai <f314b6033b6e13df94c8e9e1187d3666b6b9ce54e66ee986e31cdaa909e8dcbc@buzz.block.builderlab.xyz>
Signed-off-by: Wes <[email protected]>
(cherry picked from commit 9e65023)
Co-authored-by: Mordecai <f314b6033b6e13df94c8e9e1187d3666b6b9ce54e66ee986e31cdaa909e8dcbc@buzz.block.builderlab.xyz>
Signed-off-by: Wes <[email protected]>
(cherry picked from commit c4dba54)
The "Live retry recovers an empty paused roster" journey asserts that Retry
clicks inside the roster quota cooldown send nothing, but it installed a
page clock that keeps running in real time. The app gates the roster read
on performance.now() against a 3s deadline it arms when it consumes the
refusal, so a slow WebKit runner reached the second "ignored" click after
the cooldown had really expired and correctly sent a second roster read.

Hold the first roster read at the page boundary until navigation (which
needs a running clock) is done, pause the page clock, then release it so
the app arms its deadline in controlled time. Only crossCooldown's runFor
now passes that deadline.

Co-authored-by: Wes <[email protected]>
Signed-off-by: Wes <[email protected]>
Co-authored-by: Princess Donut <5d97ac8c272fa949af56c71e586146b1706e5c4c3daa125db5b4625e12e0686c@buzz.block.builderlab.xyz>
(cherry picked from commit ed128d8)
Thread activity in this test is lit only by typing, which stays fresh
for 8s. The detour through the activity panel and back to the thread
can take longer than that on slow WebKit runners, so the thread trigger
was gone by the time the test focused it. Refresh typing before using
the trigger, as each later section of the test already does.

Co-authored-by: Carl <32a2e2c9d428ee08902cab75d956da2c1d235a22d4766b0dd4138bf6e2e5db1d@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Wes <[email protected]>
(cherry picked from commit e9bb156)
"rejects a full attachment draft before opening the microphone" failed on
WebKit with `"page.evaluate: Test ended." while running route callback`.
WebKit defers the voice card's media reads until the first click and keeps
issuing them, and the shared media route reads its bytes from the page with
page.evaluate. This test ends about 200ms after its only click, so the
context could close under a media read still in flight.

Unroute in afterEach with `ignoreErrors`: those reads serve no assertion
once the test body has finished.

Co-authored-by: Wes <[email protected]>
Signed-off-by: Wes <[email protected]>
Co-authored-by: Princess Donut <5d97ac8c272fa949af56c71e586146b1706e5c4c3daa125db5b4625e12e0686c@buzz.block.builderlab.xyz>
(cherry picked from commit 2607d39)
Co-authored-by: Mordecai <f314b6033b6e13df94c8e9e1187d3666b6b9ce54e66ee986e31cdaa909e8dcbc@buzz.block.builderlab.xyz>
Signed-off-by: Wes <[email protected]>
(cherry picked from commit 307debe)
A helper change makes the selector repeat every journey in the file. For
layout.spec.mjs that is 18 journeys x 10, which ran past the 15-minute job
timeout on WebKit (#1022: 148/170 passed, 0 failed, then cancelled).

The selector now emits one shard per five journeys (at most eight), and the
lane spreads the repetitions with --shard. Small selections keep one runner.

Co-authored-by: Carl <32a2e2c9d428ee08902cab75d956da2c1d235a22d4766b0dd4138bf6e2e5db1d@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Wes <[email protected]>
(cherry picked from commit 6a85f66)
…not a sync error

Leaving a channel while its catch-up save is queued invalidates the save.
That rejection set the global read-state health to error, so Inbox showed
"Reading observation expired" with Retry inbox, and rows shifted 56px. CI
caught it as an inbox-refresh-layout flake on slow WebKit runners.

The caller still receives the rejection; explicit Inbox actions keep their
own alert.

Co-authored-by: Carl <32a2e2c9d428ee08902cab75d956da2c1d235a22d4766b0dd4138bf6e2e5db1d@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Wes <[email protected]>
(cherry picked from commit 63f2a1a)
Treat position samples taken from Virtua list-height commits as layout-owned so an upward virtualizer correction cannot masquerade as reader input. Keep explicit upward gestures authoritative, and cover the panel-resize ordering at both the component and browser boundaries.

Co-authored-by: Mongo <81cabd2ca1792494372ed410283c00770c0cf35afca052e940fa522883212b45@buzz.block.builderlab.xyz>

Signed-off-by: Wes <[email protected]>
(cherry picked from commit 85ea95ec179d77bb7ec48c8d2a06ef461370c894)
… bottom

Wes's #1022 P1 case. It fails 120px from the bottom on main in both
engines and passes with the ChannelTimeline bottom-follow fix.

Co-authored-by: Carl <32a2e2c9d428ee08902cab75d956da2c1d235a22d4766b0dd4138bf6e2e5db1d@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Wes <[email protected]>
wesbillman and others added 2 commits October 13, 2026 18:52
…not on row visibility

Waiting for the alpha row before releasing still let the held setup cross
the app's real 10s live-setup deadline on loaded WebKit runners: #1039's
flake check failed 4 of the first ~120 repetitions at that wait. The
first head request is the boundary the hold controls, so release there.

Co-authored-by: Carl <32a2e2c9d428ee08902cab75d956da2c1d235a22d4766b0dd4138bf6e2e5db1d@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Wes <[email protected]>
…fore its restore frame

#1039's CI still failed the panel-resizing journey 121px from bottom on
WebKit: after a list-height commit, the browser delivered Virtua's 1px
offset correction as a scroll event before the restore frame, and
onScroll read it as reader movement. Treat scrolls in that window as
layout-owned until the restore frame or the next reader input.

Co-authored-by: Carl <32a2e2c9d428ee08902cab75d956da2c1d235a22d4766b0dd4138bf6e2e5db1d@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Wes <[email protected]>

@kalvinnchau kalvinnchau left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 no findings in the code review at 9a1e69e. reviewed the complete diff, read-save cancellation through storage/callers, timeline correction and reader-input paths, browser test synchronization, and flake-shard wiring.

minimalness: 10/10. elegance: 9/10 — correction ownership necessarily spans the mutation observer, scroll handler, and animation frame. correctness: 9/10 — no concrete defects found, but browser ordering and shard execution remain unverified at runtime.

code-only review using the adversarial-code-review skill; no tests run or hosted CI results assessed.

wesbillman and others added 5 commits October 13, 2026 19:03
…tent

Review follow-up: only open the window when the height commit was
recorded for the current intent, so a reader drag that started first
still cancels follow, and keep it across an effect re-run until the
next effect's first frame so a correction scroll in between is still
layout-owned.

Co-authored-by: Carl <32a2e2c9d428ee08902cab75d956da2c1d235a22d4766b0dd4138bf6e2e5db1d@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Wes <[email protected]>
…to the first slot

The removed photo tile leaves the accessibility tree when its exit
starts but keeps its slot until the exit ends; the clip then slides
left. A click straddling that slide pressed the button and released
beside it. A forced 300ms press failed 6/6 before and passed 6/6 after.

Co-authored-by: Carl <32a2e2c9d428ee08902cab75d956da2c1d235a22d4766b0dd4138bf6e2e5db1d@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Wes <[email protected]>
…enu journey

"row menu moves and removes a channel optimistically…" failed on WebKit at
openSort: the Sort menu item never appeared after clicking "More actions for
Channels". The click came right after a reload, while the sidebar still
showed "Loading Me placement…". Base UI opens the menu on mousedown and a
document mouseup outside the trigger and popup cancels it; WebKit took
~500ms between press and release, the placement status left during it, the
sidebar shifted up, and the release landed off the trigger.

Open the menu from the keyboard, like openMove already does, and wait for
Base UI to highlight Sort before focusing it. Pointer opening of the section
menu stays covered by navigation-sorting.

Co-authored-by: Wes <[email protected]>
Signed-off-by: Wes <[email protected]>
Co-authored-by: Princess Donut <5d97ac8c272fa949af56c71e586146b1706e5c4c3daa125db5b4625e12e0686c@buzz.block.builderlab.xyz>
(cherry picked from commit fef8a23)
…imeline positions it

On loaded Linux WebKit the timeline's own end alignment can land short
of the DOM bottom while rows re-measure, and onScroll read that
programmatic upward scroll as the reader leaving the bottom. Keep the
timeline-owned position until the next reader input; a held pointer
(scrollbar drag) still lets geometry infer upward movement.

Instrumented in the pinned Linux Playwright image at 1.5 CPUs, the
panel-resizing journey failed 5-7 of 30 before this change and 30/30
passed after.

Co-authored-by: Carl <32a2e2c9d428ee08902cab75d956da2c1d235a22d4766b0dd4138bf6e2e5db1d@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Wes <[email protected]>
…text

#828's SVG <text> numeral made every interaction near day dividers
about 1.5x slower on Linux WebKit; the crowded nested-replies journey
doubled in CI and pushed WebKit shard 4/8 past its 15 minute limit on
main. Keep the SVG tile and lay an HTML numeral over it. Before/after
WebKit screenshots of the divider match.

Co-authored-by: Carl <32a2e2c9d428ee08902cab75d956da2c1d235a22d4766b0dd4138bf6e2e5db1d@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Wes <[email protected]>

@wpfleger96 wpfleger96 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Thanks for pulling these together. Blocking: two correctness issues in the bottom-follow change at cb5ca76e2a42d993a26ddf2ef0ab8bc83f9d3b48, both from 3c92ca64d ("only reader input can cancel bottom follow"). Everything else in the 4 commits since 8d14bee6 looks right to me.

1. A real upward scroll can stay classified as layout correction indefinitely

  • layoutCorrection is now set on every bottom alignment the timeline does (ChannelTimeline.tsx:366, :570, :613) and cleared only in gesture() (:696). The restore frame no longer clears it, so for a reader who is following the bottom, the flag stays on until their next wheel/key/touch/pointer event on the section.
  • While it's on, recordPosition gets layoutOwned=true (:776-779), so movedUp needs an upward gesture (:322-329) and the sample forces bottom=true (:334-335), even hundreds of px from the bottom. The next append or row-height change end-aligns again.
  • That drops the geometry fallback main has for upward scrolls that don't come through those handlers: screen-reader navigation, scrollIntoView, find-in-page. Those readers get snapped back to the bottom on the next message.
  • Fix: scope the exemption to outstanding positioning/correction work (e.g. clear it once the timeline's own alignment has landed or its scrollend fires, rather than at the next reader input) so a settled upward move without an input event is still detected. Add a regression: settle at bottom, scroll up with no input event, then append, and separately grow a row.

2. The held-pointer guard is bypassed by the height observer

  • onScroll honors pointerHeld, but the observer still calls recordPosition(scroller.current, true) (:612). Ordering that still fails: pointer down, append re-arms the observer, the drag moves scrollTop up, and a row-height commit lands before the scroll event (the comment at :583 notes input can move the DOM before its scroll event). The observer sample keeps bottom=true and overwrites measuredPosition.offset with the dragged offset, so the later pointer-aware onScroll sees no upward delta and the queued restore snaps back.
  • Same for release-before-scroll: onPointerUp (:745-747) clears the held state without sampling, so a re-armed correction can hide that delta too.
  • Fix: make the observer sample pointer-aware as well (!pointerHeld.current at :612) and cover the observer-before-scroll order. The new case at ChannelTimeline.test.tsx:1005-1022 releases the pointer before the correction, so it doesn't exercise the guard; the reversed order needs its own test.

Optional

  • pointerHeld only clears on pointerup/pointercancel on the section (:745-750). Mouse pointers have no implicit capture, so releasing a drag outside the timeline leaves it true until the next click in the timeline, which puts that reader back on the pre-commit WebKit short-end-align behavior. A window-level pointerup (or pointer capture) would close it.
  • Day divider (MessageTimestamp.tsx): the numeral's color and fontSize are inline while .dayCalendar [data-calendar-date] already exists in Messages.module.css; moving them there keeps the styling in one place. Footprint, decorative hiding, and the accessible day label all hold.
  • The description doesn't cover 3c92ca64d (reader-input-only cancel) or cb5ca76e2 (day divider numeral), and still says hosted CI is pending. No buzz-review-completed yet.

CI: the re-run on this head (attempt 2) failed WebKit 4/8 on notifications.spec.mjs:327 (mention still unread after 10s, line 424); attempt 1 passed. I haven't tied it to this PR, and it isn't in main's recent WebKit failures. Re-running it.

@loganj
loganj merged commit 52cdd38 into main Oct 14, 2026
107 of 111 checks passed
@loganj
loganj deleted the carl/stabilize-main branch October 14, 2026 13:55
loganj added a commit that referenced this pull request Oct 14, 2026
… clock

Main (#1039) now pauses the page clock before the roster refusal so the
ignored Retry clicks land inside the cooldown. Automatic recovery shows its
notice with the first retry, 1 s after the refusal, so run the paused clock
that far before expecting the notice.

Cross the cooldown in the broker's real time first, then on the page clock:
the queued automatic retry sends as soon as the page deadline passes, and a
second refusal would arm a deadline the paused clock never reaches. Listen for
the observer update before crossing, because automatic recovery can read the
roster during the crossing.

Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
Signed-off-by: Logan Johnson <[email protected]>
johnmatthewtennant added a commit that referenced this pull request Oct 14, 2026
* commit '506ac5d4d':
  Fix JPEG uploads and attachment control overlap (#1068)
  feat(channels): read, write and look up channel labels (#964)
  refactor(agents): separate the agent runner and its reads from the desktop app (#961)
  fix(media): show desktop animated avatars and match dev broker media rules to native (#990)
  ci(browser): split journeys across nine shards per engine (#989)
  test(inbox): open the edit journey's conversation by name (#997)
  test(voice-notes): keep each attachment's own type in the upload fixture (#999)
  Stabilize main: consolidate WebKit flake and bottom-follow fixes (#1039)
  Push main-pane threads in and hold threads behind a loading mark (#1018)
  [buzz-app] Show full sent images and GIFs (#886)
  feat(agents): say the agents this copy runs are online (#862)
  [buzz-app] Keep the sidebar in place when switching to Personal space (#945)
  fix(builderlab): link the Buzz key before attesting remote agents (#978)
  feat(agents): show agents typing to everyone while they work on a mention (#998)

Signed-off-by: John Tennant <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants