Reanudar una session comprimida lo cuenta todo dos veces: dentro del summary carrier

Comprimes una conversación larga, sales y vuelves por la mañana con --resume. La primera mitad tiene buen aspecto. En algún punto cerca de la compaction, una respuesta del modelo empieza a repetirse: no una frase, el pasaje entero, palabra por palabra, dos veces. El scrollback dobla su longitud y empiezas a preguntarte si el contexto se ha ido al garete y el mensaje está realmente guardado dos veces. Luego cuentas filas en state.db y hay menos de las que esperabas.
Ese duplicado se arregló el 20 de octubre de 2026 (PR #142187). Merece un post propio porque deja a la vista una estructura que normalmente no ves nunca: el summary carrier.
La compaction envuelve esa respuesta una vez
La compaction in-place no borra los mensajes antiguos. Conserva una cola protegida — el turno más reciente, intacto — y la empareja con un resumen de compaction dentro de una fila nueva. Esa fila nueva es el carrier. Mantiene el message_uid del original pero no su contenido: su texto es “el turno anterior, y luego el resumen”.
El diseño es sólido. El uid mantiene estable “qué mensaje es este” a través de la compaction, y rewind, export y dedupe se apoyan en él. El problema estaba en la capa de display.
Por qué se renderizaba dos veces
Desde el #140971, reanudar una conversación comprimida muestra todo el historial — justo lo que pedían los usuarios, en lugar de solo el resumen. Así que la lectura de display devuelve dos conjuntos de filas: los originales archivados de antes de la compaction y el carrier nuevo. El carrier comparte el message_uid pero envuelve el resumen en su cuerpo, y la capa de display no sabía distinguir que era un contenedor del mismo pasaje. Renderizaba las dos filas, así que el texto original aparecía una vez y la copia dentro del carrier aparecía otra vez.
El test end-to-end oficial de la TUI le puso número: el E2E de resume/compress reportaba 200 words rendered more than once en /resume, --resume y /compress. Ese tipo de aserción vale más que una captura de pantalla: afirma sin rodeos que 200 palabras se dibujaron más de una vez.
El arreglo: el carrier toma prestada la identidad de display del original
El patch no toca el almacenamiento. Unifica cómo se calcula la identidad de display: _dedupe_display_generations en hermes_state_messages.py gana una regla — un summary carrier comparte la identidad de display de la fila anterior con su mismo message_uid, y gana la copia más nueva, así que la copia vieja deja de renderizarse por separado.
El docstring de la propia función es explícito: colapsa las generaciones de compaction para que cada mensaje lógico aparezca una sola vez (la cola protegida se copia en cada generación: mismo rol, contenido y timestamp, distinto flag active e id); prefiere la copia viva y luego la más nueva. Y es una definición compartida por todas las proyecciones de display, no una implementación distinta por superficie.
El arreglo es deliberadamente estrecho: solo el carrier. Un stub de herramienta pruned mantiene visible su salida completa archivada, y test_display_dedupe_pruned_tool_payload lo fija — una coincidencia de uid anterior y sin acotar lo rompía.
Un commit compañero (c8aa61b7883) aplicó la misma regla a las exportaciones y a la lista de transcripts, así que hermes sessions export tampoco emite ya dos veces una respuesta transportada.
¿Se perdió algún dato?
No. Todo el bug era de display. La validación usó el state.db del propio E2E que fallaba: antes del arreglo, la lectura de display devolvía 15 filas con repeticiones; después, 14 filas sin ninguna. Menos filas es la prueba de que la copia extra era un artefacto de renderizado, no datos guardados.
Una autocomprobación útil: cuando sospeches duplicación, compara el número de filas de la base de datos con el número de mensajes que muestra la interfaz. Si la base está normal pero la vista dobla, es renderizado. Si doblan las dos, algo escribió de verdad dos veces. La batería de tests más amplia detrás del patch (tests/hermes_state, los RPC de resume y recovery del gateway, tests/tui_gateway) reportó 4328 pasados, 0 fallos.
Queda un problema vecino
Un issue de la misma zona sigue abierto: cuando la compaction fusiona un mensaje de usuario con un aviso anterior de “cambio de modelo”, ese mensaje de usuario desaparece del transcript. No es el mismo bug — uno muestra una copia de más, el otro muestra un mensaje de menos — y este arreglo no lo absorbió. Si un mensaje de usuario se esfuma después de la compaction, no apliques la conclusión de arriba.
Para entender bien la compaction y los presupuestos de contexto, lee cómo la compaction se completa en una sola llamada y la reconstrucción del prompt de compaction. Los comandos de historial, export y resume de session están en la referencia de hermes sessions, y si tu objetivo es mantener sessions largas dentro de un presupuesto, el artículo que te interesa es el de optimización de tokens de contexto.
¿Puedes usarlo hoy?
Este arreglo es uno de los que desbloquean la v0.22.3, así que vive solo en main y no está en ninguna release publicada (la última es la v0.22.2). Cuando estés en una build que lo contenga, /resume, --resume y /compress deberían mostrar una respuesta transportada una sola vez; si sigue repitiéndose, confirma primero la versión y luego mira la capa de display.
La versión corta
El summary carrier es el contenedor que deja la compaction: hereda el uid del original, pero su cuerpo es “turno anterior más resumen”. Una capa de display que identifica los mensajes solo por uid, sin reconocer los carriers, dibuja el mismo pasaje dos veces. El arreglo le presta al carrier la identidad de display del original y acota la regla solo a los carriers, así que la salida pruned de herramientas sigue mostrando su contenido archivado. Guarda la regla de diagnóstico: vista doblada y base de datos normal, problema de display; las dos dobladas, problema de datos.