El culpable del OOM a las 3 de la mañana: los procesos huérfanos que dejó tu agente


El ventilador del portátil vuelve a arrancar, localhost:3000 dice que el puerto está ocupado y ps aux muestra cuatro procesos node y python que no reconoces. En algún rincón de tu memoria: la semana pasada le pediste al agente que “levantara un servidor y lo probara”, la session terminó y el servidor no. Lo que todos esos restos tienen en común es que nadie los recuerda y nada lleva la cuenta — y ese es exactamente el trabajo de agent-janitor.

De dónde salen los procesos huérfanos

Una llamada de herramienta del agente se parece mucho a que tú escribas en una terminal. Le pides que ejecute npm run dev en segundo plano, que abra un túnel cloudflared o que enganche un watcher de archivos; el comando retorna y el proceso sigue vivo. Cuando la session termina y su terminal desaparece, Linux reasigna el superviviente al PID 1 — eso es lo que significa ppid == 1, y es la firma de “huérfano” más fiable que existe.

El costo es concreto: puertos ocupados, RAM consumida, un OOM a las 3 de la mañana y, la próxima vez que arrancas el agente, un servicio propio al que no puede llegar. Peor aún es el costo de clasificar el problema: no tienes ni idea de quién lanzó ese python app.py.

Juzga con pruebas, no con corazonadas

agent-janitor clasifica los procesos en lugar de disparar contra cualquier cosa sospechosa:

Prueba Confianza
Reasignado a init (ppid=1) y retiene un puerto / rasgos de dev server / TTL superado confirmado
Solo reasignado a init (ppid=1) probable
Rasgos de dev y TTL superado, pero el padre sigue vivo posible (nunca se limpia solo)

Todo lo que lee es local: ppid y líneas de comando desde /proc, sockets en escucha desde /proc/net/tcp{,6}, y un umbral de antigüedad. Solo los confirmed y los likely se pueden matar.

Dos clases quedan exentas por nombre: los daemons del sistema (multipathd, udisksd, ModemManager, systemd-* y compañía) y los agentes de gestión de host y VM (tailscaled, agentes de proveedores cloud, amazon-ssm-agent, walinuxagent, google_guest_agent). Tú añades tu propia infraestructura a una whitelist:

janitor whitelist add ngrok
janitor whitelist add cloudflared
janitor whitelist add tmux

Las entradas de la whitelist son subcadenas que se comparan con la línea de comando (distingue mayúsculas, se escapan las regex, sin inyección). Un array vacío o un archivo ausente significa que no hay nada en la whitelist.

Instala y escanea antes de limpiar

Vive en el catálogo curado (agent-janitor 0.3.1, nivel comunitario, requiere Hermes >= 0.21.6). Instálalo por nombre si tu catálogo lo lista; si no, desde el repositorio como muestra el README:

git clone https://github.com/ZidoCode/agent-janitor ~/.hermes/plugins/agent-janitor
hermes plugins enable agent-janitor

Ahora solo escanea; no limpies nada todavía:

python3 ~/.hermes/plugins/agent-janitor/janitor_cli.py scan
python3 ~/.hermes/plugins/agent-janitor/janitor_cli.py sweep --ttl-h 6

Ese segundo comando suena alarmante, pero por defecto es un dry run: las decisiones se imprimen y se registran como would_kill, nada muere.

Un solo archivo habilita la matanza real — <hermes-home>/plugin-data/agent-janitor/policy_mode, cuyo contenido debe ser exactamente enforce:

echo enforce > ~/.hermes/plugin-data/agent-janitor/policy_mode

Ese diseño vale la pena copiarlo: seguro por defecto, y el estado peligroso exige una decisión escrita.

Antes de activar enforce, haz estas cinco cosas

  1. Llena la whitelist primero. Tu túnel, proxy inverso, tmux, cualquier cosa “lanzada por el agente pero mantenida a propósito” entra con janitor whitelist add.
  2. Lee la salida del dry run como la lista de bajas. Eso es exactamente sobre lo que actuará enforce, ni más ni menos.
  3. Sabe a quién alcanza. La regla es “reasignado al PID 1 hace más de un minuto, no exento, no en whitelist” — no comprueba quién arrancó el proceso. Un hermes gateway lanzado con nohup o un servidor tmux entra en el objetivo en modo enforce si no lo pusiste en la whitelist, y al ejecutarse como root eso incluye procesos de otros usuarios.
  4. Sabe qué deja atrás. spawns.jsonl registra los comandos de terminal que parecían iniciar trabajo en segundo plano, y sweeps.jsonl registra cada decisión (would_kill o ejecutado), así que puedes auditar la limpieza después.
  5. Sabe el orden de la matanza. SIGTERM, espera dos segundos, SIGKILL — y antes del kill -9 vuelve a leer la línea de comando y aborta si el PID fue reciclado. Sin fuego amigo por reutilización de PID.

Combínalo con el ledger para saber de quién es cada proceso

Por sí solo, janitor solo puede decir “este proceso es un huérfano”. Con agent-ledger, del mismo autor, instalado, lee el ledger y convierte un python app.py anónimo en “lanzado por esta llamada de herramienta concreta en el puerto 8080, hace 12 días”. La atribución es difusa — compara por el nombre base del ejecutable — así que tómala como pista, no como prueba. El propio autor la etiqueta igual.

Tres límites que tienes que conocer

  • Solo Linux, porque lee /proc. macOS y Windows no están soportados, así que en un Mac esta herramienta va en tus servidores, no en tu portátil.
  • La suspensión del portátil infla el uptime. El uptime es relativo al arranque y sigue contando mientras la máquina duerme, así que puede haber falsos positivos de TTL en equipos que cierran la tapa. En un servidor — el objetivo principal — esto no aplica.
  • La whitelist compara subcadenas de la línea de comando, así que un shell en whitelist que luego lanza algo peligroso es invisible para ella.

Para reducir en general el problema de “no sé qué hizo mi agente”: janitor cubre la dimensión de procesos, el fingerprinting de skills cubre la de skills y el cron doctor cubre los trabajos programados. Para la rutina general de instalar y fijar versiones, consulta la guía del catálogo de plugins.