compact: preserve incoming items during pre-turn trim retries

This commit is contained in:
Charles Cunningham
2026-02-17 17:10:48 -08:00
parent 1e6e4c15ea
commit c40132caf9
5 changed files with 98 additions and 10 deletions
+2 -2
View File
@@ -3406,10 +3406,10 @@ mod handlers {
// Attempt to inject input into current task.
if let Err(SteerInputError::NoActiveTurn(items)) = sess.steer_input(items, None).await {
sess.seed_initial_context_if_needed(&current_context).await;
let resumed_model = sess.take_pending_resume_previous_model().await;
let previous_model = sess.previous_model().await;
let pre_turn_context_items = sess.build_settings_update_items(
previous_context.as_ref(),
resumed_model.as_deref(),
previous_model.as_deref(),
&current_context,
);
let has_user_input = !items.is_empty();
+7 -1
View File
@@ -167,6 +167,12 @@ async fn run_compact_task_inner(
&[initial_input_for_turn.into()],
turn_context.truncation_policy,
);
// Keep incoming turn items and the compaction prompt pinned at the tail while trimming.
// Pre-turn compaction should fail with ContextWindowExceeded rather than dropping incoming
// items to force compaction to succeed.
let protected_tail_items = incoming_items
.as_ref()
.map_or(1_usize, |items| items.len().saturating_add(1));
let mut truncated_count = 0usize;
@@ -224,7 +230,7 @@ async fn run_compact_task_inner(
return Err(CodexErr::Interrupted);
}
Err(e @ CodexErr::ContextWindowExceeded) => {
if turn_input_len > 1 {
if turn_input_len > 1 && history.raw_items().len() > protected_tail_items {
// Trim from the beginning to preserve cache (prefix-based) and keep recent
// messages intact.
error!(
+83 -2
View File
@@ -3235,9 +3235,9 @@ async fn snapshot_request_shape_pre_turn_compaction_context_window_exceeded() {
insta::assert_snapshot!(
"pre_turn_compaction_context_window_exceeded_shapes",
format_labeled_requests_snapshot(
"Pre-turn auto-compaction context-window failure: compaction request excludes the incoming user message and the turn errors.",
"Pre-turn auto-compaction context-window failure: compaction request includes the incoming user message and the turn errors.",
&[(
"Local Compaction Request (Incoming User Excluded)",
"Local Compaction Request (Incoming User Included)",
&requests[1]
),]
)
@@ -3249,6 +3249,87 @@ async fn snapshot_request_shape_pre_turn_compaction_context_window_exceeded() {
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn pre_turn_local_compaction_trim_retries_keep_incoming_items() {
skip_if_no_network!();
let server = start_mock_server().await;
let compact_failed = sse_failed(
"compact-failed",
"context_length_exceeded",
CONTEXT_LIMIT_MESSAGE,
);
let request_log = mount_sse_sequence(
&server,
vec![
compact_failed.clone(),
compact_failed.clone(),
compact_failed.clone(),
compact_failed,
],
)
.await;
let model_provider = non_openai_model_provider(&server);
let codex = test_codex()
.with_config(move |config| {
config.model_provider = model_provider;
set_test_compact_prompt(config);
config.model_auto_compact_token_limit = Some(1);
})
.build(&server)
.await
.expect("build codex")
.codex;
let incoming_text = "PRETURN_INCOMING_USER";
codex
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: incoming_text.to_string(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})
.await
.expect("submit user input");
let error_message = wait_for_event_match(&codex, |event| match event {
EventMsg::Error(err) => Some(err.message.clone()),
_ => None,
})
.await;
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
assert!(
error_message.contains(
"Incoming user message and/or turn context is too large to fit in context window"
),
"expected oversized incoming-items error, got {error_message}"
);
let compact_attempts = request_log.requests();
assert_eq!(
compact_attempts.len(),
4,
"expected pre-turn compaction to retry while trimming seeded history only"
);
for (index, request) in compact_attempts.iter().enumerate() {
let body = request.body_json().to_string();
assert!(
body_contains_text(&body, SUMMARIZATION_PROMPT),
"request {index} should be a compaction attempt"
);
assert!(
request
.message_input_texts("user")
.iter()
.any(|text| text == incoming_text),
"request {index} dropped incoming user text during trim retries"
);
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn snapshot_request_shape_manual_compact_without_previous_user_messages() {
skip_if_no_network!();
+1 -1
View File
@@ -139,7 +139,7 @@ async fn settings_only_empty_turn_persists_updates_for_next_non_empty_turn() ->
final_output_json_schema: None,
cwd: test.cwd_path().to_path_buf(),
approval_policy: AskForApproval::Never,
sandbox_policy: SandboxPolicy::ReadOnly,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
model: model.clone(),
effort: test.config.model_reasoning_effort,
summary: ReasoningSummary::Auto,
@@ -1,13 +1,14 @@
---
source: core/tests/suite/compact.rs
expression: "format_labeled_requests_snapshot(\"Pre-turn auto-compaction context-window failure: compaction request excludes the incoming user message and the turn errors.\",\n&[(\"Local Compaction Request (Incoming User Excluded)\", &requests[1]),])"
expression: "format_labeled_requests_snapshot(\"Pre-turn auto-compaction context-window failure: compaction request includes the incoming user message and the turn errors.\",\n&[(\"Local Compaction Request (Incoming User Included)\", &requests[1]),])"
---
Scenario: Pre-turn auto-compaction context-window failure: compaction request excludes the incoming user message and the turn errors.
Scenario: Pre-turn auto-compaction context-window failure: compaction request includes the incoming user message and the turn errors.
## Local Compaction Request (Incoming User Excluded)
## Local Compaction Request (Incoming User Included)
00:message/developer:<PERMISSIONS_INSTRUCTIONS>
01:message/user:<AGENTS_MD>
02:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
03:message/user:USER_ONE
04:message/assistant:FIRST_REPLY
05:message/user:<SUMMARIZATION_PROMPT>
05:message/user:USER_TWO
06:message/user:<SUMMARIZATION_PROMPT>