feat(tui): resolve resume titles through the projection cache
session-title already registers a title projection unit; /resume now reads it instead of scanning logs: live rows from the registry snapshot, persisted rows from the durable checkpoint row (cachedSnapshot, zero I/O), and only rows without a usable checkpoint pay a coldSnapshot — checkpoint plus readFrom tail, written back so the next scan is metadata-only. Cold reads are bounded by the new resumeScanConcurrency config; compositions without the cache fall back to the bounded readTitleSnapshots batch. The TUI overlay mounts the projection registry, storage, and projection-cache rows over the same storages root the web surface uses, so checkpoints serve both.
This commit is contained in:
@@ -12,13 +12,13 @@ Opening the TUI `/resume` selector called `sessionQuery.readSession()` once per
|
||||
|
||||
Selector rows fold nothing but titles, and everything else a row shows comes from metadata:
|
||||
|
||||
- Titles come from the existing public batch `readTitleSnapshots` — one persistence listing, at most `persistedInspectConcurrency` concurrent inspections, per-id failure isolation. This is the selector's only per-log read; a rejected title read degrades to that row's disabled "Unreadable session" fallback.
|
||||
- Titles come from the projection system: `session-title` already registers a `title` unit, so a live row reads the registry snapshot, a persisted row reads the durable checkpoint row (`sessionProjectionCache.cachedSnapshot`, zero I/O), and only a row without a usable checkpoint pays a `coldSnapshot` — checkpoint plus a `readFrom` tail, written back so the next scan is zero-I/O. Cold reads are bounded by the TUI `resumeScanConcurrency` config. A composition without the cache falls back to one bounded `readTitleSnapshots` batch over the logs; either path isolates a per-row failure into the disabled "Unreadable session" fallback.
|
||||
- The activity timestamp never reads a log: a live session uses its last in-memory event time; a persisted session stats the artifact named by the optional `sessionPersistence.locate()` (mtime), falling back to the header's creation time when the backend locates no per-session artifact (SQLite) or the stat fails. Any append moves the mtime, so a mere pickup boundary now floats a browsed session up — accepted as the price of a metadata-only timestamp.
|
||||
- The last-turn label, provider/model route, and goal phase columns are gone from rows. Route availability is now enforced by the Enter-time preflight, which fully reads and replay-validates the one chosen log through `readSession` before handoff.
|
||||
|
||||
The selector overlay opens synchronously when `/resume` dispatches, before the scan settles: an `undefined` candidate set renders a "Loading sessions…" placeholder, the picker owns terminal input from its first frame, Enter reports that sessions are still loading, and Escape cancels. Closing the overlay aborts the scan through the `AbortSignal` the query methods accept; a signal-ignoring backend's late settlement is dropped by a staleness check. The finished scan swaps rows in through `setCandidates` (clearing a stale still-loading error) without replacing the overlay; a queued activation behind a closing predecessor receives an already-scanned set at construction; one catch spans listing, titles, and mtimes, so any scan failure closes the overlay and reports a notice rather than stranding the loading placeholder.
|
||||
|
||||
The change is confined to the TUI package: no session-query or session-persistence surface changed.
|
||||
No session-query or session-persistence surface changed. The shipped TUI composition gains the projection registry, storage, and projection-cache rows (mirroring the web overlay over the same `storages` root, so checkpoints written by either surface serve both); the first scan over a pre-existing store still reads each log once to seed checkpoints, and every later scan is metadata-only.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
@@ -28,8 +28,8 @@ The change is confined to the TUI package: no session-query or session-persisten
|
||||
|
||||
**Surface a last-modified time through `listSnapshots`/`SessionRecord`.** Cleanest seam-wise, but touches the persistence contract, both backends, and the query record shape for what the TUI can already derive from `locate()` plus one stat. Reintroduce if a second consumer needs metadata activity times.
|
||||
|
||||
**A persisted summary/title index.** Rejected for now: one bounded title pass is acceptable selector latency, and an index adds an invalidation contract. Reintroduce if title reads over large stores become the bottleneck.
|
||||
**A bespoke persisted title index or TUI-local title cache.** Rejected: the session-projection cache already is the owned durable checkpoint system with an invalidation contract (`stateVersion`, identity binding, shrunk-log anchoring); mounting it beats adding a parallel cache.
|
||||
|
||||
## Consequences
|
||||
|
||||
Opening `/resume` performs one listing, one stat per persisted row, and one bounded title pass instead of N listings and N validated full copies. Rows show title, timestamp, status, and id only; route problems surface as an Enter-time preflight error instead of a disabled row, and a session that fails replay is caught by preflight rather than the listing. Browsed-then-abandoned sessions float up on their pickup mtime. Fake `sessionQuery` services in TUI tests provide `readTitleSnapshots` alongside `listSessions`/`readSession`, and the test harness forwards an optional `locate`. Because the picker takes focus immediately, starting a second scan requires dismissing the current overlay first — a second `/resume` typed during a scan lands in the search field, which is the intended input capture.
|
||||
Opening `/resume` performs one listing, one stat per persisted row, and per-row title reads that touch only checkpoint rows and log tails once checkpoints exist — O(session count) metadata instead of O(total log bytes); the fallback path without the cache remains one bounded title pass. Rows show title, timestamp, status, and id only; route problems surface as an Enter-time preflight error instead of a disabled row, and a session that fails replay is caught by preflight rather than the listing. Browsed-then-abandoned sessions float up on their pickup mtime. Fake `sessionQuery` services in TUI tests provide `readTitleSnapshots` alongside `listSessions`/`readSession`, and the test harness forwards an optional `locate`. Because the picker takes focus immediately, starting a second scan requires dismissing the current overlay first — a second `/resume` typed during a scan lands in the search field, which is the intended input capture.
|
||||
|
||||
Reference in New Issue
Block a user