From ed26d8671a0fbed82aa2019fdaa3c1d681224def Mon Sep 17 00:00:00 2001 From: Owen Lin Date: Mon, 26 Jan 2026 16:22:04 -0800 Subject: [PATCH 1/6] fix(app-server, core): dont write to rollout file if no config overrides on thread/resume --- .../app-server/src/codex_message_processor.rs | 20 +++++- .../tests/suite/v2/thread_resume.rs | 64 +++++++++++++++++++ codex-rs/core/src/codex.rs | 48 ++++++++++++-- codex-rs/core/src/rollout/recorder.rs | 1 + codex-rs/core/tests/suite/resume_warning.rs | 1 + codex-rs/protocol/src/protocol.rs | 9 +++ 6 files changed, 137 insertions(+), 6 deletions(-) diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index ab2f31fc51c..b9cec97f15b 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -1914,7 +1914,7 @@ impl CodexMessageProcessor { personality, } = params; - let thread_history = if let Some(history) = history { + let mut thread_history = if let Some(history) = history { if history.is_empty() { self.send_invalid_request_error( request_id, @@ -1988,6 +1988,24 @@ impl CodexMessageProcessor { } }; + let has_request_overrides = request_overrides + .as_ref() + .is_some_and(|overrides| !overrides.is_empty()); + + let has_resume_overrides = model.is_some() + || model_provider.is_some() + || cwd.is_some() + || approval_policy.is_some() + || sandbox.is_some() + || has_request_overrides + || base_instructions.is_some() + || developer_instructions.is_some() + || personality.is_some(); + + if !has_resume_overrides && let InitialHistory::Resumed(resumed) = &mut thread_history { + resumed.persist_initial_context = false; + } + let history_cwd = thread_history.session_cwd(); let typesafe_overrides = self.build_thread_config_overrides( model, diff --git a/codex-rs/app-server/tests/suite/v2/thread_resume.rs b/codex-rs/app-server/tests/suite/v2/thread_resume.rs index 6129c0de2f3..7450b217c3e 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_resume.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_resume.rs @@ -2,7 +2,9 @@ use anyhow::Result; use app_test_support::McpProcess; use app_test_support::create_fake_rollout_with_text_elements; use app_test_support::create_mock_responses_server_repeating_assistant; +use app_test_support::rollout_path; use app_test_support::to_response; +use chrono::Utc; use codex_app_server_protocol::JSONRPCResponse; use codex_app_server_protocol::RequestId; use codex_app_server_protocol::SessionSource; @@ -22,6 +24,8 @@ use codex_protocol::user_input::TextElement; use core_test_support::responses; use core_test_support::skip_if_no_network; use pretty_assertions::assert_eq; +use std::fs::FileTimes; +use std::path::Path; use std::path::PathBuf; use tempfile::TempDir; use tokio::time::timeout; @@ -147,6 +151,56 @@ async fn thread_resume_returns_rollout_history() -> Result<()> { Ok(()) } +#[tokio::test] +async fn thread_resume_without_overrides_does_not_change_updated_at_or_mtime() -> Result<()> { + let server = create_mock_responses_server_repeating_assistant("Done").await; + let codex_home = TempDir::new()?; + create_config_toml(codex_home.path(), &server.uri())?; + + let preview = "Saved user message"; + let filename_ts = "2025-01-05T12-00-00"; + let meta_rfc3339 = "2025-01-05T12:00:00Z"; + let expected_updated_at_rfc3339 = "2025-01-07T00:00:00Z"; + let conversation_id = create_fake_rollout_with_text_elements( + codex_home.path(), + filename_ts, + meta_rfc3339, + preview, + Vec::new(), + Some("mock_provider"), + None, + )?; + let rollout_file_path = rollout_path(codex_home.path(), filename_ts, &conversation_id); + set_rollout_mtime(rollout_file_path.as_path(), expected_updated_at_rfc3339)?; + let before_modified = std::fs::metadata(&rollout_file_path)?.modified()?; + + let mut mcp = McpProcess::new(codex_home.path()).await?; + timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; + + let resume_id = mcp + .send_thread_resume_request(ThreadResumeParams { + thread_id: conversation_id, + ..Default::default() + }) + .await?; + let resume_resp: JSONRPCResponse = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(resume_id)), + ) + .await??; + let ThreadResumeResponse { thread, .. } = to_response::(resume_resp)?; + + let expected_updated_at = chrono::DateTime::parse_from_rfc3339(expected_updated_at_rfc3339)? + .with_timezone(&Utc) + .timestamp(); + assert_eq!(thread.updated_at, expected_updated_at); + + let after_modified = std::fs::metadata(&rollout_file_path)?.modified()?; + assert_eq!(after_modified, before_modified); + + Ok(()) +} + #[tokio::test] async fn thread_resume_prefers_path_over_thread_id() -> Result<()> { let server = create_mock_responses_server_repeating_assistant("Done").await; @@ -364,3 +418,13 @@ stream_max_retries = 0 ), ) } + +fn set_rollout_mtime(path: &Path, updated_at_rfc3339: &str) -> Result<()> { + let parsed = chrono::DateTime::parse_from_rfc3339(updated_at_rfc3339)?.with_timezone(&Utc); + let times = FileTimes::new().set_modified(parsed.into()); + std::fs::OpenOptions::new() + .append(true) + .open(path)? + .set_times(times)?; + Ok(()) +} diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 6401605eec0..9a1edde8d55 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -934,12 +934,14 @@ impl Session { // Ensure initial items are visible to immediate readers (e.g., tests, forks). self.flush_rollout().await; } - InitialHistory::Resumed(_) | InitialHistory::Forked(_) => { - let rollout_items = conversation_history.get_rollout_items(); - let persist = matches!(conversation_history, InitialHistory::Forked(_)); + InitialHistory::Resumed(resumed_history) => { + let rollout_items = resumed_history.history; + let persist_rollout_items = false; + let persist_initial_context = resumed_history.persist_initial_context; + let is_resumed = true; // If resuming, warn when the last recorded model differs from the current one. - if let InitialHistory::Resumed(_) = conversation_history + if is_resumed && let Some(prev) = rollout_items.iter().rev().find_map(|it| { if let RolloutItem::TurnContext(ctx) = it { Some(ctx.model.as_str()) @@ -983,7 +985,41 @@ impl Session { } // If persisting, persist all rollout items as-is (recorder filters) - if persist && !rollout_items.is_empty() { + if persist_rollout_items && !rollout_items.is_empty() { + self.persist_rollout_items(&rollout_items).await; + } + + // Append the current session's initial context after the reconstructed history. + let initial_context = self.build_initial_context(&turn_context).await; + if persist_initial_context { + self.record_conversation_items(&turn_context, &initial_context) + .await; + } else { + self.record_into_history(&initial_context, &turn_context) + .await; + } + // Flush after seeding history and any persisted rollout copy. + self.flush_rollout().await; + } + InitialHistory::Forked(rollout_items) => { + // Always add response items to conversation history + let reconstructed_history = self + .reconstruct_history_from_rollout(&turn_context, &rollout_items) + .await; + if !reconstructed_history.is_empty() { + self.record_into_history(&reconstructed_history, &turn_context) + .await; + } + + // Seed usage info from the recorded rollout so UIs can show token counts + // immediately on resume/fork. + if let Some(info) = Self::last_token_info_from_rollout(&rollout_items) { + let mut state = self.state.lock().await; + state.set_token_info(Some(info)); + } + + // If persisting, persist all rollout items as-is (recorder filters) + if !rollout_items.is_empty() { self.persist_rollout_items(&rollout_items).await; } @@ -3727,6 +3763,7 @@ mod tests { conversation_id: ThreadId::default(), history: rollout_items, rollout_path: PathBuf::from("/tmp/resume.jsonl"), + persist_initial_context: true, })) .await; @@ -3805,6 +3842,7 @@ mod tests { conversation_id: ThreadId::default(), history: rollout_items, rollout_path: PathBuf::from("/tmp/resume.jsonl"), + persist_initial_context: true, })) .await; diff --git a/codex-rs/core/src/rollout/recorder.rs b/codex-rs/core/src/rollout/recorder.rs index 53425051cf0..c470952f0b3 100644 --- a/codex-rs/core/src/rollout/recorder.rs +++ b/codex-rs/core/src/rollout/recorder.rs @@ -349,6 +349,7 @@ impl RolloutRecorder { conversation_id, history: items, rollout_path: path.to_path_buf(), + persist_initial_context: true, })) } diff --git a/codex-rs/core/tests/suite/resume_warning.rs b/codex-rs/core/tests/suite/resume_warning.rs index 5e5fc6d74aa..e639e6d41d2 100644 --- a/codex-rs/core/tests/suite/resume_warning.rs +++ b/codex-rs/core/tests/suite/resume_warning.rs @@ -40,6 +40,7 @@ fn resume_history( conversation_id: ThreadId::default(), history: vec![RolloutItem::TurnContext(turn_ctx)], rollout_path: rollout_path.to_path_buf(), + persist_initial_context: true, }) } diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index e9cb8147d7b..9e01650892c 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -1418,6 +1418,11 @@ pub struct ResumedHistory { pub conversation_id: ThreadId, pub history: Vec, pub rollout_path: PathBuf, + /// Whether to persist the initial context when resuming this rollout. + /// + /// Defaults to true to preserve existing behavior for older serialized data. + #[serde(default = "resumed_history_default_persist_initial_context")] + pub persist_initial_context: bool, } #[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)] @@ -1503,6 +1508,10 @@ impl InitialHistory { } } +fn resumed_history_default_persist_initial_context() -> bool { + true +} + fn session_cwd_from_items(items: &[RolloutItem]) -> Option { items.iter().find_map(|item| match item { RolloutItem::SessionMeta(meta_line) => Some(meta_line.meta.cwd.clone()), From 6c7230bf431be21ae8d4bcdbc92bf35f32d6825a Mon Sep 17 00:00:00 2001 From: Owen Lin Date: Mon, 26 Jan 2026 16:33:16 -0800 Subject: [PATCH 2/6] update docstring --- codex-rs/protocol/src/protocol.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index 9e01650892c..d982f081b94 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -1419,6 +1419,12 @@ pub struct ResumedHistory { pub history: Vec, pub rollout_path: PathBuf, /// Whether to persist the initial context when resuming this rollout. + /// When true, the resumed session's initial context (instructions and environment) + /// is appended to the rollout file. When false, it is only added to in-memory history. + /// + /// This helps avoid duplicating unchanged context on resume without overrides, and + /// unnecessarily changing the rollout file's mtime which is used as the Thread's updated_at + /// timestamp. /// /// Defaults to true to preserve existing behavior for older serialized data. #[serde(default = "resumed_history_default_persist_initial_context")] From f6523936290d5fd7facbe7280bf42d5d76f3ff71 Mon Sep 17 00:00:00 2001 From: Owen Lin Date: Mon, 26 Jan 2026 20:53:17 -0800 Subject: [PATCH 3/6] defer writing rollout lines until turn start --- .../tests/suite/v2/thread_resume.rs | 75 ++++++++++++ codex-rs/core/src/codex.rs | 112 ++++++++++++++---- codex-rs/core/src/state/session.rs | 6 + codex-rs/core/src/tasks/mod.rs | 2 + 4 files changed, 174 insertions(+), 21 deletions(-) diff --git a/codex-rs/app-server/tests/suite/v2/thread_resume.rs b/codex-rs/app-server/tests/suite/v2/thread_resume.rs index 7450b217c3e..d86079846d4 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_resume.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_resume.rs @@ -201,6 +201,81 @@ async fn thread_resume_without_overrides_does_not_change_updated_at_or_mtime() - Ok(()) } +#[tokio::test] +async fn thread_resume_with_overrides_defers_updated_at_until_turn_start() -> Result<()> { + let server = create_mock_responses_server_repeating_assistant("Done").await; + let codex_home = TempDir::new()?; + create_config_toml(codex_home.path(), &server.uri())?; + + let preview = "Saved user message"; + let filename_ts = "2025-01-05T12-00-00"; + let meta_rfc3339 = "2025-01-05T12:00:00Z"; + let expected_updated_at_rfc3339 = "2025-01-07T00:00:00Z"; + let conversation_id = create_fake_rollout_with_text_elements( + codex_home.path(), + filename_ts, + meta_rfc3339, + preview, + Vec::new(), + Some("mock_provider"), + None, + )?; + let rollout_file_path = rollout_path(codex_home.path(), filename_ts, &conversation_id); + set_rollout_mtime(rollout_file_path.as_path(), expected_updated_at_rfc3339)?; + let before_modified = std::fs::metadata(&rollout_file_path)?.modified()?; + + let mut mcp = McpProcess::new(codex_home.path()).await?; + timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; + + let resume_id = mcp + .send_thread_resume_request(ThreadResumeParams { + thread_id: conversation_id.clone(), + model: Some("mock-model".to_string()), + ..Default::default() + }) + .await?; + let resume_resp: JSONRPCResponse = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(resume_id)), + ) + .await??; + let ThreadResumeResponse { thread, .. } = to_response::(resume_resp)?; + + let expected_updated_at = chrono::DateTime::parse_from_rfc3339(expected_updated_at_rfc3339)? + .with_timezone(&Utc) + .timestamp(); + assert_eq!(thread.updated_at, expected_updated_at); + + let after_resume_modified = std::fs::metadata(&rollout_file_path)?.modified()?; + assert_eq!(after_resume_modified, before_modified); + + let turn_id = mcp + .send_turn_start_request(TurnStartParams { + thread_id: conversation_id, + input: vec![UserInput::Text { + text: "Hello".to_string(), + text_elements: Vec::new(), + }], + ..Default::default() + }) + .await?; + timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(turn_id)), + ) + .await??; + timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_notification_message("turn/completed"), + ) + .await??; + + let after_turn_modified = std::fs::metadata(&rollout_file_path)?.modified()?; + assert!(after_turn_modified > before_modified); + + Ok(()) +} + #[tokio::test] async fn thread_resume_prefers_path_over_thread_id() -> Result<()> { let server = create_mock_responses_server_repeating_assistant("Done").await; diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 9a1edde8d55..6ca09473eb9 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -931,15 +931,24 @@ impl Session { // Build and record initial items (user instructions + environment context) let items = self.build_initial_context(&turn_context).await; self.record_conversation_items(&turn_context, &items).await; + { + let mut state = self.state.lock().await; + state.initial_context_seeded = true; + state.persist_initial_context_on_seed = true; + } // Ensure initial items are visible to immediate readers (e.g., tests, forks). self.flush_rollout().await; } InitialHistory::Resumed(resumed_history) => { let rollout_items = resumed_history.history; - let persist_rollout_items = false; - let persist_initial_context = resumed_history.persist_initial_context; let is_resumed = true; + { + let mut state = self.state.lock().await; + state.initial_context_seeded = false; + state.persist_initial_context_on_seed = resumed_history.persist_initial_context; + } + // If resuming, warn when the last recorded model differs from the current one. if is_resumed && let Some(prev) = rollout_items.iter().rev().find_map(|it| { @@ -984,21 +993,8 @@ impl Session { state.set_token_info(Some(info)); } - // If persisting, persist all rollout items as-is (recorder filters) - if persist_rollout_items && !rollout_items.is_empty() { - self.persist_rollout_items(&rollout_items).await; - } - - // Append the current session's initial context after the reconstructed history. - let initial_context = self.build_initial_context(&turn_context).await; - if persist_initial_context { - self.record_conversation_items(&turn_context, &initial_context) - .await; - } else { - self.record_into_history(&initial_context, &turn_context) - .await; - } - // Flush after seeding history and any persisted rollout copy. + // Defer seeding the session's initial context until the first turn starts so + // turn/start overrides can be merged before we write to the rollout. self.flush_rollout().await; } InitialHistory::Forked(rollout_items) => { @@ -1027,6 +1023,11 @@ impl Session { let initial_context = self.build_initial_context(&turn_context).await; self.record_conversation_items(&turn_context, &initial_context) .await; + { + let mut state = self.state.lock().await; + state.initial_context_seeded = true; + state.persist_initial_context_on_seed = true; + } // Flush after seeding history and any persisted rollout copy. self.flush_rollout().await; } @@ -1677,6 +1678,27 @@ impl Session { state.replace_history(items); } + pub(crate) async fn seed_initial_context_if_needed(&self, turn_context: &TurnContext) { + let should_persist = { + let mut state = self.state.lock().await; + if state.initial_context_seeded { + return; + } + state.initial_context_seeded = true; + state.persist_initial_context_on_seed + }; + + let initial_context = self.build_initial_context(turn_context).await; + if should_persist { + self.record_conversation_items(turn_context, &initial_context) + .await; + } else { + self.record_into_history(&initial_context, turn_context) + .await; + } + self.flush_rollout().await; + } + async fn persist_rollout_response_items(&self, items: &[ResponseItem]) { let rollout_items: Vec = items .iter() @@ -2342,6 +2364,12 @@ mod handlers { sub_id: String, updates: SessionSettingsUpdate, ) { + let has_override_updates = updates.cwd.is_some() + || updates.approval_policy.is_some() + || updates.sandbox_policy.is_some() + || updates.collaboration_mode.is_some() + || updates.reasoning_summary.is_some() + || updates.personality.is_some(); let previous_context = sess .new_default_turn_with_sub_id(sess.next_internal_sub_id()) .await; @@ -2366,6 +2394,17 @@ mod handlers { return; } + let initial_context_seeded = { + let mut state = sess.state.lock().await; + if !state.initial_context_seeded && has_override_updates { + state.persist_initial_context_on_seed = true; + } + state.initial_context_seeded + }; + if !initial_context_seeded { + return; + } + let current_context = sess.new_default_turn_with_sub_id(sub_id).await; let update_items = sess.build_settings_update_items( Some(&previous_context), @@ -2453,6 +2492,7 @@ mod handlers { // Attempt to inject input into current task if let Err(items) = sess.inject_input(items).await { + sess.seed_initial_context_if_needed(¤t_context).await; let update_items = sess.build_settings_update_items( previous_context.as_ref(), ¤t_context, @@ -3756,7 +3796,7 @@ mod tests { #[tokio::test] async fn record_initial_history_reconstructs_resumed_transcript() { let (session, turn_context) = make_session_and_context().await; - let (rollout_items, mut expected) = sample_rollout(&session, &turn_context).await; + let (rollout_items, expected) = sample_rollout(&session, &turn_context).await; session .record_initial_history(InitialHistory::Resumed(ResumedHistory { @@ -3767,11 +3807,37 @@ mod tests { })) .await; - expected.extend(session.build_initial_context(&turn_context).await); let history = session.state.lock().await.clone_history(); assert_eq!(expected, history.raw_items()); } + #[tokio::test] + async fn resumed_history_seeds_initial_context_on_first_turn_only() { + let (session, turn_context) = make_session_and_context().await; + let (rollout_items, mut expected) = sample_rollout(&session, &turn_context).await; + + session + .record_initial_history(InitialHistory::Resumed(ResumedHistory { + conversation_id: ThreadId::default(), + history: rollout_items, + rollout_path: PathBuf::from("/tmp/resume.jsonl"), + persist_initial_context: false, + })) + .await; + + let history_before_seed = session.state.lock().await.clone_history(); + assert_eq!(expected, history_before_seed.raw_items()); + + session.seed_initial_context_if_needed(&turn_context).await; + expected.extend(session.build_initial_context(&turn_context).await); + let history_after_seed = session.clone_history().await; + assert_eq!(expected, history_after_seed.raw_items()); + + session.seed_initial_context_if_needed(&turn_context).await; + let history_after_second_seed = session.clone_history().await; + assert_eq!(expected, history_after_second_seed.raw_items()); + } + #[tokio::test] async fn record_initial_history_seeds_token_info_from_rollout() { let (session, turn_context) = make_session_and_context().await; @@ -4385,7 +4451,9 @@ mod tests { session_configuration.session_source.clone(), ); - let state = SessionState::new(session_configuration.clone()); + let mut state = SessionState::new(session_configuration.clone()); + state.initial_context_seeded = true; + state.persist_initial_context_on_seed = true; let skills_manager = Arc::new(SkillsManager::new(config.codex_home.clone())); let services = SessionServices { @@ -4494,7 +4562,9 @@ mod tests { session_configuration.session_source.clone(), ); - let state = SessionState::new(session_configuration.clone()); + let mut state = SessionState::new(session_configuration.clone()); + state.initial_context_seeded = true; + state.persist_initial_context_on_seed = true; let skills_manager = Arc::new(SkillsManager::new(config.codex_home.clone())); let services = SessionServices { diff --git a/codex-rs/core/src/state/session.rs b/codex-rs/core/src/state/session.rs index 746396949e2..d7f049bc6d4 100644 --- a/codex-rs/core/src/state/session.rs +++ b/codex-rs/core/src/state/session.rs @@ -15,6 +15,10 @@ pub(crate) struct SessionState { pub(crate) history: ContextManager, pub(crate) latest_rate_limits: Option, pub(crate) server_reasoning_included: bool, + /// Whether the session's initial context has been seeded into history. + pub(crate) initial_context_seeded: bool, + /// Whether seeding the initial context should persist to the rollout file. + pub(crate) persist_initial_context_on_seed: bool, } impl SessionState { @@ -26,6 +30,8 @@ impl SessionState { history, latest_rate_limits: None, server_reasoning_included: false, + initial_context_seeded: false, + persist_initial_context_on_seed: true, } } diff --git a/codex-rs/core/src/tasks/mod.rs b/codex-rs/core/src/tasks/mod.rs index bbf9d9c2761..08d23f79873 100644 --- a/codex-rs/core/src/tasks/mod.rs +++ b/codex-rs/core/src/tasks/mod.rs @@ -115,6 +115,8 @@ impl Session { task: T, ) { self.abort_all_tasks(TurnAbortReason::Replaced).await; + self.seed_initial_context_if_needed(turn_context.as_ref()) + .await; let task: Arc = Arc::new(task); let task_kind = task.kind(); From ed9e252c95fdaaf76d292ace6c24ca0b31dc1932 Mon Sep 17 00:00:00 2001 From: Owen Lin Date: Mon, 26 Jan 2026 21:05:16 -0800 Subject: [PATCH 4/6] cleanup --- .../app-server/src/codex_message_processor.rs | 50 ++++++--- .../tests/suite/v2/thread_resume.rs | 104 +++++++++--------- codex-rs/core/src/codex.rs | 74 +++++++------ 3 files changed, 126 insertions(+), 102 deletions(-) diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index b9cec97f15b..24db2e85db7 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -1567,6 +1567,32 @@ impl CodexMessageProcessor { } } + #[allow(clippy::too_many_arguments)] + fn has_thread_resume_overrides( + model: bool, + model_provider: bool, + cwd: bool, + approval_policy: bool, + sandbox: bool, + request_overrides: Option<&HashMap>, + base_instructions: bool, + developer_instructions: bool, + personality: bool, + ) -> bool { + let has_request_overrides = + request_overrides.is_some_and(|overrides| !overrides.is_empty()); + + model + || model_provider + || cwd + || approval_policy + || sandbox + || has_request_overrides + || base_instructions + || developer_instructions + || personality + } + #[allow(clippy::too_many_arguments)] fn build_thread_config_overrides( &self, @@ -1988,19 +2014,17 @@ impl CodexMessageProcessor { } }; - let has_request_overrides = request_overrides - .as_ref() - .is_some_and(|overrides| !overrides.is_empty()); - - let has_resume_overrides = model.is_some() - || model_provider.is_some() - || cwd.is_some() - || approval_policy.is_some() - || sandbox.is_some() - || has_request_overrides - || base_instructions.is_some() - || developer_instructions.is_some() - || personality.is_some(); + let has_resume_overrides = Self::has_thread_resume_overrides( + model.is_some(), + model_provider.is_some(), + cwd.is_some(), + approval_policy.is_some(), + sandbox.is_some(), + request_overrides.as_ref(), + base_instructions.is_some(), + developer_instructions.is_some(), + personality.is_some(), + ); if !has_resume_overrides && let InitialHistory::Resumed(resumed) = &mut thread_history { resumed.persist_initial_context = false; diff --git a/codex-rs/app-server/tests/suite/v2/thread_resume.rs b/codex-rs/app-server/tests/suite/v2/thread_resume.rs index d86079846d4..91cc41d365e 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_resume.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_resume.rs @@ -155,31 +155,14 @@ async fn thread_resume_returns_rollout_history() -> Result<()> { async fn thread_resume_without_overrides_does_not_change_updated_at_or_mtime() -> Result<()> { let server = create_mock_responses_server_repeating_assistant("Done").await; let codex_home = TempDir::new()?; - create_config_toml(codex_home.path(), &server.uri())?; - - let preview = "Saved user message"; - let filename_ts = "2025-01-05T12-00-00"; - let meta_rfc3339 = "2025-01-05T12:00:00Z"; - let expected_updated_at_rfc3339 = "2025-01-07T00:00:00Z"; - let conversation_id = create_fake_rollout_with_text_elements( - codex_home.path(), - filename_ts, - meta_rfc3339, - preview, - Vec::new(), - Some("mock_provider"), - None, - )?; - let rollout_file_path = rollout_path(codex_home.path(), filename_ts, &conversation_id); - set_rollout_mtime(rollout_file_path.as_path(), expected_updated_at_rfc3339)?; - let before_modified = std::fs::metadata(&rollout_file_path)?.modified()?; + let rollout = setup_rollout_fixture(codex_home.path(), &server.uri())?; let mut mcp = McpProcess::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let resume_id = mcp .send_thread_resume_request(ThreadResumeParams { - thread_id: conversation_id, + thread_id: rollout.conversation_id, ..Default::default() }) .await?; @@ -190,13 +173,10 @@ async fn thread_resume_without_overrides_does_not_change_updated_at_or_mtime() - .await??; let ThreadResumeResponse { thread, .. } = to_response::(resume_resp)?; - let expected_updated_at = chrono::DateTime::parse_from_rfc3339(expected_updated_at_rfc3339)? - .with_timezone(&Utc) - .timestamp(); - assert_eq!(thread.updated_at, expected_updated_at); + assert_eq!(thread.updated_at, rollout.expected_updated_at); - let after_modified = std::fs::metadata(&rollout_file_path)?.modified()?; - assert_eq!(after_modified, before_modified); + let after_modified = std::fs::metadata(&rollout.rollout_file_path)?.modified()?; + assert_eq!(after_modified, rollout.before_modified); Ok(()) } @@ -205,31 +185,14 @@ async fn thread_resume_without_overrides_does_not_change_updated_at_or_mtime() - async fn thread_resume_with_overrides_defers_updated_at_until_turn_start() -> Result<()> { let server = create_mock_responses_server_repeating_assistant("Done").await; let codex_home = TempDir::new()?; - create_config_toml(codex_home.path(), &server.uri())?; - - let preview = "Saved user message"; - let filename_ts = "2025-01-05T12-00-00"; - let meta_rfc3339 = "2025-01-05T12:00:00Z"; - let expected_updated_at_rfc3339 = "2025-01-07T00:00:00Z"; - let conversation_id = create_fake_rollout_with_text_elements( - codex_home.path(), - filename_ts, - meta_rfc3339, - preview, - Vec::new(), - Some("mock_provider"), - None, - )?; - let rollout_file_path = rollout_path(codex_home.path(), filename_ts, &conversation_id); - set_rollout_mtime(rollout_file_path.as_path(), expected_updated_at_rfc3339)?; - let before_modified = std::fs::metadata(&rollout_file_path)?.modified()?; + let rollout = setup_rollout_fixture(codex_home.path(), &server.uri())?; let mut mcp = McpProcess::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let resume_id = mcp .send_thread_resume_request(ThreadResumeParams { - thread_id: conversation_id.clone(), + thread_id: rollout.conversation_id.clone(), model: Some("mock-model".to_string()), ..Default::default() }) @@ -241,17 +204,14 @@ async fn thread_resume_with_overrides_defers_updated_at_until_turn_start() -> Re .await??; let ThreadResumeResponse { thread, .. } = to_response::(resume_resp)?; - let expected_updated_at = chrono::DateTime::parse_from_rfc3339(expected_updated_at_rfc3339)? - .with_timezone(&Utc) - .timestamp(); - assert_eq!(thread.updated_at, expected_updated_at); + assert_eq!(thread.updated_at, rollout.expected_updated_at); - let after_resume_modified = std::fs::metadata(&rollout_file_path)?.modified()?; - assert_eq!(after_resume_modified, before_modified); + let after_resume_modified = std::fs::metadata(&rollout.rollout_file_path)?.modified()?; + assert_eq!(after_resume_modified, rollout.before_modified); let turn_id = mcp .send_turn_start_request(TurnStartParams { - thread_id: conversation_id, + thread_id: rollout.conversation_id, input: vec![UserInput::Text { text: "Hello".to_string(), text_elements: Vec::new(), @@ -270,8 +230,8 @@ async fn thread_resume_with_overrides_defers_updated_at_until_turn_start() -> Re ) .await??; - let after_turn_modified = std::fs::metadata(&rollout_file_path)?.modified()?; - assert!(after_turn_modified > before_modified); + let after_turn_modified = std::fs::metadata(&rollout.rollout_file_path)?.modified()?; + assert!(after_turn_modified > rollout.before_modified); Ok(()) } @@ -503,3 +463,41 @@ fn set_rollout_mtime(path: &Path, updated_at_rfc3339: &str) -> Result<()> { .set_times(times)?; Ok(()) } + +struct RolloutFixture { + conversation_id: String, + rollout_file_path: PathBuf, + before_modified: std::time::SystemTime, + expected_updated_at: i64, +} + +fn setup_rollout_fixture(codex_home: &Path, server_uri: &str) -> Result { + create_config_toml(codex_home, server_uri)?; + + let preview = "Saved user message"; + let filename_ts = "2025-01-05T12-00-00"; + let meta_rfc3339 = "2025-01-05T12:00:00Z"; + let expected_updated_at_rfc3339 = "2025-01-07T00:00:00Z"; + let conversation_id = create_fake_rollout_with_text_elements( + codex_home, + filename_ts, + meta_rfc3339, + preview, + Vec::new(), + Some("mock_provider"), + None, + )?; + let rollout_file_path = rollout_path(codex_home, filename_ts, &conversation_id); + set_rollout_mtime(rollout_file_path.as_path(), expected_updated_at_rfc3339)?; + let before_modified = std::fs::metadata(&rollout_file_path)?.modified()?; + let expected_updated_at = chrono::DateTime::parse_from_rfc3339(expected_updated_at_rfc3339)? + .with_timezone(&Utc) + .timestamp(); + + Ok(RolloutFixture { + conversation_id, + rollout_file_path, + before_modified, + expected_updated_at, + }) +} diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 6ca09473eb9..66be52926ee 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -931,34 +931,23 @@ impl Session { // Build and record initial items (user instructions + environment context) let items = self.build_initial_context(&turn_context).await; self.record_conversation_items(&turn_context, &items).await; - { - let mut state = self.state.lock().await; - state.initial_context_seeded = true; - state.persist_initial_context_on_seed = true; - } + self.mark_initial_context_seeded().await; // Ensure initial items are visible to immediate readers (e.g., tests, forks). self.flush_rollout().await; } InitialHistory::Resumed(resumed_history) => { let rollout_items = resumed_history.history; - let is_resumed = true; - - { - let mut state = self.state.lock().await; - state.initial_context_seeded = false; - state.persist_initial_context_on_seed = resumed_history.persist_initial_context; - } + self.set_initial_context_seed_state(false, resumed_history.persist_initial_context) + .await; // If resuming, warn when the last recorded model differs from the current one. - if is_resumed - && let Some(prev) = rollout_items.iter().rev().find_map(|it| { - if let RolloutItem::TurnContext(ctx) = it { - Some(ctx.model.as_str()) - } else { - None - } - }) - { + if let Some(prev) = rollout_items.iter().rev().find_map(|it| { + if let RolloutItem::TurnContext(ctx) = it { + Some(ctx.model.as_str()) + } else { + None + } + }) { let curr = turn_context.client.get_model(); if prev != curr { warn!( @@ -1023,11 +1012,7 @@ impl Session { let initial_context = self.build_initial_context(&turn_context).await; self.record_conversation_items(&turn_context, &initial_context) .await; - { - let mut state = self.state.lock().await; - state.initial_context_seeded = true; - state.persist_initial_context_on_seed = true; - } + self.mark_initial_context_seeded().await; // Flush after seeding history and any persisted rollout copy. self.flush_rollout().await; } @@ -1678,6 +1663,16 @@ impl Session { state.replace_history(items); } + async fn set_initial_context_seed_state(&self, seeded: bool, persist: bool) { + let mut state = self.state.lock().await; + state.initial_context_seeded = seeded; + state.persist_initial_context_on_seed = persist; + } + + async fn mark_initial_context_seeded(&self) { + self.set_initial_context_seed_state(true, true).await; + } + pub(crate) async fn seed_initial_context_if_needed(&self, turn_context: &TurnContext) { let should_persist = { let mut state = self.state.lock().await; @@ -2364,12 +2359,7 @@ mod handlers { sub_id: String, updates: SessionSettingsUpdate, ) { - let has_override_updates = updates.cwd.is_some() - || updates.approval_policy.is_some() - || updates.sandbox_policy.is_some() - || updates.collaboration_mode.is_some() - || updates.reasoning_summary.is_some() - || updates.personality.is_some(); + let has_override_updates = has_override_updates(&updates); let previous_context = sess .new_default_turn_with_sub_id(sess.next_internal_sub_id()) .await; @@ -2418,6 +2408,15 @@ mod handlers { } } + fn has_override_updates(updates: &SessionSettingsUpdate) -> bool { + updates.cwd.is_some() + || updates.approval_policy.is_some() + || updates.sandbox_policy.is_some() + || updates.collaboration_mode.is_some() + || updates.reasoning_summary.is_some() + || updates.personality.is_some() + } + pub async fn user_input_or_turn( sess: &Arc, sub_id: String, @@ -4452,8 +4451,7 @@ mod tests { ); let mut state = SessionState::new(session_configuration.clone()); - state.initial_context_seeded = true; - state.persist_initial_context_on_seed = true; + mark_state_initial_context_seeded(&mut state); let skills_manager = Arc::new(SkillsManager::new(config.codex_home.clone())); let services = SessionServices { @@ -4563,8 +4561,7 @@ mod tests { ); let mut state = SessionState::new(session_configuration.clone()); - state.initial_context_seeded = true; - state.persist_initial_context_on_seed = true; + mark_state_initial_context_seeded(&mut state); let skills_manager = Arc::new(SkillsManager::new(config.codex_home.clone())); let services = SessionServices { @@ -4610,6 +4607,11 @@ mod tests { (session, turn_context, rx_event) } + fn mark_state_initial_context_seeded(state: &mut SessionState) { + state.initial_context_seeded = true; + state.persist_initial_context_on_seed = true; + } + #[tokio::test] async fn refresh_mcp_servers_is_deferred_until_next_turn() { let (session, turn_context) = make_session_and_context().await; From f7d69143a7a557e420d8f4183444b92502aedb70 Mon Sep 17 00:00:00 2001 From: Owen Lin Date: Mon, 26 Jan 2026 21:18:55 -0800 Subject: [PATCH 5/6] cleanup --- .../app-server/src/codex_message_processor.rs | 44 +------------ .../tests/suite/v2/thread_resume.rs | 27 +++++++- codex-rs/core/src/codex.rs | 62 +++++-------------- codex-rs/core/src/rollout/recorder.rs | 1 - codex-rs/core/src/state/session.rs | 3 - codex-rs/core/tests/suite/resume_warning.rs | 1 - codex-rs/protocol/src/protocol.rs | 15 ----- 7 files changed, 44 insertions(+), 109 deletions(-) diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index 24db2e85db7..ab2f31fc51c 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -1567,32 +1567,6 @@ impl CodexMessageProcessor { } } - #[allow(clippy::too_many_arguments)] - fn has_thread_resume_overrides( - model: bool, - model_provider: bool, - cwd: bool, - approval_policy: bool, - sandbox: bool, - request_overrides: Option<&HashMap>, - base_instructions: bool, - developer_instructions: bool, - personality: bool, - ) -> bool { - let has_request_overrides = - request_overrides.is_some_and(|overrides| !overrides.is_empty()); - - model - || model_provider - || cwd - || approval_policy - || sandbox - || has_request_overrides - || base_instructions - || developer_instructions - || personality - } - #[allow(clippy::too_many_arguments)] fn build_thread_config_overrides( &self, @@ -1940,7 +1914,7 @@ impl CodexMessageProcessor { personality, } = params; - let mut thread_history = if let Some(history) = history { + let thread_history = if let Some(history) = history { if history.is_empty() { self.send_invalid_request_error( request_id, @@ -2014,22 +1988,6 @@ impl CodexMessageProcessor { } }; - let has_resume_overrides = Self::has_thread_resume_overrides( - model.is_some(), - model_provider.is_some(), - cwd.is_some(), - approval_policy.is_some(), - sandbox.is_some(), - request_overrides.as_ref(), - base_instructions.is_some(), - developer_instructions.is_some(), - personality.is_some(), - ); - - if !has_resume_overrides && let InitialHistory::Resumed(resumed) = &mut thread_history { - resumed.persist_initial_context = false; - } - let history_cwd = thread_history.session_cwd(); let typesafe_overrides = self.build_thread_config_overrides( model, diff --git a/codex-rs/app-server/tests/suite/v2/thread_resume.rs b/codex-rs/app-server/tests/suite/v2/thread_resume.rs index 91cc41d365e..af746b37b76 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_resume.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_resume.rs @@ -156,13 +156,14 @@ async fn thread_resume_without_overrides_does_not_change_updated_at_or_mtime() - let server = create_mock_responses_server_repeating_assistant("Done").await; let codex_home = TempDir::new()?; let rollout = setup_rollout_fixture(codex_home.path(), &server.uri())?; + let thread_id = rollout.conversation_id.clone(); let mut mcp = McpProcess::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let resume_id = mcp .send_thread_resume_request(ThreadResumeParams { - thread_id: rollout.conversation_id, + thread_id: thread_id.clone(), ..Default::default() }) .await?; @@ -178,6 +179,30 @@ async fn thread_resume_without_overrides_does_not_change_updated_at_or_mtime() - let after_modified = std::fs::metadata(&rollout.rollout_file_path)?.modified()?; assert_eq!(after_modified, rollout.before_modified); + let turn_id = mcp + .send_turn_start_request(TurnStartParams { + thread_id, + input: vec![UserInput::Text { + text: "Hello".to_string(), + text_elements: Vec::new(), + }], + ..Default::default() + }) + .await?; + timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(turn_id)), + ) + .await??; + timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_notification_message("turn/completed"), + ) + .await??; + + let after_turn_modified = std::fs::metadata(&rollout.rollout_file_path)?.modified()?; + assert!(after_turn_modified > rollout.before_modified); + Ok(()) } diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 66be52926ee..73cd4ba7776 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -931,14 +931,19 @@ impl Session { // Build and record initial items (user instructions + environment context) let items = self.build_initial_context(&turn_context).await; self.record_conversation_items(&turn_context, &items).await; - self.mark_initial_context_seeded().await; + { + let mut state = self.state.lock().await; + state.initial_context_seeded = true; + } // Ensure initial items are visible to immediate readers (e.g., tests, forks). self.flush_rollout().await; } InitialHistory::Resumed(resumed_history) => { let rollout_items = resumed_history.history; - self.set_initial_context_seed_state(false, resumed_history.persist_initial_context) - .await; + { + let mut state = self.state.lock().await; + state.initial_context_seeded = false; + } // If resuming, warn when the last recorded model differs from the current one. if let Some(prev) = rollout_items.iter().rev().find_map(|it| { @@ -1012,7 +1017,10 @@ impl Session { let initial_context = self.build_initial_context(&turn_context).await; self.record_conversation_items(&turn_context, &initial_context) .await; - self.mark_initial_context_seeded().await; + { + let mut state = self.state.lock().await; + state.initial_context_seeded = true; + } // Flush after seeding history and any persisted rollout copy. self.flush_rollout().await; } @@ -1663,34 +1671,18 @@ impl Session { state.replace_history(items); } - async fn set_initial_context_seed_state(&self, seeded: bool, persist: bool) { - let mut state = self.state.lock().await; - state.initial_context_seeded = seeded; - state.persist_initial_context_on_seed = persist; - } - - async fn mark_initial_context_seeded(&self) { - self.set_initial_context_seed_state(true, true).await; - } - pub(crate) async fn seed_initial_context_if_needed(&self, turn_context: &TurnContext) { - let should_persist = { + { let mut state = self.state.lock().await; if state.initial_context_seeded { return; } state.initial_context_seeded = true; - state.persist_initial_context_on_seed - }; + } let initial_context = self.build_initial_context(turn_context).await; - if should_persist { - self.record_conversation_items(turn_context, &initial_context) - .await; - } else { - self.record_into_history(&initial_context, turn_context) - .await; - } + self.record_conversation_items(turn_context, &initial_context) + .await; self.flush_rollout().await; } @@ -2359,7 +2351,6 @@ mod handlers { sub_id: String, updates: SessionSettingsUpdate, ) { - let has_override_updates = has_override_updates(&updates); let previous_context = sess .new_default_turn_with_sub_id(sess.next_internal_sub_id()) .await; @@ -2384,13 +2375,7 @@ mod handlers { return; } - let initial_context_seeded = { - let mut state = sess.state.lock().await; - if !state.initial_context_seeded && has_override_updates { - state.persist_initial_context_on_seed = true; - } - state.initial_context_seeded - }; + let initial_context_seeded = sess.state.lock().await.initial_context_seeded; if !initial_context_seeded { return; } @@ -2408,15 +2393,6 @@ mod handlers { } } - fn has_override_updates(updates: &SessionSettingsUpdate) -> bool { - updates.cwd.is_some() - || updates.approval_policy.is_some() - || updates.sandbox_policy.is_some() - || updates.collaboration_mode.is_some() - || updates.reasoning_summary.is_some() - || updates.personality.is_some() - } - pub async fn user_input_or_turn( sess: &Arc, sub_id: String, @@ -3802,7 +3778,6 @@ mod tests { conversation_id: ThreadId::default(), history: rollout_items, rollout_path: PathBuf::from("/tmp/resume.jsonl"), - persist_initial_context: true, })) .await; @@ -3820,7 +3795,6 @@ mod tests { conversation_id: ThreadId::default(), history: rollout_items, rollout_path: PathBuf::from("/tmp/resume.jsonl"), - persist_initial_context: false, })) .await; @@ -3907,7 +3881,6 @@ mod tests { conversation_id: ThreadId::default(), history: rollout_items, rollout_path: PathBuf::from("/tmp/resume.jsonl"), - persist_initial_context: true, })) .await; @@ -4609,7 +4582,6 @@ mod tests { fn mark_state_initial_context_seeded(state: &mut SessionState) { state.initial_context_seeded = true; - state.persist_initial_context_on_seed = true; } #[tokio::test] diff --git a/codex-rs/core/src/rollout/recorder.rs b/codex-rs/core/src/rollout/recorder.rs index c470952f0b3..53425051cf0 100644 --- a/codex-rs/core/src/rollout/recorder.rs +++ b/codex-rs/core/src/rollout/recorder.rs @@ -349,7 +349,6 @@ impl RolloutRecorder { conversation_id, history: items, rollout_path: path.to_path_buf(), - persist_initial_context: true, })) } diff --git a/codex-rs/core/src/state/session.rs b/codex-rs/core/src/state/session.rs index d7f049bc6d4..323c75c548a 100644 --- a/codex-rs/core/src/state/session.rs +++ b/codex-rs/core/src/state/session.rs @@ -17,8 +17,6 @@ pub(crate) struct SessionState { pub(crate) server_reasoning_included: bool, /// Whether the session's initial context has been seeded into history. pub(crate) initial_context_seeded: bool, - /// Whether seeding the initial context should persist to the rollout file. - pub(crate) persist_initial_context_on_seed: bool, } impl SessionState { @@ -31,7 +29,6 @@ impl SessionState { latest_rate_limits: None, server_reasoning_included: false, initial_context_seeded: false, - persist_initial_context_on_seed: true, } } diff --git a/codex-rs/core/tests/suite/resume_warning.rs b/codex-rs/core/tests/suite/resume_warning.rs index e639e6d41d2..5e5fc6d74aa 100644 --- a/codex-rs/core/tests/suite/resume_warning.rs +++ b/codex-rs/core/tests/suite/resume_warning.rs @@ -40,7 +40,6 @@ fn resume_history( conversation_id: ThreadId::default(), history: vec![RolloutItem::TurnContext(turn_ctx)], rollout_path: rollout_path.to_path_buf(), - persist_initial_context: true, }) } diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index d982f081b94..e9cb8147d7b 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -1418,17 +1418,6 @@ pub struct ResumedHistory { pub conversation_id: ThreadId, pub history: Vec, pub rollout_path: PathBuf, - /// Whether to persist the initial context when resuming this rollout. - /// When true, the resumed session's initial context (instructions and environment) - /// is appended to the rollout file. When false, it is only added to in-memory history. - /// - /// This helps avoid duplicating unchanged context on resume without overrides, and - /// unnecessarily changing the rollout file's mtime which is used as the Thread's updated_at - /// timestamp. - /// - /// Defaults to true to preserve existing behavior for older serialized data. - #[serde(default = "resumed_history_default_persist_initial_context")] - pub persist_initial_context: bool, } #[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)] @@ -1514,10 +1503,6 @@ impl InitialHistory { } } -fn resumed_history_default_persist_initial_context() -> bool { - true -} - fn session_cwd_from_items(items: &[RolloutItem]) -> Option { items.iter().find_map(|item| match item { RolloutItem::SessionMeta(meta_line) => Some(meta_line.meta.cwd.clone()), From 31590809eefc0c1d40da2a337d8650d0aa9012ae Mon Sep 17 00:00:00 2001 From: Owen Lin Date: Tue, 27 Jan 2026 09:52:09 -0800 Subject: [PATCH 6/6] document --- codex-rs/core/src/state/session.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/codex-rs/core/src/state/session.rs b/codex-rs/core/src/state/session.rs index 323c75c548a..067f7b33789 100644 --- a/codex-rs/core/src/state/session.rs +++ b/codex-rs/core/src/state/session.rs @@ -16,6 +16,9 @@ pub(crate) struct SessionState { pub(crate) latest_rate_limits: Option, pub(crate) server_reasoning_included: bool, /// Whether the session's initial context has been seeded into history. + /// + /// TODO(owen): This is a temporary solution to avoid updating a thread's updated_at + /// timestamp when resuming a session. Remove this once SQLite is in place. pub(crate) initial_context_seeded: bool, }