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

+49 -2
View File
@@ -23,6 +23,7 @@ use crate::INTERACTIVE_SESSION_SOURCES;
use crate::ResponseItemEnvelope;
use crate::RolloutItem;
use crate::RolloutLine;
use crate::find_rollout_path_by_rollout_id;
use crate::find_thread_path_by_id_str;
use crate::list::Cursor;
use crate::list::ThreadItem;
@@ -132,7 +133,53 @@ async fn find_thread_path_falls_back_when_db_path_is_stale() {
.await
.expect("lookup should succeed");
assert_eq!(found, Some(fs_rollout_path.clone()));
assert_state_db_rollout_path(home, thread_id, Some(fs_rollout_path.as_path())).await;
assert_state_db_rollout_path(home, thread_id, Some(stale_db_path.as_path())).await;
}
#[tokio::test]
async fn filesystem_lookup_distinguishes_thread_ids_from_rollout_ids() {
let temp = TempDir::new().unwrap();
let home = temp.path();
let thread_uuid = Uuid::from_u128(401);
let rollout_uuid = Uuid::from_u128(402);
let original_ts = "2025-01-03T13-00-00";
let reverted_ts = "2025-01-04T13-00-00";
write_session_file(
home,
original_ts,
thread_uuid,
/*num_records*/ 1,
Some(SessionSource::Cli),
)
.unwrap();
write_session_file(
home,
reverted_ts,
thread_uuid,
/*num_records*/ 1,
Some(SessionSource::Cli),
)
.unwrap();
let reverted_source = home.join(format!(
"sessions/2025/01/04/rollout-{reverted_ts}-{thread_uuid}.jsonl"
));
let reverted_path = home.join(format!(
"sessions/2025/01/04/rollout-{reverted_ts}-{thread_uuid}_{rollout_uuid}.jsonl"
));
fs::rename(reverted_source, reverted_path.as_path()).unwrap();
assert_eq!(
find_thread_path_by_id_str(home, &thread_uuid.to_string(), /*state_db_ctx*/ None)
.await
.unwrap(),
Some(reverted_path.clone())
);
assert_eq!(
find_rollout_path_by_rollout_id(home, thread_id_from_uuid(rollout_uuid))
.await
.unwrap(),
Some(reverted_path)
);
}
#[tokio::test]
@@ -212,7 +259,7 @@ async fn find_thread_path_falls_back_when_db_path_points_to_another_thread() {
.await
.expect("lookup should succeed");
assert_eq!(found, Some(fs_rollout_path.clone()));
assert_state_db_rollout_path(home, thread_id, Some(fs_rollout_path.as_path())).await;
assert_state_db_rollout_path(home, thread_id, Some(stale_db_path.as_path())).await;
}
#[tokio::test]