From d1ca6f64c17378161b7a83b11b686033aa36f8a0 Mon Sep 17 00:00:00 2001 From: jif-oai Date: Mon, 18 May 2026 18:54:15 +0200 Subject: [PATCH] Drop parent spawn call from forked history --- codex-rs/core/src/agent/control.rs | 13 ++++++++--- codex-rs/core/src/agent/control_tests.rs | 28 +++++++++++++++++++++--- 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/codex-rs/core/src/agent/control.rs b/codex-rs/core/src/agent/control.rs index 81572113ba..54ababd3b6 100644 --- a/codex-rs/core/src/agent/control.rs +++ b/codex-rs/core/src/agent/control.rs @@ -18,12 +18,12 @@ use codex_protocol::SessionId; use codex_protocol::ThreadId; use codex_protocol::error::CodexErr; use codex_protocol::error::Result as CodexResult; -#[cfg(test)] use codex_protocol::models::ResponseItem; use codex_protocol::protocol::InitialHistory; use codex_protocol::protocol::InterAgentCommunication; use codex_protocol::protocol::Op; use codex_protocol::protocol::ResumedHistory; +use codex_protocol::protocol::RolloutItem; use codex_protocol::protocol::SessionSource; use codex_protocol::protocol::SubAgentSource; use codex_protocol::protocol::ThreadSource; @@ -316,11 +316,11 @@ impl AgentControl { inherited_shell_snapshot: Option>, inherited_exec_policy: Option>, ) -> CodexResult { - if options.fork_parent_spawn_call_id.is_none() { + let Some(parent_spawn_call_id) = options.fork_parent_spawn_call_id.as_deref() else { return Err(CodexErr::Fatal( "spawn_agent fork requires a parent spawn call id".to_string(), )); - } + }; let Some(fork_mode) = options.fork_mode.as_ref() else { return Err(CodexErr::Fatal( "spawn_agent fork requires a fork mode".to_string(), @@ -363,6 +363,13 @@ impl AgentControl { forked_rollout_items = truncate_rollout_to_last_n_fork_turns(&forked_rollout_items, *last_n_turns); } + forked_rollout_items.retain(|item| { + !matches!( + item, + RolloutItem::ResponseItem(ResponseItem::FunctionCall { name, call_id, .. }) + if name == "spawn_agent" && call_id == parent_spawn_call_id + ) + }); state .fork_thread_with_source( diff --git a/codex-rs/core/src/agent/control_tests.rs b/codex-rs/core/src/agent/control_tests.rs index 1baae94058..40a01f4a9d 100644 --- a/codex-rs/core/src/agent/control_tests.rs +++ b/codex-rs/core/src/agent/control_tests.rs @@ -177,6 +177,15 @@ fn history_contains_text(history_items: &[ResponseItem], needle: &str) -> bool { }) } +fn history_contains_function_call(history_items: &[ResponseItem], call_id: &str) -> bool { + history_items.iter().any(|item| { + matches!( + item, + ResponseItem::FunctionCall { call_id: item_call_id, .. } if item_call_id == call_id + ) + }) +} + fn history_contains_assistant_inter_agent_communication( history_items: &[ResponseItem], expected: &InterAgentCommunication, @@ -623,7 +632,7 @@ async fn spawn_agent_creates_thread_and_sends_prompt() { } #[tokio::test] -async fn spawn_agent_can_fork_parent_thread_history_without_rewriting_rollout() { +async fn spawn_agent_can_fork_parent_thread_history_without_replaying_parent_spawn_call() { let harness = AgentControlHarness::new().await; let mut parent_config = harness.config.clone(); let _ = parent_config.features.enable(Feature::MultiAgentV2); @@ -730,11 +739,16 @@ async fn spawn_agent_can_fork_parent_thread_history_without_rewriting_rollout() assert_ne!(child_thread_id, parent_thread_id); let history = child_thread.codex.session.clone_history().await; let mut expected_history = vec![user_message("parent seed context")]; - expected_history.extend(parent_items); + let mut expected_parent_items = parent_items; + assert_eq!( + expected_parent_items.pop(), + Some(spawn_agent_call(&parent_spawn_call_id)) + ); + expected_history.extend(expected_parent_items); assert_eq!( history.raw_items(), expected_history.as_slice(), - "forked child history should replay the parent rollout without rewriting model-visible items" + "forked child history should replay the parent rollout except for the trigger spawn call" ); assert_eq!( serde_json::to_value(child_thread.codex.session.reference_context_item().await) @@ -824,6 +838,10 @@ async fn spawn_agent_fork_flushes_parent_rollout_before_loading_history() { history_contains_text(history.raw_items(), "unflushed final answer"), "forked child history should include unflushed assistant final answers after flushing the parent rollout" ); + assert!( + !history_contains_function_call(history.raw_items(), &parent_spawn_call_id), + "forked child history should drop the trigger spawn call after flushing the parent rollout" + ); let _ = harness .control @@ -947,6 +965,10 @@ async fn spawn_agent_fork_last_n_turns_keeps_only_recent_turns() { history_contains_text(history.raw_items(), "current parent task"), "forked child history should keep the parent user message from the requested last-N turn window" ); + assert!( + !history_contains_function_call(history.raw_items(), &parent_spawn_call_id), + "forked child history should drop the trigger spawn call from the requested last-N turn window" + ); let _ = harness .control