测试完成,环境已清理。以下是完整结论。 ## 回退范围测试结果(在重启后的真实运行进程里实测) **方法说明**:`/rollback` 是人类命令,我没有对应的 model tool 无法直接输入它;所以我注册了一个动态探测插件,在**当前运行进程**里直接驱动 `ctx.rollback.rollbackTo()`,在合成 scratch 会话上跑完整边界矩阵(有效 fork 产生的是真实子会话,会真实落盘)。 ### 1. 加载状态:全部就位 ✅ - `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 亦各追加一条,格式正确) ### 2. 边界矩阵:全部符合设计 ✅ | 探针 | 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. 一个值得注意的发现 每个新会话的**前 3 个事件**是 harness 自动附加的会话策略初始化事件(`permission/preset`、`sandbox/mode`、`approval/policy`)。这意味着 seq 0-2 也是合法的"轮间"回退点——fork 出来的是一个只有策略事件、没有任何 turn 的空历史子会话。这是符合设计的(boundary 只要求不落在 open turn/compaction 内),但和你之前说明书里的"boundary 只能是原始 seq 或 checkpoint"结合起来,边界语义更完整了。 ### 4. 清理 - 探测插件 `rbp-1` 已停止(版本保留,需要复测可随时重启) - 14 个合成会话文件(4 个 scratch + 10 个 fork 子会话)已从 `.dsh/sessions` 删除,只留你的真实会话;live store 里的 fork 子会话会在下次重启后自然消失(已通过重启验证) ## 最终结论 **插件在当前 harness 里完全可用。** 你现在可以直接在 GUI 输入框试:`/rollback`(无参数,看用法和 checkpoint 列表)、`/rollback checkpoint`(回到最近锚点)、`/rollback checkpoint:`、`/rollback `。回滚后子会话 ID 会显示在结果里,但按已知限制,GUI 里激活子时间线的面板还没做。 结合轨迹(trajectory)来设计的话,我的完整方案如下——核心思路:**把回退做成轨迹本身的原生交互,而不是旁边另开一个面板**。理由:checkpoint 锚点、rollback/start|end 标记本来就是会话日志里的事件,它们有精确的 seq 和时间位置,放在轨迹里既有真实锚点,又天然只暴露"合法边界"。 ## 〇、实现前必读:rc.6 运行时 API 核查结果 方案落地前对 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 行),重建后重部署——这是动手前的第一个补丁 | ## 一、轨迹内的三个视觉层 ### 1. 锚点节点(rollback/checkpoint → 可点击回退点) 每个 `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 ` 命令作为高级入口。 ### 2. 交易括号(rollback/start → rollback/end → 子会话入口) `rollback/start…end` 渲染为一个**括起来的 span**(视觉语言与 compaction bracket 对称——compaction 的括号定义在 trajectory 包里已存在,直接复用那套样式和 definition 结构),span 上带一个小徽标:**"衍生出子会话 xxx"**,点击直接跳转/打开子会话。这样"源会话上发生了什么"在轨迹里一目了然:历史没变,只多了一对成对的括号。 ### 3. 谱系条(子会话打开后的"从哪来") 打开子会话时,轨迹顶部显示一条 lineage 面包屑:**"← 从 [源会话] 的 turn N 处派生"**(数据端已经有 `sessionQuery.traceSession` 返回谱系,fork 时 `parentSession` 已记录,不用新造)。子会话的轨迹 = 源历史前缀 + `session/end-seed`,锚点节点原样继承,交互一致。 ## 二、关键交互细节 1. **确认弹窗**(必做):目标 turn、将会派生子会话(源历史不变)、以及诚实提示"**工作区/沙箱文件状态不会回退**"——这是已知限制里最大的用户陷阱,UI 必须在确认时讲清楚。可选:给出"将放弃 turn N 之后共 M 轮内容"的预览。 2. **禁用态**: - 轮次正在运行 → 所有锚点禁用,提示"agent 忙碌中,回退需空闲" - 锚点落在 compaction bracket 内 → 该锚点禁用并说明原因。**注意**:核查后确认 `rollback/checkpoint` 是 `queueMicrotask` 在 `turn/end` 同一宏任务内追加的,而 `compaction/start` 是后续异步维护任务,时序上 checkpoint 必然先落——该场景实际几乎不会发生。此禁用逻辑是低成本防御(保险,而非已知故障模式),保留但不阻塞 MVP。 3. **执行 + 自动激活**:确认 → client 调 `commands.execute`(Remote RPC,复用事务/invariant 全链路)→ 从返回的 `childSessionId`(需先完成"〇"表中的前置补丁)→ `sessions.open(childId)` 切换活跃会话。这一步顺手就把 deferred 的另一半("切换 UI 活跃会话")也交付了——积木都是现成的。 4. **失败反馈**:`RollbackError` 的 code(busy/boundary/persistence…)映射成人类文本显示在锚点旁的内联提示,而不是只丢进结果文本。 5. **源会话保持在侧栏**(增量建议):`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 逻辑)。