Tu conexión parece viva, pero no hay nadie en casa: cómo los heartbeats de Hermes curan las desconexiones silenciosas


Cierras el portátil, te vas a comer y vuelves. Hermes sigue ahí, en pantalla: historial de la sesión, cursor, todo parece normal. Escribes un mensaje y pulsas Enter. No pasa nada. Sin error, sin spinner, sin timeout: solo silencio. Tu mensaje se ha desvanecido en un agujero negro.

Esa situación de «interfaz viva, conexión muerta» se llama silent disconnect (desconexión silenciosa). No es una rareza de Hermes: es la enfermedad de todos los programas que viven en red. La red ya no está, pero ninguno de los dos extremos lo sabe, así que un lado espera y el otro también. Un conjunto de cambios recién fusionados en Hermes permite que la TUI y el Desktop detecten estas conexiones zombi y las reconstruyan por su cuenta. Esto es lo que arreglan de verdad, y por qué le importa a cualquiera que ejecute Hermes de forma remota.

Por qué las conexiones fingen su propia muerte

Primero, lo básico: tu interfaz de Hermes (la TUI del terminal o la app de Desktop) se comunica con el «cerebro» real — el proceso gateway que corre en un servidor o en segundo plano en tu máquina — a través de una única conexión WebSocket. Cada mensaje que envías y cada token que devuelve viajan por ese conducto.

El problema es la red que hay debajo. Supón que tu Mac duerme diez minutos, el Wi-Fi se cae por el camino y despierta con una IP nueva. O que cambias del Wi-Fi de la oficina al hotspot del móvil. O que tu VPN se reconecta. En todos estos casos, la conexión TCP antigua ya no existe — pero tu máquina no lo sabe. En jerga, esto se llama half-open connection (conexión a medio abrir): un extremo del conducto ha desaparecido y el otro sigue creyendo que todo está bien.

¿Por qué ninguno de los dos lados se da cuenta? TCP tiene un mecanismo de keepalive, pero por defecto sondea el cable con tan poca frecuencia que no sirve de nada, y los navegadores ni siquiera exponen ping/pong a los desarrolladores de WebSocket. Así que el cliente sigue escribiendo en una conexión muerta — y esas escrituras no fallan de forma ruidosa: simplemente se pierden en silencio. Ese es tu mensaje entrando en el agujero negro.

La solución: un heartbeat en tres capas

PR #93792, fusionado el 24 de agosto (integrando tres contribuciones del desarrollador comunitario @100yenadmin — #89958, #90012 y #89984), ataca exactamente este problema: cada conexión de cliente ahora hace ping al gateway de forma periódica, declara la conexión muerta cuando no llega ninguna respuesta y la reconstruye automáticamente. Funciona en tres capas, y cada una resuelve un problema.

Capa 1: el servidor aprende a responder a los pings

El lado del servidor (tui_gateway/ws.py) hace tres cosas pequeñas. Primero, su handshake de arranque (gateway.ready) ahora anuncia heartbeat: true, avisando a los clientes: «soporto heartbeats». Segundo, añade un método gateway.ping que se responde inline, dentro del read loop — sin colas ni programación: se responde al recibirlo. Tercero, sella cada conexión con last_inbound_at (cuándo llegaron los últimos datos), lo que facilita el diagnóstico posterior.

Capa 2: el cliente hace ping con regularidad y declara la muerte al agotar el tiempo

El cliente de la TUI (ui-tui/src/gatewayClient.ts) y el JsonRpcGatewayClient compartido del Desktop usan los mismos parámetros: enviar un gateway.ping cada 15 segundos; si no llega ningún dato del gateway en 45 segundos (no solo acks de heartbeat — cualquier dato cuenta), declarar la conexión muerta y tirarla abajo para reconectar.

¿Por qué 45 segundos y no 15? Los round-trips de red tienen latencia, y un heartbeat perdido no significa que la conexión haya muerto. Si no has oído nada en 45 segundos (tres ciclos de heartbeat), casi con seguridad no es jitter: la conexión se ha ido de verdad.

Capa 3: exponential backoff y descarte de datos obsoletos

