From a926bb004afcf0456efe7f2d11751c3ac5d7c876 Mon Sep 17 00:00:00 2001 From: jif-oai Date: Sun, 17 May 2026 14:13:59 +0200 Subject: [PATCH] Drop parent setup context from forked agents --- codex-rs/core/src/agent/control.rs | 45 ++++++++++++++++++++++++ codex-rs/core/src/agent/control_tests.rs | 37 ++++++++++--------- 2 files changed, 65 insertions(+), 17 deletions(-) diff --git a/codex-rs/core/src/agent/control.rs b/codex-rs/core/src/agent/control.rs index 71bc026a13..d556e8ec4d 100644 --- a/codex-rs/core/src/agent/control.rs +++ b/codex-rs/core/src/agent/control.rs @@ -127,6 +127,47 @@ fn keep_forked_rollout_item(item: &RolloutItem) -> bool { } } +fn strip_parent_context_updates_from_forked_rollout(items: &mut Vec) { + let mut drop_item = vec![false; items.len()]; + for idx in 0..items.len() { + if !matches!(items[idx], RolloutItem::TurnContext(_)) { + continue; + } + + drop_item[idx] = true; + let mut context_idx = idx; + while context_idx > 0 { + let should_drop = match &items[context_idx - 1] { + RolloutItem::ResponseItem(ResponseItem::Message { role, content, .. }) + if role == "developer" => + { + true + } + RolloutItem::ResponseItem(ResponseItem::Message { role, content, .. }) + if role == "user" + && crate::event_mapping::is_contextual_user_message_content(content) => + { + true + } + _ => false, + }; + if !should_drop { + break; + } + + context_idx -= 1; + drop_item[context_idx] = true; + } + } + + let mut idx = 0; + items.retain(|_| { + let keep = !drop_item[idx]; + idx += 1; + keep + }); +} + /// Control-plane handle for multi-agent operations. /// `AgentControl` is held by each session (via `SessionServices`). It provides capability to /// spawn new agents and the inter-agent communication layer. @@ -396,6 +437,10 @@ impl AgentControl { forked_rollout_items = truncate_rollout_to_last_n_fork_turns(&forked_rollout_items, *last_n_turns); } + // Parent context updates are keyed by parent TurnContext snapshots. Forked children drop + // those snapshots and build their own startup context, so inherited context messages would + // otherwise be duplicated or stale in the child prompt. + strip_parent_context_updates_from_forked_rollout(&mut forked_rollout_items); // MultiAgentV2 root/subagent usage hints are injected as standalone developer // messages at thread start. When forking history, drop hints from the parent // so the child gets a fresh hint that matches its own session source/config. diff --git a/codex-rs/core/src/agent/control_tests.rs b/codex-rs/core/src/agent/control_tests.rs index b95aad4489..859e903db4 100644 --- a/codex-rs/core/src/agent/control_tests.rs +++ b/codex-rs/core/src/agent/control_tests.rs @@ -608,6 +608,7 @@ async fn spawn_agent_can_fork_parent_thread_history_with_sanitized_items() { Some("Parent root guidance.".to_string()); parent_config.multi_agent_v2.subagent_usage_hint_text = Some("Parent subagent guidance.".to_string()); + parent_config.developer_instructions = Some("Parent developer instructions.".to_string()); let mut child_config = harness.config.clone(); let _ = child_config.features.enable(Feature::MultiAgentV2); child_config.multi_agent_v2.root_agent_usage_hint_text = @@ -621,10 +622,15 @@ async fn spawn_agent_can_fork_parent_thread_history_with_sanitized_items() { .expect("start parent thread"); let parent_thread_id = new_thread.thread_id; let parent_thread = new_thread.thread; + let turn_context = parent_thread.codex.session.new_default_turn().await; + parent_thread + .codex + .session + .record_context_updates_and_set_reference_context_item(turn_context.as_ref()) + .await; parent_thread .inject_user_message_without_turn("parent seed context".to_string()) .await; - let turn_context = parent_thread.codex.session.new_default_turn().await; let parent_spawn_call_id = "spawn-call-history".to_string(); let trigger_message = InterAgentCommunication::new( AgentPath::root(), @@ -639,22 +645,6 @@ async fn spawn_agent_can_fork_parent_thread_history_with_sanitized_items() { .record_conversation_items( turn_context.as_ref(), &[ - ResponseItem::Message { - id: None, - role: "developer".to_string(), - content: vec![ContentItem::InputText { - text: "Parent root guidance.".to_string(), - }], - phase: None, - }, - ResponseItem::Message { - id: None, - role: "developer".to_string(), - content: vec![ContentItem::InputText { - text: "Parent subagent guidance.".to_string(), - }], - phase: None, - }, assistant_message("parent commentary", Some(MessagePhase::Commentary)), assistant_message("parent final answer", Some(MessagePhase::FinalAnswer)), assistant_message("parent unknown phase", /*phase*/ None), @@ -726,6 +716,19 @@ async fn spawn_agent_can_fork_parent_thread_history_with_sanitized_items() { &expected_history, "forked child history should keep only parent user messages and assistant final answers" ); + let child_rollout_path = child_thread + .rollout_path() + .expect("forked child rollout path"); + let child_rollout = std::fs::read_to_string(&child_rollout_path) + .expect("forked child rollout should be readable"); + assert!( + !child_rollout.contains("Parent developer instructions."), + "forked child rollout should not retain parent developer instructions from setup context" + ); + assert!( + !child_rollout.contains("Parent root guidance."), + "forked child rollout should not retain parent multi-agent setup guidance" + ); let expected = ( child_thread_id,