点了下"重新生成",107 条消息没了

你对着刚生成的那条回复不太满意,随手点了「重新生成」。界面刷了一下,看起来一切正常——直到你往上滚,发现四小时之前那一段对话不见了,中间几十条消息像被橡皮擦掉了一样。你没有删任何东西,也没有点过任何“清空”。
这是真实报上来的一个案例:一次“重新生成”,把 430 条消息的会话变成了 323 条——107 条活跃消息被静默归档,而这 107 条里包含四小时前那轮对话之后的全部内容。
430 变成 323
问题出在“这一次重新生成,到底要回退到哪一行”。
在 Desktop 里,重新生成(regenerate)和编辑(edit)都会先算一个“截断计划”:从哪一条开始重做,后面的算作历史。服务端收到这个计划时会做校验,但它当时只拒绝三种情况:
- 锚点过期(错误码 4018);
- 序号漂移(4030);
- 整段会话被截断(4028)。
问题在于:“一个指向很旧的行的、但本身完全有效的 row id 锚点”能通过这全部三类检查。客户端在发起重新生成时,又信任了自己的 parentId,没有额外确认。两者叠加,一次点在消息末尾的重新生成,可以合法地重放 31 号位置的那一轮,然后把后面的一百多条消息归档掉。
在 main 上复现时,430 条消息的会话被切到 310 条(在 ordinal 31 处),中间没有任何拦截。值得说明的是:客户端的根因没有被复现出来,也没有定位到某一个“罪魁祸首”提交——也就是说,这类误伤不止一条路径能触发,所以修复做在了出口处。
现在会先问你
修复的思路不是“更聪明地猜你想回退到哪”,而是在动作发生前把后果摊开给你看。
- 重新生成(
planConfirmedReload)和编辑(planConfirmedEdit)现在共用一个深截断判断(confirmIfDeepCut); - 只要这次回退会丢掉更晚的一个可见用户轮次,就弹出已有的那个破坏性确认对话框(
deepCutConfirmRequest,复用“恢复会话”的措辞,因此没有新增任何界面文案); - 不打扰的情况也明确了:重新生成最后一条、编辑最后一轮、编辑一轮失败的对话,都不会弹窗;
- 你点取消,就什么都不发出去。恢复(restore)保留它自己原来的对话框。
对话框可能开着很久(毕竟你去泡了杯咖啡),所以这期间的变化也处理了:
- 重新生成:如果会话变忙了,或者对话记录变了,就把这份计划丢掉;
- 编辑:用
revalidateEditPlan对着当前的对话记录重新瞄准,这样正在流式的回复长出来的内容也能被算进去;如果被编辑的那一轮移动了,或者来了一个新的更晚的用户轮次,它会停下来给你一个提示; - 两者都会在你切换了会话之后放弃这个动作。
服务端也在数
客户端弹窗是一层保护,服务端则真正开始数清楚这次截断会丢掉什么:_archived_user_turns 会在持久的用户行里统计这次截断覆盖了几轮。细节都按保守方向处理:
- 一个被修复过的 carrier 会统计它合并进来的整个 user;user 区段的每一行(从物理行读);
- 被吸收的展示标记不计入;
- 未知 id 和未盖章的 carrier 自己的那一轮算进去——宁可多算(fail closed),也不要少算;
- 在持久 carrier 的截断上,目标之前的行会保留下来。
如果客户端和服务端对“几轮”的算法不一致,服务端会回一个 4033。客户端收到 4033 时会:
- 立刻回滚乐观状态,因为被拒绝的那次提交什么都没写;
- 然后问你。你同意的话,重新生成或编辑会通过私有的
runReload/runEdit从头上再跑一遍,重读会话、重新校验; - 这次被授权的深截断只覆盖你同意过的那份对话记录。中间只要有变化,就再问一次。
编辑原有的“锚点过期(4018)“重试也收紧了:只有当刷新后的对话记录里”更晚的用户轮次数量“相同时,才带上这次深截断确认。
这一版还只是“警告”
一个重要的现状:这个版本还不拦,只是记下来。
服务端发现一次未确认的、丢掉超过一个用户轮次的截断时,会记一行 ALLOWED (warn-only release),然后照常执行。真正的强制开关是 DEEP_TRUNCATE_ENFORCED,在下一个 release 打开之后,同样的截断会以 4033 被拒绝,由客户端走上面那条“回滚 + 问你 + 重跑”的路。
这样安排的用意是:先用一个版本收集真实的误伤数据,再打开硬拦截,避免一上线就把正常的深回退需求全部堵死。
状态与升级
这个修复(PR #133748,修复 issue #133716,标记 P0)2026-10-11 合并进 main。当前最新标签 v0.21.6(10 月 8 日发布)里还没有它,下一个 release 会带上。
升级之后你会发现:误伤变少了,但偶尔会多出一个确认框。那个确认框就是这次改动的全部意义——它出现的时候,说明你本来会丢掉一些还没读过的对话。
小结
“重新生成”是日常操作里最容易被当成“撤销上一步”的一个按钮,但它实际的含义是“从某一行开始重做,后面的都算历史”。当这两件事被混为一谈,一次误点就能吞掉一整段工作。现在的规则很直白:会丢掉更晚的对话,就先问一句。保护一个会话的代价是一次点击,而丢掉 107 条消息的代价,是你根本不知道丢了什么。
相关阅读:会话恢复与续聊讲了会话中断后怎么接着聊,会话边界指南解释了什么样的操作会被当作“用户边界”,Desktop 的界面隔离则关系到界面里发生了什么、以及被明确告知了什么。