Differentiate model-switch compaction failures from oversize input

This commit is contained in:
Charles Cunningham
2026-02-17 18:29:12 -08:00
parent bddd53bae5
commit d9b02b6a9e
2 changed files with 149 additions and 8 deletions
+25 -8
View File
@@ -4284,6 +4284,31 @@ pub(crate) async fn run_turn(
collaboration_mode_kind: turn_context.collaboration_mode.mode,
});
sess.send_event(&turn_context, event).await;
let total_usage_tokens_before_compaction = sess.get_total_token_usage().await;
if let Err(err) = maybe_run_previous_model_inline_compact(
&sess,
&turn_context,
total_usage_tokens_before_compaction,
)
.await
{
if !pre_turn_context_items.is_empty() {
// Preserve model-visible settings updates even when pre-turn compaction fails
// before we can persist turn input.
sess.record_conversation_items(&turn_context, &pre_turn_context_items)
.await;
}
let compact_error_prefix = if should_use_remote_compact_task(&turn_context.provider) {
"Error running remote compact task"
} else {
"Error running local compact task"
};
let event = EventMsg::Error(err.to_error_event(Some(compact_error_prefix.to_string())));
sess.send_event(&turn_context, event).await;
return None;
}
let pre_turn_compaction_outcome = match run_pre_turn_auto_compaction_if_needed(
&sess,
&turn_context,
@@ -4764,14 +4789,6 @@ async fn run_pre_turn_auto_compaction_if_needed(
auto_compact_limit: i64,
incoming_turn_items: &[ResponseItem],
) -> CodexResult<PreTurnCompactionOutcome> {
let total_usage_tokens_before_compaction = sess.get_total_token_usage().await;
maybe_run_previous_model_inline_compact(
sess,
turn_context,
total_usage_tokens_before_compaction,
)
.await?;
let total_usage_tokens = sess.get_total_token_usage().await;
let incoming_items_tokens_estimate = incoming_turn_items
.iter()
+124
View File
@@ -1800,6 +1800,130 @@ async fn pre_sampling_compact_runs_on_switch_to_smaller_context_model() {
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn pre_sampling_compact_context_window_failure_surfaces_compact_task_error() {
skip_if_no_network!();
let server = MockServer::start().await;
let previous_model = "gpt-5.2-codex";
let next_model = "gpt-5.1-codex-max";
let models_mock = mount_models_once(
&server,
ModelsResponse {
models: vec![
model_info_with_context_window(previous_model, 273_000),
model_info_with_context_window(next_model, 125_000),
],
},
)
.await;
let request_log = mount_sse_sequence(
&server,
vec![
sse(vec![
ev_assistant_message("m1", "before switch"),
ev_completed_with_tokens("r1", 120_000),
]),
sse_failed(
"compact-failed",
"context_length_exceeded",
CONTEXT_LIMIT_MESSAGE,
),
],
)
.await;
let mut model_provider = non_openai_model_provider(&server);
model_provider.stream_max_retries = Some(0);
let mut builder = test_codex()
.with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing())
.with_model(previous_model)
.with_config(move |config| {
config.model_provider = model_provider;
set_test_compact_prompt(config);
config.features.enable(Feature::RemoteModels);
});
let test = builder.build(&server).await.expect("build test codex");
test.codex
.submit(Op::UserTurn {
items: vec![UserInput::Text {
text: "before switch".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: test.cwd.path().to_path_buf(),
approval_policy: AskForApproval::Never,
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: previous_model.to_string(),
effort: None,
summary: ReasoningSummary::Auto,
collaboration_mode: None,
personality: None,
})
.await
.expect("submit first user turn");
wait_for_event(&test.codex, |event| {
matches!(event, EventMsg::TurnComplete(_))
})
.await;
test.codex
.submit(Op::UserTurn {
items: vec![UserInput::Text {
text: "after switch".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: test.cwd.path().to_path_buf(),
approval_policy: AskForApproval::Never,
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: next_model.to_string(),
effort: None,
summary: ReasoningSummary::Auto,
collaboration_mode: None,
personality: None,
})
.await
.expect("submit second user turn");
let error_message = wait_for_event_match(&test.codex, |event| match event {
EventMsg::Error(err) => Some(err.message.clone()),
_ => None,
})
.await;
wait_for_event(&test.codex, |event| {
matches!(event, EventMsg::TurnComplete(_))
})
.await;
assert_eq!(models_mock.requests().len(), 1);
assert!(
error_message.contains("Error running local compact task"),
"expected local compact-task failure prefix, got {error_message}"
);
assert!(
!error_message.contains(
"Incoming user message and/or turn context is too large to fit in context window"
),
"model-switch pre-sampling compaction failure should not be misclassified as incoming-input oversize: {error_message}"
);
let requests = request_log.requests();
assert_eq!(
requests.len(),
2,
"expected first user turn and one pre-sampling compaction request"
);
let compact_request_body = requests[1].body_json().to_string();
assert!(
body_contains_text(&compact_request_body, SUMMARIZATION_PROMPT),
"second request should be the pre-sampling compaction request"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn pre_sampling_compact_runs_after_resume_and_switch_to_smaller_model() {
skip_if_no_network!();