El Hermes en Docker perdió la mitad de su árbol de dependencias — la imagen nunca registró una línea base


Abres por la mañana el Hermes de tu contenedor Docker y el servidor de API no arranca: aiohttp not installed. Los adaptadores de mensajería han desaparecido, los servidores MCP han desaparecido, los agentes alojados aparecen como degradados. No cambiaste ninguna configuración, la imagen no se ha movido — lo único que pasó es que el contenedor instaló una dependencia opcional el día anterior.

Esto no es una imagen rota. Es un mecanismo más silencioso: la primera generación de dependencias que un contenedor construye por sí mismo se convierte en la línea base para todos los arranques posteriores. Como las dependencias integradas en la imagen nunca se registraron, la primera generación autoconstruida las tiró sin hacer ruido.

Cómo funciona una «generación de dependencias»

Las dependencias en Hermes no son una instalación de un solo sentido que se queda quieta. Las gestiona un gestor de dependencias (PM) que produce una generación en cada instalación o actualización: registra una selección (qué extras deben estar presentes) más un sello, y esa generación es contra la que se comprueban los arranques posteriores.

El diseño se apoya en una suposición: la línea base está registrada, así que los incrementos pueden construirse sobre ella. Cuando más tarde instalas una dependencia opcional de forma perezosa, debería unirse a la línea base registrada. Si la línea base está vacía, esa única instalación perezosa se convierte en la línea base y desplaza todo lo demás.

security.allow_lazy_installs vale true por defecto, lo que significa que los paquetes que faltan se pueden instalar bajo demanda. Esa comodidad es justo lo que amplifica el problema, porque cualquier instalación perezosa puede disparar «construir una nueva generación».

Por qué los contenedores son el lugar perfecto para que te muerda

La imagen de Docker hace algo totalmente razonable por su cuenta — integra ocho extras directamente en /opt/hermes/.venv:

all  messaging  otlp  anthropic  bedrock  azure-identity  matrix  google-chat

Desglosado, eso es el gateway, la telemetría OTLP, los SDKs y credenciales de Anthropic / Bedrock / Azure, el adaptador de Matrix y el adaptador de Google Chat. Todo lo que necesitas para funcionar de verdad.

El problema es que en el momento de construir la imagen, esos extras nunca se registraron como la selección de dependencias del PM. Así que la secuencia es:

  1. el contenedor construye su primera generación a causa de alguna instalación perezosa — un plugin con sus propias dependencias, o tú activando un canal bajo demanda como fal, parallel-web o exa;
  2. esa generación se construye solo con el extra solicitado en ese momento;
  3. a partir del siguiente arranque, esa selección incompleta es la línea base.

Esa es la escena inicial: servidor de API, adaptadores de mensajería y servidores MCP, todos desaparecidos a la vez. Desde v0.21.6, cualquier contenedor sobre la imagen oficial que haya construido una generación propia tiene esto delante.

Merece decirse sin rodeos: el diagnóstico no se pasó por alto. Otro colaborador llegó de forma independiente a la misma conclusión al mismo tiempo — ninguna línea base registrada en la imagen — y propuso un arreglo para instalaciones nuevas. Lo que se publicó hace dos cosas más además de prevenir: repara volúmenes que ya están rotos y registra la lista de extras solicitada en lugar de un inventario de lo que se instaló.

¿Por qué la lista solicitada y no un inventario? Porque en una compilación real de la imagen el inventario resultó omitir meta-extras como all, cron y sms — extras que no poseen ningún paquete propio y por tanto no pueden enumerarse, pero que deben permanecer en la línea base.

Arreglo uno: la compilación de la imagen registra su línea base

La compilación de la imagen ahora escribe esos extras en los hechos de dependencias del store, a través exactamente de la misma ruta de registro que ya usaba la compilación bundled. Por tanto, cada instalación posterior es una unión sobre esa línea base en lugar de un comienzo desde cero.

Arreglo dos: cada arranque restaura lo que faltaba

La prevención solo ayuda a imágenes nuevas. ¿Y los volúmenes que ya están rotos? Se curan al arrancar.

Cada arranque ahora compara los extras que registró la imagen con la selección que el volumen realmente contiene, y restaura lo que falta por la ruta normal de sincronización explícita. Un volumen degradado a ["fal"], o a una lista vacía, se recupera en su primer arranque con una imagen corregida, sin ningún paso manual.

