La compaction se comió el resultado de tool que tu modelo estaba esperando


Le pides al agente que ejecute la suite de integración. Lo hace: miles de líneas de salida pasan por delante. Después le preguntas qué tests han fallado, y te contesta que solo vio un placeholder de una línea, y que las capturas que tomó por el camino también han desaparecido. Nada falló de forma ruidosa. El comando se ejecutó de verdad; el resultado llegó de verdad a la conversación. Simplemente no sobrevivió al trayecto de «la tool ha terminado» a «el modelo lo ha leído». Tres correcciones fusionadas el 2026-09-26 (PRs #124208, #124207, #124231) cierran esa brecha, y esto es lo que protegen — y lo que deliberadamente siguen sin proteger.

Qué es realmente la «compaction a mitad de turno»

Una sesión larga acaba llenando su context window. Hermes lo gestiona con compaction: la historia más antigua se sustituye por un resumen para que la conversación pueda seguir avanzando. La mayoría de las veces la compaction ocurre entre turnos, y no te enteras de nada.

El momento peligroso es otro: el paso de preparación antes de que salga una petición (el código lo llama preflight). Justo ahí, el contexto contiene un resultado de tool que acaba de llegar y que todavía no se ha leído. El comando ya se ha ejecutado, la salida está en el transcript y la siguiente petición del modelo aún no se ha enviado.

La implementación antigua trataba esa ronda como cualquier otro fragmento de historia: el pressure pass degradaba «todo menos lo más reciente», la lógica de retirada de imágenes ignoraba por completo la ronda pendiente y el recorte de cola bajaba hasta un suelo de cero. Una compaction que se disparara en esa ventana podía reducir el resultado a un stub de una línea, o retirar sus imágenes. El modelo se despertaba sin nada que leer.

Corrección uno: la ronda que estás esperando se conserva tal cual (PR #124208)

La corrección gira en torno a un nuevo helper, _pending_tool_round, que localiza la racha final de resultados de tool — y se salta las filas contiguas de /steer que pueden quedar entre un resultado y la lectura del modelo cuando mandas una corrección de rumbo a mitad de ejecución. Una vez localizada esa ronda, queda exenta en cuatro sitios:

Etapa Qué pasaba antes Qué pasa ahora
Pass 2 (pressure pass) La ronda se degradaba a un stub o a un resumen Se conserva tal cual
Retirada de imágenes (ambos escaneos) Solo sobrevivía la imagen más reciente Sobreviven todas las imágenes de una ronda con varias
Recorte de cola Suelo de 0, así que podía recortarse La ronda queda fuera de alcance
Pass 3 (truncado de argumentos) Se podía cortar la fila del assistant que hizo la llamada La fila tool_calls propietaria también se protege

Hay una condición límite que conviene conocer: una ronda que ocupe más del 20% de la ventana de entrada efectiva no se conserva. Es deliberado — un resultado sobredimensionado sigue usando la válvula de escape original del desbordamiento, porque de lo contrario la compaction fallaría justo cuando más falta hace.

Corrección dos: toda elisión visible para el modelo pasa por el marker con guarda (PR #124207)

Antes el truncado se anunciaba sin más: ...[truncated]. El problema es que Hermes mantiene una guarda en la frontera de tool dispatch (_COMPRESSION_MARKER_RE en agent/tool_dispatch_helpers.py) cuya única misión es pillar a un modelo que copia un marker de elisión dentro de una escritura de archivo. Solo reconoce el marker especial. No ve ...[truncated].

La consecuencia no es una línea de log cosmética; es la clase de pérdida de datos. Un modelo que copia texto que nunca llegó a ver del todo, marker incluido, dentro de write_file o patch pasa sin que nadie lo pare, y el archivo se escribe mal.

Ahora todo truncado visible para el modelo pasa por el marker no imitable ⟪HERMES-CONTEXT-COMPRESSION: …⟫: el propio compressor, la salida de inline-shell de las skills, el reporter del LSP, el stop de verificación, el contexto de memoria, y el serializador de Slack y el compressor de trayectorias de cara al agente. Quedan unos cuantos sitios abiertos bajo tools/* — el PR lo dice en lugar de fingir lo contrario.

Corrección tres: el «unchanged» que no lo era (PR #124231)

read_file y skill_view mantienen ambos una dedup cache: si lees el mismo archivo por segunda vez recibes un «unchanged» barato en lugar del cuerpo. Es un buen intercambio — hasta que la cache y el transcript se contradicen.

Hay tres rutas de reescritura que descartan cuerpos de resultados de tool sin resetearla:

  • micro-compaction posterior al turno;
  • un checkpoint replayable nativo de Responses;
  • la compaction del app-server de Codex.

Entonces el modelo no tiene ni el cuerpo (descartado) ni el contenido (el «unchanged» implica que aún lo tiene). Las tres resetean ahora las caches; la micro-compaction solo resetea cuando realmente ha reescrito el transcript, de modo que una pasada no-op no tira una cache que sigue siendo válida.

Qué deberías hacer

  • Los tres PR se fusionaron el 2026-09-26, después del tag de release v0.21.5, así que viven en main. hermes update te los trae; un despliegue anclado a tags espera al siguiente.
  • No hay cambios de configuración. Aquí no hay ningún switch nuevo, solo comportamiento corregido.
  • En una sesión larga de trabajo por lotes, el síntoma revelador era que el modelo afirmaba no haber visto nunca una salida que el transcript demuestra que recibió. Esa contradicción debería dejar de ocurrir.

El principio que hay debajo

La compaction a mitad de turno pasó de «degradar todo menos lo más reciente» a «no tocar nunca la ronda que el modelo no ha leído». El marker de elisión dejó de ser una cadena por sitio y se convirtió en un único token con guarda. La cache de lectura se realineó con la realidad. Los tres apuntan a la misma regla: la compaction puede sacrificar información antigua, nunca información que está en uso. Para el contexto que lo rodea, empieza por una petición de compaction en lugar de diecinueve para la parte de velocidad, qué le hacen cuarenta rondas de compaction a una sesión para las cifras medidas, y el trade-off de la lean tail para lo que se sacrifica a propósito. El resto de esta ventana está en la página del release v0.21.5.