From d20417c6d144fe2e6d02805d46f688a1ec255ec9 Mon Sep 17 00:00:00 2001 From: Charles Cunningham Date: Tue, 24 Mar 2026 16:47:14 -0700 Subject: [PATCH 1/4] Defer fork context injection until first turn Stop fork startup from appending build_initial_context and preserve the reconstructed reference_context_item as the baseline until the first real turn. Update fork-history coverage and the request snapshot, and leave a TODO for remaining nondiffable initial context inputs. Co-authored-by: Codex --- codex-rs/core/src/codex.rs | 13 +++++------- codex-rs/core/src/codex_tests.rs | 20 ++++++++++--------- ..._startup_context_then_first_turn_diff.snap | 10 ++++------ 3 files changed, 20 insertions(+), 23 deletions(-) diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 2f8c2877f33..d308210cc83 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -2215,14 +2215,11 @@ impl Session { 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; - self.record_conversation_items(&turn_context, &initial_context) - .await; - { - let mut state = self.state.lock().await; - state.set_reference_context_item(Some(turn_context.to_turn_context_item())); - } + // Defer seeding the fork's current-session context until the first real turn so + // turn/start overrides can be merged before we write model-visible context. + // TODO(ccunningham): Some build_initial_context content is still not representable + // as steady-state diffs. Persist the remaining model-visible inputs or add + // explicit replay events so fork/resume can diff everything deterministically. // Forked threads should remain file-backed immediately after startup. self.ensure_rollout_materialized().await; diff --git a/codex-rs/core/src/codex_tests.rs b/codex-rs/core/src/codex_tests.rs index 0ba0a1beb0c..438305a0a6c 100644 --- a/codex-rs/core/src/codex_tests.rs +++ b/codex-rs/core/src/codex_tests.rs @@ -1115,18 +1115,12 @@ async fn recompute_token_usage_updates_model_context_window() { #[tokio::test] async fn record_initial_history_reconstructs_forked_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::Forked(rollout_items)) .await; - let reconstruction_turn = session.new_default_turn().await; - expected.extend( - session - .build_initial_context(reconstruction_turn.as_ref()) - .await, - ); let history = session.state.lock().await.clone_history(); assert_eq!(expected, history.raw_items()); } @@ -1244,7 +1238,7 @@ async fn fork_startup_context_then_first_turn_diff_snapshot() -> anyhow::Result< let request = first_forked_request.single_request(); let snapshot = context_snapshot::format_labeled_requests_snapshot( - "First request after fork when fork startup changes approval policy and the first forked turn changes approval policy again and enters plan mode.", + "First request after fork when startup preserves the parent baseline, the fork changes approval policy, and the first forked turn enters plan mode.", &[("First Forked Turn Request", &request)], &ContextSnapshotOptions::default() .render_mode(ContextSnapshotRenderMode::KindWithTextPrefix { max_chars: 96 }) @@ -1309,7 +1303,7 @@ async fn record_initial_history_forked_hydrates_previous_turn_settings() { text_elements: Vec::new(), }, )), - RolloutItem::TurnContext(previous_context_item), + RolloutItem::TurnContext(previous_context_item.clone()), RolloutItem::EventMsg(EventMsg::TurnComplete( codex_protocol::protocol::TurnCompleteEvent { turn_id, @@ -1322,6 +1316,7 @@ async fn record_initial_history_forked_hydrates_previous_turn_settings() { .record_initial_history(InitialHistory::Forked(rollout_items)) .await; + let history = session.clone_history().await; assert_eq!( session.previous_turn_settings().await, Some(PreviousTurnSettings { @@ -1329,6 +1324,13 @@ async fn record_initial_history_forked_hydrates_previous_turn_settings() { realtime_active: Some(turn_context.realtime_active), }) ); + assert_eq!(history.raw_items(), &[user_message("forked seed")]); + assert_eq!( + serde_json::to_value(session.reference_context_item().await) + .expect("serialize fork reference context item"), + serde_json::to_value(Some(previous_context_item)) + .expect("serialize expected reference context item") + ); } #[tokio::test] diff --git a/codex-rs/core/src/snapshots/codex_core__codex_tests__fork_startup_context_then_first_turn_diff.snap b/codex-rs/core/src/snapshots/codex_core__codex_tests__fork_startup_context_then_first_turn_diff.snap index 90bb82d4091..d55f29a2a7b 100644 --- a/codex-rs/core/src/snapshots/codex_core__codex_tests__fork_startup_context_then_first_turn_diff.snap +++ b/codex-rs/core/src/snapshots/codex_core__codex_tests__fork_startup_context_then_first_turn_diff.snap @@ -1,17 +1,15 @@ --- source: core/src/codex_tests.rs -assertion_line: 1282 +assertion_line: 1254 expression: snapshot --- -Scenario: First request after fork when fork startup changes approval policy and the first forked turn changes approval policy again and enters plan mode. +Scenario: First request after fork when startup preserves the parent baseline, the fork changes approval policy, and the first forked turn enters plan mode. ## First Forked Turn Request 00:message/developer: 01:message/user:> 02:message/user:fork seed -03:message/developer: -04:message/user:> -05:message/developer[2]: +03:message/developer[2]: [01] [02] Fork turn collaboration instructions. -06:message/user:after fork +04:message/user:after fork From 2b0b2e5bd5f3db24c5a9488af859a4a3eab1ceea Mon Sep 17 00:00:00 2001 From: Charles Cunningham Date: Tue, 24 Mar 2026 17:03:54 -0700 Subject: [PATCH 2/4] Move fork diffing TODO to update builder Remove the stale fork-startup TODO block and attach the note to build_settings_update_items, which is where the current diff coverage gap actually lives. Co-authored-by: Codex --- codex-rs/core/src/codex.rs | 6 ------ codex-rs/core/src/context_manager/updates.rs | 4 ++++ 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index d308210cc83..a773c924dea 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -2215,12 +2215,6 @@ impl Session { self.persist_rollout_items(&rollout_items).await; } - // Defer seeding the fork's current-session context until the first real turn so - // turn/start overrides can be merged before we write model-visible context. - // TODO(ccunningham): Some build_initial_context content is still not representable - // as steady-state diffs. Persist the remaining model-visible inputs or add - // explicit replay events so fork/resume can diff everything deterministically. - // Forked threads should remain file-backed immediately after startup. self.ensure_rollout_materialized().await; diff --git a/codex-rs/core/src/context_manager/updates.rs b/codex-rs/core/src/context_manager/updates.rs index c1221879011..34fd41c0833 100644 --- a/codex-rs/core/src/context_manager/updates.rs +++ b/codex-rs/core/src/context_manager/updates.rs @@ -193,6 +193,10 @@ pub(crate) fn build_settings_update_items( exec_policy: &Policy, personality_feature_enabled: bool, ) -> Vec { + // TODO(ccunningham): build_settings_update_items still does not cover every + // model-visible item emitted by build_initial_context. Persist the remaining + // inputs or add explicit replay events so fork/resume can diff everything + // deterministically. let contextual_user_message = build_environment_update_item(previous, next, shell); let developer_update_sections = [ // Keep model-switch instructions first so model-specific guidance is read before From 72c97731866363ca1bf46ed70ec8c9f733de786c Mon Sep 17 00:00:00 2001 From: Charles Cunningham Date: Tue, 24 Mar 2026 17:27:10 -0700 Subject: [PATCH 3/4] Fix fork expectation regressions in tests Update the fork hydration test to match replay semantics for EventMsg-only user turns, and update the permissions suite to expect a single new permissions message on the first forked turn now that fork startup no longer reinjects context. Co-authored-by: Codex --- codex-rs/core/src/codex_tests.rs | 2 +- codex-rs/core/tests/suite/permissions_messages.rs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/codex-rs/core/src/codex_tests.rs b/codex-rs/core/src/codex_tests.rs index 438305a0a6c..12720f2437e 100644 --- a/codex-rs/core/src/codex_tests.rs +++ b/codex-rs/core/src/codex_tests.rs @@ -1324,7 +1324,7 @@ async fn record_initial_history_forked_hydrates_previous_turn_settings() { realtime_active: Some(turn_context.realtime_active), }) ); - assert_eq!(history.raw_items(), &[user_message("forked seed")]); + assert_eq!(history.raw_items(), &[]); assert_eq!( serde_json::to_value(session.reference_context_item().await) .expect("serialize fork reference context item"), diff --git a/codex-rs/core/tests/suite/permissions_messages.rs b/codex-rs/core/tests/suite/permissions_messages.rs index 30180c8cf6e..e71a158797d 100644 --- a/codex-rs/core/tests/suite/permissions_messages.rs +++ b/codex-rs/core/tests/suite/permissions_messages.rs @@ -443,14 +443,14 @@ async fn resume_and_fork_append_permissions_messages() -> Result<()> { let body4 = req4.single_request().body_json(); let input4 = body4["input"].as_array().expect("input array"); let permissions_fork = permissions_texts(input4); - assert_eq!(permissions_fork.len(), permissions_base.len() + 2); + assert_eq!(permissions_fork.len(), permissions_base.len() + 1); assert_eq!( &permissions_fork[..permissions_base.len()], permissions_base.as_slice() ); let new_permissions = &permissions_fork[permissions_base.len()..]; - assert_eq!(new_permissions.len(), 2); - assert_eq!(new_permissions[0], new_permissions[1]); + assert_eq!(new_permissions.len(), 1); + assert_eq!(permissions_fork, permissions_resume); assert!(!permissions_base.contains(&new_permissions[0])); Ok(()) From 5be83e7560d09812bc3f422cccb813990f2144e5 Mon Sep 17 00:00:00 2001 From: Charles Cunningham Date: Tue, 24 Mar 2026 17:47:57 -0700 Subject: [PATCH 4/4] Fix fork rollout truncation test Update the fork-thread suite to expect exact truncated rollout copies after each fork now that fork startup no longer appends extra initial context. Co-authored-by: Codex --- codex-rs/core/tests/suite/fork_thread.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/codex-rs/core/tests/suite/fork_thread.rs b/codex-rs/core/tests/suite/fork_thread.rs index a16e4602048..e24cb74d7c0 100644 --- a/codex-rs/core/tests/suite/fork_thread.rs +++ b/codex-rs/core/tests/suite/fork_thread.rs @@ -125,9 +125,8 @@ async fn fork_thread_twice_drops_to_first_message() { // GetHistory on fork1 flushed; the file is ready. let fork1_items = read_items(&fork1_path); - assert!(fork1_items.len() > expected_after_first.len()); pretty_assertions::assert_eq!( - serde_json::to_value(&fork1_items[..expected_after_first.len()]).unwrap(), + serde_json::to_value(&fork1_items).unwrap(), serde_json::to_value(&expected_after_first).unwrap() ); @@ -156,9 +155,8 @@ async fn fork_thread_twice_drops_to_first_message() { .unwrap_or(0); let expected_after_second: Vec = fork1_items[..cut_last_on_fork1].to_vec(); let fork2_items = read_items(&fork2_path); - assert!(fork2_items.len() > expected_after_second.len()); pretty_assertions::assert_eq!( - serde_json::to_value(&fork2_items[..expected_after_second.len()]).unwrap(), + serde_json::to_value(&fork2_items).unwrap(), serde_json::to_value(&expected_after_second).unwrap() ); }