use std::sync::Arc;
use std::sync::OnceLock;
use crate::compact::CompactedHistoryMetadata;
use crate::compact::CompactionAnalyticsAttempt;
use crate::compact::CompactionAnalyticsDetails;
use crate::compact::InitialContextInjection;
use crate::compact::build_compaction_initial_context;
use crate::compact::compaction_status_from_result;
use crate::compact::insert_initial_context_before_last_real_user_or_summary;
use crate::compact_model_fallback::record_model_fallback;
use crate::compact_model_fallback::should_retry_with_current_model;
use crate::context::world_state::WorldState;
use crate::context_manager::ContextManager;
use crate::context_manager::estimate_item_token_count;
use crate::hook_runtime::PostCompactHookOutcome;
use crate::hook_runtime::PreCompactHookOutcome;
use crate::hook_runtime::run_post_compact_hooks;
use crate::hook_runtime::run_pre_compact_hooks;
use crate::responses_metadata::CompactionTurnMetadata;
use crate::session::session::Session;
use crate::session::step_context::StepContext;
use crate::session::turn_context::TurnContext;
use codex_analytics::CompactionImplementation;
use codex_analytics::CompactionPhase;
use codex_analytics::CompactionReason;
use codex_analytics::CompactionTrigger;
use codex_protocol::error::CodexErr;
use codex_protocol::error::Result as CodexResult;
use codex_protocol::items::ContextCompactionItem;
use codex_protocol::items::TurnItem;
use codex_protocol::models::BaseInstructions;
use codex_protocol::models::FunctionCallOutputBody;
use codex_protocol::models::FunctionCallOutputPayload;
use codex_protocol::models::ResponseItem;
use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::TurnStartedEvent;
use codex_rollout_trace::CompactionCheckpointTracePayload;
use codex_utils_output_truncation::approx_token_count;
#[path = "compact_remote_request.rs"]
mod request;
use request::RemoteCompactAttempt;
use request::run_remote_compact_attempt;
const CONTEXT_WINDOW_TRUNCATED_OUTPUT_MESSAGE: &str =
"Output exceeded the available model context and was truncated";
pub(crate) async fn run_inline_remote_auto_compact_task(
sess: Arc<Session>,
step_context: Arc<StepContext>,
fallback_step_context: Option<Arc<StepContext>>,
turn_state: Arc<OnceLock<String>>,
initial_context_injection: InitialContextInjection,
reason: CompactionReason,
phase: CompactionPhase,
) -> CodexResult<()> {
let compaction_metadata = CompactionTurnMetadata::new(
CompactionTrigger::Auto,
reason,
CompactionImplementation::ResponsesCompact,
phase,
);
run_remote_compact_task_inner(
&sess,
&step_context,
fallback_step_context.as_ref(),
Some(turn_state),
initial_context_injection,
compaction_metadata,
)
.await?;
Ok(())
}
pub(crate) async fn run_remote_compact_task(
sess: Arc<Session>,
turn_context: Arc<TurnContext>,
) -> CodexResult<()> {
let step_context = sess.capture_step_context(Arc::clone(&turn_context)).await;
let start_event = EventMsg::TurnStarted(TurnStartedEvent {
turn_id: turn_context.sub_id.clone(),
trace_id: turn_context.trace_id.clone(),
started_at: turn_context.turn_timing_state.started_at_unix_secs().await,
model_context_window: turn_context.model_context_window(),
collaboration_mode_kind: turn_context.mode,
});
sess.send_event(&turn_context, start_event).await;
let compaction_metadata = CompactionTurnMetadata::new(
CompactionTrigger::Manual,
CompactionReason::UserRequested,
CompactionImplementation::ResponsesCompact,
CompactionPhase::StandaloneTurn,
);
run_remote_compact_task_inner(
&sess,
&step_context,
None,
None,
InitialContextInjection::DoNotInject,
compaction_metadata,
)
.await?;
Ok(())
}
async fn run_remote_compact_task_inner(
sess: &Arc<Session>,
step_context: &Arc<StepContext>,
fallback_step_context: Option<&Arc<StepContext>>,
turn_state: Option<Arc<OnceLock<String>>>,
initial_context_injection: InitialContextInjection,
compaction_metadata: CompactionTurnMetadata,
) -> CodexResult<()> {
let turn_context = &step_context.turn;
let trigger = compaction_metadata.trigger();
let reason = compaction_metadata.reason();
let implementation = compaction_metadata.implementation();
let phase = compaction_metadata.phase();
let mut analytics_details = CompactionAnalyticsDetails {
active_context_tokens_before: Some(sess.get_total_token_usage().await),
..Default::default()
};
let attempt = CompactionAnalyticsAttempt::begin(
sess.as_ref(),
turn_context.as_ref(),
trigger,
reason,
implementation,
phase,
)
.await;
let pre_compact_outcome = run_pre_compact_hooks(sess, turn_context, trigger).await;
match pre_compact_outcome {
PreCompactHookOutcome::Continue => {}
PreCompactHookOutcome::Stopped => {
let error = CodexErr::TurnAborted;
attempt
.track(
sess.as_ref(),
codex_analytics::CompactionStatus::Interrupted,
Some(&error),
analytics_details,
)
.await;
return Err(error);
}
}
let result = run_remote_compact_task_inner_impl(
sess,
step_context,
fallback_step_context,
turn_state,
initial_context_injection,
compaction_metadata,
&mut analytics_details,
)
.await;
let status = compaction_status_from_result(&result);
let codex_error = result.as_ref().err();
if result.is_ok() {
let post_compact_outcome = run_post_compact_hooks(sess, turn_context, trigger).await;
if let PostCompactHookOutcome::Stopped = post_compact_outcome {
attempt
.track(sess.as_ref(), status, codex_error, analytics_details)
.await;
return Err(CodexErr::TurnAborted);
}
}
attempt
.track(sess.as_ref(), status, codex_error, analytics_details)
.await;
if let Err(err) = result {
sess.track_turn_codex_error(turn_context, &err);
let event = EventMsg::Error(
err.to_error_event(Some("Error running remote compact task".to_string())),
);
sess.send_event(turn_context, event).await;
return Err(err);
}
Ok(())
}
async fn run_remote_compact_task_inner_impl(
sess: &Arc<Session>,
step_context: &Arc<StepContext>,
fallback_step_context: Option<&Arc<StepContext>>,
turn_state: Option<Arc<OnceLock<String>>>,
initial_context_injection: InitialContextInjection,
compaction_metadata: CompactionTurnMetadata,
analytics_details: &mut CompactionAnalyticsDetails,
) -> CodexResult<()> {
let turn_context = &step_context.turn;
let context_compaction_item = ContextCompactionItem::new();
let compaction_id = context_compaction_item.id.clone();
let compaction_trace = sess.services.rollout_thread_trace.compaction_trace_context(
turn_context.sub_id.as_str(),
compaction_id.as_str(),
turn_context.model_info.slug.as_str(),
turn_context.provider.info().name.as_str(),
);
let compaction_item = TurnItem::ContextCompaction(context_compaction_item);
sess.emit_turn_item_started(turn_context, &compaction_item)
.await;
let attempt = run_remote_compact_attempt(
sess,
step_context,
turn_state.clone(),
&compaction_trace,
compaction_metadata,
analytics_details,
)
.await;
let (attempt, compaction_turn_context) = match attempt {
Ok(attempt) => (attempt, turn_context),
Err(error) => {
let Some(fallback_step_context) = fallback_step_context else {
return Err(error);
};
if !should_retry_with_current_model(&error) {
return Err(error);
}
let fallback_turn_context = &fallback_step_context.turn;
let fallback_compaction_trace =
sess.services.rollout_thread_trace.compaction_trace_context(
fallback_turn_context.sub_id.as_str(),
compaction_id.as_str(),
fallback_turn_context.model_info.slug.as_str(),
fallback_turn_context.provider.info().name.as_str(),
);
let fallback_result = run_remote_compact_attempt(
sess,
fallback_step_context,
turn_state,
&fallback_compaction_trace,
compaction_metadata,
analytics_details,
)
.await;
record_model_fallback(
&sess.services.session_telemetry,
turn_context.model_info.slug.as_str(),
fallback_turn_context.model_info.slug.as_str(),
compaction_metadata.reason(),
compaction_metadata.implementation(),
fallback_result.as_ref().err(),
);
match fallback_result {
Ok(attempt) => (attempt, fallback_turn_context),
Err(_) => return Err(error),
}
}
};
let RemoteCompactAttempt {
new_history,
trace_input_history,
} = attempt;
let (new_window_number, new_window_ids) = sess.advance_auto_compact_window().await;
let (new_history, world_state_baseline) =
process_compacted_history(sess.as_ref(), new_history, &initial_context_injection).await;
let reference_context_item = match initial_context_injection {
InitialContextInjection::DoNotInject => None,
InitialContextInjection::BeforeLastUserMessage { .. } => {
Some(compaction_turn_context.to_turn_context_item())
}
};
if let Some(trace_input_history) = trace_input_history.as_deref() {
compaction_trace.record_installed(&CompactionCheckpointTracePayload {
input_history: trace_input_history,
replacement_history: &new_history,
});
}
sess.replace_compacted_history(
new_history,
reference_context_item,
world_state_baseline,
CompactedHistoryMetadata {
message: String::new(),
window_number: new_window_number,
window_ids: new_window_ids,
},
)
.await;
sess.recompute_token_usage(compaction_turn_context).await;
sess.emit_turn_item_completed(compaction_turn_context, compaction_item)
.await;
Ok(())
}
pub(crate) async fn process_compacted_history(
sess: &Session,
mut compacted_history: Vec<ResponseItem>,
initial_context_injection: &InitialContextInjection,
) -> (Vec<ResponseItem>, Option<Arc<WorldState>>) {
let (initial_context, world_state_baseline) =
build_compaction_initial_context(sess, initial_context_injection).await;
compacted_history.retain(should_keep_compacted_history_item);
(
insert_initial_context_before_last_real_user_or_summary(compacted_history, initial_context),
world_state_baseline,
)
}
pub(crate) fn should_keep_compacted_history_item(item: &ResponseItem) -> bool {
match item {
ResponseItem::Message { role, .. } if role == "developer" => false,
ResponseItem::Message { role, .. } if role == "user" => {
matches!(
crate::event_mapping::parse_turn_item(item),
Some(TurnItem::UserMessage(_) | TurnItem::HookPrompt(_))
)
}
ResponseItem::Message { role, .. } if role == "assistant" => true,
ResponseItem::Message { .. } => false,
ResponseItem::AgentMessage { .. } => true,
ResponseItem::Compaction { .. } | ResponseItem::ContextCompaction { .. } => true,
ResponseItem::CompactionTrigger { .. } => false,
ResponseItem::AdditionalTools { .. }
| ResponseItem::Reasoning { .. }
| ResponseItem::LocalShellCall { .. }
| ResponseItem::FunctionCall { .. }
| ResponseItem::ToolSearchCall { .. }
| ResponseItem::FunctionCallOutput { .. }
| ResponseItem::ToolSearchOutput { .. }
| ResponseItem::CustomToolCall { .. }
| ResponseItem::CustomToolCallOutput { .. }
| ResponseItem::WebSearchCall { .. }
| ResponseItem::ImageGenerationCall { .. }
| ResponseItem::Other => false,
}
}
pub(crate) fn trim_function_call_history_to_fit_context_window(
history: &mut ContextManager,
turn_context: &TurnContext,
base_instructions: &BaseInstructions,
) -> (usize, i64) {
let Some(context_window) = turn_context.model_context_window() else {
return (0, 0);
};
let base_tokens =
i128::try_from(approx_token_count(&base_instructions.text)).unwrap_or(i128::MAX);
let original_items = history.raw_items();
let item_token_estimates = original_items
.iter()
.map(estimate_item_token_count)
.collect::<Vec<_>>();
let mut estimated_tokens = item_token_estimates
.iter()
.copied()
.map(i128::from)
.fold(base_tokens, i128::saturating_add);
let initial_estimated_tokens = i64::try_from(estimated_tokens).unwrap_or(i64::MAX);
let mut rewritten_items = Vec::new();
for (item, item_tokens) in original_items.iter().zip(item_token_estimates).rev() {
if i64::try_from(estimated_tokens).unwrap_or(i64::MAX) <= context_window {
break;
}
let Some(rewritten_item) = rewritten_output_for_context_window(item) else {
break;
};
estimated_tokens = estimated_tokens
.saturating_sub(i128::from(item_tokens))
.saturating_add(i128::from(estimate_item_token_count(&rewritten_item)));
rewritten_items.push(rewritten_item);
}
let rewritten_outputs = rewritten_items.len();
if rewritten_outputs > 0 {
let retained_len = original_items.len() - rewritten_outputs;
let mut items = original_items[..retained_len].to_vec();
items.extend(rewritten_items.into_iter().rev());
history.replace(items);
}
let final_estimated_tokens = i64::try_from(estimated_tokens).unwrap_or(i64::MAX);
let estimated_deleted_tokens = initial_estimated_tokens.saturating_sub(final_estimated_tokens);
(rewritten_outputs, estimated_deleted_tokens)
}
fn rewritten_output_for_context_window(item: &ResponseItem) -> Option<ResponseItem> {
Some(match item {
ResponseItem::FunctionCallOutput {
id,
call_id,
output,
internal_chat_message_metadata_passthrough: metadata,
} => ResponseItem::FunctionCallOutput {
id: id.clone(),
call_id: call_id.clone(),
output: truncated_output_payload(output),
internal_chat_message_metadata_passthrough: metadata.clone(),
},
ResponseItem::CustomToolCallOutput {
id,
call_id,
name,
output,
internal_chat_message_metadata_passthrough: metadata,
} => ResponseItem::CustomToolCallOutput {
id: id.clone(),
call_id: call_id.clone(),
name: name.clone(),
output: truncated_output_payload(output),
internal_chat_message_metadata_passthrough: metadata.clone(),
},
ResponseItem::ToolSearchOutput {
id,
call_id,
status,
execution,
internal_chat_message_metadata_passthrough: metadata,
..
} => ResponseItem::ToolSearchOutput {
id: id.clone(),
call_id: call_id.clone(),
status: status.clone(),
execution: execution.clone(),
tools: Vec::new(),
internal_chat_message_metadata_passthrough: metadata.clone(),
},
_ => return None,
})
}
fn truncated_output_payload(output: &FunctionCallOutputPayload) -> FunctionCallOutputPayload {
FunctionCallOutputPayload {
body: FunctionCallOutputBody::Text(CONTEXT_WINDOW_TRUNCATED_OUTPUT_MESSAGE.to_string()),
success: output.success,
}
}