Por qué tu exportación de /save perdía turnos — y el límite que hay detrás

Tienes una sesión de larga duración, la compactación ha archivado varias rondas de ella y quieres exportarla para conservarla. /save json desde la CLI funciona sin problema. Haces lo mismo en la app de escritorio, importas el resultado más tarde y te das cuenta de que faltan fragmentos: los turnos que la compactación archivó simplemente han desaparecido. Empiezas a preguntarte si lo recordaste mal, o si algún /undo los borró.
Aquí se acumulan dos problemas: el mismo /save pasaba por tres rutas de código distintas (CLI, plataformas de mensajería y el RPC session.save de escritorio/TUI), y las dos reglas que importan —qué exportar y a partir de qué tamaño es demasiado grande— acabaron en desacuerdo entre sí. La corrección converge todo en una sola ruta.
Tres superficies de /save, antes tres respuestas
/save admite json (una instantánea completa), md (una transcripción legible) y html (una página independiente para compartir). El escritorio y el TUI llaman al RPC session.save, mientras que la CLI y las plataformas de mensajería usaban otra implementación distinta. La diferencia se manifestaba en la proyección de la exportación:
- Una transcripción (
md/html) es «lo que vio el usuario», así que debe incluir los turnos que la compactación in situ archivó; - Una instantánea JSON es «lo que restaura una importación», así que lleva todas las filas almacenadas con sus marcas
active/compacted. Exportar solo las filas activas descarta cada tramo archivado de la conversación; exportar las marcas permite que la importación las restaure como archivadas en lugar de reproducirlas como contexto activo.
La ruta RPC no seguía esa regla de forma consistente, así que una misma sesión se exportaba de manera distinta según la superficie que usaras.
La nueva regla: una ruta compartida por las tres
La corrección extrae la lógica ya correcta de la CLI a una función compartida load_save_snapshot(db, session_id, fmt) en hermes_cli/session_export.py, y las tres superficies la llaman. Hace tres cosas:
- Elige la proyección según el formato:
include_compacted=Truepara las transcripciones,include_inactive=Truepara JSON; - Para una instantánea JSON, descarta las filas deshechas: solo sobreviven las filas
activeocompacted, mientras que las filas eliminadas por/undo, rewind o edición se quedan fuera —de forma deliberada, ya que una importación no debería resucitar lo que el usuario deshizo; - Aplica el límite de exportación (más abajo).
Dos detalles menores vinieron de la mano. La instantánea JSON ahora usa la forma canónica de exportación, igual que la CLI, en lugar de las claves heredadas de la app de escritorio —y cuando una sesión no tiene fila almacenada (o la lectura falla), el respaldo en memoria escribe esa misma forma (id, model, started_at, system_prompt, messages). También lee la fila que el agente vivo está escribiendo en ese momento, de modo que una rotación de compresión no exporta un padre obsoleto.
El límite de exportación: sessions.max_export_messages
Materializar en memoria de golpe una sesión de 100 000 filas puede agotar el proceso, así que hay una salvaguarda:
sessions:
max_export_messages: 20000 # el valor por defecto; 0 desactiva la salvaguarda
Se comprueba por sesión, así que una copia de seguridad de toda la base de datos con muchas sesiones pequeñas no se ve afectada: solo se rechaza la sesión sobredimensionada. Tras esta corrección, el /save json del escritorio/TUI también lo respeta, y:
- el rechazo llega como código de error RPC 4131 (no el genérico 5011) con el mismo mensaje que imprime la CLI;
- en las sesiones aisladas por turno (compute-host), el host reenvía el código de error del handler en
control.errory el padre lo transmite, de modo que las sesiones ejecutadas en remoto informan del motivo real; - el mensaje te indica que pongas
sessions.max_export_messages: 0enconfig.yamlpara desactivar la salvaguarda.
El último detalle es fácil de pasar por alto y es importante: el límite se lee de la configuración del perfil al que pertenece la sesión. La app de escritorio ahora lee el state.db propio de ese perfil bajo la configuración de ese perfil, así que el límite de exportación de un perfil secundario es el suyo, no el de tu perfil principal.
Elegir según el escenario
Una vez claras las reglas, elegir el formato es sencillo:
| Tu objetivo | Usa | Por qué |
|---|---|---|
| Archivar / mover a otra máquina | /save json |
Instantánea completa con marcas de archivado; reimportable tal cual |
| Enviar a un colega | /save html o /save md |
Transcripción legible, con los turnos archivados por compactación incluidos |
| Sesión demasiado grande, exportación rechazada | hermes sessions export para una sola sesión, o subir/desactivar max_export_messages |
El límite se juzga por sesión |
| Publicar en algún sitio público | Cualquier formato más redact |
Limpia claves de API, tokens y credenciales antes de escribir |
La conclusión que merece la pena conservar: la «exportación» sirve a dos propósitos distintos y las reglas difieren a propósito. Una transcripción es para humanos, así que se mantiene lo más cerca posible de lo que viste, con los turnos archivados incluidos. El JSON es para máquinas, así que preserva las marcas y elimina las filas deshechas. Si habías dado por sentado que la escasez del JSON significaba pérdida de datos, esta regla lo explica.
Cuándo puedes conseguirla
La corrección se fusionó en la rama main el 3 de octubre de 2026. La última versión es la v0.21.5 (etiqueta v2026.9.24), así que actualmente solo vive en main, aunque sessions.max_export_messages en sí ya existe en las versiones publicadas y funciona hoy. Las instalaciones que siguen main se vuelven coherentes con el próximo hermes update.
Para una visión más amplia, la guía de guardado y exportación de sesiones compara /save con hermes sessions export, y la guía de límites de sesión explica cómo se delimitan las sesiones. Si lo que te interesa es el mecanismo de compresión en sí, integridad de la compactación a mitad de turno y el artículo sobre la compactación en una sola llamada profundizan más. Los detalles a nivel de comando están en la referencia del comando sessions.
La versión breve
El valor aquí no es una función nueva, sino la eliminación de la ambigüedad: antes no podías saber si una exportación JSON corta del escritorio era un bug o una configuración. Ahora las tres superficies comparten una sola regla, las dos proyecciones tienen justificaciones explícitas, y una sesión sobredimensionada devuelve un código concreto con un consejo que puedes aplicar directamente. Si exportas sesiones de forma programada, merece la pena hacer hoy una comprobación: qué superficie y formato usa tu pipeline de copias de seguridad, y si lo que produce se reimporta de verdad sin problemas.