Skip to content

修正 checkpoint run baseline 与 restore 同步链路,避免漂移后 diff/rollback 基线错误 #593

@Yumiue

Description

@Yumiue

新增字段说明

CheckpointDiffResult.prev_checkpoint_id

  • 所属位置:
    • 后端:internal/runtime/checkpoint_restore.go
    • Gateway/TUI contract:internal/gateway/contracts.gointernal/tui/services/runtime_contract.go
    • 前端协议:web/src/api/protocol.ts
  • 含义:
    • 本次 diff 的前置基线 checkpoint_id。
    • scope=run 下,它表示当前 run 开始前的权威 rollback baseline。
  • 为什么需要:
    • checkpoint_id 表示 diff 的目标 checkpoint,不能表示“应该回退到哪里”。
    • 前端 rollback 需要知道“本轮改动开始前”的 checkpoint,而不是“本轮结束后”的 checkpoint。
  • 使用方式:
    • 前端文件变更项的 checkpoint_id 应优先绑定到 prev_checkpoint_id
    • 如果缺失,再使用已有文件项上的 checkpoint 或后端 warning 提示用户基线不完整。

CheckpointDiffResult.workspace_drifted

  • 所属位置:
    • 后端:internal/runtime/checkpoint_restore.go
    • Gateway/TUI contract:internal/gateway/contracts.gointernal/tui/services/runtime_contract.go
    • 前端协议:web/src/api/protocol.ts
  • 含义:
    • 当前 run 开始前是否检测到 workspace drift。
  • 为什么需要:
    • 前端和调用方需要区分“普通 run diff”和“基线已因外部改动重锚的 run diff”。
    • 这能解释为什么 prev_checkpoint_id 可能不是上一个普通 end_of_turn checkpoint。
  • 使用方式:
    • 后端在检测到 drift 并创建 rebase checkpoint 后返回 true
    • 前端可配合 warning 展示提示,不直接用它推导文件状态。

CheckpointDiffResult.warning

  • 所属位置:
    • 后端:internal/runtime/checkpoint_restore.go
    • Gateway/TUI contract:internal/gateway/contracts.gointernal/tui/services/runtime_contract.go
    • 前端协议:web/src/api/protocol.ts
  • 含义:
    • run diff 的非致命提示信息。
  • 为什么需要:
    • baseline 缺失、workspace drift、baseline 重锚都不一定要让 diff 请求失败。
    • 但这些情况会影响用户对 diff/rollback 语义的理解,不能静默吞掉。
  • 使用方式:
    • 后端返回可读 warning。
    • 前端收到后展示 toast,例如 Checkpoint warning: ...
    • warning 不参与业务状态判断,只用于提示。

CheckpointReasonPreRunDriftRebase

  • 所属位置:
    • internal/session/checkpoint_types.go
  • 字段值:
    • pre_run_drift_rebase
  • 含义:
    • 表示该 checkpoint 是 run 开始前因为 workspace drift 创建的重锚基线。
  • 为什么需要:
    • 需要把 drift rebase checkpoint 与普通 pre_writeend_of_turnpre_restore_guard 区分开。
    • 后续 diff、restore、调试和测试都可以按 reason 判断 checkpoint 来源。
  • 使用方式:
    • runtime 检测到 drift 后创建该 reason 的 checkpoint。
    • run diff 可将它作为 prev_checkpoint_id

WorkspaceCheckpointState.WorkspaceKey

  • 所属位置:
    • internal/checkpoint/checkpoint_manager.go
  • 含义:
    • workspace 的稳定标识,通常由 workdir 规范化后生成。
  • 为什么需要:
    • 同一个 session 可能切换 run,同一个 workspace 也可能跨 session 复用。
    • 以 workspace 为维度保存状态,才能检测跨 session 的外部改动。
  • 使用方式:
    • 保存和读取 workspace checkpoint state 时作为主键。

