github feder-cr/AIHawk v0.68.4
0.68.4

3 hours ago

It reads as "the transcript stops scrolling after a while". It never scrolled. For the first 24 rows the transcript is shorter than the window, so there is nothing to scroll and every distance to the bottom is zero by construction. From there the view falls behind by exactly the height that arrives, 623 px per 20 rows, for as long as the agent keeps working.

Two separate things were wrong, and the second one only became visible once the first was fixed.

The page had two answers to "is the reader following". settleOnce() scrolled once, 150 ms after the first event, and set a latch so nothing would scroll again. At 150 ms the transcript is empty, so that one scroll had nothing to do. Meanwhile an IntersectionObserver knew the answer continuously and was used only to show a button. The latch drove the scrolling. The rest was left to #anchor and overflow-anchor:auto, which can only hold a bottom something else has already reached, because the browser will not choose an anchor that is off screen. The CSS carried a comment claiming "bottom-pinning with no scroll handler"; that was false and is corrected here.

The alternative reading was that anchoring is suppressed at scroll offset zero, so it was measured: with the scroller one pixel from the top, 374 px of rows arrived and it moved by zero. One pixel is not zero, so the offset is not the reason.

A scroll scheduled while appending runs before the layout of what was appended. With the latch gone and put scrolling on a frame, a live run followed perfectly and a reopened conversation still opened at the top, three times out of three, 2084 px from the bottom. Instrumented from inside that callback: the DOM already held all 65 rows and the scroller still reported a height of 808, its own window. It scrolled to 808, which clamps to zero. No choice of target fixes that, because scrollIntoView on the sentinel reads the same layout, and checking whether the scroll arrived does not either, because the check reads that layout too and concludes it did.

So there is one fact and one trigger. The fact is the observer, which knows continuously whether the reader is following. The trigger is a ResizeObserver on the transcript, delivered after layout, which is the only moment the bottom is knowable and which also covers every other way the transcript can grow: a picture that loads, a font that swaps, a row expanded, the window resized. put no longer scrolls. The latch, the timer and the two variables behind them are gone rather than repaired, and a tombstone says why.

Five tests, which execute the real page code in node under a shim rather than scanning it. The shim is half the gate, because two earlier versions passed while lying. It counts the scrollTop setter instead of its own ticks, since a page that scrolled sixty times inside one tick reported one and passed; what is promised is one scroll per arrival, so scrolls are what is counted. It clamps like a real scroller, or toBottom would leave the offset past the end and every distance would read negative. And it delivers the two observations in the order a browser delivers them, the resize after layout and the intersection after that, so a page that cannot work in a browser cannot pass here.

The other half is the regression this file exists to prevent. The old design bought "never yank a reader who scrolled up" by never scrolling at all, and a fix that follows the bottom and drags the reader back every time a row lands would be worse than the defect it replaces. So the "left alone" half is asserted twice in a row: a latch passes it once and never again, which is exactly the shape of what is being removed.

Six known-bad mutations, all killed: grew scrolling never, grew ignoring the reader, grew with the condition inverted, the scroll moved back into put, toBottom going to the top, and nothing counting what arrived.

Verified in a real browser, not only under the shim: a 65-row conversation reopens at the bottom three times out of three, the view follows for a whole agent run, and a reader who scrolls up is left alone twice in a row.

The scan terms took three tries to get narrow enough, which is this repository's most repeated gate defect taken from the side where it accuses rather than passes. pinned on its own accuses stage.pinned, the pane the person chose to watch. requestAnimationFrame over the whole page accuses fitOrOpen, which uses one to batch layout reads and has nothing to do with scrolling. And settleOnce was satisfied by its own tombstone. So comments come out before the scan, and the frame scheduling is asserted absent from the mechanism, and only there.

One mutation survived and was not a hole: reading the observer after the append instead of before changes nothing, because an IntersectionObserver is asynchronous and the two orderings are equivalent. The fix was to correct the comment that claimed too much, not to weaken the gate.

The third commit is a rename. The gate was written with Italian identifiers and invisible_core.english refuses them; the six mutations were re-run afterwards and all six are still killed.

Verified locally: 714 passed, ruff clean, invisible_core.english clean, check_content.py clean, and the version gate green against the real index.

Don't miss a new AIHawk release

NewReleases is sending notifications on new releases.