凌晨三点 OOM 的元凶:Agent 留下的孤儿进程


笔记本风扇又开始狂转,localhost:3000 提示端口被占用,ps aux 里躺着四五个你不认识的 node 和 python 进程。你隐约记得上周让 agent“起个服务试试”,然后会话结束了,服务没结束。这类残留有一个共同点:没人记得它们,也没人给它们记账——agent-janitor 干的就是这件事。

孤儿进程是怎么产生的

Agent 的工具调用很像人在终端里敲命令。你让它在后台跑 npm run dev、开一条 cloudflared 隧道、挂一个文件 watcher,命令返回了,进程留下来了。会话结束、进程被终端释放,Linux 会把它挂到 PID 1 下面收养——这就是 ppid == 1 的含义,也是“孤儿”最可靠的信号。

后果不是抽象的:端口被占、内存被吃、凌晨三点 OOM、下次启动 agent 时连不上自己的服务。更麻烦的是排查成本——你不知道那个 python app.py 是谁起的。

它凭什么判断,而不是凭感觉

agent-janitor 用证据给进程分级,而不是见到可疑就杀:

证据 置信度
被 init 收养(ppid=1)且 占用端口 / 有 dev server 特征 / 超过 TTL confirmed
仅被 init 收养(ppid=1) likely
有 dev 特征且超过 TTL,但父进程还活着 possible(永不自动清理)

判据来源都是本地的:/proc 里的 ppid 和命令行、/proc/net/tcp{,6} 里的监听端口、超时阈值。只有 confirmed 和 likely 两类是可杀的。

两类进程被按名字豁免:系统守护进程(multipathd、udisksd、ModemManager、systemd-* 等),以及主机/虚拟机管理 agent(tailscaled、各云厂商的 agent、amazon-ssm-agent、walinuxagent、google_guest_agent 等)。你还可以自己维护白名单,把真正在跑的基础设施加进去:

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

白名单是子串匹配命令行(区分大小写、正则转义,不会变成注入)。空数组或者没有这个文件,就表示什么都没白名单。

装好之后,先扫一遍别杀

官方目录里有它(agent-janitor 0.3.1,community 档,要求 Hermes >= 0.21.6)。能按名字装就按名字装,否则按 README 从仓库装:

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

然后只做扫描,不要碰清理:

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

第二条命令名字看起来吓人,但默认是 dry-run:它只打印决策并记 would_kill,什么都不杀。

真正允许杀进程的开关只有一个文件——<hermes-home>/plugin-data/agent-janitor/policy_mode,内容必须正好是 enforce:

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

这个设计很值得抄:默认状态是安全的,要冒风险必须显式写下决心。

真杀之前,把这几件事做完

  1. 白名单先加满。你的隧道、反向代理、tmux、任何“agent 起的但你想留着的”服务,先 janitor whitelist add。
  2. 把 dry-run 的输出当 kill list 读一遍。enforce 之后它杀的就是这批进程,不多不少。
  3. 想清楚它会杀谁:它按“ppid=1 且超过一分钟、且不在豁免和白名单里”来判断,不区分是谁起的。也就是说,一个 nohup 起来的 hermes gateway、或者一个 tmux server,只要没有写进白名单,都可能被它在 enforce 模式下杀掉——用 root 跑时甚至包括其他用户的进程。
  4. 知道留下了什么痕迹:spawns.jsonl 记录看起来会起后台任务的终端命令,sweeps.jsonl 记录每次清扫的每个决策(would_kill 或真的杀了),事后可以逐条审计。
  5. 杀的顺序是 SIGTERM → 等 2 秒 → SIGKILL,并且在下 kill -9 之前会重读一次命令行,PID 被回收(复用了)就放弃——避免“杀错人”。

它和 ledger 一起用才有归因

单独跑 janitor 只能告诉你“这个进程是孤儿”。如果装了同一位作者的 agent-ledger,janitor 会读它的账本,把一条匿名的 python app.py 变成“12 天前由这次工具调用起在 8080 上”。归因是模糊匹配(按可执行文件名),所以只能当线索,不能当证据——作者自己也这么标注。

三个必须知道的限制

  • 只支持 Linux,因为它读 /proc。macOS 和 Windows 目前不行——如果你的主力机是 Mac,这个工具只能用在服务器上。
  • 笔记本休眠会让存活时长虚高。uptime 按开机时间算,睡眠期间仍在增长,所以一台经常合盖的机器上更容易出现 TTL 误判;服务器场景不受影响。
  • 白名单是按命令行子串匹配的,一个“被白名单的 shell 又起了危险子进程”这种组合它看不见。

想减少“agent 干了什么我不知道”的问题,janitor 解决的是进程维度;技能维度用指纹校验,定时任务的健康度用 doctor。装插件的通用姿势和版本钉法,见插件目录指南。