Instalar un plugin sin reiniciar el gateway: Hermes ahora lo cablea en vivo


Instalas un plugin en tu bot dentro de un grupo de Telegram. Trae un comando de barra nuevo. Lo escribes en el grupo, lleno de expectación — y el bot actúa como si no hubiera oído nada. La documentación dice: reinicia el gateway. Lo cual significa que una respuesta en streaming se corta, los trabajos en segundo plano mueren y todas las sesiones vuelven a empezar. Es un precio muy alto por un solo comando de barra.

Así que la mayoría espera al siguiente reinicio. Eso acaba de cambiar (#119266, que arregla #87770): instala un plugin con el gateway en marcha y sus handlers de plataforma quedan cableados al instante.

Por qué antes el reinicio era obligatorio

Todo el problema estaba en cuándo se descubren los plugins:

  • discover_plugins() hace cortocircuito una vez que ya ha ocurrido el descubrimiento, salvo que pases explícitamente force=True.
  • Ninguna de las rutas de instalación en caliente lo pasaba. Peor aún, tools/mcp_tool_config._portable_mcp_servers la llamaba sin forzar — así que ejecutar mcp.reload tras una instalación en caliente recargaba el conjunto antiguo de servers.
  • El plugins.manage install de la TUI volvía en cuanto terminaba el helper de instalación, sin ningún reescaneo.
  • Los adaptadores de plataforma (Telegram, Slack, …) construían su registro de handlers al arrancar y no volvían a mirar.

Efecto neto: los archivos del plugin estaban en disco, la config lo tenía activado y el proceso en ejecución no sabía nada de él.

Un resumen de activación: en vivo ahora vs. siguiente sesión

Toda ruta de carga en caliente es ahora un reescaneo forzado real, y emite un callback desde dentro de ese reescaneo (PluginManager.on_plugin_loaded) con un resumen de activación por cada plugin recién cargado. Se ve así:

{"name": "late-mcp", "key": "late-mcp",
 "activated_now": {"gateway_commands": ["late"], "callbacks": ["telegram"]},
 "deferred": {"tools": ["late_tool"], "prompt": ["late.section"], "mcp_servers": ["worker"]}}

En términos llanos:

Categoría Cuándo aplica Qué cubre
Comandos de barra del gateway De inmediato los nombres de comando que registró el plugin
Hooks de transformación del gateway De inmediato transform_llm_output, transform_tool_result, transform_terminal_output, pre_gateway_dispatch, gateway_platform_event, pre_command
Otros hooks De inmediato el resto de nombres de hook
Callbacks de plataforma De inmediato plataformas con una fábrica register_platform_handler, más los action ids de Slack
Herramientas Siguiente sesión nombres de herramienta
Secciones del system prompt Siguiente sesión ids de sección
Servidores MCP portables Un mcp.reload, o la siguiente sesión los nombres exactamente como aparecen en mcp.json

De las tres filas de “de inmediato”, la más útil es callbacks de plataforma: los callbacks de botón y las transformaciones de mensajes entrantes de un plugin ya funcionan sin reinicio.

Por qué el cableado ni se duplica ni pierde frente a los handlers del core

“Funciona de inmediato” suena simple; dos trampas lo hacen menos simple.

Trampa uno: doble registro. Una recarga forzada devuelve objetos función nuevos, así que la identidad de función no sirve para saber si algo ya estaba cableado. La solución es indexar cada cableado por (plugin, qualname de la fábrica) — un plugin, una fábrica, un cableado. Por eso el método se llama recableado idempotente (rewire_plugin_handlers()): vuelve a leer el registro de fábricas y ejecuta solo las fábricas que aún no están cableadas al cliente nativo vivo.

Trampa dos: el orden. En Telegram, los handlers recién añadidos se izan por delante de los catch-alls del core. Esto importa porque PTB solo despacha la primera coincidencia por grupo, y el core registra un filters.COMMAND catch-all — un handler tardío situado detrás nunca se dispararía. Slack hace el equivalente deduplicando register_slack_action_handler por cada AsyncApp.

El runner del gateway se suscribe por cada profile atendido al arrancar, y al recibir el evento recablea cada adaptador vivo en el event loop. Todo esto ocurre dentro del proceso del gateway, no en el proceso que ejecutó la instalación.

Cada superficie de instalación te dice la verdad

CLI, Desktop, TUI o la instalación REST del dashboard — todas informan exactamente de qué está vivo ahora y qué tiene que esperar:

hermes plugins install late-mcp --enable

El comando primero avisa al gateway en ejecución (un verbo reload-plugins en el control socket que le pide reescanear a la fuerza el home indicado) y después imprime ese desglose de en vivo vs. diferido. El plugins.manage install/toggle/update de Desktop/TUI devuelve activation más gateway_reloaded; restart_required es true solo cuando ningún gateway respondió.

Hay una postura editorial deliberada en ese diseño: mejor no hacer nada que fingir. Por eso:

  • Las herramientas y las secciones del system prompt de un plugin cargado tarde no se aplican a la sesión en curso. La misma regla que /skills install: el prompt y el esquema de herramientas de una sesión viva se mantienen estables en cache, y un cambio a mitad de vuelo rompería eso.
  • Los servidores MCP portables esperan a mcp.reload.
  • Desactivar no descablea. Desactiva un plugin en caliente y sus handlers ya cableados siguen ahí hasta que el gateway reinicie. Todas las superficies lo dicen, en vez de dejarte creer que todo queda limpio al instante.

Cómo se ve en la práctica

Una ejecución típica:

# 1. instalar y activar (avisa al gateway en ejecución para que reescanee)
hermes plugins install late-mcp --enable

# 2. la salida indica que gateway_commands: ["late"] ya está vivo
#    así que /late ya funciona en tu grupo de Telegram — sin reinicio

# 3. si el plugin también trae un server MCP, levántalo también
#    (con mcp.reload, o simplemente abriendo una sesión nueva)

Una advertencia que merece interiorizarse: el gateway tiene que estar sirviendo el home donde vive el plugin. El evento se propaga por profile, y un gateway que no sirve ese profile no cablea nada para él. En despliegues multi-profile y multi-gateway esto se pisa con más facilidad que en una sola máquina — contexto en un gateway por host.

Límites y advertencias

  • Esta versión cubre solo handlers. Las herramientas, las secciones del system prompt y los servidores MCP portables quedan fuera del conjunto inmediato, por las razones de prompt cache y de ciclo de vida de conexión ya explicadas.
  • No hay descableado. El cableado es unidireccional: una instalación aplica de inmediato; una desactivación solo limpia de verdad después de un reinicio. Las actualizaciones se comportan igual.
  • La confianza en los plugins no se relajó. Esto es un mecanismo de cableado en runtime, no una relajación de la revisión; sobre de dónde vienen los plugins y cómo se admiten, mira el modelo de confianza del catálogo de plugins y el SDK de plugins de escritorio.
  • Los plugins de backend de terminal son otra vía — mira plugins de backend de terminal.

Para cerrar

“Reinicia después de instalar” se soporta en un setup de juguete, pero en un gateway que hace trabajo real es una barrera dura — empuja cada actualización a las 3 de la madrugada. La mitad de esa barrera ya no está: lo que debería surtir efecto de inmediato (handlers de plataforma, comandos de barra, hooks de transformación) lo hace, y lo que debe esperar una ronda (herramientas, secciones de prompt, conexiones MCP) lo dice en voz alta — incluido el hecho de que desactivar no descablea. La otra mitad no se moverá pronto, porque choca de frente con el invariante del prompt cache.