diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 21a72b00c5..63e459a918 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -801,6 +801,7 @@ impl Session { InitialHistory::Resumed(_) | InitialHistory::Forked(_) => { let rollout_items = conversation_history.get_rollout_items(); let persist = matches!(conversation_history, InitialHistory::Forked(_)); + let append_env_context = matches!(conversation_history, InitialHistory::Resumed(_)); // If resuming, warn when the last recorded model differs from the current one. if let InitialHistory::Resumed(_) = conversation_history @@ -845,6 +846,12 @@ impl Session { state.set_token_info(Some(info)); } + if append_env_context { + let env_item = self.build_environment_context_item(&turn_context); + self.record_conversation_items(&turn_context, std::slice::from_ref(&env_item)) + .await; + } + // If persisting, persist all rollout items as-is (recorder filters) if persist && !rollout_items.is_empty() { self.persist_rollout_items(&rollout_items).await; @@ -1333,9 +1340,18 @@ impl Session { } } + fn build_environment_context_item(&self, turn_context: &TurnContext) -> ResponseItem { + let shell = self.user_shell(); + ResponseItem::from(EnvironmentContext::new( + Some(turn_context.cwd.clone()), + Some(turn_context.approval_policy), + Some(turn_context.sandbox_policy.clone()), + shell.as_ref().clone(), + )) + } + pub(crate) fn build_initial_context(&self, turn_context: &TurnContext) -> Vec { let mut items = Vec::::with_capacity(3); - let shell = self.user_shell(); if let Some(developer_instructions) = turn_context.developer_instructions.as_deref() { items.push(DeveloperInstructions::new(developer_instructions.to_string()).into()); } @@ -1348,12 +1364,7 @@ impl Session { .into(), ); } - items.push(ResponseItem::from(EnvironmentContext::new( - Some(turn_context.cwd.clone()), - Some(turn_context.approval_policy), - Some(turn_context.sandbox_policy.clone()), - shell.as_ref().clone(), - ))); + items.push(self.build_environment_context_item(turn_context)); items } @@ -2925,6 +2936,8 @@ mod tests { async fn record_initial_history_reconstructs_resumed_transcript() { let (session, turn_context) = make_session_and_context().await; let (rollout_items, expected) = sample_rollout(&session, &turn_context); + let mut expected = expected; + expected.push(session.build_environment_context_item(&turn_context)); session .record_initial_history(InitialHistory::Resumed(ResumedHistory { diff --git a/codex-rs/core/tests/suite/resume.rs b/codex-rs/core/tests/suite/resume.rs index 1fee3858e2..0b733c11a8 100644 --- a/codex-rs/core/tests/suite/resume.rs +++ b/codex-rs/core/tests/suite/resume.rs @@ -1,4 +1,7 @@ use anyhow::Result; +use codex_core::config::Constrained; +use codex_core::protocol::AskForApproval; +use codex_core::protocol::ENVIRONMENT_CONTEXT_OPEN_TAG; use codex_core::protocol::EventMsg; use codex_core::protocol::Op; use codex_protocol::user_input::UserInput; @@ -12,6 +15,7 @@ use core_test_support::responses::start_mock_server; use core_test_support::skip_if_no_network; use core_test_support::test_codex::test_codex; use core_test_support::wait_for_event; +use pretty_assertions::assert_eq; use std::sync::Arc; #[tokio::test(flavor = "multi_thread", worker_threads = 2)] @@ -120,3 +124,78 @@ async fn resume_includes_initial_messages_from_reasoning_events() -> Result<()> Ok(()) } + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn resume_appends_environment_context_on_first_turn() -> Result<()> { + skip_if_no_network!(Ok(())); + + let server = start_mock_server().await; + let mut builder = test_codex(); + let initial = builder.build(&server).await?; + let codex = Arc::clone(&initial.codex); + let home = initial.home.clone(); + let rollout_path = initial.session_configured.rollout_path.clone(); + + let initial_sse = sse(vec![ + ev_response_created("resp-initial"), + ev_assistant_message("msg-1", "Completed first turn"), + ev_completed("resp-initial"), + ]); + mount_sse_once(&server, initial_sse).await; + + codex + .submit(Op::UserInput { + items: vec![UserInput::Text { + text: "Record some messages".into(), + }], + final_output_json_schema: None, + }) + .await?; + + wait_for_event(&codex, |event| matches!(event, EventMsg::TaskComplete(_))).await; + + let mut resume_builder = test_codex().with_config(|config| { + config.approval_policy = Constrained::allow_any(AskForApproval::Never); + }); + let resumed = resume_builder.resume(&server, home, rollout_path).await?; + + let resumed_sse = sse(vec![ + ev_response_created("resp-resume"), + ev_assistant_message("msg-2", "Completed after resume"), + ev_completed("resp-resume"), + ]); + let resumed_mock = mount_sse_once(&server, resumed_sse).await; + + resumed + .codex + .submit(Op::UserInput { + items: vec![UserInput::Text { + text: "After resume".into(), + }], + final_output_json_schema: None, + }) + .await?; + wait_for_event(&resumed.codex, |event| { + matches!(event, EventMsg::TaskComplete(_)) + }) + .await; + + let body = resumed_mock.single_request().body_json(); + let input = body["input"] + .as_array() + .expect("resume request input is an array"); + let env_texts: Vec<&str> = input + .iter() + .filter_map(|item| item["content"][0]["text"].as_str()) + .filter(|text| text.starts_with(ENVIRONMENT_CONTEXT_OPEN_TAG)) + .collect(); + + assert_eq!(env_texts.is_empty(), false); + let last_env = env_texts.last().expect("environment context present"); + assert_eq!( + last_env.contains("never"), + true, + ); + + Ok(()) +}