删掉的会话又回来了:两个 bug 和一道新守卫


你在桌面端删掉一个已经跑歪的会话,顺手刷新列表——它又回来了。或者在命令行里把一条会话删了,之后这个会话的每一轮回复都不再进数据库:agent 照常回答,历史里却什么都没有,唯一的痕迹是日志里一行 Session DB append_message failed: FOREIGN KEY constraint failed。这两件事都是真的,来自同一批被修掉的问题。

2026-09-27 合并的 PR #125157 改的是行为本身:在会话被一个活跃回合持有时,删除会被拒绝,而不是被悄悄撤销。

先把两个真事儿说清楚

第一条在 issue #123583 里记录得很完整:会话行从 store 里消失,但持有它的 agent 对象还活着。它内部那个 _session_db_created 布尔量仍然认为是“行已经建好了”,于是刷新只调用一次 append_message,然后被外键拒绝——失败只写一条 WARNING,没有重试,也没有任何事后可观测的残留(回滚就是回滚)。每一轮都这样,聊天界面看起来一切正常,你的历史却停止生长。

第二条是它的修复(PR #124117,来自 #123641 的挽救工作):确认行真的不在了之后,下一次 flush 会用同一个 id 重建会话,并重放整份 transcript 前缀,而不是只补新一轮。第一版修复只存下了新一轮,因为历史里的那些 dict 看起来“已经保存过”——这类细节在事后看来总是很明显。这条修复还专门强调:只有“确认缺失”的行才会被重建,其他写失败不会触发它。

新守卫:删除被拒绝,而不是被撤销

入口 会话被活跃回合持有 空闲 / 已因压缩结束
CLI sessions delete、export --delete-after-verified 拒绝,退出码 1,提示 Cannot delete active session 正常删除
桌面端单个删除 409 正常删除
桌面端批量删除 跳过活跃项,在 skipped_active 里报告,一条 toast 说明跳过与删除的数量 正常删除
API server DELETE 409 正常删除
TUI session.delete 4023(既有的“不能删除活跃会话”错误码) 正常删除
sessions prune 活跃行留着(行为不变) 正常清理

几个值得注意的实现细节:

  • 守卫是在删除自己的写事务里检查的(复用 prune 已经在用的 _write_guards_reject / _guarded_ids 路径),所以“检查通过”和“真的删掉”之间没有窗口。
  • 如果根会话本身空闲、但它有一个还持有租约的 delegate 子会话,删除同样会被拒绝——因为这次删除会级联到那个子会话。
  • 已经结束(例如被压缩收尾)且没有回合持有的会话,仍然可以正常删除。
  • 批量删除只做一次批量守卫检查,只有真的碰到被守卫的行才会退回逐个检查。

你实际会看到什么

hermes sessions list                                  # 先找到目标会话
hermes sessions delete 20260928_101500_ab12cd        # 活跃中 -> Cannot delete active session,退出码 1
hermes sessions export 20260928_101500_ab12cd --delete-after-verified   # 先导出并校验,再删
hermes sessions prune --older-than 30d               # 活跃行一律跳过

在 TUI 里删活跃会话会拿到 4023,在桌面端或 API 上是 409。最省事的做法是等这一轮结束,或者先 /stop 掉那个回合,再删。

那“删了又回来”还有救吗

有。重建修复(#124117)负责“行没了但对话还在”的情形:下一次写成功时,会话会带着完整的历史前缀回来,所以你不会因为中间删除而丢掉后面几轮内容。守卫(#125157)负责另一侧:让“删除”这件事要么真的发生,要么明确告诉你原因,不再出现“界面上删掉了、后端还在写”的中间态。

如果你怀疑自己的库里已经有这类残留,先看一眼会话统计与状态,再决定要不要动手:会话边界讲的是会话在什么时候被切开、什么时候结束,state.db 那篇讲的是数据库层面的自救手段;命令行细节都在本站的 hermes sessions 参考里。

发布状态

这三个改动都合并于 2026-09-27,晚于 v0.21.5 的发行标签(v2026.9.24),目前只在 main:hermes update 就能拿到。

小结

“删了又回来”和“删了但还在写”表面是两件事,本质是同一件事:删除这个动作没有跟正在使用这条会话的回合对齐。现在的处理方式是显式化的——活跃期间拒绝删除并告诉你原因,确实丢失的会话行则在下一次写入时重建。对你来说,日常习惯只需要改一点点:删会话之前,先确认那个回合已经结束。