fix(compact): trim history on context overflow and retry

This commit is contained in:
James Peter
2025-11-24 21:24:46 +10:00
parent 019adc32c0
commit 2014c10d6f
2 changed files with 168 additions and 41 deletions
+111 -32
View File
@@ -221,33 +221,62 @@ pub(super) async fn perform_compaction(
) -> CodexResult<()> {
// Convert core InputItem -> ResponseInputItem using the same logic as the main turn flow
let initial_input_for_turn: ResponseInputItem = response_input_from_core_items(input);
let turn_input = sess.turn_input_with_history(vec![initial_input_for_turn.clone().into()]);
let mut turn_input = sess.turn_input_with_history(vec![initial_input_for_turn.clone().into()]);
let turn_input = sanitize_items_for_compact(turn_input);
let mut prompt = Prompt::default();
prompt.input = turn_input;
prompt.store = !sess.disable_response_storage;
prompt.user_instructions = turn_context.user_instructions.clone();
prompt.environment_context = Some(EnvironmentContext::new(
Some(turn_context.cwd.clone()),
Some(turn_context.approval_policy),
Some(turn_context.sandbox_policy.clone()),
Some(sess.user_shell.clone()),
));
prompt.model_descriptions = sess.model_descriptions.clone();
prompt.log_tag = Some("codex/compact".to_string());
turn_input = sanitize_items_for_compact(turn_input);
let max_retries = turn_context.client.get_provider().stream_max_retries();
let mut retries = 0;
let mut truncated_count = 0usize;
// Do not persist a TurnContext rollout item here; inline compaction is a
// background maintenance task and should not affect rollout reconstruction.
loop {
let mut prompt = Prompt::default();
prompt.input = turn_input.clone();
prompt.store = !sess.disable_response_storage;
prompt.user_instructions = turn_context.user_instructions.clone();
prompt.environment_context = Some(EnvironmentContext::new(
Some(turn_context.cwd.clone()),
Some(turn_context.approval_policy),
Some(turn_context.sandbox_policy.clone()),
Some(sess.user_shell.clone()),
));
prompt.model_descriptions = sess.model_descriptions.clone();
prompt.log_tag = Some("codex/compact".to_string());
match drain_to_completed(&sess, turn_context.as_ref(), &prompt).await {
Ok(()) => break,
Ok(()) => {
if truncated_count > 0 {
tracing::warn!(
"Context window exceeded during compact; trimmed {truncated_count} item(s) from prompt"
);
}
break;
}
Err(CodexErr::Interrupted) => return Err(CodexErr::Interrupted),
Err(e) if is_context_overflow_error(&e) => {
if turn_input.len() > 1 {
tracing::warn!(
"Context window exceeded while compacting; dropping oldest item ({} remaining)",
turn_input.len().saturating_sub(1)
);
turn_input.remove(0);
truncated_count = truncated_count.saturating_add(1);
retries = 0;
continue;
}
let event = sess.make_event(
&sub_id,
EventMsg::Error(ErrorEvent {
message: e.to_string(),
}),
);
sess.send_event(event).await;
return Err(e);
}
Err(e) => {
if retries < max_retries {
retries += 1;
@@ -325,29 +354,58 @@ async fn run_compact_task_inner_inline(
) -> Vec<ResponseItem> {
// Convert core InputItem -> ResponseInputItem and build prompt
let initial_input_for_turn: ResponseInputItem = response_input_from_core_items(input);
let turn_input = sess.turn_input_with_history(vec![initial_input_for_turn.clone().into()]);
let mut turn_input = sess.turn_input_with_history(vec![initial_input_for_turn.clone().into()]);
let turn_input = sanitize_items_for_compact(turn_input);
let mut prompt = Prompt::default();
prompt.input = turn_input;
prompt.store = !sess.disable_response_storage;
prompt.user_instructions = turn_context.user_instructions.clone();
prompt.environment_context = Some(EnvironmentContext::new(
Some(turn_context.cwd.clone()),
Some(turn_context.approval_policy),
Some(turn_context.sandbox_policy.clone()),
Some(sess.user_shell.clone()),
));
prompt.model_descriptions = sess.model_descriptions.clone();
prompt.log_tag = Some("codex/compact".to_string());
turn_input = sanitize_items_for_compact(turn_input);
let max_retries = turn_context.client.get_provider().stream_max_retries();
let mut retries = 0;
let mut truncated_count = 0usize;
loop {
let mut prompt = Prompt::default();
prompt.input = turn_input.clone();
prompt.store = !sess.disable_response_storage;
prompt.user_instructions = turn_context.user_instructions.clone();
prompt.environment_context = Some(EnvironmentContext::new(
Some(turn_context.cwd.clone()),
Some(turn_context.approval_policy),
Some(turn_context.sandbox_policy.clone()),
Some(sess.user_shell.clone()),
));
prompt.model_descriptions = sess.model_descriptions.clone();
prompt.log_tag = Some("codex/compact".to_string());
match drain_to_completed(&sess, turn_context.as_ref(), &prompt).await {
Ok(()) => break,
Ok(()) => {
if truncated_count > 0 {
tracing::warn!(
"Context window exceeded during inline compact; trimmed {truncated_count} item(s) from prompt"
);
}
break;
}
Err(CodexErr::Interrupted) => return Vec::new(),
Err(e) if is_context_overflow_error(&e) => {
if turn_input.len() > 1 {
tracing::warn!(
"Context window exceeded while compacting; dropping oldest item ({} remaining)",
turn_input.len().saturating_sub(1)
);
turn_input.remove(0);
truncated_count = truncated_count.saturating_add(1);
retries = 0;
continue;
}
let event = sess.make_event(
&sub_id,
EventMsg::Error(ErrorEvent {
message: e.to_string(),
}),
);
sess.send_event(event).await;
return Vec::new();
}
Err(e) => {
if retries < max_retries {
retries += 1;
@@ -441,6 +499,27 @@ fn truncate_for_compact(text: String, max_bytes: usize) -> String {
truncate_middle(&text, max_bytes).0
}
fn looks_like_context_overflow(message: &str) -> bool {
let lower = message.to_ascii_lowercase();
lower.contains("context_length_exceeded")
|| lower.contains("context length exceeded")
|| lower.contains("context window")
&& (lower.contains("exceed")
|| lower.contains("exceeded")
|| lower.contains("full")
|| lower.contains("too long"))
|| lower.contains("maximum context length")
|| lower.contains("exceeds the context window")
}
pub(super) fn is_context_overflow_error(err: &CodexErr) -> bool {
match err {
CodexErr::UnexpectedStatus(resp) => looks_like_context_overflow(&resp.body),
CodexErr::Stream(msg, _) => looks_like_context_overflow(msg),
_ => false,
}
}
pub fn sanitize_items_for_compact(items: Vec<ResponseItem>) -> Vec<ResponseItem> {
items
.into_iter()
+57 -9
View File
@@ -1,6 +1,7 @@
use std::sync::Arc;
use super::compact::{
is_context_overflow_error,
response_input_from_core_items,
sanitize_items_for_compact,
send_compaction_checkpoint_warning,
@@ -17,6 +18,7 @@ use code_protocol::models::ResponseInputItem;
use code_protocol::models::ResponseItem;
use code_protocol::protocol::CompactedItem;
use code_protocol::protocol::RolloutItem;
use crate::util::backoff;
pub(super) async fn run_inline_remote_auto_compact_task(
sess: Arc<Session>,
@@ -81,17 +83,63 @@ async fn run_remote_compact_task_inner(
});
turn_items = sanitize_items_for_compact(turn_items);
let mut truncated_count = 0usize;
let max_retries = turn_context.client.get_provider().stream_max_retries();
let mut retries = 0;
let new_history = loop {
let mut prompt = Prompt::default();
prompt.input = turn_items.clone();
prompt.base_instructions_override = turn_context.base_instructions.clone();
prompt.include_additional_instructions = false;
prompt.log_tag = Some("codex/remote-compact".to_string());
let mut prompt = Prompt::default();
prompt.input = turn_items;
prompt.base_instructions_override = turn_context.base_instructions.clone();
prompt.include_additional_instructions = false;
prompt.log_tag = Some("codex/remote-compact".to_string());
match turn_context
.client
.compact_conversation_history(&prompt)
.await
{
Ok(history) => {
if truncated_count > 0 {
tracing::warn!(
"Context window exceeded during remote compact; trimmed {truncated_count} item(s) from prompt"
);
}
break history;
}
Err(err) if is_context_overflow_error(&err) => {
if turn_items.len() > 1 {
tracing::warn!(
"Context window exceeded while remote compacting; dropping oldest item ({} remaining)",
turn_items.len().saturating_sub(1)
);
turn_items.remove(0);
truncated_count = truncated_count.saturating_add(1);
retries = 0;
continue;
}
let new_history = turn_context
.client
.compact_conversation_history(&prompt)
.await?;
return Err(err);
}
Err(err) => {
if retries < max_retries {
retries += 1;
let delay = backoff(retries);
sess
.notify_stream_error(
sub_id,
format!(
"remote compact error: {err}; retrying {retries}/{max_retries} in {delay:?}…"
),
)
.await;
tokio::time::sleep(delay).await;
continue;
}
return Err(err);
}
}
};
sess.replace_history(new_history.clone());
{