WorkspaceCheckpointState.CurrentCheckpointID

  • 所属位置:
    • internal/checkpoint/checkpoint_manager.go
  • 含义:
    • 当前 workspace 对应的权威 checkpoint_id。
  • 为什么需要:
    • run start 如果没有内存 baseline,需要从持久化状态恢复当前代码基线。
    • restore 成功后也需要更新它,避免下一轮 run 继续使用旧基线。
  • 使用方式:
    • run end / restore 后写入。
    • run start drift 检测时读取。

WorkspaceCheckpointState.FingerprintPayload

  • 所属位置:
    • internal/checkpoint/checkpoint_manager.go
  • 含义:
    • 序列化后的 workspace fingerprint。
  • 为什么需要:
    • 需要跨进程、跨 session、重启后继续比较工作区是否 drift。
  • 使用方式:
    • 保存时把 fingerprint JSON 化。
    • 读取时反序列化后与当前扫描结果比较。

WorkspaceCheckpointState.UpdatedAt

  • 所属位置:
    • internal/checkpoint/checkpoint_manager.go
  • 含义:
    • workspace checkpoint state 的更新时间。
  • 为什么需要:
    • 用于调试和后续可能的清理策略。
  • 使用方式:
    • 保存 workspace state 时写入当前时间。

RunCheckpointBaseline.SessionID

  • 所属位置:
    • internal/checkpoint/checkpoint_manager.go
  • 含义:
    • baseline 所属 session。
  • 为什么需要:
    • run_id 需要和 session_id 一起作为唯一定位条件,避免不同 session 的 run_id 冲突。
  • 使用方式:
    • 保存和读取 run baseline 时与 RunID 组成联合主键。

RunCheckpointBaseline.RunID

  • 所属位置:
    • internal/checkpoint/checkpoint_manager.go
  • 含义:
    • baseline 所属 run。
  • 为什么需要:
    • scope=run diff 必须知道当前查询对应哪一次 run。
  • 使用方式:
    • 保存和读取 run baseline 时与 SessionID 组成联合主键。

RunCheckpointBaseline.CheckpointID

  • 所属位置:
    • internal/checkpoint/checkpoint_manager.go
  • 含义:
    • 当前 run 的权威 rollback baseline checkpoint_id。
  • 为什么需要:
    • run 结束后,前端可能异步请求 diff;此时必须仍能恢复准确 baseline。
  • 使用方式:
    • run start 确定 baseline 后写入。
    • checkpoint.diff(scope=run) 查询时读取,并返回为 prev_checkpoint_id

RunCheckpointBaseline.Drifted

  • 所属位置:
    • internal/checkpoint/checkpoint_manager.go
  • 含义:
    • 当前 run 开始前是否检测到 workspace drift。
  • 为什么需要:
    • 异步 diff 查询时,内存里的 drift 标记可能已经清理,需要从 SQLite 恢复。
  • 使用方式:
    • 保存 run baseline 时写入。
    • run diff 查询时读取,并返回为 workspace_drifted

RunCheckpointBaseline.UpdatedAt

  • 所属位置:
    • internal/checkpoint/checkpoint_manager.go
  • 含义:
    • run baseline 的更新时间。
  • 为什么需要:
    • 用于调试和后续可能的清理策略。
  • 使用方式:
    • 保存 run baseline 时写入当前时间。

FileChange.status = "pending"

  • 所属位置:
    • web/src/stores/useUIStore.ts
    • web/src/components/panels/FileChangePanel.tsx
  • 含义:
    • 前端已知道某个文件可能发生变化,但后端还没返回准确 added / modified / deleted。
  • 为什么需要:
    • ToolStart 阶段只能从工具参数知道路径,不能可靠判断文件最终状态。
    • 先显示 pending,后续由 tool_diffbash_side_effect 或 run diff 覆盖为真实状态。
  • 使用方式:
    • 文件写入工具开始时创建 pending 变更项。
    • 后端 diff 事件到达后更新为真实状态。