Algunos casos límite se tratan con cuidado:

  • si la restauración falla — por ejemplo, sin conexión — el venv de la imagen arranca igual, y los extras que faltan siguen registrados como faltantes, así que una instalación perezosa antes del siguiente arranque no puede volver a descartarlos;
  • un extra registrado que la fuente ya no declara se ignora (comparado por nombre PEP 685);
  • un hecho registrado malformado (extras incorrectos, sello ausente o que no es string) reporta invalid recorded dependency state en lugar de leerse en silencio como «nada registrado»;
  • enabled-features.json deliberadamente no se escribe, así que allow_lazy_installs se comporta exactamente como antes. Este cambio toca la línea base y nada más.

La misma forma de fallo, por segunda vez: la app de escritorio tirando la TUI

El mismo lote trae otro ejemplo de «una parte falla y todo el mundo paga» — y esta vez quien paga es un servidor headless.

Una actualización desde código fuente prepara las dependencias instalando ui-tui, web y apps/desktop como una sola unión npm ci. Solo la app de escritorio arrastra addons nativos que necesitan un compilador: node-pty, electron. Así que un servidor Ubuntu que nunca ha ejecutado la app de escritorio y solo tiene g++ 9 — que rechaza -std=gnu++20 — hacía fallar toda la instalación de dependencias, y la TUI y la web UI nunca se construían aunque no tuvieran nada que ver con la app de escritorio. Cada arranque imprimía a source update could not be finished automatically (6 attempts).

Ahora ese caso reintenta una vez sin la app de escritorio, construye la TUI y la web UI como siempre, y registra la compilación del escritorio como pendiente:

Desktop app build owed: desktop dependencies failed

También imprime una salida realmente útil:

ⓘ If this host never runs the desktop app, hermes uninstall --gui removes it and
  future updates stop building it.

En una máquina headless esa es la respuesta más limpia — no cargar la app de escritorio en absoluto:

hermes uninstall --gui

El aviso aparece en las dos situaciones que lo necesitan: tras ese reintento exitoso, y en cualquier actualización de Linux headless (sin DISPLAY ni WAYLAND_DISPLAY) que seleccione la app de escritorio.

Qué revisar y qué hacer

Ejecutando en Docker:

  1. primero confirma que el síntoma encaja — el servidor de API, los adaptadores de mensajería y los servidores MCP desapareciendo juntos, con los agentes reportados como degradados;
  2. descarga la imagen corregida y reinicia; un volumen roto restaura sus extras que faltan en el primer arranque, y el volumen no necesita recrearse;
  3. si aún no puedes mover la imagen, instalar explícitamente los extras que faltan restaurará ese arranque — pero la línea base sigue mal, así que la siguiente instalación perezosa puede volver a descartarlos.

Ejecutando en un servidor headless:

  1. si las actualizaciones siguen fallando con could not be finished automatically, comprueba si las dependencias nativas de la app de escritorio son el obstáculo;
  2. si esta máquina nunca ejecuta la app de escritorio, elimínala del checkout con hermes uninstall --gui;
  3. las futuras actualizaciones dejarán entonces de construir node-pty y electron para un front end que no usas.

Dos hábitos que se generalizan:

  • los problemas de dependencias rara vez son «se rompió un paquete» y mucho más a menudo una generación cuya línea base se escribió mal. Cuando algo pierde de repente todo un conjunto de capacidades, sospecha de la línea base antes de reinstalar paquetes uno a uno;
  • las instalaciones perezosas son cómodas precisamente porque convierten «falta» en «el nuevo valor por defecto». Si quieres estabilidad a largo plazo, una línea base registrada gana a una instalación improvisada todas las veces.

La versión corta

Los dos arreglos de este lote van de una sola cosa: el estado de las dependencias necesita un registro de línea base fiable. La imagen de Docker integró ocho extras sin registrarlos, así que la primera generación autoconstruida del contenedor los borró; los módulos nativos de la app de escritorio fallaron al compilar y se llevaron por delante una TUI y una web UI que no tenían nada que ver. El primero se resuelve registrando en tiempo de compilación y restaurando al arrancar; el segundo, degradando a la parte que realmente se necesita.

Para la lectura operativa del entorno, mira ejecutar Hermes en un contenedor compartido, el problema del WAL de state.db y qué hacer cuando una actualización te llena el disco. Los detalles de los comandos están en las páginas de referencia de hermes uninstall y hermes update, y la guía de instalación es donde empezar desde cero.