连接还“活着”,回复却再也不来:Hermes 心跳机制让静默断线自动愈合

你合上笔记本去吃了个午饭,回来掀开盖子,Hermes 的界面还好好停在屏幕上,会话记录、光标、一切如常。你打了一句话按回车——然后什么都没有发生。没有报错,没有转圈,没有超时提示,就是一片安静。消息像被扔进了黑洞。
这种“界面还活着,连接其实已经死了”的情况,叫静默断线(silent disconnect)。它不是 Hermes 独有的毛病,而是所有走网络连接的程序的通病:网络断了,但两端都不知道,于是一边傻等,一边干等。最近 Hermes 合并了一组改动,让 TUI 和桌面端能自己发现假死的连接、主动重建——这篇文章讲讲它到底修了什么,以及为什么这对每个远程使用 Hermes 的人都重要。
为什么连接会“假死”
先说个基础概念:Hermes 的界面(终端 TUI 或桌面 App)和真正的“大脑”(跑在服务器或本地后台的 gateway 进程)之间,靠一条 WebSocket 连接通信。你发的每句话、它回的每个字,都从这条连接上走。
问题出在网络本身。假设你的 Mac 合盖休眠了十分钟,期间 Wi-Fi 断了一次,醒来后拿到了新 IP——或者你从办公室的网切到了手机热点,又或者 VPN 重连了一下。这几种情况下,旧的 TCP 连接已经不存在了,但你的电脑并不知道。用行话说,这叫“半开连接”(half-open connection):连接的一头已经消失,另一头还傻傻地认为一切正常。
为什么发现不了?TCP 协议本来有个保活机制(keepalive),但它默认要等很久很久才探测一次,而且浏览器里的 WebSocket 根本不向开发者暴露 ping/pong 接口。结果就是:客户端继续往一条死掉的连接上发数据,发不出去也不报错,只是默默地等——你看到的,就是那条“发进黑洞”的消息。
修复:三管齐下的心跳机制
8 月 24 日合并的 PR #93792(整合了社区开发者 @100yenadmin 的三部分贡献 #89958/#90012/#89984)解决的就是这件事:让每条客户端连接定期“拍一拍”网关,拍不到回音就判定连接已死,然后自动重建。它分三层,每一层解决一个问题。
第一层:服务端学会应答“心跳”
服务端(tui_gateway/ws.py)做了三件小事:一,在启动握手消息 gateway.ready 里广播 heartbeat: true,告诉所有客户端“我支持心跳”;二,新增一个 gateway.ping 方法,在读取循环里内联应答——不需要排队、不需要调度,收到就回;三,记录每条连接的 last_inbound_at(最后收到数据的时间),用于后续诊断。
第二层:客户端定时“拍一拍”,超时判死
TUI 客户端(ui-tui/src/gatewayClient.ts)和桌面端共用的 JsonRpcGatewayClient 采用同一套参数:每 15 秒发一次 gateway.ping,如果 45 秒内没有收到任何来自网关的数据(不只是心跳应答,任何数据都算),就判定这条连接已经死了,主动把它关掉重连。
这里有个容易忽略的细节:为什么 45 秒而不是 15 秒?因为网络往返本身有延迟,而且一次心跳丢包不代表连接断了。45 秒(= 3 个心跳周期)都没听到任何声音,基本可以确定不是网络抖动,而是连接真的没了。
第三层:指数退避重连,迟到的数据直接扔掉
判定连接死亡后,客户端不会立刻疯狂重连(那只会把服务器打爆),而是用指数退避:第一次 1 秒后重试,失败等 2 秒,再失败等 4 秒、8 秒、16 秒……直到 30 秒封顶,一直重试到连上为止。每次尝试还会发布一个 gateway.reconnecting 事件(带尝试次数和等待毫秒数),方便排查。
桌面端还有一个更精细的机制:socket generation 失效。连接重建时,客户端会给新连接发一个新“代号”,旧连接上迟到的任何数据帧——比如断线前网关最后发出的那几条消息——都因为没有“代号”被直接丢弃。这保证了新连接从干净的状态开始,不会拿旧连接的残留数据污染新会话,也不会重复执行已经执行过的工作。
老版本网关不受影响
这套心跳是能力门控的:客户端只有在收到 gateway.ready 里 heartbeat: true 的广播后,才会启用心跳逻辑。如果你连的还是旧版网关(比如远程服务器上的 Hermes 还没升级),客户端就按老行为工作,一切照旧。这意味着这个修复可以放心地随新版客户端推送,不会把老服务器搞出兼容问题。
覆盖了哪些场景
- 合盖睡眠/唤醒:macOS 和 Windows 睡眠唤醒后网络栈重建,最常见的假死来源。之前桌面端在唤醒时只会做一次性探测(那是同一天早些时候合并的 #93694,针对远程网关更新后卡死在死 socket 上的问题),现在则有了持续的心跳兜底。
- 切换网络:Wi-Fi 换热点、网线拔插、办公室网络漫游。
- VPN 重连:VPN 隧道重建后,旧连接的 TCP 包全部走丢。
- 静默的服务器端重启:网关进程重启,但客户端没收到任何关闭通知。
另外服务端还顺手开启了 WebSocket socket 的 TCP keepalive(死对端检测),双保险。
什么时候能用上
诚实地说:这些改动目前只在 main 分支上,还没有进入任何正式发布版本(最新的 v0.20.5 打的是 8 月 19 日的标签,早于这批提交)。也就是说,你现在用 hermes update 更新到最新稳定版,还不会看到心跳日志。等下一个版本发布后升级即可——升级方式可以参考我们的安装与升级指南,也欢迎围观 v0.20.5 的发版说明看看上一版都改了啥。
升级之后怎么确认心跳在工作?在 TUI 里留意 [lifecycle] 开头的日志即可:正常时它安静无声;一旦网络抖动,你会看到 websocket silent drop detected; forcing reconnect 和 scheduling gateway reconnect in Xms 这样的行——这不再是坏事,而是客户端正在自己救自己。
顺带一提,如果你关心的是另一种“卡住”——agent 思考循环卡死(不是连接问题,是 agent 本身不干活),那是另一套机制:网关侧的 loop watchdog,我们之前专门写过一篇《网关循环看门狗调优指南》,两篇对照着看,连接层和运行层的可靠性就都覆盖了。
总结
静默断线是所有远程工具的隐形杀手:没有报错、没有提示,只有你的一脸茫然。Hermes 这次的修复思路很朴素——定时拍一拍,拍不响就重连,重连要克制——但恰恰是这种朴素的机制,把“连接假死”从玄学变成了可检测、可恢复的事件。下次你的网络抖了一下,别再盯着屏幕等黑洞了:客户端自己会想办法。