UIState.isRestoringCheckpoint

  • 所属位置:
    • web/src/stores/useUIStore.ts
  • 含义:
    • 当前前端是否正在等待 checkpoint restore 同步完成。
  • 为什么需要:
    • restore 期间需要禁用 accept / rollback,避免重复请求和状态竞态。
  • 使用方式:
    • 用户确认 rollback 后置为 true
    • restore 事件驱动的 session reload 完成或失败后置为 false

ToolDiffFileEntry.kind

  • 所属位置:
    • web/src/api/protocol.ts
  • 含义:
    • 后端直接声明文件变化类型:addedmodifieddeleted
  • 为什么需要:
    • 旧逻辑主要依赖 was_new 推断,只能区分新增和修改,无法准确覆盖删除或复杂多文件工具。
  • 使用方式:
    • 前端优先使用 kind 作为文件变更状态。
    • kind 缺失时再回退到旧的 was_new 推断。

BashSideEffectPayload.tool_call_id

  • 所属位置:
    • web/src/api/protocol.ts
  • 含义:
    • 产生副作用的 bash tool call ID。
  • 为什么需要:
    • 用于把 bash 文件变化关联回具体工具调用。
  • 使用方式:
    • 前端当前主要用于识别事件归属,后续可用于 UI 关联展示。

BashSideEffectPayload.command

  • 所属位置:
    • web/src/api/protocol.ts
  • 含义:
    • 触发文件副作用的 bash 命令文本。
  • 为什么需要:
    • 方便调试和后续 UI 展示。
  • 使用方式:
    • 可选字段,前端不依赖它推导文件变化。

BashSideEffectPayload.changes

  • 所属位置:
    • web/src/api/protocol.ts
  • 含义:
    • bash 命令造成的文件变化列表。
  • 为什么需要:
    • bash 可以通过脚本、重定向、命令行工具间接修改文件,普通文件写入工具事件捕捉不到。
  • 使用方式:
    • 前端遍历 changes,将文件加入文件变更面板。
    • 每个 change 的 kind 决定 added / modified / deleted 状态。

BashSideEffectPayload.preemptively_captured_paths

  • 所属位置:
    • web/src/api/protocol.ts
  • 含义:
    • 执行 bash 前后端提前捕获快照的路径。
  • 为什么需要:
    • 用于说明哪些路径已经纳入 checkpoint 捕获范围。
  • 使用方式:
    • 当前主要作为诊断信息,前端可暂不展示。

BashSideEffectPayload.uncovered_paths

  • 所属位置:
    • web/src/api/protocol.ts
  • 含义:
    • bash 执行后发现变化,但没有提前捕获快照的路径。
  • 为什么需要:
    • 用于识别 checkpoint 覆盖不足的风险路径。
  • 使用方式:
    • 当前主要作为诊断信息,后续可用于 warning 或调试面板。

为什么

当前 checkpoint 链路在以下场景中基线不稳定:

  1. run 级 diff 缺少权威回退基线

    • scope=run 只聚合当前 run 内的代码 checkpoint。
    • 如果用户在两次 run 之间手动改动工作区,下一次 run 的 diff/rollback 可能仍按旧 checkpoint 推导。
    • 异步 diff 查询发生在 run 结束后时,内存态 baseline 也可能丢失。
  2. 空闲期 workspace drift 没有被显式记录

    • runtime 没有在 run start / run end 持久化工作区 fingerprint。
    • 跨 session 或重启后,同一个 workspace 的外部修改无法被识别。
    • 这会导致“用户手动改动”和“agent 本轮改动”混在一起,影响文件变更面板与 rollback 语义。
  3. restore 后前后端状态不同步

    • 后端 restore 已改变 session 与工作区状态,但前端仍可能保留旧消息、旧 insight、旧文件变更和旧 checkpoint_id。
    • restore/undo 之后下一轮文件首次变更应绑定 restored checkpoint 或 guard checkpoint,但当前事件桥接没有可靠维护这个基线。
  4. 文件变更面板缺少可用的 rollback 操作

    • 前端只能把文件变更标记为 accepted。
    • 用户无法从具体文件变更项直接回退到对应 checkpoint。
    • 生成中或 restore 进行中仍可能触发操作,存在重复调用和竞态风险。

