Fetch a restore's history in one request, and let RUST_LOG through

Now that a scroll anchor is a seq, the restore knows exactly how far back
it has to reach, so it asks for that span in one request instead of
walking there a page at a time. ai-app-2 suggested it; the arithmetic is
theirs. `read_window` counts lines and a transcript numbers them one per
seq, so the distance to the anchor is the number of events to ask for --
and were seqs ever sparse, that difference is larger than the count,
which overshoots into older history rather than stopping short.

Capped at [RESTORE_PAGE_MAX], and the loop already there is what makes
the cap safe: a span past it comes back in several requests rather than
one, which is what every restore did until now. The bytes are the same
either way -- every row between the anchor and the newest end has to be
loaded for the list to be able to count to it -- so this only trades
round trips against response size.

Measured on a 16,133-event session, restoring to seq 2000 (14,133 events
back, the extreme case): **19 requests before, 5 after.** The realistic
case, a couple of thousand events back, is 4 before and 2 after. At
`--delay 150` the deep one puts the row on screen at 5.7s and the
moderate one at 2.5s, and in both the row does not move once it lands.

Also: `RUST_LOG` did nothing. `with_env_filter("info")` is a fixed
directive that never reads the environment, so the per-request page
diagnostics AGENTS.md tells you to turn on with `RUST_LOG=ai_server=debug`
printed nothing at all -- which reads as the code you are instrumenting
being wrong rather than as the switch being disconnected. It is a
fallback now, so the default is still `info`. Those diagnostics are what
the request counts above were measured with.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
irisandClaude Opus 5 committed 2026-08-30 20:07:03 -04:00
1 parent 6079ac15a2
commit 0dc248b9e4
2 files changed
+43 -5

No files matched your search

@@ -113,6 +113,20 @@ private const val HISTORY_SCREENS = 3
*/
private const val HISTORY_PAGE = 800
/**
* The most events one request of a restore may ask for.
*
* A restore knows exactly how far back it has to reach, so it asks for that in one request rather
* than walking there a page at a time. This bounds the request anyway, because "exactly how far" is
* however far the reader had scrolled and there is no bound on that -- and a single response of
* arbitrary size is the one shape a phone on a slow tunnel handles worst. At roughly 800 bytes an
* event, measured on a real transcript, this is about three megabytes.
*
* Going past it costs another request rather than anything being missed, so the number only trades
* round trips against response size.
*/
private const val RESTORE_PAGE_MAX = 4000
/**
* Which row was asked to hold its top edge, and how tall it was when it last measured.
*
@@ -879,7 +893,7 @@ fun SessionScreen(settings: ServerSettings, summary: SessionSummary, onBack: ()
* Reads `items` rather than `rows`: this runs in a coroutine, and `rows` is the composition's
* value, which does not change under a running one.
*/
suspend fun loadOlderPage(): Boolean {
suspend fun loadOlderPage(limit: Int = HISTORY_PAGE): Boolean {
// The fetch *and* the fold, both off the thread that draws. Only the fetch used to be,
// and the fold is the expensive half: `foldEvent` returns a new list per event, so a page
// of [HISTORY_PAGE] events is that many copies of a list growing to that length -- around
@@ -894,8 +908,7 @@ fun SessionScreen(settings: ServerSettings, summary: SessionSummary, onBack: ()
// `items` read below happens back on the caller's thread, where the write does too.
val page =
withContext(Dispatchers.IO) {
val older =
fetchTranscript(settings, summary.id, before = oldestSeq, limit = HISTORY_PAGE)
val older = fetchTranscript(settings, summary.id, before = oldestSeq, limit = limit)
if (older.isEmpty()) return@withContext null
// Folded oldest-first into a list of their own, then put in front: `foldEvent`
// merges streaming text into the item before it, so replaying an older page
@@ -999,7 +1012,22 @@ fun SessionScreen(settings: ServerSettings, summary: SessionSummary, onBack: ()
// conversation every time an active session was reopened.
var index = indexOfSeq(anchor.seq)
while (moreHistory && (index == null || index >= listItemCount() - 1)) {
if (!loadOlderPage()) break
// The whole span in one request rather than a page at a time. `read_window`
// counts *lines* and a transcript numbers them one per seq, so the distance
// back to the anchor is the number of events to ask for -- and were seqs ever
// sparse, that difference is larger than the count, which overshoots into
// older history rather than stopping short. [HISTORY_PAGE] on top is the
// cushion that keeps the anchor's row off the oldest edge, where it would
// still grow.
//
// Capped, and the loop is what makes the cap safe: a span past it comes back
// in several requests instead of one, which is what this did for every
// restore until now -- thirteen sequential round trips to reopen a session
// somebody had read a little way back into, and a spinner for all of them.
// The bytes are the same either way, since every row between the anchor and
// the newest end has to be there for the list to be able to count to it.
val span = oldestSeq - anchor.seq + HISTORY_PAGE
if (!loadOlderPage(span.coerceIn(1L, RESTORE_PAGE_MAX.toLong()).toInt())) break
index = indexOfSeq(anchor.seq)
}
// Both writes before this coroutine yields, so the list's first measurement is