Keep visited transcripts on the phone
Reopening a session downloaded the conversation again, every time, over the tunnel. It now draws from a copy of what the server has already sent and asks for one event to check that copy is still current. Per session, under cacheDir, the server's own event lines in chunks named for the range they cover -- so a coalesced page, whose lines do not say what they cover, still records it. Only the contiguous run ending at the newest chunk is served; a gap is closed by paging through it, bounded by `after` on /transcript so the page stops where the phone's copy starts and can therefore be kept. Nothing is derived and stored: rows are a rendering, and a cache of them would need throwing away on every change to the fold. Nothing here is load-bearing. Missing, evicted, damaged or unwritable all degrade to the cold open this screen did before, and the check before the stream resumes -- one request, one event -- is what stops a replaced or truncated file being spliced onto a copy of a different conversation. What that check cannot see, a line changed mid-file with the tail intact, is what Reload in session settings is for. Measured on the emulator against ui-sandbox, on a 505-event session: reopening it costs one request for one event, including scrolling the whole conversation back; a cold open is two requests and 100 events. A reset after falling 300 behind fetched the gap as four coalesced rows rather than re-fetching 104 events and discarding them. Every chunk was checked line by line against what the server says for the range its name claims, across the reset and the gap-fill. transcript-bench.sh, same viewport content and gestures, before and after: p50 16.9ms both, p90 25.6 -> 23.2ms, p99 33.5 -> 36.7ms, and the transcript's own draw accounting 0.33ms -> 0.32ms with place 0.31ms either way. Within the emulator's noise, which is what a cache must be: it changes what is fetched, not what is drawn. Building it also found that the server handed out the same transcript line two different ways. serde_json's default float parser is not correctly rounded, so a ts written as ...0757 came back from /transcript as ...0755 while the SSE stream sent the original -- invisible on screen, since a ts is drawn as a relative time, and visible here only because the cache compares a line it holds against the server's answer. Fixed with float_roundtrip, with a test that fails the moment it is dropped. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
8881a40919
commit
a802522039
17 files changed
+2140
-74
No files matched your search
@@ -21,6 +21,9 @@
|
||||
//! GET /sessions/{id}/events?after=N SSE: backlog after N, then live
|
||||
//! (a backlog past CATCH_UP_LIMIT arrives as a
|
||||
//! `reset` frame plus the newest window)
|
||||
//! GET /sessions/{id}/transcript a page of history: ?before=N (newest when absent),
|
||||
//! ?limit=N, ?coalesce=true to count rows not deltas,
|
||||
//! ?after=N to floor it at what the caller already holds
|
||||
//! POST /sessions/{id}/message {text, attachmentIds?}
|
||||
//! (starts the process first if it has exited)
|
||||
//! POST /sessions/{id}/unqueue {messageId} -- take back one not read yet
|
||||
@@ -1647,6 +1650,11 @@ struct TranscriptQuery {
|
||||
/// counts events to reach a known seq. See `read_window`.
|
||||
#[serde(default)]
|
||||
coalesce: bool,
|
||||
/// Return nothing at or below this seq; the page stops here instead of at `limit`.
|
||||
/// The phone passes the end of what it already holds, so a page never overlaps it.
|
||||
/// Named to match the SSE route's `after`, and exclusive in the same way.
|
||||
#[serde(default)]
|
||||
after: Option<u64>,
|
||||
}
|
||||
|
||||
fn default_window() -> usize {
|
||||
@@ -1668,6 +1676,7 @@ async fn transcript(
|
||||
let events = crate::session::transcript::read_window(
|
||||
session.transcript_path(),
|
||||
query.before,
|
||||
query.after,
|
||||
query.limit,
|
||||
query.coalesce,
|
||||
)
|
||||
@@ -1679,6 +1688,7 @@ async fn transcript(
|
||||
tracing::debug!(
|
||||
session = %id,
|
||||
before = ?query.before,
|
||||
after = ?query.after,
|
||||
limit = query.limit,
|
||||
got = events.len(),
|
||||
oldest = ?events.first().map(|entry| entry.seq),
|
||||
|
||||
Reference in new issue
Block a user