怎么做

1. 持久化 workspace 与 run baseline

  • 新增 workspace fingerprint 持久化能力:
    • 保存 workspace 最新 fingerprint。
    • 读取 workspace 最近 fingerprint。
  • 新增 workspace checkpoint state:
    • 保存 workspace 当前权威 checkpoint_id。
    • 保存对应 fingerprint payload。
  • 新增 run checkpoint baseline:
    • session_id + run_id 保存本次 run 的权威 rollback checkpoint。
    • 保存 drifted 标记,供异步 run diff 查询使用。
  • runtime 在 run start:
    • 扫描当前 workdir fingerprint。
    • 对比上次 run end 或持久化 fingerprint。
    • 如果检测到 drift,创建 pre_run_drift_rebase checkpoint。
    • 将该 checkpoint 作为当前 run 的 baseline。
  • runtime 在 run end / restore 后:
    • 保存最新 workspace fingerprint。
    • 保存当前 workspace checkpoint state。
    • 清理 run 级内存缓存,避免跨 run 污染。

2. 修正 run scope diff 的 baseline 语义

  • checkpoint.diff(scope=run) 优先使用持久化 run baseline。
  • 如果没有持久化 baseline,再回退到 run 开始前最近的可用代码 checkpoint。
  • 返回 prev_checkpoint_idworkspace_driftedwarning
  • 如果 target checkpoint 不属于当前 run,直接返回错误。
  • baseline 缺失时返回 warning,不静默产出含糊 diff。
  • drift rebase 场景返回明确 warning,例如 baseline 已重锚。

3. 修正 restore 的文件范围和 exact state

  • 新增 checkpoint reason:pre_run_drift_rebase
  • restore 时收集目标 checkpoint 之后仍 available 的相关代码 checkpoint。
  • per-edit restore 支持传入 related checkpoint IDs。
  • 有 related scope 时,只恢复 target 到后续 checkpoint 之间发生净变化的路径。
  • 避免 target 是全量 rebase checkpoint 时误覆盖无关文件。
  • RestoreExact 优先使用 ExactFileVersions,确保 guard / rebase 恢复到 checkpoint 精确结束态。
  • restore 成功后更新 workspace checkpoint state,保证下一次 run 的 drift 检测基线正确。

4. 前端事件桥接维护 rollback baseline

  • 文件变更占位状态新增 pending
  • 文件首次触碰时记录 first-touch rollback checkpoint,避免后续 checkpoint 覆盖同一路径的回退基线。
  • run diff 返回 prev_checkpoint_id 时,以它作为 rollback baseline 权威来源。
  • pre_restore_guard 不允许覆盖最新 rollback baseline。
  • restore/undo 事件只影响当前 session。
  • restore/undo 后:
    • 清空旧文件变更。
    • 重载 session 消息。
    • 重置 runtime insight。
    • 将下一轮 first-touch baseline 绑定到 restored checkpoint 或 guard checkpoint。
  • 连续 restore 事件到达时,只应用最新一次 reload 结果。
  • 支持 bash_side_effect 事件进入文件变更面板。
  • 支持 tool_diff.kind,不要只依赖 was_new 推断状态。

5. 文件变更面板增加 rollback 操作

  • 对带 checkpoint_id 的文件变更项展示 Rollback 按钮。
  • 点击 rollback 前展示确认弹窗。
  • 确认后调用 restoreCheckpoint({ session_id, checkpoint_id })
  • 成功后的 UI 刷新由 restore 事件链路驱动。
  • 以下状态禁用 accept / rollback:
    • 当前 session 正在生成。
    • checkpoint restore 正在进行。
    • 文件变更已 accepted / rejected。
    • 文件变更缺少 checkpoint_id
  • restore in-flight 期间避免重复提交。

