Files
codex/codex-rs/rollout/src/rollout_file_name.rs
T
Owen Lin 4ef836f883 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
2026-08-12 05:20:32 +00:00

88 lines
2.6 KiB
Rust

use codex_protocol::RolloutId;
use codex_protocol::ThreadId;
use time::OffsetDateTime;
use time::PrimitiveDateTime;
use time::format_description::FormatItem;
use time::macros::format_description;
use crate::compression;
/// Parsed canonical rollout basename.
///
/// Ordinary rollout filenames encode one ID, which is both the thread ID and rollout ID.
/// Filenames for reverted threads append an underscore and a distinct rollout ID after the stable
/// thread ID.
#[derive(Debug, PartialEq, Eq)]
pub(crate) struct RolloutFileName {
timestamp: OffsetDateTime,
thread_id: ThreadId,
rollout_id: RolloutId,
}
#[cfg(test)]
#[path = "rollout_file_name_tests.rs"]
mod tests;
impl RolloutFileName {
pub(crate) fn new(
timestamp: OffsetDateTime,
thread_id: ThreadId,
rollout_id: RolloutId,
) -> Self {
Self {
timestamp,
thread_id,
rollout_id,
}
}
pub(crate) fn parse(name: &str) -> Option<Self> {
let name = compression::parse_rollout_file_name(name)?;
let core = name.strip_prefix("rollout-")?.strip_suffix(".jsonl")?;
let timestamp = core.get(..19)?;
if core.get(19..20)? != "-" {
return None;
}
let ids = core.get(20..)?;
let (thread_id, rollout_id) = ids.split_once('_').unwrap_or((ids, ids));
let thread_id = ThreadId::from_string(thread_id).ok()?;
let rollout_id = RolloutId::from_string(rollout_id).ok()?;
let format: &[FormatItem] =
format_description!("[year]-[month]-[day]T[hour]-[minute]-[second]");
let timestamp = PrimitiveDateTime::parse(timestamp, format)
.ok()?
.assume_utc();
Some(Self {
timestamp,
thread_id,
rollout_id,
})
}
pub(crate) fn render(&self) -> Result<String, time::error::Format> {
let format: &[FormatItem] =
format_description!("[year]-[month]-[day]T[hour]-[minute]-[second]");
let timestamp = self.timestamp.format(format)?;
Ok(if self.thread_id == self.rollout_id {
format!("rollout-{timestamp}-{}.jsonl", self.thread_id)
} else {
format!(
"rollout-{timestamp}-{}_{}.jsonl",
self.thread_id, self.rollout_id
)
})
}
pub(crate) fn timestamp(&self) -> OffsetDateTime {
self.timestamp
}
pub(crate) fn thread_id(&self) -> ThreadId {
self.thread_id
}
pub(crate) fn rollout_id(&self) -> RolloutId {
self.rollout_id
}
}