La sesión que borraste volvió: dos bugs y una nueva protección


Borras una sesión que se torció en la aplicación de escritorio, actualizas la lista y ahí está otra vez. O borras una desde la CLI y después notas que la sesión nunca vuelve a guardar una respuesta: el agente sigue contestando, el historial se queda vacío y el único rastro es una sola línea en el log: Session DB append_message failed: FOREIGN KEY constraint failed. Las dos historias son reales, y ambas salieron del mismo lote de correcciones.

El PR #125157, fusionado el 2026-09-27, cambia el comportamiento en sí: mientras un turno en vivo es dueño de una sesión, el borrado se rechaza en lugar de deshacerse en silencio.

Primero, dos bugs reales

El issue #123583 documentó el primero por completo: la fila de la sesión desaparece del almacén mientras el objeto del agente que la posee sigue vivo. Su flag interno _session_db_created sigue creyendo que la fila existe, así que el flush llama una vez a append_message, la clave foránea lo rechaza y el fallo solo se registra en el log: no hay reintento ni nada observable después, porque un rollback es un rollback. Eso se repite en cada turno. El chat parece completamente normal mientras tu transcripción duradera deja de crecer.

El segundo es su corrección (PR #124117, rescatado del #123641): una vez que se confirma que una fila falta, el siguiente flush recrea la sesión con el mismo id y reproduce todo el prefijo de la transcripción, no solo el turno nuevo. El primer intento solo guardaba el turno nuevo, porque los diccionarios de historial parecían «ya guardados»: el tipo de detalle que siempre resulta obvio en retrospectiva. La corrección también insiste en que solo se cura una fila cuya ausencia está confirmada; otros fallos de escritura no disparan una reconstrucción.

La nueva protección: rechazar el borrado, no deshacerlo

Superficie Sesión retenida por un turno en vivo Inactiva / cerrada por compactación
CLI sessions delete, export --delete-after-verified Rechazado, código de salida 1, Cannot delete active session Borrada
Borrado individual en el escritorio 409 Borrada
Borrado masivo en el escritorio Las raíces en vivo se omiten y se informan en skipped_active, con un aviso que muestra ambos recuentos Borrada
DELETE del servidor de API 409 Borrada
TUI session.delete 4023 (el código ya existente de «no se puede borrar una sesión activa») Borrada
sessions prune Las filas en vivo se respetan (sin cambios) Purgada

Vale la pena conocer algunos detalles de implementación:

  • La protección se comprueba dentro de la propia transacción de escritura del borrado (la misma ruta _write_guards_reject / _guarded_ids que ya usaba prune), así que nada puede cambiar entre la comprobación y el borrado.
  • Si una sesión raíz está inactiva pero un hijo delegado aún mantiene un lease en vivo, el borrado también se rechaza: el borrado se propagaría en cascada a ese hijo.
  • Las sesiones que terminaron (por ejemplo, cerradas por compactación) sin ningún turno dueño siguen siendo borrables.
  • El borrado masivo hace una única comprobación de protección por lotes y solo recurre a comprobaciones por raíz cuando algo está realmente protegido.

Lo que verás

hermes sessions list                                   # busca el objetivo
hermes sessions delete 20260928_101500_ab12cd         # en vivo -> Cannot delete active session, exit 1
hermes sessions export 20260928_101500_ab12cd --delete-after-verified   # exporta y verifica, luego borra
hermes sessions prune --older-than 30d                # las filas en vivo siempre se respetan

En la TUI, un borrado en vivo devuelve 4023; en el escritorio o en la API es un 409. En la práctica, lo más sencillo es dejar que el turno termine, o hacerle /stop, y borrar después.

Entonces, ¿el caso del «regreso» sigue siendo un problema?

Está cubierto, por ambos lados. La corrección de reconstrucción (#124117) cubre «la fila desapareció pero la conversación no»: la sesión vuelve con todo su prefijo de historial en la siguiente escritura correcta, así que un borrado a mitad de vuelo ya no te cuesta los turnos siguientes. La protección (#125157) cubre el otro lado: un borrado o se produce, o te dice por qué, sin ningún estado intermedio en el que la interfaz lo muestre como borrado y el backend siga escribiendo.

Si sospechas que ya arrastras residuos de esta clase de bug, mira las estadísticas del propio almacén antes de hacer algo drástico. Límites de sesión explica cuándo se dividen las sesiones y cuándo terminan, el artículo sobre state.db cubre la recuperación a nivel de base de datos, y los flags exactos están en nuestra referencia de hermes sessions.

Estado de la versión

Los tres cambios se fusionaron el 2026-09-27, después de la etiqueta de publicación v0.21.5 (v2026.9.24), así que solo están en main: hermes update los trae.

La conclusión

«La sesión volvió» y «la sesión desapareció pero sigue recibiendo turnos» parecen dos historias distintas, pero son una sola: el borrado nunca se alineó con el turno que estaba usando la sesión. La corrección lo hace explícito: rechazar mientras el turno está vivo y decirle al usuario por qué; reconstruir una fila realmente perdida en la siguiente escritura. El único hábito que vale la pena cambiar es pequeño: antes de borrar una sesión, asegúrate de que su turno ha terminado de verdad.