Hermes 突然"失忆" 798 条消息?——崩溃重启后的会话错位事故,已修复


周一早上,你打开 Telegram,想继续昨晚和 Hermes 聊到一半的工作。它却像失忆了一样,开始寒暄一个三天前就该结束的话题:“那个 PR 后来合并了吗?”

这不是模型犯迷糊,也不是你的错觉。这是 Hermes Agent 主理人 Teknium 自己在 2026 年 8 月初踩到的真实事故:他的 Telegram 私聊在一次崩溃重启后,悄悄“回到”了 2.7 天前的旧上下文,期间积累的 798 条真实对话全部“消失”了。数据没有丢——但 Hermes 找不到了。

事故还原:一条消息如何让会话“穿越”

先看官方 issue(#82616)里贴出的 production 数据库证据,整个事故分四步:

时间 发生了什么
8月6日 16:18 一条入站消息触发恢复路径,Hermes 悄悄创建了一个没有身份的新会话行
8月6日 → 8月8日 798 条真实消息全部写进这个孤儿行;内存里的映射让对话看起来一切正常,分裂完全不可见
8月8日 17:03 Gateway 崩溃后重启,内存中的“聊天 → 会话”映射被当作陈旧数据丢弃
8月9日 09:40 下一条消息到达:Hermes 按数据库里的会话 key 寻找会话,唯一带 key 的是 8月3日的陈旧会话——于是它恢复了 3 天前的上下文,开始闲聊一个早已合并的 PR

数据库里的证据长这样:一行是 8月3日创建的“正主”(带着完整的会话 key,但 ended_at 为空、再没有新消息),另一行是 8月6日创建的孤儿(session_keychat_id 全是 NULL,但 798 条消息都挂在它下面)。

通俗地说:Hermes 在后台悄悄“分裂”出了一个没有名牌的会话副本,之后所有消息都写进了副本;一次重启弄丢了内存里的寻址表,Hermes 再找会话时只认得带着名牌的“正主”——于是它回到了过去。

根因:三个故障叠在一起,而且全部“静默”

深度排查(issue 评论区有完整代码级分析)把根因收敛为一条链条:

  1. 会话轮换时数据库写入失败,且失败被吞掉。Hermes 的空闲重置(auto-reset)会结束旧会话、创建新会话——这两个数据库写入都失败了:一个只记在 debug 日志,另一个干脆只是 print 到控制台。没有任何人发现。
  2. 路由表却已经切到新会话。于是旧会话变成“僵尸”(没被标记结束、还占着会话 key),新会话在磁盘上根本不存在。
  3. 一个“顺手”的写入把幽灵行物化了出来。之后某个后台写操作(比如统计 token 用量)发现目标行不存在,就随手创建了一行——但身份字段(session_key、chat_id 等)全是空的。孤儿会话从此诞生,而且没有任何代码会再回头补上身份
  4. 崩溃重启后,解析逻辑看不见孤儿。它按会话 key 找、按聊天信息找——孤儿行两项都查不到,唯一能匹配上的就是那个僵尸“正主”。恢复出的自然是旧上下文。

有个耐人寻味的插曲:日志里反复出现 possible FTS write corruption(可能的全文索引损坏)、disk=0(磁盘读回 0 行),最初大家都怀疑是数据库损坏。但调查证明读路径根本不查全文索引——真正的原因是“会话 ID 路由”错了:写入方跟着重定向映射走,读取方却按旧 ID 查询,自然读到 0 行。数据库从头到尾是健康的,这也解释了为什么数据一条都没丢。

数据没丢:798 条消息躺在“找不到”的行里

这是整个事故里最值得安慰的一点:798 条消息完整地存在,只是所在的行没有身份标签,恢复逻辑看不见它。对已中招的用户,官方在 issue 里给出了手动找回步骤(见下文)。

修复:预防 + 治疗,2026-08-09 已合入 main

修复拆成两个 PR,一个管“不再发生”,一个管“已经发生的怎么办”:

#82633 —— 写入侧加固(预防)

  • 身份原子写入:会话的 key、聊天 ID、来源信息在创建行的同一时刻写入(INSERT 直接带上,ON CONFLICT 用 COALESCE 回填),不再依赖事后补写的 UPDATE。身份和行从此同生共死。
  • 每次刷新都是自愈机会:gateway 每轮对话都会刷新会话信息,现在如果发现行缺失,会直接插入完整身份——顺手就把损坏修了。
  • 按最近活动时间解析:崩溃重启后找会话时,优先带消息的行、按 last_activity_at 排序,绝不新铸一个 ID(有带 key 的行存在时)。
  • 错误不再沉默:结束/创建会话的写入失败从 debug 级别提升到 WARNING 并说明路由后果;读取转录的异常也不再静默返回空列表。
  • 顺带修复了 #12857:会话重置时丢失父会话 ID(谱系)的老问题。

验证方式很硬核:11 个回归测试在未修复的 main 上跑出 6 个失败,修复后全部通过;还用真实数据库复现了事故(3 天僵尸会话 + 798 条消息的孤儿行),修复后解析直接回到真实会话。

#82712 —— hermes sessions repair-routing(治疗)

新命令专门“捞”已经中招的孤儿会话,设计原则是宁可不动,不可错认

hermes sessions repair-routing              # 先干跑:报告发现了什么,不改任何数据
hermes sessions repair-routing --apply      # 确认后真正修复
  • 检测:扫描没有会话 key、但带真实消息的网关会话行(分支、委托、工具类的行天然不带 key,直接排除,不误伤)。
  • 证据分级:只有当证据无歧义时才动手——要么有明确的谱系(parent_session_id 指向同一个来源的带 key 行),要么在孤儿创建前后 15 分钟内只有一个同来源的会话静默结束。两个候选前驱、或者两个孤儿抢一个前驱,都拒绝修复并说明原因——猜错了会把 A 的对话拼到 B 的聊天里,所以它拒绝猜测。
  • 修复动作:把前驱的身份用 COALESCE 盖章到孤儿行(绝不覆盖已有值)、记录谱系,然后用 end_reason='superseded_by_repair' 退役前驱——刻意不用普通的重置原因,防止下次重启又飘回去。

你中招了吗?

先别慌,对照特征自查:

  • 受影响的是 gateway 会话:Telegram、Discord、Slack、WhatsApp 等消息平台接入的会话。纯 CLI 用户结构性免疫——CLI 没有这套会话路由机制。
  • 高危人群:长时间运行的会话、经历过 gateway 重启/更新/崩溃、多模态消息较多的用户。
  • 这不是一次性事件:同一台安装从 6 月以来已发现 5 起孤儿会话事故(42、34、5、798、2 条消息),只是这次规模最大、被主理人撞上并公开了。
  • 典型症状:gateway 重启后 Hermes 开始聊旧话题、不记得最近几天的对话、提到早已结束的事情。

更新与数据找回

重要:修复已在 main 分支(2026-08-09 合入),但还没有进入任何发布版本——目前最新发布仍是 v0.20.0(2026-08-03)。长期用户建议更新,两条路:

  1. 等下一个 release,或
  2. 从 main 分支运行,立即可用(见安装指南更新命令参考)。

已经中招、想找回对话

  • 首选:更新到含修复的版本后运行 hermes sessions repair-routing(先干跑确认,再 --apply)。
  • 手动方案(工具不可用时的应急步骤,出自官方 issue):
    1. 停止 gateway;
    2. 先备份cp state.db state.db.bak
    3. 把陈旧“正主”行的 session_keychat_idchat_typeorigin_json 等字段复制到孤儿行(只补空值,不覆盖已有内容);
    4. 给陈旧行设置 ended_atend_reason='superseded_by_repair'
    5. 重启 gateway,让解析逻辑重新找会话。

任何手动操作前请务必备份 state.db,并在动手前确认 gateway 已完全停止。拿不准就等修复版发布后用 repair-routing,它比手改更安全。

这件事告诉我们什么

这次事故最值得警惕的不是 bug 本身,而是它的静默性:会话分裂在后台悄悄发生,内存映射维持着“一切正常”的假象,直到一次重启才暴露。它提醒所有长期运行 AI 助理的用户两件事:一是定期重启/更新后检查会话是否连贯,二是好消息是 Hermes 的数据从不轻易丢——会话找不到了,往往只是“路由错了”,而不是“内容没了”。

想深入了解 Hermes 的会话管理,可以看看hermes sessions 命令参考;关于最近一次大版本发布,见 v0.20.0 发布记录