diff --git a/AGENTS.md b/AGENTS.md index 8432cf9..20c5c03 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -408,6 +408,16 @@ belongs in `~/.claude/TOOLCHAIN.md` or `~/.claude/MACHINE.md` instead. the reader hit the end of what was loaded on every swipe and stood there for a round trip. It is `HISTORY_SCREENS` viewports now, counted from what is actually on screen. +- **A page landing while the history observer was fetching it could spend the + layout change meant to ask for the next one.** The observer collected only + `LazyListState.layoutInfo`; while its collector was suspended in + `loadOlderPage`, a compact page could be composed and laid out without + leaving another change to observe afterward. Codex exposed it because a + page full of calls collapses into one tool group: loading stopped until + expanding that group forced a layout. The observer is also keyed on + `oldestSeq` now, so every successful page restarts it against the settled + layout. A failed page does not advance that cursor and still waits for the + next scroll instead of retrying in a loop. - **Only `fetchTranscript` was off the main thread; the fold was not.** `foldEvent` returns a new list per event, so a page is that many copies of a growing list — fine at 80 events and about 300,000 element copies at 800, diff --git a/app/androidApp/src/main/kotlin/com/example/aiapp/SessionScreen.kt b/app/androidApp/src/main/kotlin/com/example/aiapp/SessionScreen.kt index d144d0e..13bfc91 100644 --- a/app/androidApp/src/main/kotlin/com/example/aiapp/SessionScreen.kt +++ b/app/androidApp/src/main/kotlin/com/example/aiapp/SessionScreen.kt @@ -1041,7 +1041,10 @@ fun SessionScreen( // There is no correction beside this one. Following the newest message is not an effect: the // list is reversed, so an arriving message extends the end the viewport is pinned to. val unitSizes = remember(address) { HashMap() } - LaunchedEffect(listState, moreHistory) { + // A successful page advances `oldestSeq` even when its collapsed rows add too little height to + // produce another layout after this collector returns. Restarting on that cursor makes the + // promised re-check happen; a failed page leaves it unchanged and still waits for a scroll. + LaunchedEffect(listState, moreHistory, oldestSeq) { snapshotFlow { listState.layoutInfo } .collect { info -> val visible = info.visibleItemsInfo