useVirtualHistory set up its useSyncExternalStore subscription during
the first render, when scrollRef.current was still null (the ScrollBox
ref attaches during commit, after render). Its useCallback for
subscribe had a stable scrollRef identity as its only dep, so it never
re-subscribed once the ref actually attached — the hook stayed stuck
with vp=0, top=0, no scroll subscription. Small sessions fit entirely
in cold-start so you didn't notice; big /resume sessions got sliced to
the last 40 items with a huge topSpacer and the viewport sat on empty
space until some unrelated state change (e.g. a keystroke) re-rendered
and finally read a real vp.
- flip a hasScrollRef flag in useLayoutEffect once the ref attaches and
add it to the subscribe useCallback deps so useSyncExternalStore
rearms with a real subscription
- on resume, scrollToBottom() after history hydrates so the ScrollBox
lands at the newest messages instead of scrollTop=0 (stickyScroll
doesn't auto-engage on the initial empty→full dump)