测试完成,环境已清理。以下是完整结论。
方法说明:/rollback 是人类命令,我没有对应的 model tool 无法直接输入它;所以我注册了一个动态探测插件,在当前运行进程里直接驱动 ctx.rollback.rollbackTo(),在合成 scratch 会话上跑完整边界矩阵(有效 fork 产生的是真实子会话,会真实落盘)。
ctx.rollback已加载(此前 Inspect 服务目录看不到它,是因为那个目录是发布期静态清单,不反映动态安装——运行态探测才是权威)/rollback命令已注册(描述:Fork a child session through a history boundary)- checkpoint 锚点自动记录生效:每个
turn/end后自动追加rollback/checkpoint(turn 0 → seq 4,turn 1 → seq 9;后续真实会话中 turn 10、11 亦各追加一条,格式正确)
| 探针 | boundary | 结果 |
|---|---|---|
| 负数 | -1 | ❌ boundary 拒绝 |
| 超出日志末端 | 21 | ❌ boundary 拒绝 |
| NaN | NaN | ❌ boundary 拒绝 |
| 落在 open turn 内 | 3 / 8 | ❌ boundary 拒绝("ends inside open turn N") |
| 落在 compaction bracket 内 | 6 | ❌ boundary 拒绝 |
| 合法:turn 结束处 | 4 | ✅ fork 出子会话,标记 rollback/start(11)→end(12) |
| 合法:checkpoint 锚点 | 9 | ✅ fork 出子会话,标记 13/14 |
| agent 忙(runMaintenance 抛错) | — | ❌ busy 拒绝 |
| 信号已中止 | — | ❌ 拒绝(reason: probe-abort) |
事务闭合验证通过:每次成功 fork 都在源会话留下成对的 rollback/start→rollback/end,无悬空 bracket。
每个新会话的前 3 个事件是 harness 自动附加的会话策略初始化事件(permission/preset、sandbox/mode、approval/policy)。这意味着 seq 0-2 也是合法的"轮间"回退点——fork 出来的是一个只有策略事件、没有任何 turn 的空历史子会话。这是符合设计的(boundary 只要求不落在 open turn/compaction 内),但和你之前说明书里的"boundary 只能是原始 seq 或 checkpoint"结合起来,边界语义更完整了。
- 探测插件
rbp-1已停止(版本保留,需要复测可随时重启) - 14 个合成会话文件(4 个 scratch + 10 个 fork 子会话)已从
.dsh/sessions删除,只留你的真实会话;live store 里的 fork 子会话会在下次重启后自然消失(已通过重启验证)
插件在当前 harness 里完全可用。 你现在可以直接在 GUI 输入框试:/rollback(无参数,看用法和 checkpoint 列表)、/rollback checkpoint(回到最近锚点)、/rollback checkpoint:<turn>、/rollback <seq>。回滚后子会话 ID 会显示在结果里,但按已知限制,GUI 里激活子时间线的面板还没做。
结合轨迹(trajectory)来设计的话,我的完整方案如下——核心思路:把回退做成轨迹本身的原生交互,而不是旁边另开一个面板。理由:checkpoint 锚点、rollback/start|end 标记本来就是会话日志里的事件,它们有精确的 seq 和时间位置,放在轨迹里既有真实锚点,又天然只暴露"合法边界"。
方案落地前对 rc.6 全局安装做了逐项 API 核查,事实基础如下:
| 方案依赖的能力 | 核查结果 | 说明 |
|---|---|---|
| 轨迹扩展机制 | ✅ ctx.conversationEvents.register(definition) |
轨迹内所有节点的真实接入点,不是 Slot。definition 含 match(按事件类型匹配)/start/update/buildViewNode/publication 字段。trajectory 包现有 8 个定义:assistant、turn-end、compaction 括号、session-end、inbox、message、request-header、tool——rollback 锚点/括号照抄 compaction 定义的模式即可 |
| 执行回滚 | ✅ commands.execute(Remote RPC) |
走完整命令链路(command/run、command/done 事件 + sourceCommandId 记录),复用事务与 invariant 全链路 |
| 切换活跃会话 | ✅ sessions.open(id)(client 侧) |
已有现成先例:sessions.fork({ sessionId, atSeq, increaseTitle }).then(childId => sessions.open(childId)) |
| 谱系数据 | ✅ sessionQuery.traceSession(sessionId) |
公共方法存在;fork 子会话 header 记录 parentSession: 源会话id(dsh-session L1851),谱系无需新造 |
| 结构化 childSessionId | ❌ 缺失,MVP 前置 | commands.execute('rollback') 返回的 CommandResult 只有 { kind, text, sourceEventSeq },childSessionId 被埋在 text 字符串里("new child session xxx")。自动激活需要给 command-rollback 的返回增加 childSessionId 字段(源码约 3 行),重建后重部署——这是动手前的第一个补丁 |
每个 rollback/checkpoint 事件渲染成 turn 之间的一个小节点(圆点/旗帜,带 turn 号和相对时间),位置精确落在它所属的 turn 块尾部——因为锚点本身就是在 turn/end 之后追加的。
实现方式:新插件注册 conversationEvents 定义,match: event.type === 'rollback/checkpoint',buildViewNode 产出锚点节点(与 turn-end 节点同层,落在 turn 块尾)。
- 悬停:显示 "turn N 处锚点 · 回退到此处" 工具条
- 点击:弹确认框(见下),确认后执行回滚
- 已派生标记(增量建议):源会话里被 fork 过的锚点显示"已从这里派生子会话"徽标——数据现成,
rollback/start的boundarySeq恰好指向被使用的锚点 seq,用户一眼知道哪些锚点用过了,避免重复派生 - 这个设计有个关键红利:锚点是"构造上合法"的边界(只在轮间产生),UI 只暴露合法回退点,用户永远不会撞上
boundary错误——手敲 seq 那种易错路径留给/rollback <seq>命令作为高级入口。
rollback/start…end 渲染为一个括起来的 span(视觉语言与 compaction bracket 对称——compaction 的括号定义在 trajectory 包里已存在,直接复用那套样式和 definition 结构),span 上带一个小徽标:"衍生出子会话 xxx",点击直接跳转/打开子会话。这样"源会话上发生了什么"在轨迹里一目了然:历史没变,只多了一对成对的括号。
打开子会话时,轨迹顶部显示一条 lineage 面包屑:"← 从 [源会话] 的 turn N 处派生"(数据端已经有 sessionQuery.traceSession 返回谱系,fork 时 parentSession 已记录,不用新造)。子会话的轨迹 = 源历史前缀 + session/end-seed,锚点节点原样继承,交互一致。
- 确认弹窗(必做):目标 turn、将会派生子会话(源历史不变)、以及诚实提示"工作区/沙箱文件状态不会回退"——这是已知限制里最大的用户陷阱,UI 必须在确认时讲清楚。可选:给出"将放弃 turn N 之后共 M 轮内容"的预览。
- 禁用态:
- 轮次正在运行 → 所有锚点禁用,提示"agent 忙碌中,回退需空闲"
- 锚点落在 compaction bracket 内 → 该锚点禁用并说明原因。注意:核查后确认
rollback/checkpoint是queueMicrotask在turn/end同一宏任务内追加的,而compaction/start是后续异步维护任务,时序上 checkpoint 必然先落——该场景实际几乎不会发生。此禁用逻辑是低成本防御(保险,而非已知故障模式),保留但不阻塞 MVP。
- 执行 + 自动激活:确认 → client 调
commands.execute(Remote RPC,复用事务/invariant 全链路)→ 从返回的childSessionId(需先完成"〇"表中的前置补丁)→sessions.open(childId)切换活跃会话。这一步顺手就把 deferred 的另一半("切换 UI 活跃会话")也交付了——积木都是现成的。 - 失败反馈:
RollbackError的 code(busy/boundary/persistence…)映射成人类文本显示在锚点旁的内联提示,而不是只丢进结果文本。 - 源会话保持在侧栏(增量建议):
sessions.open只切换不替换,源会话仍在会话列表中——天然支持"两边对照",V3 的双侧对比可以更早以弱形式出现。
| 阶段 | 内容 | 依赖 |
|---|---|---|
| 前置补丁 | command-rollback 返回增加 childSessionId 字段,重建重部署 |
源码 3 行 + 重部署 |
| MVP(最小可用) | 锚点节点渲染(conversationEvents 定义)+ 点击确认回滚 + 成功自动打开子会话 + 失败内联提示 + 已派生标记 | 前置补丁 + commands.execute + sessions.open |
| V2 | 交易括号 span + 子会话徽标(源会话视角)+ lineage 面包屑(子会话视角) | rollback/start|end 事件定义、traceSession |
| V3 | 谱系树(多代派生)、双侧对比、工作区快照提示(等 WorkspaceSnapshotter seam) | 更大;快照能力建议等官方 seam,收益后置 |
设计上的两个原则:锚点即合法边界(UI 替用户消掉 boundary 错误类)、与 compaction 渲染对称(复用视觉语言和 definition 逻辑)。