你的磁盘为什么少了 90GB:删掉 worktree 之后还留着的依赖环境


笔记本弹出“磁盘空间不足”的提示时,你第一反应是去翻下载目录和 Docker 镜像。清理完、重启,过了两周又来一次。真凶往往在一个你根本没打开过的目录里:~/.hermes/installs。它不会在活动监视器里冒尖,也不会在 du -sh ~/Downloads 里露出,只是安静地长大。

原因很简单,而且不是 bug:每当你用 hermes -w 开一个 worktree、开一棵评审树、或者拉一个临时 clone 来跑 agent,Hermes 都会为这个目录单独准备一份依赖环境——python 环境、测试环境、PM 运行时,一份约 200MB。问题是这棵树被删掉的时候,那份环境不会被带走。2026 年 10 月 7 日合并的 #133808 把这条遗漏补上了。

那份环境是按路径存的

先说清楚它长什么样。依赖状态放在 <hermes 目录>/installs/<install_key>/,而 install_key 不是项目名,是路径的哈希:

# pm/environments.py
def install_key(project_root: Path) -> str:
    canonical = str(Path(project_root).resolve())
    return hashlib.sha256(canonical.encode("utf-8")).hexdigest()[:16]

也就是说,同一个仓库的不同 worktree,因为路径不同,拿到的是不同的 key、不同的环境。这是刻意的:worktree 的意义就是互不干扰,共享一份依赖环境会把这份隔离吃掉。

代价是这份隔离有寿命。git worktree remove、启动时的清理、hermes worktree prune、甚至你手动 rm -rf 那棵树——这些动作都不会顺带删掉 installs/<key>/。依赖环境是“按 checkout 路径”存在的,树没了,环境还在,而且再也没人会引用它。

PR 里给出的实测数据说明规模:一台开发机连续两周做 worktree 相关的工作之后,~/.hermes/installs 下有 499 个条目、共 92GB,其中 402 个条目(80GB)记录的 checkout 已经不存在(360 个来自 .worktrees/),中位年龄只有 4 天。真正属于存活 checkout 的只有 76 个条目。换句话说,八成以上的空间是“幽灵环境”。

修复:靠一条记录判断孤儿

新的 pm/install_states.py 做的事很朴素:每个成功同步过的状态目录里都有 inputs/.project-root,记着它属于哪个 checkout(由 record_activation_inputs 写入)。逻辑就是拿这个路径去问一句“它还在吗”。

关键在于**“不在”不等于“被删了”**。一个容器共享的数据目录(-v ~/.hermes:/opt/data)、一个没挂载的卷,在宿主进程看来路径同样不存在。所以判定被收紧成“可证明已删除“:只有当那棵树本该在的位置是进程能看到的——数据根目录之下的 clone,或者一个仍然存在的 .worktrees/ 目录里的条目——才认定为孤儿。没有记录文件的旧状态目录一律不碰。

回收这一步同样保守。以下情况一律保留:

  • .install.lock 拿不到锁(有进程正在用);
  • environments/ 下任何一代环境还有活跃租约(正在被读);
  • 连锁都打不开的情况,也按“被别人持有”处理。

这套“拿不准就不删”的取向是对的:误删一份正在用的依赖环境,代价是当前会话直接崩;少删一份,代价只是空间。

现在怎么回收

不需要你做任何迁移。两条路径都会自动回收:

hermes pm gc          # 输出的尾段会多出一项:N install state dir(s) of deleted checkouts
hermes worktree prune # 分支清理之后,顺带回收树已经不在的状态目录

另外,每次启动 Hermes 时的那个 worktree 清理器也会做这件事。所以正常使用下,一棵被删掉的 worktree,它的依赖状态会在下一次 hermes 启动或下一次 pm gc 时消失。启动期的回收是 fail-soft 的(出错只写 debug 日志),不会因为清理失败卡住你的启动。

想先看看自己有多少:

du -sh ~/.hermes/installs           # 总计
ls ~/.hermes/installs | wc -l       # 条目数
hermes worktree list                # 顺便看 .worktrees/ 里每棵树的年龄、大小和处置建议

hermes worktree list 会给出判定和理由,适合在 prune 之前先确认它打算删什么——它从不删有未提交改动、有未推送的唯一提交、或正在使用的树;只有未跟踪文件的草稿树会被归档到 ~/.hermes/archive/worktree-prune/ 再移除。

和之前几个磁盘问题的区别

本站写过几次和磁盘有关的问题,机制各不相同,别搞混:

前两个是防线问题,这次是覆盖范围问题。三种加起来才是 ~/.hermes 空间管理的完整画像,另外一篇把存储压到原来的 78%讲的是数据侧(会话、缓存)的瘦身,可以和这次的依赖侧一起看。

小结

  • ~/.hermes/installs/ 按 checkout 路径的哈希存依赖环境,每个 worktree、临时 clone 一份,约 200MB;
  • 删掉树不会删掉这份环境,pm gc、worktree prune、rm -rf 都不会——这是这次修复的缺口;
  • 现在 hermes pm gc、hermes worktree prune 和启动清理器都会回收可证明已删除的 checkout 留下的状态目录,锁或租约被持有的不动;
  • PR 实测:一台机器 499 个条目 92GB,其中 402 个是孤儿(80GB);清理后 92GB 降到 18GB。

如果你经常开 worktree 或跑一次性 clone,今天值得跑一条 du -sh ~/.hermes/installs。数字比预期大的话,hermes pm gc 就够了。