Una vez declarada muerta la conexión, el cliente no martillea al servidor con intentos de reconexión. Usa exponential backoff: reintenta tras 1 segundo, luego 2, luego 4, 8, 16… con tope en 30 segundos, y sigue reintentando hasta conectar. Cada intento publica un evento gateway.reconnecting (número de intento + retraso en milisegundos), lo que hace que la depuración sea muy fácil.

El Desktop añade un mecanismo más fino: la invalidación de la generación del socket (socket generation). Cuando una conexión se reconstruye, el cliente entrega al socket nuevo un token de generación fresco; cualquier frame tardío que llegue por el socket viejo — por ejemplo, los últimos mensajes que emitió el gateway justo antes de la caída — se descarta porque lleva el token antiguo. La conexión nueva arranca limpia, así que el tráfico sobrante de un socket muerto no puede contaminar la sesión nueva, y ningún trabajo se ejecuta dos veces.

Los gateways antiguos no se ven afectados

Todo el heartbeat está capability-gated: los clientes solo empiezan a hacer heartbeat después de recibir heartbeat: true en el handshake de gateway.ready. Si sigues conectado a un gateway antiguo — digamos, un servidor remoto que no se ha actualizado — el cliente se comporta exactamente igual que antes. Es decir, este arreglo puede viajar con los clientes nuevos sin romper los servidores viejos.

Qué escenarios cubre

  • Sleep/wake: los ciclos de sueño de macOS y Windows reconstruyen la pila de red — la fuente más común de conexiones zombi. Antes, el Desktop solo ejecutaba una sonda única al despertar (eso era #93694, fusionado antes ese mismo día, que evitaba que las actualizaciones de la gateway remota dejaran la app varada en un socket muerto). Ahora hay un heartbeat continuo por debajo.
  • Cambios de red: de Wi-Fi a hotspot, desenchufar y volver a enchufar el Ethernet, roaming en la oficina.
  • Reconexiones de VPN: cuando el túnel se reconstruye, todos los paquetes TCP de la conexión antigua se pierden.
  • Reinicios silenciosos del servidor: el proceso gateway se reinicia, pero el cliente nunca recibe un aviso de cierre.

El servidor también activó TCP keepalive en los sockets WebSocket (detección de peers muertos) como segunda línea de defensa.

Cuándo lo tendrás realmente

Para ser honestos sobre el estado: estos cambios viven solo en maintodavía no han llegado a ninguna release etiquetada (la última, v0.20.5, se etiquetó el 19 de agosto, antes de que aterrizara este lote). Así que si ejecutas hermes update hoy, todavía no verás logs de heartbeat. Actualiza cuando salga la próxima versión — nuestra guía de instalación y actualización te muestra cómo, y puedes hojear las notas de la release v0.20.5 para ver qué cambió en la versión anterior.

¿Cómo confirmas que el heartbeat funciona después de actualizar? Vigila las líneas [lifecycle] en la TUI: en operación normal permanecen en silencio; cuando la red tartamudea, verás websocket silent drop detected; forcing reconnect y scheduling gateway reconnect in Xms. Eso ya no es una mala noticia — es el cliente salvándose a sí mismo.

Y si lo que te preocupa es el otro tipo de atasco — el bucle de razonamiento del agente estancado (no un problema de conexión, sino el agente dejando de trabajar) — ese es un mecanismo aparte: el loop watchdog del lado de la gateway, que cubrimos en la guía de ajuste del loop watchdog. Léelas las dos juntas y tendrás cubierta la fiabilidad tanto en la capa de transporte como en la capa de ejecución.

Resumen

Las desconexiones silenciosas son el asesino invisible de las herramientas remotas: sin error, sin pista, solo tu mirada de desconcierto. El arreglo de Hermes es refrescantemente simple en espíritu — ping periódico, reconexión cuando nadie responde y un backoff educado mientras tanto — pero esa sencillez es exactamente la razón por la que funciona. Convierte «la conexión fingió su propia muerte» de folklore en un evento detectable y recuperable. La próxima vez que tu red tartamudee, deja de mirar el agujero negro: el cliente ya está trabajando en la manera de volver.