Evolución de la compaction de contexto
codex trata el conversation history (historial de conversación) como un recurso limitado. En cuanto los tokens acumulados se acercan al context window del modelo, hay que plegar el historial viejo en un resumen para dejar hueco y que la siguiente ronda siga corriendo. Esta ruta de compaction se ha reescrito varias veces en core/src/compact*.rs: al principio local, generando el resumen con el propio modelo; luego se añadió una server-side remote compaction; y más tarde evolucionó a v2. Las tres implementaciones comparten el mismo ciclo de hook y analytics, pero el «resumen» que obtienen viene de fuentes muy distintas.
Responsabilidades
- Decidir cuándo disparar la compaction: tres fuentes — auto (al cruzar el umbral de tokens), manual (el usuario escribe
/compact), oCompactionReason(model downshift, cambio de comp hash, etc.) — dispatchadas porrun_inline_auto_compact_taskyrun_compact_task. - Correr el turn de resumen: la versión local hace otra llamada al modelo vía Responses API para obtener el resultado de
SUMMARIZATION_PROMPT; la remote pide al server que devuelva directamente unResponseItem::Compaction; la v2 exige además que el server devuelva estrictamente un único compaction item. - Construir el historial de reemplazo: une el resumen + las últimas user messages como nuevo historial, y descarta el viejo entero;
build_compacted_history_with_limitrecorta las user messages bajo el límite de 20k tokens. - Mantener el context window: cada compaction abre una ventana nueva con
advance_auto_compact_window/start_new_context_window, actualizandowindow_idyprevious_window_idpara el conteo de tokens posterior y el rollout trace. - Exponer hook y analytics:
PreCompactHookOutcome/PostCompactHookOutcomepermite a los plugins detener la compaction;CompactionAnalyticsAttemptreporta trigger, reason, implementation y phase de cada intento.
Motivación de diseño
La compaction local original (compact.rs) era «el cliente envía otra petición al modelo para que haga el resumen». El problema es que el propio resumen consume tokens, y la calidad depende de cómo el modelo ejecute SUMMARIZATION_PROMPT. Cuando los model providers empezaron a soportar server-side compaction de forma nativa, surgió compact_remote.rs: el server devuelve directamente un item Compaction plegado dentro de una única petición Responses, sin que el cliente participe en generar el resumen.
Pero v1 remote envía el historial entero al server, este devuelve solo el resumen, y el cliente todavía recorta por su cuenta la salida de function call. v2 (compact_remote_v2.rs) endurece ese flujo: el server no solo devuelve el resumen, sino que decide qué mensajes conservar (retained messages); el cliente solo recolecta el único Compaction de salida, y ante error hace fatal. v2 además introduce el límite RETAINED_MESSAGE_TOKEN_BUDGET = 64_000 para que los retained messages no revienten el contexto por sí mismos.
compact_token_budget.rs es otra ramificación: salta la generación de resumen y abre directamente una ventana nueva. Esta ruta queda para los escenarios que «no quieren gastar otra llamada de modelo en hacer resumen» — por ejemplo, cuando el usuario dice explícitamente «vacía el contexto». Aun así pasa por el ciclo completo de hook + ContextCompaction turn item, solo que no llama al modelo.
Archivos clave
codex-rs/core/src/compact.rs:150-219 — run_compact_task_inner, entrada principal de la compaction local, con pre/post hook y envoltorio de analytics.codex-rs/core/src/compact.rs:221-378 — run_compact_task_inner_impl: hace stream al modelo, reintenta y al final llama a replace_compacted_history.codex-rs/core/src/compact.rs:602-663 — build_compacted_history_with_limit, va metiendo user messages desde el final hacia atrás hasta el límite de 20k tokens.codex-rs/core/src/compact_remote.rs:76-107 — run_remote_compact_task, entrada de remote v1, marcada como ResponsesCompact.codex-rs/core/src/compact_remote_v2.rs:85-115 — run_remote_compact_task v2, marcada como ResponsesCompactionV2.codex-rs/core/src/compact_remote_v2.rs:385-443 — collect_compaction_output: imponiendo que el server solo pueda devolver un compaction item; en caso contrario, fatal.codex-rs/core/src/compact_remote_v2_attempt.rs:32-142 — run_remote_compact_v2_attempt: primero trim_function_call_history_to_fit_context_window y luego envía la petición.codex-rs/core/src/compact_model_fallback.rs:8-19 — should_retry_with_current_model, define qué errores merecen reintentar cambiando de modelo.codex-rs/core/src/compact_token_budget.rs:64-90 — compaction por token-budget, salta el resumen y abre ventana nueva directamente.InitialContextInjection es un enum que parece trivial pero es crítico: determina si el nuevo historial tras la compaction debe insertar el initial context. La compaction pre-turn / manual usa DoNotInject, para que el siguiente turn normal lo reinyecte por su cuenta; la compaction mid-turn tiene que usar BeforeLastUserMessage, porque el modelo está entrenado para esperar que «tras el resumen va la última user message», así que hay que intercalar el contexto justo antes de la última user message real.
// compact.rs:56-69 — estrategia de inyección de contexto para mid-turn vs pre-turn
#[derive(Debug)]
pub(crate) enum InitialContextInjection {
BeforeLastUserMessage(Arc<WorldState>),
DoNotInject,
}El núcleo de la compaction local envía al modelo el historial entero + SUMMARIZATION_PROMPT, y al terminar el stream toma el último mensaje assistant como summary. Ojo: no usa directamente la salida del modelo como nuevo historial, sino que la envuelve con un prefijo format!("{SUMMARY_PREFIX}\n{summary_suffix}"), para que luego is_summary_message lo identifique.
// compact.rs:323-336 — saca el resumen del stream del modelo y lo etiqueta
let history_snapshot = sess.clone_history().await;
let history_items = history_snapshot.raw_items();
let summary_suffix = get_last_assistant_message_from_turn(history_items).unwrap_or_default();
let summary_text = format!("{SUMMARY_PREFIX}\n{summary_suffix}");
let user_messages = collect_user_messages(history_items);
let mut new_history = build_compacted_history(Vec::new(), &user_messages, &summary_text);La restricción clave de v2 remote es que el server debe devolver exactamente uno de ResponseItem::Compaction; en caso contrario, fatal directo:
// compact_remote_v2.rs:419-434 — el server devuelve varios compaction → error de protocolo
if !saw_completed {
return Err(CodexErr::Stream(
"remote compaction v2 stream closed before response.completed".to_string(),
None,
));
}
if compaction_count != 1 {
return Err(CodexErr::Fatal(format!(
"remote compaction v2 expected exactly one compaction output item, got {compaction_count} from {output_item_count} output items"
)));
}Flujo de datos
Bordes y fallos
- Autocuración de ContextWindowExceeded: si al correr la compaction local su propio prompt revienta el window, se mete en un bucle
history.remove_first_item()que va recortando la entrada más vieja y reintentando, hasta que si con una sola entrada sigue reventando, suelta error y sale (codex-rs/core/src/compact.rs:285-300). - Límite de reintentos de stream más estricto: v2 remote clampa los reintentos a
MAX_REMOTE_COMPACTION_V2_STREAM_RETRIES = 2, porque la compaction ya tarda bastante; heredar el presupuesto de reintentos del stream general lastraría una compaction durante demasiado tiempo (codex-rs/core/src/compact_remote_v2.rs:54-57). - Fallback de modelo: errores como
InvalidRequest,ContextWindowExceeded,ServerOverloadeddisparanshould_retry_with_current_model, que reintenta cambiando al modelo principal actual, para que una especificidad del provider no atasque la compaction (codex-rs/core/src/compact_model_fallback.rs:8-19). - Posición de inyección en mid-turn: en el nuevo historial tras el resumen, el initial context no se puede simplemente hacer push al final; hay que hacer splice antes de la última real user message; si no hay real user message, se inserta antes del summary, para que el summary sea siempre lo último (
codex-rs/core/src/compact.rs:542-587). - Compaction por token-budget sin resumen: va por
start_new_context_windowy cambia de ventana directamente, sin llamar al modelo, pero aun así emite el turn itemContextCompaction, para que hook y UI no se pierdan eventos por ir por una ruta distinta (codex-rs/core/src/compact_token_budget.rs:76-82).
Resumen
La complejidad de esta parte del código viene sobre todo de que coexisten tres implementaciones: stream local, remote v1 y remote v2, cada una con su propio entry y su inner_impl, pero compartiendo el esqueleto de CompactionAnalyticsAttempt, hook e InitialContextInjection. Al escribir funcionalidad nueva, confirma primero por qué ruta va el provider — should_use_remote_compact_task es el punto de bifurcación — y luego mira el archivo correspondiente. Para ver cómo se envía la propia llamada al modelo en la siguiente ronda, sigue por Cliente LLM y Responses API; para ver cómo se almacena el resultado de la compaction en el historial, ver Hilo de sesión e historial de mensajes.