hermes update 卡住不动?四个坑今天一次修完

周五晚上,你准备收工前把 Hermes 升到最新版,敲下 hermes update,然后屏幕就停在 Fetching... 上不动了。十分钟后你按了 Ctrl+C,换到另一台机器再试——这次它一秒钟就回你一句 Already up to date!,可重启之后 hermes --version 还是老版本。最难受的是第三种:它明明动了代码,却什么都没报错,你以为已经升好了。
这三种体验背后是四个独立的坑,都在 2026-09-28 合并进 main。它们不影响日常对话,但每一个都会让“升级”这件事在最需要它的时候失灵。下面逐个说清楚:什么情况下会踩到、根因是什么、现在的行为变成了什么样。
一、浅克隆的机器:更新一直卡在 fetch 上
hermes 的安装脚本为了省时间,通常拉一个浅克隆(depth-1,只有最近一次提交的历史)。这种 checkout 本来没问题,直到它落后得太多。
从深度为 1 的克隆里执行一次普通的 git fetch origin main,服务器得把浅边界之后所有被合并进来的侧分支的完整祖先都推过来——这个下载量根本不可能在 300 秒的网络上限内完成。而更讽刺的是,真正的 unshallow 步骤被排在更新成功之后,所以它永远没有机会执行:超时 → 更新失败 → 不 unshallow → 下次还超时。
现在的做法是反过来:在发起受限 fetch 之前,先调用 heal_shallow_history 把历史补齐(只补提交历史,给 900 秒预算,并把目标分支一起带上),这样后面那次有上限的 fetch 才会真正变小。
同一批修复还处理了两个更隐蔽的故障:
- git 2.53+ 的
index-pack --promisor崩溃。 在某些局部对象上,pack-objects会触发内部的BUG()。麻烦的是这个状态留在仓库里,之后每一次 fetch(包括更新检查和更新本身)都会撞上它。修复方式很克制:只在命中这个确切的崩溃特征时,用-c remote.origin.promisor=重试一次,只对本次调用生效,不写进仓库配置。 --filter=tree:0把完整克隆变成了部分克隆。 更新器原本每次抓标签都带这个参数,而git fetch --filter会顺手写下remote.origin.promisor和partialclonefilter两个配置——于是一个好端端的完整克隆被悄悄改造成了部分克隆,之后每次 fetch 都多了一层 promisor 依赖。现在标签抓取会复用克隆自己的过滤器:部分克隆照旧带过滤器,完整克隆就以不过滤的方式抓取。
想确认自己是不是这类机器,两条命令就够:
git rev-parse --is-shallow-repository # true 就是浅克隆
git config --get remote.origin.partialclonefilter
二、它说“已是最新”,但代码确实换了
这个坑更阴:你以为没更新,其实更新已经跑了,只是依赖同步、配置迁移和网关重启全被跳过了。
触发条件是 detached checkout——比如你之前为了回滚执行过 git checkout <tag>。这种情况下更新器先把 HEAD 切到 main,然后再去数 HEAD..origin/main 有多少提交。因为你本地的 main 早就指向 origin/main 了,这个差值就是 0,于是它打印 Already up to date! 并直接退出——而此刻你实际运行的代码刚刚变过。
现在计数的起点改成“真正在跑的那个提交”,不再切换到 main 之后再数。
同一处还修了回滚逻辑:更新后发现语法错误时,原来直接对更新分支执行 git reset --hard。结果是 detached 的用户被丢到 main 上,而一个被临时搁置的功能分支的提交会被写到 main 上。新行为分两种情况:
- detached checkout:
git checkout --detach <sha>,回到更新开始前那个提交; - 搁置的分支:
git checkout <branch>,不碰main;如果该分支被别的 worktree 占着切不回去,就把它的提交以 detached 方式恢复。
原则只有一句:更新失败时,安装目录绝不会被留在坏代码上。
三、公司网络里:SSL_CERT_FILE 传不进 git
如果你在公司内网,证书由运维通过 SSL_CERT_FILE 环境变量下发,那你会看到一种很割裂的现象:版本检查(Python 发起)一切正常,但 git fetch 始终 TLS 失败。原因很直白——git 的 libcurl 不读 SSL_CERT_FILE,它只认 GIT_SSL_CAINFO 或 http.sslCAInfo。
修复方式是把 SSL_CERT_FILE 映射成 GIT_SSL_CAINFO,每次更新或检查时导出一次;而且写进进程环境而不是单条命令的参数,这样部分克隆懒加载对象时触发的临时 fetch 也能拿到。如果你显式设过 GIT_SSL_CAINFO,或者 git 配置里有 http.sslCAInfo,你的设置优先,不会被覆盖。
同一批修复还让 hermes pm install 在内网 npm 镜像后面也能正常工作——公司把 npm 指向内部 registry 时,之前会因为取不到锁文件里的公网 URL 而失败。
自查两条:
echo "$SSL_CERT_FILE"
git config --get http.sslCAInfo
四、被写成“成功”的失败
前面三条是“该做的事没做成”,这一条是“没做成却告诉你做成了”,更容易让人误判。
.env 写入被拒,却打印 ✓ Set。 hermes config set 在保存 provider 凭据时会走到 .env 的写入路径,而这条路径在检测到锁冲突时会“打印一句提示然后返回”。调用方没有检查返回值,于是继续清理 config.yaml 里的镜像项、打印 ✓ Set、以 0 退出——你以为凭据写进去了,其实没有。现在四个多存储写入器都会在最前面调用 require_env_writable,拒绝就直接抛错:命令行以 1 退出,PUT/DELETE /api/env 返回 400 并带上原因,TUI 的 model.disconnect 也会先把所有变量检查一遍。
面板重启失败,却记成 restarted。 Hermes 更新时会接管并重启此前手动启动的 dashboard。原来它把停掉那个进程的 /proc 命令行原样重放,可其中那个启动器路径已经被接管流程改写成 shell 包装脚本,于是子进程一起步就死在 SyntaxError 上。而 Popen 本身是成功的,所以更新器依旧打印 ✓ restarted。现在命令行会基于所有 Hermes 进程都用的安装级启动器重建,并且一秒内退出的子进程会被判定为重启失败,失败的 dashboard 会让整次更新的回执变成 partial 并以 1 退出。
升级前想先看一眼,用这两个只读命令
hermes update --check # 只问一句:有没有新版本
hermes update --plan # 只读计划:安装类型、每个 profile 的进程、会被如何重启
--plan 是这批修复带来的好搭档:它把“这次更新会动到哪些东西”列出来给你看,不改任何文件,在跑着多个 profile 的生产机上可以放心用。其余常用的还有 --no-gateway-restart(只换代码不重启网关)、--keep-stash 和 --backup / --no-backup。
你需要做什么
什么都不用做。这四个补丁已经在 main 上,下次 hermes update 就会带上;如果你的机器正好是浅克隆或公司内网环境,建议这次更新时别急着 Ctrl+C,给它把历史补完的时间。升级完成后用 hermes doctor 过一遍,能确认依赖、配置版本和运行中的进程都对齐了。
如果你对升级流程本身还没理清,可以先看我们整理的 升级流程指南,以及升级时如何保住 profile 里的技能和配置。顺带一提,这次更新还会让上一周写的插件兼容层正式失效——如果你的插件还没改 import,升级完正好收一条提示。初次安装的话,安装指南里有不同系统的完整步骤。