凌晨三点 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
这个设计很值得抄:默认状态是安全的,要冒风险必须显式写下决心。
真杀之前,把这几件事做完
- 白名单先加满。你的隧道、反向代理、tmux、任何“agent 起的但你想留着的”服务,先
janitor whitelist add。 - 把 dry-run 的输出当 kill list 读一遍。enforce 之后它杀的就是这批进程,不多不少。
- 想清楚它会杀谁:它按“ppid=1 且超过一分钟、且不在豁免和白名单里”来判断,不区分是谁起的。也就是说,一个
nohup起来的hermes gateway、或者一个 tmux server,只要没有写进白名单,都可能被它在 enforce 模式下杀掉——用 root 跑时甚至包括其他用户的进程。 - 知道留下了什么痕迹:
spawns.jsonl记录看起来会起后台任务的终端命令,sweeps.jsonl记录每次清扫的每个决策(would_kill或真的杀了),事后可以逐条审计。 - 杀的顺序是 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。装插件的通用姿势和版本钉法,见插件目录指南。