feat(android): resume from the seq, and walk back through the session

The chat screen asked the claw for everything the cache did not hold by
counting events, and a session that ran for weeks cost the whole record
the first time it opened here. The stream now names each frame's seq and
the cache stores it, so a reopening screen sends since=<seq> and receives
only the gap. The cache migrates in place: the old count of events is
exactly the last seq plus one, because the record numbers densely.

A screen with no cache opens on the newest turn and walks back through
the new history route when the reader reaches the top, one whole turn
per page, so the phone does not hold a weeks-long transcript to show the
last few messages. Pages are folded through the same reducer the stream
uses and prepended, since the list runs oldest first: appended, they
would land at the newest end. A reset comment (the record no longer
holds the cursor) drops the mirror, and the tail that follows rebuilds
it. The outcome tag colours a steered turn as an accent rather than a
failure, matching the web UI.
This commit is contained in:
vhaudiquet committed 2026-11-08 18:01:52 +01:00
1 parent 70d9bb0d34
commit fa0e3d4470
7 files changed
+277 -51

No files matched your search

@@ -12,17 +12,39 @@ import rs.rhema.phone.net.ClawEvent
// One session's cached transcript tail: the events this device has
// applied, persisted as they stream. Opening a session seeds the screen
// from here instead of watching the claw's whole replay again, and the
// stream opens with `total` as its `since` cursor so the claw sends only
// from here instead of watching the claw replay the session again, and
// the stream opens with [cursor] as its `since` so the claw sends only
// the events past the cache.
@Serializable
data class CachedSession(
// The session's full event count — the `since` cursor. Can exceed
// [events]: the list keeps only the newest [SESSION_WINDOW] events,
// the count counts from the session's first.
val total: Long = 0,
// The seq of the newest event in [events] — the `since` cursor. -1
// when nothing is cached.
val cursor: Long = -1,
// The seq of the oldest event in [events]: the anchor the walk back
// through /history ends at.
val first: Long = -1,
val events: List<ClawEvent> = emptyList(),
)
// The cursor of a cache written before the stream named seqs: a
// count of the session's events. Read only to migrate one, see
// [resumeCursor]; -1 for every cache written since.
val total: Long = -1,
) {
// A cache from before seqs still resumes exactly: the record numbers
// events densely from zero, so the last one it applied is `total - 1`,
// and the window above it is contiguous. The first open after the
// upgrade costs nothing.
val resumeCursor: Long
get() = if (cursor >= 0) cursor else if (total > 0) total - 1 else -1
val oldestSeq: Long
get() = if (first >= 0) {
first
} else if (events.isNotEmpty() && resumeCursor >= 0) {
resumeCursor - events.size + 1
} else {
-1
}
}
// How many events the cache keeps per session. A resident session runs
// for weeks; a bounded window keeps the DataStore write flat, and a tail