描述
/retry 的语义是「撤销上一次失败的交换,并重新发送上一条用户消息」。但实际行为是:
- 界面显示:被撤销的消息从 transcript 消失(看起来只有一条);
- 发给模型的上下文:被撤销的消息仍然存在,且每次重试都会再累积一条——AI 的思维链里会出现"用户连续发了 N 条重复的 xxx";
- 持久化会话:磁盘上的会话记录也没有同步,退出后重新加载会话,被撤销的消息全部回归。
也就是说,/retry 只改了 UI 镜像,没有改变模型真正看到的上下文,也没有改变落盘的会话。
复现步骤
- 新开会话,发送一条消息;
- 手动连续执行
/retry 若干次(例如 10 次);
- 观察 TUI transcript:由于撤销,界面只显示一条消息;
- 观察模型侧的思维链/上下文:出现了多条重复的用户消息("用户连续发了 10 条重复的 xxx");
- 退出 TUI 并重新加载该会话:被撤销的消息全部重新出现。
预期行为
/retry 撤销后,以下三处应保持一致,都回到「重发前」的状态:
- TUI 显示层;
- 下一次模型请求的上下文;
- 持久化的会话记录。
实际行为
只有 TUI 显示层被撤销;模型上下文和持久化记录都保留(甚至累积)了被撤销的消息。
根因
模型请求的消息源是 engine 内部的 session.messages(AppendLog),而不是 TUI 的 app.api_messages。而 /retry、/undo 的撤销逻辑(undo_conversation)只做了:
app.history 的 pop;
app.api_messages(TUI 镜像)的 pop。
它既不通知 engine 截断 session.messages,也不触发会话持久化(journal)的同步。engine 侧也没有任何「消息回滚」的 op 能力。因此在消息源从 TUI 镜像迁移到 engine 内部记录之后,undo/retry 命令没有跟着迁移,形成了"只撤 UI、不撤上下文和磁盘"的缺陷。
修复尝试
- engine 增加消息截断/回滚能力(
session.messages.truncate_to + revision 失效),/undo、/retry 撤销后通知 engine 截断到撤销点;
- 撤销后同步持久化(journal 对齐 + 落盘),避免重进会话时撤销内容复活。
修改后,三者行为都保持了一致。再也不会出现 "用户连续发了很多次 xxx, 非常着急" 的思维链了
环境
- 分支:
0.10.0
- 相关代码:
crates/tui/src/commands/groups/debug/undo.rs(undo_conversation / retry)、crates/tui/src/core/session.rs(session.messages,AppendLog)
描述
/retry的语义是「撤销上一次失败的交换,并重新发送上一条用户消息」。但实际行为是:也就是说,
/retry只改了 UI 镜像,没有改变模型真正看到的上下文,也没有改变落盘的会话。复现步骤
/retry若干次(例如 10 次);预期行为
/retry撤销后,以下三处应保持一致,都回到「重发前」的状态:实际行为
只有 TUI 显示层被撤销;模型上下文和持久化记录都保留(甚至累积)了被撤销的消息。
根因
模型请求的消息源是 engine 内部的
session.messages(AppendLog),而不是 TUI 的app.api_messages。而/retry、/undo的撤销逻辑(undo_conversation)只做了:app.history的 pop;app.api_messages(TUI 镜像)的 pop。它既不通知 engine 截断
session.messages,也不触发会话持久化(journal)的同步。engine 侧也没有任何「消息回滚」的 op 能力。因此在消息源从 TUI 镜像迁移到 engine 内部记录之后,undo/retry命令没有跟着迁移,形成了"只撤 UI、不撤上下文和磁盘"的缺陷。修复尝试
session.messages.truncate_to+ revision 失效),/undo、/retry撤销后通知 engine 截断到撤销点;修改后,三者行为都保持了一致。再也不会出现 "用户连续发了很多次 xxx, 非常着急" 的思维链了
环境
0.10.0crates/tui/src/commands/groups/debug/undo.rs(undo_conversation/retry)、crates/tui/src/core/session.rs(session.messages,AppendLog)