From 55fa6f9a7728495cda712cfe73651cfd5944680b Mon Sep 17 00:00:00 2001 From: Dylan Hurd Date: Sat, 16 May 2026 02:05:12 -0700 Subject: [PATCH] Deduplicate forked subagent developer instructions --- codex-rs/core/src/agent/control.rs | 14 +++++++++----- codex-rs/core/src/agent/control_tests.rs | 18 ++++++++++++++++++ 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/codex-rs/core/src/agent/control.rs b/codex-rs/core/src/agent/control.rs index 71bc026a13..15f7ae992d 100644 --- a/codex-rs/core/src/agent/control.rs +++ b/codex-rs/core/src/agent/control.rs @@ -396,10 +396,9 @@ impl AgentControl { forked_rollout_items = truncate_rollout_to_last_n_fork_turns(&forked_rollout_items, *last_n_turns); } - // 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. - let multi_agent_v2_usage_hint_texts_to_filter: Vec = + // Standalone developer items that the child reinjects at startup should not + // survive the inherited forked history, or they appear twice in the child prompt. + let mut forked_developer_texts_to_filter: Vec = if let Some(parent_thread) = parent_thread.as_ref() { parent_thread .codex @@ -417,11 +416,16 @@ impl AgentControl { } else { Vec::new() }; + if let Some(developer_instructions) = config.developer_instructions.as_deref() + && !developer_instructions.is_empty() + { + forked_developer_texts_to_filter.push(developer_instructions.to_string()); + } forked_rollout_items.retain(|item| { if let RolloutItem::ResponseItem(ResponseItem::Message { role, content, .. }) = item && role == "developer" && let [ContentItem::InputText { text }] = content.as_slice() - && multi_agent_v2_usage_hint_texts_to_filter + && forked_developer_texts_to_filter .iter() .any(|usage_hint_text| usage_hint_text == text) { diff --git a/codex-rs/core/src/agent/control_tests.rs b/codex-rs/core/src/agent/control_tests.rs index b95aad4489..688bbd0bf7 100644 --- a/codex-rs/core/src/agent/control_tests.rs +++ b/codex-rs/core/src/agent/control_tests.rs @@ -614,6 +614,7 @@ async fn spawn_agent_can_fork_parent_thread_history_with_sanitized_items() { Some("Child root guidance.".to_string()); child_config.multi_agent_v2.subagent_usage_hint_text = Some("Child subagent guidance.".to_string()); + child_config.developer_instructions = Some("Parent developer instructions.".to_string()); let new_thread = harness .manager .start_thread(parent_config.clone()) @@ -655,6 +656,14 @@ async fn spawn_agent_can_fork_parent_thread_history_with_sanitized_items() { }], phase: None, }, + ResponseItem::Message { + id: None, + role: "developer".to_string(), + content: vec![ContentItem::InputText { + text: "Parent developer instructions.".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 +735,15 @@ 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 developer instructions that will be reinjected" + ); let expected = ( child_thread_id,