| 1 | # Immutable list and search reads |
| 2 | |
| 3 | [中文](READ_SNAPSHOT_PAGINATION.zh-CN.md) |
| 4 | |
| 5 | ## Problem and repair boundary |
| 6 | |
| 7 | A local Desktop session could report `session_operation:stale_cursor:The session list changed. Reload it.` while its owner was writing titles/results. A double metadata collect required every writer to remain idle. Removing that collect fixed first-page starvation but still allowed live re-sorting to invalidate subsequent pages. The frontend could miss the failure when its first logical window required multiple 200-row RPC calls. This error does not establish a failure of SSH, Git, Docker or remote Reasonix. |
| 8 | |
| 9 | The read result is now owned by `desktop/read_snapshot_store.go`. Continuations read immutable rows by ordinal. They do not re-sort current data or compare global catalog revisions. This is a read contract; commands still resolve current authoritative ownership and lifecycle. |
| 10 | |
| 11 | ## Entry points |
| 12 | |
| 13 | | Surface | Implementation | |
| 14 | | --- | --- | |
| 15 | | Local project/global/group/query lists | `session_workspace_sidebar.go`, `session_topic_index.go` | |
| 16 | | Pinned shells | Same snapshot pages, released after collecting the complete pin list | |
| 17 | | Compatibility project tree | Same pages; `ListProjectTree` returns an RPC error instead of successful partial children | |
| 18 | | Legacy history metadata | `history_read_snapshots.go` plus one `sessioncatalog.WithReadView` | |
| 19 | | Legacy full-text and exact-target search | Shared frozen candidate/snippet builder; `historycatalog.CaptureSearch` streams one ranked SQL result | |
| 20 | | Project list recovery | `projectTreeWindow.ts`, `ProjectTree.tsx` | |
| 21 | | Paired history list/search recovery | `useHistoryCatalog.ts` | |
| 22 | |
| 23 | Canonical transcript/history-window mechanisms and persisted session/provider formats are unchanged. |
| 24 | |
| 25 | ## Read protocol |
| 26 | |
| 27 | 1. An empty cursor starts or joins a build for a query binding. The binding includes the API kind and target/filter/sort parameters, excluding page size. |
| 28 | 2. Capture membership, presentation, organization and lifecycle from one registry projection; read each canonical session's metadata once. Nested legacy catalog reads share one SQLite read transaction. |
| 29 | 3. Freeze the selected rows and their order. Search captures candidates before loading source content. Each snippet and its content digest come from the same source read; mismatches request reindexing and mark coverage partial. I/O errors fail the build. |
| 30 | 4. Publish only a finished result. Each caller receives a distinct opaque release handle, even when concurrent callers shared a build. |
| 31 | 5. Version-1 cursors contain a random process-local handle, query binding and next ordinal. Repeating a cursor returns the same frozen page. Unknown old-version/restarted/evicted handles are rejected; they are never interpreted as live offsets. |
| 32 | 6. Ordinary writes and organization edits leave existing results readable. A fresh query sees updates. Relevant archive/delete/move/adoption or source replacement invalidates its dependent result. |
| 33 | 7. `snapshotId` and `snapshotExpiresAt` are additive response fields. History APIs retain `staleCursor` and add `readError`. RPC errors retain `stale_cursor` and add a structured `readReason`. `ReleaseReadSnapshot` is idempotent. |
| 34 | |
| 35 | Metadata lists validate catalog/registry ownership without opening transcript files. Search also fences file identity, and context requests carrying `contentDigest` refuse changed content before interpreting message indexes. |
| 36 | |
| 37 | ## Resource and lifetime policy |
| 38 | |
| 39 | | Limit | Default | |
| 40 | | --- | --- | |
| 41 | | Stored handles, including shared-build cache leases | 64 per App | |
| 42 | | Accounted resident rows and retained dependency metadata | 32 MiB | |
| 43 | | Concurrent builders | 2 | |
| 44 | | Spill threshold per result | 512 KiB of accounted data | |
| 45 | | Private SQLite storage | 256 MiB total; reserve up to 64 MiB per file | |
| 46 | | Single captured result | 64 MiB | |
| 47 | | Idle / absolute cursor lifetime | 10 / 30 minutes | |
| 48 | | Public page size | At most 200 | |
| 49 | |
| 50 | Storage reservations include unpublished builds and intermediate search candidates. SQL/file work and lifecycle validation execute outside the store management lock. A per-result read lease protects storage from disposal. A cancelled waiter does not cancel other waiters; App shutdown cancels builds and disposes files. Temporary files live in newly created private directories; cleanup never scans another process's directories. |
| 51 | |
| 52 | Expiry is enforced on access and expired entries are reclaimed by subsequent builds or shutdown. Limits describe accounted snapshot storage, not a whole-process RSS ceiling: SQLite caches, registry projections and a single source replay have their own transient working memory. Disk reservations are conservative, so four spilling results/candidate files can exhaust the disk allowance before physical disk use reaches 256 MiB. Exhaustion is an explicit failure, never truncated success. |
| 53 | |
| 54 | ## Frontend publication and refresh |
| 55 | |
| 56 | - A logical window stages all its RPC pages before committing. A stale internal second page is handled the same as a stale outer continuation. |
| 57 | - One automatic rebuild is permitted per logical operation. Rebuilding an append replaces the complete visible window; it never appends fresh rows to an older snapshot. A second failure retains the current display with a retry path. |
| 58 | - Mixed IDs, nonadvancing cursors and empty pages with a continuation fail explicitly. |
| 59 | - Background events coalesce over a fixed 200 ms window. Pending reads complete; events queue one follow-up. Background refresh starts are separated by at least 500 ms per logical list. Explicit query/sort changes retire the previous generation immediately. |
| 60 | - Expanded size, stable row identity and selection survive refresh. The project list restores its visible row anchor without overriding an intervening user scroll. Superseded snapshots are released; obsolete responses cannot release the current generation's handle. |
| 61 | - History metadata and body hits stage and commit as one UI generation. Tool parts participate in hit identity, avoiding collisions between same-message tool inputs. |
| 62 | - Project, current-session, query and filter changes immediately isolate old rows/cursors and retire old button callbacks, previews and search contexts. Background failures retain results only within the same identity. |
| 63 | - Removing and re-adding a project never reuses request generations. An obsolete completion cannot consume a newer generation's queued refresh. Invalidation, window reset, project removal and archive release retired snapshots. |
| 64 | - History lists and ordinary searches bind the captured active-session identity to prevent sharing a build across a focus change. Explicit-target searches remain owned by their selected source and survive unrelated foreground changes. |
| 65 | |
| 66 | ## Verification and delivery gates |
| 67 | |
| 68 | Deterministic regressions cover 205/405/1000-row windows, ongoing metadata writes, organization changes, archive fences, cross-query cursor rejection, fixed ranking during unrelated index writes, source deletion, spill/expiry/resource rejection, independent releases and shutdown. Frontend tests cover paired recovery, retained errors, pending refreshes, query/sort invalidation and malformed pagination. The Chromium `independent-sessions` scenario includes the three large-window sizes. |
| 69 | |
| 70 | Run root catalog/history package tests separately from the Desktop Go module, focused `-race`, generated-host-contract checks, frontend typecheck/lint and the browser scenario. The pre-existing `TestLargeTranscriptSwitchPhaseMeasurement` hang has been reproduced on the original HEAD. An exploratory broader run excluding it still exceeded the ten-minute suite budget; there is no full-suite pass. Run contract generation and its freshness check sequentially so regeneration during a running test cannot produce a misleading stale-artifact failure. |
| 71 | |
| 72 | Before release, verify the packaged macOS/Windows app with a local running conversation, SSH connected, an expanded list above 200 rows, legacy body search, archive/restore, and shutdown. Browser/local Go evidence does not establish native-package acceptance. No Git push, release or production deployment is part of this local implementation. |
| 73 |