Drop parent spawn call from forked history

This commit is contained in:
jif-oai
2026-05-18 18:54:15 +02:00
parent d845500096
commit d1ca6f64c1
2 changed files with 35 additions and 6 deletions
+10 -3
View File
@@ -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<Arc<ShellSnapshot>>,
inherited_exec_policy: Option<Arc<crate::exec_policy::ExecPolicyManager>>,
) -> CodexResult<crate::thread_manager::NewThread> {
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(
+25 -3
View File
@@ -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