Tu plugin ya puede enterarse de que una sesión se comprime o se borra

Tienes un plugin de memoria que mete en cada turno las convenciones de código de tu equipo y tres endpoints de API. Durante veinte minutos funciona de maravilla. Después Hermes decide que la conversación es demasiado larga y la compacta: todos los mensajes iniciales quedan plegados en un solo párrafo de resumen, tus convenciones incluidas. A partir de ese momento el modelo sigue preguntando cosas que ya le habías contestado, y tu plugin no se enteró de nada.
Hay una versión más silenciosa del mismo agujero: mantienes un pequeño archivo de índice por sesión en disco. Un compañero borra cuarenta sesiones desde la app de escritorio y tu índice sigue igual de gordo que antes, lleno de entradas que nadie va a volver a consultar.
Los dos problemas tienen la misma raíz: los plugins podían ver «se llamó a una herramienta» y «empezó un turno», pero no «reescribieron la transcripción» ni «esta sesión ya no existe». Dos parches fusionados el 18 de octubre cierran el hueco: un hook completamente nuevo, post_compaction, y un on_session_delete que cubre todas las vías de borrado.
Por qué antes no podían verlo
El sistema de plugins ya traía un juego de hooks de ciclo de vida: on_session_start, on_session_end, on_session_finalize, on_session_reset. Marcan momentos en el tiempo, no reescrituras de contenido.
La compactación es una acción interna. Puede ocurrir a mitad de turno cuando el contexto se llena, justo después de que aterrice el resultado gigante de una herramienta, cuando el agente recorta por iniciativa propia salidas antiguas, o cuando alguien escribe /compress. Para un plugin, todo eso era invisible. El borrado de sesiones es la misma historia vista desde el otro lado: hermes sessions delete, los borrados individuales y masivos desde Desktop, /exit --delete, el barrido de sesiones vacías, la purga por retención, el mantenimiento de filas fantasma. Todos acaban en los mismos métodos de base de datos, pero los plugins no recibían ningún callback.
Así que los autores de plugins hicieron lo que se hace cuando no hay señal: sondear el almacén de sesiones, adivinar por el conteo de tokens si hubo compactación, escanear directorios buscando huérfanos. Lento en el mejor de los casos, equivocado en el peor.
post_compaction: devolver lo que se comió el resumen
El registro es igual que el de cualquier otro hook:
def register(ctx):
ctx.register_hook("post_compaction", on_compaction)
def on_compaction(session_id, kind, reason, **kwargs):
# flag it, then re-inject from the next pre_llm_call
mark_session_for_reinjection(session_id, kwargs.get("messages_removed"))
Qué recibe el callback:
| Campo | Significado |
|---|---|
session_id |
La sesión que ahora contiene la transcripción compactada |
old_session_id |
La sesión en la que empezó cuando la compactación rotó; cadena vacía si se compactó en el sitio |
kind |
summarize (compactación completa), micro_summarize, prune_tool_results, provider_native (Codex app-server) |
reason |
Qué lo disparó: manual (/compress), pre_api, post_tool, overflow, gateway_hygiene, between_turns, proactive_prune, provider, unknown |
before_tokens / after_tokens |
Estimaciones aproximadas de tokens, None cuando se desconocen |
messages_removed |
Cuántos mensajes se fueron, None cuando se desconoce |
Lo importante es que se dispara una vez por cada compactación que de verdad se aplica, en todas las rutas: CLI, los backends de TUI y Desktop, agentes de gateway, sesiones de cron, el /compress del gateway, la compactación nativa de Codex, la micro-compactación y la poda proactiva de resultados de herramientas. Los intentos que no cambiaron nada —un resumen abortado, uno bloqueado por un cooldown, uno rechazado porque haría crecer la transcripción, un /compress vacío— no disparan nada, de modo que nunca tienes que preguntarte si aquella «compactación» fue en realidad un no-op.
Un patrón que sí funciona: anota que esta sesión necesita un refresco y vuelve a inyectar desde el siguiente pre_llm_call. El hook en sí es un observador: su valor de retorno se ignora y no puedes reescribir la conversación desde dentro. Es deliberado: la compactación ya está confirmada y cambiarla en pleno vuelo rompería la consistencia.
_pending = {}
def on_compaction(session_id, kind, reason, messages_removed=None, **kw):
if kind in ("summarize", "micro_summarize"):
_pending[session_id] = {"kind": kind, "removed": messages_removed}
def inject(session_id, conversation_history, **kw):
if session_id in _pending:
_pending.pop(session_id)
return "Project rules: every new endpoint needs OpenAPI annotations; migrations go through alembic only."
return None
def register(ctx):
ctx.register_hook("post_compaction", on_compaction)
ctx.register_hook("pre_llm_call", inject)
on_session_delete: cuadrar tus propias cuentas
El segundo hook se ocupa de la limpieza. Se dispara una vez, después de que el borrado o el archivado deliberado hagan commit:
| Parámetro | Significado |
|---|---|
session_id |
La sesión a la que apuntaba la operación |
reason |
delete, archive, empty_sweep, prune, ghost_prune, profile_repair |
profile |
El perfil dueño del almacén (default para el home raíz) |
deleted_ids |
Todas las filas eliminadas (hijos en cascada, enlaces de la cadena de compresión, filas de borrados masivos) cuando se fue más de una |
sessions_dir |
El directorio sessions/ del perfil cuando quien llamó disponía de él |
def on_delete(session_id, reason, profile=None, deleted_ids=None, sessions_dir=None, **kw):
ids = deleted_ids or [session_id]
for sid in ids:
drop_sidecar(sid) # your per-session cache / index
log.info("cleaned %d session rows (%s)", len(ids), reason)
def register(ctx):
ctx.register_hook("on_session_delete", on_delete)
Dos detalles de ese parche son fáciles de pasar por alto y conviene conocerlos.
Primero, Desktop nunca disparaba nada antes. Los borrados y archivados individuales de Desktop y del dashboard no pasan por SessionDB.delete_session, pasan por una ruta de recibo de sesión en runtime, así que el cableado antiguo callaba ahí. Esa ruta ahora también dispara, y un reintento idéntico reproduce el recibo sin emitir un segundo evento.
Segundo, archivar cuenta. Archivar significa que el usuario escondió la sesión; la transcripción sigue ahí y se puede desarchivar. Para un plugin eso normalmente quiere decir «este hilo se acabó»: si estabas siguiendo una tarea, que el usuario archive el chat es un buen momento para cerrarla. Solo dispara un cambio real de no archivado a archivado, así que los barridos de auto-archivado por inactividad y la adopción de perfiles se quedan en silencio.
Una tercera cosa arreglada en la misma ventana: los slash commands ya saben en qué sesión están
Un cambio relacionado merece mención. Un slash command de plugin registrado con ctx.register_command solo recibía la cadena de argumentos en crudo: no tenía idea de quién lo ejecutó, en qué plataforma ni en qué sesión. Ahora un handler que declara estas palabras clave las recibe:
def _handle_whoami(raw_args, *, session_id="", platform="", source=None):
user = source.user_id if source is not None else "local user"
return f"{user} on {platform}, session {session_id or '(none yet)'}"
def register(ctx):
ctx.register_command("whoami", handler=_handle_whoami, description="Show the calling session")
Las palabras clave disponibles son session_id, session_key, platform y source (el objeto de identidad derivado del adaptador, con user_id, chat_id, thread_id y compañía); también puedes declarar simplemente **kwargs y recibirlas todas. Los plugins existentes no necesitan ningún cambio: un handler de un solo argumento se sigue llamando como handler(raw_args), y los handlers async se siguen esperando.
Una nota de seguridad: source es información de identidad, no una concesión de autorización. Un comando de plugin sensible debería decidir a partir de ids estables (user_id, chat_id) y fallar cerrado cuando source es None.
Tres límites con los que hay que diseñar
- Ambos son observadores. Los valores de retorno se ignoran; un callback que lanza una excepción queda registrado en el log y se salta. El borrado o la compactación ya están confirmados: un plugin no puede vetarlos ni esperar un rollback.
- Los callbacks tienen límite de tiempo, marcado por
plugins.hook_callback_timeout(30 segundos por defecto). Si haces llamadas de red dentro de un hook, pon tu propio timeout; lo que se queda colgado es tu callback, no la conversación. - La entrega es local al proceso y sin garantía de exactamente una vez. El evento de borrado solo se dispara en el proceso que confirmó el borrado. Los efectos secundarios externos que dependan del callback deberían tolerar una llamada perdida o duplicada.
Cuándo puedes usarlo
Ambos hooks se fusionaron a la rama main el 18 de octubre, después de la última release v0.22.2 del 15 de octubre, así que solo viven en main y todavía no están en ninguna versión publicada. Si instalaste Hermes desde PyPI o desde un canal de actualizaciones, hoy no puedes usarlos; ejecutar la rama main desde el código fuente es la vía para probarlos antes de tiempo.
Para la lista completa de hooks y las condiciones exactas de disparo, la página oficial de Event Hooks es la referencia canónica; la referencia de comandos de plugins de este sitio cubre la superficie del CLI. Si estás construyendo en esta zona, hay dos artículos anteriores que encajan bien con este: el broker de tokens para plugins muestra cómo un plugin autentica a un usuario contra una API de terceros sin llegar a sostener el token, y la reconstrucción de prompts en la compactación explica cómo se reensamblan los prompts durante la compactación. ¿Escribes un plugin de memoria? La consolidación de memoria estilo dreaming toma el camino opuesto: ordenar mientras el sistema está inactivo en lugar de reaccionar a la compactación.
La versión corta
post_compaction responde a «qué hago cuando el contexto que inyecté desaparece dentro de un resumen». on_session_delete responde a «quién limpia mi desorden cuando la sesión ya no está». Ninguno de los dos cambia lo que hace Hermes; simplemente exponen dos momentos que antes eran internos. Para quien escribe plugins, eso es toda una clase de sondeos, conjeturas y fallbacks defensivos que ahora puede borrar.