From 2d4123c267de909db05cab48c7d6d3a6bcdda548 Mon Sep 17 00:00:00 2001
From: Charles Cunningham <ccunningham@openai.com>
Date: Fri, 13 Feb 2026 18:03:53 -0800
Subject: [PATCH 01/80] Fix model switch compaction

---
 codex-rs/core/src/codex.rs | 50 +++++++++++++++++++++++++++++---------
 1 file changed, 38 insertions(+), 12 deletions(-)

diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs
index 7744480ab418..2e29b5c361d3 100644
--- a/codex-rs/core/src/codex.rs
+++ b/codex-rs/core/src/codex.rs
@@ -4479,7 +4479,7 @@ pub(crate) async fn run_turn(
 
                 // as long as compaction works well in getting us way below the token limit, we shouldn't worry about being in an infinite loop.
                 if token_limit_reached && needs_follow_up {
-                    if run_auto_compact(&sess, &turn_context).await.is_err() {
+                    if run_auto_compact(&sess, &turn_context, false).await.is_err() {
                         return None;
                     }
                     continue;
@@ -4586,12 +4586,23 @@ async fn run_pre_sampling_compact(
     turn_context: &Arc<TurnContext>,
 ) -> CodexResult<()> {
     let total_usage_tokens_before_compaction = sess.get_total_token_usage().await;
-    maybe_run_previous_model_inline_compact(
+    let previous_model = sess.previous_model().await;
+    let previous_model_compaction_ran = maybe_run_previous_model_inline_compact(
         sess,
         turn_context,
         total_usage_tokens_before_compaction,
     )
     .await?;
+    if previous_model_compaction_ran
+        && let Some(model_switch_item) = sess.build_model_instructions_update_item(
+            None,
+            previous_model.as_deref(),
+            turn_context.as_ref(),
+        )
+    {
+        sess.record_conversation_items(turn_context.as_ref(), &[model_switch_item])
+            .await;
+    }
     let total_usage_tokens = sess.get_total_token_usage().await;
     let auto_compact_limit = turn_context
         .model_info
@@ -4599,7 +4610,7 @@ async fn run_pre_sampling_compact(
         .unwrap_or(i64::MAX);
     // Compact if the total usage tokens are greater than the auto compact limit
     if total_usage_tokens >= auto_compact_limit {
-        run_auto_compact(sess, turn_context).await?;
+        run_auto_compact(sess, turn_context, false).await?;
     }
     Ok(())
 }
@@ -4614,9 +4625,9 @@ async fn maybe_run_previous_model_inline_compact(
     sess: &Arc<Session>,
     turn_context: &Arc<TurnContext>,
     total_usage_tokens: i64,
-) -> CodexResult<()> {
+) -> CodexResult<bool> {
     let Some(previous_model) = sess.previous_model().await else {
-        return Ok(());
+        return Ok(false);
     };
     let previous_turn_context = Arc::new(
         turn_context
@@ -4625,10 +4636,10 @@ async fn maybe_run_previous_model_inline_compact(
     );
 
     let Some(old_context_window) = previous_turn_context.model_context_window() else {
-        return Ok(());
+        return Ok(false);
     };
     let Some(new_context_window) = turn_context.model_context_window() else {
-        return Ok(());
+        return Ok(false);
     };
     let new_auto_compact_limit = turn_context
         .model_info
@@ -4638,16 +4649,31 @@ async fn maybe_run_previous_model_inline_compact(
         && previous_turn_context.model_info.slug != turn_context.model_info.slug
         && old_context_window > new_context_window;
     if should_run {
-        run_auto_compact(sess, &previous_turn_context).await?;
+        run_auto_compact(sess, &previous_turn_context, true).await?;
+        return Ok(true);
     }
-    Ok(())
+    Ok(false)
 }
 
-async fn run_auto_compact(sess: &Arc<Session>, turn_context: &Arc<TurnContext>) -> CodexResult<()> {
+async fn run_auto_compact(
+    sess: &Arc<Session>,
+    turn_context: &Arc<TurnContext>,
+    strip_trailing_model_switch_update: bool,
+) -> CodexResult<()> {
     if should_use_remote_compact_task(&turn_context.provider) {
-        run_inline_remote_auto_compact_task(Arc::clone(sess), Arc::clone(turn_context)).await?;
+        run_inline_remote_auto_compact_task(
+            Arc::clone(sess),
+            Arc::clone(turn_context),
+            strip_trailing_model_switch_update,
+        )
+        .await?;
     } else {
-        run_inline_auto_compact_task(Arc::clone(sess), Arc::clone(turn_context)).await?;
+        run_inline_auto_compact_task(
+            Arc::clone(sess),
+            Arc::clone(turn_context),
+            strip_trailing_model_switch_update,
+        )
+        .await?;
     }
     Ok(())
 }

From a79bebe5cab8aec533843119a9f631a79d6b83d3 Mon Sep 17 00:00:00 2001
From: Charles Cunningham <ccunningham@openai.com>
Date: Fri, 13 Feb 2026 18:28:27 -0800
Subject: [PATCH 02/80] Simplify

---
 codex-rs/core/src/codex.rs | 50 +++++++++-----------------------------
 1 file changed, 12 insertions(+), 38 deletions(-)

diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs
index 2e29b5c361d3..7744480ab418 100644
--- a/codex-rs/core/src/codex.rs
+++ b/codex-rs/core/src/codex.rs
@@ -4479,7 +4479,7 @@ pub(crate) async fn run_turn(
 
                 // as long as compaction works well in getting us way below the token limit, we shouldn't worry about being in an infinite loop.
                 if token_limit_reached && needs_follow_up {
-                    if run_auto_compact(&sess, &turn_context, false).await.is_err() {
+                    if run_auto_compact(&sess, &turn_context).await.is_err() {
                         return None;
                     }
                     continue;
@@ -4586,23 +4586,12 @@ async fn run_pre_sampling_compact(
     turn_context: &Arc<TurnContext>,
 ) -> CodexResult<()> {
     let total_usage_tokens_before_compaction = sess.get_total_token_usage().await;
-    let previous_model = sess.previous_model().await;
-    let previous_model_compaction_ran = maybe_run_previous_model_inline_compact(
+    maybe_run_previous_model_inline_compact(
         sess,
         turn_context,
         total_usage_tokens_before_compaction,
     )
     .await?;
-    if previous_model_compaction_ran
-        && let Some(model_switch_item) = sess.build_model_instructions_update_item(
-            None,
-            previous_model.as_deref(),
-            turn_context.as_ref(),
-        )
-    {
-        sess.record_conversation_items(turn_context.as_ref(), &[model_switch_item])
-            .await;
-    }
     let total_usage_tokens = sess.get_total_token_usage().await;
     let auto_compact_limit = turn_context
         .model_info
@@ -4610,7 +4599,7 @@ async fn run_pre_sampling_compact(
         .unwrap_or(i64::MAX);
     // Compact if the total usage tokens are greater than the auto compact limit
     if total_usage_tokens >= auto_compact_limit {
-        run_auto_compact(sess, turn_context, false).await?;
+        run_auto_compact(sess, turn_context).await?;
     }
     Ok(())
 }
@@ -4625,9 +4614,9 @@ async fn maybe_run_previous_model_inline_compact(
     sess: &Arc<Session>,
     turn_context: &Arc<TurnContext>,
     total_usage_tokens: i64,
-) -> CodexResult<bool> {
+) -> CodexResult<()> {
     let Some(previous_model) = sess.previous_model().await else {
-        return Ok(false);
+        return Ok(());
     };
     let previous_turn_context = Arc::new(
         turn_context
@@ -4636,10 +4625,10 @@ async fn maybe_run_previous_model_inline_compact(
     );
 
     let Some(old_context_window) = previous_turn_context.model_context_window() else {
-        return Ok(false);
+        return Ok(());
     };
     let Some(new_context_window) = turn_context.model_context_window() else {
-        return Ok(false);
+        return Ok(());
     };
     let new_auto_compact_limit = turn_context
         .model_info
@@ -4649,31 +4638,16 @@ async fn maybe_run_previous_model_inline_compact(
         && previous_turn_context.model_info.slug != turn_context.model_info.slug
         && old_context_window > new_context_window;
     if should_run {
-        run_auto_compact(sess, &previous_turn_context, true).await?;
-        return Ok(true);
+        run_auto_compact(sess, &previous_turn_context).await?;
     }
-    Ok(false)
+    Ok(())
 }
 
-async fn run_auto_compact(
-    sess: &Arc<Session>,
-    turn_context: &Arc<TurnContext>,
-    strip_trailing_model_switch_update: bool,
-) -> CodexResult<()> {
+async fn run_auto_compact(sess: &Arc<Session>, turn_context: &Arc<TurnContext>) -> CodexResult<()> {
     if should_use_remote_compact_task(&turn_context.provider) {
-        run_inline_remote_auto_compact_task(
-            Arc::clone(sess),
-            Arc::clone(turn_context),
-            strip_trailing_model_switch_update,
-        )
-        .await?;
+        run_inline_remote_auto_compact_task(Arc::clone(sess), Arc::clone(turn_context)).await?;
     } else {
-        run_inline_auto_compact_task(
-            Arc::clone(sess),
-            Arc::clone(turn_context),
-            strip_trailing_model_switch_update,
-        )
-        .await?;
+        run_inline_auto_compact_task(Arc::clone(sess), Arc::clone(turn_context)).await?;
     }
     Ok(())
 }

From c7cef7dab177e236a057b78ac1d7daa314de31e9 Mon Sep 17 00:00:00 2001
From: Charles Cunningham <ccunningham@openai.com>
Date: Sat, 14 Feb 2026 18:02:02 -0800
Subject: [PATCH 03/80] update snapshot

---
 ...ng_model_switch_compaction_shapes.snap.new | 31 +++++++++++++++++++
 1 file changed, 31 insertions(+)
 create mode 100644 codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_sampling_model_switch_compaction_shapes.snap.new

diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_sampling_model_switch_compaction_shapes.snap.new b/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_sampling_model_switch_compaction_shapes.snap.new
new file mode 100644
index 000000000000..8d6c4d9b1aa3
--- /dev/null
+++ b/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_sampling_model_switch_compaction_shapes.snap.new
@@ -0,0 +1,31 @@
+---
+source: core/tests/suite/compact.rs
+assertion_line: 1773
+expression: "format_labeled_requests_snapshot(\"Pre-sampling compaction on model switch to a smaller context window: current behavior compacts using prior-turn history only (incoming user message excluded), and the follow-up request carries compacted history plus the new user message.\",\n&[(\"Initial Request (Previous Model)\", &requests[0]),\n(\"Pre-sampling Compaction Request\", &requests[1]),\n(\"Post-Compaction Follow-up Request (Next Model)\", &requests[2]),])"
+---
+Scenario: Pre-sampling compaction on model switch to a smaller context window: current behavior compacts using prior-turn history only (incoming user message excluded), and the follow-up request carries compacted history plus the new user message.
+
+## Initial Request (Previous Model)
+00:message/developer:<PERMISSIONS_INSTRUCTIONS>
+01:message/user:<AGENTS_MD>
+02:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
+03:message/developer:<PERMISSIONS_INSTRUCTIONS>
+04:message/user:before switch
+
+## Pre-sampling Compaction Request
+00:message/developer:<PERMISSIONS_INSTRUCTIONS>
+01:message/user:<AGENTS_MD>
+02:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
+03:message/developer:<PERMISSIONS_INSTRUCTIONS>
+04:message/user:before switch
+05:message/assistant:before switch
+06:message/user:<SUMMARIZATION_PROMPT>
+
+## Post-Compaction Follow-up Request (Next Model)
+00:message/developer:<PERMISSIONS_INSTRUCTIONS>
+01:message/user:<AGENTS_MD>
+02:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
+03:message/user:before switch
+04:message/user:<COMPACTION_SUMMARY>\nPRE_SAMPLING_SUMMARY
+05:message/developer:<model_switch>\nThe user was previously using a different model....
+06:message/user:after switch

From 5c389f9f202566274ee3bff2f61795c9454d3dfa Mon Sep 17 00:00:00 2001
From: Charles Cunningham <ccunningham@openai.com>
Date: Sat, 14 Feb 2026 18:05:09 -0800
Subject: [PATCH 04/80] Update pre-sampling model-switch compaction snapshot

---
 ...ng_model_switch_compaction_shapes.snap.new | 31 -------------------
 1 file changed, 31 deletions(-)
 delete mode 100644 codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_sampling_model_switch_compaction_shapes.snap.new

diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_sampling_model_switch_compaction_shapes.snap.new b/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_sampling_model_switch_compaction_shapes.snap.new
deleted file mode 100644
index 8d6c4d9b1aa3..000000000000
--- a/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_sampling_model_switch_compaction_shapes.snap.new
+++ /dev/null
@@ -1,31 +0,0 @@
----
-source: core/tests/suite/compact.rs
-assertion_line: 1773
-expression: "format_labeled_requests_snapshot(\"Pre-sampling compaction on model switch to a smaller context window: current behavior compacts using prior-turn history only (incoming user message excluded), and the follow-up request carries compacted history plus the new user message.\",\n&[(\"Initial Request (Previous Model)\", &requests[0]),\n(\"Pre-sampling Compaction Request\", &requests[1]),\n(\"Post-Compaction Follow-up Request (Next Model)\", &requests[2]),])"
----
-Scenario: Pre-sampling compaction on model switch to a smaller context window: current behavior compacts using prior-turn history only (incoming user message excluded), and the follow-up request carries compacted history plus the new user message.
-
-## Initial Request (Previous Model)
-00:message/developer:<PERMISSIONS_INSTRUCTIONS>
-01:message/user:<AGENTS_MD>
-02:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
-03:message/developer:<PERMISSIONS_INSTRUCTIONS>
-04:message/user:before switch
-
-## Pre-sampling Compaction Request
-00:message/developer:<PERMISSIONS_INSTRUCTIONS>
-01:message/user:<AGENTS_MD>
-02:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
-03:message/developer:<PERMISSIONS_INSTRUCTIONS>
-04:message/user:before switch
-05:message/assistant:before switch
-06:message/user:<SUMMARIZATION_PROMPT>
-
-## Post-Compaction Follow-up Request (Next Model)
-00:message/developer:<PERMISSIONS_INSTRUCTIONS>
-01:message/user:<AGENTS_MD>
-02:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
-03:message/user:before switch
-04:message/user:<COMPACTION_SUMMARY>\nPRE_SAMPLING_SUMMARY
-05:message/developer:<model_switch>\nThe user was previously using a different model....
-06:message/user:after switch

From e0f99ebaa4093514fdeca3e73348e20e20ab6c31 Mon Sep 17 00:00:00 2001
From: Charles Cunningham <ccunningham@openai.com>
Date: Wed, 11 Feb 2026 12:05:26 -0800
Subject: [PATCH 05/80] compact: split core logic changes from snapshot test
 coverage

---
 codex-rs/core/src/codex.rs                   | 462 +++++++++++++--
 codex-rs/core/src/compact.rs                 | 558 +++++++++++++++++--
 codex-rs/core/src/compact_remote.rs          | 183 +++++-
 codex-rs/core/src/context_manager/history.rs |   2 +-
 codex-rs/core/src/context_manager/mod.rs     |   1 +
 codex-rs/core/src/tasks/compact.rs           |  34 +-
 codex-rs/core/src/tasks/regular.rs           |  10 +
 codex-rs/core/tests/suite/model_switching.rs |  88 +++
 8 files changed, 1228 insertions(+), 110 deletions(-)

diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs
index 7744480ab418..0d30b8d602ee 100644
--- a/codex-rs/core/src/codex.rs
+++ b/codex-rs/core/src/codex.rs
@@ -19,6 +19,8 @@ use crate::analytics_client::build_track_events_context;
 use crate::apps::render_apps_section;
 use crate::commit_attribution::commit_message_trailer_instruction;
 use crate::compact;
+use crate::compact::AutoCompactCallsite;
+use crate::compact::TurnContextReinjection;
 use crate::compact::run_inline_auto_compact_task;
 use crate::compact::should_use_remote_compact_task;
 use crate::compact_remote::run_inline_remote_auto_compact_task;
@@ -126,6 +128,7 @@ use crate::config::types::McpServerConfig;
 use crate::config::types::ShellEnvironmentPolicy;
 use crate::context_manager::ContextManager;
 use crate::context_manager::TotalTokenUsageBreakdown;
+use crate::context_manager::estimate_item_token_count;
 use crate::environment_context::EnvironmentContext;
 use crate::error::CodexErr;
 use crate::error::Result as CodexResult;
@@ -2385,11 +2388,11 @@ impl Session {
                         history.replace(replacement.clone());
                     } else {
                         let user_messages = collect_user_messages(history.raw_items());
-                        let rebuilt = compact::build_compacted_history(
-                            self.build_initial_context(turn_context).await,
+                        let mut rebuilt = self.build_initial_context(turn_context).await;
+                        rebuilt.extend(compact::build_compacted_history(
                             &user_messages,
                             &compacted.message,
-                        );
+                        ));
                         history.replace(rebuilt);
                     }
                 }
@@ -2406,9 +2409,14 @@ impl Session {
         &self,
         turn_context: &TurnContext,
         compacted_history: Vec<ResponseItem>,
+        turn_context_reinjection: TurnContextReinjection,
     ) -> Vec<ResponseItem> {
         let initial_context = self.build_initial_context(turn_context).await;
-        compact::process_compacted_history(compacted_history, &initial_context)
+        compact::process_compacted_history(
+            compacted_history,
+            &initial_context,
+            turn_context_reinjection,
+        )
     }
 
     /// Append ResponseItems to the in-memory conversation history only.
@@ -2499,6 +2507,11 @@ impl Session {
         self.flush_rollout().await;
     }
 
+    pub(crate) async fn mark_initial_context_unseeded_for_next_turn(&self) {
+        let mut state = self.state.lock().await;
+        state.initial_context_seeded = false;
+    }
+
     async fn persist_rollout_response_items(&self, items: &[ResponseItem]) {
         let rollout_items: Vec<RolloutItem> = items
             .iter()
@@ -3399,20 +3412,30 @@ mod handlers {
         // Attempt to inject input into current task.
         if let Err(SteerInputError::NoActiveTurn(items)) = sess.steer_input(items, None).await {
             sess.seed_initial_context_if_needed(&current_context).await;
-            let previous_model = sess.previous_model().await;
-            let update_items = sess.build_settings_update_items(
+            let resumed_model = sess.take_pending_resume_previous_model().await;
+            let pre_turn_context_items = sess.build_settings_update_items(
                 previous_context.as_ref(),
-                previous_model.as_deref(),
+                resumed_model.as_deref(),
                 &current_context,
             );
-            if !update_items.is_empty() {
-                sess.record_conversation_items(&current_context, &update_items)
+            let has_user_input = !items.is_empty();
+            if !has_user_input && !pre_turn_context_items.is_empty() {
+                // Empty-input UserTurn still needs these model-visible updates persisted now.
+                // Otherwise `previous_context` advances and the next non-empty turn computes no diff.
+                sess.record_conversation_items(&current_context, &pre_turn_context_items)
                     .await;
             }
 
             sess.refresh_mcp_servers_if_requested(&current_context)
                 .await;
-            let regular_task = sess.take_startup_regular_task().await.unwrap_or_default();
+            let regular_task = if has_user_input {
+                sess.take_startup_regular_task()
+                    .await
+                    .unwrap_or_default()
+                    .with_pre_turn_context_items(pre_turn_context_items)
+            } else {
+                sess.take_startup_regular_task().await.unwrap_or_default()
+            };
             sess.spawn_task(Arc::clone(&current_context), items, regular_task)
                 .await;
             *previous_context = Some(current_context);
@@ -4241,6 +4264,7 @@ pub(crate) async fn run_turn(
     sess: Arc<Session>,
     turn_context: Arc<TurnContext>,
     input: Vec<UserInput>,
+    pre_turn_context_items: Vec<ResponseItem>,
     prewarmed_client_session: Option<ModelClientSession>,
     cancellation_token: CancellationToken,
 ) -> Option<String> {
@@ -4250,6 +4274,9 @@ pub(crate) async fn run_turn(
 
     let model_info = turn_context.model_info.clone();
     let auto_compact_limit = model_info.auto_compact_token_limit().unwrap_or(i64::MAX);
+    let response_item: ResponseItem = ResponseInputItem::from(input.clone()).into();
+    let mut incoming_turn_items = pre_turn_context_items.clone();
+    incoming_turn_items.push(response_item.clone());
 
     let event = EventMsg::TurnStarted(TurnStartedEvent {
         turn_id: turn_context.sub_id.clone(),
@@ -4257,13 +4284,34 @@ pub(crate) async fn run_turn(
         collaboration_mode_kind: turn_context.collaboration_mode.mode,
     });
     sess.send_event(&turn_context, event).await;
-    if run_pre_sampling_compact(&sess, &turn_context)
-        .await
-        .is_err()
+    let pre_turn_compaction_outcome = match run_pre_turn_auto_compaction_if_needed(
+        &sess,
+        &turn_context,
+        auto_compact_limit,
+        &incoming_turn_items,
+    )
+    .await
     {
-        error!("Failed to run pre-sampling compact");
-        return None;
-    }
+        Ok(outcome) => outcome,
+        Err(CodexErr::ContextWindowExceeded) => {
+            let incoming_items_tokens_estimate = incoming_turn_items
+                .iter()
+                .map(estimate_item_token_count)
+                .fold(0_i64, i64::saturating_add);
+            let message = format!(
+                "Incoming user message and/or turn context is too large to fit in context window. Please reduce the size of your message and try again. (incoming_items_tokens_estimate={incoming_items_tokens_estimate})"
+            );
+            let event =
+                EventMsg::Error(CodexErr::ContextWindowExceeded.to_error_event(Some(message)));
+            sess.send_event(&turn_context, event).await;
+            return None;
+        }
+        Err(err) => {
+            let event = EventMsg::Error(err.to_error_event(None));
+            sess.send_event(&turn_context, event).await;
+            return None;
+        }
+    };
 
     let skills_outcome = Some(
         sess.services
@@ -4372,10 +4420,15 @@ pub(crate) async fn run_turn(
     sess.merge_connector_selection(explicitly_enabled_connectors.clone())
         .await;
 
-    let initial_input_for_turn: ResponseInputItem = ResponseInputItem::from(input.clone());
-    let response_item: ResponseItem = initial_input_for_turn.clone().into();
-    sess.record_user_prompt_and_emit_turn_item(turn_context.as_ref(), &input, response_item)
-        .await;
+    persist_pre_turn_items_for_compaction_outcome(
+        &sess,
+        &turn_context,
+        pre_turn_compaction_outcome,
+        &pre_turn_context_items,
+        &input,
+        response_item,
+    )
+    .await;
 
     if !skill_items.is_empty() {
         sess.record_conversation_items(&turn_context, &skill_items)
@@ -4479,7 +4532,19 @@ pub(crate) async fn run_turn(
 
                 // as long as compaction works well in getting us way below the token limit, we shouldn't worry about being in an infinite loop.
                 if token_limit_reached && needs_follow_up {
-                    if run_auto_compact(&sess, &turn_context).await.is_err() {
+                    if let Err(err) = run_auto_compact(
+                        &sess,
+                        &turn_context,
+                        AutoCompactCallsite::MidTurnContinuation,
+                        TurnContextReinjection::ReinjectAboveLastRealUser,
+                        None,
+                    )
+                    .await
+                    {
+                        let event = EventMsg::Error(
+                            err.to_error_event(Some("Error running auto compact task".to_string())),
+                        );
+                        sess.send_event(&turn_context, event).await;
                         return None;
                     }
                     continue;
@@ -4581,35 +4646,6 @@ pub(crate) async fn run_turn(
     last_agent_message
 }
 
-async fn run_pre_sampling_compact(
-    sess: &Arc<Session>,
-    turn_context: &Arc<TurnContext>,
-) -> CodexResult<()> {
-    let total_usage_tokens_before_compaction = sess.get_total_token_usage().await;
-    maybe_run_previous_model_inline_compact(
-        sess,
-        turn_context,
-        total_usage_tokens_before_compaction,
-    )
-    .await?;
-    let total_usage_tokens = sess.get_total_token_usage().await;
-    let auto_compact_limit = turn_context
-        .model_info
-        .auto_compact_token_limit()
-        .unwrap_or(i64::MAX);
-    // Compact if the total usage tokens are greater than the auto compact limit
-    if total_usage_tokens >= auto_compact_limit {
-        run_auto_compact(sess, turn_context).await?;
-    }
-    Ok(())
-}
-
-/// Runs pre-sampling compaction against the previous model when switching to a smaller
-/// context-window model.
-///
-/// Returns `Ok(())` when compaction either completed successfully or was skipped because the
-/// model/context-window preconditions were not met. Returns `Err(_)` only when compaction was
-/// attempted and failed.
 async fn maybe_run_previous_model_inline_compact(
     sess: &Arc<Session>,
     turn_context: &Arc<TurnContext>,
@@ -4638,18 +4674,177 @@ async fn maybe_run_previous_model_inline_compact(
         && previous_turn_context.model_info.slug != turn_context.model_info.slug
         && old_context_window > new_context_window;
     if should_run {
-        run_auto_compact(sess, &previous_turn_context).await?;
+        run_auto_compact(
+            sess,
+            &previous_turn_context,
+            AutoCompactCallsite::PreTurnExcludingIncomingUserMessage,
+            TurnContextReinjection::Skip,
+            None,
+        )
+        .await?;
     }
     Ok(())
 }
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+enum PreTurnCompactionOutcome {
+    /// Pre-turn input fits without compaction.
+    NotNeeded,
+    /// Pre-turn compaction succeeded with incoming turn context + user message included.
+    CompactedWithIncomingItems,
+    /// Pre-turn compaction succeeded without incoming turn items
+    /// (incoming user message should be appended after the compaction summary).
+    /// This compaction strategy is currently out of distribution for our compaction model,
+    /// but is planned to be trained on in the future.
+    #[cfg(test)]
+    CompactedWithoutIncomingItems,
+}
+
+async fn persist_pre_turn_items_for_compaction_outcome(
+    sess: &Arc<Session>,
+    turn_context: &Arc<TurnContext>,
+    outcome: PreTurnCompactionOutcome,
+    pre_turn_context_items: &[ResponseItem],
+    input: &[UserInput],
+    response_item: ResponseItem,
+) {
+    match outcome {
+        PreTurnCompactionOutcome::CompactedWithIncomingItems => {
+            // Incoming turn items were already part of pre-turn compaction input, and the
+            // user prompt is already persisted in history after compaction. Emit lifecycle events
+            // only so UI/consumers still observe a normal user turn item transition.
+            let turn_item = TurnItem::UserMessage(UserMessageItem::new(input));
+            sess.emit_turn_item_started(turn_context.as_ref(), &turn_item)
+                .await;
+            sess.emit_turn_item_completed(turn_context.as_ref(), turn_item)
+                .await;
+            sess.ensure_rollout_materialized().await;
+        }
+        PreTurnCompactionOutcome::NotNeeded => {
+            if !pre_turn_context_items.is_empty() {
+                sess.record_conversation_items(turn_context, pre_turn_context_items)
+                    .await;
+            }
+            sess.record_user_prompt_and_emit_turn_item(turn_context.as_ref(), input, response_item)
+                .await;
+        }
+        #[cfg(test)]
+        PreTurnCompactionOutcome::CompactedWithoutIncomingItems => {
+            // Reserved path for future models that compact pre-turn history without incoming turn
+            // items; reseed canonical initial context above the incoming user message.
+            let initial_context = sess.build_initial_context(turn_context.as_ref()).await;
+            if !initial_context.is_empty() {
+                sess.record_conversation_items(turn_context, &initial_context)
+                    .await;
+            }
+            sess.record_user_prompt_and_emit_turn_item(turn_context.as_ref(), input, response_item)
+                .await;
+        }
+    }
+}
+
+/// Runs pre-turn auto-compaction with incoming turn context + user message included.
+async fn run_pre_turn_auto_compaction_if_needed(
+    sess: &Arc<Session>,
+    turn_context: &Arc<TurnContext>,
+    auto_compact_limit: i64,
+    incoming_turn_items: &[ResponseItem],
+) -> CodexResult<PreTurnCompactionOutcome> {
+    let total_usage_tokens_before_compaction = sess.get_total_token_usage().await;
+    maybe_run_previous_model_inline_compact(
+        sess,
+        turn_context,
+        total_usage_tokens_before_compaction,
+    )
+    .await?;
+
+    let total_usage_tokens = sess.get_total_token_usage().await;
+    let incoming_items_tokens_estimate = incoming_turn_items
+        .iter()
+        .map(estimate_item_token_count)
+        .fold(0_i64, i64::saturating_add);
+    if !is_projected_submission_over_auto_compact_limit(
+        total_usage_tokens,
+        incoming_items_tokens_estimate,
+        auto_compact_limit,
+    ) {
+        return Ok(PreTurnCompactionOutcome::NotNeeded);
+    }
+
+    let compact_result = run_auto_compact(
+        sess,
+        turn_context,
+        AutoCompactCallsite::PreTurnIncludingIncomingUserMessage,
+        TurnContextReinjection::ReinjectAboveLastRealUser,
+        Some(incoming_turn_items.to_vec()),
+    )
+    .await;
+
+    if let Err(err) = compact_result {
+        if matches!(err, CodexErr::ContextWindowExceeded) {
+            error!(
+                turn_id = %turn_context.sub_id,
+                auto_compact_callsite = ?AutoCompactCallsite::PreTurnIncludingIncomingUserMessage,
+                incoming_items_tokens_estimate,
+                auto_compact_limit,
+                reason = "pre-turn compaction exceeded context window",
+                "incoming user/context is too large for pre-turn auto-compaction flow"
+            );
+        }
+        return Err(err);
+    }
+
+    Ok(PreTurnCompactionOutcome::CompactedWithIncomingItems)
+}
 
-async fn run_auto_compact(sess: &Arc<Session>, turn_context: &Arc<TurnContext>) -> CodexResult<()> {
-    if should_use_remote_compact_task(&turn_context.provider) {
-        run_inline_remote_auto_compact_task(Arc::clone(sess), Arc::clone(turn_context)).await?;
+fn is_projected_submission_over_auto_compact_limit(
+    total_usage_tokens: i64,
+    incoming_user_tokens_estimate: i64,
+    auto_compact_limit: i64,
+) -> bool {
+    if auto_compact_limit == i64::MAX {
+        return false;
+    }
+
+    total_usage_tokens.saturating_add(incoming_user_tokens_estimate) >= auto_compact_limit
+}
+
+async fn run_auto_compact(
+    sess: &Arc<Session>,
+    turn_context: &Arc<TurnContext>,
+    auto_compact_callsite: AutoCompactCallsite,
+    turn_context_reinjection: TurnContextReinjection,
+    incoming_items: Option<Vec<ResponseItem>>,
+) -> CodexResult<()> {
+    let result = if should_use_remote_compact_task(&turn_context.provider) {
+        run_inline_remote_auto_compact_task(
+            Arc::clone(sess),
+            Arc::clone(turn_context),
+            auto_compact_callsite,
+            turn_context_reinjection,
+            incoming_items,
+        )
+        .await
     } else {
-        run_inline_auto_compact_task(Arc::clone(sess), Arc::clone(turn_context)).await?;
+        run_inline_auto_compact_task(
+            Arc::clone(sess),
+            Arc::clone(turn_context),
+            auto_compact_callsite,
+            turn_context_reinjection,
+            incoming_items,
+        )
+        .await
+    };
+
+    if let Err(err) = &result {
+        error!(
+            turn_id = %turn_context.sub_id,
+            auto_compact_callsite = ?auto_compact_callsite,
+            compact_error = %err,
+            "auto compaction failed"
+        );
     }
-    Ok(())
+
+    result
 }
 
 fn collect_explicit_app_ids_from_skill_items(
@@ -5598,7 +5793,7 @@ async fn try_run_sampling_request(
             ResponseEvent::Completed {
                 response_id: _,
                 token_usage,
-                can_append: _,
+                ..
             } => {
                 if let Some(state) = plan_mode_state.as_mut() {
                     flush_proposed_plan_segments_all(&sess, &turn_context, state).await;
@@ -5829,6 +6024,151 @@ mod tests {
         }
     }
 
+    #[test]
+    fn pre_turn_projection_uses_incoming_user_tokens_for_compaction() {
+        assert!(is_projected_submission_over_auto_compact_limit(90, 15, 100));
+        assert!(!is_projected_submission_over_auto_compact_limit(90, 9, 100));
+    }
+
+    #[test]
+    fn pre_turn_projection_does_not_compact_with_unbounded_limit() {
+        assert!(!is_projected_submission_over_auto_compact_limit(
+            i64::MAX - 1,
+            100,
+            i64::MAX,
+        ));
+    }
+
+    #[test]
+    fn post_compaction_projection_triggers_error_when_still_over_limit() {
+        assert!(is_projected_submission_over_auto_compact_limit(95, 10, 100));
+        assert!(is_projected_submission_over_auto_compact_limit(
+            100, 10, 100
+        ));
+        assert!(!is_projected_submission_over_auto_compact_limit(
+            80, 10, 100
+        ));
+    }
+
+    #[tokio::test]
+    async fn reserved_compacted_without_incoming_items_records_initial_context_and_prompt() {
+        let (session, turn_context) = make_session_and_context().await;
+        let session = Arc::new(session);
+        let turn_context = Arc::new(turn_context);
+        let input = vec![UserInput::Text {
+            text: "hello".to_string(),
+            text_elements: Vec::new(),
+        }];
+        let response_item: ResponseItem = ResponseInputItem::from(input.clone()).into();
+        let stale_pre_turn_context_items = vec![ResponseItem::Message {
+            id: None,
+            role: "developer".to_string(),
+            content: vec![ContentItem::InputText {
+                text: "stale context diff".to_string(),
+            }],
+            end_turn: None,
+            phase: None,
+        }];
+
+        persist_pre_turn_items_for_compaction_outcome(
+            &session,
+            &turn_context,
+            PreTurnCompactionOutcome::CompactedWithoutIncomingItems,
+            &stale_pre_turn_context_items,
+            &input,
+            response_item.clone(),
+        )
+        .await;
+
+        let mut expected = session.build_initial_context(turn_context.as_ref()).await;
+        expected.push(response_item);
+        let actual = session.clone_history().await.raw_items().to_vec();
+        assert_eq!(actual, expected);
+    }
+
+    #[tokio::test]
+    async fn compacted_with_incoming_items_emits_lifecycle_without_history_writes() {
+        let (session, turn_context) = make_session_and_context().await;
+        let session = Arc::new(session);
+        let turn_context = Arc::new(turn_context);
+        let input = vec![UserInput::Text {
+            text: "hello".to_string(),
+            text_elements: Vec::new(),
+        }];
+        let response_item: ResponseItem = ResponseInputItem::from(input.clone()).into();
+        let stale_pre_turn_context_items = vec![ResponseItem::Message {
+            id: None,
+            role: "developer".to_string(),
+            content: vec![ContentItem::InputText {
+                text: "stale context diff".to_string(),
+            }],
+            end_turn: None,
+            phase: None,
+        }];
+
+        persist_pre_turn_items_for_compaction_outcome(
+            &session,
+            &turn_context,
+            PreTurnCompactionOutcome::CompactedWithIncomingItems,
+            &stale_pre_turn_context_items,
+            &input,
+            response_item,
+        )
+        .await;
+
+        let actual = session.clone_history().await.raw_items().to_vec();
+        assert_eq!(actual, Vec::<ResponseItem>::new());
+    }
+
+    #[test]
+    fn estimate_user_input_token_count_is_positive_for_text_input() {
+        let input = vec![UserInput::Text {
+            text: "hello".to_string(),
+            text_elements: Vec::new(),
+        }];
+        let response_input_item = ResponseInputItem::from(input);
+        let response_item: ResponseItem = response_input_item.into();
+        let estimated_tokens = estimate_item_token_count(&response_item);
+        assert!(estimated_tokens > 0);
+    }
+
+    #[test]
+    fn estimate_user_input_token_count_ignores_skill_and_mention_payload_lengths() {
+        let short = vec![
+            UserInput::Skill {
+                name: "s".to_string(),
+                path: PathBuf::from("/s"),
+            },
+            UserInput::Mention {
+                name: "m".to_string(),
+                path: "app://m".to_string(),
+            },
+        ];
+        let long = vec![
+            UserInput::Skill {
+                name: "very-long-skill-name-that-should-not-affect-prompt-serialization"
+                    .to_string(),
+                path: PathBuf::from(
+                    "/very/long/skill/path/that/should/not/affect/prompt/serialization/SKILL.md",
+                ),
+            },
+            UserInput::Mention {
+                name: "very-long-mention-name-that-should-not-affect-prompt-serialization"
+                    .to_string(),
+                path: "app://very-long-connector-path-that-should-not-affect-prompt-serialization"
+                    .to_string(),
+            },
+        ];
+
+        let short_response_input_item = ResponseInputItem::from(short);
+        let long_response_input_item = ResponseInputItem::from(long);
+        let short_response_item: ResponseItem = short_response_input_item.into();
+        let long_response_item: ResponseItem = long_response_input_item.into();
+        let short_tokens = estimate_item_token_count(&short_response_item);
+        let long_tokens = estimate_item_token_count(&long_response_item);
+        assert_eq!(short_tokens, long_tokens);
+    }
+
     fn make_connector(id: &str, name: &str) -> AppInfo {
         AppInfo {
             id: id.to_string(),
@@ -8083,8 +8423,8 @@ mod tests {
             .clone()
             .for_prompt(&reconstruction_turn.model_info.input_modalities);
         let user_messages1 = collect_user_messages(&snapshot1);
-        let rebuilt1 =
-            compact::build_compacted_history(initial_context.clone(), &user_messages1, summary1);
+        let mut rebuilt1 = initial_context.clone();
+        rebuilt1.extend(compact::build_compacted_history(&user_messages1, summary1));
         live_history.replace(rebuilt1);
         rollout_items.push(RolloutItem::Compacted(CompactedItem {
             message: summary1.to_string(),
@@ -8126,8 +8466,8 @@ mod tests {
             .clone()
             .for_prompt(&reconstruction_turn.model_info.input_modalities);
         let user_messages2 = collect_user_messages(&snapshot2);
-        let rebuilt2 =
-            compact::build_compacted_history(initial_context.clone(), &user_messages2, summary2);
+        let mut rebuilt2 = initial_context.clone();
+        rebuilt2.extend(compact::build_compacted_history(&user_messages2, summary2));
         live_history.replace(rebuilt2);
         rollout_items.push(RolloutItem::Compacted(CompactedItem {
             message: summary2.to_string(),
diff --git a/codex-rs/core/src/compact.rs b/codex-rs/core/src/compact.rs
index a40f1a0b8efb..d21028e90d90 100644
--- a/codex-rs/core/src/compact.rs
+++ b/codex-rs/core/src/compact.rs
@@ -8,6 +8,7 @@ use crate::codex::Session;
 use crate::codex::TurnContext;
 use crate::codex::get_last_assistant_message_from_turn;
 use crate::context_manager::ContextManager;
+use crate::context_manager::is_user_turn_boundary;
 use crate::error::CodexErr;
 use crate::error::Result as CodexResult;
 use crate::protocol::CompactedItem;
@@ -32,6 +33,32 @@ pub const SUMMARIZATION_PROMPT: &str = include_str!("../templates/compact/prompt
 pub const SUMMARY_PREFIX: &str = include_str!("../templates/compact/summary_prefix.md");
 const COMPACT_USER_MESSAGE_MAX_TOKENS: usize = 20_000;
 
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub(crate) enum AutoCompactCallsite {
+    /// Pre-turn auto-compaction where the incoming turn context + user message are included in
+    /// the compaction request.
+    PreTurnIncludingIncomingUserMessage,
+    /// Reserved pre-turn auto-compaction strategy that compacts from the end of the previous turn
+    /// only, excluding incoming turn context + user message. This is currently unused by the
+    /// default pre-turn flow and retained for future model-specific strategies.
+    #[allow(dead_code)]
+    PreTurnExcludingIncomingUserMessage,
+    /// Mid-turn compaction between assistant responses in a follow-up loop.
+    MidTurnContinuation,
+}
+
+/// Controls whether compacted-history processing should reinsert canonical turn context.
+///
+/// When callers exclude incoming user/context from the compaction request, they should typically
+/// set reinjection to `Skip` and append canonical context together with the next user message.
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub(crate) enum TurnContextReinjection {
+    /// Insert canonical context immediately above the last real user message in compacted history.
+    ReinjectAboveLastRealUser,
+    /// Do not reinsert canonical context while processing compacted history.
+    Skip,
+}
+
 pub(crate) fn should_use_remote_compact_task(provider: &ModelProviderInfo) -> bool {
     provider.is_openai()
 }
@@ -64,6 +91,9 @@ pub(crate) fn extract_trailing_model_switch_update_for_compaction_request(
 pub(crate) async fn run_inline_auto_compact_task(
     sess: Arc<Session>,
     turn_context: Arc<TurnContext>,
+    auto_compact_callsite: AutoCompactCallsite,
+    turn_context_reinjection: TurnContextReinjection,
+    incoming_items: Option<Vec<ResponseItem>>,
 ) -> CodexResult<()> {
     let prompt = turn_context.compact_prompt().to_string();
     let input = vec![UserInput::Text {
@@ -72,7 +102,15 @@ pub(crate) async fn run_inline_auto_compact_task(
         text_elements: Vec::new(),
     }];
 
-    run_compact_task_inner(sess, turn_context, input).await?;
+    run_compact_task_inner(
+        sess,
+        turn_context,
+        input,
+        Some(auto_compact_callsite),
+        turn_context_reinjection,
+        incoming_items,
+    )
+    .await?;
     Ok(())
 }
 
@@ -87,13 +125,24 @@ pub(crate) async fn run_compact_task(
         collaboration_mode_kind: turn_context.collaboration_mode.mode,
     });
     sess.send_event(&turn_context, start_event).await;
-    run_compact_task_inner(sess.clone(), turn_context, input).await
+    run_compact_task_inner(
+        sess,
+        turn_context,
+        input,
+        None,
+        TurnContextReinjection::Skip,
+        None,
+    )
+    .await
 }
 
 async fn run_compact_task_inner(
     sess: Arc<Session>,
     turn_context: Arc<TurnContext>,
     input: Vec<UserInput>,
+    auto_compact_callsite: Option<AutoCompactCallsite>,
+    turn_context_reinjection: TurnContextReinjection,
+    incoming_items: Option<Vec<ResponseItem>>,
 ) -> CodexResult<()> {
     let compaction_item = TurnItem::ContextCompaction(ContextCompactionItem::new());
     sess.emit_turn_item_started(&turn_context, &compaction_item)
@@ -105,6 +154,15 @@ async fn run_compact_task_inner(
     // tail of history (between turns), exclude it from the compaction request payload.
     let stripped_model_switch_item =
         extract_trailing_model_switch_update_for_compaction_request(&mut history);
+    if let Some(incoming_items) = incoming_items.as_ref() {
+        history.record_items(incoming_items.iter(), turn_context.truncation_policy);
+    }
+    if !history.raw_items().iter().any(is_user_turn_boundary) {
+        // Nothing to compact: do not rewrite history when there is no user-turn boundary.
+        sess.emit_turn_item_completed(&turn_context, compaction_item)
+            .await;
+        return Ok(());
+    }
     history.record_items(
         &[initial_input_for_turn.into()],
         turn_context.truncation_policy,
@@ -167,8 +225,11 @@ async fn run_compact_task_inner(
             }
             Err(e @ CodexErr::ContextWindowExceeded) => {
                 if turn_input_len > 1 {
-                    // Trim from the beginning to preserve cache (prefix-based) and keep recent messages intact.
+                    // Trim from the beginning to preserve cache (prefix-based) and keep recent
+                    // messages intact.
                     error!(
+                        turn_id = %turn_context.sub_id,
+                        auto_compact_callsite = ?auto_compact_callsite,
                         "Context window exceeded while compacting; removing oldest history item. Error: {e}"
                     );
                     history.remove_first_item();
@@ -177,8 +238,12 @@ async fn run_compact_task_inner(
                     continue;
                 }
                 sess.set_total_tokens_full(turn_context.as_ref()).await;
-                let event = EventMsg::Error(e.to_error_event(None));
-                sess.send_event(&turn_context, event).await;
+                error!(
+                    turn_id = %turn_context.sub_id,
+                    auto_compact_callsite = ?auto_compact_callsite,
+                    compact_error = %e,
+                    "compaction failed after history truncation could not proceed"
+                );
                 return Err(e);
             }
             Err(e) => {
@@ -193,11 +258,16 @@ async fn run_compact_task_inner(
                     .await;
                     tokio::time::sleep(delay).await;
                     continue;
-                } else {
-                    let event = EventMsg::Error(e.to_error_event(None));
-                    sess.send_event(&turn_context, event).await;
-                    return Err(e);
                 }
+                error!(
+                    turn_id = %turn_context.sub_id,
+                    auto_compact_callsite = ?auto_compact_callsite,
+                    retries,
+                    max_retries,
+                    compact_error = %e,
+                    "compaction failed after retry exhaustion"
+                );
+                return Err(e);
             }
         }
     }
@@ -207,9 +277,32 @@ async fn run_compact_task_inner(
     let summary_suffix = get_last_assistant_message_from_turn(history_items).unwrap_or_default();
     let summary_text = format!("{SUMMARY_PREFIX}\n{summary_suffix}");
     let user_messages = collect_user_messages(history_items);
+    let incoming_user_items = match incoming_items.as_ref() {
+        Some(items) => items
+            .iter()
+            .filter(|item| real_user_message_text(item).is_some())
+            .cloned()
+            .collect(),
+        None => Vec::new(),
+    };
 
-    let initial_context = sess.build_initial_context(turn_context.as_ref()).await;
-    let mut new_history = build_compacted_history(initial_context, &user_messages, &summary_text);
+    let initial_context = match turn_context_reinjection {
+        TurnContextReinjection::ReinjectAboveLastRealUser => {
+            sess.build_initial_context(turn_context.as_ref()).await
+        }
+        TurnContextReinjection::Skip => Vec::new(),
+    };
+    let compacted_history = build_compacted_history_with_limit(
+        &user_messages,
+        &incoming_user_items,
+        &summary_text,
+        COMPACT_USER_MESSAGE_MAX_TOKENS,
+    );
+    let mut new_history = process_compacted_history(
+        compacted_history,
+        &initial_context,
+        turn_context_reinjection,
+    );
     // Reattach the stripped model-switch update only after successful compaction so the model
     // still sees the switch instructions on the next real sampling request.
     if let Some(model_switch_item) = stripped_model_switch_item {
@@ -226,7 +319,7 @@ async fn run_compact_task_inner(
 
     let rollout_item = RolloutItem::Compacted(CompactedItem {
         message: summary_text.clone(),
-        replacement_history: None,
+        replacement_history: Some(sess.clone_history().await.raw_items().to_vec()),
     });
     sess.persist_rollout_items(&[rollout_item]).await;
 
@@ -281,28 +374,39 @@ pub(crate) fn is_summary_message(message: &str) -> bool {
 pub(crate) fn process_compacted_history(
     mut compacted_history: Vec<ResponseItem>,
     initial_context: &[ResponseItem],
+    turn_context_reinjection: TurnContextReinjection,
 ) -> Vec<ResponseItem> {
+    // Keep only model-visible transcript items that we allow from remote compaction output.
     compacted_history.retain(should_keep_compacted_history_item);
 
-    let initial_context = initial_context.to_vec();
-
-    // Re-inject canonical context from the current session since we stripped it
-    // from the pre-compaction history. Keep it right before the last user
-    // message so older user messages remain earlier in the transcript.
-    if let Some(last_user_index) = compacted_history.iter().rposition(|item| {
-        matches!(
-            crate::event_mapping::parse_turn_item(item),
-            Some(TurnItem::UserMessage(_))
-        )
-    }) {
-        compacted_history.splice(last_user_index..last_user_index, initial_context);
-    } else {
-        compacted_history.extend(initial_context);
+    match turn_context_reinjection {
+        TurnContextReinjection::ReinjectAboveLastRealUser => {
+            // Insert immediately above the last real user message so turn context applies to that
+            // user input rather than an earlier turn.
+            if let Some(insertion_index) = compacted_history
+                .iter()
+                .rposition(|item| real_user_message_text(item).is_some())
+            {
+                compacted_history
+                    .splice(insertion_index..insertion_index, initial_context.to_vec());
+            }
+        }
+        TurnContextReinjection::Skip => {}
     }
 
     compacted_history
 }
 
+fn real_user_message_text(item: &ResponseItem) -> Option<String> {
+    match crate::event_mapping::parse_turn_item(item) {
+        Some(TurnItem::UserMessage(user_message)) => {
+            let message = user_message.message();
+            (!is_summary_message(&message)).then_some(message)
+        }
+        _ => None,
+    }
+}
+
 /// Returns whether an item from remote compaction output should be preserved.
 ///
 /// Called while processing the model-provided compacted transcript, before we
@@ -328,24 +432,24 @@ fn should_keep_compacted_history_item(item: &ResponseItem) -> bool {
 }
 
 pub(crate) fn build_compacted_history(
-    initial_context: Vec<ResponseItem>,
     user_messages: &[String],
     summary_text: &str,
 ) -> Vec<ResponseItem> {
     build_compacted_history_with_limit(
-        initial_context,
         user_messages,
+        &[],
         summary_text,
         COMPACT_USER_MESSAGE_MAX_TOKENS,
     )
 }
 
 fn build_compacted_history_with_limit(
-    mut history: Vec<ResponseItem>,
     user_messages: &[String],
+    incoming_user_items: &[ResponseItem],
     summary_text: &str,
     max_tokens: usize,
 ) -> Vec<ResponseItem> {
+    let mut history = Vec::new();
     let mut selected_messages: Vec<String> = Vec::new();
     if max_tokens > 0 {
         let mut remaining = max_tokens;
@@ -378,6 +482,8 @@ fn build_compacted_history_with_limit(
         });
     }
 
+    history.extend(incoming_user_items.iter().cloned());
+
     let summary_text = if summary_text.is_empty() {
         "(no summary available)".to_string()
     } else {
@@ -657,8 +763,8 @@ do things
         let max_tokens = 16;
         let big = "word ".repeat(200);
         let history = super::build_compacted_history_with_limit(
-            Vec::new(),
             std::slice::from_ref(&big),
+            &[],
             "SUMMARY",
             max_tokens,
         );
@@ -694,11 +800,10 @@ do things
 
     #[test]
     fn build_token_limited_compacted_history_appends_summary_message() {
-        let initial_context: Vec<ResponseItem> = Vec::new();
         let user_messages = vec!["first user message".to_string()];
         let summary_text = "summary text";
 
-        let history = build_compacted_history(initial_context, &user_messages, summary_text);
+        let history = build_compacted_history(&user_messages, summary_text);
         assert!(
             !history.is_empty(),
             "expected compacted history to include summary"
@@ -714,6 +819,55 @@ do things
         assert_eq!(summary, summary_text);
     }
 
+    #[test]
+    fn build_compacted_history_preserves_incoming_user_item_structure() {
+        let preserved_user_item = ResponseItem::Message {
+            id: None,
+            role: "user".to_string(),
+            content: vec![
+                ContentItem::InputImage {
+                    image_url: "data:image/png;base64,AAAA".to_string(),
+                },
+                ContentItem::InputText {
+                    text: "latest user with image".to_string(),
+                },
+            ],
+            end_turn: None,
+            phase: None,
+        };
+
+        let history = super::build_compacted_history_with_limit(
+            &["older user".to_string()],
+            std::slice::from_ref(&preserved_user_item),
+            "SUMMARY",
+            128,
+        );
+
+        let expected = vec![
+            ResponseItem::Message {
+                id: None,
+                role: "user".to_string(),
+                content: vec![ContentItem::InputText {
+                    text: "older user".to_string(),
+                }],
+                end_turn: None,
+                phase: None,
+            },
+            preserved_user_item,
+            ResponseItem::Message {
+                id: None,
+                role: "user".to_string(),
+                content: vec![ContentItem::InputText {
+                    text: "SUMMARY".to_string(),
+                }],
+                end_turn: None,
+                phase: None,
+            },
+        ];
+
+        assert_eq!(history, expected);
+    }
+
     #[test]
     fn process_compacted_history_replaces_developer_messages() {
         let compacted_history = vec![
@@ -779,7 +933,11 @@ do things
             },
         ];
 
-        let refreshed = process_compacted_history(compacted_history, &initial_context);
+        let refreshed = process_compacted_history(
+            compacted_history,
+            &initial_context,
+            TurnContextReinjection::ReinjectAboveLastRealUser,
+        );
         let expected = vec![
             ResponseItem::Message {
                 id: None,
@@ -888,7 +1046,11 @@ keep me updated
             },
         ];
 
-        let refreshed = process_compacted_history(compacted_history, &initial_context);
+        let refreshed = process_compacted_history(
+            compacted_history,
+            &initial_context,
+            TurnContextReinjection::ReinjectAboveLastRealUser,
+        );
         let expected = vec![
             ResponseItem::Message {
                 id: None,
@@ -1024,7 +1186,11 @@ keep me updated
             phase: None,
         }];
 
-        let refreshed = process_compacted_history(compacted_history, &initial_context);
+        let refreshed = process_compacted_history(
+            compacted_history,
+            &initial_context,
+            TurnContextReinjection::ReinjectAboveLastRealUser,
+        );
         let expected = vec![
             ResponseItem::Message {
                 id: None,
@@ -1089,7 +1255,11 @@ keep me updated
             phase: None,
         }];
 
-        let refreshed = process_compacted_history(compacted_history, &initial_context);
+        let refreshed = process_compacted_history(
+            compacted_history,
+            &initial_context,
+            TurnContextReinjection::ReinjectAboveLastRealUser,
+        );
         let expected = vec![
             ResponseItem::Message {
                 id: None,
@@ -1130,4 +1300,320 @@ keep me updated
         ];
         assert_eq!(refreshed, expected);
     }
+
+    #[test]
+    fn process_compacted_history_pre_turn_places_summary_last() {
+        let compacted_history = vec![
+            ResponseItem::Message {
+                id: None,
+                role: "user".to_string(),
+                content: vec![ContentItem::InputText {
+                    text: "older user".to_string(),
+                }],
+                end_turn: None,
+                phase: None,
+            },
+            ResponseItem::Message {
+                id: None,
+                role: "user".to_string(),
+                content: vec![ContentItem::InputText {
+                    text: format!("{SUMMARY_PREFIX}\nsummary text"),
+                }],
+                end_turn: None,
+                phase: None,
+            },
+        ];
+        let initial_context = vec![ResponseItem::Message {
+            id: None,
+            role: "developer".to_string(),
+            content: vec![ContentItem::InputText {
+                text: "fresh permissions".to_string(),
+            }],
+            end_turn: None,
+            phase: None,
+        }];
+
+        let refreshed = process_compacted_history(
+            compacted_history,
+            &initial_context,
+            TurnContextReinjection::ReinjectAboveLastRealUser,
+        );
+        let expected = vec![
+            ResponseItem::Message {
+                id: None,
+                role: "developer".to_string(),
+                content: vec![ContentItem::InputText {
+                    text: "fresh permissions".to_string(),
+                }],
+                end_turn: None,
+                phase: None,
+            },
+            ResponseItem::Message {
+                id: None,
+                role: "user".to_string(),
+                content: vec![ContentItem::InputText {
+                    text: "older user".to_string(),
+                }],
+                end_turn: None,
+                phase: None,
+            },
+            ResponseItem::Message {
+                id: None,
+                role: "user".to_string(),
+                content: vec![ContentItem::InputText {
+                    text: format!("{SUMMARY_PREFIX}\nsummary text"),
+                }],
+                end_turn: None,
+                phase: None,
+            },
+        ];
+        assert_eq!(refreshed, expected);
+    }
+
+    #[test]
+    fn process_compacted_history_preserves_summary_order() {
+        let compacted_history = vec![
+            ResponseItem::Message {
+                id: None,
+                role: "user".to_string(),
+                content: vec![ContentItem::InputText {
+                    text: "older user".to_string(),
+                }],
+                end_turn: None,
+                phase: None,
+            },
+            ResponseItem::Message {
+                id: None,
+                role: "user".to_string(),
+                content: vec![ContentItem::InputText {
+                    text: format!("{SUMMARY_PREFIX}\nolder summary"),
+                }],
+                end_turn: None,
+                phase: None,
+            },
+            ResponseItem::Message {
+                id: None,
+                role: "user".to_string(),
+                content: vec![ContentItem::InputText {
+                    text: "newer user".to_string(),
+                }],
+                end_turn: None,
+                phase: None,
+            },
+            ResponseItem::Message {
+                id: None,
+                role: "user".to_string(),
+                content: vec![ContentItem::InputText {
+                    text: format!("{SUMMARY_PREFIX}\nlatest summary"),
+                }],
+                end_turn: None,
+                phase: None,
+            },
+            ResponseItem::Message {
+                id: None,
+                role: "assistant".to_string(),
+                content: vec![ContentItem::OutputText {
+                    text: "assistant after latest summary".to_string(),
+                }],
+                end_turn: None,
+                phase: None,
+            },
+        ];
+
+        let refreshed =
+            process_compacted_history(compacted_history, &[], TurnContextReinjection::Skip);
+        let expected = vec![
+            ResponseItem::Message {
+                id: None,
+                role: "user".to_string(),
+                content: vec![ContentItem::InputText {
+                    text: "older user".to_string(),
+                }],
+                end_turn: None,
+                phase: None,
+            },
+            ResponseItem::Message {
+                id: None,
+                role: "user".to_string(),
+                content: vec![ContentItem::InputText {
+                    text: format!("{SUMMARY_PREFIX}\nolder summary"),
+                }],
+                end_turn: None,
+                phase: None,
+            },
+            ResponseItem::Message {
+                id: None,
+                role: "user".to_string(),
+                content: vec![ContentItem::InputText {
+                    text: "newer user".to_string(),
+                }],
+                end_turn: None,
+                phase: None,
+            },
+            ResponseItem::Message {
+                id: None,
+                role: "user".to_string(),
+                content: vec![ContentItem::InputText {
+                    text: format!("{SUMMARY_PREFIX}\nlatest summary"),
+                }],
+                end_turn: None,
+                phase: None,
+            },
+            ResponseItem::Message {
+                id: None,
+                role: "assistant".to_string(),
+                content: vec![ContentItem::OutputText {
+                    text: "assistant after latest summary".to_string(),
+                }],
+                end_turn: None,
+                phase: None,
+            },
+        ];
+        assert_eq!(refreshed, expected);
+    }
+
+    #[test]
+    fn process_compacted_history_skips_context_insertion_without_real_user_message() {
+        let compacted_history = vec![ResponseItem::Message {
+            id: None,
+            role: "user".to_string(),
+            content: vec![ContentItem::InputText {
+                text: format!("{SUMMARY_PREFIX}\nsummary text"),
+            }],
+            end_turn: None,
+            phase: None,
+        }];
+        let initial_context = vec![ResponseItem::Message {
+            id: None,
+            role: "developer".to_string(),
+            content: vec![ContentItem::InputText {
+                text: "fresh permissions".to_string(),
+            }],
+            end_turn: None,
+            phase: None,
+        }];
+
+        let refreshed = process_compacted_history(
+            compacted_history,
+            &initial_context,
+            TurnContextReinjection::Skip,
+        );
+        let expected = vec![ResponseItem::Message {
+            id: None,
+            role: "user".to_string(),
+            content: vec![ContentItem::InputText {
+                text: format!("{SUMMARY_PREFIX}\nsummary text"),
+            }],
+            end_turn: None,
+            phase: None,
+        }];
+        assert_eq!(refreshed, expected);
+    }
+
+    #[test]
+    fn process_compacted_history_reinject_noops_without_real_user_message() {
+        let compacted_history = vec![ResponseItem::Message {
+            id: None,
+            role: "user".to_string(),
+            content: vec![ContentItem::InputText {
+                text: format!("{SUMMARY_PREFIX}\nsummary text"),
+            }],
+            end_turn: None,
+            phase: None,
+        }];
+        let initial_context = vec![ResponseItem::Message {
+            id: None,
+            role: "developer".to_string(),
+            content: vec![ContentItem::InputText {
+                text: "fresh permissions".to_string(),
+            }],
+            end_turn: None,
+            phase: None,
+        }];
+
+        let refreshed = process_compacted_history(
+            compacted_history,
+            &initial_context,
+            TurnContextReinjection::ReinjectAboveLastRealUser,
+        );
+        let expected = vec![ResponseItem::Message {
+            id: None,
+            role: "user".to_string(),
+            content: vec![ContentItem::InputText {
+                text: format!("{SUMMARY_PREFIX}\nsummary text"),
+            }],
+            end_turn: None,
+            phase: None,
+        }];
+        assert_eq!(refreshed, expected);
+    }
+
+    #[test]
+    fn process_compacted_history_mid_turn_without_orphan_user_places_summary_last() {
+        let compacted_history = vec![
+            ResponseItem::Message {
+                id: None,
+                role: "user".to_string(),
+                content: vec![ContentItem::InputText {
+                    text: "older user".to_string(),
+                }],
+                end_turn: None,
+                phase: None,
+            },
+            ResponseItem::Message {
+                id: None,
+                role: "user".to_string(),
+                content: vec![ContentItem::InputText {
+                    text: format!("{SUMMARY_PREFIX}\nsummary text"),
+                }],
+                end_turn: None,
+                phase: None,
+            },
+        ];
+        let initial_context = vec![ResponseItem::Message {
+            id: None,
+            role: "developer".to_string(),
+            content: vec![ContentItem::InputText {
+                text: "fresh permissions".to_string(),
+            }],
+            end_turn: None,
+            phase: None,
+        }];
+
+        let refreshed = process_compacted_history(
+            compacted_history,
+            &initial_context,
+            TurnContextReinjection::ReinjectAboveLastRealUser,
+        );
+        let expected = vec![
+            ResponseItem::Message {
+                id: None,
+                role: "developer".to_string(),
+                content: vec![ContentItem::InputText {
+                    text: "fresh permissions".to_string(),
+                }],
+                end_turn: None,
+                phase: None,
+            },
+            ResponseItem::Message {
+                id: None,
+                role: "user".to_string(),
+                content: vec![ContentItem::InputText {
+                    text: "older user".to_string(),
+                }],
+                end_turn: None,
+                phase: None,
+            },
+            ResponseItem::Message {
+                id: None,
+                role: "user".to_string(),
+                content: vec![ContentItem::InputText {
+                    text: format!("{SUMMARY_PREFIX}\nsummary text"),
+                }],
+                end_turn: None,
+                phase: None,
+            },
+        ];
+        assert_eq!(refreshed, expected);
+    }
 }
diff --git a/codex-rs/core/src/compact_remote.rs b/codex-rs/core/src/compact_remote.rs
index 3d181f885526..6d357aecdc92 100644
--- a/codex-rs/core/src/compact_remote.rs
+++ b/codex-rs/core/src/compact_remote.rs
@@ -4,10 +4,14 @@ use crate::Prompt;
 use crate::codex::Session;
 use crate::codex::TurnContext;
 use crate::compact::extract_trailing_model_switch_update_for_compaction_request;
+use crate::compact::AutoCompactCallsite;
+use crate::compact::TurnContextReinjection;
 use crate::context_manager::ContextManager;
 use crate::context_manager::TotalTokenUsageBreakdown;
+use crate::context_manager::estimate_item_token_count;
 use crate::context_manager::estimate_response_item_model_visible_bytes;
 use crate::context_manager::is_codex_generated_item;
+use crate::context_manager::is_user_turn_boundary;
 use crate::error::CodexErr;
 use crate::error::Result as CodexResult;
 use crate::protocol::CompactedItem;
@@ -25,8 +29,19 @@ use tracing::info;
 pub(crate) async fn run_inline_remote_auto_compact_task(
     sess: Arc<Session>,
     turn_context: Arc<TurnContext>,
+    auto_compact_callsite: AutoCompactCallsite,
+    // Controls whether canonical turn context should be reinserted into compacted history.
+    turn_context_reinjection: TurnContextReinjection,
+    incoming_items: Option<Vec<ResponseItem>>,
 ) -> CodexResult<()> {
-    run_remote_compact_task_inner(&sess, &turn_context).await?;
+    run_remote_compact_task_inner(
+        &sess,
+        &turn_context,
+        auto_compact_callsite,
+        turn_context_reinjection,
+        incoming_items,
+    )
+    .await?;
     Ok(())
 }
 
@@ -41,18 +56,40 @@ pub(crate) async fn run_remote_compact_task(
     });
     sess.send_event(&turn_context, start_event).await;
 
-    run_remote_compact_task_inner(&sess, &turn_context).await
+    run_remote_compact_task_inner(
+        &sess,
+        &turn_context,
+        AutoCompactCallsite::PreTurnExcludingIncomingUserMessage,
+        // Manual `/compact` should not reinsert turn context into compacted history; we reseed
+        // canonical initial context before the next user turn.
+        TurnContextReinjection::Skip,
+        None,
+    )
+    .await
 }
 
 async fn run_remote_compact_task_inner(
     sess: &Arc<Session>,
     turn_context: &Arc<TurnContext>,
+    auto_compact_callsite: AutoCompactCallsite,
+    turn_context_reinjection: TurnContextReinjection,
+    incoming_items: Option<Vec<ResponseItem>>,
 ) -> CodexResult<()> {
-    if let Err(err) = run_remote_compact_task_inner_impl(sess, turn_context).await {
-        let event = EventMsg::Error(
-            err.to_error_event(Some("Error running remote compact task".to_string())),
+    if let Err(err) = run_remote_compact_task_inner_impl(
+        sess,
+        turn_context,
+        auto_compact_callsite,
+        turn_context_reinjection,
+        incoming_items,
+    )
+    .await
+    {
+        error!(
+            turn_id = %turn_context.sub_id,
+            auto_compact_callsite = ?auto_compact_callsite,
+            compact_error = %err,
+            "remote compaction task failed"
         );
-        sess.send_event(turn_context, event).await;
         return Err(err);
     }
     Ok(())
@@ -61,6 +98,9 @@ async fn run_remote_compact_task_inner(
 async fn run_remote_compact_task_inner_impl(
     sess: &Arc<Session>,
     turn_context: &Arc<TurnContext>,
+    auto_compact_callsite: AutoCompactCallsite,
+    turn_context_reinjection: TurnContextReinjection,
+    incoming_items: Option<Vec<ResponseItem>>,
 ) -> CodexResult<()> {
     let compaction_item = TurnItem::ContextCompaction(ContextCompactionItem::new());
     sess.emit_turn_item_started(turn_context, &compaction_item)
@@ -75,10 +115,21 @@ async fn run_remote_compact_task_inner_impl(
         &mut history,
         turn_context.as_ref(),
         &base_instructions,
+        incoming_items.as_deref(),
     );
+    if let Some(incoming_items) = incoming_items {
+        history.record_items(incoming_items.iter(), turn_context.truncation_policy);
+    }
+    if !history.raw_items().iter().any(is_user_turn_boundary) {
+        // Nothing to compact: do not rewrite history when there is no user-turn boundary.
+        sess.emit_turn_item_completed(turn_context, compaction_item)
+            .await;
+        return Ok(());
+    }
     if deleted_items > 0 {
         info!(
             turn_id = %turn_context.sub_id,
+            auto_compact_callsite = ?auto_compact_callsite,
             deleted_items,
             "trimmed history items before remote compaction"
         );
@@ -115,6 +166,7 @@ async fn run_remote_compact_task_inner_impl(
                 build_compact_request_log_data(&prompt.input, &prompt.base_instructions.text);
             log_remote_compact_failure(
                 turn_context,
+                auto_compact_callsite,
                 &compact_request_log_data,
                 total_usage_breakdown,
                 &err,
@@ -123,7 +175,7 @@ async fn run_remote_compact_task_inner_impl(
         })
         .await?;
     new_history = sess
-        .process_compacted_history(turn_context, new_history)
+        .process_compacted_history(turn_context, new_history, turn_context_reinjection)
         .await;
     // Reattach the stripped model-switch update only after successful compaction so the model
     // still sees the switch instructions on the next real sampling request.
@@ -173,12 +225,14 @@ fn build_compact_request_log_data(
 
 fn log_remote_compact_failure(
     turn_context: &TurnContext,
+    auto_compact_callsite: AutoCompactCallsite,
     log_data: &CompactRequestLogData,
     total_usage_breakdown: TotalTokenUsageBreakdown,
     err: &CodexErr,
 ) {
     error!(
         turn_id = %turn_context.sub_id,
+        auto_compact_callsite = ?auto_compact_callsite,
         last_api_response_total_tokens = total_usage_breakdown.last_api_response_total_tokens,
         all_history_items_model_visible_bytes = total_usage_breakdown.all_history_items_model_visible_bytes,
         estimated_tokens_of_items_added_since_last_successful_api_response = total_usage_breakdown.estimated_tokens_of_items_added_since_last_successful_api_response,
@@ -194,15 +248,37 @@ fn trim_function_call_history_to_fit_context_window(
     history: &mut ContextManager,
     turn_context: &TurnContext,
     base_instructions: &BaseInstructions,
+    incoming_items: Option<&[ResponseItem]>,
 ) -> usize {
-    let mut deleted_items = 0usize;
     let Some(context_window) = turn_context.model_context_window() else {
-        return deleted_items;
+        return 0;
     };
+    let incoming_items_tokens = incoming_items
+        .unwrap_or_default()
+        .iter()
+        .map(estimate_item_token_count)
+        .fold(0_i64, i64::saturating_add);
+    trim_codex_generated_tail_items_to_fit_context_window(
+        history,
+        context_window,
+        base_instructions,
+        incoming_items_tokens,
+    )
+}
+
+fn trim_codex_generated_tail_items_to_fit_context_window(
+    history: &mut ContextManager,
+    context_window: i64,
+    base_instructions: &BaseInstructions,
+    incoming_items_tokens: i64,
+) -> usize {
+    let mut deleted_items = 0usize;
 
     while history
         .estimate_token_count_with_base_instructions(base_instructions)
-        .is_some_and(|estimated_tokens| estimated_tokens > context_window)
+        .is_some_and(|estimated_tokens| {
+            estimated_tokens.saturating_add(incoming_items_tokens) > context_window
+        })
     {
         let Some(last_item) = history.raw_items().last() else {
             break;
@@ -218,3 +294,90 @@ fn trim_function_call_history_to_fit_context_window(
 
     deleted_items
 }
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::truncate::TruncationPolicy;
+    use codex_protocol::models::ContentItem;
+    use pretty_assertions::assert_eq;
+
+    fn user_message(text: &str) -> ResponseItem {
+        ResponseItem::Message {
+            id: None,
+            role: "user".to_string(),
+            content: vec![ContentItem::InputText {
+                text: text.to_string(),
+            }],
+            end_turn: None,
+            phase: None,
+        }
+    }
+
+    fn developer_message(text: &str) -> ResponseItem {
+        ResponseItem::Message {
+            id: None,
+            role: "developer".to_string(),
+            content: vec![ContentItem::InputText {
+                text: text.to_string(),
+            }],
+            end_turn: None,
+            phase: None,
+        }
+    }
+
+    #[test]
+    fn trim_accounts_for_incoming_items_tokens() {
+        let base_instructions = BaseInstructions {
+            text: String::new(),
+        };
+        let incoming_items = vec![user_message(
+            "INCOMING_USER_MESSAGE_THAT_TIPS_OVER_THE_WINDOW",
+        )];
+        let incoming_items_tokens = incoming_items
+            .iter()
+            .map(estimate_item_token_count)
+            .fold(0_i64, i64::saturating_add);
+        assert!(
+            incoming_items_tokens > 0,
+            "expected incoming item token estimate to be positive"
+        );
+
+        let mut history = ContextManager::new();
+        let history_items = vec![
+            user_message("USER_ONE"),
+            developer_message("TRAILING_CODEX_GENERATED_CONTEXT"),
+        ];
+        history.record_items(history_items.iter(), TruncationPolicy::Tokens(10_000));
+        let history_tokens = history
+            .estimate_token_count_with_base_instructions(&base_instructions)
+            .unwrap_or_default();
+        let context_window = history_tokens
+            .saturating_add(incoming_items_tokens)
+            .saturating_sub(1);
+        let mut without_incoming_projection = history.clone();
+
+        let deleted_without_incoming = trim_codex_generated_tail_items_to_fit_context_window(
+            &mut without_incoming_projection,
+            context_window,
+            &base_instructions,
+            0,
+        );
+        assert_eq!(
+            deleted_without_incoming, 0,
+            "history-only projection should not trim when currently under the limit"
+        );
+
+        let deleted_with_incoming = trim_codex_generated_tail_items_to_fit_context_window(
+            &mut history,
+            context_window,
+            &base_instructions,
+            incoming_items_tokens,
+        );
+        assert_eq!(
+            deleted_with_incoming, 1,
+            "incoming projection should trim trailing codex-generated history to fit pre-turn request"
+        );
+        assert_eq!(history.raw_items(), vec![user_message("USER_ONE")]);
+    }
+}
diff --git a/codex-rs/core/src/context_manager/history.rs b/codex-rs/core/src/context_manager/history.rs
index b6b3ceae5af7..acd9778d969d 100644
--- a/codex-rs/core/src/context_manager/history.rs
+++ b/codex-rs/core/src/context_manager/history.rs
@@ -395,7 +395,7 @@ fn estimate_reasoning_length(encoded_len: usize) -> usize {
         .saturating_sub(650)
 }
 
-fn estimate_item_token_count(item: &ResponseItem) -> i64 {
+pub(crate) fn estimate_item_token_count(item: &ResponseItem) -> i64 {
     let model_visible_bytes = estimate_response_item_model_visible_bytes(item);
     approx_tokens_from_byte_count_i64(model_visible_bytes)
 }
diff --git a/codex-rs/core/src/context_manager/mod.rs b/codex-rs/core/src/context_manager/mod.rs
index 853f8af5ac09..2118ebc5754d 100644
--- a/codex-rs/core/src/context_manager/mod.rs
+++ b/codex-rs/core/src/context_manager/mod.rs
@@ -4,6 +4,7 @@ pub(crate) mod updates;
 
 pub(crate) use history::ContextManager;
 pub(crate) use history::TotalTokenUsageBreakdown;
+pub(crate) use history::estimate_item_token_count;
 pub(crate) use history::estimate_response_item_model_visible_bytes;
 pub(crate) use history::is_codex_generated_item;
 pub(crate) use history::is_user_turn_boundary;
diff --git a/codex-rs/core/src/tasks/compact.rs b/codex-rs/core/src/tasks/compact.rs
index b56f7b1df52a..9835c95bee0b 100644
--- a/codex-rs/core/src/tasks/compact.rs
+++ b/codex-rs/core/src/tasks/compact.rs
@@ -3,6 +3,8 @@ use std::sync::Arc;
 use super::SessionTask;
 use super::SessionTaskContext;
 use crate::codex::TurnContext;
+use crate::context_manager::is_user_turn_boundary;
+use crate::protocol::EventMsg;
 use crate::state::TaskKind;
 use async_trait::async_trait;
 use codex_protocol::user_input::UserInput;
@@ -25,20 +27,48 @@ impl SessionTask for CompactTask {
         _cancellation_token: CancellationToken,
     ) -> Option<String> {
         let session = session.clone_session();
+        let has_user_turn_boundary = session
+            .clone_history()
+            .await
+            .raw_items()
+            .iter()
+            .any(is_user_turn_boundary);
         if crate::compact::should_use_remote_compact_task(&ctx.provider) {
             let _ = session.services.otel_manager.counter(
                 "codex.task.compact",
                 1,
                 &[("type", "remote")],
             );
-            let _ = crate::compact_remote::run_remote_compact_task(session, ctx).await;
+            if let Err(err) =
+                crate::compact_remote::run_remote_compact_task(session.clone(), ctx.clone()).await
+            {
+                let event = EventMsg::Error(
+                    err.to_error_event(Some("Error running remote compact task".to_string())),
+                );
+                session.send_event(&ctx, event).await;
+            } else if has_user_turn_boundary {
+                // Manual `/compact` rewrites history to compacted transcript items and drops
+                // per-turn context entries. Force initial-context reseeding on the next user turn.
+                session.mark_initial_context_unseeded_for_next_turn().await;
+            }
         } else {
             let _ = session.services.otel_manager.counter(
                 "codex.task.compact",
                 1,
                 &[("type", "local")],
             );
-            let _ = crate::compact::run_compact_task(session, ctx, input).await;
+            if let Err(err) =
+                crate::compact::run_compact_task(session.clone(), ctx.clone(), input).await
+            {
+                let event = EventMsg::Error(
+                    err.to_error_event(Some("Error running local compact task".to_string())),
+                );
+                session.send_event(&ctx, event).await;
+            } else if has_user_turn_boundary {
+                // Manual `/compact` rewrites history to compacted transcript items and drops
+                // per-turn context entries. Force initial-context reseeding on the next user turn.
+                session.mark_initial_context_unseeded_for_next_turn().await;
+            }
         }
 
         None
diff --git a/codex-rs/core/src/tasks/regular.rs b/codex-rs/core/src/tasks/regular.rs
index 1428f7775bb0..4d44fc9b8147 100644
--- a/codex-rs/core/src/tasks/regular.rs
+++ b/codex-rs/core/src/tasks/regular.rs
@@ -8,6 +8,7 @@ use crate::codex::run_turn;
 use crate::state::TaskKind;
 use async_trait::async_trait;
 use codex_otel::OtelManager;
+use codex_protocol::models::ResponseItem;
 use codex_protocol::openai_models::ModelInfo;
 use codex_protocol::user_input::UserInput;
 use tokio::task::JoinHandle;
@@ -23,12 +24,14 @@ type PrewarmedSessionTask = JoinHandle<Option<ModelClientSession>>;
 
 pub(crate) struct RegularTask {
     prewarmed_session_task: Mutex<Option<PrewarmedSessionTask>>,
+    pre_turn_context_items: Vec<ResponseItem>,
 }
 
 impl Default for RegularTask {
     fn default() -> Self {
         Self {
             prewarmed_session_task: Mutex::new(None),
+            pre_turn_context_items: Vec::new(),
         }
     }
 }
@@ -55,9 +58,15 @@ impl RegularTask {
 
         Self {
             prewarmed_session_task: Mutex::new(Some(prewarmed_session_task)),
+            pre_turn_context_items: Vec::new(),
         }
     }
 
+    pub(crate) fn with_pre_turn_context_items(mut self, items: Vec<ResponseItem>) -> Self {
+        self.pre_turn_context_items = items;
+        self
+    }
+
     async fn take_prewarmed_session(&self) -> Option<ModelClientSession> {
         let prewarmed_session_task = self
             .prewarmed_session_task
@@ -101,6 +110,7 @@ impl SessionTask for RegularTask {
             sess,
             ctx,
             input,
+            self.pre_turn_context_items.clone(),
             prewarmed_client_session,
             cancellation_token,
         )
diff --git a/codex-rs/core/tests/suite/model_switching.rs b/codex-rs/core/tests/suite/model_switching.rs
index 4d4abafeb6e1..cbbbddaac38e 100644
--- a/codex-rs/core/tests/suite/model_switching.rs
+++ b/codex-rs/core/tests/suite/model_switching.rs
@@ -115,6 +115,94 @@ async fn model_change_appends_model_instructions_developer_message() -> Result<(
     Ok(())
 }
 
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn settings_only_empty_turn_persists_updates_for_next_non_empty_turn() -> Result<()> {
+    skip_if_no_network!(Ok(()));
+
+    let server = start_mock_server().await;
+    let resp_mock = mount_sse_sequence(
+        &server,
+        vec![sse_completed("resp-1"), sse_completed("resp-2")],
+    )
+    .await;
+
+    let mut builder = test_codex().with_model("gpt-5.2-codex");
+    let test = builder.build(&server).await?;
+    let model = test.session_configured.model.clone();
+
+    test.codex
+        .submit(Op::UserTurn {
+            items: vec![UserInput::Text {
+                text: "first".into(),
+                text_elements: Vec::new(),
+            }],
+            final_output_json_schema: None,
+            cwd: test.cwd_path().to_path_buf(),
+            approval_policy: AskForApproval::Never,
+            sandbox_policy: SandboxPolicy::ReadOnly,
+            model: model.clone(),
+            effort: test.config.model_reasoning_effort,
+            summary: ReasoningSummary::Auto,
+            collaboration_mode: None,
+            personality: None,
+        })
+        .await?;
+    wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
+
+    // Settings-only turn with no user message.
+    test.codex
+        .submit(Op::UserTurn {
+            items: Vec::new(),
+            final_output_json_schema: None,
+            cwd: test.cwd_path().to_path_buf(),
+            approval_policy: AskForApproval::Never,
+            sandbox_policy: SandboxPolicy::DangerFullAccess,
+            model: model.clone(),
+            effort: test.config.model_reasoning_effort,
+            summary: ReasoningSummary::Auto,
+            collaboration_mode: None,
+            personality: None,
+        })
+        .await?;
+
+    test.codex
+        .submit(Op::UserTurn {
+            items: vec![UserInput::Text {
+                text: "after settings-only turn".into(),
+                text_elements: Vec::new(),
+            }],
+            final_output_json_schema: None,
+            cwd: test.cwd_path().to_path_buf(),
+            approval_policy: AskForApproval::Never,
+            sandbox_policy: SandboxPolicy::DangerFullAccess,
+            model,
+            effort: test.config.model_reasoning_effort,
+            summary: ReasoningSummary::Auto,
+            collaboration_mode: None,
+            personality: None,
+        })
+        .await?;
+    wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
+
+    let requests = resp_mock.requests();
+    assert_eq!(
+        requests.len(),
+        2,
+        "expected only first and third turns to hit the model"
+    );
+
+    let third_turn_request = requests.last().expect("expected third turn request");
+    let developer_texts = third_turn_request.message_input_texts("developer");
+    assert!(
+        developer_texts
+            .iter()
+            .any(|text| text.contains("sandbox_mode` is `danger-full-access`")),
+        "expected danger-full-access permissions update in next non-empty turn: {developer_texts:?}"
+    );
+
+    Ok(())
+}
+
 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
 async fn model_and_personality_change_only_appends_model_instructions() -> Result<()> {
     skip_if_no_network!(Ok(()));

From 4022de74efbf43dcbd6f2242a8c2b22d31214100 Mon Sep 17 00:00:00 2001
From: Charles Cunningham <ccunningham@openai.com>
Date: Thu, 12 Feb 2026 01:28:47 -0800
Subject: [PATCH 06/80] compact: preserve incoming items during pre-turn trim
 retries

---
 codex-rs/core/src/codex.rs                    |  4 +-
 codex-rs/core/src/compact.rs                  |  8 +-
 codex-rs/core/tests/suite/compact.rs          | 85 ++++++++++++++++++-
 codex-rs/core/tests/suite/model_switching.rs  |  2 +-
 ...action_context_window_exceeded_shapes.snap |  9 +-
 5 files changed, 98 insertions(+), 10 deletions(-)

diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs
index 0d30b8d602ee..2437501836e6 100644
--- a/codex-rs/core/src/codex.rs
+++ b/codex-rs/core/src/codex.rs
@@ -3412,10 +3412,10 @@ mod handlers {
         // Attempt to inject input into current task.
         if let Err(SteerInputError::NoActiveTurn(items)) = sess.steer_input(items, None).await {
             sess.seed_initial_context_if_needed(&current_context).await;
-            let resumed_model = sess.take_pending_resume_previous_model().await;
+            let previous_model = sess.previous_model().await;
             let pre_turn_context_items = sess.build_settings_update_items(
                 previous_context.as_ref(),
-                resumed_model.as_deref(),
+                previous_model.as_deref(),
                 &current_context,
             );
             let has_user_input = !items.is_empty();
diff --git a/codex-rs/core/src/compact.rs b/codex-rs/core/src/compact.rs
index d21028e90d90..40de1b5ec4ea 100644
--- a/codex-rs/core/src/compact.rs
+++ b/codex-rs/core/src/compact.rs
@@ -167,6 +167,12 @@ async fn run_compact_task_inner(
         &[initial_input_for_turn.into()],
         turn_context.truncation_policy,
     );
+    // Keep incoming turn items and the compaction prompt pinned at the tail while trimming.
+    // Pre-turn compaction should fail with ContextWindowExceeded rather than dropping incoming
+    // items to force compaction to succeed.
+    let protected_tail_items = incoming_items
+        .as_ref()
+        .map_or(1_usize, |items| items.len().saturating_add(1));
 
     let mut truncated_count = 0usize;
 
@@ -224,7 +230,7 @@ async fn run_compact_task_inner(
                 return Err(CodexErr::Interrupted);
             }
             Err(e @ CodexErr::ContextWindowExceeded) => {
-                if turn_input_len > 1 {
+                if turn_input_len > 1 && history.raw_items().len() > protected_tail_items {
                     // Trim from the beginning to preserve cache (prefix-based) and keep recent
                     // messages intact.
                     error!(
diff --git a/codex-rs/core/tests/suite/compact.rs b/codex-rs/core/tests/suite/compact.rs
index 08850ec38f60..c7da207a6f67 100644
--- a/codex-rs/core/tests/suite/compact.rs
+++ b/codex-rs/core/tests/suite/compact.rs
@@ -3235,9 +3235,9 @@ async fn snapshot_request_shape_pre_turn_compaction_context_window_exceeded() {
     insta::assert_snapshot!(
         "pre_turn_compaction_context_window_exceeded_shapes",
         format_labeled_requests_snapshot(
-            "Pre-turn auto-compaction context-window failure: compaction request excludes the incoming user message and the turn errors.",
+            "Pre-turn auto-compaction context-window failure: compaction request includes the incoming user message and the turn errors.",
             &[(
-                "Local Compaction Request (Incoming User Excluded)",
+                "Local Compaction Request (Incoming User Included)",
                 &requests[1]
             ),]
         )
@@ -3249,6 +3249,87 @@ async fn snapshot_request_shape_pre_turn_compaction_context_window_exceeded() {
     );
 }
 
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn pre_turn_local_compaction_trim_retries_keep_incoming_items() {
+    skip_if_no_network!();
+
+    let server = start_mock_server().await;
+    let compact_failed = sse_failed(
+        "compact-failed",
+        "context_length_exceeded",
+        CONTEXT_LIMIT_MESSAGE,
+    );
+    let request_log = mount_sse_sequence(
+        &server,
+        vec![
+            compact_failed.clone(),
+            compact_failed.clone(),
+            compact_failed.clone(),
+            compact_failed,
+        ],
+    )
+    .await;
+
+    let model_provider = non_openai_model_provider(&server);
+    let codex = test_codex()
+        .with_config(move |config| {
+            config.model_provider = model_provider;
+            set_test_compact_prompt(config);
+            config.model_auto_compact_token_limit = Some(1);
+        })
+        .build(&server)
+        .await
+        .expect("build codex")
+        .codex;
+
+    let incoming_text = "PRETURN_INCOMING_USER";
+    codex
+        .submit(Op::UserInput {
+            items: vec![UserInput::Text {
+                text: incoming_text.to_string(),
+                text_elements: Vec::new(),
+            }],
+            final_output_json_schema: None,
+        })
+        .await
+        .expect("submit user input");
+
+    let error_message = wait_for_event_match(&codex, |event| match event {
+        EventMsg::Error(err) => Some(err.message.clone()),
+        _ => None,
+    })
+    .await;
+    wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
+
+    assert!(
+        error_message.contains(
+            "Incoming user message and/or turn context is too large to fit in context window"
+        ),
+        "expected oversized incoming-items error, got {error_message}"
+    );
+
+    let compact_attempts = request_log.requests();
+    assert_eq!(
+        compact_attempts.len(),
+        4,
+        "expected pre-turn compaction to retry while trimming seeded history only"
+    );
+    for (index, request) in compact_attempts.iter().enumerate() {
+        let body = request.body_json().to_string();
+        assert!(
+            body_contains_text(&body, SUMMARIZATION_PROMPT),
+            "request {index} should be a compaction attempt"
+        );
+        assert!(
+            request
+                .message_input_texts("user")
+                .iter()
+                .any(|text| text == incoming_text),
+            "request {index} dropped incoming user text during trim retries"
+        );
+    }
+}
+
 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
 async fn snapshot_request_shape_manual_compact_without_previous_user_messages() {
     skip_if_no_network!();
diff --git a/codex-rs/core/tests/suite/model_switching.rs b/codex-rs/core/tests/suite/model_switching.rs
index cbbbddaac38e..d02374f54ccf 100644
--- a/codex-rs/core/tests/suite/model_switching.rs
+++ b/codex-rs/core/tests/suite/model_switching.rs
@@ -139,7 +139,7 @@ async fn settings_only_empty_turn_persists_updates_for_next_non_empty_turn() ->
             final_output_json_schema: None,
             cwd: test.cwd_path().to_path_buf(),
             approval_policy: AskForApproval::Never,
-            sandbox_policy: SandboxPolicy::ReadOnly,
+            sandbox_policy: SandboxPolicy::new_read_only_policy(),
             model: model.clone(),
             effort: test.config.model_reasoning_effort,
             summary: ReasoningSummary::Auto,
diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_turn_compaction_context_window_exceeded_shapes.snap b/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_turn_compaction_context_window_exceeded_shapes.snap
index da30b01bd7c5..89066bacaac1 100644
--- a/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_turn_compaction_context_window_exceeded_shapes.snap
+++ b/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_turn_compaction_context_window_exceeded_shapes.snap
@@ -1,13 +1,14 @@
 ---
 source: core/tests/suite/compact.rs
-expression: "format_labeled_requests_snapshot(\"Pre-turn auto-compaction context-window failure: compaction request excludes the incoming user message and the turn errors.\",\n&[(\"Local Compaction Request (Incoming User Excluded)\", &requests[1]),])"
+expression: "format_labeled_requests_snapshot(\"Pre-turn auto-compaction context-window failure: compaction request includes the incoming user message and the turn errors.\",\n&[(\"Local Compaction Request (Incoming User Included)\", &requests[1]),])"
 ---
-Scenario: Pre-turn auto-compaction context-window failure: compaction request excludes the incoming user message and the turn errors.
+Scenario: Pre-turn auto-compaction context-window failure: compaction request includes the incoming user message and the turn errors.
 
-## Local Compaction Request (Incoming User Excluded)
+## Local Compaction Request (Incoming User Included)
 00:message/developer:<PERMISSIONS_INSTRUCTIONS>
 01:message/user:<AGENTS_MD>
 02:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
 03:message/user:USER_ONE
 04:message/assistant:FIRST_REPLY
-05:message/user:<SUMMARIZATION_PROMPT>
+05:message/user:USER_TWO
+06:message/user:<SUMMARIZATION_PROMPT>

From e87455e2840cc62a85d6d80120648fbffe793cc1 Mon Sep 17 00:00:00 2001
From: Charles Cunningham <ccunningham@openai.com>
Date: Thu, 12 Feb 2026 01:33:41 -0800
Subject: [PATCH 07/80] codex: persist pre-turn updates when compaction fails

---
 codex-rs/core/src/codex.rs           |  33 ++++---
 codex-rs/core/tests/suite/compact.rs | 133 +++++++++++++++++++++++++++
 2 files changed, 152 insertions(+), 14 deletions(-)

diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs
index 2437501836e6..25e8849cdfa1 100644
--- a/codex-rs/core/src/codex.rs
+++ b/codex-rs/core/src/codex.rs
@@ -4293,21 +4293,26 @@ pub(crate) async fn run_turn(
     .await
     {
         Ok(outcome) => outcome,
-        Err(CodexErr::ContextWindowExceeded) => {
-            let incoming_items_tokens_estimate = incoming_turn_items
-                .iter()
-                .map(estimate_item_token_count)
-                .fold(0_i64, i64::saturating_add);
-            let message = format!(
-                "Incoming user message and/or turn context is too large to fit in context window. Please reduce the size of your message and try again. (incoming_items_tokens_estimate={incoming_items_tokens_estimate})"
-            );
-            let event =
-                EventMsg::Error(CodexErr::ContextWindowExceeded.to_error_event(Some(message)));
-            sess.send_event(&turn_context, event).await;
-            return None;
-        }
         Err(err) => {
-            let event = EventMsg::Error(err.to_error_event(None));
+            if !pre_turn_context_items.is_empty() {
+                // Preserve model-visible settings updates even when pre-turn compaction fails
+                // before we can persist turn input.
+                sess.record_conversation_items(&turn_context, &pre_turn_context_items)
+                    .await;
+            }
+            let event = match err {
+                CodexErr::ContextWindowExceeded => {
+                    let incoming_items_tokens_estimate = incoming_turn_items
+                        .iter()
+                        .map(estimate_item_token_count)
+                        .fold(0_i64, i64::saturating_add);
+                    let message = format!(
+                        "Incoming user message and/or turn context is too large to fit in context window. Please reduce the size of your message and try again. (incoming_items_tokens_estimate={incoming_items_tokens_estimate})"
+                    );
+                    EventMsg::Error(CodexErr::ContextWindowExceeded.to_error_event(Some(message)))
+                }
+                other => EventMsg::Error(other.to_error_event(None)),
+            };
             sess.send_event(&turn_context, event).await;
             return None;
         }
diff --git a/codex-rs/core/tests/suite/compact.rs b/codex-rs/core/tests/suite/compact.rs
index c7da207a6f67..d504f9ff55a7 100644
--- a/codex-rs/core/tests/suite/compact.rs
+++ b/codex-rs/core/tests/suite/compact.rs
@@ -3330,6 +3330,139 @@ async fn pre_turn_local_compaction_trim_retries_keep_incoming_items() {
     }
 }
 
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn pre_turn_compaction_failure_persists_context_updates_for_next_turn() {
+    skip_if_no_network!();
+
+    let server = start_mock_server().await;
+    let compact_failed = sse_failed(
+        "compact-failed",
+        "context_length_exceeded",
+        CONTEXT_LIMIT_MESSAGE,
+    );
+    let third_turn = sse(vec![
+        ev_assistant_message("m3", FINAL_REPLY),
+        ev_completed_with_tokens("r3", 80),
+    ]);
+    let request_log = mount_sse_sequence(
+        &server,
+        vec![
+            compact_failed.clone(),
+            compact_failed.clone(),
+            compact_failed.clone(),
+            compact_failed,
+            third_turn,
+        ],
+    )
+    .await;
+
+    let mut model_provider = non_openai_model_provider(&server);
+    model_provider.stream_max_retries = Some(0);
+    let codex = test_codex()
+        .with_config(move |config| {
+            config.model_provider = model_provider;
+            set_test_compact_prompt(config);
+            config.model_auto_compact_token_limit = Some(300);
+        })
+        .build(&server)
+        .await
+        .expect("build codex")
+        .codex;
+
+    // Seed `previous_context` without adding a user turn to history.
+    codex
+        .submit(Op::UserTurn {
+            items: Vec::new(),
+            final_output_json_schema: None,
+            cwd: PathBuf::from("."),
+            approval_policy: AskForApproval::Never,
+            sandbox_policy: SandboxPolicy::new_read_only_policy(),
+            model: "gpt-5.2-codex".to_string(),
+            effort: None,
+            summary: ReasoningSummary::Auto,
+            collaboration_mode: None,
+            personality: None,
+        })
+        .await
+        .expect("submit empty settings-only turn");
+
+    let oversized_input = "X".repeat(2_000);
+    codex
+        .submit(Op::UserTurn {
+            items: vec![UserInput::Text {
+                text: oversized_input,
+                text_elements: Vec::new(),
+            }],
+            final_output_json_schema: None,
+            cwd: PathBuf::from("."),
+            approval_policy: AskForApproval::Never,
+            sandbox_policy: SandboxPolicy::DangerFullAccess,
+            model: "gpt-5.2-codex".to_string(),
+            effort: None,
+            summary: ReasoningSummary::Auto,
+            collaboration_mode: None,
+            personality: None,
+        })
+        .await
+        .expect("submit oversized turn that triggers pre-turn compaction failure");
+    let error_message = wait_for_event_match(&codex, |event| match event {
+        EventMsg::Error(err) => Some(err.message.clone()),
+        _ => None,
+    })
+    .await;
+    wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
+    assert!(
+        error_message.contains(
+            "Incoming user message and/or turn context is too large to fit in context window"
+        ),
+        "expected oversized incoming-items error, got {error_message}"
+    );
+
+    let follow_up_text = "after failed pre-turn compact";
+    codex
+        .submit(Op::UserTurn {
+            items: vec![UserInput::Text {
+                text: follow_up_text.to_string(),
+                text_elements: Vec::new(),
+            }],
+            final_output_json_schema: None,
+            cwd: PathBuf::from("."),
+            approval_policy: AskForApproval::Never,
+            sandbox_policy: SandboxPolicy::DangerFullAccess,
+            model: "gpt-5.2-codex".to_string(),
+            effort: None,
+            summary: ReasoningSummary::Auto,
+            collaboration_mode: None,
+            personality: None,
+        })
+        .await
+        .expect("submit follow-up turn");
+    wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
+
+    let requests = request_log.requests();
+    assert_eq!(
+        requests.len(),
+        5,
+        "expected four failed pre-turn compaction attempts and one follow-up model request"
+    );
+
+    let follow_up_request = requests.last().expect("missing follow-up request");
+    let follow_up_developer_texts = follow_up_request.message_input_texts("developer");
+    assert!(
+        follow_up_developer_texts
+            .iter()
+            .any(|text| text.contains("sandbox_mode` is `danger-full-access`")),
+        "expected danger-full-access permissions update in follow-up turn after failed pre-turn compaction: {follow_up_developer_texts:?}"
+    );
+    assert!(
+        follow_up_request
+            .message_input_texts("user")
+            .iter()
+            .any(|text| text == follow_up_text),
+        "expected follow-up request to include follow-up user message"
+    );
+}
+
 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
 async fn snapshot_request_shape_manual_compact_without_previous_user_messages() {
     skip_if_no_network!();

From 8d06dd5f8b4f332084ab40f807b86ef17e82c4f1 Mon Sep 17 00:00:00 2001
From: Charles Cunningham <ccunningham@openai.com>
Date: Thu, 12 Feb 2026 11:33:42 -0800
Subject: [PATCH 08/80] Persist pre-turn updates before apps tool-list
 cancellation

---
 codex-rs/core/src/codex.rs | 63 +++++++++++++++++++++++++++++++-------
 1 file changed, 52 insertions(+), 11 deletions(-)

diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs
index 25e8849cdfa1..d13f51c4367a 100644
--- a/codex-rs/core/src/codex.rs
+++ b/codex-rs/core/src/codex.rs
@@ -4317,6 +4317,15 @@ pub(crate) async fn run_turn(
             return None;
         }
     };
+    persist_pre_turn_items_for_compaction_outcome(
+        &sess,
+        &turn_context,
+        pre_turn_compaction_outcome,
+        &pre_turn_context_items,
+        &input,
+        response_item,
+    )
+    .await;
 
     let skills_outcome = Some(
         sess.services
@@ -4424,17 +4433,6 @@ pub(crate) async fn run_turn(
         .track_app_mentioned(tracking.clone(), mentioned_app_invocations);
     sess.merge_connector_selection(explicitly_enabled_connectors.clone())
         .await;
-
-    persist_pre_turn_items_for_compaction_outcome(
-        &sess,
-        &turn_context,
-        pre_turn_compaction_outcome,
-        &pre_turn_context_items,
-        &input,
-        response_item,
-    )
-    .await;
-
     if !skill_items.is_empty() {
         sess.record_conversation_items(&turn_context, &skill_items)
             .await;
@@ -6125,6 +6123,49 @@ mod tests {
         assert_eq!(actual, Vec::<ResponseItem>::new());
     }
 
+    #[tokio::test]
+    async fn run_turn_persists_pre_turn_context_before_apps_tool_listing_cancellation() {
+        let (session, mut turn_context) = make_session_and_context().await;
+        let session = Arc::new(session);
+        let mut config = (*turn_context.config).clone();
+        config.features.enable(Feature::Apps);
+        turn_context.features = config.features.clone();
+        turn_context.config = Arc::new(config);
+        let turn_context = Arc::new(turn_context);
+        let input = vec![UserInput::Text {
+            text: "hello".to_string(),
+            text_elements: Vec::new(),
+        }];
+        let response_item: ResponseItem = ResponseInputItem::from(input.clone()).into();
+        let pre_turn_context_items = vec![ResponseItem::Message {
+            id: None,
+            role: "developer".to_string(),
+            content: vec![ContentItem::InputText {
+                text: "model-visible setting diff".to_string(),
+            }],
+            end_turn: None,
+            phase: None,
+        }];
+        let cancellation_token = CancellationToken::new();
+        cancellation_token.cancel();
+
+        let result = run_turn(
+            Arc::clone(&session),
+            Arc::clone(&turn_context),
+            input,
+            pre_turn_context_items.clone(),
+            None,
+            cancellation_token,
+        )
+        .await;
+        assert_eq!(result, None);
+
+        let mut expected_history = pre_turn_context_items;
+        expected_history.push(response_item);
+        let actual_history = session.clone_history().await.raw_items().to_vec();
+        assert_eq!(actual_history, expected_history);
+    }
+
     #[test]
     fn estimate_user_input_token_count_is_positive_for_text_input() {
         let input = vec![UserInput::Text {

From 36499b59f0c2809519a7aeeeb354170aa9cd78fc Mon Sep 17 00:00:00 2001
From: Charles Cunningham <ccunningham@openai.com>
Date: Thu, 12 Feb 2026 11:39:43 -0800
Subject: [PATCH 09/80] Reinject canonical context for model-switch pre-turn
 compaction

---
 codex-rs/core/src/codex.rs           | 6 +++++-
 codex-rs/core/tests/suite/compact.rs | 9 +++++++++
 2 files changed, 14 insertions(+), 1 deletion(-)

diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs
index d13f51c4367a..b30a1fa89ab1 100644
--- a/codex-rs/core/src/codex.rs
+++ b/codex-rs/core/src/codex.rs
@@ -4681,7 +4681,11 @@ async fn maybe_run_previous_model_inline_compact(
             sess,
             &previous_turn_context,
             AutoCompactCallsite::PreTurnExcludingIncomingUserMessage,
-            TurnContextReinjection::Skip,
+            // Even though incoming turn items are excluded here, this pass can be the only
+            // compaction run before submission. Reinject canonical context so unchanged
+            // model-visible instructions remain present if no follow-up pre-turn compaction
+            // is needed.
+            TurnContextReinjection::ReinjectAboveLastRealUser,
             None,
         )
         .await?;
diff --git a/codex-rs/core/tests/suite/compact.rs b/codex-rs/core/tests/suite/compact.rs
index d504f9ff55a7..5a6ad5b42657 100644
--- a/codex-rs/core/tests/suite/compact.rs
+++ b/codex-rs/core/tests/suite/compact.rs
@@ -132,11 +132,20 @@ fn assert_pre_sampling_switch_compaction_requests(
         !compact_body.contains("<model_switch>"),
         "pre-sampling compact request should strip trailing model-switch update item"
     );
+    let first_body = first.to_string();
+    assert!(
+        body_contains_text(&first_body, "<environment_context>"),
+        "first request should include canonical environment context"
+    );
     let follow_up_body = follow_up.to_string();
     assert!(
         follow_up_body.contains("<model_switch>"),
         "follow-up request after successful model-switch compaction should include model-switch update item"
     );
+    assert!(
+        body_contains_text(&follow_up_body, "<environment_context>"),
+        "follow-up request should preserve canonical environment context after pre-sampling compaction"
+    );
 }
 
 async fn assert_compaction_uses_turn_lifecycle_id(codex: &std::sync::Arc<codex_core::CodexThread>) {

From 59d3431a1547703b35c91c0ac09b3c234a58b438 Mon Sep 17 00:00:00 2001
From: Charles Cunningham <ccunningham@openai.com>
Date: Thu, 12 Feb 2026 12:15:09 -0800
Subject: [PATCH 10/80] Fix clippy useless_vec in compact_remote test

---
 codex-rs/core/src/compact_remote.rs | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/codex-rs/core/src/compact_remote.rs b/codex-rs/core/src/compact_remote.rs
index 6d357aecdc92..1039e24401d9 100644
--- a/codex-rs/core/src/compact_remote.rs
+++ b/codex-rs/core/src/compact_remote.rs
@@ -331,7 +331,7 @@ mod tests {
         let base_instructions = BaseInstructions {
             text: String::new(),
         };
-        let incoming_items = vec![user_message(
+        let incoming_items = [user_message(
             "INCOMING_USER_MESSAGE_THAT_TIPS_OVER_THE_WINDOW",
         )];
         let incoming_items_tokens = incoming_items
@@ -344,7 +344,7 @@ mod tests {
         );
 
         let mut history = ContextManager::new();
-        let history_items = vec![
+        let history_items = [
             user_message("USER_ONE"),
             developer_message("TRAILING_CODEX_GENERATED_CONTEXT"),
         ];

From 2a804d9f4d9e67741e64c8c95b989e6fb3c584c1 Mon Sep 17 00:00:00 2001
From: Charles Cunningham <ccunningham@openai.com>
Date: Thu, 12 Feb 2026 14:34:31 -0800
Subject: [PATCH 11/80] Update snaps

---
 ...anual_compact_with_history_shapes.snap.new | 22 +++++++++++++++
 ...mpact__mid_turn_compaction_shapes.snap.new | 25 +++++++++++++++++
 ...paction_including_incoming_shapes.snap.new | 27 +++++++++++++++++++
 ...anual_compact_with_history_shapes.snap.new | 21 +++++++++++++++
 ...remote_mid_turn_compaction_shapes.snap.new | 21 +++++++++++++++
 ...on_context_window_exceeded_shapes.snap.new | 14 ++++++++++
 ...paction_including_incoming_shapes.snap.new | 25 +++++++++++++++++
 7 files changed, 155 insertions(+)
 create mode 100644 codex-rs/core/tests/suite/snapshots/all__suite__compact__manual_compact_with_history_shapes.snap.new
 create mode 100644 codex-rs/core/tests/suite/snapshots/all__suite__compact__mid_turn_compaction_shapes.snap.new
 create mode 100644 codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_turn_compaction_including_incoming_shapes.snap.new
 create mode 100644 codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_manual_compact_with_history_shapes.snap.new
 create mode 100644 codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_mid_turn_compaction_shapes.snap.new
 create mode 100644 codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_pre_turn_compaction_context_window_exceeded_shapes.snap.new
 create mode 100644 codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_pre_turn_compaction_including_incoming_shapes.snap.new

diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact__manual_compact_with_history_shapes.snap.new b/codex-rs/core/tests/suite/snapshots/all__suite__compact__manual_compact_with_history_shapes.snap.new
new file mode 100644
index 000000000000..6fa78f6e20bd
--- /dev/null
+++ b/codex-rs/core/tests/suite/snapshots/all__suite__compact__manual_compact_with_history_shapes.snap.new
@@ -0,0 +1,22 @@
+---
+source: core/tests/suite/compact.rs
+assertion_line: 2395
+expression: "format_labeled_requests_snapshot(\"Manual /compact with prior user history compacts existing history and the follow-up turn includes the compact summary plus new user message.\",\n&[(\"Local Compaction Request\", &requests[1]),\n(\"Local Post-Compaction History Layout\", &requests[2]),])"
+---
+Scenario: Manual /compact with prior user history compacts existing history and the follow-up turn includes the compact summary plus new user message.
+
+## Local Compaction Request
+00:message/developer:<PERMISSIONS_INSTRUCTIONS>
+01:message/user:<AGENTS_MD>
+02:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
+03:message/user:first manual turn
+04:message/assistant:FIRST_REPLY
+05:message/user:<SUMMARIZATION_PROMPT>
+
+## Local Post-Compaction History Layout
+00:message/user:first manual turn
+01:message/user:<COMPACTION_SUMMARY>\nFIRST_MANUAL_SUMMARY
+02:message/developer:<PERMISSIONS_INSTRUCTIONS>
+03:message/user:<AGENTS_MD>
+04:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
+05:message/user:second manual turn
diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact__mid_turn_compaction_shapes.snap.new b/codex-rs/core/tests/suite/snapshots/all__suite__compact__mid_turn_compaction_shapes.snap.new
new file mode 100644
index 000000000000..9d87e6a9e1f1
--- /dev/null
+++ b/codex-rs/core/tests/suite/snapshots/all__suite__compact__mid_turn_compaction_shapes.snap.new
@@ -0,0 +1,25 @@
+---
+source: core/tests/suite/compact.rs
+assertion_line: 2657
+expression: "format_labeled_requests_snapshot(\"Mid-turn continuation compaction after tool output: compact request includes tool artifacts and follow-up request includes the summary.\",\n&[(\"Local Compaction Request\", &auto_compact_mock.single_request()),\n(\"Local Post-Compaction History Layout\",\n&post_auto_compact_mock.single_request()),])"
+---
+Scenario: Mid-turn continuation compaction after tool output: compact request includes tool artifacts and follow-up request includes the summary.
+
+## Local Compaction Request
+00:message/developer:<PERMISSIONS_INSTRUCTIONS>
+01:message/user:<AGENTS_MD>
+02:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
+03:message/user:function call limit push
+04:function_call/test_tool
+05:function_call_output:unsupported call: test_tool
+06:message/assistant:FINAL_REPLY
+07:message/user:FOLLOW_UP_AFTER_LIMIT
+08:message/user:<SUMMARIZATION_PROMPT>
+
+## Local Post-Compaction History Layout
+00:message/user:function call limit push
+01:message/developer:<PERMISSIONS_INSTRUCTIONS>
+02:message/user:<AGENTS_MD>
+03:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
+04:message/user:FOLLOW_UP_AFTER_LIMIT
+05:message/user:<COMPACTION_SUMMARY>\nAUTO_SUMMARY
diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_turn_compaction_including_incoming_shapes.snap.new b/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_turn_compaction_including_incoming_shapes.snap.new
new file mode 100644
index 000000000000..0458d8807299
--- /dev/null
+++ b/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_turn_compaction_including_incoming_shapes.snap.new
@@ -0,0 +1,27 @@
+---
+source: core/tests/suite/compact.rs
+assertion_line: 3026
+expression: "format_labeled_requests_snapshot(\"Pre-turn auto-compaction with a context override emits the context diff in the compact request while the incoming user message is still excluded.\",\n&[(\"Local Compaction Request\", &requests[2]),\n(\"Local Post-Compaction History Layout\", &requests[3]),])"
+---
+Scenario: Pre-turn auto-compaction with a context override emits the context diff in the compact request while the incoming user message is still excluded.
+
+## Local Compaction Request
+00:message/developer:<PERMISSIONS_INSTRUCTIONS>
+01:message/user:<AGENTS_MD>
+02:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
+03:message/user:USER_ONE
+04:message/assistant:FIRST_REPLY
+05:message/user:USER_TWO
+06:message/assistant:SECOND_REPLY
+07:message/user:<ENVIRONMENT_CONTEXT:cwd=PRETURN_CONTEXT_DIFF_CWD>
+08:message/user:<image> | </image> | USER_THREE
+09:message/user:<SUMMARIZATION_PROMPT>
+
+## Local Post-Compaction History Layout
+00:message/user:USER_ONE
+01:message/user:USER_TWO
+02:message/developer:<PERMISSIONS_INSTRUCTIONS>
+03:message/user:<AGENTS_MD>
+04:message/user:<ENVIRONMENT_CONTEXT:cwd=PRETURN_CONTEXT_DIFF_CWD>
+05:message/user:<image> | </image> | USER_THREE
+06:message/user:<COMPACTION_SUMMARY>\nPRE_TURN_SUMMARY
diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_manual_compact_with_history_shapes.snap.new b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_manual_compact_with_history_shapes.snap.new
new file mode 100644
index 000000000000..f9e4ad23aa3d
--- /dev/null
+++ b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_manual_compact_with_history_shapes.snap.new
@@ -0,0 +1,21 @@
+---
+source: core/tests/suite/compact_remote.rs
+assertion_line: 174
+expression: "format_labeled_requests_snapshot(\"Remote manual /compact with prior user history compacts existing history and follow-up includes compact summary plus new user message.\",\n&[(\"Remote Compaction Request\", &compact_request),\n(\"Remote Post-Compaction History Layout\", follow_up_request),])"
+---
+Scenario: Remote manual /compact with prior user history compacts existing history and follow-up includes compact summary plus new user message.
+
+## Remote Compaction Request
+00:message/developer:<PERMISSIONS_INSTRUCTIONS>
+01:message/user:<AGENTS_MD>
+02:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
+03:message/user:hello remote compact
+04:message/assistant:FIRST_REMOTE_REPLY
+
+## Remote Post-Compaction History Layout
+00:message/user:REMOTE_COMPACTED_SUMMARY
+01:compaction:encrypted=true
+02:message/developer:<PERMISSIONS_INSTRUCTIONS>
+03:message/user:<AGENTS_MD>
+04:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
+05:message/user:after compact
diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_mid_turn_compaction_shapes.snap.new b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_mid_turn_compaction_shapes.snap.new
new file mode 100644
index 000000000000..e7621c95404f
--- /dev/null
+++ b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_mid_turn_compaction_shapes.snap.new
@@ -0,0 +1,21 @@
+---
+source: core/tests/suite/compact_remote.rs
+assertion_line: 1578
+expression: "format_labeled_requests_snapshot(\"Remote mid-turn continuation compaction after tool output: compact request includes tool artifacts and follow-up request includes the summary.\",\n&[(\"Remote Compaction Request\", &compact_request),\n(\"Remote Post-Compaction History Layout\", &requests[1]),])"
+---
+Scenario: Remote mid-turn continuation compaction after tool output: compact request includes tool artifacts and follow-up request includes the summary.
+
+## Remote Compaction Request
+00:message/developer:<PERMISSIONS_INSTRUCTIONS>
+01:message/user:<AGENTS_MD>
+02:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
+03:message/user:USER_ONE
+04:function_call/test_tool
+05:function_call_output:unsupported call: test_tool
+
+## Remote Post-Compaction History Layout
+00:message/developer:<PERMISSIONS_INSTRUCTIONS>
+01:message/user:<AGENTS_MD>
+02:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
+03:message/user:USER_ONE
+04:message/user:<COMPACTION_SUMMARY>\nREMOTE_MID_TURN_SUMMARY
diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_pre_turn_compaction_context_window_exceeded_shapes.snap.new b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_pre_turn_compaction_context_window_exceeded_shapes.snap.new
new file mode 100644
index 000000000000..01bbca3bba18
--- /dev/null
+++ b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_pre_turn_compaction_context_window_exceeded_shapes.snap.new
@@ -0,0 +1,14 @@
+---
+source: core/tests/suite/compact_remote.rs
+assertion_line: 1503
+expression: "format_labeled_requests_snapshot(\"Remote pre-turn auto-compaction context-window failure: compaction request excludes the incoming user message and the turn errors.\",\n&[(\"Remote Compaction Request (Incoming User Excluded)\",\n&include_attempt_request),])"
+---
+Scenario: Remote pre-turn auto-compaction context-window failure: compaction request excludes the incoming user message and the turn errors.
+
+## Remote Compaction Request (Incoming User Excluded)
+00:message/developer:<PERMISSIONS_INSTRUCTIONS>
+01:message/user:<AGENTS_MD>
+02:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
+03:message/user:USER_ONE
+04:message/assistant:REMOTE_FIRST_REPLY
+05:message/user:USER_TWO
diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_pre_turn_compaction_including_incoming_shapes.snap.new b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_pre_turn_compaction_including_incoming_shapes.snap.new
new file mode 100644
index 000000000000..a77063353042
--- /dev/null
+++ b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_pre_turn_compaction_including_incoming_shapes.snap.new
@@ -0,0 +1,25 @@
+---
+source: core/tests/suite/compact_remote.rs
+assertion_line: 1398
+expression: "format_labeled_requests_snapshot(\"Remote pre-turn auto-compaction with a context override emits the context diff in the compact request while excluding the incoming user message.\",\n&[(\"Remote Compaction Request\", &compact_request),\n(\"Remote Post-Compaction History Layout\", &requests[2]),])"
+---
+Scenario: Remote pre-turn auto-compaction with a context override emits the context diff in the compact request while excluding the incoming user message.
+
+## Remote Compaction Request
+00:message/developer:<PERMISSIONS_INSTRUCTIONS>
+01:message/user:<AGENTS_MD>
+02:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
+03:message/user:USER_ONE
+04:message/assistant:REMOTE_FIRST_REPLY
+05:message/user:USER_TWO
+06:message/assistant:REMOTE_SECOND_REPLY
+07:message/user:<ENVIRONMENT_CONTEXT:cwd=PRETURN_CONTEXT_DIFF_CWD>
+08:message/user:USER_THREE
+
+## Remote Post-Compaction History Layout
+00:message/user:USER_ONE
+01:message/developer:<PERMISSIONS_INSTRUCTIONS>
+02:message/user:<AGENTS_MD>
+03:message/user:<ENVIRONMENT_CONTEXT:cwd=PRETURN_CONTEXT_DIFF_CWD>
+04:message/user:USER_TWO
+05:message/user:<COMPACTION_SUMMARY>\nREMOTE_PRE_TURN_SUMMARY

From 8b2f5066a57c9a909b132ba32f00da7c7fb2583b Mon Sep 17 00:00:00 2001
From: Charles Cunningham <ccunningham@openai.com>
Date: Thu, 12 Feb 2026 14:36:13 -0800
Subject: [PATCH 12/80] update snaps

---
 ...t__manual_compact_with_history_shapes.snap | 10 +++----
 ...anual_compact_with_history_shapes.snap.new | 22 ---------------
 ...mpact__mid_turn_compaction_shapes.snap.new | 25 -----------------
 ..._compaction_including_incoming_shapes.snap | 17 ++++++------
 ...paction_including_incoming_shapes.snap.new | 27 -------------------
 ...te_manual_compact_with_history_shapes.snap | 10 +++----
 ...anual_compact_with_history_shapes.snap.new | 21 ---------------
 ...te__remote_mid_turn_compaction_shapes.snap |  8 +++---
 ...remote_mid_turn_compaction_shapes.snap.new | 21 ---------------
 ...action_context_window_exceeded_shapes.snap |  1 +
 ...on_context_window_exceeded_shapes.snap.new | 14 ----------
 ..._compaction_including_incoming_shapes.snap | 10 +++----
 ...paction_including_incoming_shapes.snap.new | 25 -----------------
 13 files changed, 29 insertions(+), 182 deletions(-)
 delete mode 100644 codex-rs/core/tests/suite/snapshots/all__suite__compact__manual_compact_with_history_shapes.snap.new
 delete mode 100644 codex-rs/core/tests/suite/snapshots/all__suite__compact__mid_turn_compaction_shapes.snap.new
 delete mode 100644 codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_turn_compaction_including_incoming_shapes.snap.new
 delete mode 100644 codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_manual_compact_with_history_shapes.snap.new
 delete mode 100644 codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_mid_turn_compaction_shapes.snap.new
 delete mode 100644 codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_pre_turn_compaction_context_window_exceeded_shapes.snap.new
 delete mode 100644 codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_pre_turn_compaction_including_incoming_shapes.snap.new

diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact__manual_compact_with_history_shapes.snap b/codex-rs/core/tests/suite/snapshots/all__suite__compact__manual_compact_with_history_shapes.snap
index bcca5d357552..c88ff33122d6 100644
--- a/codex-rs/core/tests/suite/snapshots/all__suite__compact__manual_compact_with_history_shapes.snap
+++ b/codex-rs/core/tests/suite/snapshots/all__suite__compact__manual_compact_with_history_shapes.snap
@@ -13,9 +13,9 @@ Scenario: Manual /compact with prior user history compacts existing history and
 05:message/user:<SUMMARIZATION_PROMPT>
 
 ## Local Post-Compaction History Layout
-00:message/developer:<PERMISSIONS_INSTRUCTIONS>
-01:message/user:<AGENTS_MD>
-02:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
-03:message/user:first manual turn
-04:message/user:<COMPACTION_SUMMARY>\nFIRST_MANUAL_SUMMARY
+00:message/user:first manual turn
+01:message/user:<COMPACTION_SUMMARY>\nFIRST_MANUAL_SUMMARY
+02:message/developer:<PERMISSIONS_INSTRUCTIONS>
+03:message/user:<AGENTS_MD>
+04:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
 05:message/user:second manual turn
diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact__manual_compact_with_history_shapes.snap.new b/codex-rs/core/tests/suite/snapshots/all__suite__compact__manual_compact_with_history_shapes.snap.new
deleted file mode 100644
index 6fa78f6e20bd..000000000000
--- a/codex-rs/core/tests/suite/snapshots/all__suite__compact__manual_compact_with_history_shapes.snap.new
+++ /dev/null
@@ -1,22 +0,0 @@
----
-source: core/tests/suite/compact.rs
-assertion_line: 2395
-expression: "format_labeled_requests_snapshot(\"Manual /compact with prior user history compacts existing history and the follow-up turn includes the compact summary plus new user message.\",\n&[(\"Local Compaction Request\", &requests[1]),\n(\"Local Post-Compaction History Layout\", &requests[2]),])"
----
-Scenario: Manual /compact with prior user history compacts existing history and the follow-up turn includes the compact summary plus new user message.
-
-## Local Compaction Request
-00:message/developer:<PERMISSIONS_INSTRUCTIONS>
-01:message/user:<AGENTS_MD>
-02:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
-03:message/user:first manual turn
-04:message/assistant:FIRST_REPLY
-05:message/user:<SUMMARIZATION_PROMPT>
-
-## Local Post-Compaction History Layout
-00:message/user:first manual turn
-01:message/user:<COMPACTION_SUMMARY>\nFIRST_MANUAL_SUMMARY
-02:message/developer:<PERMISSIONS_INSTRUCTIONS>
-03:message/user:<AGENTS_MD>
-04:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
-05:message/user:second manual turn
diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact__mid_turn_compaction_shapes.snap.new b/codex-rs/core/tests/suite/snapshots/all__suite__compact__mid_turn_compaction_shapes.snap.new
deleted file mode 100644
index 9d87e6a9e1f1..000000000000
--- a/codex-rs/core/tests/suite/snapshots/all__suite__compact__mid_turn_compaction_shapes.snap.new
+++ /dev/null
@@ -1,25 +0,0 @@
----
-source: core/tests/suite/compact.rs
-assertion_line: 2657
-expression: "format_labeled_requests_snapshot(\"Mid-turn continuation compaction after tool output: compact request includes tool artifacts and follow-up request includes the summary.\",\n&[(\"Local Compaction Request\", &auto_compact_mock.single_request()),\n(\"Local Post-Compaction History Layout\",\n&post_auto_compact_mock.single_request()),])"
----
-Scenario: Mid-turn continuation compaction after tool output: compact request includes tool artifacts and follow-up request includes the summary.
-
-## Local Compaction Request
-00:message/developer:<PERMISSIONS_INSTRUCTIONS>
-01:message/user:<AGENTS_MD>
-02:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
-03:message/user:function call limit push
-04:function_call/test_tool
-05:function_call_output:unsupported call: test_tool
-06:message/assistant:FINAL_REPLY
-07:message/user:FOLLOW_UP_AFTER_LIMIT
-08:message/user:<SUMMARIZATION_PROMPT>
-
-## Local Post-Compaction History Layout
-00:message/user:function call limit push
-01:message/developer:<PERMISSIONS_INSTRUCTIONS>
-02:message/user:<AGENTS_MD>
-03:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
-04:message/user:FOLLOW_UP_AFTER_LIMIT
-05:message/user:<COMPACTION_SUMMARY>\nAUTO_SUMMARY
diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_turn_compaction_including_incoming_shapes.snap b/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_turn_compaction_including_incoming_shapes.snap
index e586c8521a08..553ee5b52025 100644
--- a/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_turn_compaction_including_incoming_shapes.snap
+++ b/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_turn_compaction_including_incoming_shapes.snap
@@ -13,13 +13,14 @@ Scenario: Pre-turn auto-compaction with a context override emits the context dif
 05:message/user:USER_TWO
 06:message/assistant:SECOND_REPLY
 07:message/user:<ENVIRONMENT_CONTEXT:cwd=PRETURN_CONTEXT_DIFF_CWD>
-08:message/user:<SUMMARIZATION_PROMPT>
+08:message/user:<image> | </image> | USER_THREE
+09:message/user:<SUMMARIZATION_PROMPT>
 
 ## Local Post-Compaction History Layout
-00:message/developer:<PERMISSIONS_INSTRUCTIONS>
-01:message/user:<AGENTS_MD>
-02:message/user:<ENVIRONMENT_CONTEXT:cwd=PRETURN_CONTEXT_DIFF_CWD>
-03:message/user:USER_ONE
-04:message/user:USER_TWO
-05:message/user:<COMPACTION_SUMMARY>\nPRE_TURN_SUMMARY
-06:message/user:<image> | <input_image:image_url> | </image> | USER_THREE
+00:message/user:USER_ONE
+01:message/user:USER_TWO
+02:message/developer:<PERMISSIONS_INSTRUCTIONS>
+03:message/user:<AGENTS_MD>
+04:message/user:<ENVIRONMENT_CONTEXT:cwd=PRETURN_CONTEXT_DIFF_CWD>
+05:message/user:<image> | </image> | USER_THREE
+06:message/user:<COMPACTION_SUMMARY>\nPRE_TURN_SUMMARY
diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_turn_compaction_including_incoming_shapes.snap.new b/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_turn_compaction_including_incoming_shapes.snap.new
deleted file mode 100644
index 0458d8807299..000000000000
--- a/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_turn_compaction_including_incoming_shapes.snap.new
+++ /dev/null
@@ -1,27 +0,0 @@
----
-source: core/tests/suite/compact.rs
-assertion_line: 3026
-expression: "format_labeled_requests_snapshot(\"Pre-turn auto-compaction with a context override emits the context diff in the compact request while the incoming user message is still excluded.\",\n&[(\"Local Compaction Request\", &requests[2]),\n(\"Local Post-Compaction History Layout\", &requests[3]),])"
----
-Scenario: Pre-turn auto-compaction with a context override emits the context diff in the compact request while the incoming user message is still excluded.
-
-## Local Compaction Request
-00:message/developer:<PERMISSIONS_INSTRUCTIONS>
-01:message/user:<AGENTS_MD>
-02:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
-03:message/user:USER_ONE
-04:message/assistant:FIRST_REPLY
-05:message/user:USER_TWO
-06:message/assistant:SECOND_REPLY
-07:message/user:<ENVIRONMENT_CONTEXT:cwd=PRETURN_CONTEXT_DIFF_CWD>
-08:message/user:<image> | </image> | USER_THREE
-09:message/user:<SUMMARIZATION_PROMPT>
-
-## Local Post-Compaction History Layout
-00:message/user:USER_ONE
-01:message/user:USER_TWO
-02:message/developer:<PERMISSIONS_INSTRUCTIONS>
-03:message/user:<AGENTS_MD>
-04:message/user:<ENVIRONMENT_CONTEXT:cwd=PRETURN_CONTEXT_DIFF_CWD>
-05:message/user:<image> | </image> | USER_THREE
-06:message/user:<COMPACTION_SUMMARY>\nPRE_TURN_SUMMARY
diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_manual_compact_with_history_shapes.snap b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_manual_compact_with_history_shapes.snap
index bc795a19f44c..f332d65d7973 100644
--- a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_manual_compact_with_history_shapes.snap
+++ b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_manual_compact_with_history_shapes.snap
@@ -13,9 +13,9 @@ Scenario: Remote manual /compact where remote compact output is summary-only: fo
 04:message/assistant:FIRST_REMOTE_REPLY
 
 ## Remote Post-Compaction History Layout
-00:message/developer:<PERMISSIONS_INSTRUCTIONS>
-01:message/user:<AGENTS_MD>
-02:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
-03:message/user:REMOTE_COMPACTED_SUMMARY
-04:compaction:encrypted=true
+00:message/user:REMOTE_COMPACTED_SUMMARY
+01:compaction:encrypted=true
+02:message/developer:<PERMISSIONS_INSTRUCTIONS>
+03:message/user:<AGENTS_MD>
+04:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
 05:message/user:after compact
diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_manual_compact_with_history_shapes.snap.new b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_manual_compact_with_history_shapes.snap.new
deleted file mode 100644
index f9e4ad23aa3d..000000000000
--- a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_manual_compact_with_history_shapes.snap.new
+++ /dev/null
@@ -1,21 +0,0 @@
----
-source: core/tests/suite/compact_remote.rs
-assertion_line: 174
-expression: "format_labeled_requests_snapshot(\"Remote manual /compact with prior user history compacts existing history and follow-up includes compact summary plus new user message.\",\n&[(\"Remote Compaction Request\", &compact_request),\n(\"Remote Post-Compaction History Layout\", follow_up_request),])"
----
-Scenario: Remote manual /compact with prior user history compacts existing history and follow-up includes compact summary plus new user message.
-
-## Remote Compaction Request
-00:message/developer:<PERMISSIONS_INSTRUCTIONS>
-01:message/user:<AGENTS_MD>
-02:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
-03:message/user:hello remote compact
-04:message/assistant:FIRST_REMOTE_REPLY
-
-## Remote Post-Compaction History Layout
-00:message/user:REMOTE_COMPACTED_SUMMARY
-01:compaction:encrypted=true
-02:message/developer:<PERMISSIONS_INSTRUCTIONS>
-03:message/user:<AGENTS_MD>
-04:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
-05:message/user:after compact
diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_mid_turn_compaction_shapes.snap b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_mid_turn_compaction_shapes.snap
index 8ef8701673bc..488bee0bef94 100644
--- a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_mid_turn_compaction_shapes.snap
+++ b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_mid_turn_compaction_shapes.snap
@@ -13,8 +13,8 @@ Scenario: Remote mid-turn continuation compaction after tool output: compact req
 05:function_call_output:unsupported call: test_tool
 
 ## Remote Post-Compaction History Layout
-00:message/user:USER_ONE
-01:message/developer:<PERMISSIONS_INSTRUCTIONS>
-02:message/user:<AGENTS_MD>
-03:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
+00:message/developer:<PERMISSIONS_INSTRUCTIONS>
+01:message/user:<AGENTS_MD>
+02:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
+03:message/user:USER_ONE
 04:message/user:<COMPACTION_SUMMARY>\nREMOTE_MID_TURN_SUMMARY
diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_mid_turn_compaction_shapes.snap.new b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_mid_turn_compaction_shapes.snap.new
deleted file mode 100644
index e7621c95404f..000000000000
--- a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_mid_turn_compaction_shapes.snap.new
+++ /dev/null
@@ -1,21 +0,0 @@
----
-source: core/tests/suite/compact_remote.rs
-assertion_line: 1578
-expression: "format_labeled_requests_snapshot(\"Remote mid-turn continuation compaction after tool output: compact request includes tool artifacts and follow-up request includes the summary.\",\n&[(\"Remote Compaction Request\", &compact_request),\n(\"Remote Post-Compaction History Layout\", &requests[1]),])"
----
-Scenario: Remote mid-turn continuation compaction after tool output: compact request includes tool artifacts and follow-up request includes the summary.
-
-## Remote Compaction Request
-00:message/developer:<PERMISSIONS_INSTRUCTIONS>
-01:message/user:<AGENTS_MD>
-02:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
-03:message/user:USER_ONE
-04:function_call/test_tool
-05:function_call_output:unsupported call: test_tool
-
-## Remote Post-Compaction History Layout
-00:message/developer:<PERMISSIONS_INSTRUCTIONS>
-01:message/user:<AGENTS_MD>
-02:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
-03:message/user:USER_ONE
-04:message/user:<COMPACTION_SUMMARY>\nREMOTE_MID_TURN_SUMMARY
diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_pre_turn_compaction_context_window_exceeded_shapes.snap b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_pre_turn_compaction_context_window_exceeded_shapes.snap
index e718bfda8861..2bac15dfda70 100644
--- a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_pre_turn_compaction_context_window_exceeded_shapes.snap
+++ b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_pre_turn_compaction_context_window_exceeded_shapes.snap
@@ -10,3 +10,4 @@ Scenario: Remote pre-turn auto-compaction context-window failure: compaction req
 02:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
 03:message/user:USER_ONE
 04:message/assistant:REMOTE_FIRST_REPLY
+05:message/user:USER_TWO
diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_pre_turn_compaction_context_window_exceeded_shapes.snap.new b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_pre_turn_compaction_context_window_exceeded_shapes.snap.new
deleted file mode 100644
index 01bbca3bba18..000000000000
--- a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_pre_turn_compaction_context_window_exceeded_shapes.snap.new
+++ /dev/null
@@ -1,14 +0,0 @@
----
-source: core/tests/suite/compact_remote.rs
-assertion_line: 1503
-expression: "format_labeled_requests_snapshot(\"Remote pre-turn auto-compaction context-window failure: compaction request excludes the incoming user message and the turn errors.\",\n&[(\"Remote Compaction Request (Incoming User Excluded)\",\n&include_attempt_request),])"
----
-Scenario: Remote pre-turn auto-compaction context-window failure: compaction request excludes the incoming user message and the turn errors.
-
-## Remote Compaction Request (Incoming User Excluded)
-00:message/developer:<PERMISSIONS_INSTRUCTIONS>
-01:message/user:<AGENTS_MD>
-02:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
-03:message/user:USER_ONE
-04:message/assistant:REMOTE_FIRST_REPLY
-05:message/user:USER_TWO
diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_pre_turn_compaction_including_incoming_shapes.snap b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_pre_turn_compaction_including_incoming_shapes.snap
index 00aaaeaa92cb..88fa34fa85e8 100644
--- a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_pre_turn_compaction_including_incoming_shapes.snap
+++ b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_pre_turn_compaction_including_incoming_shapes.snap
@@ -13,12 +13,12 @@ Scenario: Remote pre-turn auto-compaction with a context override emits the cont
 05:message/user:USER_TWO
 06:message/assistant:REMOTE_SECOND_REPLY
 07:message/user:<ENVIRONMENT_CONTEXT:cwd=PRETURN_CONTEXT_DIFF_CWD>
+08:message/user:USER_THREE
 
 ## Remote Post-Compaction History Layout
 00:message/user:USER_ONE
-01:message/user:USER_TWO
-02:message/developer:<PERMISSIONS_INSTRUCTIONS>
-03:message/user:<AGENTS_MD>
-04:message/user:<ENVIRONMENT_CONTEXT:cwd=PRETURN_CONTEXT_DIFF_CWD>
+01:message/developer:<PERMISSIONS_INSTRUCTIONS>
+02:message/user:<AGENTS_MD>
+03:message/user:<ENVIRONMENT_CONTEXT:cwd=PRETURN_CONTEXT_DIFF_CWD>
+04:message/user:USER_TWO
 05:message/user:<COMPACTION_SUMMARY>\nREMOTE_PRE_TURN_SUMMARY
-06:message/user:USER_THREE
diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_pre_turn_compaction_including_incoming_shapes.snap.new b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_pre_turn_compaction_including_incoming_shapes.snap.new
deleted file mode 100644
index a77063353042..000000000000
--- a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_pre_turn_compaction_including_incoming_shapes.snap.new
+++ /dev/null
@@ -1,25 +0,0 @@
----
-source: core/tests/suite/compact_remote.rs
-assertion_line: 1398
-expression: "format_labeled_requests_snapshot(\"Remote pre-turn auto-compaction with a context override emits the context diff in the compact request while excluding the incoming user message.\",\n&[(\"Remote Compaction Request\", &compact_request),\n(\"Remote Post-Compaction History Layout\", &requests[2]),])"
----
-Scenario: Remote pre-turn auto-compaction with a context override emits the context diff in the compact request while excluding the incoming user message.
-
-## Remote Compaction Request
-00:message/developer:<PERMISSIONS_INSTRUCTIONS>
-01:message/user:<AGENTS_MD>
-02:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
-03:message/user:USER_ONE
-04:message/assistant:REMOTE_FIRST_REPLY
-05:message/user:USER_TWO
-06:message/assistant:REMOTE_SECOND_REPLY
-07:message/user:<ENVIRONMENT_CONTEXT:cwd=PRETURN_CONTEXT_DIFF_CWD>
-08:message/user:USER_THREE
-
-## Remote Post-Compaction History Layout
-00:message/user:USER_ONE
-01:message/developer:<PERMISSIONS_INSTRUCTIONS>
-02:message/user:<AGENTS_MD>
-03:message/user:<ENVIRONMENT_CONTEXT:cwd=PRETURN_CONTEXT_DIFF_CWD>
-04:message/user:USER_TWO
-05:message/user:<COMPACTION_SUMMARY>\nREMOTE_PRE_TURN_SUMMARY

From 15730c2fcb4d6a0af3aa409a9dd68f38971e6d68 Mon Sep 17 00:00:00 2001
From: Charles Cunningham <ccunningham@openai.com>
Date: Thu, 12 Feb 2026 15:07:54 -0800
Subject: [PATCH 13/80] Align compaction tests with incoming-item and
 empty-history behavior

---
 .../app-server/tests/suite/v2/compaction.rs   |   5 -
 codex-rs/core/src/codex.rs                    |  10 +-
 codex-rs/core/tests/suite/compact.rs          | 138 ++--
 codex-rs/core/tests/suite/compact_remote.rs   |  31 +-
 .../core/tests/suite/compact_resume_fork.rs   | 616 ++----------------
 ...nual_compact_without_prev_user_shapes.snap |  13 +-
 ..._compaction_including_incoming_shapes.snap |   4 +-
 ...nual_compact_without_prev_user_shapes.snap |   9 +-
 ...te_pre_turn_compaction_failure_shapes.snap |   7 +-
 ..._compaction_including_incoming_shapes.snap |  15 +-
 10 files changed, 176 insertions(+), 672 deletions(-)

diff --git a/codex-rs/app-server/tests/suite/v2/compaction.rs b/codex-rs/app-server/tests/suite/v2/compaction.rs
index 5b5faa02d6de..09d75879f717 100644
--- a/codex-rs/app-server/tests/suite/v2/compaction.rs
+++ b/codex-rs/app-server/tests/suite/v2/compaction.rs
@@ -203,11 +203,6 @@ async fn thread_compact_start_triggers_compaction_and_returns_empty_response() -
     skip_if_no_network!(Ok(()));
 
     let server = responses::start_mock_server().await;
-    let sse = responses::sse(vec![
-        responses::ev_assistant_message("m1", "MANUAL_COMPACT_SUMMARY"),
-        responses::ev_completed_with_tokens("r1", 200),
-    ]);
-    responses::mount_sse_sequence(&server, vec![sse]).await;
 
     let codex_home = TempDir::new()?;
     write_mock_responses_config_toml(
diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs
index b30a1fa89ab1..11b1e9f98204 100644
--- a/codex-rs/core/src/codex.rs
+++ b/codex-rs/core/src/codex.rs
@@ -4311,7 +4311,15 @@ pub(crate) async fn run_turn(
                     );
                     EventMsg::Error(CodexErr::ContextWindowExceeded.to_error_event(Some(message)))
                 }
-                other => EventMsg::Error(other.to_error_event(None)),
+                other => {
+                    let compact_error_prefix =
+                        if should_use_remote_compact_task(&turn_context.provider) {
+                            "Error running remote compact task"
+                        } else {
+                            "Error running local compact task"
+                        };
+                    EventMsg::Error(other.to_error_event(Some(compact_error_prefix.to_string())))
+                }
             };
             sess.send_event(&turn_context, event).await;
             return None;
diff --git a/codex-rs/core/tests/suite/compact.rs b/codex-rs/core/tests/suite/compact.rs
index 5a6ad5b42657..4ef9fd15de35 100644
--- a/codex-rs/core/tests/suite/compact.rs
+++ b/codex-rs/core/tests/suite/compact.rs
@@ -510,6 +510,10 @@ async fn manual_compact_emits_api_and_local_token_usage_events() {
 
     let server = start_mock_server().await;
 
+    let sse_turn = sse(vec![
+        ev_assistant_message("m0", FIRST_REPLY),
+        ev_completed_with_tokens("r0", 0),
+    ]);
     // Compact run where the API reports zero tokens in usage. Our local
     // estimator should still compute a non-zero context size for the compacted
     // history.
@@ -517,7 +521,7 @@ async fn manual_compact_emits_api_and_local_token_usage_events() {
         ev_assistant_message("m1", SUMMARY_TEXT),
         ev_completed_with_tokens("r1", 0),
     ]);
-    mount_sse_once(&server, sse_compact).await;
+    mount_sse_sequence(&server, vec![sse_turn, sse_compact]).await;
 
     let model_provider = non_openai_model_provider(&server);
     let mut builder = test_codex().with_config(move |config| {
@@ -526,39 +530,42 @@ async fn manual_compact_emits_api_and_local_token_usage_events() {
     });
     let codex = builder.build(&server).await.unwrap().codex;
 
+    codex
+        .submit(Op::UserInput {
+            items: vec![UserInput::Text {
+                text: "seed compact history".into(),
+                text_elements: Vec::new(),
+            }],
+            final_output_json_schema: None,
+        })
+        .await
+        .unwrap();
+    wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
+
     // Trigger manual compact and collect TokenCount events for the compact turn.
     codex.submit(Op::Compact).await.unwrap();
 
-    // First TokenCount: from the compact API call (usage.total_tokens = 0).
-    let first = wait_for_event_match(&codex, |ev| match ev {
-        EventMsg::TokenCount(tc) => tc
-            .info
-            .as_ref()
-            .map(|info| info.last_token_usage.total_tokens),
-        _ => None,
-    })
-    .await;
-
-    // Second TokenCount: from the local post-compaction estimate.
-    let last = wait_for_event_match(&codex, |ev| match ev {
-        EventMsg::TokenCount(tc) => tc
-            .info
-            .as_ref()
-            .map(|info| info.last_token_usage.total_tokens),
-        _ => None,
-    })
-    .await;
-
-    // Ensure the compact task itself completes.
-    wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
+    let mut compact_turn_token_totals = Vec::new();
+    loop {
+        let event = wait_for_event(&codex, |_| true).await;
+        match event {
+            EventMsg::TokenCount(tc) => {
+                if let Some(info) = tc.info {
+                    compact_turn_token_totals.push(info.last_token_usage.total_tokens);
+                }
+            }
+            EventMsg::TurnComplete(_) => break,
+            _ => {}
+        }
+    }
 
-    assert_eq!(
-        first, 0,
-        "expected first TokenCount from compact API usage to be zero"
+    assert!(
+        compact_turn_token_totals.contains(&0),
+        "expected compact turn token events to include API-reported zero usage"
     );
     assert!(
-        last > 0,
-        "second TokenCount should reflect a non-zero estimated context size after compaction"
+        compact_turn_token_totals.iter().any(|total| *total > 0),
+        "expected compact turn token events to include a non-zero local estimate"
     );
 }
 
@@ -2414,35 +2421,33 @@ async fn manual_compact_twice_preserves_latest_user_messages() {
         "compact requests should consistently include or omit the summarization prompt"
     );
 
-    let first_request_user_texts = requests[0].message_input_texts("user");
-    let first_turn_user_index = first_request_user_texts
-        .len()
-        .checked_sub(1)
-        .unwrap_or_else(|| panic!("first turn request missing user messages"));
-    assert_eq!(
-        first_request_user_texts[first_turn_user_index], first_user_message,
-        "first turn request should end with the submitted user message"
-    );
-    let seeded_user_prefix = &first_request_user_texts[..first_turn_user_index];
-
     let final_request_user_texts = requests
         .last()
         .unwrap_or_else(|| panic!("final turn request missing for {final_user_message}"))
         .message_input_texts("user");
-    assert!(
-        final_request_user_texts
-            .as_slice()
-            .starts_with(seeded_user_prefix),
-        "final request should start with seeded user prefix from first request: {seeded_user_prefix:?}"
-    );
-    let final_output = &final_request_user_texts[seeded_user_prefix.len()..];
+    let Some(first_user_index) = final_request_user_texts
+        .iter()
+        .position(|text| text == first_user_message)
+    else {
+        panic!("final request missing first user message: {final_request_user_texts:?}");
+    };
+    let final_output = &final_request_user_texts[first_user_index..];
     let expected = vec![
         first_user_message.to_string(),
         second_user_message.to_string(),
         expected_second_summary,
         final_user_message.to_string(),
     ];
-    assert_eq!(final_output, expected.as_slice());
+    let mut final_output_iter = final_output.iter();
+    for expected_text in &expected {
+        final_output_iter
+            .position(|text| text == expected_text)
+            .unwrap_or_else(|| {
+                panic!(
+                    "final request should preserve expected user-message order; missing `{expected_text}` in {final_output:?}"
+                )
+            });
+    }
 }
 
 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
@@ -2713,8 +2718,8 @@ async fn auto_compact_clamps_config_limit_to_context_window() {
 
     let auto_compact_body = auto_compact_mock.single_request().body_json().to_string();
     assert!(
-        body_contains_text(&auto_compact_body, SUMMARIZATION_PROMPT),
-        "auto compact should run with the summarization prompt when config limit exceeds context"
+        body_contains_text(&auto_compact_body, "OVER_LIMIT_TURN"),
+        "auto compact should run when the configured limit clamps to the model context window"
     );
 }
 
@@ -2928,7 +2933,6 @@ async fn auto_compact_runs_when_reasoning_header_clears_between_turns() {
 }
 
 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
-// TODO(ccunningham): Update once pre-turn compaction includes incoming user input.
 async fn snapshot_request_shape_pre_turn_compaction_including_incoming_user_message() {
     skip_if_no_network!();
 
@@ -3016,7 +3020,7 @@ async fn snapshot_request_shape_pre_turn_compaction_including_incoming_user_mess
     insta::assert_snapshot!(
         "pre_turn_compaction_including_incoming_shapes",
         format_labeled_requests_snapshot(
-            "Pre-turn auto-compaction with a context override emits the context diff in the compact request while the incoming user message is still excluded.",
+            "Pre-turn auto-compaction with a context override includes incoming user content in the compact request and preserves it after compaction.",
             &[
                 ("Local Compaction Request", &requests[2]),
                 ("Local Post-Compaction History Layout", &requests[3]),
@@ -3025,10 +3029,17 @@ async fn snapshot_request_shape_pre_turn_compaction_including_incoming_user_mess
     );
     let compact_request_user_texts = requests[2].message_input_texts("user");
     assert!(
-        !compact_request_user_texts
+        compact_request_user_texts
             .iter()
             .any(|text| text == "USER_THREE"),
-        "current behavior excludes incoming user message from pre-turn compaction input"
+        "pre-turn compaction request should include the incoming user message"
+    );
+    let compact_request_user_images = requests[2].message_input_image_urls("user");
+    assert!(
+        compact_request_user_images
+            .iter()
+            .any(|url| url == image_url.as_str()),
+        "pre-turn compaction request should include incoming user image content"
     );
     let follow_up_user_texts = requests[3].message_input_texts("user");
     assert!(
@@ -3478,15 +3489,11 @@ async fn snapshot_request_shape_manual_compact_without_previous_user_messages()
 
     let server = start_mock_server().await;
 
-    let compact_turn = sse(vec![
-        ev_assistant_message("m1", "MANUAL_EMPTY_SUMMARY"),
-        ev_completed_with_tokens("r1", 90),
-    ]);
     let follow_up_turn = sse(vec![
-        ev_assistant_message("m2", FINAL_REPLY),
-        ev_completed_with_tokens("r2", 80),
+        ev_assistant_message("m1", FINAL_REPLY),
+        ev_completed_with_tokens("r1", 80),
     ]);
-    let request_log = mount_sse_sequence(&server, vec![compact_turn, follow_up_turn]).await;
+    let request_log = mount_sse_once(&server, follow_up_turn).await;
 
     let model_provider = non_openai_model_provider(&server);
     let codex = test_codex()
@@ -3517,18 +3524,15 @@ async fn snapshot_request_shape_manual_compact_without_previous_user_messages()
     let requests = request_log.requests();
     assert_eq!(
         requests.len(),
-        2,
-        "expected manual /compact request and follow-up turn request"
+        1,
+        "manual /compact with no prior user should be a no-op; only the follow-up turn should hit /responses"
     );
 
     insta::assert_snapshot!(
         "manual_compact_without_prev_user_shapes",
         format_labeled_requests_snapshot(
-            "Manual /compact with no prior user turn currently still issues a compaction request; follow-up turn carries canonical context and the new user message.",
-            &[
-                ("Local Compaction Request", &requests[0]),
-                ("Local Post-Compaction History Layout", &requests[1]),
-            ]
+            "Manual /compact with no prior user turn is a no-op; follow-up turn carries canonical context and the new user message.",
+            &[("Local Post-Compaction History Layout", &requests[0]),]
         )
     );
 }
diff --git a/codex-rs/core/tests/suite/compact_remote.rs b/codex-rs/core/tests/suite/compact_remote.rs
index c9acde41ab0c..e125b29d7a9f 100644
--- a/codex-rs/core/tests/suite/compact_remote.rs
+++ b/codex-rs/core/tests/suite/compact_remote.rs
@@ -579,9 +579,9 @@ async fn auto_remote_compact_failure_stops_agent_loop() -> Result<()> {
     insta::assert_snapshot!(
         "remote_pre_turn_compaction_failure_shapes",
         format_labeled_requests_snapshot(
-            "Remote pre-turn auto-compaction parse failure: compaction request excludes the incoming user message and the turn stops.",
+            "Remote pre-turn auto-compaction parse failure: compaction request includes incoming user content and the turn stops.",
             &[(
-                "Remote Compaction Request (Incoming User Excluded)",
+                "Remote Compaction Request (Incoming User Included)",
                 &first_compact_mock.single_request()
             ),]
         )
@@ -1310,7 +1310,6 @@ async fn remote_compact_refreshes_stale_developer_instructions_without_resume()
 }
 
 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
-// TODO(ccunningham): Update once remote pre-turn compaction includes incoming user input.
 async fn snapshot_request_shape_remote_pre_turn_compaction_including_incoming_user_message()
 -> Result<()> {
     skip_if_no_network!(Ok(()));
@@ -1390,13 +1389,20 @@ async fn snapshot_request_shape_remote_pre_turn_compaction_including_incoming_us
     insta::assert_snapshot!(
         "remote_pre_turn_compaction_including_incoming_shapes",
         format_labeled_requests_snapshot(
-            "Remote pre-turn auto-compaction with a context override emits the context diff in the compact request while excluding the incoming user message.",
+            "Remote pre-turn auto-compaction with a context override includes incoming user content in the compact request and preserves it after compaction.",
             &[
                 ("Remote Compaction Request", &compact_request),
                 ("Remote Post-Compaction History Layout", &requests[2]),
             ]
         )
     );
+    assert!(
+        compact_request
+            .message_input_texts("user")
+            .iter()
+            .any(|text| text == "USER_THREE"),
+        "remote pre-turn compaction request should include incoming user message"
+    );
     assert_eq!(
         requests[2]
             .message_input_texts("user")
@@ -1928,10 +1934,6 @@ async fn snapshot_request_shape_remote_manual_compact_without_previous_user_mess
     )
     .await;
 
-    let compact_mock =
-        responses::mount_compact_json_once(harness.server(), serde_json::json!({ "output": [] }))
-            .await;
-
     codex.submit(Op::Compact).await?;
     wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
 
@@ -1946,21 +1948,12 @@ async fn snapshot_request_shape_remote_manual_compact_without_previous_user_mess
         .await?;
     wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
 
-    assert_eq!(
-        compact_mock.requests().len(),
-        1,
-        "current behavior still issues remote compaction for manual /compact without prior user"
-    );
-    let compact_request = compact_mock.single_request();
     let follow_up_request = responses_mock.single_request();
     insta::assert_snapshot!(
         "remote_manual_compact_without_prev_user_shapes",
         format_labeled_requests_snapshot(
-            "Remote manual /compact with no prior user turn still issues a compact request; follow-up turn carries canonical context and new user message.",
-            &[
-                ("Remote Compaction Request", &compact_request),
-                ("Remote Post-Compaction History Layout", &follow_up_request),
-            ]
+            "Remote manual /compact with no prior user turn is a no-op; follow-up turn carries canonical context and the new user message.",
+            &[("Remote Post-Compaction History Layout", &follow_up_request),]
         )
     );
 
diff --git a/codex-rs/core/tests/suite/compact_resume_fork.rs b/codex-rs/core/tests/suite/compact_resume_fork.rs
index fc77a2621fc4..9c9618a97835 100644
--- a/codex-rs/core/tests/suite/compact_resume_fork.rs
+++ b/codex-rs/core/tests/suite/compact_resume_fork.rs
@@ -200,24 +200,11 @@ async fn compact_resume_and_fork_preserve_model_history_view() {
         &fork_arr[..compact_arr.len()]
     );
 
-    let expected_model = requests[0]["model"]
-        .as_str()
-        .unwrap_or_default()
-        .to_string();
-    let prompt = requests[0]["instructions"]
-        .as_str()
-        .unwrap_or_default()
-        .to_string();
-    let permissions_message = requests[0]["input"][0].clone();
-    let user_instructions = requests[0]["input"][1]["content"][0]["text"]
-        .as_str()
-        .unwrap_or_default()
-        .to_string();
-    let environment_context = requests[0]["input"][2]["content"][0]["text"]
-        .as_str()
-        .unwrap_or_default()
-        .to_string();
-    let tool_calls = json!(requests[0]["tools"].as_array());
+    assert_eq!(requests.len(), 5);
+    let expected_model = requests[0]["model"].as_str();
+    for request in &requests {
+        assert_eq!(request["model"].as_str(), expected_model);
+    }
     let prompt_cache_key = requests[0]["prompt_cache_key"]
         .as_str()
         .unwrap_or_default()
@@ -226,433 +213,46 @@ async fn compact_resume_and_fork_preserve_model_history_view() {
         .as_str()
         .unwrap_or_default()
         .to_string();
+    assert_ne!(
+        prompt_cache_key, fork_prompt_cache_key,
+        "forked request should use a new prompt cache key"
+    );
     let summary_after_compact = extract_summary_message(&requests[2], SUMMARY_TEXT);
     let summary_after_resume = extract_summary_message(&requests[3], SUMMARY_TEXT);
     let summary_after_fork = extract_summary_message(&requests[4], SUMMARY_TEXT);
-    let user_turn_1 = json!(
-    {
-      "model": expected_model,
-      "instructions": prompt,
-      "input": [
-        permissions_message,
-        {
-          "type": "message",
-          "role": "user",
-          "content": [
-            {
-              "type": "input_text",
-              "text": user_instructions
-            }
-          ]
-        },
-        {
-          "type": "message",
-          "role": "user",
-          "content": [
-            {
-              "type": "input_text",
-              "text": environment_context
-            }
-          ]
-        },
-        {
-          "type": "message",
-          "role": "user",
-          "content": [
-            {
-              "type": "input_text",
-              "text": "hello world"
-            }
-          ]
-        }
-      ],
-      "tools": tool_calls,
-      "tool_choice": "auto",
-      "parallel_tool_calls": false,
-      "reasoning": {
-        "effort": "medium",
-        "summary": "auto"
-      },
-      "store": false,
-      "stream": true,
-      "include": [
-        "reasoning.encrypted_content"
-      ],
-      "prompt_cache_key": prompt_cache_key
-    });
-    let compact_1 = json!(
-    {
-      "model": expected_model,
-      "instructions": prompt,
-      "input": [
-        permissions_message,
-        {
-          "type": "message",
-          "role": "user",
-          "content": [
-            {
-              "type": "input_text",
-              "text": user_instructions
-            }
-          ]
-        },
-        {
-          "type": "message",
-          "role": "user",
-          "content": [
-            {
-              "type": "input_text",
-              "text": environment_context
-            }
-          ]
-        },
-        {
-          "type": "message",
-          "role": "user",
-          "content": [
-            {
-              "type": "input_text",
-              "text": "hello world"
-            }
-          ]
-        },
-        {
-          "type": "message",
-          "role": "assistant",
-          "content": [
-            {
-              "type": "output_text",
-              "text": "FIRST_REPLY"
-            }
-          ]
-        },
-        {
-          "type": "message",
-          "role": "user",
-          "content": [
-            {
-              "type": "input_text",
-              "text": SUMMARIZATION_PROMPT
-            }
-          ]
-        }
-      ],
-      "tools": [],
-      "tool_choice": "auto",
-      "parallel_tool_calls": false,
-      "reasoning": {
-        "effort": "medium",
-        "summary": "auto"
-      },
-      "store": false,
-      "stream": true,
-      "include": [
-        "reasoning.encrypted_content"
-      ],
-      "prompt_cache_key": prompt_cache_key
-    });
-    let user_turn_2_after_compact = json!(
-    {
-      "model": expected_model,
-      "instructions": prompt,
-      "input": [
-        permissions_message,
-        {
-          "type": "message",
-          "role": "user",
-          "content": [
-            {
-              "type": "input_text",
-              "text": user_instructions
-            }
-          ]
-        },
-        {
-          "type": "message",
-          "role": "user",
-          "content": [
-            {
-              "type": "input_text",
-              "text": environment_context
-            }
-          ]
-        },
-        {
-          "type": "message",
-          "role": "user",
-          "content": [
-            {
-              "type": "input_text",
-              "text": "hello world"
-            }
-          ]
-        },
-        summary_after_compact,
-        {
-          "type": "message",
-          "role": "user",
-          "content": [
-            {
-              "type": "input_text",
-              "text": "AFTER_COMPACT"
-            }
-          ]
-        }
-      ],
-      "tools": tool_calls,
-      "tool_choice": "auto",
-      "parallel_tool_calls": false,
-      "reasoning": {
-        "effort": "medium",
-        "summary": "auto"
-      },
-      "store": false,
-      "stream": true,
-      "include": [
-        "reasoning.encrypted_content"
-      ],
-      "prompt_cache_key": prompt_cache_key
-    });
-    let usert_turn_3_after_resume = json!(
-    {
-      "model": expected_model,
-      "instructions": prompt,
-      "input": [
-        permissions_message,
-        {
-          "type": "message",
-          "role": "user",
-          "content": [
-            {
-              "type": "input_text",
-              "text": user_instructions
-            }
-          ]
-        },
-        {
-          "type": "message",
-          "role": "user",
-          "content": [
-            {
-              "type": "input_text",
-              "text": environment_context
-            }
-          ]
-        },
-        {
-          "type": "message",
-          "role": "user",
-          "content": [
-            {
-              "type": "input_text",
-              "text": "hello world"
-            }
-          ]
-        },
-        summary_after_resume,
-        {
-          "type": "message",
-          "role": "user",
-          "content": [
-            {
-              "type": "input_text",
-              "text": "AFTER_COMPACT"
-            }
-          ]
-        },
-        {
-          "type": "message",
-          "role": "assistant",
-          "content": [
-            {
-              "type": "output_text",
-              "text": "AFTER_COMPACT_REPLY"
-            }
-          ]
-        },
-        permissions_message,
-        {
-          "type": "message",
-          "role": "user",
-          "content": [
-            {
-              "type": "input_text",
-              "text": user_instructions
-            }
-          ]
-        },
-        {
-          "type": "message",
-          "role": "user",
-          "content": [
-            {
-              "type": "input_text",
-              "text": environment_context
-            }
-          ]
-        },
-        {
-          "type": "message",
-          "role": "user",
-          "content": [
-            {
-              "type": "input_text",
-              "text": "AFTER_RESUME"
-            }
-          ]
-        }
-      ],
-      "tools": tool_calls,
-      "tool_choice": "auto",
-      "parallel_tool_calls": false,
-      "reasoning": {
-        "effort": "medium",
-        "summary": "auto"
-      },
-      "store": false,
-      "stream": true,
-      "include": [
-        "reasoning.encrypted_content"
-      ],
-      "prompt_cache_key": prompt_cache_key
-    });
-    let user_turn_3_after_fork = json!(
-    {
-      "model": expected_model,
-      "instructions": prompt,
-      "input": [
-        permissions_message,
-        {
-          "type": "message",
-          "role": "user",
-          "content": [
-            {
-              "type": "input_text",
-              "text": user_instructions
-            }
-          ]
-        },
-        {
-          "type": "message",
-          "role": "user",
-          "content": [
-            {
-              "type": "input_text",
-              "text": environment_context
-            }
-          ]
-        },
-        {
-          "type": "message",
-          "role": "user",
-          "content": [
-            {
-              "type": "input_text",
-              "text": "hello world"
-            }
-          ]
-        },
-        summary_after_fork,
-        {
-          "type": "message",
-          "role": "user",
-          "content": [
-            {
-              "type": "input_text",
-              "text": "AFTER_COMPACT"
-            }
-          ]
-        },
-        {
-          "type": "message",
-          "role": "assistant",
-          "content": [
-            {
-              "type": "output_text",
-              "text": "AFTER_COMPACT_REPLY"
-            }
-          ]
-        },
-        permissions_message,
-        {
-          "type": "message",
-          "role": "user",
-          "content": [
-            {
-              "type": "input_text",
-              "text": user_instructions
-            }
-          ]
-        },
-        {
-          "type": "message",
-          "role": "user",
-          "content": [
-            {
-              "type": "input_text",
-              "text": environment_context
-            }
-          ]
-        },
-        permissions_message,
-        {
-          "type": "message",
-          "role": "user",
-          "content": [
-            {
-              "type": "input_text",
-              "text": user_instructions
-            }
-          ]
-        },
-        {
-          "type": "message",
-          "role": "user",
-          "content": [
-            {
-              "type": "input_text",
-              "text": environment_context
-            }
-          ]
-        },
-        {
-          "type": "message",
-          "role": "user",
-          "content": [
-            {
-              "type": "input_text",
-              "text": "AFTER_FORK"
-            }
-          ]
-        }
-      ],
-      "tools": tool_calls,
-      "tool_choice": "auto",
-      "parallel_tool_calls": false,
-      "reasoning": {
-        "effort": "medium",
-        "summary": "auto"
-      },
-      "store": false,
-      "stream": true,
-      "include": [
-        "reasoning.encrypted_content"
-      ],
-      "prompt_cache_key": fork_prompt_cache_key
-    });
-    let mut expected = json!([
-        user_turn_1,
-        compact_1,
-        user_turn_2_after_compact,
-        usert_turn_3_after_resume,
-        user_turn_3_after_fork
-    ]);
-    normalize_line_endings(&mut expected);
-    if let Some(arr) = expected.as_array_mut() {
-        normalize_compact_prompts(arr);
+    for summary in [
+        &summary_after_compact,
+        &summary_after_resume,
+        &summary_after_fork,
+    ] {
+        assert_eq!(summary["role"].as_str(), Some("user"));
+        assert!(
+            summary["content"][0]["text"]
+                .as_str()
+                .unwrap_or_default()
+                .contains(SUMMARY_TEXT),
+            "summary message should include compacted summary text"
+        );
     }
-    assert_eq!(requests.len(), 5);
-    assert_eq!(json!(requests), expected);
+    let request_2_body = requests[2].to_string();
+    assert!(
+        request_2_body.contains("\"text\":\"AFTER_COMPACT\""),
+        "post-compact request should include AFTER_COMPACT"
+    );
+    let request_3_body = requests[3].to_string();
+    assert!(
+        request_3_body.contains("\"text\":\"AFTER_RESUME\""),
+        "post-resume request should include AFTER_RESUME"
+    );
+    let request_4_body = requests[4].to_string();
+    assert!(
+        request_4_body.contains("\"text\":\"AFTER_FORK\""),
+        "post-fork request should include AFTER_FORK"
+    );
+    assert!(
+        !request_4_body.contains("\"text\":\"AFTER_RESUME\""),
+        "forked request should not include resumed-branch user input"
+    );
 }
 
 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
@@ -725,118 +325,32 @@ async fn compact_resume_after_second_compaction_preserves_history() {
         compact_filtered.as_slice(),
         &resume_filtered[..compact_filtered.len()]
     );
-    // hard coded test
-    let prompt = requests[0]["instructions"]
-        .as_str()
-        .unwrap_or_default()
-        .to_string();
-    let permissions_message = requests[0]["input"][0].clone();
-    let user_instructions = requests[0]["input"][1]["content"][0]["text"]
-        .as_str()
-        .unwrap_or_default()
-        .to_string();
-    let environment_instructions = requests[0]["input"][2]["content"][0]["text"]
-        .as_str()
-        .unwrap_or_default()
-        .to_string();
-
-    // Build expected final request input: initial context + forked user message +
-    // compacted summary + post-compact user message + resumed user message.
+    // Final resumed request should include the fork branch history, the second compaction
+    // summary, and the resumed-again user message.
     let summary_after_second_compact =
         extract_summary_message(&requests[requests.len() - 3], SUMMARY_TEXT);
-
-    let mut expected = json!([
-      {
-        "instructions": prompt,
-        "input": [
-          permissions_message,
-          {
-            "type": "message",
-            "role": "user",
-            "content": [
-              {
-                "type": "input_text",
-                "text": user_instructions
-              }
-            ]
-          },
-          {
-            "type": "message",
-            "role": "user",
-            "content": [
-              {
-                "type": "input_text",
-                "text": environment_instructions
-              }
-            ]
-          },
-          {
-            "type": "message",
-            "role": "user",
-            "content": [
-              {
-                "type": "input_text",
-                "text": "AFTER_FORK"
-              }
-            ]
-          },
-          summary_after_second_compact,
-          {
-            "type": "message",
-            "role": "user",
-            "content": [
-              {
-                "type": "input_text",
-                "text": "AFTER_COMPACT_2"
-              }
-            ]
-          },
-          permissions_message,
-          {
-            "type": "message",
-            "role": "user",
-            "content": [
-              {
-                "type": "input_text",
-                "text": user_instructions
-              }
-            ]
-          },
-          {
-            "type": "message",
-            "role": "user",
-            "content": [
-              {
-                "type": "input_text",
-                "text": environment_instructions
-              }
-            ]
-          },
-          {
-            "type": "message",
-            "role": "user",
-            "content": [
-              {
-                "type": "input_text",
-                "text": "AFTER_SECOND_RESUME"
-              }
-            ]
-          }
-        ],
-      }
-    ]);
-    normalize_line_endings(&mut expected);
-    let mut last_request_after_2_compacts = json!([{
-        "instructions": requests[requests.len() -1]["instructions"],
-        "input": requests[requests.len() -1]["input"],
-    }]);
-    if let Some(arr) = expected.as_array_mut() {
-        normalize_compact_prompts(arr);
-    }
-    if let Some(arr) = last_request_after_2_compacts.as_array_mut() {
-        normalize_compact_prompts(arr);
-    }
-    assert_eq!(expected, last_request_after_2_compacts);
+    assert_eq!(summary_after_second_compact["role"].as_str(), Some("user"));
+    assert!(
+        summary_after_second_compact["content"][0]["text"]
+            .as_str()
+            .unwrap_or_default()
+            .contains(SUMMARY_TEXT),
+        "second compaction summary should include compacted summary text"
+    );
+    let last_request_after_two_compacts = &requests[requests.len() - 1];
+    let last_request_body = last_request_after_two_compacts.to_string();
+    assert!(
+        last_request_body.contains("\"text\":\"AFTER_FORK\""),
+        "last request should retain fork-branch user message"
+    );
+    assert!(
+        last_request_body.contains("\"text\":\"AFTER_COMPACT_2\""),
+        "last request should include post-second-compaction user message"
+    );
+    assert!(
+        last_request_body.contains(&format!("\"text\":\"{AFTER_SECOND_RESUME}\"")),
+        "last request should include resumed-again user message"
+    );
 }
 
 fn normalize_line_endings(value: &mut Value) {
diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact__manual_compact_without_prev_user_shapes.snap b/codex-rs/core/tests/suite/snapshots/all__suite__compact__manual_compact_without_prev_user_shapes.snap
index bdb4fe9baefe..b0898ee0280b 100644
--- a/codex-rs/core/tests/suite/snapshots/all__suite__compact__manual_compact_without_prev_user_shapes.snap
+++ b/codex-rs/core/tests/suite/snapshots/all__suite__compact__manual_compact_without_prev_user_shapes.snap
@@ -1,18 +1,11 @@
 ---
 source: core/tests/suite/compact.rs
-expression: "format_labeled_requests_snapshot(\"Manual /compact with no prior user turn currently still issues a compaction request; follow-up turn carries canonical context and the new user message.\",\n&[(\"Local Compaction Request\", &requests[0]),\n(\"Local Post-Compaction History Layout\", &requests[1]),])"
+expression: "format_labeled_requests_snapshot(\"Manual /compact with no prior user turn is a no-op; follow-up turn carries canonical context and the new user message.\",\n&[(\"Local Post-Compaction History Layout\", &requests[0]),])"
 ---
-Scenario: Manual /compact with no prior user turn currently still issues a compaction request; follow-up turn carries canonical context and the new user message.
-
-## Local Compaction Request
-00:message/developer:<PERMISSIONS_INSTRUCTIONS>
-01:message/user:<AGENTS_MD>
-02:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
-03:message/user:<SUMMARIZATION_PROMPT>
+Scenario: Manual /compact with no prior user turn is a no-op; follow-up turn carries canonical context and the new user message.
 
 ## Local Post-Compaction History Layout
 00:message/developer:<PERMISSIONS_INSTRUCTIONS>
 01:message/user:<AGENTS_MD>
 02:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
-03:message/user:<COMPACTION_SUMMARY>\nMANUAL_EMPTY_SUMMARY
-04:message/user:AFTER_MANUAL_EMPTY_COMPACT
+03:message/user:AFTER_MANUAL_EMPTY_COMPACT
diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_turn_compaction_including_incoming_shapes.snap b/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_turn_compaction_including_incoming_shapes.snap
index 553ee5b52025..57d461c02606 100644
--- a/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_turn_compaction_including_incoming_shapes.snap
+++ b/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_turn_compaction_including_incoming_shapes.snap
@@ -1,8 +1,8 @@
 ---
 source: core/tests/suite/compact.rs
-expression: "format_labeled_requests_snapshot(\"Pre-turn auto-compaction with a context override emits the context diff in the compact request while the incoming user message is still excluded.\",\n&[(\"Local Compaction Request\", &requests[2]),\n(\"Local Post-Compaction History Layout\", &requests[3]),])"
+expression: "format_labeled_requests_snapshot(\"Pre-turn auto-compaction with a context override includes incoming user content in the compact request and preserves it after compaction.\",\n&[(\"Local Compaction Request\", &requests[2]),\n(\"Local Post-Compaction History Layout\", &requests[3]),])"
 ---
-Scenario: Pre-turn auto-compaction with a context override emits the context diff in the compact request while the incoming user message is still excluded.
+Scenario: Pre-turn auto-compaction with a context override includes incoming user content in the compact request and preserves it after compaction.
 
 ## Local Compaction Request
 00:message/developer:<PERMISSIONS_INSTRUCTIONS>
diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_manual_compact_without_prev_user_shapes.snap b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_manual_compact_without_prev_user_shapes.snap
index ce62806d99c5..46eb3b76c5a8 100644
--- a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_manual_compact_without_prev_user_shapes.snap
+++ b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_manual_compact_without_prev_user_shapes.snap
@@ -1,13 +1,8 @@
 ---
 source: core/tests/suite/compact_remote.rs
-expression: "format_labeled_requests_snapshot(\"Remote manual /compact with no prior user turn still issues a compact request; follow-up turn carries canonical context and new user message.\",\n&[(\"Remote Compaction Request\", &compact_request),\n(\"Remote Post-Compaction History Layout\", &follow_up_request),])"
+expression: "format_labeled_requests_snapshot(\"Remote manual /compact with no prior user turn is a no-op; follow-up turn carries canonical context and the new user message.\",\n&[(\"Remote Post-Compaction History Layout\", &follow_up_request),])"
 ---
-Scenario: Remote manual /compact with no prior user turn still issues a compact request; follow-up turn carries canonical context and new user message.
-
-## Remote Compaction Request
-00:message/developer:<PERMISSIONS_INSTRUCTIONS>
-01:message/user:<AGENTS_MD>
-02:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
+Scenario: Remote manual /compact with no prior user turn is a no-op; follow-up turn carries canonical context and the new user message.
 
 ## Remote Post-Compaction History Layout
 00:message/developer:<PERMISSIONS_INSTRUCTIONS>
diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_pre_turn_compaction_failure_shapes.snap b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_pre_turn_compaction_failure_shapes.snap
index f8aa74e9a1fc..864c06b4b8a2 100644
--- a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_pre_turn_compaction_failure_shapes.snap
+++ b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_pre_turn_compaction_failure_shapes.snap
@@ -1,12 +1,13 @@
 ---
 source: core/tests/suite/compact_remote.rs
-expression: "format_labeled_requests_snapshot(\"Remote pre-turn auto-compaction parse failure: compaction request excludes the incoming user message and the turn stops.\",\n&[(\"Remote Compaction Request (Incoming User Excluded)\",\n&first_compact_mock.single_request()),])"
+expression: "format_labeled_requests_snapshot(\"Remote pre-turn auto-compaction parse failure: compaction request includes incoming user content and the turn stops.\",\n&[(\"Remote Compaction Request (Incoming User Included)\",\n&first_compact_mock.single_request()),])"
 ---
-Scenario: Remote pre-turn auto-compaction parse failure: compaction request excludes the incoming user message and the turn stops.
+Scenario: Remote pre-turn auto-compaction parse failure: compaction request includes incoming user content and the turn stops.
 
-## Remote Compaction Request (Incoming User Excluded)
+## Remote Compaction Request (Incoming User Included)
 00:message/developer:<PERMISSIONS_INSTRUCTIONS>
 01:message/user:<AGENTS_MD>
 02:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
 03:message/user:turn that exceeds token threshold
 04:message/assistant:initial turn complete
+05:message/user:turn that triggers auto compact
diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_pre_turn_compaction_including_incoming_shapes.snap b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_pre_turn_compaction_including_incoming_shapes.snap
index 88fa34fa85e8..ec30aa08614b 100644
--- a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_pre_turn_compaction_including_incoming_shapes.snap
+++ b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_pre_turn_compaction_including_incoming_shapes.snap
@@ -1,8 +1,8 @@
 ---
 source: core/tests/suite/compact_remote.rs
-expression: "format_labeled_requests_snapshot(\"Remote pre-turn auto-compaction with a context override emits the context diff in the compact request while excluding the incoming user message.\",\n&[(\"Remote Compaction Request\", &compact_request),\n(\"Remote Post-Compaction History Layout\", &requests[2]),])"
+expression: "format_labeled_requests_snapshot(\"Remote pre-turn auto-compaction with a context override includes incoming user content in the compact request and preserves it after compaction.\",\n&[(\"Remote Compaction Request\", &compact_request),\n(\"Remote Post-Compaction History Layout\", &requests[2]),])"
 ---
-Scenario: Remote pre-turn auto-compaction with a context override emits the context diff in the compact request while excluding the incoming user message.
+Scenario: Remote pre-turn auto-compaction with a context override includes incoming user content in the compact request and preserves it after compaction.
 
 ## Remote Compaction Request
 00:message/developer:<PERMISSIONS_INSTRUCTIONS>
@@ -17,8 +17,9 @@ Scenario: Remote pre-turn auto-compaction with a context override emits the cont
 
 ## Remote Post-Compaction History Layout
 00:message/user:USER_ONE
-01:message/developer:<PERMISSIONS_INSTRUCTIONS>
-02:message/user:<AGENTS_MD>
-03:message/user:<ENVIRONMENT_CONTEXT:cwd=PRETURN_CONTEXT_DIFF_CWD>
-04:message/user:USER_TWO
-05:message/user:<COMPACTION_SUMMARY>\nREMOTE_PRE_TURN_SUMMARY
+01:message/user:USER_TWO
+02:message/developer:<PERMISSIONS_INSTRUCTIONS>
+03:message/user:<AGENTS_MD>
+04:message/user:<ENVIRONMENT_CONTEXT:cwd=PRETURN_CONTEXT_DIFF_CWD>
+05:message/user:USER_THREE
+06:message/user:<COMPACTION_SUMMARY>\nREMOTE_PRE_TURN_SUMMARY

From c92190f130328dd8c43883e099ca56a252ca630b Mon Sep 17 00:00:00 2001
From: Charles Cunningham <ccunningham@openai.com>
Date: Thu, 12 Feb 2026 15:10:11 -0800
Subject: [PATCH 14/80] Avoid wildcard pattern in ResponseEvent::Completed
 match

---
 codex-rs/core/src/codex.rs | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs
index 11b1e9f98204..008ff170ab75 100644
--- a/codex-rs/core/src/codex.rs
+++ b/codex-rs/core/src/codex.rs
@@ -5806,9 +5806,9 @@ async fn try_run_sampling_request(
                 sess.services.models_manager.refresh_if_new_etag(etag).await;
             }
             ResponseEvent::Completed {
-                response_id: _,
+                response_id: _response_id,
                 token_usage,
-                ..
+                can_append: _can_append,
             } => {
                 if let Some(state) = plan_mode_state.as_mut() {
                     flush_proposed_plan_segments_all(&sess, &turn_context, state).await;

From bddd53bae5d987653bdd3a3d4c071edaa2f30676 Mon Sep 17 00:00:00 2001
From: Charles Cunningham <ccunningham@openai.com>
Date: Sat, 14 Feb 2026 13:28:59 -0800
Subject: [PATCH 15/80] Rebase: align compaction snapshots and imports

---
 codex-rs/core/src/compact_remote.rs                           | 2 +-
 ...ompact__pre_turn_compaction_including_incoming_shapes.snap | 4 ++--
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/codex-rs/core/src/compact_remote.rs b/codex-rs/core/src/compact_remote.rs
index 1039e24401d9..b338686ea053 100644
--- a/codex-rs/core/src/compact_remote.rs
+++ b/codex-rs/core/src/compact_remote.rs
@@ -3,9 +3,9 @@ use std::sync::Arc;
 use crate::Prompt;
 use crate::codex::Session;
 use crate::codex::TurnContext;
-use crate::compact::extract_trailing_model_switch_update_for_compaction_request;
 use crate::compact::AutoCompactCallsite;
 use crate::compact::TurnContextReinjection;
+use crate::compact::extract_trailing_model_switch_update_for_compaction_request;
 use crate::context_manager::ContextManager;
 use crate::context_manager::TotalTokenUsageBreakdown;
 use crate::context_manager::estimate_item_token_count;
diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_turn_compaction_including_incoming_shapes.snap b/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_turn_compaction_including_incoming_shapes.snap
index 57d461c02606..bd4eb41236cb 100644
--- a/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_turn_compaction_including_incoming_shapes.snap
+++ b/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_turn_compaction_including_incoming_shapes.snap
@@ -13,7 +13,7 @@ Scenario: Pre-turn auto-compaction with a context override includes incoming use
 05:message/user:USER_TWO
 06:message/assistant:SECOND_REPLY
 07:message/user:<ENVIRONMENT_CONTEXT:cwd=PRETURN_CONTEXT_DIFF_CWD>
-08:message/user:<image> | </image> | USER_THREE
+08:message/user:<image> | <input_image:image_url> | </image> | USER_THREE
 09:message/user:<SUMMARIZATION_PROMPT>
 
 ## Local Post-Compaction History Layout
@@ -22,5 +22,5 @@ Scenario: Pre-turn auto-compaction with a context override includes incoming use
 02:message/developer:<PERMISSIONS_INSTRUCTIONS>
 03:message/user:<AGENTS_MD>
 04:message/user:<ENVIRONMENT_CONTEXT:cwd=PRETURN_CONTEXT_DIFF_CWD>
-05:message/user:<image> | </image> | USER_THREE
+05:message/user:<image> | <input_image:image_url> | </image> | USER_THREE
 06:message/user:<COMPACTION_SUMMARY>\nPRE_TURN_SUMMARY

From d9b02b6a9e26f45a4842cc67d6527b5fe61634a0 Mon Sep 17 00:00:00 2001
From: Charles Cunningham <ccunningham@openai.com>
Date: Sat, 14 Feb 2026 17:02:01 -0800
Subject: [PATCH 16/80] Differentiate model-switch compaction failures from
 oversize input

---
 codex-rs/core/src/codex.rs           |  33 +++++--
 codex-rs/core/tests/suite/compact.rs | 124 +++++++++++++++++++++++++++
 2 files changed, 149 insertions(+), 8 deletions(-)

diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs
index 008ff170ab75..08c5a4da2884 100644
--- a/codex-rs/core/src/codex.rs
+++ b/codex-rs/core/src/codex.rs
@@ -4284,6 +4284,31 @@ pub(crate) async fn run_turn(
         collaboration_mode_kind: turn_context.collaboration_mode.mode,
     });
     sess.send_event(&turn_context, event).await;
+
+    let total_usage_tokens_before_compaction = sess.get_total_token_usage().await;
+    if let Err(err) = maybe_run_previous_model_inline_compact(
+        &sess,
+        &turn_context,
+        total_usage_tokens_before_compaction,
+    )
+    .await
+    {
+        if !pre_turn_context_items.is_empty() {
+            // Preserve model-visible settings updates even when pre-turn compaction fails
+            // before we can persist turn input.
+            sess.record_conversation_items(&turn_context, &pre_turn_context_items)
+                .await;
+        }
+        let compact_error_prefix = if should_use_remote_compact_task(&turn_context.provider) {
+            "Error running remote compact task"
+        } else {
+            "Error running local compact task"
+        };
+        let event = EventMsg::Error(err.to_error_event(Some(compact_error_prefix.to_string())));
+        sess.send_event(&turn_context, event).await;
+        return None;
+    }
+
     let pre_turn_compaction_outcome = match run_pre_turn_auto_compaction_if_needed(
         &sess,
         &turn_context,
@@ -4764,14 +4789,6 @@ async fn run_pre_turn_auto_compaction_if_needed(
     auto_compact_limit: i64,
     incoming_turn_items: &[ResponseItem],
 ) -> CodexResult<PreTurnCompactionOutcome> {
-    let total_usage_tokens_before_compaction = sess.get_total_token_usage().await;
-    maybe_run_previous_model_inline_compact(
-        sess,
-        turn_context,
-        total_usage_tokens_before_compaction,
-    )
-    .await?;
-
     let total_usage_tokens = sess.get_total_token_usage().await;
     let incoming_items_tokens_estimate = incoming_turn_items
         .iter()
diff --git a/codex-rs/core/tests/suite/compact.rs b/codex-rs/core/tests/suite/compact.rs
index 4ef9fd15de35..ee77570414c6 100644
--- a/codex-rs/core/tests/suite/compact.rs
+++ b/codex-rs/core/tests/suite/compact.rs
@@ -1800,6 +1800,130 @@ async fn pre_sampling_compact_runs_on_switch_to_smaller_context_model() {
     );
 }
 
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn pre_sampling_compact_context_window_failure_surfaces_compact_task_error() {
+    skip_if_no_network!();
+
+    let server = MockServer::start().await;
+    let previous_model = "gpt-5.2-codex";
+    let next_model = "gpt-5.1-codex-max";
+
+    let models_mock = mount_models_once(
+        &server,
+        ModelsResponse {
+            models: vec![
+                model_info_with_context_window(previous_model, 273_000),
+                model_info_with_context_window(next_model, 125_000),
+            ],
+        },
+    )
+    .await;
+
+    let request_log = mount_sse_sequence(
+        &server,
+        vec![
+            sse(vec![
+                ev_assistant_message("m1", "before switch"),
+                ev_completed_with_tokens("r1", 120_000),
+            ]),
+            sse_failed(
+                "compact-failed",
+                "context_length_exceeded",
+                CONTEXT_LIMIT_MESSAGE,
+            ),
+        ],
+    )
+    .await;
+
+    let mut model_provider = non_openai_model_provider(&server);
+    model_provider.stream_max_retries = Some(0);
+    let mut builder = test_codex()
+        .with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing())
+        .with_model(previous_model)
+        .with_config(move |config| {
+            config.model_provider = model_provider;
+            set_test_compact_prompt(config);
+            config.features.enable(Feature::RemoteModels);
+        });
+    let test = builder.build(&server).await.expect("build test codex");
+
+    test.codex
+        .submit(Op::UserTurn {
+            items: vec![UserInput::Text {
+                text: "before switch".into(),
+                text_elements: Vec::new(),
+            }],
+            final_output_json_schema: None,
+            cwd: test.cwd.path().to_path_buf(),
+            approval_policy: AskForApproval::Never,
+            sandbox_policy: SandboxPolicy::DangerFullAccess,
+            model: previous_model.to_string(),
+            effort: None,
+            summary: ReasoningSummary::Auto,
+            collaboration_mode: None,
+            personality: None,
+        })
+        .await
+        .expect("submit first user turn");
+    wait_for_event(&test.codex, |event| {
+        matches!(event, EventMsg::TurnComplete(_))
+    })
+    .await;
+
+    test.codex
+        .submit(Op::UserTurn {
+            items: vec![UserInput::Text {
+                text: "after switch".into(),
+                text_elements: Vec::new(),
+            }],
+            final_output_json_schema: None,
+            cwd: test.cwd.path().to_path_buf(),
+            approval_policy: AskForApproval::Never,
+            sandbox_policy: SandboxPolicy::DangerFullAccess,
+            model: next_model.to_string(),
+            effort: None,
+            summary: ReasoningSummary::Auto,
+            collaboration_mode: None,
+            personality: None,
+        })
+        .await
+        .expect("submit second user turn");
+
+    let error_message = wait_for_event_match(&test.codex, |event| match event {
+        EventMsg::Error(err) => Some(err.message.clone()),
+        _ => None,
+    })
+    .await;
+    wait_for_event(&test.codex, |event| {
+        matches!(event, EventMsg::TurnComplete(_))
+    })
+    .await;
+
+    assert_eq!(models_mock.requests().len(), 1);
+    assert!(
+        error_message.contains("Error running local compact task"),
+        "expected local compact-task failure prefix, got {error_message}"
+    );
+    assert!(
+        !error_message.contains(
+            "Incoming user message and/or turn context is too large to fit in context window"
+        ),
+        "model-switch pre-sampling compaction failure should not be misclassified as incoming-input oversize: {error_message}"
+    );
+
+    let requests = request_log.requests();
+    assert_eq!(
+        requests.len(),
+        2,
+        "expected first user turn and one pre-sampling compaction request"
+    );
+    let compact_request_body = requests[1].body_json().to_string();
+    assert!(
+        body_contains_text(&compact_request_body, SUMMARIZATION_PROMPT),
+        "second request should be the pre-sampling compaction request"
+    );
+}
+
 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
 async fn pre_sampling_compact_runs_after_resume_and_switch_to_smaller_model() {
     skip_if_no_network!();

From ce7777801d68f40851eb8622213bf038bb5e3013 Mon Sep 17 00:00:00 2001
From: Charles Cunningham <ccunningham@openai.com>
Date: Sun, 15 Feb 2026 15:28:19 -0800
Subject: [PATCH 17/80] Enable compaction tests and remove stale TODO markers

---
 codex-rs/core/tests/suite/compact.rs        | 5 -----
 codex-rs/core/tests/suite/compact_remote.rs | 5 -----
 2 files changed, 10 deletions(-)

diff --git a/codex-rs/core/tests/suite/compact.rs b/codex-rs/core/tests/suite/compact.rs
index ee77570414c6..b29ba0184a45 100644
--- a/codex-rs/core/tests/suite/compact.rs
+++ b/codex-rs/core/tests/suite/compact.rs
@@ -2312,9 +2312,6 @@ async fn manual_compact_retries_after_context_window_error() {
 }
 
 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
-// TODO(ccunningham): Re-enable after the follow-up compaction behavior PR lands.
-// Current main behavior around non-context manual /compact failures is known-incorrect.
-#[ignore = "behavior change covered in follow-up compaction PR"]
 async fn manual_compact_non_context_failure_retries_then_emits_task_error() {
     skip_if_no_network!();
 
@@ -3180,8 +3177,6 @@ async fn snapshot_request_shape_pre_turn_compaction_including_incoming_user_mess
 }
 
 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
-// TODO(ccunningham): Update once pre-turn compaction context-overflow handling includes incoming
-// user input and emits richer oversized-input messaging.
 async fn snapshot_request_shape_pre_turn_compaction_strips_incoming_model_switch() {
     skip_if_no_network!();
 
diff --git a/codex-rs/core/tests/suite/compact_remote.rs b/codex-rs/core/tests/suite/compact_remote.rs
index e125b29d7a9f..d6cd61121191 100644
--- a/codex-rs/core/tests/suite/compact_remote.rs
+++ b/codex-rs/core/tests/suite/compact_remote.rs
@@ -930,9 +930,6 @@ async fn remote_manual_compact_failure_emits_task_error_event() -> Result<()> {
 }
 
 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
-// TODO(ccunningham): Re-enable after the follow-up compaction behavior PR lands.
-// Current main behavior for rollout replacement-history persistence is known-incorrect.
-#[ignore = "behavior change covered in follow-up compaction PR"]
 async fn remote_compact_persists_replacement_history_in_rollout() -> Result<()> {
     skip_if_no_network!(Ok(()));
 
@@ -1553,8 +1550,6 @@ async fn snapshot_request_shape_remote_pre_turn_compaction_strips_incoming_model
 }
 
 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
-// TODO(ccunningham): Update once remote pre-turn compaction context-overflow handling includes
-// incoming user input and emits richer oversized-input messaging.
 async fn snapshot_request_shape_remote_pre_turn_compaction_context_window_exceeded() -> Result<()> {
     skip_if_no_network!(Ok(()));
 

From 9e6ef13d4ddadb849fb322c70e7ea19509c33baa Mon Sep 17 00:00:00 2001
From: Charles Cunningham <ccunningham@openai.com>
Date: Sun, 15 Feb 2026 15:32:30 -0800
Subject: [PATCH 18/80] Add comment

---
 codex-rs/core/src/tasks/compact.rs | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/codex-rs/core/src/tasks/compact.rs b/codex-rs/core/src/tasks/compact.rs
index 9835c95bee0b..c011edccbae3 100644
--- a/codex-rs/core/src/tasks/compact.rs
+++ b/codex-rs/core/src/tasks/compact.rs
@@ -49,6 +49,7 @@ impl SessionTask for CompactTask {
             } else if has_user_turn_boundary {
                 // Manual `/compact` rewrites history to compacted transcript items and drops
                 // per-turn context entries. Force initial-context reseeding on the next user turn.
+                // TODO(ccunningham): Eliminate this when we have better TurnContextItem diffing (compaction aware)
                 session.mark_initial_context_unseeded_for_next_turn().await;
             }
         } else {
@@ -67,6 +68,7 @@ impl SessionTask for CompactTask {
             } else if has_user_turn_boundary {
                 // Manual `/compact` rewrites history to compacted transcript items and drops
                 // per-turn context entries. Force initial-context reseeding on the next user turn.
+                // TODO(ccunningham): Eliminate this when we have better TurnContextItem diffing (compaction aware)
                 session.mark_initial_context_unseeded_for_next_turn().await;
             }
         }

From b7b50d6e98dd5b9d4d627d851b371792a4bc55c8 Mon Sep 17 00:00:00 2001
From: Charles Cunningham <ccunningham@openai.com>
Date: Sun, 15 Feb 2026 16:38:55 -0800
Subject: [PATCH 19/80] Add TODO for legacy compacted history context
 reinjection

---
 codex-rs/core/src/codex.rs | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs
index 08c5a4da2884..860849385134 100644
--- a/codex-rs/core/src/codex.rs
+++ b/codex-rs/core/src/codex.rs
@@ -2387,6 +2387,10 @@ impl Session {
                     if let Some(replacement) = &compacted.replacement_history {
                         history.replace(replacement.clone());
                     } else {
+                        // TODO(ccunningham): When we have TurnContextItem-based legacy reconstruction,
+                        // build historical turn context from those items and inject it at the correct
+                        // point in compacted history instead of prepending the current initial context.
+                        // This matters for legacy rollouts with replacement_history=None.
                         let user_messages = collect_user_messages(history.raw_items());
                         let mut rebuilt = self.build_initial_context(turn_context).await;
                         rebuilt.extend(compact::build_compacted_history(

From 5682f7255130b641e2caf6c371cc80455edc8f7c Mon Sep 17 00:00:00 2001
From: Charles Cunningham <ccunningham@openai.com>
Date: Sun, 15 Feb 2026 16:56:23 -0800
Subject: [PATCH 20/80] Move previous-model compact failure handling into
 helper

---
 codex-rs/core/src/codex.rs | 73 +++++++++++++++++++++-----------------
 1 file changed, 41 insertions(+), 32 deletions(-)

diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs
index 860849385134..e6b1576203f9 100644
--- a/codex-rs/core/src/codex.rs
+++ b/codex-rs/core/src/codex.rs
@@ -4290,26 +4290,14 @@ pub(crate) async fn run_turn(
     sess.send_event(&turn_context, event).await;
 
     let total_usage_tokens_before_compaction = sess.get_total_token_usage().await;
-    if let Err(err) = maybe_run_previous_model_inline_compact(
+    if !maybe_run_previous_model_inline_compact(
         &sess,
         &turn_context,
         total_usage_tokens_before_compaction,
+        &pre_turn_context_items,
     )
     .await
     {
-        if !pre_turn_context_items.is_empty() {
-            // Preserve model-visible settings updates even when pre-turn compaction fails
-            // before we can persist turn input.
-            sess.record_conversation_items(&turn_context, &pre_turn_context_items)
-                .await;
-        }
-        let compact_error_prefix = if should_use_remote_compact_task(&turn_context.provider) {
-            "Error running remote compact task"
-        } else {
-            "Error running local compact task"
-        };
-        let event = EventMsg::Error(err.to_error_event(Some(compact_error_prefix.to_string())));
-        sess.send_event(&turn_context, event).await;
         return None;
     }
 
@@ -4690,9 +4678,10 @@ async fn maybe_run_previous_model_inline_compact(
     sess: &Arc<Session>,
     turn_context: &Arc<TurnContext>,
     total_usage_tokens: i64,
-) -> CodexResult<()> {
+    pre_turn_context_items: &[ResponseItem],
+) -> bool {
     let Some(previous_model) = sess.previous_model().await else {
-        return Ok(());
+        return true;
     };
     let previous_turn_context = Arc::new(
         turn_context
@@ -4701,10 +4690,10 @@ async fn maybe_run_previous_model_inline_compact(
     );
 
     let Some(old_context_window) = previous_turn_context.model_context_window() else {
-        return Ok(());
+        return true;
     };
     let Some(new_context_window) = turn_context.model_context_window() else {
-        return Ok(());
+        return true;
     };
     let new_auto_compact_limit = turn_context
         .model_info
@@ -4713,21 +4702,41 @@ async fn maybe_run_previous_model_inline_compact(
     let should_run = total_usage_tokens > new_auto_compact_limit
         && previous_turn_context.model_info.slug != turn_context.model_info.slug
         && old_context_window > new_context_window;
-    if should_run {
-        run_auto_compact(
-            sess,
-            &previous_turn_context,
-            AutoCompactCallsite::PreTurnExcludingIncomingUserMessage,
-            // Even though incoming turn items are excluded here, this pass can be the only
-            // compaction run before submission. Reinject canonical context so unchanged
-            // model-visible instructions remain present if no follow-up pre-turn compaction
-            // is needed.
-            TurnContextReinjection::ReinjectAboveLastRealUser,
-            None,
-        )
-        .await?;
+    if !should_run {
+        return true;
+    }
+
+    match run_auto_compact(
+        sess,
+        &previous_turn_context,
+        AutoCompactCallsite::PreTurnExcludingIncomingUserMessage,
+        // Even though incoming turn items are excluded here, this pass can be the only
+        // compaction run before submission. Reinject canonical context so unchanged
+        // model-visible instructions remain present if no follow-up pre-turn compaction
+        // is needed.
+        TurnContextReinjection::ReinjectAboveLastRealUser,
+        None,
+    )
+    .await
+    {
+        Ok(()) => true,
+        Err(err) => {
+            if !pre_turn_context_items.is_empty() {
+                // Preserve model-visible settings updates even when pre-turn compaction fails
+                // before we can persist turn input.
+                sess.record_conversation_items(turn_context, pre_turn_context_items)
+                    .await;
+            }
+            let compact_error_prefix = if should_use_remote_compact_task(&turn_context.provider) {
+                "Error running remote compact task"
+            } else {
+                "Error running local compact task"
+            };
+            let event = EventMsg::Error(err.to_error_event(Some(compact_error_prefix.to_string())));
+            sess.send_event(turn_context, event).await;
+            false
+        }
     }
-    Ok(())
 }
 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
 enum PreTurnCompactionOutcome {

From e3efe3a02080a143505711ec73d70afaca98bf8e Mon Sep 17 00:00:00 2001
From: Charles Cunningham <ccunningham@openai.com>
Date: Sun, 15 Feb 2026 17:00:13 -0800
Subject: [PATCH 21/80] Return sentinel error from model-switch compaction
 helper

---
 codex-rs/core/src/codex.rs | 22 ++++++++++++++--------
 1 file changed, 14 insertions(+), 8 deletions(-)

diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs
index e6b1576203f9..310fce3838ac 100644
--- a/codex-rs/core/src/codex.rs
+++ b/codex-rs/core/src/codex.rs
@@ -4290,14 +4290,16 @@ pub(crate) async fn run_turn(
     sess.send_event(&turn_context, event).await;
 
     let total_usage_tokens_before_compaction = sess.get_total_token_usage().await;
-    if !maybe_run_previous_model_inline_compact(
+    if maybe_run_previous_model_inline_compact(
         &sess,
         &turn_context,
         total_usage_tokens_before_compaction,
         &pre_turn_context_items,
     )
     .await
+    .is_err()
     {
+        // Error messaging is emitted inside maybe_run_previous_model_inline_compact.
         return None;
     }
 
@@ -4674,14 +4676,18 @@ pub(crate) async fn run_turn(
     last_agent_message
 }
 
+/// Runs the pre-sampling model-switch compaction pass when needed.
+///
+/// On failure this function emits any user-visible error event itself and returns `Err(())` as a
+/// sentinel so callers can stop the turn without duplicating error messaging logic.
 async fn maybe_run_previous_model_inline_compact(
     sess: &Arc<Session>,
     turn_context: &Arc<TurnContext>,
     total_usage_tokens: i64,
     pre_turn_context_items: &[ResponseItem],
-) -> bool {
+) -> Result<(), ()> {
     let Some(previous_model) = sess.previous_model().await else {
-        return true;
+        return Ok(());
     };
     let previous_turn_context = Arc::new(
         turn_context
@@ -4690,10 +4696,10 @@ async fn maybe_run_previous_model_inline_compact(
     );
 
     let Some(old_context_window) = previous_turn_context.model_context_window() else {
-        return true;
+        return Ok(());
     };
     let Some(new_context_window) = turn_context.model_context_window() else {
-        return true;
+        return Ok(());
     };
     let new_auto_compact_limit = turn_context
         .model_info
@@ -4703,7 +4709,7 @@ async fn maybe_run_previous_model_inline_compact(
         && previous_turn_context.model_info.slug != turn_context.model_info.slug
         && old_context_window > new_context_window;
     if !should_run {
-        return true;
+        return Ok(());
     }
 
     match run_auto_compact(
@@ -4719,7 +4725,7 @@ async fn maybe_run_previous_model_inline_compact(
     )
     .await
     {
-        Ok(()) => true,
+        Ok(()) => Ok(()),
         Err(err) => {
             if !pre_turn_context_items.is_empty() {
                 // Preserve model-visible settings updates even when pre-turn compaction fails
@@ -4734,7 +4740,7 @@ async fn maybe_run_previous_model_inline_compact(
             };
             let event = EventMsg::Error(err.to_error_event(Some(compact_error_prefix.to_string())));
             sess.send_event(turn_context, event).await;
-            false
+            Err(())
         }
     }
 }

From 85956640929b8fd4ad5fbeaa1f1192368bf4dfc4 Mon Sep 17 00:00:00 2001
From: Charles Cunningham <ccunningham@openai.com>
Date: Sun, 15 Feb 2026 17:04:32 -0800
Subject: [PATCH 22/80] Move pre-turn auto-compact error handling into helper

---
 codex-rs/core/src/codex.rs | 87 ++++++++++++++++++--------------------
 1 file changed, 42 insertions(+), 45 deletions(-)

diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs
index 310fce3838ac..1110fa3d51a7 100644
--- a/codex-rs/core/src/codex.rs
+++ b/codex-rs/core/src/codex.rs
@@ -4303,46 +4303,17 @@ pub(crate) async fn run_turn(
         return None;
     }
 
-    let pre_turn_compaction_outcome = match run_pre_turn_auto_compaction_if_needed(
+    let Ok(pre_turn_compaction_outcome) = run_pre_turn_auto_compaction_if_needed(
         &sess,
         &turn_context,
         auto_compact_limit,
         &incoming_turn_items,
+        &pre_turn_context_items,
     )
     .await
-    {
-        Ok(outcome) => outcome,
-        Err(err) => {
-            if !pre_turn_context_items.is_empty() {
-                // Preserve model-visible settings updates even when pre-turn compaction fails
-                // before we can persist turn input.
-                sess.record_conversation_items(&turn_context, &pre_turn_context_items)
-                    .await;
-            }
-            let event = match err {
-                CodexErr::ContextWindowExceeded => {
-                    let incoming_items_tokens_estimate = incoming_turn_items
-                        .iter()
-                        .map(estimate_item_token_count)
-                        .fold(0_i64, i64::saturating_add);
-                    let message = format!(
-                        "Incoming user message and/or turn context is too large to fit in context window. Please reduce the size of your message and try again. (incoming_items_tokens_estimate={incoming_items_tokens_estimate})"
-                    );
-                    EventMsg::Error(CodexErr::ContextWindowExceeded.to_error_event(Some(message)))
-                }
-                other => {
-                    let compact_error_prefix =
-                        if should_use_remote_compact_task(&turn_context.provider) {
-                            "Error running remote compact task"
-                        } else {
-                            "Error running local compact task"
-                        };
-                    EventMsg::Error(other.to_error_event(Some(compact_error_prefix.to_string())))
-                }
-            };
-            sess.send_event(&turn_context, event).await;
-            return None;
-        }
+    else {
+        // Error messaging is emitted inside run_pre_turn_auto_compaction_if_needed.
+        return None;
     };
     persist_pre_turn_items_for_compaction_outcome(
         &sess,
@@ -4802,12 +4773,16 @@ async fn persist_pre_turn_items_for_compaction_outcome(
 }
 
 /// Runs pre-turn auto-compaction with incoming turn context + user message included.
+///
+/// On failure this function emits any user-visible error event itself and returns `Err(())` as a
+/// sentinel so callers can stop the turn without duplicating error messaging logic.
 async fn run_pre_turn_auto_compaction_if_needed(
     sess: &Arc<Session>,
     turn_context: &Arc<TurnContext>,
     auto_compact_limit: i64,
     incoming_turn_items: &[ResponseItem],
-) -> CodexResult<PreTurnCompactionOutcome> {
+    pre_turn_context_items: &[ResponseItem],
+) -> Result<PreTurnCompactionOutcome, ()> {
     let total_usage_tokens = sess.get_total_token_usage().await;
     let incoming_items_tokens_estimate = incoming_turn_items
         .iter()
@@ -4831,17 +4806,39 @@ async fn run_pre_turn_auto_compaction_if_needed(
     .await;
 
     if let Err(err) = compact_result {
-        if matches!(err, CodexErr::ContextWindowExceeded) {
-            error!(
-                turn_id = %turn_context.sub_id,
-                auto_compact_callsite = ?AutoCompactCallsite::PreTurnIncludingIncomingUserMessage,
-                incoming_items_tokens_estimate,
-                auto_compact_limit,
-                reason = "pre-turn compaction exceeded context window",
-                "incoming user/context is too large for pre-turn auto-compaction flow"
-            );
+        if !pre_turn_context_items.is_empty() {
+            // Preserve model-visible settings updates even when pre-turn compaction fails
+            // before we can persist turn input.
+            sess.record_conversation_items(turn_context, pre_turn_context_items)
+                .await;
         }
-        return Err(err);
+        let event = match err {
+            CodexErr::ContextWindowExceeded => {
+                error!(
+                    turn_id = %turn_context.sub_id,
+                    auto_compact_callsite = ?AutoCompactCallsite::PreTurnIncludingIncomingUserMessage,
+                    incoming_items_tokens_estimate,
+                    auto_compact_limit,
+                    reason = "pre-turn compaction exceeded context window",
+                    "incoming user/context is too large for pre-turn auto-compaction flow"
+                );
+                let message = format!(
+                    "Incoming user message and/or turn context is too large to fit in context window. Please reduce the size of your message and try again. (incoming_items_tokens_estimate={incoming_items_tokens_estimate})"
+                );
+                EventMsg::Error(CodexErr::ContextWindowExceeded.to_error_event(Some(message)))
+            }
+            other => {
+                let compact_error_prefix = if should_use_remote_compact_task(&turn_context.provider)
+                {
+                    "Error running remote compact task"
+                } else {
+                    "Error running local compact task"
+                };
+                EventMsg::Error(other.to_error_event(Some(compact_error_prefix.to_string())))
+            }
+        };
+        sess.send_event(turn_context, event).await;
+        return Err(());
     }
 
     Ok(PreTurnCompactionOutcome::CompactedWithIncomingItems)

From de959aafa29fe3069b504657cba31f4ee9c8d8ae Mon Sep 17 00:00:00 2001
From: Charles Cunningham <ccunningham@openai.com>
Date: Sun, 15 Feb 2026 17:16:35 -0800
Subject: [PATCH 23/80] Comments

---
 codex-rs/core/src/codex.rs | 6 ++----
 1 file changed, 2 insertions(+), 4 deletions(-)

diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs
index 1110fa3d51a7..497fecac0f08 100644
--- a/codex-rs/core/src/codex.rs
+++ b/codex-rs/core/src/codex.rs
@@ -4685,12 +4685,10 @@ async fn maybe_run_previous_model_inline_compact(
 
     match run_auto_compact(
         sess,
+        // We use previous turn context here because we compact with the previous model
         &previous_turn_context,
         AutoCompactCallsite::PreTurnExcludingIncomingUserMessage,
-        // Even though incoming turn items are excluded here, this pass can be the only
-        // compaction run before submission. Reinject canonical context so unchanged
-        // model-visible instructions remain present if no follow-up pre-turn compaction
-        // is needed.
+        // User message and turn context diff is injected in the pre-compaction NotNeeded case later
         TurnContextReinjection::ReinjectAboveLastRealUser,
         None,
     )

From 54e606ade3d4c2821cfbb417460d4bcf312cd5ef Mon Sep 17 00:00:00 2001
From: Charles Cunningham <ccunningham@openai.com>
Date: Sun, 15 Feb 2026 17:20:55 -0800
Subject: [PATCH 24/80] Comment

---
 codex-rs/core/src/codex.rs | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs
index 497fecac0f08..8fad914a2477 100644
--- a/codex-rs/core/src/codex.rs
+++ b/codex-rs/core/src/codex.rs
@@ -4755,10 +4755,12 @@ async fn persist_pre_turn_items_for_compaction_outcome(
             sess.record_user_prompt_and_emit_turn_item(turn_context.as_ref(), input, response_item)
                 .await;
         }
+        // TODO(ccunningham): Followup PR will use compacting excluding incoming items as a fallback
+        // (even though it is out of distribution for current models).
+        // Also future models may prefer compacting pre-turn history without incoming turn items.
         #[cfg(test)]
         PreTurnCompactionOutcome::CompactedWithoutIncomingItems => {
-            // Reserved path for future models that compact pre-turn history without incoming turn
-            // items; reseed canonical initial context above the incoming user message.
+            // Reseed canonical initial context above the incoming user message.
             let initial_context = sess.build_initial_context(turn_context.as_ref()).await;
             if !initial_context.is_empty() {
                 sess.record_conversation_items(turn_context, &initial_context)

From 3a04f2c6895d2d32a995f2a6b3521fd1f54ba640 Mon Sep 17 00:00:00 2001
From: Charles Cunningham <ccunningham@openai.com>
Date: Sun, 15 Feb 2026 17:31:24 -0800
Subject: [PATCH 25/80] Use explicit user-message predicate for compaction
 filtering

---
 codex-rs/core/src/compact.rs | 25 ++++++++++++++++++++-----
 1 file changed, 20 insertions(+), 5 deletions(-)

diff --git a/codex-rs/core/src/compact.rs b/codex-rs/core/src/compact.rs
index 40de1b5ec4ea..9d8cbede59ae 100644
--- a/codex-rs/core/src/compact.rs
+++ b/codex-rs/core/src/compact.rs
@@ -286,7 +286,7 @@ async fn run_compact_task_inner(
     let incoming_user_items = match incoming_items.as_ref() {
         Some(items) => items
             .iter()
-            .filter(|item| real_user_message_text(item).is_some())
+            .filter(|item| is_non_summary_user_message(item))
             .cloned()
             .collect(),
         None => Vec::new(),
@@ -391,7 +391,7 @@ pub(crate) fn process_compacted_history(
             // user input rather than an earlier turn.
             if let Some(insertion_index) = compacted_history
                 .iter()
-                .rposition(|item| real_user_message_text(item).is_some())
+                .rposition(is_non_summary_user_message)
             {
                 compacted_history
                     .splice(insertion_index..insertion_index, initial_context.to_vec());
@@ -403,13 +403,13 @@ pub(crate) fn process_compacted_history(
     compacted_history
 }
 
-fn real_user_message_text(item: &ResponseItem) -> Option<String> {
+fn is_non_summary_user_message(item: &ResponseItem) -> bool {
     match crate::event_mapping::parse_turn_item(item) {
         Some(TurnItem::UserMessage(user_message)) => {
             let message = user_message.message();
-            (!is_summary_message(&message)).then_some(message)
+            !is_summary_message(&message)
         }
-        _ => None,
+        _ => false,
     }
 }
 
@@ -874,6 +874,21 @@ do things
         assert_eq!(history, expected);
     }
 
+    #[test]
+    fn non_summary_user_message_includes_image_only_user_messages() {
+        let image_only_user = ResponseItem::Message {
+            id: None,
+            role: "user".to_string(),
+            content: vec![ContentItem::InputImage {
+                image_url: "data:image/png;base64,AAAA".to_string(),
+            }],
+            end_turn: None,
+            phase: None,
+        };
+
+        assert!(super::is_non_summary_user_message(&image_only_user));
+    }
+
     #[test]
     fn process_compacted_history_replaces_developer_messages() {
         let compacted_history = vec![

From f832de63716c6a21a8263ebe157f4ce50f09734a Mon Sep 17 00:00:00 2001
From: Charles Cunningham <ccunningham@openai.com>
Date: Sun, 15 Feb 2026 17:33:29 -0800
Subject: [PATCH 26/80] Document manual compact turn-context reinjection policy

---
 codex-rs/core/src/compact.rs | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/codex-rs/core/src/compact.rs b/codex-rs/core/src/compact.rs
index 9d8cbede59ae..6223287ee2a5 100644
--- a/codex-rs/core/src/compact.rs
+++ b/codex-rs/core/src/compact.rs
@@ -130,6 +130,8 @@ pub(crate) async fn run_compact_task(
         turn_context,
         input,
         None,
+        // Manual `/compact` should not reinsert turn context into compacted history; we reseed
+        // canonical initial context before the next user turn.
         TurnContextReinjection::Skip,
         None,
     )

From b029cc2d01d2e5bd334fa46b1d7bdb4596a69dd7 Mon Sep 17 00:00:00 2001
From: Charles Cunningham <ccunningham@openai.com>
Date: Sun, 15 Feb 2026 17:56:37 -0800
Subject: [PATCH 27/80] Reinject context for summary-only compacted history

---
 codex-rs/core/src/compact.rs                  | 56 +++++++++++++------
 ..._history_reinject_summary_only_shapes.snap |  9 +++
 2 files changed, 48 insertions(+), 17 deletions(-)
 create mode 100644 codex-rs/core/src/snapshots/codex_core__compact__tests__process_compacted_history_reinject_summary_only_shapes.snap

diff --git a/codex-rs/core/src/compact.rs b/codex-rs/core/src/compact.rs
index 6223287ee2a5..f7f395d8b611 100644
--- a/codex-rs/core/src/compact.rs
+++ b/codex-rs/core/src/compact.rs
@@ -389,15 +389,27 @@ pub(crate) fn process_compacted_history(
 
     match turn_context_reinjection {
         TurnContextReinjection::ReinjectAboveLastRealUser => {
-            // Insert immediately above the last real user message so turn context applies to that
-            // user input rather than an earlier turn.
-            if let Some(insertion_index) = compacted_history
+            // Prefer inserting immediately above the last real user message so turn context
+            // applies to that user input rather than an earlier turn. If compaction output is
+            // summary-only, insert before the first summary user message to keep canonical context
+            // present for the next sampling request.
+            let insertion_index = if let Some(last_real_user_index) = compacted_history
                 .iter()
                 .rposition(is_non_summary_user_message)
             {
-                compacted_history
-                    .splice(insertion_index..insertion_index, initial_context.to_vec());
-            }
+                last_real_user_index
+            } else if let Some(first_summary_index) = compacted_history.iter().position(|item| {
+                matches!(
+                    crate::event_mapping::parse_turn_item(item),
+                    Some(TurnItem::UserMessage(user_message))
+                        if is_summary_message(&user_message.message())
+                )
+            }) {
+                first_summary_index
+            } else {
+                compacted_history.len()
+            };
+            compacted_history.splice(insertion_index..insertion_index, initial_context.to_vec());
         }
         TurnContextReinjection::Skip => {}
     }
@@ -560,7 +572,12 @@ async fn drain_to_completed(
 mod tests {
 
     use super::*;
+    use core_test_support::context_snapshot;
+    use core_test_support::context_snapshot::ContextSnapshotOptions;
+    use core_test_support::context_snapshot::ContextSnapshotRenderMode;
+    use insta::assert_snapshot;
     use pretty_assertions::assert_eq;
+    use serde_json::Value;
 
     #[test]
     fn content_items_to_text_joins_non_empty_segments() {
@@ -1534,7 +1551,7 @@ keep me updated
     }
 
     #[test]
-    fn process_compacted_history_reinject_noops_without_real_user_message() {
+    fn process_compacted_history_reinjects_context_when_compaction_output_is_summary_only() {
         let compacted_history = vec![ResponseItem::Message {
             id: None,
             role: "user".to_string(),
@@ -1559,16 +1576,21 @@ keep me updated
             &initial_context,
             TurnContextReinjection::ReinjectAboveLastRealUser,
         );
-        let expected = vec![ResponseItem::Message {
-            id: None,
-            role: "user".to_string(),
-            content: vec![ContentItem::InputText {
-                text: format!("{SUMMARY_PREFIX}\nsummary text"),
-            }],
-            end_turn: None,
-            phase: None,
-        }];
-        assert_eq!(refreshed, expected);
+        let refreshed_value =
+            serde_json::to_value(&refreshed).expect("serialize refreshed history");
+        let Value::Array(refreshed_items) = refreshed_value else {
+            panic!("expected refreshed history to serialize as array");
+        };
+
+        assert_snapshot!(
+            "process_compacted_history_reinject_summary_only_shapes",
+            context_snapshot::format_labeled_items_snapshot(
+                "When compaction output contains only a summary user message, canonical context is still reinserted before the summary.",
+                &[("Refreshed History Layout", refreshed_items.as_slice())],
+                &ContextSnapshotOptions::default()
+                    .render_mode(ContextSnapshotRenderMode::KindWithTextPrefix { max_chars: 64 }),
+            )
+        );
     }
 
     #[test]
diff --git a/codex-rs/core/src/snapshots/codex_core__compact__tests__process_compacted_history_reinject_summary_only_shapes.snap b/codex-rs/core/src/snapshots/codex_core__compact__tests__process_compacted_history_reinject_summary_only_shapes.snap
new file mode 100644
index 000000000000..aba7c96a6d7f
--- /dev/null
+++ b/codex-rs/core/src/snapshots/codex_core__compact__tests__process_compacted_history_reinject_summary_only_shapes.snap
@@ -0,0 +1,9 @@
+---
+source: core/src/compact.rs
+expression: "context_snapshot::format_labeled_items_snapshot(\"When compaction output contains only a summary user message, canonical context is still reinserted before the summary.\",\n&[(\"Refreshed History Layout\", refreshed_items.as_slice())],\n&ContextSnapshotOptions::default().render_mode(ContextSnapshotRenderMode::KindWithTextPrefix\n{ max_chars: 64 }),)"
+---
+Scenario: When compaction output contains only a summary user message, canonical context is still reinserted before the summary.
+
+## Refreshed History Layout
+00:message/developer:fresh permissions
+01:message/user:<COMPACTION_SUMMARY>\nsummary text

From 466c689766436df953c1483bc9a5ef6626276494 Mon Sep 17 00:00:00 2001
From: Charles Cunningham <ccunningham@openai.com>
Date: Sun, 15 Feb 2026 20:45:20 -0800
Subject: [PATCH 28/80] compact: reinsert context above last summary

---
 codex-rs/core/src/compact.rs | 79 +++++++++++++++++++++++++++++++++---
 1 file changed, 74 insertions(+), 5 deletions(-)

diff --git a/codex-rs/core/src/compact.rs b/codex-rs/core/src/compact.rs
index f7f395d8b611..ba507279426e 100644
--- a/codex-rs/core/src/compact.rs
+++ b/codex-rs/core/src/compact.rs
@@ -390,22 +390,22 @@ pub(crate) fn process_compacted_history(
     match turn_context_reinjection {
         TurnContextReinjection::ReinjectAboveLastRealUser => {
             // Prefer inserting immediately above the last real user message so turn context
-            // applies to that user input rather than an earlier turn. If compaction output is
-            // summary-only, insert before the first summary user message to keep canonical context
-            // present for the next sampling request.
+            // applies to that user input rather than an earlier turn. If compaction output has no
+            // real user messages, insert before the last summary user message to keep canonical
+            // context present for the next sampling request.
             let insertion_index = if let Some(last_real_user_index) = compacted_history
                 .iter()
                 .rposition(is_non_summary_user_message)
             {
                 last_real_user_index
-            } else if let Some(first_summary_index) = compacted_history.iter().position(|item| {
+            } else if let Some(last_summary_index) = compacted_history.iter().rposition(|item| {
                 matches!(
                     crate::event_mapping::parse_turn_item(item),
                     Some(TurnItem::UserMessage(user_message))
                         if is_summary_message(&user_message.message())
                 )
             }) {
-                first_summary_index
+                last_summary_index
             } else {
                 compacted_history.len()
             };
@@ -1593,6 +1593,75 @@ keep me updated
         );
     }
 
+    #[test]
+    fn process_compacted_history_reinjects_context_above_last_summary_when_no_real_user() {
+        let compacted_history = vec![
+            ResponseItem::Message {
+                id: None,
+                role: "user".to_string(),
+                content: vec![ContentItem::InputText {
+                    text: format!("{SUMMARY_PREFIX}\nolder summary"),
+                }],
+                end_turn: None,
+                phase: None,
+            },
+            ResponseItem::Message {
+                id: None,
+                role: "user".to_string(),
+                content: vec![ContentItem::InputText {
+                    text: format!("{SUMMARY_PREFIX}\nlatest summary"),
+                }],
+                end_turn: None,
+                phase: None,
+            },
+        ];
+        let initial_context = vec![ResponseItem::Message {
+            id: None,
+            role: "developer".to_string(),
+            content: vec![ContentItem::InputText {
+                text: "fresh permissions".to_string(),
+            }],
+            end_turn: None,
+            phase: None,
+        }];
+
+        let refreshed = process_compacted_history(
+            compacted_history,
+            &initial_context,
+            TurnContextReinjection::ReinjectAboveLastRealUser,
+        );
+        let expected = vec![
+            ResponseItem::Message {
+                id: None,
+                role: "user".to_string(),
+                content: vec![ContentItem::InputText {
+                    text: format!("{SUMMARY_PREFIX}\nolder summary"),
+                }],
+                end_turn: None,
+                phase: None,
+            },
+            ResponseItem::Message {
+                id: None,
+                role: "developer".to_string(),
+                content: vec![ContentItem::InputText {
+                    text: "fresh permissions".to_string(),
+                }],
+                end_turn: None,
+                phase: None,
+            },
+            ResponseItem::Message {
+                id: None,
+                role: "user".to_string(),
+                content: vec![ContentItem::InputText {
+                    text: format!("{SUMMARY_PREFIX}\nlatest summary"),
+                }],
+                end_turn: None,
+                phase: None,
+            },
+        ];
+        assert_eq!(refreshed, expected);
+    }
+
     #[test]
     fn process_compacted_history_mid_turn_without_orphan_user_places_summary_last() {
         let compacted_history = vec![

From 5929f058b04301e7eea7fd9a2d85c2b5ea531c51 Mon Sep 17 00:00:00 2001
From: Charles Cunningham <ccunningham@openai.com>
Date: Sun, 15 Feb 2026 21:13:40 -0800
Subject: [PATCH 29/80] compact: snapshot reinjection above last summary

---
 codex-rs/core/src/compact.rs                     | 16 ++++++++++++++++
 ...story_reinject_above_last_summary_shapes.snap | 10 ++++++++++
 2 files changed, 26 insertions(+)
 create mode 100644 codex-rs/core/src/snapshots/codex_core__compact__tests__process_compacted_history_reinject_above_last_summary_shapes.snap

diff --git a/codex-rs/core/src/compact.rs b/codex-rs/core/src/compact.rs
index ba507279426e..bc7d2fc7e577 100644
--- a/codex-rs/core/src/compact.rs
+++ b/codex-rs/core/src/compact.rs
@@ -1660,6 +1660,22 @@ keep me updated
             },
         ];
         assert_eq!(refreshed, expected);
+
+        let refreshed_value =
+            serde_json::to_value(&refreshed).expect("serialize refreshed history");
+        let Value::Array(refreshed_items) = refreshed_value else {
+            panic!("expected refreshed history to serialize as array");
+        };
+
+        assert_snapshot!(
+            "process_compacted_history_reinject_above_last_summary_shapes",
+            context_snapshot::format_labeled_items_snapshot(
+                "When compaction output has multiple summary-only user messages and no real user message, canonical context is reinserted above the last summary.",
+                &[("Refreshed History Layout", refreshed_items.as_slice())],
+                &ContextSnapshotOptions::default()
+                    .render_mode(ContextSnapshotRenderMode::KindWithTextPrefix { max_chars: 64 }),
+            )
+        );
     }
 
     #[test]
diff --git a/codex-rs/core/src/snapshots/codex_core__compact__tests__process_compacted_history_reinject_above_last_summary_shapes.snap b/codex-rs/core/src/snapshots/codex_core__compact__tests__process_compacted_history_reinject_above_last_summary_shapes.snap
new file mode 100644
index 000000000000..906f79b55f81
--- /dev/null
+++ b/codex-rs/core/src/snapshots/codex_core__compact__tests__process_compacted_history_reinject_above_last_summary_shapes.snap
@@ -0,0 +1,10 @@
+---
+source: core/src/compact.rs
+expression: "context_snapshot::format_labeled_items_snapshot(\"When compaction output has multiple summary-only user messages and no real user message, canonical context is reinserted above the last summary.\",\n&[(\"Refreshed History Layout\", refreshed_items.as_slice())],\n&ContextSnapshotOptions::default().render_mode(ContextSnapshotRenderMode::KindWithTextPrefix\n{ max_chars: 64 }),)"
+---
+Scenario: When compaction output has multiple summary-only user messages and no real user message, canonical context is reinserted above the last summary.
+
+## Refreshed History Layout
+00:message/user:<COMPACTION_SUMMARY>\nolder summary
+01:message/developer:fresh permissions
+02:message/user:<COMPACTION_SUMMARY>\nlatest summary

From b5aeb30d59b4ca228b04b29f4f2bca29f2bbb9c5 Mon Sep 17 00:00:00 2001
From: Charles Cunningham <ccunningham@openai.com>
Date: Mon, 16 Feb 2026 11:03:52 -0800
Subject: [PATCH 30/80] compact: strip incoming model-switch before compaction

---
 codex-rs/core/src/compact.rs | 110 ++++++++++++++++++++++++++++++++++-
 1 file changed, 107 insertions(+), 3 deletions(-)

diff --git a/codex-rs/core/src/compact.rs b/codex-rs/core/src/compact.rs
index bc7d2fc7e577..9806d34d1ee6 100644
--- a/codex-rs/core/src/compact.rs
+++ b/codex-rs/core/src/compact.rs
@@ -88,6 +88,17 @@ pub(crate) fn extract_trailing_model_switch_update_for_compaction_request(
     Some(model_switch_item)
 }
 
+fn extract_latest_model_switch_update_from_items(
+    items: &mut Vec<ResponseItem>,
+) -> Option<ResponseItem> {
+    let model_switch_index = items
+        .iter()
+        .enumerate()
+        .rev()
+        .find_map(|(i, item)| Session::is_model_switch_developer_message(item).then_some(i))?;
+    Some(items.remove(model_switch_index))
+}
+
 pub(crate) async fn run_inline_auto_compact_task(
     sess: Arc<Session>,
     turn_context: Arc<TurnContext>,
@@ -152,10 +163,14 @@ async fn run_compact_task_inner(
     let initial_input_for_turn: ResponseInputItem = ResponseInputItem::from(input);
 
     let mut history = sess.clone_history().await;
+    let mut incoming_items = incoming_items;
     // Keep compaction prompts in-distribution: if a model-switch update was injected at the
-    // tail of history (between turns), exclude it from the compaction request payload.
-    let stripped_model_switch_item =
-        extract_trailing_model_switch_update_for_compaction_request(&mut history);
+    // tail of incoming turn items (pre-turn path) or between turns in history, exclude it from
+    // the compaction request payload.
+    let stripped_model_switch_item = incoming_items
+        .as_mut()
+        .and_then(extract_latest_model_switch_update_from_items)
+        .or_else(|| extract_trailing_model_switch_update_for_compaction_request(&mut history));
     if let Some(incoming_items) = incoming_items.as_ref() {
         history.record_items(incoming_items.iter(), turn_context.truncation_policy);
     }
@@ -710,6 +725,95 @@ mod tests {
         );
     }
 
+    #[test]
+    fn extract_model_switch_update_for_compaction_request_prefers_incoming_items() {
+        let mut history = ContextManager::new();
+        history.replace(vec![
+            ResponseItem::Message {
+                id: None,
+                role: "user".to_string(),
+                content: vec![ContentItem::InputText {
+                    text: "USER_MESSAGE".to_string(),
+                }],
+                end_turn: None,
+                phase: None,
+            },
+            ResponseItem::Message {
+                id: None,
+                role: "assistant".to_string(),
+                content: vec![ContentItem::OutputText {
+                    text: "ASSISTANT_REPLY".to_string(),
+                }],
+                end_turn: None,
+                phase: None,
+            },
+            ResponseItem::Message {
+                id: None,
+                role: "developer".to_string(),
+                content: vec![ContentItem::InputText {
+                    text: "<model_switch>\nHISTORY_MODEL_INSTRUCTIONS".to_string(),
+                }],
+                end_turn: None,
+                phase: None,
+            },
+        ]);
+        let mut incoming_items = vec![
+            ResponseItem::Message {
+                id: None,
+                role: "developer".to_string(),
+                content: vec![ContentItem::InputText {
+                    text: "<model_switch>\nINCOMING_MODEL_INSTRUCTIONS".to_string(),
+                }],
+                end_turn: None,
+                phase: None,
+            },
+            ResponseItem::Message {
+                id: None,
+                role: "user".to_string(),
+                content: vec![ContentItem::InputText {
+                    text: "INCOMING_USER_MESSAGE".to_string(),
+                }],
+                end_turn: None,
+                phase: None,
+            },
+        ];
+
+        let model_switch_item = Some(&mut incoming_items)
+            .and_then(extract_latest_model_switch_update_from_items)
+            .or_else(|| extract_trailing_model_switch_update_for_compaction_request(&mut history));
+
+        assert_eq!(
+            model_switch_item,
+            Some(ResponseItem::Message {
+                id: None,
+                role: "developer".to_string(),
+                content: vec![ContentItem::InputText {
+                    text: "<model_switch>\nINCOMING_MODEL_INSTRUCTIONS".to_string(),
+                }],
+                end_turn: None,
+                phase: None,
+            })
+        );
+        assert_eq!(
+            incoming_items,
+            vec![ResponseItem::Message {
+                id: None,
+                role: "user".to_string(),
+                content: vec![ContentItem::InputText {
+                    text: "INCOMING_USER_MESSAGE".to_string(),
+                }],
+                end_turn: None,
+                phase: None,
+            }]
+        );
+        assert!(
+            history
+                .raw_items()
+                .iter()
+                .any(Session::is_model_switch_developer_message)
+        );
+    }
+
     #[test]
     fn collect_user_messages_extracts_user_text_only() {
         let items = vec![

From 9d34abf9542d8cd471976c2e42b9bc1ffa69fd79 Mon Sep 17 00:00:00 2001
From: Charles Cunningham <ccunningham@openai.com>
Date: Mon, 16 Feb 2026 11:10:50 -0800
Subject: [PATCH 31/80] core: snapshot pre-turn model-switch compaction strip
 behavior

---
 codex-rs/core/tests/suite/compact.rs          |  5 ++---
 ...n_strips_incoming_model_switch_shapes.snap | 20 +++++++++----------
 2 files changed, 12 insertions(+), 13 deletions(-)

diff --git a/codex-rs/core/tests/suite/compact.rs b/codex-rs/core/tests/suite/compact.rs
index b29ba0184a45..f33d67624640 100644
--- a/codex-rs/core/tests/suite/compact.rs
+++ b/codex-rs/core/tests/suite/compact.rs
@@ -5,6 +5,7 @@ use codex_core::built_in_model_providers;
 use codex_core::compact::SUMMARIZATION_PROMPT;
 use codex_core::compact::SUMMARY_PREFIX;
 use codex_core::config::Config;
+use codex_core::features::Feature;
 use codex_core::protocol::AskForApproval;
 use codex_core::protocol::EventMsg;
 use codex_core::protocol::ItemCompletedEvent;
@@ -3210,9 +3211,7 @@ async fn snapshot_request_shape_pre_turn_compaction_strips_incoming_model_switch
         .with_config(move |config| {
             config.model_provider = model_provider;
             set_test_compact_prompt(config);
-            config
-                .features
-                .enable(codex_core::features::Feature::RemoteModels);
+            config.features.enable(Feature::RemoteModels);
             config.model_auto_compact_token_limit = Some(200);
         })
         .build(&server)
diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_turn_compaction_strips_incoming_model_switch_shapes.snap b/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_turn_compaction_strips_incoming_model_switch_shapes.snap
index 4d67af911d84..48b721f2c298 100644
--- a/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_turn_compaction_strips_incoming_model_switch_shapes.snap
+++ b/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_turn_compaction_strips_incoming_model_switch_shapes.snap
@@ -1,6 +1,5 @@
 ---
 source: core/tests/suite/compact.rs
-assertion_line: 3152
 expression: "format_labeled_requests_snapshot(\"Pre-turn compaction during model switch (without pre-sampling model-switch compaction): current behavior strips incoming <model_switch> from the compact request and restores it in the post-compaction follow-up request.\",\n&[(\"Initial Request (Previous Model)\", &requests[0]),\n(\"Local Compaction Request\", &requests[1]),\n(\"Local Post-Compaction History Layout\", &requests[2]),])"
 ---
 Scenario: Pre-turn compaction during model switch (without pre-sampling model-switch compaction): current behavior strips incoming <model_switch> from the compact request and restores it in the post-compaction follow-up request.
@@ -19,14 +18,15 @@ Scenario: Pre-turn compaction during model switch (without pre-sampling model-sw
 03:message/developer:<PERMISSIONS_INSTRUCTIONS>
 04:message/user:BEFORE_SWITCH_USER
 05:message/assistant:BEFORE_SWITCH_REPLY
-06:message/user:<SUMMARIZATION_PROMPT>
+06:message/user:AFTER_SWITCH_USER
+07:message/user:<SUMMARIZATION_PROMPT>
 
 ## Local Post-Compaction History Layout
-00:message/developer:<PERMISSIONS_INSTRUCTIONS>
-01:message/developer:<personality_spec> The user has requested a new communication st...
-02:message/user:<AGENTS_MD>
-03:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
-04:message/user:BEFORE_SWITCH_USER
-05:message/user:<COMPACTION_SUMMARY>\nPRETURN_SWITCH_SUMMARY
-06:message/developer:<model_switch>\nThe user was previously using a different model....
-07:message/user:AFTER_SWITCH_USER
+00:message/user:BEFORE_SWITCH_USER
+01:message/developer:<PERMISSIONS_INSTRUCTIONS>
+02:message/developer:<personality_spec> The user has requested a new communication st...
+03:message/user:<AGENTS_MD>
+04:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
+05:message/user:AFTER_SWITCH_USER
+06:message/user:<COMPACTION_SUMMARY>\nPRETURN_SWITCH_SUMMARY
+07:message/developer:<model_switch>\nThe user was previously using a different model....

From 713978497024e3b2163adfe2fee432fa61c4d987 Mon Sep 17 00:00:00 2001
From: Charles Cunningham <ccunningham@openai.com>
Date: Mon, 16 Feb 2026 15:11:37 -0800
Subject: [PATCH 32/80] Strip incoming model-switch updates from remote
 compaction input

---
 codex-rs/core/src/compact.rs                          |  2 +-
 codex-rs/core/src/compact_remote.rs                   | 11 ++++++++---
 codex-rs/core/tests/suite/compact_remote.rs           |  8 ++++----
 ...ompaction_strips_incoming_model_switch_shapes.snap | 11 ++++++-----
 4 files changed, 19 insertions(+), 13 deletions(-)

diff --git a/codex-rs/core/src/compact.rs b/codex-rs/core/src/compact.rs
index 9806d34d1ee6..4056deb4e2f5 100644
--- a/codex-rs/core/src/compact.rs
+++ b/codex-rs/core/src/compact.rs
@@ -88,7 +88,7 @@ pub(crate) fn extract_trailing_model_switch_update_for_compaction_request(
     Some(model_switch_item)
 }
 
-fn extract_latest_model_switch_update_from_items(
+pub(crate) fn extract_latest_model_switch_update_from_items(
     items: &mut Vec<ResponseItem>,
 ) -> Option<ResponseItem> {
     let model_switch_index = items
diff --git a/codex-rs/core/src/compact_remote.rs b/codex-rs/core/src/compact_remote.rs
index b338686ea053..4768abe0d6b8 100644
--- a/codex-rs/core/src/compact_remote.rs
+++ b/codex-rs/core/src/compact_remote.rs
@@ -5,6 +5,7 @@ use crate::codex::Session;
 use crate::codex::TurnContext;
 use crate::compact::AutoCompactCallsite;
 use crate::compact::TurnContextReinjection;
+use crate::compact::extract_latest_model_switch_update_from_items;
 use crate::compact::extract_trailing_model_switch_update_for_compaction_request;
 use crate::context_manager::ContextManager;
 use crate::context_manager::TotalTokenUsageBreakdown;
@@ -106,10 +107,14 @@ async fn run_remote_compact_task_inner_impl(
     sess.emit_turn_item_started(turn_context, &compaction_item)
         .await;
     let mut history = sess.clone_history().await;
+    let mut incoming_items = incoming_items;
     // Keep compaction prompts in-distribution: if a model-switch update was injected at the
-    // tail of history (between turns), exclude it from the compaction request payload.
-    let stripped_model_switch_item =
-        extract_trailing_model_switch_update_for_compaction_request(&mut history);
+    // tail of incoming turn items (pre-turn path) or between turns in history, exclude it from
+    // the compaction request payload.
+    let stripped_model_switch_item = incoming_items
+        .as_mut()
+        .and_then(extract_latest_model_switch_update_from_items)
+        .or_else(|| extract_trailing_model_switch_update_for_compaction_request(&mut history));
     let base_instructions = sess.get_base_instructions().await;
     let deleted_items = trim_function_call_history_to_fit_context_window(
         &mut history,
diff --git a/codex-rs/core/tests/suite/compact_remote.rs b/codex-rs/core/tests/suite/compact_remote.rs
index d6cd61121191..c99314a4077c 100644
--- a/codex-rs/core/tests/suite/compact_remote.rs
+++ b/codex-rs/core/tests/suite/compact_remote.rs
@@ -1509,8 +1509,8 @@ async fn snapshot_request_shape_remote_pre_turn_compaction_strips_incoming_model
     let post_compact_turn_request = post_compact_turn_request_mock.single_request();
     let compact_body = compact_request.body_json().to_string();
     assert!(
-        !compact_body.contains("AFTER_SWITCH_USER"),
-        "current behavior excludes incoming user from the pre-turn remote compaction request"
+        compact_body.contains("AFTER_SWITCH_USER"),
+        "pre-turn remote compaction request should include incoming user message"
     );
     assert!(
         !compact_body.contains("<model_switch>"),
@@ -1524,7 +1524,7 @@ async fn snapshot_request_shape_remote_pre_turn_compaction_strips_incoming_model
     );
     assert!(
         follow_up_body.contains("AFTER_SWITCH_USER"),
-        "post-compaction follow-up should preserve incoming user message via runtime append"
+        "post-compaction follow-up should preserve incoming user message"
     );
     assert!(
         follow_up_body.contains("<model_switch>"),
@@ -1534,7 +1534,7 @@ async fn snapshot_request_shape_remote_pre_turn_compaction_strips_incoming_model
     insta::assert_snapshot!(
         "remote_pre_turn_compaction_strips_incoming_model_switch_shapes",
         format_labeled_requests_snapshot(
-            "Remote pre-turn compaction during model switch currently excludes incoming user input, strips incoming <model_switch> from the compact request payload, and restores it in the post-compaction follow-up request.",
+            "Remote pre-turn compaction during model switch strips incoming <model_switch> from the compact request and restores it in the post-compaction follow-up request.",
             &[
                 ("Initial Request (Previous Model)", &initial_turn_request),
                 ("Remote Compaction Request", &compact_request),
diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_pre_turn_compaction_strips_incoming_model_switch_shapes.snap b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_pre_turn_compaction_strips_incoming_model_switch_shapes.snap
index fba17d719ed2..0a789f5b697f 100644
--- a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_pre_turn_compaction_strips_incoming_model_switch_shapes.snap
+++ b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_pre_turn_compaction_strips_incoming_model_switch_shapes.snap
@@ -1,8 +1,8 @@
 ---
 source: core/tests/suite/compact_remote.rs
-expression: "format_labeled_requests_snapshot(\"Remote pre-turn compaction during model switch currently excludes incoming user input, strips incoming <model_switch> from the compact request payload, and restores it in the post-compaction follow-up request.\",\n&[(\"Initial Request (Previous Model)\", &requests[0]),\n(\"Remote Compaction Request\", &compact_request),\n(\"Remote Post-Compaction History Layout\", &requests[1]),])"
+expression: "format_labeled_requests_snapshot(\"Remote pre-turn compaction during model switch strips incoming <model_switch> from the compact request and restores it in the post-compaction follow-up request.\",\n&[(\"Initial Request (Previous Model)\", &requests[0]),\n(\"Remote Compaction Request\", &compact_request),\n(\"Remote Post-Compaction History Layout\", &requests[1]),])"
 ---
-Scenario: Remote pre-turn compaction during model switch currently excludes incoming user input, strips incoming <model_switch> from the compact request payload, and restores it in the post-compaction follow-up request.
+Scenario: Remote pre-turn compaction during model switch strips incoming <model_switch> from the compact request and restores it in the post-compaction follow-up request.
 
 ## Initial Request (Previous Model)
 00:message/developer:<PERMISSIONS_INSTRUCTIONS>
@@ -16,6 +16,7 @@ Scenario: Remote pre-turn compaction during model switch currently excludes inco
 02:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
 03:message/user:BEFORE_SWITCH_USER
 04:message/assistant:BEFORE_SWITCH_REPLY
+05:message/user:AFTER_SWITCH_USER
 
 ## Remote Post-Compaction History Layout
 00:message/user:BEFORE_SWITCH_USER
@@ -23,6 +24,6 @@ Scenario: Remote pre-turn compaction during model switch currently excludes inco
 02:message/developer:<personality_spec> The user has requested a new communication st...
 03:message/user:<AGENTS_MD>
 04:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
-05:message/user:<COMPACTION_SUMMARY>\nREMOTE_SWITCH_SUMMARY
-06:message/developer:<model_switch>\nThe user was previously using a different model....
-07:message/user:AFTER_SWITCH_USER
+05:message/user:AFTER_SWITCH_USER
+06:message/user:<COMPACTION_SUMMARY>\nREMOTE_SWITCH_SUMMARY
+07:message/developer:<model_switch>\nThe user was previously using a different model....

From d81d89ac0be54cb1bd759f1c149d82819b5e4442 Mon Sep 17 00:00:00 2001
From: Charles Cunningham <ccunningham@openai.com>
Date: Mon, 16 Feb 2026 22:32:38 -0800
Subject: [PATCH 33/80] Silence interrupted pre-turn compaction failures

---
 codex-rs/core/src/codex.rs | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs
index 8fad914a2477..97e5cb3c33c7 100644
--- a/codex-rs/core/src/codex.rs
+++ b/codex-rs/core/src/codex.rs
@@ -4695,6 +4695,7 @@ async fn maybe_run_previous_model_inline_compact(
     .await
     {
         Ok(()) => Ok(()),
+        Err(CodexErr::Interrupted) => Err(()),
         Err(err) => {
             if !pre_turn_context_items.is_empty() {
                 // Preserve model-visible settings updates even when pre-turn compaction fails
@@ -4806,6 +4807,9 @@ async fn run_pre_turn_auto_compaction_if_needed(
     .await;
 
     if let Err(err) = compact_result {
+        if matches!(err, CodexErr::Interrupted) {
+            return Err(());
+        }
         if !pre_turn_context_items.is_empty() {
             // Preserve model-visible settings updates even when pre-turn compaction fails
             // before we can persist turn input.

From f7956ffabc3d39b7e51c37d0a35bf3027c5e25f6 Mon Sep 17 00:00:00 2001
From: Charles Cunningham <ccunningham@openai.com>
Date: Mon, 16 Feb 2026 22:49:56 -0800
Subject: [PATCH 34/80] Persist pre-turn updates before interrupted
 model-switch compact exits

---
 codex-rs/core/src/codex.rs | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs
index 97e5cb3c33c7..b0f4703a3246 100644
--- a/codex-rs/core/src/codex.rs
+++ b/codex-rs/core/src/codex.rs
@@ -4695,7 +4695,6 @@ async fn maybe_run_previous_model_inline_compact(
     .await
     {
         Ok(()) => Ok(()),
-        Err(CodexErr::Interrupted) => Err(()),
         Err(err) => {
             if !pre_turn_context_items.is_empty() {
                 // Preserve model-visible settings updates even when pre-turn compaction fails
@@ -4703,6 +4702,9 @@ async fn maybe_run_previous_model_inline_compact(
                 sess.record_conversation_items(turn_context, pre_turn_context_items)
                     .await;
             }
+            if matches!(err, CodexErr::Interrupted) {
+                return Err(());
+            }
             let compact_error_prefix = if should_use_remote_compact_task(&turn_context.provider) {
                 "Error running remote compact task"
             } else {

From ccc270083fab40f3973ca40feee3cdcf0337fef0 Mon Sep 17 00:00:00 2001
From: Charles Cunningham <ccunningham@openai.com>
Date: Mon, 16 Feb 2026 22:50:54 -0800
Subject: [PATCH 35/80] Persist pre-turn updates before interrupted
 auto-compaction exits

---
 codex-rs/core/src/codex.rs | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs
index b0f4703a3246..c17184bf5d72 100644
--- a/codex-rs/core/src/codex.rs
+++ b/codex-rs/core/src/codex.rs
@@ -4809,15 +4809,15 @@ async fn run_pre_turn_auto_compaction_if_needed(
     .await;
 
     if let Err(err) = compact_result {
-        if matches!(err, CodexErr::Interrupted) {
-            return Err(());
-        }
         if !pre_turn_context_items.is_empty() {
             // Preserve model-visible settings updates even when pre-turn compaction fails
             // before we can persist turn input.
             sess.record_conversation_items(turn_context, pre_turn_context_items)
                 .await;
         }
+        if matches!(err, CodexErr::Interrupted) {
+            return Err(());
+        }
         let event = match err {
             CodexErr::ContextWindowExceeded => {
                 error!(

From 3f2f817ab675ca1e35752743c839018554cae30c Mon Sep 17 00:00:00 2001
From: Charles Cunningham <ccunningham@openai.com>
Date: Tue, 17 Feb 2026 10:54:56 -0800
Subject: [PATCH 36/80] Make compact unit snapshots Bazel-stable

---
 codex-rs/core/src/compact.rs | 17 +++++++++++++----
 1 file changed, 13 insertions(+), 4 deletions(-)

diff --git a/codex-rs/core/src/compact.rs b/codex-rs/core/src/compact.rs
index 4056deb4e2f5..314f7d78fed5 100644
--- a/codex-rs/core/src/compact.rs
+++ b/codex-rs/core/src/compact.rs
@@ -593,6 +593,15 @@ mod tests {
     use insta::assert_snapshot;
     use pretty_assertions::assert_eq;
     use serde_json::Value;
+    use std::path::Path;
+
+    fn assert_compact_snapshot(name: &str, rendered: String) {
+        let mut settings = insta::Settings::clone_current();
+        settings.set_snapshot_path(Path::new(env!("CARGO_MANIFEST_DIR")).join("src/snapshots"));
+        settings.bind(|| {
+            assert_snapshot!(name, rendered);
+        });
+    }
 
     #[test]
     fn content_items_to_text_joins_non_empty_segments() {
@@ -1686,14 +1695,14 @@ keep me updated
             panic!("expected refreshed history to serialize as array");
         };
 
-        assert_snapshot!(
+        assert_compact_snapshot(
             "process_compacted_history_reinject_summary_only_shapes",
             context_snapshot::format_labeled_items_snapshot(
                 "When compaction output contains only a summary user message, canonical context is still reinserted before the summary.",
                 &[("Refreshed History Layout", refreshed_items.as_slice())],
                 &ContextSnapshotOptions::default()
                     .render_mode(ContextSnapshotRenderMode::KindWithTextPrefix { max_chars: 64 }),
-            )
+            ),
         );
     }
 
@@ -1771,14 +1780,14 @@ keep me updated
             panic!("expected refreshed history to serialize as array");
         };
 
-        assert_snapshot!(
+        assert_compact_snapshot(
             "process_compacted_history_reinject_above_last_summary_shapes",
             context_snapshot::format_labeled_items_snapshot(
                 "When compaction output has multiple summary-only user messages and no real user message, canonical context is reinserted above the last summary.",
                 &[("Refreshed History Layout", refreshed_items.as_slice())],
                 &ContextSnapshotOptions::default()
                     .render_mode(ContextSnapshotRenderMode::KindWithTextPrefix { max_chars: 64 }),
-            )
+            ),
         );
     }
 

From 907e292ad970c9024d71134c369e838c9a58eb0f Mon Sep 17 00:00:00 2001
From: Charles Cunningham <ccunningham@openai.com>
Date: Tue, 17 Feb 2026 11:02:54 -0800
Subject: [PATCH 37/80] Add comment

---
 codex-rs/core/src/compact.rs | 1 +
 1 file changed, 1 insertion(+)

diff --git a/codex-rs/core/src/compact.rs b/codex-rs/core/src/compact.rs
index 314f7d78fed5..903839fdbd74 100644
--- a/codex-rs/core/src/compact.rs
+++ b/codex-rs/core/src/compact.rs
@@ -595,6 +595,7 @@ mod tests {
     use serde_json::Value;
     use std::path::Path;
 
+    // This is to make Bazel happy
     fn assert_compact_snapshot(name: &str, rendered: String) {
         let mut settings = insta::Settings::clone_current();
         settings.set_snapshot_path(Path::new(env!("CARGO_MANIFEST_DIR")).join("src/snapshots"));

From 04b157bb4c80c0161b3ae0c137c7de9183e840d3 Mon Sep 17 00:00:00 2001
From: Charles Cunningham <ccunningham@openai.com>
Date: Tue, 17 Feb 2026 13:30:41 -0800
Subject: [PATCH 38/80] Treat no-op pre-turn compaction as not-needed

---
 codex-rs/core/src/codex.rs | 57 ++++++++++++++++++++++++++++++++++++++
 1 file changed, 57 insertions(+)

diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs
index c17184bf5d72..0f9274afd8dc 100644
--- a/codex-rs/core/src/codex.rs
+++ b/codex-rs/core/src/codex.rs
@@ -4847,6 +4847,19 @@ async fn run_pre_turn_auto_compaction_if_needed(
         return Err(());
     }
 
+    // If compaction no-oped because there was no user-turn boundary even after including incoming
+    // items, do not treat this as "compacted with incoming". The caller must persist incoming
+    // items explicitly in this case.
+    let has_user_turn_boundary_after_compaction = sess
+        .clone_history()
+        .await
+        .raw_items()
+        .iter()
+        .any(crate::context_manager::is_user_turn_boundary);
+    if !has_user_turn_boundary_after_compaction {
+        return Ok(PreTurnCompactionOutcome::NotNeeded);
+    }
+
     Ok(PreTurnCompactionOutcome::CompactedWithIncomingItems)
 }
 
@@ -6217,6 +6230,50 @@ mod tests {
         assert_eq!(actual_history, expected_history);
     }
 
+    #[tokio::test]
+    async fn pre_turn_auto_compaction_noop_without_user_turn_boundary_returns_not_needed() {
+        let (session, turn_context) = make_session_and_context().await;
+        let session = Arc::new(session);
+        let turn_context = Arc::new(turn_context);
+
+        {
+            let mut state = session.state.lock().await;
+            state.set_token_info(Some(TokenUsageInfo {
+                total_token_usage: TokenUsage {
+                    total_tokens: 1_000,
+                    ..TokenUsage::default()
+                },
+                last_token_usage: TokenUsage {
+                    total_tokens: 1_000,
+                    ..TokenUsage::default()
+                },
+                model_context_window: turn_context.model_context_window(),
+            }));
+        }
+
+        let incoming_turn_items = vec![ResponseItem::Message {
+            id: None,
+            role: "user".to_string(),
+            content: vec![ContentItem::InputText {
+                text: "<user_shell_command>\necho hi\n</user_shell_command>".to_string(),
+            }],
+            end_turn: None,
+            phase: None,
+        }];
+        let outcome = run_pre_turn_auto_compaction_if_needed(
+            &session,
+            &turn_context,
+            10,
+            &incoming_turn_items,
+            &[],
+        )
+        .await
+        .expect("pre-turn compaction no-op should not fail");
+
+        assert_eq!(outcome, PreTurnCompactionOutcome::NotNeeded);
+        assert_eq!(session.clone_history().await.raw_items(), &[]);
+    }
+
     #[test]
     fn estimate_user_input_token_count_is_positive_for_text_input() {
         let input = vec![UserInput::Text {

From a38b1c9cf5153546c29624986b6582f3b6a6d6ba Mon Sep 17 00:00:00 2001
From: Charles Cunningham <ccunningham@openai.com>
Date: Tue, 17 Feb 2026 13:34:14 -0800
Subject: [PATCH 39/80] Move compact reinjection snapshots into suite coverage

---
 codex-rs/core/src/compact.rs                  | 144 ------------------
 ...ry_reinject_above_last_summary_shapes.snap |  10 --
 ..._history_reinject_summary_only_shapes.snap |   9 --
 codex-rs/core/tests/suite/compact_remote.rs   |   1 -
 4 files changed, 164 deletions(-)
 delete mode 100644 codex-rs/core/src/snapshots/codex_core__compact__tests__process_compacted_history_reinject_above_last_summary_shapes.snap
 delete mode 100644 codex-rs/core/src/snapshots/codex_core__compact__tests__process_compacted_history_reinject_summary_only_shapes.snap

diff --git a/codex-rs/core/src/compact.rs b/codex-rs/core/src/compact.rs
index 903839fdbd74..1013e87b9020 100644
--- a/codex-rs/core/src/compact.rs
+++ b/codex-rs/core/src/compact.rs
@@ -585,24 +585,8 @@ async fn drain_to_completed(
 
 #[cfg(test)]
 mod tests {
-
     use super::*;
-    use core_test_support::context_snapshot;
-    use core_test_support::context_snapshot::ContextSnapshotOptions;
-    use core_test_support::context_snapshot::ContextSnapshotRenderMode;
-    use insta::assert_snapshot;
     use pretty_assertions::assert_eq;
-    use serde_json::Value;
-    use std::path::Path;
-
-    // This is to make Bazel happy
-    fn assert_compact_snapshot(name: &str, rendered: String) {
-        let mut settings = insta::Settings::clone_current();
-        settings.set_snapshot_path(Path::new(env!("CARGO_MANIFEST_DIR")).join("src/snapshots"));
-        settings.bind(|| {
-            assert_snapshot!(name, rendered);
-        });
-    }
 
     #[test]
     fn content_items_to_text_joins_non_empty_segments() {
@@ -1664,134 +1648,6 @@ keep me updated
         assert_eq!(refreshed, expected);
     }
 
-    #[test]
-    fn process_compacted_history_reinjects_context_when_compaction_output_is_summary_only() {
-        let compacted_history = vec![ResponseItem::Message {
-            id: None,
-            role: "user".to_string(),
-            content: vec![ContentItem::InputText {
-                text: format!("{SUMMARY_PREFIX}\nsummary text"),
-            }],
-            end_turn: None,
-            phase: None,
-        }];
-        let initial_context = vec![ResponseItem::Message {
-            id: None,
-            role: "developer".to_string(),
-            content: vec![ContentItem::InputText {
-                text: "fresh permissions".to_string(),
-            }],
-            end_turn: None,
-            phase: None,
-        }];
-
-        let refreshed = process_compacted_history(
-            compacted_history,
-            &initial_context,
-            TurnContextReinjection::ReinjectAboveLastRealUser,
-        );
-        let refreshed_value =
-            serde_json::to_value(&refreshed).expect("serialize refreshed history");
-        let Value::Array(refreshed_items) = refreshed_value else {
-            panic!("expected refreshed history to serialize as array");
-        };
-
-        assert_compact_snapshot(
-            "process_compacted_history_reinject_summary_only_shapes",
-            context_snapshot::format_labeled_items_snapshot(
-                "When compaction output contains only a summary user message, canonical context is still reinserted before the summary.",
-                &[("Refreshed History Layout", refreshed_items.as_slice())],
-                &ContextSnapshotOptions::default()
-                    .render_mode(ContextSnapshotRenderMode::KindWithTextPrefix { max_chars: 64 }),
-            ),
-        );
-    }
-
-    #[test]
-    fn process_compacted_history_reinjects_context_above_last_summary_when_no_real_user() {
-        let compacted_history = vec![
-            ResponseItem::Message {
-                id: None,
-                role: "user".to_string(),
-                content: vec![ContentItem::InputText {
-                    text: format!("{SUMMARY_PREFIX}\nolder summary"),
-                }],
-                end_turn: None,
-                phase: None,
-            },
-            ResponseItem::Message {
-                id: None,
-                role: "user".to_string(),
-                content: vec![ContentItem::InputText {
-                    text: format!("{SUMMARY_PREFIX}\nlatest summary"),
-                }],
-                end_turn: None,
-                phase: None,
-            },
-        ];
-        let initial_context = vec![ResponseItem::Message {
-            id: None,
-            role: "developer".to_string(),
-            content: vec![ContentItem::InputText {
-                text: "fresh permissions".to_string(),
-            }],
-            end_turn: None,
-            phase: None,
-        }];
-
-        let refreshed = process_compacted_history(
-            compacted_history,
-            &initial_context,
-            TurnContextReinjection::ReinjectAboveLastRealUser,
-        );
-        let expected = vec![
-            ResponseItem::Message {
-                id: None,
-                role: "user".to_string(),
-                content: vec![ContentItem::InputText {
-                    text: format!("{SUMMARY_PREFIX}\nolder summary"),
-                }],
-                end_turn: None,
-                phase: None,
-            },
-            ResponseItem::Message {
-                id: None,
-                role: "developer".to_string(),
-                content: vec![ContentItem::InputText {
-                    text: "fresh permissions".to_string(),
-                }],
-                end_turn: None,
-                phase: None,
-            },
-            ResponseItem::Message {
-                id: None,
-                role: "user".to_string(),
-                content: vec![ContentItem::InputText {
-                    text: format!("{SUMMARY_PREFIX}\nlatest summary"),
-                }],
-                end_turn: None,
-                phase: None,
-            },
-        ];
-        assert_eq!(refreshed, expected);
-
-        let refreshed_value =
-            serde_json::to_value(&refreshed).expect("serialize refreshed history");
-        let Value::Array(refreshed_items) = refreshed_value else {
-            panic!("expected refreshed history to serialize as array");
-        };
-
-        assert_compact_snapshot(
-            "process_compacted_history_reinject_above_last_summary_shapes",
-            context_snapshot::format_labeled_items_snapshot(
-                "When compaction output has multiple summary-only user messages and no real user message, canonical context is reinserted above the last summary.",
-                &[("Refreshed History Layout", refreshed_items.as_slice())],
-                &ContextSnapshotOptions::default()
-                    .render_mode(ContextSnapshotRenderMode::KindWithTextPrefix { max_chars: 64 }),
-            ),
-        );
-    }
-
     #[test]
     fn process_compacted_history_mid_turn_without_orphan_user_places_summary_last() {
         let compacted_history = vec![
diff --git a/codex-rs/core/src/snapshots/codex_core__compact__tests__process_compacted_history_reinject_above_last_summary_shapes.snap b/codex-rs/core/src/snapshots/codex_core__compact__tests__process_compacted_history_reinject_above_last_summary_shapes.snap
deleted file mode 100644
index 906f79b55f81..000000000000
--- a/codex-rs/core/src/snapshots/codex_core__compact__tests__process_compacted_history_reinject_above_last_summary_shapes.snap
+++ /dev/null
@@ -1,10 +0,0 @@
----
-source: core/src/compact.rs
-expression: "context_snapshot::format_labeled_items_snapshot(\"When compaction output has multiple summary-only user messages and no real user message, canonical context is reinserted above the last summary.\",\n&[(\"Refreshed History Layout\", refreshed_items.as_slice())],\n&ContextSnapshotOptions::default().render_mode(ContextSnapshotRenderMode::KindWithTextPrefix\n{ max_chars: 64 }),)"
----
-Scenario: When compaction output has multiple summary-only user messages and no real user message, canonical context is reinserted above the last summary.
-
-## Refreshed History Layout
-00:message/user:<COMPACTION_SUMMARY>\nolder summary
-01:message/developer:fresh permissions
-02:message/user:<COMPACTION_SUMMARY>\nlatest summary
diff --git a/codex-rs/core/src/snapshots/codex_core__compact__tests__process_compacted_history_reinject_summary_only_shapes.snap b/codex-rs/core/src/snapshots/codex_core__compact__tests__process_compacted_history_reinject_summary_only_shapes.snap
deleted file mode 100644
index aba7c96a6d7f..000000000000
--- a/codex-rs/core/src/snapshots/codex_core__compact__tests__process_compacted_history_reinject_summary_only_shapes.snap
+++ /dev/null
@@ -1,9 +0,0 @@
----
-source: core/src/compact.rs
-expression: "context_snapshot::format_labeled_items_snapshot(\"When compaction output contains only a summary user message, canonical context is still reinserted before the summary.\",\n&[(\"Refreshed History Layout\", refreshed_items.as_slice())],\n&ContextSnapshotOptions::default().render_mode(ContextSnapshotRenderMode::KindWithTextPrefix\n{ max_chars: 64 }),)"
----
-Scenario: When compaction output contains only a summary user message, canonical context is still reinserted before the summary.
-
-## Refreshed History Layout
-00:message/developer:fresh permissions
-01:message/user:<COMPACTION_SUMMARY>\nsummary text
diff --git a/codex-rs/core/tests/suite/compact_remote.rs b/codex-rs/core/tests/suite/compact_remote.rs
index c99314a4077c..c815adcf7653 100644
--- a/codex-rs/core/tests/suite/compact_remote.rs
+++ b/codex-rs/core/tests/suite/compact_remote.rs
@@ -1909,7 +1909,6 @@ async fn snapshot_request_shape_remote_mid_turn_compaction_multi_summary_reinjec
 }
 
 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
-// TODO(ccunningham): Update once manual remote /compact with no prior user turn becomes a no-op.
 async fn snapshot_request_shape_remote_manual_compact_without_previous_user_messages() -> Result<()>
 {
     skip_if_no_network!(Ok(()));

From 15aa101bf67162f72eba0bf3034bffa0fcfc11b4 Mon Sep 17 00:00:00 2001
From: Charles Cunningham <ccunningham@openai.com>
Date: Tue, 17 Feb 2026 14:22:07 -0800
Subject: [PATCH 40/80] Qualify Feature enum in compact suite tests

---
 codex-rs/core/tests/suite/compact.rs | 8 ++++++--
 1 file changed, 6 insertions(+), 2 deletions(-)

diff --git a/codex-rs/core/tests/suite/compact.rs b/codex-rs/core/tests/suite/compact.rs
index f33d67624640..5493201b4f29 100644
--- a/codex-rs/core/tests/suite/compact.rs
+++ b/codex-rs/core/tests/suite/compact.rs
@@ -1844,7 +1844,9 @@ async fn pre_sampling_compact_context_window_failure_surfaces_compact_task_error
         .with_config(move |config| {
             config.model_provider = model_provider;
             set_test_compact_prompt(config);
-            config.features.enable(Feature::RemoteModels);
+            config
+                .features
+                .enable(codex_core::features::Feature::RemoteModels);
         });
     let test = builder.build(&server).await.expect("build test codex");
 
@@ -3211,7 +3213,9 @@ async fn snapshot_request_shape_pre_turn_compaction_strips_incoming_model_switch
         .with_config(move |config| {
             config.model_provider = model_provider;
             set_test_compact_prompt(config);
-            config.features.enable(Feature::RemoteModels);
+            config
+                .features
+                .enable(codex_core::features::Feature::RemoteModels);
             config.model_auto_compact_token_limit = Some(200);
         })
         .build(&server)

From 3964becfde128eeb290ae25bfa17589c4273006c Mon Sep 17 00:00:00 2001
From: Charles Cunningham <ccunningham@openai.com>
Date: Tue, 17 Feb 2026 17:55:24 -0800
Subject: [PATCH 41/80] dd comment

---
 codex-rs/core/src/codex.rs | 4 ----
 1 file changed, 4 deletions(-)

diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs
index 0f9274afd8dc..b40bfdb39697 100644
--- a/codex-rs/core/src/codex.rs
+++ b/codex-rs/core/src/codex.rs
@@ -2387,10 +2387,6 @@ impl Session {
                     if let Some(replacement) = &compacted.replacement_history {
                         history.replace(replacement.clone());
                     } else {
-                        // TODO(ccunningham): When we have TurnContextItem-based legacy reconstruction,
-                        // build historical turn context from those items and inject it at the correct
-                        // point in compacted history instead of prepending the current initial context.
-                        // This matters for legacy rollouts with replacement_history=None.
                         let user_messages = collect_user_messages(history.raw_items());
                         let mut rebuilt = self.build_initial_context(turn_context).await;
                         rebuilt.extend(compact::build_compacted_history(

From 589b0763cca57eab764cab557bd976fc55fd825d Mon Sep 17 00:00:00 2001
From: Charles Cunningham <ccunningham@openai.com>
Date: Tue, 17 Feb 2026 18:01:10 -0800
Subject: [PATCH 42/80] Fix compact test snapshot and include bazel lock update

---
 codex-rs/core/tests/suite/compact.rs                |  1 -
 ...summary_reinjects_above_last_summary_shapes.snap | 13 +++++++------
 2 files changed, 7 insertions(+), 7 deletions(-)

diff --git a/codex-rs/core/tests/suite/compact.rs b/codex-rs/core/tests/suite/compact.rs
index 5493201b4f29..17066c3c92c6 100644
--- a/codex-rs/core/tests/suite/compact.rs
+++ b/codex-rs/core/tests/suite/compact.rs
@@ -5,7 +5,6 @@ use codex_core::built_in_model_providers;
 use codex_core::compact::SUMMARIZATION_PROMPT;
 use codex_core::compact::SUMMARY_PREFIX;
 use codex_core::config::Config;
-use codex_core::features::Feature;
 use codex_core::protocol::AskForApproval;
 use codex_core::protocol::EventMsg;
 use codex_core::protocol::ItemCompletedEvent;
diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_mid_turn_compaction_multi_summary_reinjects_above_last_summary_shapes.snap b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_mid_turn_compaction_multi_summary_reinjects_above_last_summary_shapes.snap
index 1f51f965440a..9ea6233a0a1b 100644
--- a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_mid_turn_compaction_multi_summary_reinjects_above_last_summary_shapes.snap
+++ b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_mid_turn_compaction_multi_summary_reinjects_above_last_summary_shapes.snap
@@ -10,12 +10,13 @@ Scenario: Remote mid-turn compaction after an earlier summary compaction: the ol
 02:message/developer:<PERMISSIONS_INSTRUCTIONS>
 03:message/user:<AGENTS_MD>
 04:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
-05:message/user:<COMPACTION_SUMMARY>\nREMOTE_LATEST_SUMMARY
-06:message/user:USER_TWO
+05:message/user:USER_TWO
+06:message/user:<COMPACTION_SUMMARY>\nREMOTE_LATEST_SUMMARY
 
 ## Remote Compaction Request
 00:message/user:USER_ONE
-01:message/developer:<PERMISSIONS_INSTRUCTIONS>
-02:message/user:<AGENTS_MD>
-03:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
-04:message/user:<COMPACTION_SUMMARY>\nREMOTE_OLDER_SUMMARY
+01:message/user:<COMPACTION_SUMMARY>\nREMOTE_OLDER_SUMMARY
+02:message/developer:<PERMISSIONS_INSTRUCTIONS>
+03:message/user:<AGENTS_MD>
+04:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
+05:message/user:USER_TWO

From 01794f1a462639e3592593a6d140001e4f94c0e1 Mon Sep 17 00:00:00 2001
From: Charles Cunningham <ccunningham@openai.com>
Date: Tue, 17 Feb 2026 18:46:04 -0800
Subject: [PATCH 43/80] Reuse compaction keep filter for incoming items

---
 codex-rs/core/src/compact.rs | 100 +++++++++++++++++++++++++++++------
 1 file changed, 84 insertions(+), 16 deletions(-)

diff --git a/codex-rs/core/src/compact.rs b/codex-rs/core/src/compact.rs
index 1013e87b9020..16deb18f20d6 100644
--- a/codex-rs/core/src/compact.rs
+++ b/codex-rs/core/src/compact.rs
@@ -18,6 +18,7 @@ use crate::protocol::WarningEvent;
 use crate::truncate::TruncationPolicy;
 use crate::truncate::approx_token_count;
 use crate::truncate::truncate_text;
+use crate::user_shell_command::is_user_shell_command_text;
 use crate::util::backoff;
 use codex_protocol::items::ContextCompactionItem;
 use codex_protocol::items::TurnItem;
@@ -303,7 +304,9 @@ async fn run_compact_task_inner(
     let incoming_user_items = match incoming_items.as_ref() {
         Some(items) => items
             .iter()
-            .filter(|item| is_non_summary_user_message(item))
+            .filter(|item| {
+                should_keep_compacted_history_item(item) && !is_summary_user_message_item(item)
+            })
             .cloned()
             .collect(),
         None => Vec::new(),
@@ -433,13 +436,14 @@ pub(crate) fn process_compacted_history(
 }
 
 fn is_non_summary_user_message(item: &ResponseItem) -> bool {
-    match crate::event_mapping::parse_turn_item(item) {
-        Some(TurnItem::UserMessage(user_message)) => {
-            let message = user_message.message();
-            !is_summary_message(&message)
-        }
-        _ => false,
-    }
+    should_keep_compacted_history_item(item) && !is_summary_user_message_item(item)
+}
+
+fn is_summary_user_message_item(item: &ResponseItem) -> bool {
+    matches!(
+        crate::event_mapping::parse_turn_item(item),
+        Some(TurnItem::UserMessage(user_message)) if is_summary_message(&user_message.message())
+    )
 }
 
 /// Returns whether an item from remote compaction output should be preserved.
@@ -451,18 +455,39 @@ fn is_non_summary_user_message(item: &ResponseItem) -> bool {
 /// - `developer` messages because remote output can include stale/duplicated
 ///   instruction content.
 /// - non-user-content `user` messages (session prefix/instruction wrappers),
-///   keeping only real user messages as parsed by `parse_turn_item`.
+///   keeping real user messages plus user shell-command records.
 ///
 /// This intentionally keeps `user`-role warnings and compaction-generated
-/// summary messages because they parse as `TurnItem::UserMessage`.
+/// summary messages because they parse as `TurnItem::UserMessage`, and keeps
+/// `<user_shell_command>` user records for shell-execution continuity.
 fn should_keep_compacted_history_item(item: &ResponseItem) -> bool {
     match item {
-        ResponseItem::Message { role, .. } if role == "developer" => false,
-        ResponseItem::Message { role, .. } if role == "user" => matches!(
-            crate::event_mapping::parse_turn_item(item),
-            Some(TurnItem::UserMessage(_))
-        ),
-        _ => true,
+        ResponseItem::Message { role, content, .. } => {
+            if role == "developer" {
+                return false;
+            }
+            if role != "user" {
+                return true;
+            }
+
+            matches!(
+                crate::event_mapping::parse_turn_item(item),
+                Some(TurnItem::UserMessage(_))
+            ) || matches!(
+                content.as_slice(),
+                [ContentItem::InputText { text }] if is_user_shell_command_text(text)
+            )
+        }
+        ResponseItem::Reasoning { .. }
+        | ResponseItem::LocalShellCall { .. }
+        | ResponseItem::FunctionCall { .. }
+        | ResponseItem::FunctionCallOutput { .. }
+        | ResponseItem::CustomToolCall { .. }
+        | ResponseItem::CustomToolCallOutput { .. }
+        | ResponseItem::WebSearchCall { .. }
+        | ResponseItem::GhostSnapshot { .. }
+        | ResponseItem::Compaction { .. }
+        | ResponseItem::Other => true,
     }
 }
 
@@ -1006,6 +1031,49 @@ do things
         assert!(super::is_non_summary_user_message(&image_only_user));
     }
 
+    #[test]
+    fn non_summary_user_message_includes_user_shell_command_records() {
+        let shell_command_user = ResponseItem::Message {
+            id: None,
+            role: "user".to_string(),
+            content: vec![ContentItem::InputText {
+                text: "<user_shell_command>\necho hi\n</user_shell_command>".to_string(),
+            }],
+            end_turn: None,
+            phase: None,
+        };
+
+        assert!(super::is_non_summary_user_message(&shell_command_user));
+    }
+
+    #[test]
+    fn should_keep_compacted_history_item_drops_user_session_prefix_but_keeps_user_shell_command() {
+        let session_prefix = ResponseItem::Message {
+            id: None,
+            role: "user".to_string(),
+            content: vec![ContentItem::InputText {
+                text: "<environment_context>\n  <cwd>/repo</cwd>\n</environment_context>"
+                    .to_string(),
+            }],
+            end_turn: None,
+            phase: None,
+        };
+        let shell_command_user = ResponseItem::Message {
+            id: None,
+            role: "user".to_string(),
+            content: vec![ContentItem::InputText {
+                text: "<user_shell_command>\necho hi\n</user_shell_command>".to_string(),
+            }],
+            end_turn: None,
+            phase: None,
+        };
+
+        assert!(!super::should_keep_compacted_history_item(&session_prefix));
+        assert!(super::should_keep_compacted_history_item(
+            &shell_command_user
+        ));
+    }
+
     #[test]
     fn process_compacted_history_replaces_developer_messages() {
         let compacted_history = vec![

From 6e6e7187388ef44962a2b233f7902c43a52b6b84 Mon Sep 17 00:00:00 2001
From: Charles Cunningham <ccunningham@openai.com>
Date: Tue, 17 Feb 2026 18:47:38 -0800
Subject: [PATCH 44/80] Use real user predicate for compaction reinjection

---
 codex-rs/core/src/compact.rs | 19 ++++++++-----------
 1 file changed, 8 insertions(+), 11 deletions(-)

diff --git a/codex-rs/core/src/compact.rs b/codex-rs/core/src/compact.rs
index 16deb18f20d6..b85a08fa418e 100644
--- a/codex-rs/core/src/compact.rs
+++ b/codex-rs/core/src/compact.rs
@@ -304,9 +304,7 @@ async fn run_compact_task_inner(
     let incoming_user_items = match incoming_items.as_ref() {
         Some(items) => items
             .iter()
-            .filter(|item| {
-                should_keep_compacted_history_item(item) && !is_summary_user_message_item(item)
-            })
+            .filter(|item| is_real_user_message(item))
             .cloned()
             .collect(),
         None => Vec::new(),
@@ -411,9 +409,8 @@ pub(crate) fn process_compacted_history(
             // applies to that user input rather than an earlier turn. If compaction output has no
             // real user messages, insert before the last summary user message to keep canonical
             // context present for the next sampling request.
-            let insertion_index = if let Some(last_real_user_index) = compacted_history
-                .iter()
-                .rposition(is_non_summary_user_message)
+            let insertion_index = if let Some(last_real_user_index) =
+                compacted_history.iter().rposition(is_real_user_message)
             {
                 last_real_user_index
             } else if let Some(last_summary_index) = compacted_history.iter().rposition(|item| {
@@ -435,7 +432,7 @@ pub(crate) fn process_compacted_history(
     compacted_history
 }
 
-fn is_non_summary_user_message(item: &ResponseItem) -> bool {
+fn is_real_user_message(item: &ResponseItem) -> bool {
     should_keep_compacted_history_item(item) && !is_summary_user_message_item(item)
 }
 
@@ -1017,7 +1014,7 @@ do things
     }
 
     #[test]
-    fn non_summary_user_message_includes_image_only_user_messages() {
+    fn real_user_message_includes_image_only_user_messages() {
         let image_only_user = ResponseItem::Message {
             id: None,
             role: "user".to_string(),
@@ -1028,11 +1025,11 @@ do things
             phase: None,
         };
 
-        assert!(super::is_non_summary_user_message(&image_only_user));
+        assert!(super::is_real_user_message(&image_only_user));
     }
 
     #[test]
-    fn non_summary_user_message_includes_user_shell_command_records() {
+    fn real_user_message_includes_user_shell_command_records() {
         let shell_command_user = ResponseItem::Message {
             id: None,
             role: "user".to_string(),
@@ -1043,7 +1040,7 @@ do things
             phase: None,
         };
 
-        assert!(super::is_non_summary_user_message(&shell_command_user));
+        assert!(super::is_real_user_message(&shell_command_user));
     }
 
     #[test]

From afebfffc93ad8fb15ae34bd6124d2990e285a22f Mon Sep 17 00:00:00 2001
From: Charles Cunningham <ccunningham@openai.com>
Date: Tue, 17 Feb 2026 18:49:38 -0800
Subject: [PATCH 45/80] Exclude user shell command records from compaction keep
 set

---
 codex-rs/core/src/compact.rs | 19 +++++++------------
 1 file changed, 7 insertions(+), 12 deletions(-)

diff --git a/codex-rs/core/src/compact.rs b/codex-rs/core/src/compact.rs
index b85a08fa418e..1af23fda98cb 100644
--- a/codex-rs/core/src/compact.rs
+++ b/codex-rs/core/src/compact.rs
@@ -18,7 +18,6 @@ use crate::protocol::WarningEvent;
 use crate::truncate::TruncationPolicy;
 use crate::truncate::approx_token_count;
 use crate::truncate::truncate_text;
-use crate::user_shell_command::is_user_shell_command_text;
 use crate::util::backoff;
 use codex_protocol::items::ContextCompactionItem;
 use codex_protocol::items::TurnItem;
@@ -452,14 +451,13 @@ fn is_summary_user_message_item(item: &ResponseItem) -> bool {
 /// - `developer` messages because remote output can include stale/duplicated
 ///   instruction content.
 /// - non-user-content `user` messages (session prefix/instruction wrappers),
-///   keeping real user messages plus user shell-command records.
+///   keeping only real user messages as parsed by `parse_turn_item`.
 ///
 /// This intentionally keeps `user`-role warnings and compaction-generated
-/// summary messages because they parse as `TurnItem::UserMessage`, and keeps
-/// `<user_shell_command>` user records for shell-execution continuity.
+/// summary messages because they parse as `TurnItem::UserMessage`.
 fn should_keep_compacted_history_item(item: &ResponseItem) -> bool {
     match item {
-        ResponseItem::Message { role, content, .. } => {
+        ResponseItem::Message { role, .. } => {
             if role == "developer" {
                 return false;
             }
@@ -470,9 +468,6 @@ fn should_keep_compacted_history_item(item: &ResponseItem) -> bool {
             matches!(
                 crate::event_mapping::parse_turn_item(item),
                 Some(TurnItem::UserMessage(_))
-            ) || matches!(
-                content.as_slice(),
-                [ContentItem::InputText { text }] if is_user_shell_command_text(text)
             )
         }
         ResponseItem::Reasoning { .. }
@@ -1029,7 +1024,7 @@ do things
     }
 
     #[test]
-    fn real_user_message_includes_user_shell_command_records() {
+    fn real_user_message_excludes_user_shell_command_records() {
         let shell_command_user = ResponseItem::Message {
             id: None,
             role: "user".to_string(),
@@ -1040,11 +1035,11 @@ do things
             phase: None,
         };
 
-        assert!(super::is_real_user_message(&shell_command_user));
+        assert!(!super::is_real_user_message(&shell_command_user));
     }
 
     #[test]
-    fn should_keep_compacted_history_item_drops_user_session_prefix_but_keeps_user_shell_command() {
+    fn should_keep_compacted_history_item_drops_user_session_prefix_and_user_shell_command() {
         let session_prefix = ResponseItem::Message {
             id: None,
             role: "user".to_string(),
@@ -1066,7 +1061,7 @@ do things
         };
 
         assert!(!super::should_keep_compacted_history_item(&session_prefix));
-        assert!(super::should_keep_compacted_history_item(
+        assert!(!super::should_keep_compacted_history_item(
             &shell_command_user
         ));
     }

From 4c2c861e1520154eca7073d47a3c3c4617362bdb Mon Sep 17 00:00:00 2001
From: Charles Cunningham <ccunningham@openai.com>
Date: Tue, 17 Feb 2026 18:51:30 -0800
Subject: [PATCH 46/80] Keep shell command records only in incoming compaction
 items

---
 codex-rs/core/src/compact.rs | 20 +++++++++++++++++++-
 1 file changed, 19 insertions(+), 1 deletion(-)

diff --git a/codex-rs/core/src/compact.rs b/codex-rs/core/src/compact.rs
index 1af23fda98cb..1561deb41737 100644
--- a/codex-rs/core/src/compact.rs
+++ b/codex-rs/core/src/compact.rs
@@ -18,6 +18,7 @@ use crate::protocol::WarningEvent;
 use crate::truncate::TruncationPolicy;
 use crate::truncate::approx_token_count;
 use crate::truncate::truncate_text;
+use crate::user_shell_command::is_user_shell_command_text;
 use crate::util::backoff;
 use codex_protocol::items::ContextCompactionItem;
 use codex_protocol::items::TurnItem;
@@ -303,7 +304,24 @@ async fn run_compact_task_inner(
     let incoming_user_items = match incoming_items.as_ref() {
         Some(items) => items
             .iter()
-            .filter(|item| is_real_user_message(item))
+            .filter(|item| {
+                if is_real_user_message(item) {
+                    return true;
+                }
+                // TODO(ccunningham): Truncate user shell-command records before preserving them
+                // in incoming compaction items so they cannot cause repeated context-window
+                // overflows across pre-turn compaction attempts.
+                matches!(
+                    item,
+                    ResponseItem::Message { role, content, .. }
+                        if role == "user"
+                            && matches!(
+                                content.as_slice(),
+                                [ContentItem::InputText { text }]
+                                    if is_user_shell_command_text(text)
+                            )
+                )
+            })
             .cloned()
             .collect(),
         None => Vec::new(),

From d53b588309d718b2ea917fc4a7799c46909dfae2 Mon Sep 17 00:00:00 2001
From: Charles Cunningham <ccunningham@openai.com>
Date: Tue, 17 Feb 2026 18:58:35 -0800
Subject: [PATCH 47/80] Base incoming compaction filtering on keep predicate

---
 codex-rs/core/src/compact.rs | 25 ++++++++++++-------------
 1 file changed, 12 insertions(+), 13 deletions(-)

diff --git a/codex-rs/core/src/compact.rs b/codex-rs/core/src/compact.rs
index 1561deb41737..618794586d25 100644
--- a/codex-rs/core/src/compact.rs
+++ b/codex-rs/core/src/compact.rs
@@ -305,22 +305,21 @@ async fn run_compact_task_inner(
         Some(items) => items
             .iter()
             .filter(|item| {
-                if is_real_user_message(item) {
-                    return true;
-                }
                 // TODO(ccunningham): Truncate user shell-command records before preserving them
                 // in incoming compaction items so they cannot cause repeated context-window
                 // overflows across pre-turn compaction attempts.
-                matches!(
-                    item,
-                    ResponseItem::Message { role, content, .. }
-                        if role == "user"
-                            && matches!(
-                                content.as_slice(),
-                                [ContentItem::InputText { text }]
-                                    if is_user_shell_command_text(text)
-                            )
-                )
+                (should_keep_compacted_history_item(item)
+                    || matches!(
+                        item,
+                        ResponseItem::Message { role, content, .. }
+                            if role == "user"
+                                && matches!(
+                                    content.as_slice(),
+                                    [ContentItem::InputText { text }]
+                                        if is_user_shell_command_text(text)
+                                )
+                    ))
+                    && !is_summary_user_message_item(item)
             })
             .cloned()
             .collect(),

From 52e6a54a6d8c4834669adac5f7ccec1e0bf9cc08 Mon Sep 17 00:00:00 2001
From: Charles Cunningham <ccunningham@openai.com>
Date: Tue, 17 Feb 2026 19:03:04 -0800
Subject: [PATCH 48/80] Define real user predicate independent of keep policy

---
 codex-rs/core/src/compact.rs | 32 ++++++++++++++++++++------------
 1 file changed, 20 insertions(+), 12 deletions(-)

diff --git a/codex-rs/core/src/compact.rs b/codex-rs/core/src/compact.rs
index 618794586d25..31203ba37bfe 100644
--- a/codex-rs/core/src/compact.rs
+++ b/codex-rs/core/src/compact.rs
@@ -308,17 +308,7 @@ async fn run_compact_task_inner(
                 // TODO(ccunningham): Truncate user shell-command records before preserving them
                 // in incoming compaction items so they cannot cause repeated context-window
                 // overflows across pre-turn compaction attempts.
-                (should_keep_compacted_history_item(item)
-                    || matches!(
-                        item,
-                        ResponseItem::Message { role, content, .. }
-                            if role == "user"
-                                && matches!(
-                                    content.as_slice(),
-                                    [ContentItem::InputText { text }]
-                                        if is_user_shell_command_text(text)
-                                )
-                    ))
+                (should_keep_compacted_history_item(item) || is_user_shell_command_record(item))
                     && !is_summary_user_message_item(item)
             })
             .cloned()
@@ -449,7 +439,10 @@ pub(crate) fn process_compacted_history(
 }
 
 fn is_real_user_message(item: &ResponseItem) -> bool {
-    should_keep_compacted_history_item(item) && !is_summary_user_message_item(item)
+    matches!(
+        crate::event_mapping::parse_turn_item(item),
+        Some(TurnItem::UserMessage(user_message)) if !is_summary_message(&user_message.message())
+    )
 }
 
 fn is_summary_user_message_item(item: &ResponseItem) -> bool {
@@ -459,6 +452,18 @@ fn is_summary_user_message_item(item: &ResponseItem) -> bool {
     )
 }
 
+fn is_user_shell_command_record(item: &ResponseItem) -> bool {
+    matches!(
+        item,
+        ResponseItem::Message { role, content, .. }
+            if role == "user"
+                && matches!(
+                    content.as_slice(),
+                    [ContentItem::InputText { text }] if is_user_shell_command_text(text)
+                )
+    )
+}
+
 /// Returns whether an item from remote compaction output should be preserved.
 ///
 /// Called while processing the model-provided compacted transcript, before we
@@ -481,6 +486,9 @@ fn should_keep_compacted_history_item(item: &ResponseItem) -> bool {
             if role != "user" {
                 return true;
             }
+            if is_user_shell_command_record(item) {
+                return false;
+            }
 
             matches!(
                 crate::event_mapping::parse_turn_item(item),

From 8b290e8707cf11db346196934391ecb41c2690b3 Mon Sep 17 00:00:00 2001
From: Charles Cunningham <ccunningham@openai.com>
Date: Tue, 17 Feb 2026 19:06:33 -0800
Subject: [PATCH 49/80] Drop redundant summary check for incoming compaction
 items

---
 codex-rs/core/src/compact.rs | 10 +---------
 1 file changed, 1 insertion(+), 9 deletions(-)

diff --git a/codex-rs/core/src/compact.rs b/codex-rs/core/src/compact.rs
index 31203ba37bfe..b8606c29cbb8 100644
--- a/codex-rs/core/src/compact.rs
+++ b/codex-rs/core/src/compact.rs
@@ -308,8 +308,7 @@ async fn run_compact_task_inner(
                 // TODO(ccunningham): Truncate user shell-command records before preserving them
                 // in incoming compaction items so they cannot cause repeated context-window
                 // overflows across pre-turn compaction attempts.
-                (should_keep_compacted_history_item(item) || is_user_shell_command_record(item))
-                    && !is_summary_user_message_item(item)
+                should_keep_compacted_history_item(item) || is_user_shell_command_record(item)
             })
             .cloned()
             .collect(),
@@ -445,13 +444,6 @@ fn is_real_user_message(item: &ResponseItem) -> bool {
     )
 }
 
-fn is_summary_user_message_item(item: &ResponseItem) -> bool {
-    matches!(
-        crate::event_mapping::parse_turn_item(item),
-        Some(TurnItem::UserMessage(user_message)) if is_summary_message(&user_message.message())
-    )
-}
-
 fn is_user_shell_command_record(item: &ResponseItem) -> bool {
     matches!(
         item,

From 80ab62fc686055c2014fe61d15e63367e4882de1 Mon Sep 17 00:00:00 2001
From: Charles Cunningham <ccunningham@openai.com>
Date: Tue, 17 Feb 2026 19:18:59 -0800
Subject: [PATCH 50/80] cleanup

---
 codex-rs/core/src/compact.rs | 3 ---
 1 file changed, 3 deletions(-)

diff --git a/codex-rs/core/src/compact.rs b/codex-rs/core/src/compact.rs
index b8606c29cbb8..bf4ad646a6d8 100644
--- a/codex-rs/core/src/compact.rs
+++ b/codex-rs/core/src/compact.rs
@@ -478,9 +478,6 @@ fn should_keep_compacted_history_item(item: &ResponseItem) -> bool {
             if role != "user" {
                 return true;
             }
-            if is_user_shell_command_record(item) {
-                return false;
-            }
 
             matches!(
                 crate::event_mapping::parse_turn_item(item),

From 6663b690b7deddf16e25e32c8f589a531e003eb4 Mon Sep 17 00:00:00 2001
From: Charles Cunningham <ccunningham@openai.com>
Date: Tue, 17 Feb 2026 19:23:53 -0800
Subject: [PATCH 51/80] Restrict compacted-history keep set to user and
 compaction items

---
 codex-rs/core/src/compact.rs | 29 +++++++++++++++--------------
 1 file changed, 15 insertions(+), 14 deletions(-)

diff --git a/codex-rs/core/src/compact.rs b/codex-rs/core/src/compact.rs
index bf4ad646a6d8..4b18c9de2c19 100644
--- a/codex-rs/core/src/compact.rs
+++ b/codex-rs/core/src/compact.rs
@@ -466,9 +466,10 @@ fn is_user_shell_command_record(item: &ResponseItem) -> bool {
 ///   instruction content.
 /// - non-user-content `user` messages (session prefix/instruction wrappers),
 ///   keeping only real user messages as parsed by `parse_turn_item`.
+/// - all non-user transcript items except compaction records.
 ///
-/// This intentionally keeps `user`-role warnings and compaction-generated
-/// summary messages because they parse as `TurnItem::UserMessage`.
+/// This intentionally keeps compaction-generated summary messages because they
+/// parse as `TurnItem::UserMessage`.
 fn should_keep_compacted_history_item(item: &ResponseItem) -> bool {
     match item {
         ResponseItem::Message { role, .. } => {
@@ -476,7 +477,7 @@ fn should_keep_compacted_history_item(item: &ResponseItem) -> bool {
                 return false;
             }
             if role != "user" {
-                return true;
+                return false;
             }
 
             matches!(
@@ -484,6 +485,7 @@ fn should_keep_compacted_history_item(item: &ResponseItem) -> bool {
                 Some(TurnItem::UserMessage(_))
             )
         }
+        ResponseItem::Compaction { .. } => true,
         ResponseItem::Reasoning { .. }
         | ResponseItem::LocalShellCall { .. }
         | ResponseItem::FunctionCall { .. }
@@ -492,8 +494,7 @@ fn should_keep_compacted_history_item(item: &ResponseItem) -> bool {
         | ResponseItem::CustomToolCallOutput { .. }
         | ResponseItem::WebSearchCall { .. }
         | ResponseItem::GhostSnapshot { .. }
-        | ResponseItem::Compaction { .. }
-        | ResponseItem::Other => true,
+        | ResponseItem::Other => false,
     }
 }
 
@@ -1080,6 +1081,15 @@ do things
         ));
     }
 
+    #[test]
+    fn should_keep_compacted_history_item_keeps_compaction_item() {
+        let compaction = ResponseItem::Compaction {
+            encrypted_content: "abc123".to_string(),
+        };
+
+        assert!(super::should_keep_compacted_history_item(&compaction));
+    }
+
     #[test]
     fn process_compacted_history_replaces_developer_messages() {
         let compacted_history = vec![
@@ -1671,15 +1681,6 @@ keep me updated
                 end_turn: None,
                 phase: None,
             },
-            ResponseItem::Message {
-                id: None,
-                role: "assistant".to_string(),
-                content: vec![ContentItem::OutputText {
-                    text: "assistant after latest summary".to_string(),
-                }],
-                end_turn: None,
-                phase: None,
-            },
         ];
         assert_eq!(refreshed, expected);
     }

From b781a15a42235571d20876c40c8ca32ce000355f Mon Sep 17 00:00:00 2001
From: Charles Cunningham <ccunningham@openai.com>
Date: Tue, 17 Feb 2026 19:35:22 -0800
Subject: [PATCH 52/80] Keep only user+compaction items in compacted history

---
 codex-rs/core/src/compact.rs                | 1 +
 codex-rs/core/tests/suite/compact.rs        | 5 -----
 codex-rs/core/tests/suite/compact_remote.rs | 6 +++++-
 3 files changed, 6 insertions(+), 6 deletions(-)

diff --git a/codex-rs/core/src/compact.rs b/codex-rs/core/src/compact.rs
index 4b18c9de2c19..da2a664b3b69 100644
--- a/codex-rs/core/src/compact.rs
+++ b/codex-rs/core/src/compact.rs
@@ -485,6 +485,7 @@ fn should_keep_compacted_history_item(item: &ResponseItem) -> bool {
                 Some(TurnItem::UserMessage(_))
             )
         }
+        // Keep compaction records for local/remote history continuity and token accounting.
         ResponseItem::Compaction { .. } => true,
         ResponseItem::Reasoning { .. }
         | ResponseItem::LocalShellCall { .. }
diff --git a/codex-rs/core/tests/suite/compact.rs b/codex-rs/core/tests/suite/compact.rs
index 17066c3c92c6..d7051dc24519 100644
--- a/codex-rs/core/tests/suite/compact.rs
+++ b/codex-rs/core/tests/suite/compact.rs
@@ -2961,11 +2961,6 @@ async fn auto_compact_counts_encrypted_reasoning_before_last_user() {
         "second turn should not include compacted history"
     );
     let third_request_body = requests[2].body_json().to_string();
-    assert!(
-        third_request_body.contains("REMOTE_COMPACT_SUMMARY")
-            || third_request_body.contains(FINAL_REPLY),
-        "third turn should include compacted history"
-    );
     assert!(
         third_request_body.contains("ENCRYPTED_COMPACTION_SUMMARY"),
         "third turn should include compaction summary item"
diff --git a/codex-rs/core/tests/suite/compact_remote.rs b/codex-rs/core/tests/suite/compact_remote.rs
index c815adcf7653..eab0b6c52219 100644
--- a/codex-rs/core/tests/suite/compact_remote.rs
+++ b/codex-rs/core/tests/suite/compact_remote.rs
@@ -1051,11 +1051,15 @@ async fn remote_compact_persists_replacement_history_in_rollout() -> Result<()>
                 )
             });
 
-            if has_compacted_user_summary && has_compaction_item && has_compacted_assistant_note {
+            if has_compacted_user_summary && has_compaction_item {
                 assert!(
                     !has_permissions_developer_message,
                     "manual remote compact rollout replacement history should not inject permissions context"
                 );
+                assert!(
+                    !has_compacted_assistant_note,
+                    "manual remote compact rollout replacement history should drop assistant notes"
+                );
                 saw_compacted_history = true;
                 break;
             }

From 0d796c73fa6d1eccf5d713b3715e5b23a2c93481 Mon Sep 17 00:00:00 2001
From: Charles Cunningham <ccunningham@openai.com>
Date: Tue, 17 Feb 2026 19:38:39 -0800
Subject: [PATCH 53/80] Keep shell command records in compacted history filter

---
 codex-rs/core/src/compact.rs | 16 ++++++++--------
 1 file changed, 8 insertions(+), 8 deletions(-)

diff --git a/codex-rs/core/src/compact.rs b/codex-rs/core/src/compact.rs
index da2a664b3b69..92321eda1464 100644
--- a/codex-rs/core/src/compact.rs
+++ b/codex-rs/core/src/compact.rs
@@ -304,12 +304,7 @@ async fn run_compact_task_inner(
     let incoming_user_items = match incoming_items.as_ref() {
         Some(items) => items
             .iter()
-            .filter(|item| {
-                // TODO(ccunningham): Truncate user shell-command records before preserving them
-                // in incoming compaction items so they cannot cause repeated context-window
-                // overflows across pre-turn compaction attempts.
-                should_keep_compacted_history_item(item) || is_user_shell_command_record(item)
-            })
+            .filter(|item| should_keep_compacted_history_item(item))
             .cloned()
             .collect(),
         None => Vec::new(),
@@ -479,6 +474,11 @@ fn should_keep_compacted_history_item(item: &ResponseItem) -> bool {
             if role != "user" {
                 return false;
             }
+            if is_user_shell_command_record(item) {
+                // TODO(ccunningham): Truncate preserved user shell-command records so they cannot
+                // cause repeated context-window overflows across compaction attempts.
+                return true;
+            }
 
             matches!(
                 crate::event_mapping::parse_turn_item(item),
@@ -1055,7 +1055,7 @@ do things
     }
 
     #[test]
-    fn should_keep_compacted_history_item_drops_user_session_prefix_and_user_shell_command() {
+    fn should_keep_compacted_history_item_drops_user_session_prefix_and_keeps_user_shell_command() {
         let session_prefix = ResponseItem::Message {
             id: None,
             role: "user".to_string(),
@@ -1077,7 +1077,7 @@ do things
         };
 
         assert!(!super::should_keep_compacted_history_item(&session_prefix));
-        assert!(!super::should_keep_compacted_history_item(
+        assert!(super::should_keep_compacted_history_item(
             &shell_command_user
         ));
     }

From a9c6219d8516e882a44000210e813c2391993190 Mon Sep 17 00:00:00 2001
From: Charles Cunningham <ccunningham@openai.com>
Date: Tue, 17 Feb 2026 22:43:21 -0800
Subject: [PATCH 54/80] Append incoming pre-turn items after compaction summary

---
 codex-rs/core/src/codex.rs                    | 24 +++-----
 codex-rs/core/src/compact.rs                  | 58 ++++---------------
 codex-rs/core/src/compact_remote.rs           | 52 +++++++++++++++--
 ..._compaction_including_incoming_shapes.snap | 14 +++--
 ...n_strips_incoming_model_switch_shapes.snap | 17 +++---
 ..._compaction_including_incoming_shapes.snap |  6 +-
 ...n_strips_incoming_model_switch_shapes.snap |  9 +--
 7 files changed, 94 insertions(+), 86 deletions(-)

diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs
index b40bfdb39697..0f61ce4f8590 100644
--- a/codex-rs/core/src/codex.rs
+++ b/codex-rs/core/src/codex.rs
@@ -4735,18 +4735,11 @@ async fn persist_pre_turn_items_for_compaction_outcome(
     response_item: ResponseItem,
 ) {
     match outcome {
-        PreTurnCompactionOutcome::CompactedWithIncomingItems => {
-            // Incoming turn items were already part of pre-turn compaction input, and the
-            // user prompt is already persisted in history after compaction. Emit lifecycle events
-            // only so UI/consumers still observe a normal user turn item transition.
-            let turn_item = TurnItem::UserMessage(UserMessageItem::new(input));
-            sess.emit_turn_item_started(turn_context.as_ref(), &turn_item)
-                .await;
-            sess.emit_turn_item_completed(turn_context.as_ref(), turn_item)
-                .await;
-            sess.ensure_rollout_materialized().await;
-        }
-        PreTurnCompactionOutcome::NotNeeded => {
+        PreTurnCompactionOutcome::CompactedWithIncomingItems
+        | PreTurnCompactionOutcome::NotNeeded => {
+            // Pre-turn compaction includes incoming items only for the compaction model's request.
+            // We always persist canonical pre-turn updates and the current user item after the
+            // compaction summary so model-visible layout is stable regardless of compaction mode.
             if !pre_turn_context_items.is_empty() {
                 sess.record_conversation_items(turn_context, pre_turn_context_items)
                     .await;
@@ -6150,7 +6143,7 @@ mod tests {
     }
 
     #[tokio::test]
-    async fn compacted_with_incoming_items_emits_lifecycle_without_history_writes() {
+    async fn compacted_with_incoming_items_persists_context_and_prompt() {
         let (session, turn_context) = make_session_and_context().await;
         let session = Arc::new(session);
         let turn_context = Arc::new(turn_context);
@@ -6175,12 +6168,13 @@ mod tests {
             PreTurnCompactionOutcome::CompactedWithIncomingItems,
             &stale_pre_turn_context_items,
             &input,
-            response_item,
+            response_item.clone(),
         )
         .await;
 
         let actual = session.clone_history().await.raw_items().to_vec();
-        assert_eq!(actual, Vec::<ResponseItem>::new());
+        let expected = vec![stale_pre_turn_context_items[0].clone(), response_item];
+        assert_eq!(actual, expected);
     }
 
     #[tokio::test]
diff --git a/codex-rs/core/src/compact.rs b/codex-rs/core/src/compact.rs
index 92321eda1464..c649c0156650 100644
--- a/codex-rs/core/src/compact.rs
+++ b/codex-rs/core/src/compact.rs
@@ -301,15 +301,6 @@ async fn run_compact_task_inner(
     let summary_suffix = get_last_assistant_message_from_turn(history_items).unwrap_or_default();
     let summary_text = format!("{SUMMARY_PREFIX}\n{summary_suffix}");
     let user_messages = collect_user_messages(history_items);
-    let incoming_user_items = match incoming_items.as_ref() {
-        Some(items) => items
-            .iter()
-            .filter(|item| should_keep_compacted_history_item(item))
-            .cloned()
-            .collect(),
-        None => Vec::new(),
-    };
-
     let initial_context = match turn_context_reinjection {
         TurnContextReinjection::ReinjectAboveLastRealUser => {
             sess.build_initial_context(turn_context.as_ref()).await
@@ -318,7 +309,6 @@ async fn run_compact_task_inner(
     };
     let compacted_history = build_compacted_history_with_limit(
         &user_messages,
-        &incoming_user_items,
         &summary_text,
         COMPACT_USER_MESSAGE_MAX_TOKENS,
     );
@@ -327,9 +317,12 @@ async fn run_compact_task_inner(
         &initial_context,
         turn_context_reinjection,
     );
-    // Reattach the stripped model-switch update only after successful compaction so the model
-    // still sees the switch instructions on the next real sampling request.
-    if let Some(model_switch_item) = stripped_model_switch_item {
+    // Reattach stripped model-switch updates only for compaction paths that do not carry
+    // incoming turn items. Pre-turn compaction appends turn context and user input after
+    // compaction in run_turn.
+    if incoming_items.is_none()
+        && let Some(model_switch_item) = stripped_model_switch_item
+    {
         new_history.push(model_switch_item);
     }
     let ghost_snapshots: Vec<ResponseItem> = history_items
@@ -465,7 +458,7 @@ fn is_user_shell_command_record(item: &ResponseItem) -> bool {
 ///
 /// This intentionally keeps compaction-generated summary messages because they
 /// parse as `TurnItem::UserMessage`.
-fn should_keep_compacted_history_item(item: &ResponseItem) -> bool {
+pub(crate) fn should_keep_compacted_history_item(item: &ResponseItem) -> bool {
     match item {
         ResponseItem::Message { role, .. } => {
             if role == "developer" {
@@ -503,17 +496,11 @@ pub(crate) fn build_compacted_history(
     user_messages: &[String],
     summary_text: &str,
 ) -> Vec<ResponseItem> {
-    build_compacted_history_with_limit(
-        user_messages,
-        &[],
-        summary_text,
-        COMPACT_USER_MESSAGE_MAX_TOKENS,
-    )
+    build_compacted_history_with_limit(user_messages, summary_text, COMPACT_USER_MESSAGE_MAX_TOKENS)
 }
 
 fn build_compacted_history_with_limit(
     user_messages: &[String],
-    incoming_user_items: &[ResponseItem],
     summary_text: &str,
     max_tokens: usize,
 ) -> Vec<ResponseItem> {
@@ -550,8 +537,6 @@ fn build_compacted_history_with_limit(
         });
     }
 
-    history.extend(incoming_user_items.iter().cloned());
-
     let summary_text = if summary_text.is_empty() {
         "(no summary available)".to_string()
     } else {
@@ -920,7 +905,6 @@ do things
         let big = "word ".repeat(200);
         let history = super::build_compacted_history_with_limit(
             std::slice::from_ref(&big),
-            &[],
             "SUMMARY",
             max_tokens,
         );
@@ -976,28 +960,9 @@ do things
     }
 
     #[test]
-    fn build_compacted_history_preserves_incoming_user_item_structure() {
-        let preserved_user_item = ResponseItem::Message {
-            id: None,
-            role: "user".to_string(),
-            content: vec![
-                ContentItem::InputImage {
-                    image_url: "data:image/png;base64,AAAA".to_string(),
-                },
-                ContentItem::InputText {
-                    text: "latest user with image".to_string(),
-                },
-            ],
-            end_turn: None,
-            phase: None,
-        };
-
-        let history = super::build_compacted_history_with_limit(
-            &["older user".to_string()],
-            std::slice::from_ref(&preserved_user_item),
-            "SUMMARY",
-            128,
-        );
+    fn build_compacted_history_preserves_user_message_structure() {
+        let history =
+            super::build_compacted_history_with_limit(&["older user".to_string()], "SUMMARY", 128);
 
         let expected = vec![
             ResponseItem::Message {
@@ -1009,7 +974,6 @@ do things
                 end_turn: None,
                 phase: None,
             },
-            preserved_user_item,
             ResponseItem::Message {
                 id: None,
                 role: "user".to_string(),
diff --git a/codex-rs/core/src/compact_remote.rs b/codex-rs/core/src/compact_remote.rs
index 4768abe0d6b8..f91b27049720 100644
--- a/codex-rs/core/src/compact_remote.rs
+++ b/codex-rs/core/src/compact_remote.rs
@@ -7,6 +7,7 @@ use crate::compact::AutoCompactCallsite;
 use crate::compact::TurnContextReinjection;
 use crate::compact::extract_latest_model_switch_update_from_items;
 use crate::compact::extract_trailing_model_switch_update_for_compaction_request;
+use crate::compact::should_keep_compacted_history_item;
 use crate::context_manager::ContextManager;
 use crate::context_manager::TotalTokenUsageBreakdown;
 use crate::context_manager::estimate_item_token_count;
@@ -122,7 +123,7 @@ async fn run_remote_compact_task_inner_impl(
         &base_instructions,
         incoming_items.as_deref(),
     );
-    if let Some(incoming_items) = incoming_items {
+    if let Some(incoming_items) = incoming_items.as_ref() {
         history.record_items(incoming_items.iter(), turn_context.truncation_policy);
     }
     if !history.raw_items().iter().any(is_user_turn_boundary) {
@@ -182,9 +183,52 @@ async fn run_remote_compact_task_inner_impl(
     new_history = sess
         .process_compacted_history(turn_context, new_history, turn_context_reinjection)
         .await;
-    // Reattach the stripped model-switch update only after successful compaction so the model
-    // still sees the switch instructions on the next real sampling request.
-    if let Some(model_switch_item) = stripped_model_switch_item {
+    if let Some(incoming_items) = incoming_items.as_ref() {
+        let incoming_history_items: Vec<ResponseItem> = incoming_items
+            .iter()
+            .filter(|item| should_keep_compacted_history_item(item))
+            .cloned()
+            .collect();
+        for incoming_item in incoming_history_items {
+            if let Some(index) =
+                new_history
+                    .iter()
+                    .rposition(|candidate| match (candidate, &incoming_item) {
+                        (
+                            ResponseItem::Message {
+                                role: candidate_role,
+                                content: candidate_content,
+                                ..
+                            },
+                            ResponseItem::Message {
+                                role: incoming_role,
+                                content: incoming_content,
+                                ..
+                            },
+                        ) => {
+                            candidate_role == incoming_role && candidate_content == incoming_content
+                        }
+                        (
+                            ResponseItem::Compaction {
+                                encrypted_content: candidate_content,
+                            },
+                            ResponseItem::Compaction {
+                                encrypted_content: incoming_content,
+                            },
+                        ) => candidate_content == incoming_content,
+                        _ => candidate == &incoming_item,
+                    })
+            {
+                new_history.remove(index);
+            }
+        }
+    }
+    // Reattach stripped model-switch updates only for compaction paths that do not carry
+    // incoming turn items. Pre-turn compaction appends turn context and user input after
+    // compaction in run_turn.
+    if incoming_items.is_none()
+        && let Some(model_switch_item) = stripped_model_switch_item
+    {
         new_history.push(model_switch_item);
     }
 
diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_turn_compaction_including_incoming_shapes.snap b/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_turn_compaction_including_incoming_shapes.snap
index bd4eb41236cb..c30d30487911 100644
--- a/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_turn_compaction_including_incoming_shapes.snap
+++ b/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_turn_compaction_including_incoming_shapes.snap
@@ -1,5 +1,6 @@
 ---
 source: core/tests/suite/compact.rs
+assertion_line: 3138
 expression: "format_labeled_requests_snapshot(\"Pre-turn auto-compaction with a context override includes incoming user content in the compact request and preserves it after compaction.\",\n&[(\"Local Compaction Request\", &requests[2]),\n(\"Local Post-Compaction History Layout\", &requests[3]),])"
 ---
 Scenario: Pre-turn auto-compaction with a context override includes incoming user content in the compact request and preserves it after compaction.
@@ -18,9 +19,10 @@ Scenario: Pre-turn auto-compaction with a context override includes incoming use
 
 ## Local Post-Compaction History Layout
 00:message/user:USER_ONE
-01:message/user:USER_TWO
-02:message/developer:<PERMISSIONS_INSTRUCTIONS>
-03:message/user:<AGENTS_MD>
-04:message/user:<ENVIRONMENT_CONTEXT:cwd=PRETURN_CONTEXT_DIFF_CWD>
-05:message/user:<image> | <input_image:image_url> | </image> | USER_THREE
-06:message/user:<COMPACTION_SUMMARY>\nPRE_TURN_SUMMARY
+01:message/developer:<PERMISSIONS_INSTRUCTIONS>
+02:message/user:<AGENTS_MD>
+03:message/user:<ENVIRONMENT_CONTEXT:cwd=PRETURN_CONTEXT_DIFF_CWD>
+04:message/user:USER_TWO
+05:message/user:<COMPACTION_SUMMARY>\nPRE_TURN_SUMMARY
+06:message/user:<ENVIRONMENT_CONTEXT:cwd=PRETURN_CONTEXT_DIFF_CWD>
+07:message/user:<image> | <input_image:image_url> | </image> | USER_THREE
diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_turn_compaction_strips_incoming_model_switch_shapes.snap b/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_turn_compaction_strips_incoming_model_switch_shapes.snap
index 48b721f2c298..fcbad1059be6 100644
--- a/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_turn_compaction_strips_incoming_model_switch_shapes.snap
+++ b/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_turn_compaction_strips_incoming_model_switch_shapes.snap
@@ -1,5 +1,6 @@
 ---
 source: core/tests/suite/compact.rs
+assertion_line: 3288
 expression: "format_labeled_requests_snapshot(\"Pre-turn compaction during model switch (without pre-sampling model-switch compaction): current behavior strips incoming <model_switch> from the compact request and restores it in the post-compaction follow-up request.\",\n&[(\"Initial Request (Previous Model)\", &requests[0]),\n(\"Local Compaction Request\", &requests[1]),\n(\"Local Post-Compaction History Layout\", &requests[2]),])"
 ---
 Scenario: Pre-turn compaction during model switch (without pre-sampling model-switch compaction): current behavior strips incoming <model_switch> from the compact request and restores it in the post-compaction follow-up request.
@@ -22,11 +23,11 @@ Scenario: Pre-turn compaction during model switch (without pre-sampling model-sw
 07:message/user:<SUMMARIZATION_PROMPT>
 
 ## Local Post-Compaction History Layout
-00:message/user:BEFORE_SWITCH_USER
-01:message/developer:<PERMISSIONS_INSTRUCTIONS>
-02:message/developer:<personality_spec> The user has requested a new communication st...
-03:message/user:<AGENTS_MD>
-04:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
-05:message/user:AFTER_SWITCH_USER
-06:message/user:<COMPACTION_SUMMARY>\nPRETURN_SWITCH_SUMMARY
-07:message/developer:<model_switch>\nThe user was previously using a different model....
+00:message/developer:<PERMISSIONS_INSTRUCTIONS>
+01:message/developer:<personality_spec> The user has requested a new communication st...
+02:message/user:<AGENTS_MD>
+03:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
+04:message/user:BEFORE_SWITCH_USER
+05:message/user:<COMPACTION_SUMMARY>\nPRETURN_SWITCH_SUMMARY
+06:message/developer:<model_switch>\nThe user was previously using a different model....
+07:message/user:AFTER_SWITCH_USER
diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_pre_turn_compaction_including_incoming_shapes.snap b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_pre_turn_compaction_including_incoming_shapes.snap
index ec30aa08614b..7b28f8151a86 100644
--- a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_pre_turn_compaction_including_incoming_shapes.snap
+++ b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_pre_turn_compaction_including_incoming_shapes.snap
@@ -1,5 +1,6 @@
 ---
 source: core/tests/suite/compact_remote.rs
+assertion_line: 1390
 expression: "format_labeled_requests_snapshot(\"Remote pre-turn auto-compaction with a context override includes incoming user content in the compact request and preserves it after compaction.\",\n&[(\"Remote Compaction Request\", &compact_request),\n(\"Remote Post-Compaction History Layout\", &requests[2]),])"
 ---
 Scenario: Remote pre-turn auto-compaction with a context override includes incoming user content in the compact request and preserves it after compaction.
@@ -21,5 +22,6 @@ Scenario: Remote pre-turn auto-compaction with a context override includes incom
 02:message/developer:<PERMISSIONS_INSTRUCTIONS>
 03:message/user:<AGENTS_MD>
 04:message/user:<ENVIRONMENT_CONTEXT:cwd=PRETURN_CONTEXT_DIFF_CWD>
-05:message/user:USER_THREE
-06:message/user:<COMPACTION_SUMMARY>\nREMOTE_PRE_TURN_SUMMARY
+05:message/user:<COMPACTION_SUMMARY>\nREMOTE_PRE_TURN_SUMMARY
+06:message/user:<ENVIRONMENT_CONTEXT:cwd=PRETURN_CONTEXT_DIFF_CWD>
+07:message/user:USER_THREE
diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_pre_turn_compaction_strips_incoming_model_switch_shapes.snap b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_pre_turn_compaction_strips_incoming_model_switch_shapes.snap
index 0a789f5b697f..2b03802b7fb5 100644
--- a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_pre_turn_compaction_strips_incoming_model_switch_shapes.snap
+++ b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_pre_turn_compaction_strips_incoming_model_switch_shapes.snap
@@ -1,6 +1,7 @@
 ---
 source: core/tests/suite/compact_remote.rs
-expression: "format_labeled_requests_snapshot(\"Remote pre-turn compaction during model switch strips incoming <model_switch> from the compact request and restores it in the post-compaction follow-up request.\",\n&[(\"Initial Request (Previous Model)\", &requests[0]),\n(\"Remote Compaction Request\", &compact_request),\n(\"Remote Post-Compaction History Layout\", &requests[1]),])"
+assertion_line: 1538
+expression: "format_labeled_requests_snapshot(\"Remote pre-turn compaction during model switch strips incoming <model_switch> from the compact request and restores it in the post-compaction follow-up request.\",\n&[(\"Initial Request (Previous Model)\", &initial_turn_request),\n(\"Remote Compaction Request\", &compact_request),\n(\"Remote Post-Compaction History Layout\", &post_compact_turn_request),])"
 ---
 Scenario: Remote pre-turn compaction during model switch strips incoming <model_switch> from the compact request and restores it in the post-compaction follow-up request.
 
@@ -24,6 +25,6 @@ Scenario: Remote pre-turn compaction during model switch strips incoming <model_
 02:message/developer:<personality_spec> The user has requested a new communication st...
 03:message/user:<AGENTS_MD>
 04:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
-05:message/user:AFTER_SWITCH_USER
-06:message/user:<COMPACTION_SUMMARY>\nREMOTE_SWITCH_SUMMARY
-07:message/developer:<model_switch>\nThe user was previously using a different model....
+05:message/user:<COMPACTION_SUMMARY>\nREMOTE_SWITCH_SUMMARY
+06:message/developer:<model_switch>\nThe user was previously using a different model....
+07:message/user:AFTER_SWITCH_USER

From 868019da9b65e7bcc89fdcd6f8bebb1ed8a32b53 Mon Sep 17 00:00:00 2001
From: Charles Cunningham <ccunningham@openai.com>
Date: Tue, 17 Feb 2026 22:50:48 -0800
Subject: [PATCH 55/80] Skip mid-turn compaction error event on interruption

---
 codex-rs/core/src/codex.rs | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs
index 0f61ce4f8590..bc6f926d9f26 100644
--- a/codex-rs/core/src/codex.rs
+++ b/codex-rs/core/src/codex.rs
@@ -4538,6 +4538,9 @@ pub(crate) async fn run_turn(
                     )
                     .await
                     {
+                        if matches!(err, CodexErr::Interrupted) {
+                            return None;
+                        }
                         let event = EventMsg::Error(
                             err.to_error_event(Some("Error running auto compact task".to_string())),
                         );

From 583dee056d51d2d3b5cc0b14c840f3de9fc1bcb8 Mon Sep 17 00:00:00 2001
From: Charles Cunningham <ccunningham@openai.com>
Date: Tue, 17 Feb 2026 23:12:56 -0800
Subject: [PATCH 56/80] Refactor remote compaction incoming-item dedup

---
 codex-rs/core/src/compact_remote.rs | 79 +++++++++++++++++------------
 1 file changed, 46 insertions(+), 33 deletions(-)

diff --git a/codex-rs/core/src/compact_remote.rs b/codex-rs/core/src/compact_remote.rs
index f91b27049720..f9c88538aa17 100644
--- a/codex-rs/core/src/compact_remote.rs
+++ b/codex-rs/core/src/compact_remote.rs
@@ -189,39 +189,7 @@ async fn run_remote_compact_task_inner_impl(
             .filter(|item| should_keep_compacted_history_item(item))
             .cloned()
             .collect();
-        for incoming_item in incoming_history_items {
-            if let Some(index) =
-                new_history
-                    .iter()
-                    .rposition(|candidate| match (candidate, &incoming_item) {
-                        (
-                            ResponseItem::Message {
-                                role: candidate_role,
-                                content: candidate_content,
-                                ..
-                            },
-                            ResponseItem::Message {
-                                role: incoming_role,
-                                content: incoming_content,
-                                ..
-                            },
-                        ) => {
-                            candidate_role == incoming_role && candidate_content == incoming_content
-                        }
-                        (
-                            ResponseItem::Compaction {
-                                encrypted_content: candidate_content,
-                            },
-                            ResponseItem::Compaction {
-                                encrypted_content: incoming_content,
-                            },
-                        ) => candidate_content == incoming_content,
-                        _ => candidate == &incoming_item,
-                    })
-            {
-                new_history.remove(index);
-            }
-        }
+        remove_incoming_echoes_from_compacted_history(&mut new_history, &incoming_history_items);
     }
     // Reattach stripped model-switch updates only for compaction paths that do not carry
     // incoming turn items. Pre-turn compaction appends turn context and user input after
@@ -272,6 +240,51 @@ fn build_compact_request_log_data(
     }
 }
 
+/// Remote compaction may echo incoming items in `new_history`. Because incoming items are
+/// appended after compaction at the caller, remove one semantic duplicate for each incoming item
+/// so turn-context/user entries do not appear twice.
+fn remove_incoming_echoes_from_compacted_history(
+    new_history: &mut Vec<ResponseItem>,
+    incoming_history_items: &[ResponseItem],
+) {
+    for incoming_item in incoming_history_items {
+        if let Some(index) = new_history.iter().rposition(|candidate| {
+            response_items_match_for_compaction_merge(candidate, incoming_item)
+        }) {
+            new_history.remove(index);
+        }
+    }
+}
+
+fn response_items_match_for_compaction_merge(
+    candidate: &ResponseItem,
+    incoming_item: &ResponseItem,
+) -> bool {
+    match (candidate, incoming_item) {
+        (
+            ResponseItem::Message {
+                role: candidate_role,
+                content: candidate_content,
+                ..
+            },
+            ResponseItem::Message {
+                role: incoming_role,
+                content: incoming_content,
+                ..
+            },
+        ) => candidate_role == incoming_role && candidate_content == incoming_content,
+        (
+            ResponseItem::Compaction {
+                encrypted_content: candidate_content,
+            },
+            ResponseItem::Compaction {
+                encrypted_content: incoming_content,
+            },
+        ) => candidate_content == incoming_content,
+        _ => candidate == incoming_item,
+    }
+}
+
 fn log_remote_compact_failure(
     turn_context: &TurnContext,
     auto_compact_callsite: AutoCompactCallsite,

From d58204f9dfcb1acfabdf149ad9fd071cae0da234 Mon Sep 17 00:00:00 2001
From: Charles Cunningham <ccunningham@openai.com>
Date: Tue, 17 Feb 2026 23:29:28 -0800
Subject: [PATCH 57/80] Always skip reinjection during auto-compaction

---
 codex-rs/core/src/codex.rs                    | 34 ++++++++++++++-----
 codex-rs/core/src/compact.rs                  |  1 +
 ...__compact__mid_turn_compaction_shapes.snap |  8 ++---
 ..._compaction_including_incoming_shapes.snap | 14 ++++----
 ...n_strips_incoming_model_switch_shapes.snap | 13 ++++---
 ...y_reinjects_above_last_summary_shapes.snap | 12 +++----
 ...te__remote_mid_turn_compaction_shapes.snap |  7 ++--
 ...summary_only_reinjects_context_shapes.snap |  7 ++--
 ..._compaction_including_incoming_shapes.snap | 12 +++----
 ...n_strips_incoming_model_switch_shapes.snap | 11 +++---
 10 files changed, 60 insertions(+), 59 deletions(-)

diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs
index bc6f926d9f26..6c8fdc7c8948 100644
--- a/codex-rs/core/src/codex.rs
+++ b/codex-rs/core/src/codex.rs
@@ -4533,7 +4533,7 @@ pub(crate) async fn run_turn(
                         &sess,
                         &turn_context,
                         AutoCompactCallsite::MidTurnContinuation,
-                        TurnContextReinjection::ReinjectAboveLastRealUser,
+                        TurnContextReinjection::Skip,
                         None,
                     )
                     .await
@@ -4687,8 +4687,7 @@ async fn maybe_run_previous_model_inline_compact(
         // We use previous turn context here because we compact with the previous model
         &previous_turn_context,
         AutoCompactCallsite::PreTurnExcludingIncomingUserMessage,
-        // User message and turn context diff is injected in the pre-compaction NotNeeded case later
-        TurnContextReinjection::ReinjectAboveLastRealUser,
+        TurnContextReinjection::Skip,
         None,
     )
     .await
@@ -4738,11 +4737,28 @@ async fn persist_pre_turn_items_for_compaction_outcome(
     response_item: ResponseItem,
 ) {
     match outcome {
-        PreTurnCompactionOutcome::CompactedWithIncomingItems
-        | PreTurnCompactionOutcome::NotNeeded => {
-            // Pre-turn compaction includes incoming items only for the compaction model's request.
-            // We always persist canonical pre-turn updates and the current user item after the
-            // compaction summary so model-visible layout is stable regardless of compaction mode.
+        PreTurnCompactionOutcome::CompactedWithIncomingItems => {
+            // Pre-turn compaction includes incoming items only for the compaction request itself.
+            // Persist canonical turn context directly above the incoming user item so context
+            // applies to the latest user message in post-compaction history.
+            let initial_context = sess.build_initial_context(turn_context.as_ref()).await;
+            if !initial_context.is_empty() {
+                sess.record_conversation_items(turn_context, &initial_context)
+                    .await;
+            }
+            let model_switch_updates: Vec<ResponseItem> = pre_turn_context_items
+                .iter()
+                .filter(|item| Session::is_model_switch_developer_message(item))
+                .cloned()
+                .collect();
+            if !model_switch_updates.is_empty() {
+                sess.record_conversation_items(turn_context, &model_switch_updates)
+                    .await;
+            }
+            sess.record_user_prompt_and_emit_turn_item(turn_context.as_ref(), input, response_item)
+                .await;
+        }
+        PreTurnCompactionOutcome::NotNeeded => {
             if !pre_turn_context_items.is_empty() {
                 sess.record_conversation_items(turn_context, pre_turn_context_items)
                     .await;
@@ -4795,7 +4811,7 @@ async fn run_pre_turn_auto_compaction_if_needed(
         sess,
         turn_context,
         AutoCompactCallsite::PreTurnIncludingIncomingUserMessage,
-        TurnContextReinjection::ReinjectAboveLastRealUser,
+        TurnContextReinjection::Skip,
         Some(incoming_turn_items.to_vec()),
     )
     .await;
diff --git a/codex-rs/core/src/compact.rs b/codex-rs/core/src/compact.rs
index c649c0156650..fdc76109c31c 100644
--- a/codex-rs/core/src/compact.rs
+++ b/codex-rs/core/src/compact.rs
@@ -55,6 +55,7 @@ pub(crate) enum AutoCompactCallsite {
 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
 pub(crate) enum TurnContextReinjection {
     /// Insert canonical context immediately above the last real user message in compacted history.
+    #[allow(dead_code)]
     ReinjectAboveLastRealUser,
     /// Do not reinsert canonical context while processing compacted history.
     Skip,
diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact__mid_turn_compaction_shapes.snap b/codex-rs/core/tests/suite/snapshots/all__suite__compact__mid_turn_compaction_shapes.snap
index c13f78aff9c7..000d4837787f 100644
--- a/codex-rs/core/tests/suite/snapshots/all__suite__compact__mid_turn_compaction_shapes.snap
+++ b/codex-rs/core/tests/suite/snapshots/all__suite__compact__mid_turn_compaction_shapes.snap
@@ -1,6 +1,5 @@
 ---
 source: core/tests/suite/compact.rs
-assertion_line: 2646
 expression: "format_labeled_requests_snapshot(\"True mid-turn continuation compaction after tool output: compact request includes tool artifacts, and the continuation request includes the summary in the same turn.\",\n&[(\"Local Compaction Request\", &auto_compact_mock.single_request()),\n(\"Local Post-Compaction History Layout\",\n&post_auto_compact_mock.single_request()),])"
 ---
 Scenario: True mid-turn continuation compaction after tool output: compact request includes tool artifacts, and the continuation request includes the summary in the same turn.
@@ -15,8 +14,5 @@ Scenario: True mid-turn continuation compaction after tool output: compact reque
 06:message/user:<SUMMARIZATION_PROMPT>
 
 ## Local Post-Compaction History Layout
-00:message/developer:<PERMISSIONS_INSTRUCTIONS>
-01:message/user:<AGENTS_MD>
-02:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
-03:message/user:function call limit push
-04:message/user:<COMPACTION_SUMMARY>\nAUTO_SUMMARY
+00:message/user:function call limit push
+01:message/user:<COMPACTION_SUMMARY>\nAUTO_SUMMARY
diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_turn_compaction_including_incoming_shapes.snap b/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_turn_compaction_including_incoming_shapes.snap
index c30d30487911..ccafe8237a4c 100644
--- a/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_turn_compaction_including_incoming_shapes.snap
+++ b/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_turn_compaction_including_incoming_shapes.snap
@@ -1,6 +1,5 @@
 ---
 source: core/tests/suite/compact.rs
-assertion_line: 3138
 expression: "format_labeled_requests_snapshot(\"Pre-turn auto-compaction with a context override includes incoming user content in the compact request and preserves it after compaction.\",\n&[(\"Local Compaction Request\", &requests[2]),\n(\"Local Post-Compaction History Layout\", &requests[3]),])"
 ---
 Scenario: Pre-turn auto-compaction with a context override includes incoming user content in the compact request and preserves it after compaction.
@@ -19,10 +18,9 @@ Scenario: Pre-turn auto-compaction with a context override includes incoming use
 
 ## Local Post-Compaction History Layout
 00:message/user:USER_ONE
-01:message/developer:<PERMISSIONS_INSTRUCTIONS>
-02:message/user:<AGENTS_MD>
-03:message/user:<ENVIRONMENT_CONTEXT:cwd=PRETURN_CONTEXT_DIFF_CWD>
-04:message/user:USER_TWO
-05:message/user:<COMPACTION_SUMMARY>\nPRE_TURN_SUMMARY
-06:message/user:<ENVIRONMENT_CONTEXT:cwd=PRETURN_CONTEXT_DIFF_CWD>
-07:message/user:<image> | <input_image:image_url> | </image> | USER_THREE
+01:message/user:USER_TWO
+02:message/user:<COMPACTION_SUMMARY>\nPRE_TURN_SUMMARY
+03:message/developer:<PERMISSIONS_INSTRUCTIONS>
+04:message/user:<AGENTS_MD>
+05:message/user:<ENVIRONMENT_CONTEXT:cwd=PRETURN_CONTEXT_DIFF_CWD>
+06:message/user:<image> | <input_image:image_url> | </image> | USER_THREE
diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_turn_compaction_strips_incoming_model_switch_shapes.snap b/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_turn_compaction_strips_incoming_model_switch_shapes.snap
index fcbad1059be6..03b58b11f0e8 100644
--- a/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_turn_compaction_strips_incoming_model_switch_shapes.snap
+++ b/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_turn_compaction_strips_incoming_model_switch_shapes.snap
@@ -1,6 +1,5 @@
 ---
 source: core/tests/suite/compact.rs
-assertion_line: 3288
 expression: "format_labeled_requests_snapshot(\"Pre-turn compaction during model switch (without pre-sampling model-switch compaction): current behavior strips incoming <model_switch> from the compact request and restores it in the post-compaction follow-up request.\",\n&[(\"Initial Request (Previous Model)\", &requests[0]),\n(\"Local Compaction Request\", &requests[1]),\n(\"Local Post-Compaction History Layout\", &requests[2]),])"
 ---
 Scenario: Pre-turn compaction during model switch (without pre-sampling model-switch compaction): current behavior strips incoming <model_switch> from the compact request and restores it in the post-compaction follow-up request.
@@ -23,11 +22,11 @@ Scenario: Pre-turn compaction during model switch (without pre-sampling model-sw
 07:message/user:<SUMMARIZATION_PROMPT>
 
 ## Local Post-Compaction History Layout
-00:message/developer:<PERMISSIONS_INSTRUCTIONS>
-01:message/developer:<personality_spec> The user has requested a new communication st...
-02:message/user:<AGENTS_MD>
-03:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
-04:message/user:BEFORE_SWITCH_USER
-05:message/user:<COMPACTION_SUMMARY>\nPRETURN_SWITCH_SUMMARY
+00:message/user:BEFORE_SWITCH_USER
+01:message/user:<COMPACTION_SUMMARY>\nPRETURN_SWITCH_SUMMARY
+02:message/developer:<PERMISSIONS_INSTRUCTIONS>
+03:message/developer:<personality_spec> The user has requested a new communication st...
+04:message/user:<AGENTS_MD>
+05:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
 06:message/developer:<model_switch>\nThe user was previously using a different model....
 07:message/user:AFTER_SWITCH_USER
diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_mid_turn_compaction_multi_summary_reinjects_above_last_summary_shapes.snap b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_mid_turn_compaction_multi_summary_reinjects_above_last_summary_shapes.snap
index 9ea6233a0a1b..f8b97f47d98d 100644
--- a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_mid_turn_compaction_multi_summary_reinjects_above_last_summary_shapes.snap
+++ b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_mid_turn_compaction_multi_summary_reinjects_above_last_summary_shapes.snap
@@ -1,17 +1,17 @@
 ---
 source: core/tests/suite/compact_remote.rs
-expression: "format_labeled_requests_snapshot(\"Remote mid-turn compaction after an earlier summary compaction: the older summary remains in model-visible history and round-trips into the next compact request.\",\n&[(\"Second Turn Request (Before Mid-Turn Compaction)\", &requests[1]),\n(\"Remote Compaction Request\", &compact_request),])"
+expression: "format_labeled_requests_snapshot(\"Remote mid-turn compaction after an earlier summary compaction: the older summary remains in model-visible history and round-trips into the next compact request.\",\n&[(\"Second Turn Request (Before Mid-Turn Compaction)\", &second_turn_request),\n(\"Remote Compaction Request\", &compact_request),])"
 ---
 Scenario: Remote mid-turn compaction after an earlier summary compaction: the older summary remains in model-visible history and round-trips into the next compact request.
 
 ## Second Turn Request (Before Mid-Turn Compaction)
 00:message/user:USER_ONE
 01:message/user:<COMPACTION_SUMMARY>\nREMOTE_OLDER_SUMMARY
-02:message/developer:<PERMISSIONS_INSTRUCTIONS>
-03:message/user:<AGENTS_MD>
-04:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
-05:message/user:USER_TWO
-06:message/user:<COMPACTION_SUMMARY>\nREMOTE_LATEST_SUMMARY
+02:message/user:<COMPACTION_SUMMARY>\nREMOTE_LATEST_SUMMARY
+03:message/developer:<PERMISSIONS_INSTRUCTIONS>
+04:message/user:<AGENTS_MD>
+05:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
+06:message/user:USER_TWO
 
 ## Remote Compaction Request
 00:message/user:USER_ONE
diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_mid_turn_compaction_shapes.snap b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_mid_turn_compaction_shapes.snap
index 488bee0bef94..879c7fa7c627 100644
--- a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_mid_turn_compaction_shapes.snap
+++ b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_mid_turn_compaction_shapes.snap
@@ -13,8 +13,5 @@ Scenario: Remote mid-turn continuation compaction after tool output: compact req
 05:function_call_output:unsupported call: test_tool
 
 ## Remote Post-Compaction History Layout
-00:message/developer:<PERMISSIONS_INSTRUCTIONS>
-01:message/user:<AGENTS_MD>
-02:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
-03:message/user:USER_ONE
-04:message/user:<COMPACTION_SUMMARY>\nREMOTE_MID_TURN_SUMMARY
+00:message/user:USER_ONE
+01:message/user:<COMPACTION_SUMMARY>\nREMOTE_MID_TURN_SUMMARY
diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_mid_turn_compaction_summary_only_reinjects_context_shapes.snap b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_mid_turn_compaction_summary_only_reinjects_context_shapes.snap
index 4ede5c3cadd0..e99aa788e43e 100644
--- a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_mid_turn_compaction_summary_only_reinjects_context_shapes.snap
+++ b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_mid_turn_compaction_summary_only_reinjects_context_shapes.snap
@@ -1,6 +1,6 @@
 ---
 source: core/tests/suite/compact_remote.rs
-expression: "format_labeled_requests_snapshot(\"Remote mid-turn compaction where compact output has only summary user content: continuation layout reinjects canonical context before that summary.\",\n&[(\"Remote Compaction Request\", &compact_request),\n(\"Remote Post-Compaction History Layout\", &requests[1]),])"
+expression: "format_labeled_requests_snapshot(\"Remote mid-turn compaction where compact output has only summary user content: continuation layout reinjects canonical context before that summary.\",\n&[(\"Remote Compaction Request\", &compact_request),\n(\"Remote Post-Compaction History Layout\", &post_compact_turn_request),])"
 ---
 Scenario: Remote mid-turn compaction where compact output has only summary user content: continuation layout reinjects canonical context before that summary.
 
@@ -13,7 +13,4 @@ Scenario: Remote mid-turn compaction where compact output has only summary user
 05:function_call_output:unsupported call: test_tool
 
 ## Remote Post-Compaction History Layout
-00:message/developer:<PERMISSIONS_INSTRUCTIONS>
-01:message/user:<AGENTS_MD>
-02:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
-03:message/user:<COMPACTION_SUMMARY>\nREMOTE_SUMMARY_ONLY
+00:message/user:<COMPACTION_SUMMARY>\nREMOTE_SUMMARY_ONLY
diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_pre_turn_compaction_including_incoming_shapes.snap b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_pre_turn_compaction_including_incoming_shapes.snap
index 7b28f8151a86..7476a2db8fdb 100644
--- a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_pre_turn_compaction_including_incoming_shapes.snap
+++ b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_pre_turn_compaction_including_incoming_shapes.snap
@@ -1,6 +1,5 @@
 ---
 source: core/tests/suite/compact_remote.rs
-assertion_line: 1390
 expression: "format_labeled_requests_snapshot(\"Remote pre-turn auto-compaction with a context override includes incoming user content in the compact request and preserves it after compaction.\",\n&[(\"Remote Compaction Request\", &compact_request),\n(\"Remote Post-Compaction History Layout\", &requests[2]),])"
 ---
 Scenario: Remote pre-turn auto-compaction with a context override includes incoming user content in the compact request and preserves it after compaction.
@@ -19,9 +18,8 @@ Scenario: Remote pre-turn auto-compaction with a context override includes incom
 ## Remote Post-Compaction History Layout
 00:message/user:USER_ONE
 01:message/user:USER_TWO
-02:message/developer:<PERMISSIONS_INSTRUCTIONS>
-03:message/user:<AGENTS_MD>
-04:message/user:<ENVIRONMENT_CONTEXT:cwd=PRETURN_CONTEXT_DIFF_CWD>
-05:message/user:<COMPACTION_SUMMARY>\nREMOTE_PRE_TURN_SUMMARY
-06:message/user:<ENVIRONMENT_CONTEXT:cwd=PRETURN_CONTEXT_DIFF_CWD>
-07:message/user:USER_THREE
+02:message/user:<COMPACTION_SUMMARY>\nREMOTE_PRE_TURN_SUMMARY
+03:message/developer:<PERMISSIONS_INSTRUCTIONS>
+04:message/user:<AGENTS_MD>
+05:message/user:<ENVIRONMENT_CONTEXT:cwd=PRETURN_CONTEXT_DIFF_CWD>
+06:message/user:USER_THREE
diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_pre_turn_compaction_strips_incoming_model_switch_shapes.snap b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_pre_turn_compaction_strips_incoming_model_switch_shapes.snap
index 2b03802b7fb5..85d8073913ae 100644
--- a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_pre_turn_compaction_strips_incoming_model_switch_shapes.snap
+++ b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_pre_turn_compaction_strips_incoming_model_switch_shapes.snap
@@ -1,6 +1,5 @@
 ---
 source: core/tests/suite/compact_remote.rs
-assertion_line: 1538
 expression: "format_labeled_requests_snapshot(\"Remote pre-turn compaction during model switch strips incoming <model_switch> from the compact request and restores it in the post-compaction follow-up request.\",\n&[(\"Initial Request (Previous Model)\", &initial_turn_request),\n(\"Remote Compaction Request\", &compact_request),\n(\"Remote Post-Compaction History Layout\", &post_compact_turn_request),])"
 ---
 Scenario: Remote pre-turn compaction during model switch strips incoming <model_switch> from the compact request and restores it in the post-compaction follow-up request.
@@ -21,10 +20,10 @@ Scenario: Remote pre-turn compaction during model switch strips incoming <model_
 
 ## Remote Post-Compaction History Layout
 00:message/user:BEFORE_SWITCH_USER
-01:message/developer:<PERMISSIONS_INSTRUCTIONS>
-02:message/developer:<personality_spec> The user has requested a new communication st...
-03:message/user:<AGENTS_MD>
-04:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
-05:message/user:<COMPACTION_SUMMARY>\nREMOTE_SWITCH_SUMMARY
+01:message/user:<COMPACTION_SUMMARY>\nREMOTE_SWITCH_SUMMARY
+02:message/developer:<PERMISSIONS_INSTRUCTIONS>
+03:message/developer:<personality_spec> The user has requested a new communication st...
+04:message/user:<AGENTS_MD>
+05:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
 06:message/developer:<model_switch>\nThe user was previously using a different model....
 07:message/user:AFTER_SWITCH_USER

From d33a1bedd48c0b835e2771a7b8cc9f14c4425175 Mon Sep 17 00:00:00 2001
From: Charles Cunningham <ccunningham@openai.com>
Date: Tue, 17 Feb 2026 23:48:56 -0800
Subject: [PATCH 58/80] Simplify

---
 codex-rs/core/src/codex.rs          |  19 +-
 codex-rs/core/src/compact.rs        | 553 +---------------------------
 codex-rs/core/src/compact_remote.rs |  23 +-
 3 files changed, 19 insertions(+), 576 deletions(-)

diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs
index 6c8fdc7c8948..6ab4b87311fe 100644
--- a/codex-rs/core/src/codex.rs
+++ b/codex-rs/core/src/codex.rs
@@ -20,7 +20,6 @@ use crate::apps::render_apps_section;
 use crate::commit_attribution::commit_message_trailer_instruction;
 use crate::compact;
 use crate::compact::AutoCompactCallsite;
-use crate::compact::TurnContextReinjection;
 use crate::compact::run_inline_auto_compact_task;
 use crate::compact::should_use_remote_compact_task;
 use crate::compact_remote::run_inline_remote_auto_compact_task;
@@ -2407,16 +2406,9 @@ impl Session {
 
     pub(crate) async fn process_compacted_history(
         &self,
-        turn_context: &TurnContext,
         compacted_history: Vec<ResponseItem>,
-        turn_context_reinjection: TurnContextReinjection,
     ) -> Vec<ResponseItem> {
-        let initial_context = self.build_initial_context(turn_context).await;
-        compact::process_compacted_history(
-            compacted_history,
-            &initial_context,
-            turn_context_reinjection,
-        )
+        compact::process_compacted_history(compacted_history)
     }
 
     /// Append ResponseItems to the in-memory conversation history only.
@@ -4533,7 +4525,6 @@ pub(crate) async fn run_turn(
                         &sess,
                         &turn_context,
                         AutoCompactCallsite::MidTurnContinuation,
-                        TurnContextReinjection::Skip,
                         None,
                     )
                     .await
@@ -4687,7 +4678,6 @@ async fn maybe_run_previous_model_inline_compact(
         // We use previous turn context here because we compact with the previous model
         &previous_turn_context,
         AutoCompactCallsite::PreTurnExcludingIncomingUserMessage,
-        TurnContextReinjection::Skip,
         None,
     )
     .await
@@ -4811,7 +4801,6 @@ async fn run_pre_turn_auto_compaction_if_needed(
         sess,
         turn_context,
         AutoCompactCallsite::PreTurnIncludingIncomingUserMessage,
-        TurnContextReinjection::Skip,
         Some(incoming_turn_items.to_vec()),
     )
     .await;
@@ -4887,7 +4876,6 @@ async fn run_auto_compact(
     sess: &Arc<Session>,
     turn_context: &Arc<TurnContext>,
     auto_compact_callsite: AutoCompactCallsite,
-    turn_context_reinjection: TurnContextReinjection,
     incoming_items: Option<Vec<ResponseItem>>,
 ) -> CodexResult<()> {
     let result = if should_use_remote_compact_task(&turn_context.provider) {
@@ -4895,7 +4883,6 @@ async fn run_auto_compact(
             Arc::clone(sess),
             Arc::clone(turn_context),
             auto_compact_callsite,
-            turn_context_reinjection,
             incoming_items,
         )
         .await
@@ -4904,7 +4891,6 @@ async fn run_auto_compact(
             Arc::clone(sess),
             Arc::clone(turn_context),
             auto_compact_callsite,
-            turn_context_reinjection,
             incoming_items,
         )
         .await
@@ -6192,7 +6178,8 @@ mod tests {
         .await;
 
         let actual = session.clone_history().await.raw_items().to_vec();
-        let expected = vec![stale_pre_turn_context_items[0].clone(), response_item];
+        let mut expected = session.build_initial_context(turn_context.as_ref()).await;
+        expected.push(response_item);
         assert_eq!(actual, expected);
     }
 
diff --git a/codex-rs/core/src/compact.rs b/codex-rs/core/src/compact.rs
index fdc76109c31c..b3f3675fa3b3 100644
--- a/codex-rs/core/src/compact.rs
+++ b/codex-rs/core/src/compact.rs
@@ -48,19 +48,6 @@ pub(crate) enum AutoCompactCallsite {
     MidTurnContinuation,
 }
 
-/// Controls whether compacted-history processing should reinsert canonical turn context.
-///
-/// When callers exclude incoming user/context from the compaction request, they should typically
-/// set reinjection to `Skip` and append canonical context together with the next user message.
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub(crate) enum TurnContextReinjection {
-    /// Insert canonical context immediately above the last real user message in compacted history.
-    #[allow(dead_code)]
-    ReinjectAboveLastRealUser,
-    /// Do not reinsert canonical context while processing compacted history.
-    Skip,
-}
-
 pub(crate) fn should_use_remote_compact_task(provider: &ModelProviderInfo) -> bool {
     provider.is_openai()
 }
@@ -105,7 +92,6 @@ pub(crate) async fn run_inline_auto_compact_task(
     sess: Arc<Session>,
     turn_context: Arc<TurnContext>,
     auto_compact_callsite: AutoCompactCallsite,
-    turn_context_reinjection: TurnContextReinjection,
     incoming_items: Option<Vec<ResponseItem>>,
 ) -> CodexResult<()> {
     let prompt = turn_context.compact_prompt().to_string();
@@ -120,7 +106,6 @@ pub(crate) async fn run_inline_auto_compact_task(
         turn_context,
         input,
         Some(auto_compact_callsite),
-        turn_context_reinjection,
         incoming_items,
     )
     .await?;
@@ -138,17 +123,7 @@ pub(crate) async fn run_compact_task(
         collaboration_mode_kind: turn_context.collaboration_mode.mode,
     });
     sess.send_event(&turn_context, start_event).await;
-    run_compact_task_inner(
-        sess,
-        turn_context,
-        input,
-        None,
-        // Manual `/compact` should not reinsert turn context into compacted history; we reseed
-        // canonical initial context before the next user turn.
-        TurnContextReinjection::Skip,
-        None,
-    )
-    .await
+    run_compact_task_inner(sess, turn_context, input, None, None).await
 }
 
 async fn run_compact_task_inner(
@@ -156,7 +131,6 @@ async fn run_compact_task_inner(
     turn_context: Arc<TurnContext>,
     input: Vec<UserInput>,
     auto_compact_callsite: Option<AutoCompactCallsite>,
-    turn_context_reinjection: TurnContextReinjection,
     incoming_items: Option<Vec<ResponseItem>>,
 ) -> CodexResult<()> {
     let compaction_item = TurnItem::ContextCompaction(ContextCompactionItem::new());
@@ -302,22 +276,12 @@ async fn run_compact_task_inner(
     let summary_suffix = get_last_assistant_message_from_turn(history_items).unwrap_or_default();
     let summary_text = format!("{SUMMARY_PREFIX}\n{summary_suffix}");
     let user_messages = collect_user_messages(history_items);
-    let initial_context = match turn_context_reinjection {
-        TurnContextReinjection::ReinjectAboveLastRealUser => {
-            sess.build_initial_context(turn_context.as_ref()).await
-        }
-        TurnContextReinjection::Skip => Vec::new(),
-    };
     let compacted_history = build_compacted_history_with_limit(
         &user_messages,
         &summary_text,
         COMPACT_USER_MESSAGE_MAX_TOKENS,
     );
-    let mut new_history = process_compacted_history(
-        compacted_history,
-        &initial_context,
-        turn_context_reinjection,
-    );
+    let mut new_history = process_compacted_history(compacted_history);
     // Reattach stripped model-switch updates only for compaction paths that do not carry
     // incoming turn items. Pre-turn compaction appends turn context and user input after
     // compaction in run_turn.
@@ -391,41 +355,14 @@ pub(crate) fn is_summary_message(message: &str) -> bool {
 
 pub(crate) fn process_compacted_history(
     mut compacted_history: Vec<ResponseItem>,
-    initial_context: &[ResponseItem],
-    turn_context_reinjection: TurnContextReinjection,
 ) -> Vec<ResponseItem> {
     // Keep only model-visible transcript items that we allow from remote compaction output.
     compacted_history.retain(should_keep_compacted_history_item);
 
-    match turn_context_reinjection {
-        TurnContextReinjection::ReinjectAboveLastRealUser => {
-            // Prefer inserting immediately above the last real user message so turn context
-            // applies to that user input rather than an earlier turn. If compaction output has no
-            // real user messages, insert before the last summary user message to keep canonical
-            // context present for the next sampling request.
-            let insertion_index = if let Some(last_real_user_index) =
-                compacted_history.iter().rposition(is_real_user_message)
-            {
-                last_real_user_index
-            } else if let Some(last_summary_index) = compacted_history.iter().rposition(|item| {
-                matches!(
-                    crate::event_mapping::parse_turn_item(item),
-                    Some(TurnItem::UserMessage(user_message))
-                        if is_summary_message(&user_message.message())
-                )
-            }) {
-                last_summary_index
-            } else {
-                compacted_history.len()
-            };
-            compacted_history.splice(insertion_index..insertion_index, initial_context.to_vec());
-        }
-        TurnContextReinjection::Skip => {}
-    }
-
     compacted_history
 }
 
+#[cfg(test)]
 fn is_real_user_message(item: &ResponseItem) -> bool {
     matches!(
         crate::event_mapping::parse_turn_item(item),
@@ -1057,7 +994,7 @@ do things
     }
 
     #[test]
-    fn process_compacted_history_replaces_developer_messages() {
+    fn process_compacted_history_drops_developer_messages() {
         let compacted_history = vec![
             ResponseItem::Message {
                 id: None,
@@ -1087,93 +1024,9 @@ do things
                 phase: None,
             },
         ];
-        let initial_context = vec![
-            ResponseItem::Message {
-                id: None,
-                role: "developer".to_string(),
-                content: vec![ContentItem::InputText {
-                    text: "fresh permissions".to_string(),
-                }],
-                end_turn: None,
-                phase: None,
-            },
-            ResponseItem::Message {
-                id: None,
-                role: "user".to_string(),
-                content: vec![ContentItem::InputText {
-                    text: r#"<environment_context>
-  <cwd>/tmp</cwd>
-  <shell>zsh</shell>
-</environment_context>"#
-                        .to_string(),
-                }],
-                end_turn: None,
-                phase: None,
-            },
-            ResponseItem::Message {
-                id: None,
-                role: "developer".to_string(),
-                content: vec![ContentItem::InputText {
-                    text: "fresh personality".to_string(),
-                }],
-                end_turn: None,
-                phase: None,
-            },
-        ];
-
-        let refreshed = process_compacted_history(
-            compacted_history,
-            &initial_context,
-            TurnContextReinjection::ReinjectAboveLastRealUser,
-        );
-        let expected = vec![
-            ResponseItem::Message {
-                id: None,
-                role: "developer".to_string(),
-                content: vec![ContentItem::InputText {
-                    text: "fresh permissions".to_string(),
-                }],
-                end_turn: None,
-                phase: None,
-            },
-            ResponseItem::Message {
-                id: None,
-                role: "user".to_string(),
-                content: vec![ContentItem::InputText {
-                    text: r#"<environment_context>
-  <cwd>/tmp</cwd>
-  <shell>zsh</shell>
-</environment_context>"#
-                        .to_string(),
-                }],
-                end_turn: None,
-                phase: None,
-            },
-            ResponseItem::Message {
-                id: None,
-                role: "developer".to_string(),
-                content: vec![ContentItem::InputText {
-                    text: "fresh personality".to_string(),
-                }],
-                end_turn: None,
-                phase: None,
-            },
-            ResponseItem::Message {
-                id: None,
-                role: "user".to_string(),
-                content: vec![ContentItem::InputText {
-                    text: "summary".to_string(),
-                }],
-                end_turn: None,
-                phase: None,
-            },
-        ];
-        assert_eq!(refreshed, expected);
-    }
 
-    #[test]
-    fn process_compacted_history_reinjects_full_initial_context() {
-        let compacted_history = vec![ResponseItem::Message {
+        let refreshed = process_compacted_history(compacted_history);
+        let expected = vec![ResponseItem::Message {
             id: None,
             role: "user".to_string(),
             content: vec![ContentItem::InputText {
@@ -1182,123 +1035,6 @@ do things
             end_turn: None,
             phase: None,
         }];
-        let initial_context = vec![
-            ResponseItem::Message {
-                id: None,
-                role: "developer".to_string(),
-                content: vec![ContentItem::InputText {
-                    text: "fresh permissions".to_string(),
-                }],
-                end_turn: None,
-                phase: None,
-            },
-            ResponseItem::Message {
-                id: None,
-                role: "user".to_string(),
-                content: vec![ContentItem::InputText {
-                    text: r#"# AGENTS.md instructions for /repo
-
-<INSTRUCTIONS>
-keep me updated
-</INSTRUCTIONS>"#
-                        .to_string(),
-                }],
-                end_turn: None,
-                phase: None,
-            },
-            ResponseItem::Message {
-                id: None,
-                role: "user".to_string(),
-                content: vec![ContentItem::InputText {
-                    text: r#"<environment_context>
-  <cwd>/repo</cwd>
-  <shell>zsh</shell>
-</environment_context>"#
-                        .to_string(),
-                }],
-                end_turn: None,
-                phase: None,
-            },
-            ResponseItem::Message {
-                id: None,
-                role: "user".to_string(),
-                content: vec![ContentItem::InputText {
-                    text: r#"<turn_aborted>
-  <turn_id>turn-1</turn_id>
-  <reason>interrupted</reason>
-</turn_aborted>"#
-                        .to_string(),
-                }],
-                end_turn: None,
-                phase: None,
-            },
-        ];
-
-        let refreshed = process_compacted_history(
-            compacted_history,
-            &initial_context,
-            TurnContextReinjection::ReinjectAboveLastRealUser,
-        );
-        let expected = vec![
-            ResponseItem::Message {
-                id: None,
-                role: "developer".to_string(),
-                content: vec![ContentItem::InputText {
-                    text: "fresh permissions".to_string(),
-                }],
-                end_turn: None,
-                phase: None,
-            },
-            ResponseItem::Message {
-                id: None,
-                role: "user".to_string(),
-                content: vec![ContentItem::InputText {
-                    text: r#"# AGENTS.md instructions for /repo
-
-<INSTRUCTIONS>
-keep me updated
-</INSTRUCTIONS>"#
-                        .to_string(),
-                }],
-                end_turn: None,
-                phase: None,
-            },
-            ResponseItem::Message {
-                id: None,
-                role: "user".to_string(),
-                content: vec![ContentItem::InputText {
-                    text: r#"<environment_context>
-  <cwd>/repo</cwd>
-  <shell>zsh</shell>
-</environment_context>"#
-                        .to_string(),
-                }],
-                end_turn: None,
-                phase: None,
-            },
-            ResponseItem::Message {
-                id: None,
-                role: "user".to_string(),
-                content: vec![ContentItem::InputText {
-                    text: r#"<turn_aborted>
-  <turn_id>turn-1</turn_id>
-  <reason>interrupted</reason>
-</turn_aborted>"#
-                        .to_string(),
-                }],
-                end_turn: None,
-                phase: None,
-            },
-            ResponseItem::Message {
-                id: None,
-                role: "user".to_string(),
-                content: vec![ContentItem::InputText {
-                    text: "summary".to_string(),
-                }],
-                end_turn: None,
-                phase: None,
-            },
-        ];
         assert_eq!(refreshed, expected);
     }
 
@@ -1364,197 +1100,17 @@ keep me updated
                 phase: None,
             },
         ];
-        let initial_context = vec![ResponseItem::Message {
-            id: None,
-            role: "developer".to_string(),
-            content: vec![ContentItem::InputText {
-                text: "fresh developer instructions".to_string(),
-            }],
-            end_turn: None,
-            phase: None,
-        }];
-
-        let refreshed = process_compacted_history(
-            compacted_history,
-            &initial_context,
-            TurnContextReinjection::ReinjectAboveLastRealUser,
-        );
-        let expected = vec![
-            ResponseItem::Message {
-                id: None,
-                role: "developer".to_string(),
-                content: vec![ContentItem::InputText {
-                    text: "fresh developer instructions".to_string(),
-                }],
-                end_turn: None,
-                phase: None,
-            },
-            ResponseItem::Message {
-                id: None,
-                role: "user".to_string(),
-                content: vec![ContentItem::InputText {
-                    text: "summary".to_string(),
-                }],
-                end_turn: None,
-                phase: None,
-            },
-        ];
-        assert_eq!(refreshed, expected);
-    }
-
-    #[test]
-    fn process_compacted_history_inserts_context_before_last_real_user_message_only() {
-        let compacted_history = vec![
-            ResponseItem::Message {
-                id: None,
-                role: "user".to_string(),
-                content: vec![ContentItem::InputText {
-                    text: "older user".to_string(),
-                }],
-                end_turn: None,
-                phase: None,
-            },
-            ResponseItem::Message {
-                id: None,
-                role: "user".to_string(),
-                content: vec![ContentItem::InputText {
-                    text: format!("{SUMMARY_PREFIX}\nsummary text"),
-                }],
-                end_turn: None,
-                phase: None,
-            },
-            ResponseItem::Message {
-                id: None,
-                role: "user".to_string(),
-                content: vec![ContentItem::InputText {
-                    text: "latest user".to_string(),
-                }],
-                end_turn: None,
-                phase: None,
-            },
-        ];
-        let initial_context = vec![ResponseItem::Message {
-            id: None,
-            role: "developer".to_string(),
-            content: vec![ContentItem::InputText {
-                text: "fresh permissions".to_string(),
-            }],
-            end_turn: None,
-            phase: None,
-        }];
-
-        let refreshed = process_compacted_history(
-            compacted_history,
-            &initial_context,
-            TurnContextReinjection::ReinjectAboveLastRealUser,
-        );
-        let expected = vec![
-            ResponseItem::Message {
-                id: None,
-                role: "user".to_string(),
-                content: vec![ContentItem::InputText {
-                    text: "older user".to_string(),
-                }],
-                end_turn: None,
-                phase: None,
-            },
-            ResponseItem::Message {
-                id: None,
-                role: "user".to_string(),
-                content: vec![ContentItem::InputText {
-                    text: format!("{SUMMARY_PREFIX}\nsummary text"),
-                }],
-                end_turn: None,
-                phase: None,
-            },
-            ResponseItem::Message {
-                id: None,
-                role: "developer".to_string(),
-                content: vec![ContentItem::InputText {
-                    text: "fresh permissions".to_string(),
-                }],
-                end_turn: None,
-                phase: None,
-            },
-            ResponseItem::Message {
-                id: None,
-                role: "user".to_string(),
-                content: vec![ContentItem::InputText {
-                    text: "latest user".to_string(),
-                }],
-                end_turn: None,
-                phase: None,
-            },
-        ];
-        assert_eq!(refreshed, expected);
-    }
 
-    #[test]
-    fn process_compacted_history_pre_turn_places_summary_last() {
-        let compacted_history = vec![
-            ResponseItem::Message {
-                id: None,
-                role: "user".to_string(),
-                content: vec![ContentItem::InputText {
-                    text: "older user".to_string(),
-                }],
-                end_turn: None,
-                phase: None,
-            },
-            ResponseItem::Message {
-                id: None,
-                role: "user".to_string(),
-                content: vec![ContentItem::InputText {
-                    text: format!("{SUMMARY_PREFIX}\nsummary text"),
-                }],
-                end_turn: None,
-                phase: None,
-            },
-        ];
-        let initial_context = vec![ResponseItem::Message {
+        let refreshed = process_compacted_history(compacted_history);
+        let expected = vec![ResponseItem::Message {
             id: None,
-            role: "developer".to_string(),
+            role: "user".to_string(),
             content: vec![ContentItem::InputText {
-                text: "fresh permissions".to_string(),
+                text: "summary".to_string(),
             }],
             end_turn: None,
             phase: None,
         }];
-
-        let refreshed = process_compacted_history(
-            compacted_history,
-            &initial_context,
-            TurnContextReinjection::ReinjectAboveLastRealUser,
-        );
-        let expected = vec![
-            ResponseItem::Message {
-                id: None,
-                role: "developer".to_string(),
-                content: vec![ContentItem::InputText {
-                    text: "fresh permissions".to_string(),
-                }],
-                end_turn: None,
-                phase: None,
-            },
-            ResponseItem::Message {
-                id: None,
-                role: "user".to_string(),
-                content: vec![ContentItem::InputText {
-                    text: "older user".to_string(),
-                }],
-                end_turn: None,
-                phase: None,
-            },
-            ResponseItem::Message {
-                id: None,
-                role: "user".to_string(),
-                content: vec![ContentItem::InputText {
-                    text: format!("{SUMMARY_PREFIX}\nsummary text"),
-                }],
-                end_turn: None,
-                phase: None,
-            },
-        ];
         assert_eq!(refreshed, expected);
     }
 
@@ -1608,8 +1164,7 @@ keep me updated
             },
         ];
 
-        let refreshed =
-            process_compacted_history(compacted_history, &[], TurnContextReinjection::Skip);
+        let refreshed = process_compacted_history(compacted_history);
         let expected = vec![
             ResponseItem::Message {
                 id: None,
@@ -1652,7 +1207,7 @@ keep me updated
     }
 
     #[test]
-    fn process_compacted_history_skips_context_insertion_without_real_user_message() {
+    fn process_compacted_history_keeps_summary_only_history() {
         let compacted_history = vec![ResponseItem::Message {
             id: None,
             role: "user".to_string(),
@@ -1662,21 +1217,8 @@ keep me updated
             end_turn: None,
             phase: None,
         }];
-        let initial_context = vec![ResponseItem::Message {
-            id: None,
-            role: "developer".to_string(),
-            content: vec![ContentItem::InputText {
-                text: "fresh permissions".to_string(),
-            }],
-            end_turn: None,
-            phase: None,
-        }];
 
-        let refreshed = process_compacted_history(
-            compacted_history,
-            &initial_context,
-            TurnContextReinjection::Skip,
-        );
+        let refreshed = process_compacted_history(compacted_history);
         let expected = vec![ResponseItem::Message {
             id: None,
             role: "user".to_string(),
@@ -1688,73 +1230,4 @@ keep me updated
         }];
         assert_eq!(refreshed, expected);
     }
-
-    #[test]
-    fn process_compacted_history_mid_turn_without_orphan_user_places_summary_last() {
-        let compacted_history = vec![
-            ResponseItem::Message {
-                id: None,
-                role: "user".to_string(),
-                content: vec![ContentItem::InputText {
-                    text: "older user".to_string(),
-                }],
-                end_turn: None,
-                phase: None,
-            },
-            ResponseItem::Message {
-                id: None,
-                role: "user".to_string(),
-                content: vec![ContentItem::InputText {
-                    text: format!("{SUMMARY_PREFIX}\nsummary text"),
-                }],
-                end_turn: None,
-                phase: None,
-            },
-        ];
-        let initial_context = vec![ResponseItem::Message {
-            id: None,
-            role: "developer".to_string(),
-            content: vec![ContentItem::InputText {
-                text: "fresh permissions".to_string(),
-            }],
-            end_turn: None,
-            phase: None,
-        }];
-
-        let refreshed = process_compacted_history(
-            compacted_history,
-            &initial_context,
-            TurnContextReinjection::ReinjectAboveLastRealUser,
-        );
-        let expected = vec![
-            ResponseItem::Message {
-                id: None,
-                role: "developer".to_string(),
-                content: vec![ContentItem::InputText {
-                    text: "fresh permissions".to_string(),
-                }],
-                end_turn: None,
-                phase: None,
-            },
-            ResponseItem::Message {
-                id: None,
-                role: "user".to_string(),
-                content: vec![ContentItem::InputText {
-                    text: "older user".to_string(),
-                }],
-                end_turn: None,
-                phase: None,
-            },
-            ResponseItem::Message {
-                id: None,
-                role: "user".to_string(),
-                content: vec![ContentItem::InputText {
-                    text: format!("{SUMMARY_PREFIX}\nsummary text"),
-                }],
-                end_turn: None,
-                phase: None,
-            },
-        ];
-        assert_eq!(refreshed, expected);
-    }
 }
diff --git a/codex-rs/core/src/compact_remote.rs b/codex-rs/core/src/compact_remote.rs
index f9c88538aa17..c0a289288a79 100644
--- a/codex-rs/core/src/compact_remote.rs
+++ b/codex-rs/core/src/compact_remote.rs
@@ -4,7 +4,6 @@ use crate::Prompt;
 use crate::codex::Session;
 use crate::codex::TurnContext;
 use crate::compact::AutoCompactCallsite;
-use crate::compact::TurnContextReinjection;
 use crate::compact::extract_latest_model_switch_update_from_items;
 use crate::compact::extract_trailing_model_switch_update_for_compaction_request;
 use crate::compact::should_keep_compacted_history_item;
@@ -32,18 +31,10 @@ pub(crate) async fn run_inline_remote_auto_compact_task(
     sess: Arc<Session>,
     turn_context: Arc<TurnContext>,
     auto_compact_callsite: AutoCompactCallsite,
-    // Controls whether canonical turn context should be reinserted into compacted history.
-    turn_context_reinjection: TurnContextReinjection,
     incoming_items: Option<Vec<ResponseItem>>,
 ) -> CodexResult<()> {
-    run_remote_compact_task_inner(
-        &sess,
-        &turn_context,
-        auto_compact_callsite,
-        turn_context_reinjection,
-        incoming_items,
-    )
-    .await?;
+    run_remote_compact_task_inner(&sess, &turn_context, auto_compact_callsite, incoming_items)
+        .await?;
     Ok(())
 }
 
@@ -62,9 +53,6 @@ pub(crate) async fn run_remote_compact_task(
         &sess,
         &turn_context,
         AutoCompactCallsite::PreTurnExcludingIncomingUserMessage,
-        // Manual `/compact` should not reinsert turn context into compacted history; we reseed
-        // canonical initial context before the next user turn.
-        TurnContextReinjection::Skip,
         None,
     )
     .await
@@ -74,14 +62,12 @@ async fn run_remote_compact_task_inner(
     sess: &Arc<Session>,
     turn_context: &Arc<TurnContext>,
     auto_compact_callsite: AutoCompactCallsite,
-    turn_context_reinjection: TurnContextReinjection,
     incoming_items: Option<Vec<ResponseItem>>,
 ) -> CodexResult<()> {
     if let Err(err) = run_remote_compact_task_inner_impl(
         sess,
         turn_context,
         auto_compact_callsite,
-        turn_context_reinjection,
         incoming_items,
     )
     .await
@@ -101,7 +87,6 @@ async fn run_remote_compact_task_inner_impl(
     sess: &Arc<Session>,
     turn_context: &Arc<TurnContext>,
     auto_compact_callsite: AutoCompactCallsite,
-    turn_context_reinjection: TurnContextReinjection,
     incoming_items: Option<Vec<ResponseItem>>,
 ) -> CodexResult<()> {
     let compaction_item = TurnItem::ContextCompaction(ContextCompactionItem::new());
@@ -180,9 +165,7 @@ async fn run_remote_compact_task_inner_impl(
             Err(err)
         })
         .await?;
-    new_history = sess
-        .process_compacted_history(turn_context, new_history, turn_context_reinjection)
-        .await;
+    new_history = sess.process_compacted_history(new_history).await;
     if let Some(incoming_items) = incoming_items.as_ref() {
         let incoming_history_items: Vec<ResponseItem> = incoming_items
             .iter()

From 714e0b78baa688f7e32ee18a4c5408e1e6052869 Mon Sep 17 00:00:00 2001
From: Charles Cunningham <ccunningham@openai.com>
Date: Wed, 18 Feb 2026 00:08:39 -0800
Subject: [PATCH 59/80] Align compaction tests with always-skip post-layout

---
 codex-rs/core/tests/suite/compact.rs          | 409 ++----------------
 codex-rs/core/tests/suite/compact_remote.rs   |  12 +-
 ...mpling_model_switch_compaction_shapes.snap |  13 +-
 ...turn_compaction_multi_summary_shapes.snap} |   1 +
 ..._turn_compaction_summary_only_shapes.snap} |   5 +-
 5 files changed, 49 insertions(+), 391 deletions(-)
 rename codex-rs/core/tests/suite/snapshots/{all__suite__compact_remote__remote_mid_turn_compaction_multi_summary_reinjects_above_last_summary_shapes.snap => all__suite__compact_remote__remote_mid_turn_compaction_multi_summary_shapes.snap} (98%)
 rename codex-rs/core/tests/suite/snapshots/{all__suite__compact_remote__remote_mid_turn_compaction_summary_only_reinjects_context_shapes.snap => all__suite__compact_remote__remote_mid_turn_compaction_summary_only_shapes.snap} (61%)

diff --git a/codex-rs/core/tests/suite/compact.rs b/codex-rs/core/tests/suite/compact.rs
index d7051dc24519..6690609a62bd 100644
--- a/codex-rs/core/tests/suite/compact.rs
+++ b/codex-rs/core/tests/suite/compact.rs
@@ -45,7 +45,6 @@ use core_test_support::responses::sse_failed;
 use core_test_support::responses::sse_response;
 use core_test_support::responses::start_mock_server;
 use pretty_assertions::assert_eq;
-use serde_json::json;
 use wiremock::MockServer;
 // --- Test helpers -----------------------------------------------------------
 
@@ -143,8 +142,8 @@ fn assert_pre_sampling_switch_compaction_requests(
         "follow-up request after successful model-switch compaction should include model-switch update item"
     );
     assert!(
-        body_contains_text(&follow_up_body, "<environment_context>"),
-        "follow-up request should preserve canonical environment context after pre-sampling compaction"
+        !body_contains_text(&follow_up_body, "<environment_context>"),
+        "follow-up request should not reinsert canonical environment context after pre-sampling compaction"
     );
 }
 
@@ -680,10 +679,6 @@ async fn multiple_auto_compact_per_task_runs_after_token_limit_hit() {
     // mock responses from the model
 
     let reasoning_response_1 = ev_reasoning_item("m1", &["I will create a react app"], &[]);
-    let encrypted_content_1 = reasoning_response_1["item"]["encrypted_content"]
-        .as_str()
-        .unwrap();
-
     // first chunk of work
     let model_reasoning_response_1_sse = sse(vec![
         reasoning_response_1.clone(),
@@ -698,10 +693,6 @@ async fn multiple_auto_compact_per_task_runs_after_token_limit_hit() {
     ]);
 
     let reasoning_response_2 = ev_reasoning_item("m3", &["I will create a node app"], &[]);
-    let encrypted_content_2 = reasoning_response_2["item"]["encrypted_content"]
-        .as_str()
-        .unwrap();
-
     // second chunk of work
     let model_reasoning_response_2_sse = sse(vec![
         reasoning_response_2.clone(),
@@ -716,13 +707,9 @@ async fn multiple_auto_compact_per_task_runs_after_token_limit_hit() {
     ]);
 
     let reasoning_response_3 = ev_reasoning_item("m6", &["I will create a python app"], &[]);
-    let encrypted_content_3 = reasoning_response_3["item"]["encrypted_content"]
-        .as_str()
-        .unwrap();
-
     // third chunk of work
     let model_reasoning_response_3_sse = sse(vec![
-        ev_reasoning_item("m6", &["I will create a python app"], &[]),
+        reasoning_response_3.clone(),
         ev_local_shell_call("r6-shell", "completed", vec!["echo", "make-python"]),
         ev_completed_with_tokens("r6", token_count_used),
     ]);
@@ -806,9 +793,21 @@ async fn multiple_auto_compact_per_task_runs_after_token_limit_hit() {
     }
 
     let initial_input = normalize_inputs(input);
-    let environment_message = initial_input[0]["content"][0]["text"].as_str().unwrap();
+    assert!(
+        initial_input.iter().any(|value| {
+            value
+                .get("content")
+                .and_then(|content| content.as_array())
+                .and_then(|content| content.first())
+                .and_then(|item| item.get("text"))
+                .and_then(|text| text.as_str())
+                .is_some_and(|text| text.contains("<environment_context>"))
+        }),
+        "first request should include canonical environment context"
+    );
 
-    // test 1: after compaction, we should have one environment message, one user message, and one user message with summary prefix
+    // test 1: after each compaction, the next model request should include
+    // only the latest user message and the newest summary.
     let compaction_indices = [2, 4, 6];
     let expected_summaries = [
         prefixed_first_summary.as_str(),
@@ -819,11 +818,9 @@ async fn multiple_auto_compact_per_task_runs_after_token_limit_hit() {
         let body = requests_payloads.clone()[i].body_json();
         let input = body.get("input").and_then(|v| v.as_array()).unwrap();
         let input = normalize_inputs(input);
-        assert_eq!(input.len(), 3);
-        let environment_message = input[0]["content"][0]["text"].as_str().unwrap();
-        let user_message_received = input[1]["content"][0]["text"].as_str().unwrap();
-        let summary_message = input[2]["content"][0]["text"].as_str().unwrap();
-        assert_eq!(environment_message, environment_message);
+        assert_eq!(input.len(), 2);
+        let user_message_received = input[0]["content"][0]["text"].as_str().unwrap();
+        let summary_message = input[1]["content"][0]["text"].as_str().unwrap();
         assert_eq!(user_message_received, user_message);
         assert_eq!(
             summary_message, expected_summary,
@@ -831,358 +828,22 @@ async fn multiple_auto_compact_per_task_runs_after_token_limit_hit() {
         );
     }
 
-    // test 2: the expected requests inputs should be as follows:
-    let expected_requests_inputs = json!([
-    [
-        // 0: first request of the user message.
-      {
-        "content": [
-          {
-            "text": environment_message,
-            "type": "input_text"
-          }
-        ],
-        "role": "user",
-        "type": "message"
-      },
-      {
-        "content": [
-          {
-            "text": "create an app",
-            "type": "input_text"
-          }
-        ],
-        "role": "user",
-        "type": "message"
-      }
-    ]
-    ,
-    [
-        // 1: first automatic compaction request.
-      {
-        "content": [
-          {
-            "text": environment_message,
-            "type": "input_text"
-          }
-        ],
-        "role": "user",
-        "type": "message"
-      },
-      {
-        "content": [
-          {
-            "text": "create an app",
-            "type": "input_text"
-          }
-        ],
-        "role": "user",
-        "type": "message"
-      },
-      {
-        "content": null,
-        "encrypted_content": encrypted_content_1,
-        "summary": [
-          {
-            "text": "I will create a react app",
-            "type": "summary_text"
-          }
-        ],
-        "type": "reasoning"
-      },
-      {
-        "action": {
-          "command": [
-            "echo",
-            "make-react"
-          ],
-          "env": null,
-          "timeout_ms": null,
-          "type": "exec",
-          "user": null,
-          "working_directory": null
-        },
-        "call_id": "r1-shell",
-        "status": "completed",
-        "type": "local_shell_call"
-      },
-      {
-        "call_id": "r1-shell",
-        "output": "execution error: Io(Os { code: 2, kind: NotFound, message: \"No such file or directory\" })",
-        "type": "function_call_output"
-      },
-      {
-        "content": [
-          {
-            "text": SUMMARIZATION_PROMPT,
-            "type": "input_text"
-          }
-        ],
-        "role": "user",
-        "type": "message"
-      }
-    ]
-    ,
-    [
-      // 2: request after first automatic compaction.
-      {
-        "content": [
-          {
-            "text": environment_message,
-            "type": "input_text"
-          }
-        ],
-        "role": "user",
-        "type": "message"
-      },
-      {
-        "content": [
-          {
-            "text": "create an app",
-            "type": "input_text"
-          }
-        ],
-        "role": "user",
-        "type": "message"
-      },
-      {
-        "content": [
-          {
-            "text": prefixed_first_summary.clone(),
-            "type": "input_text"
-          }
-        ],
-        "role": "user",
-        "type": "message"
-      }
-    ]
-    ,
-    [
-        // 3: request for second automatic compaction.
-      {
-        "content": [
-          {
-            "text": environment_message,
-            "type": "input_text"
-          }
-        ],
-        "role": "user",
-        "type": "message"
-      },
-      {
-        "content": [
-          {
-            "text": "create an app",
-            "type": "input_text"
-          }
-        ],
-        "role": "user",
-        "type": "message"
-      },
-      {
-        "content": [
-          {
-            "text": prefixed_first_summary.clone(),
-            "type": "input_text"
-          }
-        ],
-        "role": "user",
-        "type": "message"
-      },
-      {
-        "content": null,
-        "encrypted_content": encrypted_content_2,
-        "summary": [
-          {
-            "text": "I will create a node app",
-            "type": "summary_text"
-          }
-        ],
-        "type": "reasoning"
-      },
-      {
-        "action": {
-          "command": [
-            "echo",
-            "make-node"
-          ],
-          "env": null,
-          "timeout_ms": null,
-          "type": "exec",
-          "user": null,
-          "working_directory": null
-        },
-        "call_id": "r3-shell",
-        "status": "completed",
-        "type": "local_shell_call"
-      },
-      {
-        "call_id": "r3-shell",
-        "output": "execution error: Io(Os { code: 2, kind: NotFound, message: \"No such file or directory\" })",
-        "type": "function_call_output"
-      },
-      {
-        "content": [
-          {
-            "text": SUMMARIZATION_PROMPT,
-            "type": "input_text"
-          }
-        ],
-        "role": "user",
-        "type": "message"
-      }
-    ]
-    ,
-    // 4: request after second automatic compaction.
-    [
-      {
-        "content": [
-          {
-            "text": environment_message,
-            "type": "input_text"
-          }
-        ],
-        "role": "user",
-        "type": "message"
-      },
-      {
-        "content": [
-          {
-            "text": "create an app",
-            "type": "input_text"
-          }
-        ],
-        "role": "user",
-        "type": "message"
-      },
-      {
-        "content": [
-          {
-            "text": prefixed_second_summary.clone(),
-            "type": "input_text"
-          }
-        ],
-        "role": "user",
-        "type": "message"
-      }
-    ]
-    ,
-    [
-      // 5: request for third automatic compaction.
-      {
-        "content": [
-          {
-            "text": environment_message,
-            "type": "input_text"
-          }
-        ],
-        "role": "user",
-        "type": "message"
-      },
-      {
-        "content": [
-          {
-            "text": "create an app",
-            "type": "input_text"
-          }
-        ],
-        "role": "user",
-        "type": "message"
-      },
-      {
-        "content": [
-          {
-            "text": prefixed_second_summary.clone(),
-            "type": "input_text"
-          }
-        ],
-        "role": "user",
-        "type": "message"
-      },
-      {
-        "content": null,
-        "encrypted_content": encrypted_content_3,
-        "summary": [
-          {
-            "text": "I will create a python app",
-            "type": "summary_text"
-          }
-        ],
-        "type": "reasoning"
-      },
-      {
-        "action": {
-          "command": [
-            "echo",
-            "make-python"
-          ],
-          "env": null,
-          "timeout_ms": null,
-          "type": "exec",
-          "user": null,
-          "working_directory": null
-        },
-        "call_id": "r6-shell",
-        "status": "completed",
-        "type": "local_shell_call"
-      },
-      {
-        "call_id": "r6-shell",
-        "output": "execution error: Io(Os { code: 2, kind: NotFound, message: \"No such file or directory\" })",
-        "type": "function_call_output"
-      },
-      {
-        "content": [
-          {
-            "text": SUMMARIZATION_PROMPT,
-            "type": "input_text"
-          }
-        ],
-        "role": "user",
-        "type": "message"
-      }
-    ]
-    ,
-    [
-      {
-        // 6: request after third automatic compaction.
-        "content": [
-          {
-            "text": environment_message,
-            "type": "input_text"
-          }
-        ],
-        "role": "user",
-        "type": "message"
-      },
-      {
-        "content": [
-          {
-            "text": "create an app",
-            "type": "input_text"
-          }
-        ],
-        "role": "user",
-        "type": "message"
-      },
-      {
-        "content": [
-          {
-            "text": prefixed_third_summary.clone(),
-            "type": "input_text"
-          }
-        ],
-        "role": "user",
-        "type": "message"
-      }
-    ]
-    ]);
-
-    for (i, request) in requests_payloads.iter().enumerate() {
-        let body = request.body_json();
+    // test 2: each auto-compaction request should include the summarization prompt.
+    for i in [1, 3, 5] {
+        let body = requests_payloads[i].body_json();
         let input = body.get("input").and_then(|v| v.as_array()).unwrap();
-        let expected_input = expected_requests_inputs[i].as_array().unwrap();
-        assert_eq!(normalize_inputs(input), normalize_inputs(expected_input));
+        assert!(
+            input.iter().any(|value| {
+                value
+                    .get("content")
+                    .and_then(|content| content.as_array())
+                    .and_then(|content| content.first())
+                    .and_then(|item| item.get("text"))
+                    .and_then(|text| text.as_str())
+                    .is_some_and(|text| text == SUMMARIZATION_PROMPT)
+            }),
+            "compaction request {i} should include summarization prompt"
+        );
     }
 
     // test 3: the number of requests should be 7
diff --git a/codex-rs/core/tests/suite/compact_remote.rs b/codex-rs/core/tests/suite/compact_remote.rs
index eab0b6c52219..e4168f92d30e 100644
--- a/codex-rs/core/tests/suite/compact_remote.rs
+++ b/codex-rs/core/tests/suite/compact_remote.rs
@@ -1723,8 +1723,7 @@ async fn snapshot_request_shape_remote_mid_turn_continuation_compaction() -> Res
 }
 
 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
-async fn snapshot_request_shape_remote_mid_turn_compaction_summary_only_reinjects_context()
--> Result<()> {
+async fn snapshot_request_shape_remote_mid_turn_compaction_summary_only_layout() -> Result<()> {
     skip_if_no_network!(Ok(()));
 
     let harness = TestCodexHarness::with_builder(
@@ -1789,9 +1788,9 @@ async fn snapshot_request_shape_remote_mid_turn_compaction_summary_only_reinject
     let compact_request = compact_mock.single_request();
     let post_compact_turn_request = post_compact_turn_request_mock.single_request();
     insta::assert_snapshot!(
-        "remote_mid_turn_compaction_summary_only_reinjects_context_shapes",
+        "remote_mid_turn_compaction_summary_only_shapes",
         format_labeled_requests_snapshot(
-            "Remote mid-turn compaction where compact output has only summary user content: continuation layout reinjects canonical context before that summary.",
+            "Remote mid-turn compaction where compact output has only summary user content: continuation layout keeps the summary-only compact output without inserting extra context items.",
             &[
                 ("Remote Compaction Request", &compact_request),
                 (
@@ -1806,8 +1805,7 @@ async fn snapshot_request_shape_remote_mid_turn_compaction_summary_only_reinject
 }
 
 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
-async fn snapshot_request_shape_remote_mid_turn_compaction_multi_summary_reinjects_above_last_summary()
--> Result<()> {
+async fn snapshot_request_shape_remote_mid_turn_compaction_multi_summary_layout() -> Result<()> {
     skip_if_no_network!(Ok(()));
 
     let harness = TestCodexHarness::with_builder(
@@ -1896,7 +1894,7 @@ async fn snapshot_request_shape_remote_mid_turn_compaction_multi_summary_reinjec
         "older summary should round-trip from conversation history into the next compact request"
     );
     insta::assert_snapshot!(
-        "remote_mid_turn_compaction_multi_summary_reinjects_above_last_summary_shapes",
+        "remote_mid_turn_compaction_multi_summary_shapes",
         format_labeled_requests_snapshot(
             "Remote mid-turn compaction after an earlier summary compaction: the older summary remains in model-visible history and round-trips into the next compact request.",
             &[
diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_sampling_model_switch_compaction_shapes.snap b/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_sampling_model_switch_compaction_shapes.snap
index 8d6c4d9b1aa3..da2fc5ce71d7 100644
--- a/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_sampling_model_switch_compaction_shapes.snap
+++ b/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_sampling_model_switch_compaction_shapes.snap
@@ -1,6 +1,6 @@
 ---
 source: core/tests/suite/compact.rs
-assertion_line: 1773
+assertion_line: 1460
 expression: "format_labeled_requests_snapshot(\"Pre-sampling compaction on model switch to a smaller context window: current behavior compacts using prior-turn history only (incoming user message excluded), and the follow-up request carries compacted history plus the new user message.\",\n&[(\"Initial Request (Previous Model)\", &requests[0]),\n(\"Pre-sampling Compaction Request\", &requests[1]),\n(\"Post-Compaction Follow-up Request (Next Model)\", &requests[2]),])"
 ---
 Scenario: Pre-sampling compaction on model switch to a smaller context window: current behavior compacts using prior-turn history only (incoming user message excluded), and the follow-up request carries compacted history plus the new user message.
@@ -22,10 +22,7 @@ Scenario: Pre-sampling compaction on model switch to a smaller context window: c
 06:message/user:<SUMMARIZATION_PROMPT>
 
 ## Post-Compaction Follow-up Request (Next Model)
-00:message/developer:<PERMISSIONS_INSTRUCTIONS>
-01:message/user:<AGENTS_MD>
-02:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
-03:message/user:before switch
-04:message/user:<COMPACTION_SUMMARY>\nPRE_SAMPLING_SUMMARY
-05:message/developer:<model_switch>\nThe user was previously using a different model....
-06:message/user:after switch
+00:message/user:before switch
+01:message/user:<COMPACTION_SUMMARY>\nPRE_SAMPLING_SUMMARY
+02:message/developer:<model_switch>\nThe user was previously using a different model....
+03:message/user:after switch
diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_mid_turn_compaction_multi_summary_reinjects_above_last_summary_shapes.snap b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_mid_turn_compaction_multi_summary_shapes.snap
similarity index 98%
rename from codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_mid_turn_compaction_multi_summary_reinjects_above_last_summary_shapes.snap
rename to codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_mid_turn_compaction_multi_summary_shapes.snap
index f8b97f47d98d..8107eaa37d12 100644
--- a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_mid_turn_compaction_multi_summary_reinjects_above_last_summary_shapes.snap
+++ b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_mid_turn_compaction_multi_summary_shapes.snap
@@ -1,5 +1,6 @@
 ---
 source: core/tests/suite/compact_remote.rs
+assertion_line: 1896
 expression: "format_labeled_requests_snapshot(\"Remote mid-turn compaction after an earlier summary compaction: the older summary remains in model-visible history and round-trips into the next compact request.\",\n&[(\"Second Turn Request (Before Mid-Turn Compaction)\", &second_turn_request),\n(\"Remote Compaction Request\", &compact_request),])"
 ---
 Scenario: Remote mid-turn compaction after an earlier summary compaction: the older summary remains in model-visible history and round-trips into the next compact request.
diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_mid_turn_compaction_summary_only_reinjects_context_shapes.snap b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_mid_turn_compaction_summary_only_shapes.snap
similarity index 61%
rename from codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_mid_turn_compaction_summary_only_reinjects_context_shapes.snap
rename to codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_mid_turn_compaction_summary_only_shapes.snap
index e99aa788e43e..109f91e46e35 100644
--- a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_mid_turn_compaction_summary_only_reinjects_context_shapes.snap
+++ b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_mid_turn_compaction_summary_only_shapes.snap
@@ -1,8 +1,9 @@
 ---
 source: core/tests/suite/compact_remote.rs
-expression: "format_labeled_requests_snapshot(\"Remote mid-turn compaction where compact output has only summary user content: continuation layout reinjects canonical context before that summary.\",\n&[(\"Remote Compaction Request\", &compact_request),\n(\"Remote Post-Compaction History Layout\", &post_compact_turn_request),])"
+assertion_line: 1790
+expression: "format_labeled_requests_snapshot(\"Remote mid-turn compaction where compact output has only summary user content: continuation layout keeps the summary-only compact output without inserting extra context items.\",\n&[(\"Remote Compaction Request\", &compact_request),\n(\"Remote Post-Compaction History Layout\", &post_compact_turn_request),])"
 ---
-Scenario: Remote mid-turn compaction where compact output has only summary user content: continuation layout reinjects canonical context before that summary.
+Scenario: Remote mid-turn compaction where compact output has only summary user content: continuation layout keeps the summary-only compact output without inserting extra context items.
 
 ## Remote Compaction Request
 00:message/developer:<PERMISSIONS_INSTRUCTIONS>

From a46bcf3b8e4d01436b398818d6bb23cec8332e80 Mon Sep 17 00:00:00 2001
From: Charles Cunningham <ccunningham@openai.com>
Date: Wed, 18 Feb 2026 00:23:16 -0800
Subject: [PATCH 60/80] Inline compacted history shaping call

---
 codex-rs/core/src/codex.rs          | 7 -------
 codex-rs/core/src/compact_remote.rs | 3 ++-
 2 files changed, 2 insertions(+), 8 deletions(-)

diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs
index 6ab4b87311fe..9b1e4afb4172 100644
--- a/codex-rs/core/src/codex.rs
+++ b/codex-rs/core/src/codex.rs
@@ -2404,13 +2404,6 @@ impl Session {
         history.raw_items().to_vec()
     }
 
-    pub(crate) async fn process_compacted_history(
-        &self,
-        compacted_history: Vec<ResponseItem>,
-    ) -> Vec<ResponseItem> {
-        compact::process_compacted_history(compacted_history)
-    }
-
     /// Append ResponseItems to the in-memory conversation history only.
     pub(crate) async fn record_into_history(
         &self,
diff --git a/codex-rs/core/src/compact_remote.rs b/codex-rs/core/src/compact_remote.rs
index c0a289288a79..589ebf7cbb17 100644
--- a/codex-rs/core/src/compact_remote.rs
+++ b/codex-rs/core/src/compact_remote.rs
@@ -6,6 +6,7 @@ use crate::codex::TurnContext;
 use crate::compact::AutoCompactCallsite;
 use crate::compact::extract_latest_model_switch_update_from_items;
 use crate::compact::extract_trailing_model_switch_update_for_compaction_request;
+use crate::compact::process_compacted_history;
 use crate::compact::should_keep_compacted_history_item;
 use crate::context_manager::ContextManager;
 use crate::context_manager::TotalTokenUsageBreakdown;
@@ -165,7 +166,7 @@ async fn run_remote_compact_task_inner_impl(
             Err(err)
         })
         .await?;
-    new_history = sess.process_compacted_history(new_history).await;
+    new_history = process_compacted_history(new_history);
     if let Some(incoming_items) = incoming_items.as_ref() {
         let incoming_history_items: Vec<ResponseItem> = incoming_items
             .iter()

From 25c22dcefc07b7a61cce1c8fc5b8c98386048207 Mon Sep 17 00:00:00 2001
From: Charles Cunningham <ccunningham@openai.com>
Date: Wed, 18 Feb 2026 00:26:02 -0800
Subject: [PATCH 61/80] Unify model-switch persistence through compaction
 history

---
 codex-rs/core/src/codex.rs                            |  9 ---------
 codex-rs/core/src/compact.rs                          |  9 +++------
 codex-rs/core/src/compact_remote.rs                   |  9 +++------
 ...ompaction_strips_incoming_model_switch_shapes.snap | 11 ++++++-----
 ...ompaction_strips_incoming_model_switch_shapes.snap | 11 ++++++-----
 5 files changed, 18 insertions(+), 31 deletions(-)

diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs
index 9b1e4afb4172..b68cd449604d 100644
--- a/codex-rs/core/src/codex.rs
+++ b/codex-rs/core/src/codex.rs
@@ -4729,15 +4729,6 @@ async fn persist_pre_turn_items_for_compaction_outcome(
                 sess.record_conversation_items(turn_context, &initial_context)
                     .await;
             }
-            let model_switch_updates: Vec<ResponseItem> = pre_turn_context_items
-                .iter()
-                .filter(|item| Session::is_model_switch_developer_message(item))
-                .cloned()
-                .collect();
-            if !model_switch_updates.is_empty() {
-                sess.record_conversation_items(turn_context, &model_switch_updates)
-                    .await;
-            }
             sess.record_user_prompt_and_emit_turn_item(turn_context.as_ref(), input, response_item)
                 .await;
         }
diff --git a/codex-rs/core/src/compact.rs b/codex-rs/core/src/compact.rs
index b3f3675fa3b3..ac2a7ba1f578 100644
--- a/codex-rs/core/src/compact.rs
+++ b/codex-rs/core/src/compact.rs
@@ -282,12 +282,9 @@ async fn run_compact_task_inner(
         COMPACT_USER_MESSAGE_MAX_TOKENS,
     );
     let mut new_history = process_compacted_history(compacted_history);
-    // Reattach stripped model-switch updates only for compaction paths that do not carry
-    // incoming turn items. Pre-turn compaction appends turn context and user input after
-    // compaction in run_turn.
-    if incoming_items.is_none()
-        && let Some(model_switch_item) = stripped_model_switch_item
-    {
+    // Reattach stripped model-switch updates into replacement history so post-compaction
+    // sampling keeps model-switch guidance regardless of compaction callsite.
+    if let Some(model_switch_item) = stripped_model_switch_item {
         new_history.push(model_switch_item);
     }
     let ghost_snapshots: Vec<ResponseItem> = history_items
diff --git a/codex-rs/core/src/compact_remote.rs b/codex-rs/core/src/compact_remote.rs
index 589ebf7cbb17..09296043a6a7 100644
--- a/codex-rs/core/src/compact_remote.rs
+++ b/codex-rs/core/src/compact_remote.rs
@@ -175,12 +175,9 @@ async fn run_remote_compact_task_inner_impl(
             .collect();
         remove_incoming_echoes_from_compacted_history(&mut new_history, &incoming_history_items);
     }
-    // Reattach stripped model-switch updates only for compaction paths that do not carry
-    // incoming turn items. Pre-turn compaction appends turn context and user input after
-    // compaction in run_turn.
-    if incoming_items.is_none()
-        && let Some(model_switch_item) = stripped_model_switch_item
-    {
+    // Reattach stripped model-switch updates into replacement history so post-compaction
+    // sampling keeps model-switch guidance regardless of compaction callsite.
+    if let Some(model_switch_item) = stripped_model_switch_item {
         new_history.push(model_switch_item);
     }
 
diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_turn_compaction_strips_incoming_model_switch_shapes.snap b/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_turn_compaction_strips_incoming_model_switch_shapes.snap
index 03b58b11f0e8..2f4820b282fe 100644
--- a/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_turn_compaction_strips_incoming_model_switch_shapes.snap
+++ b/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_turn_compaction_strips_incoming_model_switch_shapes.snap
@@ -1,5 +1,6 @@
 ---
 source: core/tests/suite/compact.rs
+assertion_line: 2949
 expression: "format_labeled_requests_snapshot(\"Pre-turn compaction during model switch (without pre-sampling model-switch compaction): current behavior strips incoming <model_switch> from the compact request and restores it in the post-compaction follow-up request.\",\n&[(\"Initial Request (Previous Model)\", &requests[0]),\n(\"Local Compaction Request\", &requests[1]),\n(\"Local Post-Compaction History Layout\", &requests[2]),])"
 ---
 Scenario: Pre-turn compaction during model switch (without pre-sampling model-switch compaction): current behavior strips incoming <model_switch> from the compact request and restores it in the post-compaction follow-up request.
@@ -24,9 +25,9 @@ Scenario: Pre-turn compaction during model switch (without pre-sampling model-sw
 ## Local Post-Compaction History Layout
 00:message/user:BEFORE_SWITCH_USER
 01:message/user:<COMPACTION_SUMMARY>\nPRETURN_SWITCH_SUMMARY
-02:message/developer:<PERMISSIONS_INSTRUCTIONS>
-03:message/developer:<personality_spec> The user has requested a new communication st...
-04:message/user:<AGENTS_MD>
-05:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
-06:message/developer:<model_switch>\nThe user was previously using a different model....
+02:message/developer:<model_switch>\nThe user was previously using a different model....
+03:message/developer:<PERMISSIONS_INSTRUCTIONS>
+04:message/developer:<personality_spec> The user has requested a new communication st...
+05:message/user:<AGENTS_MD>
+06:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
 07:message/user:AFTER_SWITCH_USER
diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_pre_turn_compaction_strips_incoming_model_switch_shapes.snap b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_pre_turn_compaction_strips_incoming_model_switch_shapes.snap
index 85d8073913ae..cfa6125e696b 100644
--- a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_pre_turn_compaction_strips_incoming_model_switch_shapes.snap
+++ b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_pre_turn_compaction_strips_incoming_model_switch_shapes.snap
@@ -1,5 +1,6 @@
 ---
 source: core/tests/suite/compact_remote.rs
+assertion_line: 1538
 expression: "format_labeled_requests_snapshot(\"Remote pre-turn compaction during model switch strips incoming <model_switch> from the compact request and restores it in the post-compaction follow-up request.\",\n&[(\"Initial Request (Previous Model)\", &initial_turn_request),\n(\"Remote Compaction Request\", &compact_request),\n(\"Remote Post-Compaction History Layout\", &post_compact_turn_request),])"
 ---
 Scenario: Remote pre-turn compaction during model switch strips incoming <model_switch> from the compact request and restores it in the post-compaction follow-up request.
@@ -21,9 +22,9 @@ Scenario: Remote pre-turn compaction during model switch strips incoming <model_
 ## Remote Post-Compaction History Layout
 00:message/user:BEFORE_SWITCH_USER
 01:message/user:<COMPACTION_SUMMARY>\nREMOTE_SWITCH_SUMMARY
-02:message/developer:<PERMISSIONS_INSTRUCTIONS>
-03:message/developer:<personality_spec> The user has requested a new communication st...
-04:message/user:<AGENTS_MD>
-05:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
-06:message/developer:<model_switch>\nThe user was previously using a different model....
+02:message/developer:<model_switch>\nThe user was previously using a different model....
+03:message/developer:<PERMISSIONS_INSTRUCTIONS>
+04:message/developer:<personality_spec> The user has requested a new communication st...
+05:message/user:<AGENTS_MD>
+06:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
 07:message/user:AFTER_SWITCH_USER

From 3759abaf1594bf2d43aba92bb97383c07de7803d Mon Sep 17 00:00:00 2001
From: Charles Cunningham <ccunningham@openai.com>
Date: Wed, 18 Feb 2026 00:27:44 -0800
Subject: [PATCH 62/80] Inline compact merge item matcher

---
 codex-rs/core/src/compact_remote.rs | 59 +++++++++++++----------------
 1 file changed, 27 insertions(+), 32 deletions(-)

diff --git a/codex-rs/core/src/compact_remote.rs b/codex-rs/core/src/compact_remote.rs
index 09296043a6a7..1a8eb8865694 100644
--- a/codex-rs/core/src/compact_remote.rs
+++ b/codex-rs/core/src/compact_remote.rs
@@ -229,43 +229,38 @@ fn remove_incoming_echoes_from_compacted_history(
     incoming_history_items: &[ResponseItem],
 ) {
     for incoming_item in incoming_history_items {
-        if let Some(index) = new_history.iter().rposition(|candidate| {
-            response_items_match_for_compaction_merge(candidate, incoming_item)
-        }) {
+        if let Some(index) =
+            new_history
+                .iter()
+                .rposition(|candidate| match (candidate, incoming_item) {
+                    (
+                        ResponseItem::Message {
+                            role: candidate_role,
+                            content: candidate_content,
+                            ..
+                        },
+                        ResponseItem::Message {
+                            role: incoming_role,
+                            content: incoming_content,
+                            ..
+                        },
+                    ) => candidate_role == incoming_role && candidate_content == incoming_content,
+                    (
+                        ResponseItem::Compaction {
+                            encrypted_content: candidate_content,
+                        },
+                        ResponseItem::Compaction {
+                            encrypted_content: incoming_content,
+                        },
+                    ) => candidate_content == incoming_content,
+                    _ => candidate == incoming_item,
+                })
+        {
             new_history.remove(index);
         }
     }
 }
 
-fn response_items_match_for_compaction_merge(
-    candidate: &ResponseItem,
-    incoming_item: &ResponseItem,
-) -> bool {
-    match (candidate, incoming_item) {
-        (
-            ResponseItem::Message {
-                role: candidate_role,
-                content: candidate_content,
-                ..
-            },
-            ResponseItem::Message {
-                role: incoming_role,
-                content: incoming_content,
-                ..
-            },
-        ) => candidate_role == incoming_role && candidate_content == incoming_content,
-        (
-            ResponseItem::Compaction {
-                encrypted_content: candidate_content,
-            },
-            ResponseItem::Compaction {
-                encrypted_content: incoming_content,
-            },
-        ) => candidate_content == incoming_content,
-        _ => candidate == incoming_item,
-    }
-}
-
 fn log_remote_compact_failure(
     turn_context: &TurnContext,
     auto_compact_callsite: AutoCompactCallsite,

From 2ede42d3195a4b9e84511f75e380cb0767972aff Mon Sep 17 00:00:00 2001
From: Charles Cunningham <ccunningham@openai.com>
Date: Wed, 18 Feb 2026 01:00:35 -0800
Subject: [PATCH 63/80] Reinsert initial context for mid-turn compaction

---
 codex-rs/core/src/compact.rs                  | 24 +++++++++++++++++++
 codex-rs/core/src/compact_remote.rs           |  5 ++++
 ...__compact__mid_turn_compaction_shapes.snap |  8 +++++--
 ...te__remote_mid_turn_compaction_shapes.snap |  8 +++++--
 4 files changed, 41 insertions(+), 4 deletions(-)

diff --git a/codex-rs/core/src/compact.rs b/codex-rs/core/src/compact.rs
index ac2a7ba1f578..f3a12371061d 100644
--- a/codex-rs/core/src/compact.rs
+++ b/codex-rs/core/src/compact.rs
@@ -282,6 +282,13 @@ async fn run_compact_task_inner(
         COMPACT_USER_MESSAGE_MAX_TOKENS,
     );
     let mut new_history = process_compacted_history(compacted_history);
+    if matches!(
+        auto_compact_callsite,
+        Some(AutoCompactCallsite::MidTurnContinuation)
+    ) {
+        let initial_context = sess.build_initial_context(turn_context.as_ref()).await;
+        insert_initial_context_before_last_real_user(&mut new_history, initial_context);
+    }
     // Reattach stripped model-switch updates into replacement history so post-compaction
     // sampling keeps model-switch guidance regardless of compaction callsite.
     if let Some(model_switch_item) = stripped_model_switch_item {
@@ -359,6 +366,23 @@ pub(crate) fn process_compacted_history(
     compacted_history
 }
 
+pub(crate) fn insert_initial_context_before_last_real_user(
+    compacted_history: &mut Vec<ResponseItem>,
+    initial_context: Vec<ResponseItem>,
+) {
+    if initial_context.is_empty() {
+        return;
+    }
+    if let Some(last_real_user_index) = compacted_history.iter().rposition(|item| {
+        matches!(
+            crate::event_mapping::parse_turn_item(item),
+            Some(TurnItem::UserMessage(user_message)) if !is_summary_message(&user_message.message())
+        )
+    }) {
+        compacted_history.splice(last_real_user_index..last_real_user_index, initial_context);
+    }
+}
+
 #[cfg(test)]
 fn is_real_user_message(item: &ResponseItem) -> bool {
     matches!(
diff --git a/codex-rs/core/src/compact_remote.rs b/codex-rs/core/src/compact_remote.rs
index 1a8eb8865694..66b896cd78d7 100644
--- a/codex-rs/core/src/compact_remote.rs
+++ b/codex-rs/core/src/compact_remote.rs
@@ -6,6 +6,7 @@ use crate::codex::TurnContext;
 use crate::compact::AutoCompactCallsite;
 use crate::compact::extract_latest_model_switch_update_from_items;
 use crate::compact::extract_trailing_model_switch_update_for_compaction_request;
+use crate::compact::insert_initial_context_before_last_real_user;
 use crate::compact::process_compacted_history;
 use crate::compact::should_keep_compacted_history_item;
 use crate::context_manager::ContextManager;
@@ -167,6 +168,10 @@ async fn run_remote_compact_task_inner_impl(
         })
         .await?;
     new_history = process_compacted_history(new_history);
+    if auto_compact_callsite == AutoCompactCallsite::MidTurnContinuation {
+        let initial_context = sess.build_initial_context(turn_context.as_ref()).await;
+        insert_initial_context_before_last_real_user(&mut new_history, initial_context);
+    }
     if let Some(incoming_items) = incoming_items.as_ref() {
         let incoming_history_items: Vec<ResponseItem> = incoming_items
             .iter()
diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact__mid_turn_compaction_shapes.snap b/codex-rs/core/tests/suite/snapshots/all__suite__compact__mid_turn_compaction_shapes.snap
index 000d4837787f..01ecfe67ca2b 100644
--- a/codex-rs/core/tests/suite/snapshots/all__suite__compact__mid_turn_compaction_shapes.snap
+++ b/codex-rs/core/tests/suite/snapshots/all__suite__compact__mid_turn_compaction_shapes.snap
@@ -1,5 +1,6 @@
 ---
 source: core/tests/suite/compact.rs
+assertion_line: 2434
 expression: "format_labeled_requests_snapshot(\"True mid-turn continuation compaction after tool output: compact request includes tool artifacts, and the continuation request includes the summary in the same turn.\",\n&[(\"Local Compaction Request\", &auto_compact_mock.single_request()),\n(\"Local Post-Compaction History Layout\",\n&post_auto_compact_mock.single_request()),])"
 ---
 Scenario: True mid-turn continuation compaction after tool output: compact request includes tool artifacts, and the continuation request includes the summary in the same turn.
@@ -14,5 +15,8 @@ Scenario: True mid-turn continuation compaction after tool output: compact reque
 06:message/user:<SUMMARIZATION_PROMPT>
 
 ## Local Post-Compaction History Layout
-00:message/user:function call limit push
-01:message/user:<COMPACTION_SUMMARY>\nAUTO_SUMMARY
+00:message/developer:<PERMISSIONS_INSTRUCTIONS>
+01:message/user:<AGENTS_MD>
+02:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
+03:message/user:function call limit push
+04:message/user:<COMPACTION_SUMMARY>\nAUTO_SUMMARY
diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_mid_turn_compaction_shapes.snap b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_mid_turn_compaction_shapes.snap
index 879c7fa7c627..5150a9d3b111 100644
--- a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_mid_turn_compaction_shapes.snap
+++ b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_mid_turn_compaction_shapes.snap
@@ -1,5 +1,6 @@
 ---
 source: core/tests/suite/compact_remote.rs
+assertion_line: 1711
 expression: "format_labeled_requests_snapshot(\"Remote mid-turn continuation compaction after tool output: compact request includes tool artifacts and follow-up request includes the summary.\",\n&[(\"Remote Compaction Request\", &compact_request),\n(\"Remote Post-Compaction History Layout\", &requests[1]),])"
 ---
 Scenario: Remote mid-turn continuation compaction after tool output: compact request includes tool artifacts and follow-up request includes the summary.
@@ -13,5 +14,8 @@ Scenario: Remote mid-turn continuation compaction after tool output: compact req
 05:function_call_output:unsupported call: test_tool
 
 ## Remote Post-Compaction History Layout
-00:message/user:USER_ONE
-01:message/user:<COMPACTION_SUMMARY>\nREMOTE_MID_TURN_SUMMARY
+00:message/developer:<PERMISSIONS_INSTRUCTIONS>
+01:message/user:<AGENTS_MD>
+02:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
+03:message/user:USER_ONE
+04:message/user:<COMPACTION_SUMMARY>\nREMOTE_MID_TURN_SUMMARY

From 223eda12d39e6fee59834dd93d77386778ddae23 Mon Sep 17 00:00:00 2001
From: Charles Cunningham <ccunningham@openai.com>
Date: Wed, 18 Feb 2026 01:17:13 -0800
Subject: [PATCH 64/80] Reinject previous-turn context for pre-sampling
 model-switch compaction

---
 codex-rs/core/src/codex.rs                           |  2 +-
 codex-rs/core/src/compact.rs                         |  7 ++++++-
 codex-rs/core/src/compact_remote.rs                  |  5 ++++-
 codex-rs/core/tests/suite/compact.rs                 |  4 ++--
 ..._pre_sampling_model_switch_compaction_shapes.snap | 12 +++++++-----
 5 files changed, 20 insertions(+), 10 deletions(-)

diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs
index b68cd449604d..6fec45e1dcef 100644
--- a/codex-rs/core/src/codex.rs
+++ b/codex-rs/core/src/codex.rs
@@ -4670,7 +4670,7 @@ async fn maybe_run_previous_model_inline_compact(
         sess,
         // We use previous turn context here because we compact with the previous model
         &previous_turn_context,
-        AutoCompactCallsite::PreTurnExcludingIncomingUserMessage,
+        AutoCompactCallsite::PreSamplingModelSwitch,
         None,
     )
     .await
diff --git a/codex-rs/core/src/compact.rs b/codex-rs/core/src/compact.rs
index f3a12371061d..52ee0e436d33 100644
--- a/codex-rs/core/src/compact.rs
+++ b/codex-rs/core/src/compact.rs
@@ -44,6 +44,9 @@ pub(crate) enum AutoCompactCallsite {
     /// default pre-turn flow and retained for future model-specific strategies.
     #[allow(dead_code)]
     PreTurnExcludingIncomingUserMessage,
+    /// Pre-sampling compaction triggered by model switch to a smaller context window.
+    /// This compacts prior-turn history only and should reinsert previous-turn canonical context.
+    PreSamplingModelSwitch,
     /// Mid-turn compaction between assistant responses in a follow-up loop.
     MidTurnContinuation,
 }
@@ -284,7 +287,9 @@ async fn run_compact_task_inner(
     let mut new_history = process_compacted_history(compacted_history);
     if matches!(
         auto_compact_callsite,
-        Some(AutoCompactCallsite::MidTurnContinuation)
+        Some(
+            AutoCompactCallsite::MidTurnContinuation | AutoCompactCallsite::PreSamplingModelSwitch
+        )
     ) {
         let initial_context = sess.build_initial_context(turn_context.as_ref()).await;
         insert_initial_context_before_last_real_user(&mut new_history, initial_context);
diff --git a/codex-rs/core/src/compact_remote.rs b/codex-rs/core/src/compact_remote.rs
index 66b896cd78d7..8695ea7582be 100644
--- a/codex-rs/core/src/compact_remote.rs
+++ b/codex-rs/core/src/compact_remote.rs
@@ -168,7 +168,10 @@ async fn run_remote_compact_task_inner_impl(
         })
         .await?;
     new_history = process_compacted_history(new_history);
-    if auto_compact_callsite == AutoCompactCallsite::MidTurnContinuation {
+    if matches!(
+        auto_compact_callsite,
+        AutoCompactCallsite::MidTurnContinuation | AutoCompactCallsite::PreSamplingModelSwitch
+    ) {
         let initial_context = sess.build_initial_context(turn_context.as_ref()).await;
         insert_initial_context_before_last_real_user(&mut new_history, initial_context);
     }
diff --git a/codex-rs/core/tests/suite/compact.rs b/codex-rs/core/tests/suite/compact.rs
index 6690609a62bd..49d3252647da 100644
--- a/codex-rs/core/tests/suite/compact.rs
+++ b/codex-rs/core/tests/suite/compact.rs
@@ -142,8 +142,8 @@ fn assert_pre_sampling_switch_compaction_requests(
         "follow-up request after successful model-switch compaction should include model-switch update item"
     );
     assert!(
-        !body_contains_text(&follow_up_body, "<environment_context>"),
-        "follow-up request should not reinsert canonical environment context after pre-sampling compaction"
+        body_contains_text(&follow_up_body, "<environment_context>"),
+        "follow-up request should include canonical environment context from previous-turn context reinjection"
     );
 }
 
diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_sampling_model_switch_compaction_shapes.snap b/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_sampling_model_switch_compaction_shapes.snap
index da2fc5ce71d7..16ca87707558 100644
--- a/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_sampling_model_switch_compaction_shapes.snap
+++ b/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_sampling_model_switch_compaction_shapes.snap
@@ -1,6 +1,5 @@
 ---
 source: core/tests/suite/compact.rs
-assertion_line: 1460
 expression: "format_labeled_requests_snapshot(\"Pre-sampling compaction on model switch to a smaller context window: current behavior compacts using prior-turn history only (incoming user message excluded), and the follow-up request carries compacted history plus the new user message.\",\n&[(\"Initial Request (Previous Model)\", &requests[0]),\n(\"Pre-sampling Compaction Request\", &requests[1]),\n(\"Post-Compaction Follow-up Request (Next Model)\", &requests[2]),])"
 ---
 Scenario: Pre-sampling compaction on model switch to a smaller context window: current behavior compacts using prior-turn history only (incoming user message excluded), and the follow-up request carries compacted history plus the new user message.
@@ -22,7 +21,10 @@ Scenario: Pre-sampling compaction on model switch to a smaller context window: c
 06:message/user:<SUMMARIZATION_PROMPT>
 
 ## Post-Compaction Follow-up Request (Next Model)
-00:message/user:before switch
-01:message/user:<COMPACTION_SUMMARY>\nPRE_SAMPLING_SUMMARY
-02:message/developer:<model_switch>\nThe user was previously using a different model....
-03:message/user:after switch
+00:message/developer:<PERMISSIONS_INSTRUCTIONS>
+01:message/user:<AGENTS_MD>
+02:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
+03:message/user:before switch
+04:message/user:<COMPACTION_SUMMARY>\nPRE_SAMPLING_SUMMARY
+05:message/developer:<model_switch>\nThe user was previously using a different model....
+06:message/user:after switch

From 454fa822816ac30d372636f78b4b1c9b908e9849 Mon Sep 17 00:00:00 2001
From: Charles Cunningham <ccunningham@openai.com>
Date: Wed, 18 Feb 2026 01:21:18 -0800
Subject: [PATCH 65/80] Use shared real-user matcher for initial-context
 insertion

---
 codex-rs/core/src/compact.rs | 8 +-------
 1 file changed, 1 insertion(+), 7 deletions(-)

diff --git a/codex-rs/core/src/compact.rs b/codex-rs/core/src/compact.rs
index 52ee0e436d33..c66279871711 100644
--- a/codex-rs/core/src/compact.rs
+++ b/codex-rs/core/src/compact.rs
@@ -378,17 +378,11 @@ pub(crate) fn insert_initial_context_before_last_real_user(
     if initial_context.is_empty() {
         return;
     }
-    if let Some(last_real_user_index) = compacted_history.iter().rposition(|item| {
-        matches!(
-            crate::event_mapping::parse_turn_item(item),
-            Some(TurnItem::UserMessage(user_message)) if !is_summary_message(&user_message.message())
-        )
-    }) {
+    if let Some(last_real_user_index) = compacted_history.iter().rposition(is_real_user_message) {
         compacted_history.splice(last_real_user_index..last_real_user_index, initial_context);
     }
 }
 
-#[cfg(test)]
 fn is_real_user_message(item: &ResponseItem) -> bool {
     matches!(
         crate::event_mapping::parse_turn_item(item),

From 7ed3d3f9aa4dd24237d2b440cc5eca09170d05bd Mon Sep 17 00:00:00 2001
From: Charles Cunningham <ccunningham@openai.com>
Date: Wed, 18 Feb 2026 01:21:51 -0800
Subject: [PATCH 66/80] Put NotNeeded first in pre-turn compaction outcome
 match

---
 codex-rs/core/src/codex.rs | 16 ++++++++--------
 1 file changed, 8 insertions(+), 8 deletions(-)

diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs
index 6fec45e1dcef..eb81b7e952be 100644
--- a/codex-rs/core/src/codex.rs
+++ b/codex-rs/core/src/codex.rs
@@ -4720,6 +4720,14 @@ async fn persist_pre_turn_items_for_compaction_outcome(
     response_item: ResponseItem,
 ) {
     match outcome {
+        PreTurnCompactionOutcome::NotNeeded => {
+            if !pre_turn_context_items.is_empty() {
+                sess.record_conversation_items(turn_context, pre_turn_context_items)
+                    .await;
+            }
+            sess.record_user_prompt_and_emit_turn_item(turn_context.as_ref(), input, response_item)
+                .await;
+        }
         PreTurnCompactionOutcome::CompactedWithIncomingItems => {
             // Pre-turn compaction includes incoming items only for the compaction request itself.
             // Persist canonical turn context directly above the incoming user item so context
@@ -4732,14 +4740,6 @@ async fn persist_pre_turn_items_for_compaction_outcome(
             sess.record_user_prompt_and_emit_turn_item(turn_context.as_ref(), input, response_item)
                 .await;
         }
-        PreTurnCompactionOutcome::NotNeeded => {
-            if !pre_turn_context_items.is_empty() {
-                sess.record_conversation_items(turn_context, pre_turn_context_items)
-                    .await;
-            }
-            sess.record_user_prompt_and_emit_turn_item(turn_context.as_ref(), input, response_item)
-                .await;
-        }
         // TODO(ccunningham): Followup PR will use compacting excluding incoming items as a fallback
         // (even though it is out of distribution for current models).
         // Also future models may prefer compacting pre-turn history without incoming turn items.

From c6d63129c1b5e51dd2a58b66edd6276d9155f516 Mon Sep 17 00:00:00 2001
From: Charles Cunningham <ccunningham@openai.com>
Date: Wed, 18 Feb 2026 01:34:11 -0800
Subject: [PATCH 67/80] Rename compaction callsite enum and exhaustively match
 reinjection

---
 codex-rs/core/src/codex.rs          | 12 ++++----
 codex-rs/core/src/compact.rs        | 43 ++++++++++++++++++++---------
 codex-rs/core/src/compact_remote.rs | 40 +++++++++++++++------------
 3 files changed, 58 insertions(+), 37 deletions(-)

diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs
index eb81b7e952be..f624370d59c6 100644
--- a/codex-rs/core/src/codex.rs
+++ b/codex-rs/core/src/codex.rs
@@ -19,7 +19,7 @@ use crate::analytics_client::build_track_events_context;
 use crate::apps::render_apps_section;
 use crate::commit_attribution::commit_message_trailer_instruction;
 use crate::compact;
-use crate::compact::AutoCompactCallsite;
+use crate::compact::CompactCallsite;
 use crate::compact::run_inline_auto_compact_task;
 use crate::compact::should_use_remote_compact_task;
 use crate::compact_remote::run_inline_remote_auto_compact_task;
@@ -4517,7 +4517,7 @@ pub(crate) async fn run_turn(
                     if let Err(err) = run_auto_compact(
                         &sess,
                         &turn_context,
-                        AutoCompactCallsite::MidTurnContinuation,
+                        CompactCallsite::MidTurnContinuation,
                         None,
                     )
                     .await
@@ -4670,7 +4670,7 @@ async fn maybe_run_previous_model_inline_compact(
         sess,
         // We use previous turn context here because we compact with the previous model
         &previous_turn_context,
-        AutoCompactCallsite::PreSamplingModelSwitch,
+        CompactCallsite::PreSamplingModelSwitch,
         None,
     )
     .await
@@ -4784,7 +4784,7 @@ async fn run_pre_turn_auto_compaction_if_needed(
     let compact_result = run_auto_compact(
         sess,
         turn_context,
-        AutoCompactCallsite::PreTurnIncludingIncomingUserMessage,
+        CompactCallsite::PreTurnIncludingIncomingUserMessage,
         Some(incoming_turn_items.to_vec()),
     )
     .await;
@@ -4803,7 +4803,7 @@ async fn run_pre_turn_auto_compaction_if_needed(
             CodexErr::ContextWindowExceeded => {
                 error!(
                     turn_id = %turn_context.sub_id,
-                    auto_compact_callsite = ?AutoCompactCallsite::PreTurnIncludingIncomingUserMessage,
+                    auto_compact_callsite = ?CompactCallsite::PreTurnIncludingIncomingUserMessage,
                     incoming_items_tokens_estimate,
                     auto_compact_limit,
                     reason = "pre-turn compaction exceeded context window",
@@ -4859,7 +4859,7 @@ fn is_projected_submission_over_auto_compact_limit(
 async fn run_auto_compact(
     sess: &Arc<Session>,
     turn_context: &Arc<TurnContext>,
-    auto_compact_callsite: AutoCompactCallsite,
+    auto_compact_callsite: CompactCallsite,
     incoming_items: Option<Vec<ResponseItem>>,
 ) -> CodexResult<()> {
     let result = if should_use_remote_compact_task(&turn_context.provider) {
diff --git a/codex-rs/core/src/compact.rs b/codex-rs/core/src/compact.rs
index c66279871711..95a79673ba0d 100644
--- a/codex-rs/core/src/compact.rs
+++ b/codex-rs/core/src/compact.rs
@@ -35,7 +35,9 @@ pub const SUMMARY_PREFIX: &str = include_str!("../templates/compact/summary_pref
 const COMPACT_USER_MESSAGE_MAX_TOKENS: usize = 20_000;
 
 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub(crate) enum AutoCompactCallsite {
+pub(crate) enum CompactCallsite {
+    /// Manual `/compact` task.
+    ManualCompact,
     /// Pre-turn auto-compaction where the incoming turn context + user message are included in
     /// the compaction request.
     PreTurnIncludingIncomingUserMessage,
@@ -94,7 +96,7 @@ pub(crate) fn extract_latest_model_switch_update_from_items(
 pub(crate) async fn run_inline_auto_compact_task(
     sess: Arc<Session>,
     turn_context: Arc<TurnContext>,
-    auto_compact_callsite: AutoCompactCallsite,
+    auto_compact_callsite: CompactCallsite,
     incoming_items: Option<Vec<ResponseItem>>,
 ) -> CodexResult<()> {
     let prompt = turn_context.compact_prompt().to_string();
@@ -108,7 +110,7 @@ pub(crate) async fn run_inline_auto_compact_task(
         sess,
         turn_context,
         input,
-        Some(auto_compact_callsite),
+        auto_compact_callsite,
         incoming_items,
     )
     .await?;
@@ -126,14 +128,21 @@ pub(crate) async fn run_compact_task(
         collaboration_mode_kind: turn_context.collaboration_mode.mode,
     });
     sess.send_event(&turn_context, start_event).await;
-    run_compact_task_inner(sess, turn_context, input, None, None).await
+    run_compact_task_inner(
+        sess,
+        turn_context,
+        input,
+        CompactCallsite::ManualCompact,
+        None,
+    )
+    .await
 }
 
 async fn run_compact_task_inner(
     sess: Arc<Session>,
     turn_context: Arc<TurnContext>,
     input: Vec<UserInput>,
-    auto_compact_callsite: Option<AutoCompactCallsite>,
+    auto_compact_callsite: CompactCallsite,
     incoming_items: Option<Vec<ResponseItem>>,
 ) -> CodexResult<()> {
     let compaction_item = TurnItem::ContextCompaction(ContextCompactionItem::new());
@@ -285,14 +294,22 @@ async fn run_compact_task_inner(
         COMPACT_USER_MESSAGE_MAX_TOKENS,
     );
     let mut new_history = process_compacted_history(compacted_history);
-    if matches!(
-        auto_compact_callsite,
-        Some(
-            AutoCompactCallsite::MidTurnContinuation | AutoCompactCallsite::PreSamplingModelSwitch
-        )
-    ) {
-        let initial_context = sess.build_initial_context(turn_context.as_ref()).await;
-        insert_initial_context_before_last_real_user(&mut new_history, initial_context);
+    match auto_compact_callsite {
+        CompactCallsite::MidTurnContinuation | CompactCallsite::PreSamplingModelSwitch => {
+            // Mid-turn and pre-sampling model-switch compaction continue the in-flight turn and
+            // therefore must keep canonical context anchored above the latest real user turn.
+            let initial_context = sess.build_initial_context(turn_context.as_ref()).await;
+            insert_initial_context_before_last_real_user(&mut new_history, initial_context);
+        }
+        CompactCallsite::ManualCompact => {
+            // Manual `/compact` intentionally rewrites transcript history without reseeding turn
+            // context here; the task marks initial context unseeded for the next user turn.
+        }
+        CompactCallsite::PreTurnIncludingIncomingUserMessage
+        | CompactCallsite::PreTurnExcludingIncomingUserMessage => {
+            // Pre-turn compaction persists canonical context directly above the incoming user
+            // message in `run_turn`, not inside compacted replacement history.
+        }
     }
     // Reattach stripped model-switch updates into replacement history so post-compaction
     // sampling keeps model-switch guidance regardless of compaction callsite.
diff --git a/codex-rs/core/src/compact_remote.rs b/codex-rs/core/src/compact_remote.rs
index 8695ea7582be..5d60703cef77 100644
--- a/codex-rs/core/src/compact_remote.rs
+++ b/codex-rs/core/src/compact_remote.rs
@@ -3,7 +3,7 @@ use std::sync::Arc;
 use crate::Prompt;
 use crate::codex::Session;
 use crate::codex::TurnContext;
-use crate::compact::AutoCompactCallsite;
+use crate::compact::CompactCallsite;
 use crate::compact::extract_latest_model_switch_update_from_items;
 use crate::compact::extract_trailing_model_switch_update_for_compaction_request;
 use crate::compact::insert_initial_context_before_last_real_user;
@@ -32,7 +32,7 @@ use tracing::info;
 pub(crate) async fn run_inline_remote_auto_compact_task(
     sess: Arc<Session>,
     turn_context: Arc<TurnContext>,
-    auto_compact_callsite: AutoCompactCallsite,
+    auto_compact_callsite: CompactCallsite,
     incoming_items: Option<Vec<ResponseItem>>,
 ) -> CodexResult<()> {
     run_remote_compact_task_inner(&sess, &turn_context, auto_compact_callsite, incoming_items)
@@ -51,19 +51,13 @@ pub(crate) async fn run_remote_compact_task(
     });
     sess.send_event(&turn_context, start_event).await;
 
-    run_remote_compact_task_inner(
-        &sess,
-        &turn_context,
-        AutoCompactCallsite::PreTurnExcludingIncomingUserMessage,
-        None,
-    )
-    .await
+    run_remote_compact_task_inner(&sess, &turn_context, CompactCallsite::ManualCompact, None).await
 }
 
 async fn run_remote_compact_task_inner(
     sess: &Arc<Session>,
     turn_context: &Arc<TurnContext>,
-    auto_compact_callsite: AutoCompactCallsite,
+    auto_compact_callsite: CompactCallsite,
     incoming_items: Option<Vec<ResponseItem>>,
 ) -> CodexResult<()> {
     if let Err(err) = run_remote_compact_task_inner_impl(
@@ -88,7 +82,7 @@ async fn run_remote_compact_task_inner(
 async fn run_remote_compact_task_inner_impl(
     sess: &Arc<Session>,
     turn_context: &Arc<TurnContext>,
-    auto_compact_callsite: AutoCompactCallsite,
+    auto_compact_callsite: CompactCallsite,
     incoming_items: Option<Vec<ResponseItem>>,
 ) -> CodexResult<()> {
     let compaction_item = TurnItem::ContextCompaction(ContextCompactionItem::new());
@@ -168,12 +162,22 @@ async fn run_remote_compact_task_inner_impl(
         })
         .await?;
     new_history = process_compacted_history(new_history);
-    if matches!(
-        auto_compact_callsite,
-        AutoCompactCallsite::MidTurnContinuation | AutoCompactCallsite::PreSamplingModelSwitch
-    ) {
-        let initial_context = sess.build_initial_context(turn_context.as_ref()).await;
-        insert_initial_context_before_last_real_user(&mut new_history, initial_context);
+    match auto_compact_callsite {
+        CompactCallsite::MidTurnContinuation | CompactCallsite::PreSamplingModelSwitch => {
+            // Mid-turn and pre-sampling model-switch compaction continue the in-flight turn and
+            // therefore must keep canonical context anchored above the latest real user turn.
+            let initial_context = sess.build_initial_context(turn_context.as_ref()).await;
+            insert_initial_context_before_last_real_user(&mut new_history, initial_context);
+        }
+        CompactCallsite::ManualCompact => {
+            // Manual `/compact` intentionally rewrites transcript history without reseeding turn
+            // context here; the task marks initial context unseeded for the next user turn.
+        }
+        CompactCallsite::PreTurnIncludingIncomingUserMessage
+        | CompactCallsite::PreTurnExcludingIncomingUserMessage => {
+            // Pre-turn compaction persists canonical context directly above the incoming user
+            // message in `run_turn`, not inside compacted replacement history.
+        }
     }
     if let Some(incoming_items) = incoming_items.as_ref() {
         let incoming_history_items: Vec<ResponseItem> = incoming_items
@@ -271,7 +275,7 @@ fn remove_incoming_echoes_from_compacted_history(
 
 fn log_remote_compact_failure(
     turn_context: &TurnContext,
-    auto_compact_callsite: AutoCompactCallsite,
+    auto_compact_callsite: CompactCallsite,
     log_data: &CompactRequestLogData,
     total_usage_breakdown: TotalTokenUsageBreakdown,
     err: &CodexErr,

From 4c0ca09d696e03653beb073086fba0bac3b607ef Mon Sep 17 00:00:00 2001
From: Charles Cunningham <ccunningham@openai.com>
Date: Wed, 18 Feb 2026 01:41:37 -0800
Subject: [PATCH 68/80] Rename compact callsite vars and document reinjection
 policy

---
 codex-rs/core/src/codex.rs          | 10 +++++-----
 codex-rs/core/src/compact.rs        | 29 ++++++++++++++-------------
 codex-rs/core/src/compact_remote.rs | 31 ++++++++++++-----------------
 3 files changed, 33 insertions(+), 37 deletions(-)

diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs
index f624370d59c6..81df6149f64f 100644
--- a/codex-rs/core/src/codex.rs
+++ b/codex-rs/core/src/codex.rs
@@ -4803,7 +4803,7 @@ async fn run_pre_turn_auto_compaction_if_needed(
             CodexErr::ContextWindowExceeded => {
                 error!(
                     turn_id = %turn_context.sub_id,
-                    auto_compact_callsite = ?CompactCallsite::PreTurnIncludingIncomingUserMessage,
+                    compact_callsite = ?CompactCallsite::PreTurnIncludingIncomingUserMessage,
                     incoming_items_tokens_estimate,
                     auto_compact_limit,
                     reason = "pre-turn compaction exceeded context window",
@@ -4859,14 +4859,14 @@ fn is_projected_submission_over_auto_compact_limit(
 async fn run_auto_compact(
     sess: &Arc<Session>,
     turn_context: &Arc<TurnContext>,
-    auto_compact_callsite: CompactCallsite,
+    compact_callsite: CompactCallsite,
     incoming_items: Option<Vec<ResponseItem>>,
 ) -> CodexResult<()> {
     let result = if should_use_remote_compact_task(&turn_context.provider) {
         run_inline_remote_auto_compact_task(
             Arc::clone(sess),
             Arc::clone(turn_context),
-            auto_compact_callsite,
+            compact_callsite,
             incoming_items,
         )
         .await
@@ -4874,7 +4874,7 @@ async fn run_auto_compact(
         run_inline_auto_compact_task(
             Arc::clone(sess),
             Arc::clone(turn_context),
-            auto_compact_callsite,
+            compact_callsite,
             incoming_items,
         )
         .await
@@ -4883,7 +4883,7 @@ async fn run_auto_compact(
     if let Err(err) = &result {
         error!(
             turn_id = %turn_context.sub_id,
-            auto_compact_callsite = ?auto_compact_callsite,
+            compact_callsite = ?compact_callsite,
             compact_error = %err,
             "auto compaction failed"
         );
diff --git a/codex-rs/core/src/compact.rs b/codex-rs/core/src/compact.rs
index 95a79673ba0d..2c8ca1528ba1 100644
--- a/codex-rs/core/src/compact.rs
+++ b/codex-rs/core/src/compact.rs
@@ -53,6 +53,14 @@ pub(crate) enum CompactCallsite {
     MidTurnContinuation,
 }
 
+// Canonical context reinjection policy for compacted replacement history:
+// - `MidTurnContinuation` and `PreSamplingModelSwitch`: reinsert canonical initial context above
+//   the last real user message because compaction is rewriting in-flight history that the model
+//   will continue sampling from immediately.
+// - `ManualCompact`: do not reinsert during compaction; `/compact` reseeds on the next user turn.
+// - `PreTurn*`: do not reinsert into replacement history; `run_turn` persists canonical context
+//   directly above the incoming user message after compaction.
+
 pub(crate) fn should_use_remote_compact_task(provider: &ModelProviderInfo) -> bool {
     provider.is_openai()
 }
@@ -96,7 +104,7 @@ pub(crate) fn extract_latest_model_switch_update_from_items(
 pub(crate) async fn run_inline_auto_compact_task(
     sess: Arc<Session>,
     turn_context: Arc<TurnContext>,
-    auto_compact_callsite: CompactCallsite,
+    compact_callsite: CompactCallsite,
     incoming_items: Option<Vec<ResponseItem>>,
 ) -> CodexResult<()> {
     let prompt = turn_context.compact_prompt().to_string();
@@ -106,14 +114,7 @@ pub(crate) async fn run_inline_auto_compact_task(
         text_elements: Vec::new(),
     }];
 
-    run_compact_task_inner(
-        sess,
-        turn_context,
-        input,
-        auto_compact_callsite,
-        incoming_items,
-    )
-    .await?;
+    run_compact_task_inner(sess, turn_context, input, compact_callsite, incoming_items).await?;
     Ok(())
 }
 
@@ -142,7 +143,7 @@ async fn run_compact_task_inner(
     sess: Arc<Session>,
     turn_context: Arc<TurnContext>,
     input: Vec<UserInput>,
-    auto_compact_callsite: CompactCallsite,
+    compact_callsite: CompactCallsite,
     incoming_items: Option<Vec<ResponseItem>>,
 ) -> CodexResult<()> {
     let compaction_item = TurnItem::ContextCompaction(ContextCompactionItem::new());
@@ -240,7 +241,7 @@ async fn run_compact_task_inner(
                     // messages intact.
                     error!(
                         turn_id = %turn_context.sub_id,
-                        auto_compact_callsite = ?auto_compact_callsite,
+                        compact_callsite = ?compact_callsite,
                         "Context window exceeded while compacting; removing oldest history item. Error: {e}"
                     );
                     history.remove_first_item();
@@ -251,7 +252,7 @@ async fn run_compact_task_inner(
                 sess.set_total_tokens_full(turn_context.as_ref()).await;
                 error!(
                     turn_id = %turn_context.sub_id,
-                    auto_compact_callsite = ?auto_compact_callsite,
+                    compact_callsite = ?compact_callsite,
                     compact_error = %e,
                     "compaction failed after history truncation could not proceed"
                 );
@@ -272,7 +273,7 @@ async fn run_compact_task_inner(
                 }
                 error!(
                     turn_id = %turn_context.sub_id,
-                    auto_compact_callsite = ?auto_compact_callsite,
+                    compact_callsite = ?compact_callsite,
                     retries,
                     max_retries,
                     compact_error = %e,
@@ -294,7 +295,7 @@ async fn run_compact_task_inner(
         COMPACT_USER_MESSAGE_MAX_TOKENS,
     );
     let mut new_history = process_compacted_history(compacted_history);
-    match auto_compact_callsite {
+    match compact_callsite {
         CompactCallsite::MidTurnContinuation | CompactCallsite::PreSamplingModelSwitch => {
             // Mid-turn and pre-sampling model-switch compaction continue the in-flight turn and
             // therefore must keep canonical context anchored above the latest real user turn.
diff --git a/codex-rs/core/src/compact_remote.rs b/codex-rs/core/src/compact_remote.rs
index 5d60703cef77..92d11b75985b 100644
--- a/codex-rs/core/src/compact_remote.rs
+++ b/codex-rs/core/src/compact_remote.rs
@@ -32,11 +32,10 @@ use tracing::info;
 pub(crate) async fn run_inline_remote_auto_compact_task(
     sess: Arc<Session>,
     turn_context: Arc<TurnContext>,
-    auto_compact_callsite: CompactCallsite,
+    compact_callsite: CompactCallsite,
     incoming_items: Option<Vec<ResponseItem>>,
 ) -> CodexResult<()> {
-    run_remote_compact_task_inner(&sess, &turn_context, auto_compact_callsite, incoming_items)
-        .await?;
+    run_remote_compact_task_inner(&sess, &turn_context, compact_callsite, incoming_items).await?;
     Ok(())
 }
 
@@ -57,20 +56,16 @@ pub(crate) async fn run_remote_compact_task(
 async fn run_remote_compact_task_inner(
     sess: &Arc<Session>,
     turn_context: &Arc<TurnContext>,
-    auto_compact_callsite: CompactCallsite,
+    compact_callsite: CompactCallsite,
     incoming_items: Option<Vec<ResponseItem>>,
 ) -> CodexResult<()> {
-    if let Err(err) = run_remote_compact_task_inner_impl(
-        sess,
-        turn_context,
-        auto_compact_callsite,
-        incoming_items,
-    )
-    .await
+    if let Err(err) =
+        run_remote_compact_task_inner_impl(sess, turn_context, compact_callsite, incoming_items)
+            .await
     {
         error!(
             turn_id = %turn_context.sub_id,
-            auto_compact_callsite = ?auto_compact_callsite,
+            compact_callsite = ?compact_callsite,
             compact_error = %err,
             "remote compaction task failed"
         );
@@ -82,7 +77,7 @@ async fn run_remote_compact_task_inner(
 async fn run_remote_compact_task_inner_impl(
     sess: &Arc<Session>,
     turn_context: &Arc<TurnContext>,
-    auto_compact_callsite: CompactCallsite,
+    compact_callsite: CompactCallsite,
     incoming_items: Option<Vec<ResponseItem>>,
 ) -> CodexResult<()> {
     let compaction_item = TurnItem::ContextCompaction(ContextCompactionItem::new());
@@ -116,7 +111,7 @@ async fn run_remote_compact_task_inner_impl(
     if deleted_items > 0 {
         info!(
             turn_id = %turn_context.sub_id,
-            auto_compact_callsite = ?auto_compact_callsite,
+            compact_callsite = ?compact_callsite,
             deleted_items,
             "trimmed history items before remote compaction"
         );
@@ -153,7 +148,7 @@ async fn run_remote_compact_task_inner_impl(
                 build_compact_request_log_data(&prompt.input, &prompt.base_instructions.text);
             log_remote_compact_failure(
                 turn_context,
-                auto_compact_callsite,
+                compact_callsite,
                 &compact_request_log_data,
                 total_usage_breakdown,
                 &err,
@@ -162,7 +157,7 @@ async fn run_remote_compact_task_inner_impl(
         })
         .await?;
     new_history = process_compacted_history(new_history);
-    match auto_compact_callsite {
+    match compact_callsite {
         CompactCallsite::MidTurnContinuation | CompactCallsite::PreSamplingModelSwitch => {
             // Mid-turn and pre-sampling model-switch compaction continue the in-flight turn and
             // therefore must keep canonical context anchored above the latest real user turn.
@@ -275,14 +270,14 @@ fn remove_incoming_echoes_from_compacted_history(
 
 fn log_remote_compact_failure(
     turn_context: &TurnContext,
-    auto_compact_callsite: CompactCallsite,
+    compact_callsite: CompactCallsite,
     log_data: &CompactRequestLogData,
     total_usage_breakdown: TotalTokenUsageBreakdown,
     err: &CodexErr,
 ) {
     error!(
         turn_id = %turn_context.sub_id,
-        auto_compact_callsite = ?auto_compact_callsite,
+        compact_callsite = ?compact_callsite,
         last_api_response_total_tokens = total_usage_breakdown.last_api_response_total_tokens,
         all_history_items_model_visible_bytes = total_usage_breakdown.all_history_items_model_visible_bytes,
         estimated_tokens_of_items_added_since_last_successful_api_response = total_usage_breakdown.estimated_tokens_of_items_added_since_last_successful_api_response,

From 0fbc4c76a8c179f8f8941dacb94150a437f51197 Mon Sep 17 00:00:00 2001
From: Charles Cunningham <ccunningham@openai.com>
Date: Wed, 18 Feb 2026 01:42:23 -0800
Subject: [PATCH 69/80] Clarify compaction reinjection rationale comments

---
 codex-rs/core/src/compact.rs        | 8 ++++----
 codex-rs/core/src/compact_remote.rs | 4 ++--
 2 files changed, 6 insertions(+), 6 deletions(-)

diff --git a/codex-rs/core/src/compact.rs b/codex-rs/core/src/compact.rs
index 2c8ca1528ba1..9aa872c6df8d 100644
--- a/codex-rs/core/src/compact.rs
+++ b/codex-rs/core/src/compact.rs
@@ -55,8 +55,8 @@ pub(crate) enum CompactCallsite {
 
 // Canonical context reinjection policy for compacted replacement history:
 // - `MidTurnContinuation` and `PreSamplingModelSwitch`: reinsert canonical initial context above
-//   the last real user message because compaction is rewriting in-flight history that the model
-//   will continue sampling from immediately.
+//   the last real user message because those paths do not persist/reseed canonical context in a
+//   later post-compaction step.
 // - `ManualCompact`: do not reinsert during compaction; `/compact` reseeds on the next user turn.
 // - `PreTurn*`: do not reinsert into replacement history; `run_turn` persists canonical context
 //   directly above the incoming user message after compaction.
@@ -297,8 +297,8 @@ async fn run_compact_task_inner(
     let mut new_history = process_compacted_history(compacted_history);
     match compact_callsite {
         CompactCallsite::MidTurnContinuation | CompactCallsite::PreSamplingModelSwitch => {
-            // Mid-turn and pre-sampling model-switch compaction continue the in-flight turn and
-            // therefore must keep canonical context anchored above the latest real user turn.
+            // These callsites do not get a later post-compaction canonical-context write in
+            // `run_turn`, so replacement history must carry canonical context directly.
             let initial_context = sess.build_initial_context(turn_context.as_ref()).await;
             insert_initial_context_before_last_real_user(&mut new_history, initial_context);
         }
diff --git a/codex-rs/core/src/compact_remote.rs b/codex-rs/core/src/compact_remote.rs
index 92d11b75985b..97788bd31f3d 100644
--- a/codex-rs/core/src/compact_remote.rs
+++ b/codex-rs/core/src/compact_remote.rs
@@ -159,8 +159,8 @@ async fn run_remote_compact_task_inner_impl(
     new_history = process_compacted_history(new_history);
     match compact_callsite {
         CompactCallsite::MidTurnContinuation | CompactCallsite::PreSamplingModelSwitch => {
-            // Mid-turn and pre-sampling model-switch compaction continue the in-flight turn and
-            // therefore must keep canonical context anchored above the latest real user turn.
+            // These callsites do not get a later post-compaction canonical-context write in
+            // `run_turn`, so replacement history must carry canonical context directly.
             let initial_context = sess.build_initial_context(turn_context.as_ref()).await;
             insert_initial_context_before_last_real_user(&mut new_history, initial_context);
         }

From 96b88aee15fcdcfa4b358d9d7ecc3558f9a32caf Mon Sep 17 00:00:00 2001
From: Charles Cunningham <ccunningham@openai.com>
Date: Wed, 18 Feb 2026 01:50:58 -0800
Subject: [PATCH 70/80] Reinject context for summary-only mid-turn compaction

---
 codex-rs/core/src/compact.rs                  | 86 ++++++++++++++++++-
 codex-rs/core/src/compact_remote.rs           |  4 +-
 codex-rs/core/tests/suite/compact_remote.rs   |  2 +-
 ...d_turn_compaction_summary_only_shapes.snap | 10 ++-
 4 files changed, 91 insertions(+), 11 deletions(-)

diff --git a/codex-rs/core/src/compact.rs b/codex-rs/core/src/compact.rs
index 9aa872c6df8d..109f12b6939e 100644
--- a/codex-rs/core/src/compact.rs
+++ b/codex-rs/core/src/compact.rs
@@ -300,7 +300,7 @@ async fn run_compact_task_inner(
             // These callsites do not get a later post-compaction canonical-context write in
             // `run_turn`, so replacement history must carry canonical context directly.
             let initial_context = sess.build_initial_context(turn_context.as_ref()).await;
-            insert_initial_context_before_last_real_user(&mut new_history, initial_context);
+            insert_initial_context_before_last_user_anchor(&mut new_history, initial_context);
         }
         CompactCallsite::ManualCompact => {
             // Manual `/compact` intentionally rewrites transcript history without reseeding turn
@@ -389,15 +389,27 @@ pub(crate) fn process_compacted_history(
     compacted_history
 }
 
-pub(crate) fn insert_initial_context_before_last_real_user(
+pub(crate) fn insert_initial_context_before_last_user_anchor(
     compacted_history: &mut Vec<ResponseItem>,
     initial_context: Vec<ResponseItem>,
 ) {
     if initial_context.is_empty() {
         return;
     }
-    if let Some(last_real_user_index) = compacted_history.iter().rposition(is_real_user_message) {
-        compacted_history.splice(last_real_user_index..last_real_user_index, initial_context);
+    let insertion_index = compacted_history
+        .iter()
+        .rposition(is_real_user_message)
+        .or_else(|| {
+            compacted_history.iter().rposition(|item| {
+                matches!(
+                    crate::event_mapping::parse_turn_item(item),
+                    Some(TurnItem::UserMessage(user_message))
+                        if is_summary_message(&user_message.message())
+                )
+            })
+        });
+    if let Some(index) = insertion_index {
+        compacted_history.splice(index..index, initial_context);
     }
 }
 
@@ -1268,4 +1280,70 @@ keep me updated
         }];
         assert_eq!(refreshed, expected);
     }
+
+    #[test]
+    fn insert_initial_context_before_last_user_anchor_falls_back_to_last_summary() {
+        let mut compacted_history = vec![
+            ResponseItem::Message {
+                id: None,
+                role: "user".to_string(),
+                content: vec![ContentItem::InputText {
+                    text: format!("{SUMMARY_PREFIX}\nolder summary"),
+                }],
+                end_turn: None,
+                phase: None,
+            },
+            ResponseItem::Message {
+                id: None,
+                role: "user".to_string(),
+                content: vec![ContentItem::InputText {
+                    text: format!("{SUMMARY_PREFIX}\nlatest summary"),
+                }],
+                end_turn: None,
+                phase: None,
+            },
+        ];
+        let initial_context = vec![ResponseItem::Message {
+            id: None,
+            role: "developer".to_string(),
+            content: vec![ContentItem::InputText {
+                text: "fresh permissions".to_string(),
+            }],
+            end_turn: None,
+            phase: None,
+        }];
+
+        insert_initial_context_before_last_user_anchor(&mut compacted_history, initial_context);
+
+        let expected = vec![
+            ResponseItem::Message {
+                id: None,
+                role: "user".to_string(),
+                content: vec![ContentItem::InputText {
+                    text: format!("{SUMMARY_PREFIX}\nolder summary"),
+                }],
+                end_turn: None,
+                phase: None,
+            },
+            ResponseItem::Message {
+                id: None,
+                role: "developer".to_string(),
+                content: vec![ContentItem::InputText {
+                    text: "fresh permissions".to_string(),
+                }],
+                end_turn: None,
+                phase: None,
+            },
+            ResponseItem::Message {
+                id: None,
+                role: "user".to_string(),
+                content: vec![ContentItem::InputText {
+                    text: format!("{SUMMARY_PREFIX}\nlatest summary"),
+                }],
+                end_turn: None,
+                phase: None,
+            },
+        ];
+        assert_eq!(compacted_history, expected);
+    }
 }
diff --git a/codex-rs/core/src/compact_remote.rs b/codex-rs/core/src/compact_remote.rs
index 97788bd31f3d..10dc47205153 100644
--- a/codex-rs/core/src/compact_remote.rs
+++ b/codex-rs/core/src/compact_remote.rs
@@ -6,7 +6,7 @@ use crate::codex::TurnContext;
 use crate::compact::CompactCallsite;
 use crate::compact::extract_latest_model_switch_update_from_items;
 use crate::compact::extract_trailing_model_switch_update_for_compaction_request;
-use crate::compact::insert_initial_context_before_last_real_user;
+use crate::compact::insert_initial_context_before_last_user_anchor;
 use crate::compact::process_compacted_history;
 use crate::compact::should_keep_compacted_history_item;
 use crate::context_manager::ContextManager;
@@ -162,7 +162,7 @@ async fn run_remote_compact_task_inner_impl(
             // These callsites do not get a later post-compaction canonical-context write in
             // `run_turn`, so replacement history must carry canonical context directly.
             let initial_context = sess.build_initial_context(turn_context.as_ref()).await;
-            insert_initial_context_before_last_real_user(&mut new_history, initial_context);
+            insert_initial_context_before_last_user_anchor(&mut new_history, initial_context);
         }
         CompactCallsite::ManualCompact => {
             // Manual `/compact` intentionally rewrites transcript history without reseeding turn
diff --git a/codex-rs/core/tests/suite/compact_remote.rs b/codex-rs/core/tests/suite/compact_remote.rs
index e4168f92d30e..98d5a05488a6 100644
--- a/codex-rs/core/tests/suite/compact_remote.rs
+++ b/codex-rs/core/tests/suite/compact_remote.rs
@@ -1790,7 +1790,7 @@ async fn snapshot_request_shape_remote_mid_turn_compaction_summary_only_layout()
     insta::assert_snapshot!(
         "remote_mid_turn_compaction_summary_only_shapes",
         format_labeled_requests_snapshot(
-            "Remote mid-turn compaction where compact output has only summary user content: continuation layout keeps the summary-only compact output without inserting extra context items.",
+            "Remote mid-turn compaction where compact output has only summary user content: continuation layout reinjects canonical context before the latest summary.",
             &[
                 ("Remote Compaction Request", &compact_request),
                 (
diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_mid_turn_compaction_summary_only_shapes.snap b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_mid_turn_compaction_summary_only_shapes.snap
index 109f91e46e35..88561b245b92 100644
--- a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_mid_turn_compaction_summary_only_shapes.snap
+++ b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_mid_turn_compaction_summary_only_shapes.snap
@@ -1,9 +1,8 @@
 ---
 source: core/tests/suite/compact_remote.rs
-assertion_line: 1790
-expression: "format_labeled_requests_snapshot(\"Remote mid-turn compaction where compact output has only summary user content: continuation layout keeps the summary-only compact output without inserting extra context items.\",\n&[(\"Remote Compaction Request\", &compact_request),\n(\"Remote Post-Compaction History Layout\", &post_compact_turn_request),])"
+expression: "format_labeled_requests_snapshot(\"Remote mid-turn compaction where compact output has only summary user content: continuation layout reinjects canonical context before the latest summary.\",\n&[(\"Remote Compaction Request\", &compact_request),\n(\"Remote Post-Compaction History Layout\", &post_compact_turn_request),])"
 ---
-Scenario: Remote mid-turn compaction where compact output has only summary user content: continuation layout keeps the summary-only compact output without inserting extra context items.
+Scenario: Remote mid-turn compaction where compact output has only summary user content: continuation layout reinjects canonical context before the latest summary.
 
 ## Remote Compaction Request
 00:message/developer:<PERMISSIONS_INSTRUCTIONS>
@@ -14,4 +13,7 @@ Scenario: Remote mid-turn compaction where compact output has only summary user
 05:function_call_output:unsupported call: test_tool
 
 ## Remote Post-Compaction History Layout
-00:message/user:<COMPACTION_SUMMARY>\nREMOTE_SUMMARY_ONLY
+00:message/developer:<PERMISSIONS_INSTRUCTIONS>
+01:message/user:<AGENTS_MD>
+02:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
+03:message/user:<COMPACTION_SUMMARY>\nREMOTE_SUMMARY_ONLY

From 5ae08bc00bbc96f2c0da5a0fff3ecffdd70fb418 Mon Sep 17 00:00:00 2001
From: Charles Cunningham <ccunningham@openai.com>
Date: Wed, 18 Feb 2026 01:52:29 -0800
Subject: [PATCH 71/80] Update compaction test for canonical context
 reinjection

---
 codex-rs/core/tests/suite/compact.rs | 11 ++++++++---
 1 file changed, 8 insertions(+), 3 deletions(-)

diff --git a/codex-rs/core/tests/suite/compact.rs b/codex-rs/core/tests/suite/compact.rs
index 49d3252647da..5f5a3b130e82 100644
--- a/codex-rs/core/tests/suite/compact.rs
+++ b/codex-rs/core/tests/suite/compact.rs
@@ -818,9 +818,14 @@ async fn multiple_auto_compact_per_task_runs_after_token_limit_hit() {
         let body = requests_payloads.clone()[i].body_json();
         let input = body.get("input").and_then(|v| v.as_array()).unwrap();
         let input = normalize_inputs(input);
-        assert_eq!(input.len(), 2);
-        let user_message_received = input[0]["content"][0]["text"].as_str().unwrap();
-        let summary_message = input[1]["content"][0]["text"].as_str().unwrap();
+        assert_eq!(input.len(), 3);
+        let environment_context = input[0]["content"][0]["text"].as_str().unwrap();
+        let user_message_received = input[1]["content"][0]["text"].as_str().unwrap();
+        let summary_message = input[2]["content"][0]["text"].as_str().unwrap();
+        assert!(
+            environment_context.contains("<environment_context>"),
+            "compaction request at index {i} should retain canonical environment context"
+        );
         assert_eq!(user_message_received, user_message);
         assert_eq!(
             summary_message, expected_summary,

From 7476184a225b0d97dac80dfdbdd2133ed2529517 Mon Sep 17 00:00:00 2001
From: Charles Cunningham <ccunningham@openai.com>
Date: Wed, 18 Feb 2026 01:53:05 -0800
Subject: [PATCH 72/80] Document user-anchor behavior for context reinjection

---
 codex-rs/core/src/compact.rs | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/codex-rs/core/src/compact.rs b/codex-rs/core/src/compact.rs
index 109f12b6939e..06863cafa52d 100644
--- a/codex-rs/core/src/compact.rs
+++ b/codex-rs/core/src/compact.rs
@@ -389,6 +389,11 @@ pub(crate) fn process_compacted_history(
     compacted_history
 }
 
+/// Inserts canonical initial context immediately before the latest user anchor in compacted
+/// replacement history:
+/// - prefer the last real (non-summary) user message;
+/// - otherwise fall back to the last summary user message.
+/// If no user anchor exists, this is a no-op.
 pub(crate) fn insert_initial_context_before_last_user_anchor(
     compacted_history: &mut Vec<ResponseItem>,
     initial_context: Vec<ResponseItem>,

From b66c6e63fd87ece2516c10c07e5ce636be92123f Mon Sep 17 00:00:00 2001
From: Charles Cunningham <ccunningham@openai.com>
Date: Wed, 18 Feb 2026 01:54:51 -0800
Subject: [PATCH 73/80] Reuse summary-user helper in compaction anchoring

---
 codex-rs/core/src/compact.rs | 43 +++++++++++++++---------------------
 1 file changed, 18 insertions(+), 25 deletions(-)

diff --git a/codex-rs/core/src/compact.rs b/codex-rs/core/src/compact.rs
index 06863cafa52d..0446e08f927b 100644
--- a/codex-rs/core/src/compact.rs
+++ b/codex-rs/core/src/compact.rs
@@ -363,16 +363,8 @@ pub fn content_items_to_text(content: &[ContentItem]) -> Option<String> {
 pub(crate) fn collect_user_messages(items: &[ResponseItem]) -> Vec<String> {
     items
         .iter()
-        .filter_map(|item| match crate::event_mapping::parse_turn_item(item) {
-            Some(TurnItem::UserMessage(user)) => {
-                if is_summary_message(&user.message()) {
-                    None
-                } else {
-                    Some(user.message())
-                }
-            }
-            _ => None,
-        })
+        .filter_map(parsed_user_message_text)
+        .filter(|message| !is_summary_message(message))
         .collect()
 }
 
@@ -405,13 +397,9 @@ pub(crate) fn insert_initial_context_before_last_user_anchor(
         .iter()
         .rposition(is_real_user_message)
         .or_else(|| {
-            compacted_history.iter().rposition(|item| {
-                matches!(
-                    crate::event_mapping::parse_turn_item(item),
-                    Some(TurnItem::UserMessage(user_message))
-                        if is_summary_message(&user_message.message())
-                )
-            })
+            compacted_history
+                .iter()
+                .rposition(is_summary_user_message_item)
         });
     if let Some(index) = insertion_index {
         compacted_history.splice(index..index, initial_context);
@@ -419,10 +407,18 @@ pub(crate) fn insert_initial_context_before_last_user_anchor(
 }
 
 fn is_real_user_message(item: &ResponseItem) -> bool {
-    matches!(
-        crate::event_mapping::parse_turn_item(item),
-        Some(TurnItem::UserMessage(user_message)) if !is_summary_message(&user_message.message())
-    )
+    parsed_user_message_text(item).is_some_and(|message| !is_summary_message(&message))
+}
+
+fn is_summary_user_message_item(item: &ResponseItem) -> bool {
+    parsed_user_message_text(item).is_some_and(|message| is_summary_message(&message))
+}
+
+fn parsed_user_message_text(item: &ResponseItem) -> Option<String> {
+    match crate::event_mapping::parse_turn_item(item) {
+        Some(TurnItem::UserMessage(user_message)) => Some(user_message.message()),
+        _ => None,
+    }
 }
 
 fn is_user_shell_command_record(item: &ResponseItem) -> bool {
@@ -466,10 +462,7 @@ pub(crate) fn should_keep_compacted_history_item(item: &ResponseItem) -> bool {
                 return true;
             }
 
-            matches!(
-                crate::event_mapping::parse_turn_item(item),
-                Some(TurnItem::UserMessage(_))
-            )
+            parsed_user_message_text(item).is_some()
         }
         // Keep compaction records for local/remote history continuity and token accounting.
         ResponseItem::Compaction { .. } => true,

From 16285c158ff33ba3502511a87421e18d88d7ca26 Mon Sep 17 00:00:00 2001
From: Charles Cunningham <ccunningham@openai.com>
Date: Wed, 18 Feb 2026 02:09:49 -0800
Subject: [PATCH 74/80] Move pre-turn context persistence to run_turn callsite

---
 codex-rs/core/src/codex.rs | 26 +++++++++++++-------------
 1 file changed, 13 insertions(+), 13 deletions(-)

diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs
index 81df6149f64f..5bdd1c60e20d 100644
--- a/codex-rs/core/src/codex.rs
+++ b/codex-rs/core/src/codex.rs
@@ -4284,17 +4284,25 @@ pub(crate) async fn run_turn(
         return None;
     }
 
-    let Ok(pre_turn_compaction_outcome) = run_pre_turn_auto_compaction_if_needed(
+    let pre_turn_compaction_outcome = match run_pre_turn_auto_compaction_if_needed(
         &sess,
         &turn_context,
         auto_compact_limit,
         &incoming_turn_items,
-        &pre_turn_context_items,
     )
     .await
-    else {
-        // Error messaging is emitted inside run_pre_turn_auto_compaction_if_needed.
-        return None;
+    {
+        Ok(outcome) => outcome,
+        Err(()) => {
+            if !pre_turn_context_items.is_empty() {
+                // Preserve model-visible settings updates even when pre-turn compaction fails
+                // before turn input persistence can run.
+                sess.record_conversation_items(&turn_context, &pre_turn_context_items)
+                    .await;
+            }
+            // Error messaging is emitted inside run_pre_turn_auto_compaction_if_needed.
+            return None;
+        }
     };
     persist_pre_turn_items_for_compaction_outcome(
         &sess,
@@ -4766,7 +4774,6 @@ async fn run_pre_turn_auto_compaction_if_needed(
     turn_context: &Arc<TurnContext>,
     auto_compact_limit: i64,
     incoming_turn_items: &[ResponseItem],
-    pre_turn_context_items: &[ResponseItem],
 ) -> Result<PreTurnCompactionOutcome, ()> {
     let total_usage_tokens = sess.get_total_token_usage().await;
     let incoming_items_tokens_estimate = incoming_turn_items
@@ -4790,12 +4797,6 @@ async fn run_pre_turn_auto_compaction_if_needed(
     .await;
 
     if let Err(err) = compact_result {
-        if !pre_turn_context_items.is_empty() {
-            // Preserve model-visible settings updates even when pre-turn compaction fails
-            // before we can persist turn input.
-            sess.record_conversation_items(turn_context, pre_turn_context_items)
-                .await;
-        }
         if matches!(err, CodexErr::Interrupted) {
             return Err(());
         }
@@ -6245,7 +6246,6 @@ mod tests {
             &turn_context,
             10,
             &incoming_turn_items,
-            &[],
         )
         .await
         .expect("pre-turn compaction no-op should not fail");

From 0c5363772ba90eef0d65fdfd866aeaae6ae0a199 Mon Sep 17 00:00:00 2001
From: Charles Cunningham <ccunningham@openai.com>
Date: Wed, 18 Feb 2026 02:11:25 -0800
Subject: [PATCH 75/80] nit

---
 codex-rs/core/src/codex.rs   | 94 ++++++++++++++++++------------------
 codex-rs/core/src/compact.rs |  8 ---
 2 files changed, 48 insertions(+), 54 deletions(-)

diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs
index 5bdd1c60e20d..af1d9206295f 100644
--- a/codex-rs/core/src/codex.rs
+++ b/codex-rs/core/src/codex.rs
@@ -4788,61 +4788,63 @@ async fn run_pre_turn_auto_compaction_if_needed(
         return Ok(PreTurnCompactionOutcome::NotNeeded);
     }
 
-    let compact_result = run_auto_compact(
+    match run_auto_compact(
         sess,
         turn_context,
         CompactCallsite::PreTurnIncludingIncomingUserMessage,
         Some(incoming_turn_items.to_vec()),
     )
-    .await;
+    .await
+    {
+        Ok(()) => {
+            // If compaction no-oped because there was no user-turn boundary even after including
+            // incoming items, do not treat this as "compacted with incoming". The caller must
+            // persist incoming items explicitly in this case.
+            let has_user_turn_boundary_after_compaction = sess
+                .clone_history()
+                .await
+                .raw_items()
+                .iter()
+                .any(crate::context_manager::is_user_turn_boundary);
+            if !has_user_turn_boundary_after_compaction {
+                return Ok(PreTurnCompactionOutcome::NotNeeded);
+            }
 
-    if let Err(err) = compact_result {
-        if matches!(err, CodexErr::Interrupted) {
-            return Err(());
+            Ok(PreTurnCompactionOutcome::CompactedWithIncomingItems)
         }
-        let event = match err {
-            CodexErr::ContextWindowExceeded => {
-                error!(
-                    turn_id = %turn_context.sub_id,
-                    compact_callsite = ?CompactCallsite::PreTurnIncludingIncomingUserMessage,
-                    incoming_items_tokens_estimate,
-                    auto_compact_limit,
-                    reason = "pre-turn compaction exceeded context window",
-                    "incoming user/context is too large for pre-turn auto-compaction flow"
-                );
-                let message = format!(
-                    "Incoming user message and/or turn context is too large to fit in context window. Please reduce the size of your message and try again. (incoming_items_tokens_estimate={incoming_items_tokens_estimate})"
-                );
-                EventMsg::Error(CodexErr::ContextWindowExceeded.to_error_event(Some(message)))
-            }
-            other => {
-                let compact_error_prefix = if should_use_remote_compact_task(&turn_context.provider)
-                {
-                    "Error running remote compact task"
-                } else {
-                    "Error running local compact task"
-                };
-                EventMsg::Error(other.to_error_event(Some(compact_error_prefix.to_string())))
+        Err(err) => {
+            if matches!(err, CodexErr::Interrupted) {
+                return Err(());
             }
-        };
-        sess.send_event(turn_context, event).await;
-        return Err(());
-    }
-
-    // If compaction no-oped because there was no user-turn boundary even after including incoming
-    // items, do not treat this as "compacted with incoming". The caller must persist incoming
-    // items explicitly in this case.
-    let has_user_turn_boundary_after_compaction = sess
-        .clone_history()
-        .await
-        .raw_items()
-        .iter()
-        .any(crate::context_manager::is_user_turn_boundary);
-    if !has_user_turn_boundary_after_compaction {
-        return Ok(PreTurnCompactionOutcome::NotNeeded);
+            let event = match err {
+                CodexErr::ContextWindowExceeded => {
+                    error!(
+                        turn_id = %turn_context.sub_id,
+                        compact_callsite = ?CompactCallsite::PreTurnIncludingIncomingUserMessage,
+                        incoming_items_tokens_estimate,
+                        auto_compact_limit,
+                        reason = "pre-turn compaction exceeded context window",
+                        "incoming user/context is too large for pre-turn auto-compaction flow"
+                    );
+                    let message = format!(
+                        "Incoming user message and/or turn context is too large to fit in context window. Please reduce the size of your message and try again. (incoming_items_tokens_estimate={incoming_items_tokens_estimate})"
+                    );
+                    EventMsg::Error(CodexErr::ContextWindowExceeded.to_error_event(Some(message)))
+                }
+                other => {
+                    let compact_error_prefix =
+                        if should_use_remote_compact_task(&turn_context.provider) {
+                            "Error running remote compact task"
+                        } else {
+                            "Error running local compact task"
+                        };
+                    EventMsg::Error(other.to_error_event(Some(compact_error_prefix.to_string())))
+                }
+            };
+            sess.send_event(turn_context, event).await;
+            Err(())
+        }
     }
-
-    Ok(PreTurnCompactionOutcome::CompactedWithIncomingItems)
 }
 
 fn is_projected_submission_over_auto_compact_limit(
diff --git a/codex-rs/core/src/compact.rs b/codex-rs/core/src/compact.rs
index 0446e08f927b..a4742aa7d118 100644
--- a/codex-rs/core/src/compact.rs
+++ b/codex-rs/core/src/compact.rs
@@ -53,14 +53,6 @@ pub(crate) enum CompactCallsite {
     MidTurnContinuation,
 }
 
-// Canonical context reinjection policy for compacted replacement history:
-// - `MidTurnContinuation` and `PreSamplingModelSwitch`: reinsert canonical initial context above
-//   the last real user message because those paths do not persist/reseed canonical context in a
-//   later post-compaction step.
-// - `ManualCompact`: do not reinsert during compaction; `/compact` reseeds on the next user turn.
-// - `PreTurn*`: do not reinsert into replacement history; `run_turn` persists canonical context
-//   directly above the incoming user message after compaction.
-
 pub(crate) fn should_use_remote_compact_task(provider: &ModelProviderInfo) -> bool {
     provider.is_openai()
 }

From 7b4e4b5853eac1299c8a46086823a7ea49d6613d Mon Sep 17 00:00:00 2001
From: Charles Cunningham <ccunningham@openai.com>
Date: Wed, 18 Feb 2026 02:14:54 -0800
Subject: [PATCH 76/80] Remove redundant developer-role check in compaction
 filter

---
 codex-rs/core/src/compact.rs | 3 ---
 1 file changed, 3 deletions(-)

diff --git a/codex-rs/core/src/compact.rs b/codex-rs/core/src/compact.rs
index a4742aa7d118..7e3b79abb437 100644
--- a/codex-rs/core/src/compact.rs
+++ b/codex-rs/core/src/compact.rs
@@ -442,9 +442,6 @@ fn is_user_shell_command_record(item: &ResponseItem) -> bool {
 pub(crate) fn should_keep_compacted_history_item(item: &ResponseItem) -> bool {
     match item {
         ResponseItem::Message { role, .. } => {
-            if role == "developer" {
-                return false;
-            }
             if role != "user" {
                 return false;
             }

From 6901781b757930b1f247a16f88b12d72958ac9ba Mon Sep 17 00:00:00 2001
From: Charles Cunningham <ccunningham@openai.com>
Date: Wed, 18 Feb 2026 02:18:19 -0800
Subject: [PATCH 77/80] Fix clippy doc list formatting for anchor helper

---
 codex-rs/core/src/compact.rs | 1 +
 1 file changed, 1 insertion(+)

diff --git a/codex-rs/core/src/compact.rs b/codex-rs/core/src/compact.rs
index 7e3b79abb437..ba2875cc158d 100644
--- a/codex-rs/core/src/compact.rs
+++ b/codex-rs/core/src/compact.rs
@@ -377,6 +377,7 @@ pub(crate) fn process_compacted_history(
 /// replacement history:
 /// - prefer the last real (non-summary) user message;
 /// - otherwise fall back to the last summary user message.
+///
 /// If no user anchor exists, this is a no-op.
 pub(crate) fn insert_initial_context_before_last_user_anchor(
     compacted_history: &mut Vec<ResponseItem>,

From ca84df34b8d4106e7ac85e14f3adf4137aeb9b0e Mon Sep 17 00:00:00 2001
From: Charles Cunningham <ccunningham@openai.com>
Date: Wed, 18 Feb 2026 02:22:07 -0800
Subject: [PATCH 78/80] edge case

---
 codex-rs/core/src/compact.rs | 50 +++++++++++++++++++++++++++++++++++-
 1 file changed, 49 insertions(+), 1 deletion(-)

diff --git a/codex-rs/core/src/compact.rs b/codex-rs/core/src/compact.rs
index ba2875cc158d..7eef5b530693 100644
--- a/codex-rs/core/src/compact.rs
+++ b/codex-rs/core/src/compact.rs
@@ -378,7 +378,7 @@ pub(crate) fn process_compacted_history(
 /// - prefer the last real (non-summary) user message;
 /// - otherwise fall back to the last summary user message.
 ///
-/// If no user anchor exists, this is a no-op.
+/// If no user anchor exists, append initial context at the end.
 pub(crate) fn insert_initial_context_before_last_user_anchor(
     compacted_history: &mut Vec<ResponseItem>,
     initial_context: Vec<ResponseItem>,
@@ -396,6 +396,8 @@ pub(crate) fn insert_initial_context_before_last_user_anchor(
         });
     if let Some(index) = insertion_index {
         compacted_history.splice(index..index, initial_context);
+    } else {
+        compacted_history.extend(initial_context);
     }
 }
 
@@ -1334,4 +1336,50 @@ keep me updated
         ];
         assert_eq!(compacted_history, expected);
     }
+
+    #[test]
+    fn insert_initial_context_before_last_user_anchor_appends_when_no_user_anchor_exists() {
+        let mut compacted_history = vec![ResponseItem::Message {
+            id: None,
+            role: "assistant".to_string(),
+            content: vec![ContentItem::OutputText {
+                text: "assistant only".to_string(),
+            }],
+            end_turn: None,
+            phase: None,
+        }];
+        let initial_context = vec![ResponseItem::Message {
+            id: None,
+            role: "developer".to_string(),
+            content: vec![ContentItem::InputText {
+                text: "fresh permissions".to_string(),
+            }],
+            end_turn: None,
+            phase: None,
+        }];
+
+        insert_initial_context_before_last_user_anchor(&mut compacted_history, initial_context);
+
+        let expected = vec![
+            ResponseItem::Message {
+                id: None,
+                role: "assistant".to_string(),
+                content: vec![ContentItem::OutputText {
+                    text: "assistant only".to_string(),
+                }],
+                end_turn: None,
+                phase: None,
+            },
+            ResponseItem::Message {
+                id: None,
+                role: "developer".to_string(),
+                content: vec![ContentItem::InputText {
+                    text: "fresh permissions".to_string(),
+                }],
+                end_turn: None,
+                phase: None,
+            },
+        ];
+        assert_eq!(compacted_history, expected);
+    }
 }

From c085499bb090cf7f67a0517dc77fece6be8ac5d3 Mon Sep 17 00:00:00 2001
From: Charles Cunningham <ccunningham@openai.com>
Date: Wed, 18 Feb 2026 09:13:14 -0800
Subject: [PATCH 79/80] Avoid dropping historical duplicates in remote echo
 stripping

---
 codex-rs/core/src/compact_remote.rs | 122 ++++++++++++++++++++++------
 1 file changed, 95 insertions(+), 27 deletions(-)

diff --git a/codex-rs/core/src/compact_remote.rs b/codex-rs/core/src/compact_remote.rs
index 10dc47205153..ac8f6c93005f 100644
--- a/codex-rs/core/src/compact_remote.rs
+++ b/codex-rs/core/src/compact_remote.rs
@@ -99,6 +99,7 @@ async fn run_remote_compact_task_inner_impl(
         &base_instructions,
         incoming_items.as_deref(),
     );
+    let historical_items_before_incoming = history.raw_items().to_vec();
     if let Some(incoming_items) = incoming_items.as_ref() {
         history.record_items(incoming_items.iter(), turn_context.truncation_policy);
     }
@@ -180,7 +181,11 @@ async fn run_remote_compact_task_inner_impl(
             .filter(|item| should_keep_compacted_history_item(item))
             .cloned()
             .collect();
-        remove_incoming_echoes_from_compacted_history(&mut new_history, &incoming_history_items);
+        remove_incoming_echoes_from_compacted_history(
+            &mut new_history,
+            &incoming_history_items,
+            &historical_items_before_incoming,
+        );
     }
     // Reattach stripped model-switch updates into replacement history so post-compaction
     // sampling keeps model-switch guidance regardless of compaction callsite.
@@ -234,34 +239,48 @@ fn build_compact_request_log_data(
 fn remove_incoming_echoes_from_compacted_history(
     new_history: &mut Vec<ResponseItem>,
     incoming_history_items: &[ResponseItem],
+    historical_items_before_incoming: &[ResponseItem],
 ) {
+    let items_match =
+        |candidate: &ResponseItem, incoming_item: &ResponseItem| match (candidate, incoming_item) {
+            (
+                ResponseItem::Message {
+                    role: candidate_role,
+                    content: candidate_content,
+                    ..
+                },
+                ResponseItem::Message {
+                    role: incoming_role,
+                    content: incoming_content,
+                    ..
+                },
+            ) => candidate_role == incoming_role && candidate_content == incoming_content,
+            (
+                ResponseItem::Compaction {
+                    encrypted_content: candidate_content,
+                },
+                ResponseItem::Compaction {
+                    encrypted_content: incoming_content,
+                },
+            ) => candidate_content == incoming_content,
+            _ => candidate == incoming_item,
+        };
+
     for incoming_item in incoming_history_items {
-        if let Some(index) =
-            new_history
-                .iter()
-                .rposition(|candidate| match (candidate, incoming_item) {
-                    (
-                        ResponseItem::Message {
-                            role: candidate_role,
-                            content: candidate_content,
-                            ..
-                        },
-                        ResponseItem::Message {
-                            role: incoming_role,
-                            content: incoming_content,
-                            ..
-                        },
-                    ) => candidate_role == incoming_role && candidate_content == incoming_content,
-                    (
-                        ResponseItem::Compaction {
-                            encrypted_content: candidate_content,
-                        },
-                        ResponseItem::Compaction {
-                            encrypted_content: incoming_content,
-                        },
-                    ) => candidate_content == incoming_content,
-                    _ => candidate == incoming_item,
-                })
+        let historical_count = historical_items_before_incoming
+            .iter()
+            .filter(|candidate| items_match(candidate, incoming_item))
+            .count();
+        let compacted_count = new_history
+            .iter()
+            .filter(|candidate| items_match(candidate, incoming_item))
+            .count();
+        if compacted_count <= historical_count {
+            continue;
+        }
+        if let Some(index) = new_history
+            .iter()
+            .rposition(|candidate| items_match(candidate, incoming_item))
         {
             new_history.remove(index);
         }
@@ -371,6 +390,10 @@ mod tests {
         }
     }
 
+    fn summary_user_message(text: &str) -> ResponseItem {
+        user_message(&format!("{}\n{text}", crate::compact::SUMMARY_PREFIX))
+    }
+
     #[test]
     fn trim_accounts_for_incoming_items_tokens() {
         let base_instructions = BaseInstructions {
@@ -425,4 +448,49 @@ mod tests {
         );
         assert_eq!(history.raw_items(), vec![user_message("USER_ONE")]);
     }
+
+    #[test]
+    fn remove_incoming_echoes_preserves_historical_duplicates_when_not_echoed() {
+        let mut compacted_history = vec![
+            user_message("REPEAT_MESSAGE"),
+            summary_user_message("LATEST_SUMMARY"),
+        ];
+        let incoming_items = vec![user_message("REPEAT_MESSAGE")];
+        let historical_items_before_incoming = vec![user_message("REPEAT_MESSAGE")];
+
+        remove_incoming_echoes_from_compacted_history(
+            &mut compacted_history,
+            &incoming_items,
+            &historical_items_before_incoming,
+        );
+
+        let expected = vec![
+            user_message("REPEAT_MESSAGE"),
+            summary_user_message("LATEST_SUMMARY"),
+        ];
+        assert_eq!(compacted_history, expected);
+    }
+
+    #[test]
+    fn remove_incoming_echoes_removes_net_new_echoes_only() {
+        let mut compacted_history = vec![
+            user_message("REPEAT_MESSAGE"),
+            user_message("REPEAT_MESSAGE"),
+            summary_user_message("LATEST_SUMMARY"),
+        ];
+        let incoming_items = vec![user_message("REPEAT_MESSAGE")];
+        let historical_items_before_incoming = vec![user_message("REPEAT_MESSAGE")];
+
+        remove_incoming_echoes_from_compacted_history(
+            &mut compacted_history,
+            &incoming_items,
+            &historical_items_before_incoming,
+        );
+
+        let expected = vec![
+            user_message("REPEAT_MESSAGE"),
+            summary_user_message("LATEST_SUMMARY"),
+        ];
+        assert_eq!(compacted_history, expected);
+    }
 }

From 8e6b114df018c6302e83ee0ef7b4c322a1015d8a Mon Sep 17 00:00:00 2001
From: Charles Cunningham <ccunningham@openai.com>
Date: Wed, 18 Feb 2026 09:16:00 -0800
Subject: [PATCH 80/80] Rename /compact start test for no-history semantics

---
 codex-rs/app-server/tests/suite/v2/compaction.rs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/codex-rs/app-server/tests/suite/v2/compaction.rs b/codex-rs/app-server/tests/suite/v2/compaction.rs
index 09d75879f717..d663be130fd6 100644
--- a/codex-rs/app-server/tests/suite/v2/compaction.rs
+++ b/codex-rs/app-server/tests/suite/v2/compaction.rs
@@ -199,7 +199,7 @@ async fn auto_compaction_remote_emits_started_and_completed_items() -> Result<()
 }
 
 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
-async fn thread_compact_start_triggers_compaction_and_returns_empty_response() -> Result<()> {
+async fn thread_compact_start_without_history_emits_started_and_completed_items() -> Result<()> {
     skip_if_no_network!(Ok(()));
 
     let server = responses::start_mock_server().await;
