Distinguish rollout IDs from thread IDs (#38127)

## Why

`thread/revert` preserves a thread ID while creating a new immutable rollout. Rollout persistence and filesystem fallback therefore need to distinguish the logical thread from each physical history file.

## What changed

- Add a `RolloutId` alias and canonical filename parsing for both ordinary rollouts and reverted rollouts with a distinct rollout ID.
- Allow the recorder to create a replacement rollout without changing the thread ID, and add lookup by either the stable thread ID or an exact rollout ID.
- Index history references and compression safety checks by rollout ID so multiple rollouts for one thread remain distinct.
- Preserve the rollout path selected in SQLite when filesystem repair encounters another rollout for the same thread.

## Testing

Add coverage for filename round trips, replacement rollout metadata, thread and rollout lookup, reference indexing, compression, and database path preservation.

GitOrigin-RevId: 14b1d0b4a9432b341a1307afc0abadd507e7ae7d
This commit is contained in:
Owen Lin authored and copyberry committed 2026-08-12 05:20:32 +00:00
1 parent 2230d64464
commit 4ef836f883
18 files changed
+513 -178

No files matched your search

+15
View File
@@ -554,6 +554,15 @@ pub async fn reconcile_rollout(
let memory_mode = outcome.memory_mode.unwrap_or_else(|| "enabled".to_string());
metadata.cwd = normalize_cwd_for_state_db(&metadata.cwd);
let existing_metadata = ctx.get_thread(metadata.id).await.ok().flatten();
// Filesystem repair may seed a missing row, but it must not change an existing row's
// selected rollout path. After `thread/revert`, a scan can find multiple immutable
// rollouts for one thread and cannot know which one SQLite selected.
if existing_metadata
.as_ref()
.is_some_and(|existing| existing.rollout_path.as_path() != rollout_path)
{
return;
}
// Paginated metadata updates are SQLite-only. Use the rollout mode to seed a
// missing row, then keep the value from SQLite.
let restore_memory_mode_from_rollout =
@@ -607,6 +616,12 @@ pub async fn read_repair_rollout_path(
if let Some(thread_id) = thread_id
&& let Ok(Some(metadata)) = ctx.get_thread(thread_id).await
{
// Filesystem repair may seed a missing row, but it must not change an existing row's
// selected rollout path. After `thread/revert`, a scan can find multiple immutable
// rollouts for one thread and cannot know which one SQLite selected.
if metadata.rollout_path.as_path() != rollout_path {
return;
}
saw_existing_metadata = true;
let mut repaired = metadata.clone();
repaired.rollout_path = rollout_path.to_path_buf();