diff --git a/codex-rs/rollout/src/compression.rs b/codex-rs/rollout/src/compression.rs index 98d514c529..1370e194ae 100644 --- a/codex-rs/rollout/src/compression.rs +++ b/codex-rs/rollout/src/compression.rs @@ -89,7 +89,12 @@ pub(crate) async fn file_modified_time(path: &Path) -> io::Result io::Result { +/// Opens a rollout line reader that transparently handles plain `.jsonl` and `.jsonl.zst` files. +/// +/// If the requested path disappears during a compression or decompression transition, this retries +/// the matching plain/compressed sibling once so readers do not need to know which representation is +/// currently stored on disk. +pub async fn open_rollout_line_reader(path: &Path) -> io::Result { match open_rollout_line_reader_once(path).await { Ok(reader) => Ok(reader), Err(err) if err.kind() == io::ErrorKind::NotFound => { @@ -191,7 +196,8 @@ pub(crate) fn should_skip_compressed_sibling(path: &Path) -> bool { is_compressed_rollout_path(path) && plain_rollout_path(path).exists() } -pub(crate) struct RolloutLineReader { +/// Line-oriented rollout reader returned by [`open_rollout_line_reader`]. +pub struct RolloutLineReader { inner: RolloutLineReaderInner, } @@ -201,7 +207,8 @@ enum RolloutLineReaderInner { } impl RolloutLineReader { - pub(crate) async fn next_line(&mut self) -> io::Result> { + /// Reads the next JSONL record from the rollout. + pub async fn next_line(&mut self) -> io::Result> { match &mut self.inner { RolloutLineReaderInner::Plain(lines) => lines.next_line().await, RolloutLineReaderInner::Memory(lines) => lines.next().transpose(), diff --git a/codex-rs/rollout/src/lib.rs b/codex-rs/rollout/src/lib.rs index 6019f6b7f9..863bc453c1 100644 --- a/codex-rs/rollout/src/lib.rs +++ b/codex-rs/rollout/src/lib.rs @@ -33,7 +33,9 @@ pub static INTERACTIVE_SESSION_SOURCES: LazyLock> = LazyLock: }); pub use codex_protocol::protocol::SessionMeta; +pub use compression::RolloutLineReader; pub use compression::existing_rollout_path; +pub use compression::open_rollout_line_reader; pub use compression::spawn_rollout_compression_worker; pub use config::Config; pub use config::RolloutConfig; diff --git a/codex-rs/tui/src/app/config_persistence.rs b/codex-rs/tui/src/app/config_persistence.rs index e83662c2a2..89060fe0d3 100644 --- a/codex-rs/tui/src/app/config_persistence.rs +++ b/codex-rs/tui/src/app/config_persistence.rs @@ -225,22 +225,11 @@ impl App { current_cwd: &Path, resume_cwd: PathBuf, ) -> Result { - match self.rebuild_config_for_cwd(resume_cwd.clone()).await { - Ok(config) => Ok(config), - Err(err) => { - if crate::session_resume::cwds_differ(current_cwd, &resume_cwd) { - Err(err) - } else { - let resume_cwd_display = resume_cwd.display().to_string(); - tracing::warn!( - error = %err, - cwd = %resume_cwd_display, - "failed to rebuild config for same-cwd resume; using current in-memory config" - ); - Ok(self.config.clone()) - } - } + if !crate::session_resume::cwds_differ(current_cwd, &resume_cwd) { + return Ok(self.config.clone()); } + + self.rebuild_config_for_cwd(resume_cwd).await } pub(super) fn apply_runtime_policy_overrides(&mut self, config: &mut Config) { @@ -1220,12 +1209,16 @@ terminal_resize_reflow_max_rows = 9000 } #[tokio::test] - async fn rebuild_config_for_resume_or_fallback_uses_current_config_on_same_cwd_error() - -> Result<()> { + async fn rebuild_config_for_resume_or_fallback_reuses_current_config_on_same_cwd() -> Result<()> + { let mut app = make_test_app().await; let codex_home = tempdir()?; app.config.codex_home = codex_home.path().to_path_buf().abs(); - std::fs::write(codex_home.path().join("config.toml"), "[broken")?; + app.config.model = Some("already-loaded-model".to_string()); + std::fs::write( + codex_home.path().join("config.toml"), + "model = \"freshly-loaded-model\"", + )?; let current_config = app.config.clone(); let current_cwd = current_config.cwd.clone(); diff --git a/codex-rs/tui/src/session_resume.rs b/codex-rs/tui/src/session_resume.rs index 9b47599edc..56b258704e 100644 --- a/codex-rs/tui/src/session_resume.rs +++ b/codex-rs/tui/src/session_resume.rs @@ -14,11 +14,11 @@ use crate::cwd_prompt::CwdPromptOutcome; use crate::cwd_prompt::CwdSelection; use crate::tui::Tui; use codex_protocol::ThreadId; +use codex_rollout::open_rollout_line_reader; use codex_state::StateRuntime; use codex_utils_path as path_utils; use serde::Deserialize; use serde_json::Value; -use tokio::io::AsyncBufReadExt; #[derive(Default)] struct RolloutResumeState { @@ -142,13 +142,11 @@ pub(crate) fn cwds_differ(current_cwd: &Path, session_cwd: &Path) -> bool { } async fn read_rollout_resume_state(path: &Path) -> io::Result { - let file = tokio::fs::File::open(path).await?; - let reader = tokio::io::BufReader::new(file); - let mut lines = reader.lines(); + let mut reader = open_rollout_line_reader(path).await?; let mut state = RolloutResumeState::default(); let mut saw_record = false; - while let Some(line) = lines.next_line().await? { + while let Some(line) = reader.next_line().await? { let trimmed = line.trim(); if trimmed.is_empty() { continue;