排查 Hermes 桌面客户端更新后 backend 反复 exit(0) 的问题
现象
某次 Hermes 桌面客户端(Electron 壳 + Python backend,通过 venv 启动)自动更新之后,客户端一直卡在启动界面,反复重试连接。日志(C:\Users\<user>\AppData\Local\hermes\logs\desktop.log)里刷的都是类似这样的循环:
[boot] Resolving Hermes backend
[boot] Resolving Hermes runtime
[bootstrap] Active Hermes runtime at ...\hermes-agent is usable but the bootstrap marker is missing or stale; skipping first-run bootstrap.
[boot] Hermes runtime is ready
[backend] `serve` supported for Hermes at ...\hermes-agent (venv: ...\hermes-agent\venv)
[boot] Starting Hermes backend via Hermes at ...\hermes-agent (venv: ...\hermes-agent\venv)
[boot] Waiting for Hermes backend to launch
Hermes backend exited (0)
[boot] Hermes backend exited before it became ready (0).
[boot] Desktop boot failed: Hermes backend exited before it became ready (0). Log: ...\desktop.log
reset requested by renderer → 重启连接 → 再次 exited (0),如此循环,偶尔还会触发内部的自愈机制:
[bootstrap] repair requested by renderer; forcing reinstall + clearing latched failure
(attempt=1/3, primaryBackendAlive=false, hardReinstall=false):
repair attempt 1/3: primary backend process has exited; restarting before escalating to reinstall
但一直卡在 attempt=1/3,没有真正走到 hardReinstall。
关键的坑在于:这份日志只是 Electron 主进程转发的子进程退出码,backend 自己真正打印的错误信息完全没有出现。退出码是 0(正常退出,不是崩溃),这基本可以确定是 backend 内部某个检查没通过后主动 exit(0),而不是异常导致的 crash。
第一轮排查:从 desktop.log 里挖真正的报错
拿到完整的 desktop.log(而不是只看渲染层转发出来的片段)之后,用 grep 顺着关键字往回翻,找到了几个有用的信号:
[updates] restart: Updating Hermes — this window will close and the updater will open...
[updates] venv shim unlocked; safe to proceed
[updates] update in progress (update-in-flight); deferring backend start until it finishes
[updates] launched repo hand-off script: ...\hermes-agent\scripts\desktop-update.ps1 (branch main); exiting desktop to release venv shim
[updates] update finished; proceeding with backend start
[updates] detached update finished OK (branch main)
对比更早之前的更新记录,能看到往常走的是标准安装包路径:
[updates] launched updater: ...\hermes-setup.exe --update --branch main; exiting desktop to release venv shim
而这一次触发更新时,走的是一条不同的更新路径——通过 desktop-update.ps1 这个 “repo hand-off” 脚本完成的。而恰好从这次更新之后开始,Hermes backend exited (0) 就从偶发变成了每次必现。
验证假设:手动跑一遍
第一个怀疑:这个 “repo hand-off” 脚本大概率只是把 hermes-agent 的代码仓库更新到了最新版本(类似 git pull),但没有同步跑 pip install,导致 venv 里装的 Python 依赖跟新代码对不上,backend 启动时做依赖/版本校验没通过,直接 exit(0) 退出。
验证方式很简单,激活 venv 手动跑一次 backend,看看报错是否会直接打印出来:
cd C:\Users\<user>\AppData\Local\hermes\hermes-agent
venv\Scripts\activate
pip install -e .
hermes serve
结果:
HERMES_BACKEND_READY port=9119
Hermes backend listening on 127.0.0.1:9119
手动跑完全正常。这证实了确实是这次更新漏掉了依赖同步这一步——pip install -e . 把缺的依赖补上之后,backend 本身是能正常启动监听的。
第二轮排查:修完依赖,桌面客户端还是失败
问题并没有到此结束。补完依赖、确认没有残留进程占用端口(关掉了手动测试用的终端)之后,重新打开桌面客户端——依然是同样的 exited (0) 循环。
这说明”依赖缺失”只是问题的一部分,不是全部。到这一步,能排除的东西已经排除得差不多了:
-
❌ 依赖问题 → 已通过 pip install -e .修复,手动运行验证通过 -
❌ 端口冲突/残留进程 → 已确认手动测试的终端已完全关闭
那么剩下唯一还没解释的差异就是:手动敲命令启动的 backend,和桌面客户端启动的 backend,走的不是完全一样的路径。
日志里其实留了一条线索——backend 向桌面端上报的会话信息里带着一个字段:
"desktop_contract": 2
这提示 Electron 壳程序和 Python backend 之间存在一个版本握手协议。而这次的 “repo hand-off” 更新脚本,只更新了 hermes-agent(Python 代码部分),并没有触碰 Electron 壳程序本身(app.asar、win-unpacked 目录这些,理论上是走 hermes-setup.exe 全量安装包更新的)。
也就是说,更新之后很可能出现了这样的错配:
-
backend 代码:已经是新版本,期望新的 desktop_contract -
Electron 外壳:还停留在旧版本,用旧的协议去启动/校验 backend
桌面壳启动 backend 时会带着这层握手检查,检查不通过就让 backend 主动退出(exit 0,属于”优雅失败”,不是异常);而手动敲 hermes serve 走的是纯 CLI 模式,不经过这层握手,所以能正常跑起来——这也解释了为什么”修完依赖,手动能跑,桌面客户端还是不行”这个看似矛盾的现象。
最终解决
沿着这个思路,最直接的做法不是继续在代码/依赖层面抠细节,而是让壳程序和 backend 版本重新对齐:
-
完全退出 Hermes(包括任务栏后台图标) -
不走程序内的”检查更新”,直接去官方下载页拿最新的完整安装包 -
用安装包做一次全新覆盖安装 -
重新打开客户端
重装之后问题解决。
复盘
这次排查过程里,几个比较有代表性的经验:
-
exit code 0 ≠ 没问题,尤其是在”应该长期运行的服务/守护进程”场景下,正常退出码反而往往意味着程序自己判断了某个前提条件不满足然后主动退出,值得往”内部校验逻辑”方向去想,而不是当成崩溃去查栈。 -
Electron 主进程转发的子进程退出信息,通常不等于子进程自己的报错。真正有效的调试方式是绕开壳程序,直接手动、原样地跑一遍被壳程序调用的那个命令/脚本,报错才会老老实实打印出来。 -
“自动更新之后开始出问题”,第一反应应该是去看更新走了哪条路径,尤其是有多种更新机制并存的系统(比如这里的全量安装包 vs 局部代码同步脚本)。不同更新路径之间如果没有做好版本对齐,很容易出现”代码更新了、外壳没更新”或反过来的错配问题。 -
单独验证某一层(比如手动把依赖装好、backend 能跑)不代表整条链路都通了。修复一个环节后要用同样的触发方式(这里是桌面客户端本身,而不是手动命令行)重新验证,才能确认问题真的解决了,而不是提前把”部分修复”误判成”完全修复”。

