Resolve paginated thread history by rollout ID (#38244)

## Why

`thread/revert` keeps the logical thread ID while selecting a new immutable
rollout. Using the thread ID for persisted history can therefore read or update
the wrong rollout.

## What changed

- Centralize resolution of a thread's current rollout across live writers,
  SQLite metadata, and filesystem fallbacks.
- Key history materialization, lineage traversal, paging, turn lookup, and fork
  boundaries by immutable rollout ID while retaining the thread ID for the
  logical thread.
- Treat SQLite's selected rollout as authoritative for paginated threads so a
  filesystem scan cannot select an older rollout after `thread/revert`.

## Testing

Add a regression test showing that update-ordinal item paging reads from the
selected rollout ID when it differs from the thread ID.

GitOrigin-RevId: 1101e548a6e6c6d319996e24cb650e5b03ab7fbb
This commit is contained in:
Owen Lin authored and copyberry committed 2026-08-12 20:31:52 +00:00
1 parent 0e0ef5d818
commit 8d4d57387a
14 files changed
+471 -213

No files matched your search

+6
View File
@@ -15,6 +15,7 @@ mod rollout_lineage;
mod search_threads;
mod thread_history;
mod thread_history_materialization;
mod thread_rollout_resolver;
mod thread_sections;
mod unarchive_thread;
mod update_thread_metadata;
@@ -112,6 +113,9 @@ pub struct LocalThreadStore {
struct LiveRecorderEntry {
recorder: RolloutRecorder,
// Rollout projection rows are keyed by immutable rollout ID, not the stable thread ID used
// to find this live writer.
rollout_id: ThreadId,
// Local rollout files are materialized lazily, but metadata updates can arrive before the
// canonical SessionMeta is durable. Retain the mode captured when live persistence was opened
// so missing SQLite rows can still be seeded.
@@ -291,6 +295,7 @@ impl LocalThreadStore {
&self,
thread_id: ThreadId,
recorder: RolloutRecorder,
rollout_id: ThreadId,
history_mode: ThreadHistoryMode,
writer_lock: WriterLockGuard,
) -> ThreadStoreResult<()> {
@@ -301,6 +306,7 @@ impl LocalThreadStore {
Entry::Vacant(entry) => {
entry.insert(LiveRecorderEntry {
recorder,
rollout_id,
history_mode,
writer_lock,
});