恢复被压缩的会话后,每条消息都念了两遍:摘要“载体”是什么


你压缩完一段长对话,退出,第二天用 --resume 回来。前半段正常,翻到压缩点附近时,某条模型回复开始重复——不是重复一句,而是整段逐字重复两遍。滚动条因此变得很长,你开始怀疑是不是上下文炸了、消息真的存了两份。查一下 state.db 行数,却比你想的少。

这条“念两遍”的 bug 于 2026 年 10 月 20 日修复(PR #142187)。它值得单独讲,因为它暴露了压缩机制里一个平时看不见的结构:摘要载体(summary carrier)。

压缩时,那轮回复被“包”了一次

就地压缩(in-place compaction)不是把旧消息删掉,而是保留一个“受保护的尾巴”:最近那轮对话原样带着,再配上一段压缩摘要,一起包进一个新的行里。这个新行叫载体,它保留原消息的 message_uid,但不保留正文——正文就是“上一轮文本 + 摘要”这套组合。

这个设计本身没错:uid 让“这条消息是谁的”在压缩前后保持一致,回溯、导出、去重都靠它。问题出在显示层。

为什么显示成了两份

从 #140971 起,恢复被压缩的会话时会展示完整历史(这正是用户要的:不要只给我摘要)。显示读取于是返回了两批行:压缩前归档的原始行,以及新的载体行。载体带着同样的 message_uid,但正文里包着摘要——显示层看不出它是“同一段话的容器”,于是照着两行分别渲染。原文出现一次,载体里的那份再出现一次。

官方 TUI 的端到端测试把它量化了:Resume/压缩的 E2E 在 /resume、--resume 和 /compress 三条路径上都报出 200 words rendered more than once。这类断言比截图靠谱得多——它明确说“有 200 个词被渲染了不止一次”。

修复:让载体借用原件在显示层的身份

补丁没有改动存储,而是统一了“显示身份”的计算:hermes_state_messages.py 里的 _dedupe_display_generations 增加了一条规则——摘要载体与它 message_uid 相同的那一行共享显示身份,最新的一份胜出,旧的那份不再单独渲染。

函数自己的注释写得很清楚:把压缩的“代”折叠起来,让每条逻辑消息只出现一次(受保护的尾巴每一代都被复制:角色、内容、时间戳相同,只是 active 标志和 id 不同);优先活的副本,其次最新的。这是所有显示投影共用的同一个定义,而不是每个界面各写一套。

修复刻意收窄了范围:只处理摘要载体。被裁剪的工具输出(pruned tool stub)仍然保留它归档的完整内容可见——测试 test_display_dedupe_pruned_tool_payload 就是钉住这一点的(早先一个不加区分的 uid 匹配把这条打破了)。

配套还有一处:导出和 transcript 列表也一并按同样规则处理(提交 c8aa61b7883),所以 hermes sessions export 出来的记录不会重复。

数据有没有丢

没有。这条 bug 全程只影响“怎么显示”。修复后的验证里,出问题的那个 E2E 用自己留下的 state.db 做了对比:修复前显示读取返回 15 行、含重复;修复后 14 行、无重复。行数变少恰恰说明多余的是一份显示副本,不是原始数据。

一个可用的自查思路:怀疑消息被复制时,比较“数据库里的行数”和“界面上数出来的条数”。如果前者正常而后者翻倍,问题在渲染层;两者都翻倍才是真的写了两份。相关模块的完整测试(tests/hermes_state、gateway 的恢复/恢复类 RPC、tests/tui_gateway)在补丁里报 4328 通过、0 失败。

还留着一个相邻问题

同一个区域还有一条未修复的 issue:压缩把用户消息并入前一条“模型切换通知”时,那条用户消息会从 transcript 里消失。它和上面这条不是同一回事——一个是“多显示一份”,一个是“少显示一条”——修复时也没顺手合并处理。遇到“压缩后某条用户消息不见了”,别拿这篇的结论去套。

想系统了解压缩与上下文预算,可以看压缩如何一次调用完成和压缩提示词重建;会话历史、导出与恢复的命令入口在 hermes sessions 命令参考;如果你的目标是把上下文长期压在预算内,上下文 Token 优化那篇更对路。

现在能用吗

这条修复属于 v0.22.3 的解锁项之一,目前只在 main 上,尚未进入正式发版(线上最新为 v0.22.2)。升级到包含它的版本后,/resume、--resume、/compress 三条路径都应当只显示一次;如果还是重复,先确认你跑的是新版本,再去查显示层。

小结

“摘要载体”是压缩留下的容器:它继承原消息的 uid,正文却是“上一轮 + 摘要”。显示层如果只按 uid 判断身份、却不区分载体,就会把同一段话画两遍。修复的做法是给载体借用原件的显示身份,并且只对载体生效——裁剪过的工具输出照旧保留完整内容。记住判断口径:界面翻倍而库里没翻倍,是显示问题;两边都翻倍,才是数据问题。