Rebase resume model switch on hydrated history

This commit is contained in:
Charles Cunningham
2026-02-17 16:03:15 -08:00
parent 08bfb1e7f2
commit 8a4e7dab76
2 changed files with 127 additions and 6 deletions

View File

@@ -3482,12 +3482,34 @@ mod handlers {
if let Some(previous_model) = sess.previous_model().await
&& let Some(previous_context_item) = previous_context_item.as_ref()
&& previous_model != previous_context_item.model
&& !update_items
.iter()
.any(Session::is_model_switch_developer_message)
{
// Apply resume/fork model hydration only to model-switch diffing so it does not
// suppress other updates (for example personality changes).
// Rebase model-switch diffing on resume/fork model hydration so model-switch
// updates reflect rollout history while other diffs (for example personality) still
// use the real previous turn context.
let model_switch_insert_index = update_items
.iter()
.position(Session::is_model_switch_developer_message)
.or_else(|| {
update_items.iter().position(|item| {
let codex_protocol::models::ResponseItem::Message {
role,
content,
..
} = item
else {
return false;
};
role == "developer"
&& content.iter().any(|content_item| {
matches!(
content_item,
codex_protocol::models::ContentItem::InputText { text } if text.starts_with("<personality_spec>")
)
})
})
});
update_items.retain(|item| !Session::is_model_switch_developer_message(item));
let mut previous_context_item_for_model_switch = previous_context_item.clone();
previous_context_item_for_model_switch.model = previous_model;
if let Some(model_switch_item) =
@@ -3496,7 +3518,11 @@ mod handlers {
&current_context_item,
)
{
update_items.push(model_switch_item);
if let Some(index) = model_switch_insert_index {
update_items.insert(index, model_switch_item);
} else {
update_items.push(model_switch_item);
}
}
}
if !update_items.is_empty() {

View File

@@ -472,3 +472,98 @@ async fn resume_model_hydration_does_not_suppress_personality_update() -> Result
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn resume_override_matching_rollout_model_skips_model_switch_update() -> Result<()> {
skip_if_no_network!(Ok(()));
let server = start_mock_server().await;
let mut builder = test_codex().with_config(|config| {
config.model = Some("gpt-5.2".to_string());
});
let initial = builder.build(&server).await?;
let codex = Arc::clone(&initial.codex);
let home = initial.home.clone();
let rollout_path = initial
.session_configured
.rollout_path
.clone()
.expect("rollout path");
let initial_mock = mount_sse_once(
&server,
sse(vec![
ev_response_created("resp-initial"),
ev_assistant_message("msg-1", "Completed first turn"),
ev_completed("resp-initial"),
]),
)
.await;
codex
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: "record initial rollout model".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})
.await?;
wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await;
let _ = initial_mock.single_request();
let resumed_mock = mount_sse_once(
&server,
sse(vec![
ev_response_created("resp-resume"),
ev_assistant_message("msg-2", "Resumed turn"),
ev_completed("resp-resume"),
]),
)
.await;
let mut resume_builder = test_codex().with_config(|config| {
config.model = Some("gpt-5.2-codex".to_string());
});
let resumed = resume_builder.resume(&server, home, rollout_path).await?;
resumed
.codex
.submit(Op::OverrideTurnContext {
cwd: None,
approval_policy: None,
sandbox_policy: None,
windows_sandbox_level: None,
model: Some("gpt-5.2".to_string()),
effort: None,
summary: None,
collaboration_mode: None,
personality: None,
})
.await?;
resumed
.codex
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: "first turn after override to rollout model".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})
.await?;
wait_for_event(&resumed.codex, |event| {
matches!(event, EventMsg::TurnComplete(_))
})
.await;
let request = resumed_mock.single_request();
let developer_texts = request.message_input_texts("developer");
let model_switch_count = developer_texts
.iter()
.filter(|text| text.contains("<model_switch>"))
.count();
assert_eq!(
model_switch_count, 0,
"did not expect model switch update when override matches rollout model"
);
Ok(())
}