Renombrar un profile de Hermes dejaba un fantasma

Renombraste el profile de tu bot de Telegram de mybot a assistant. hermes profile rename mybot assistant terminó sin quejarse: el directorio pasó de ~/.hermes/profiles/mybot a ~/.hermes/profiles/assistant, el alias de shell se movió con él y el bot siguió respondiendo en el grupo. Días después abres una conversación antigua y la ves archivada bajo mybot, un profile que ya no existe, mientras la tabla de routing del gateway conserva claves como agent:mybot:*. La otra cara del mismo bug: borras un profile y unos días más tarde su directorio reaparece en disco con un archivo de caché dentro. El PR #112653 arregla los dos casos (#111926 y #112592).
Tres palabras antes de seguir. Un profile es un agente de Hermes autocontenido —su propia config, credenciales, memoria y sesiones— que vive en ~/.hermes/profiles/<nombre>. Una routing key es la cadena con la que el gateway decide a qué profile y a qué sesión pertenece un mensaje entrante, y tiene la forma agent:<profile>:<sesión>. Un gateway multiplexado es un solo proceso gateway que atiende varios profiles a la vez y mantiene el índice de routing en memoria.
El rename antiguo movía el directorio, no la identidad
El código viejo hacía la mitad del trabajo, la del sistema de archivos: renombrar el directorio del profile y renombrar el alias de shell. La mitad de la base de datos nunca se tocaba. Y eso duele porque muchísimas filas del state store de Hermes usan el nombre del profile como clave. Después de un rename esas filas seguían diciendo mybot, y mybot ya no existía en disco: apuntaban a un fantasma.
Nada estalla de inmediato, va goteando. Las sesiones antiguas dejan de cuadrar, una fila de heartbeat sigue informando de un profile que ya no está, las delivery obligations (los registros que dicen «este mensaje todavía no se entregó y hay que reintentarlo») se quedan bajo el nombre muerto y la propiedad de los temas de Telegram deja de corresponder. Quien corre varios profiles bajo un mismo gateway lo nota antes que nadie; los límites de ese montaje están en nuestra guía de profiles multi-instancia.
Por qué las tablas de identidad usan el nombre del profile como clave
Para la base de datos, el nombre del profile es la identidad. Cambiar el nombre significa que el agente cambia de identidad en el state store, así que la migración no es mover archivos: es un rename más una reescritura de todas las filas indexadas por ese nombre. El PR #112653 enumera exactamente qué filas entran:
sessions.session_key,sessions.profile_name,sessions.origin_jsongateway_heartbeats.profiledelivery_obligations- las tablas de temas de Telegram
- el índice
gateway_routing(la clave y el JSON que lleva incrustado)
Hay una segunda frontera que conviene conocer —qué sigue al profile y qué sigue a la sesión— y la guía de fronteras de sesión la recorre entera.
Qué se migra ahora: una transacción y un índice en memoria
El lado de la base de datos es SessionDB.rekey_profile_state(old, new) en hermes_state_gateway.py: una sola transacción que reescribe al nombre nuevo todas las filas indexadas por nombre de profile. Antes comprueba colisiones: si el nombre nuevo ya tiene filas que chocarían, se niega en lugar de escribir un desastre. Es idempotente y se salta las tablas que todavía no se han creado.
Reescribir solo la base de datos no alcanza, sin embargo. Un multiplexer en marcha vuelca cada cierto tiempo sus _entries en memoria hacia la base de datos, así que se comería la reescritura. De eso se ocupa SessionStore.rekey_profile_routing(old, new) en gateway/session.py: reajusta las claves del índice de routing en memoria —las entradas y origin.profile— y lo persiste. Por eso la migración la manda un gateway vivo: el índice vive en su proceso, así que él tiene la última palabra.
La cadena funciona así. rename_profile llama a _migrate_profile_identity en hermes_cli/profile_identity.py. Si hay un multiplexer vivo, delega en el verbo migrate-profile-identity del control socket; si no hay ningún gateway sujetando el store, el CLI hace la reescritura duradera por su cuenta. En cualquiera de los dos casos la migración nunca es fatal para el rename: el peor escenario es un aviso, un profile renombrado y unas filas de identidad que siguen con el nombre viejo.
Renombrar un profile paso a paso
hermes profile list # mira qué vas a renombrar
hermes profile rename mybot assistant
hermes profile show assistant # confirma el nombre nuevo y su información
hermes profile list # la lista debe mostrar assistant, no mybot
Lee la salida del rename. Si avisa de que el gateway vivo no pudo migrar la identidad de sesión, no lo dejes pasar: arréglalo con el comando de la sección siguiente. Si no sabes si tu gateway es un multiplexer, sigue lo que imprima el comando: sin aviso, la migración ya está aplicada; con aviso, reinicia el gateway y reintenta. Todos los flags de estos comandos están en la referencia del comando hermes profile, y la página de instalación cubre cómo tener Hermes en la máquina.
Si la migración no pudo ejecutarse: reintenta con migrate-identity
El rename no se puede repetir —profiles/mybot ya no está—, así que el reintento tiene su propio comando:
hermes profile migrate-identity <old-name> <new-name>
Una sesión real se ve así:
hermes profile rename mybot assistant
# ⚠ Profile was renamed, but the live gateway could not migrate session identity (…).
# Restart the gateway, then run:
# hermes profile migrate-identity mybot assistant
hermes profile migrate-identity mybot assistant
# ✓ Session/routing identity migrated: mybot → assistant
El primer bloque te dice que el profile se renombró pero que el gateway vivo no pudo migrar la identidad de sesión, y te da dos salidas: reiniciar el gateway (al arrancar recarga el índice de routing desde la base de datos, así que la migración se aplica) o pararlo, porque sin ningún proceso sujetando el store el comando hace la reescritura duradera por su cuenta.
Propiedades que conviene conocer:
- La migración se guía por las filas que aún nombran al profile viejo, así que
profiles/mybotno tiene que existir; solo se comprueba el nombre nuevo. - Es idempotente: repetir una migración ya hecha termina bien, sin nada que reindexar.
- Sale con código distinto de cero cuando un gateway vivo rechaza la migración o cuando una base de datos rechaza la reescritura: una colisión de routing, un lock, o que una de las dos bases falle mientras la otra tiene éxito. El error nombra la base y el problema.
Renombrar el profile default solo cambia el nombre visible
Quizá quieras un nombre más bonito para el profile default. Aquí está la trampa: el ID interno del profile default es siempre default, porque ~/.hermes es la raíz de instalación y no se puede renombrar de verdad. Lo que fija este comando es un nombre visible (display name):
hermes profile rename default Harumesu # Unicode sin problema: 小助手
Ese nombre aparece en hermes profile list y show, en el comando de chat /profile, en el dashboard y en la app de escritorio, incluida la plantilla de Bot Mode. Es solo presentación: -p default, los nombres de servicio, los cron jobs y cualquier otra referencia siguen usando el ID canónico default. Se guarda como display_name en ~/.hermes/profile.yaml; borra esa línea para revertirlo. Los profiles con nombre también pueden llevar un display_name (y sobrevive a un rename real), pero en ellos rename renombra el profile en sí.
Un profile borrado deja una lápida: nada puede resucitarlo
Borrar se ve así:
hermes profile delete coder
Detiene el gateway, quita el servicio de systemd/launchd, elimina el alias del comando y borra todos los datos del profile. Te pide escribir el nombre del profile para confirmar; --yes se salta esa confirmación. El profile default no se puede borrar (es ~/.hermes mismo): para eliminarlo todo, usa hermes uninstall.
Lo que solía pasar después: un hilo que aún llevaba la ruta del home muerto podía hacer una escritura atómica tardía, y el mkdir(parents=True) que la precede reconstruía el árbol de directorios. Tu coder borrado reaparecía en disco con poco más que un archivo de caché.
#112592 cierra ese camino. utils._atomic_write y atomic_roundtrip_yaml_update / atomic_roundtrip_yaml_save ahora crean sus directorios padre a través de mkdir_under_hermes_home, el guardián de lápidas, así que cualquier escritura atómica de JSON, texto, bytes o YAML rechaza con FileNotFoundError un home con nombre que tenga lápida y no deja nada atrás. Los mkdir(parents=True) previos que quedaban sueltos en los llamadores pasaron por el mismo guardián: la caché de modelos, el ETag de models.dev, el centinela de ciclo de vida, la caché de secretos, el OAuth de MCP, el store de memoria y el barrido mecánico del resto de puntos de escritura previa; dos sitios que necesitan un mode= explícito mantienen su mkdir detrás de assert_named_profile_home_live.
El guardián no es indiscriminado. Si la ruta destino no tiene un ancestro profiles/<nombre> demostrable, no hace nada: el home por defecto, los directorios ocultos .staging y las rutas que solo contienen profiles de casualidad siguen escribiendo con normalidad.
Advertencias y resumen
- Lee la salida del rename. Una migración fallida nunca hace fallar el rename, solo avisa: reinicia el gateway y ejecuta
hermes profile migrate-identity <viejo> <nuevo>. - El reintento es idempotente, así que lanzarlo cuando dudas no cuesta nada.
- Manda el multiplexer vivo, porque tiene el índice de routing en memoria; si prefieres parar el gateway, el comando hace la reescritura duradera por su cuenta.
- El profile default nunca se renombra de verdad: solo fijas un nombre visible y el ID interno sigue siendo
default. - Después de borrar, deja que siga borrado. El guardián de lápidas impide que una escritura tardía convierta un profile borrado en un directorio en disco.