做好的验收

后端验收

  • 两次 run 之间手动新增、修改、删除文件后,下一次 run 能检测到 workspace drift。
  • 检测到 drift 时会创建 pre_run_drift_rebase checkpoint。
  • drift rebase checkpoint 会成为当前 run 的 prev_checkpoint_id
  • run 结束后再次异步查询 checkpoint.diff(scope=run),仍能从持久化数据读到正确 baseline。
  • checkpoint.diff(scope=run) 在 baseline 缺失时返回 warning。
  • checkpoint.diff(scope=run) 在 target checkpoint 不属于当前 run 时返回错误。
  • restore 到 drift rebase checkpoint 时,只恢复相关变更路径,不覆盖无关用户文件。
  • RestoreExact 能恢复到 checkpoint 的精确结束态。
  • restore 成功后,workspace checkpoint state 会更新为目标 checkpoint。

前端验收

  • 文件写入工具开始时,文件变更面板先显示 pending 状态。
  • tool diff / run diff 到达后,文件状态更新为真实的 added / modified / deleted。
  • run diff 返回 prev_checkpoint_id 时,文件项的 rollback checkpoint 使用该值。
  • 同一文件多次被修改时,rollback checkpoint 保持首次触碰基线,不被后续 checkpoint 覆盖。
  • pre_restore_guard 不会覆盖下一轮 rollback baseline。
  • 收到 restore 事件后:
    • 当前 session 消息会重载。
    • runtime insight 会重置。
    • 文件变更列表会清空。
    • 下一轮文件变更会绑定 restored checkpoint。
  • 收到 undo restore 事件后,下一轮文件变更会绑定 guard checkpoint。
  • 连续 restore 事件只应用最后一次 reload 结果。
  • bash_side_effect 能把 bash 引起的文件变化加入文件变更面板。
  • 文件变更项有 Rollback 按钮,并且必须二次确认。
  • 生成中、restore 中、缺少 checkpoint、已 review 的文件项不能触发 rollback。
  • restore in-flight 时重复点击不会产生重复 restore 请求。

测试验收

  • Go 测试覆盖:
    • workspace fingerprint 持久化。
    • workspace checkpoint state 持久化。
    • run checkpoint baseline 持久化。
    • drift rebase checkpoint 创建。
    • run diff 使用持久化 baseline。
    • run diff baseline 缺失 warning。
    • target checkpoint 跨 run 拒绝。
    • restore 不误碰无关文件。
    • exact state restore。
  • 前端测试覆盖:
    • pending 文件变更占位。
    • run diff baseline 绑定。
    • warning toast。
    • first-touch checkpoint 不被覆盖。
    • bash side-effect。
    • restore/undo 后 session reload。
    • restore 后下一轮 baseline 绑定。
    • 连续 restore 只应用最新结果。
    • FileChangePanel rollback 按钮、禁用态、二次确认、重复提交防护。

Related Files

  • internal/checkpoint/checkpoint_manager.go
  • internal/checkpoint/per_edit_snapshot.go
  • internal/runtime/checkpoint_run_baseline.go
  • internal/runtime/checkpoint_gate.go
  • internal/runtime/checkpoint_restore.go
  • internal/runtime/run.go
  • internal/runtime/runtime.go
  • internal/session/checkpoint_types.go
  • internal/gateway/contracts.go
  • internal/cli/gateway_runtime_bridge.go
  • internal/tui/services/runtime_contract.go
  • web/src/api/protocol.ts
  • web/src/utils/eventBridge.ts
  • web/src/stores/useSessionStore.ts
  • web/src/stores/useUIStore.ts
  • web/src/components/panels/FileChangePanel.tsx

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions