T3 Code 源码拆解
核心运行时

08 · Git 检查点、Worktree 与回退

如何用临时 index 和隐藏 refs 捕获 Agent 变更,同时不破坏用户暂存区。

问题比 git commit 更难

Agent 开始前和完成后都需要一个可比较、可恢复的工作区状态。但直接运行 git add -A && git commit 会:

  • 修改用户真实 index,把未准备提交的文件变成 staged;
  • 改变当前分支和 HEAD;
  • 触发 hooks 或签名配置;
  • 把产品内部 checkpoint 混入用户历史;
  • 在多 worktree 仓库中碰错 git dir。

T3 Code 的目标不是替用户提交,而是创建旁路的不可见快照。

抽象边界

CheckpointStore.ts 不直接写 Git 命令,而是委托 VCS driver 的可选 checkpoint capability。这样非 Git 项目仍可运行,只是没有回退能力; 未来其他 VCS 也可以提供自己的实现。

CheckpointReactor 将业务时机与 VCS 细节分开:它知道 turn、baseline、completed、revert 和 diff, Git driver 只知道如何捕获/恢复一个工作目录。

隐藏 ref 命名

Git 实现使用类似下面的 ref:

refs/t3/checkpoints/<base64url(threadId)>/turn/<n>

Thread ID 先做 base64url 编码,避免任意字符破坏 ref 语法;turn number 形成稳定顺序。ref 不出现在普通 branch 列表, 但仍是 Git 可达对象,垃圾回收不会立即删除 commit/tree。

捕获算法:临时 index 是关键

GitVcsDriver.ts 的核心流程可抽象为:

GIT_INDEX_FILE=<temporary-index> git read-tree HEAD   # 有 HEAD 时
GIT_INDEX_FILE=<temporary-index> git add -A
tree=$(GIT_INDEX_FILE=<temporary-index> git write-tree)
commit=$(printf <message> | git commit-tree "$tree" -p <parent>)
git update-ref <hidden-ref> "$commit"

实现并不是原样 shell 脚本,但 Git plumbing 意义相同:

  1. 在 git common dir 附近创建隔离 index;
  2. 从 HEAD 初始化基线(空仓库则处理无 HEAD);
  3. 将当前工作树的 tracked/untracked 变化写入临时 index;
  4. write-tree 创建树对象;
  5. commit-tree 创建不切换分支的 commit;
  6. update-ref 原子移动 T3 隐藏 ref;
  7. 删除临时 index。
检查点不会触碰用户 index
untouched unchanged Working Tree Temporary GIT_INDEX_FILE HEAD tree git write-tree git commit-tree refs/t3/checkpoints/... User Index Current Branch / HEAD

这是整套设计最值得迁移的技巧:Git index 是可替换的文件,不必为了得到 tree 而修改用户当前暂存状态。

为什么使用 common dir

Git worktree 有工作树自己的 git dir,也有共享 object database/ref 的 common dir。临时 index 与隐藏 ref 必须在正确的仓库语境中创建, 否则 linked worktree、submodule 或 bare/特殊布局会出现路径错误。GitVcsDriverCore.ts 集中处理 Git 命令和路径解析, 上层 driver 组合 checkpoint、diff 与 source-control 语义。

完成 checkpoint 与 diff

Turn 前的 baseline 和 turn 后的 completed ref 都指向完整树。Diff 可直接在两个 commit/tree 间计算,不依赖工作区此刻是否又被用户修改:

baseline commit --git diff--> completed commit

这使代码审查视图稳定。Diff blob 按需存储,并有约 10 MB上限;超过上限应返回明确的截断/不可用状态, 不能把 WebSocket 与移动端内存作为无限传输通道。

回退算法

恢复 checkpoint 时,Git driver 以目标 commit 为 source,同时更新 worktree 和 staged state,并清理快照之外的 untracked 文件:

git restore --source <checkpoint> --worktree --staged ...
git clean -fd ...

之后还需要把 index 恢复到产品定义的期望状态。这里的风险远高于捕获:git clean -fd 会删除未跟踪文件。 因此回退必须是明确用户动作、严格限定工作目录与 checkpoint,并在业务层显示影响,不能作为自动错误恢复随意执行。

Checkpoint 不是无损撤销所有外部变化

它捕获 Git 可表示的工作树内容,不会回滚数据库、启动的服务、已发送网络请求、被忽略文件或仓库外文件。 UI 应把它称为代码/工作区回退,而不是系统级时间机器。

与 Git worktree 的配合

T3 Code 可以让 thread 在独立 worktree 中运行,从而隔离并行 Agent。Thread 保存 environment/runtime mode 和 worktree 关联, 首次 turn 的标题/branch 生成可能为它命名。Checkpoint ref 仍以 thread/turn 标识,不依赖 branch 名,因此重命名 UI 标题不会破坏历史。

并行 worktree 降低文件冲突,却引入生命周期问题:创建失败、分支已存在、主仓库被移动、thread 删除后的清理。 这些操作应由 Server/VCS 服务集中拥有,客户端只发送意图。

边界情况

情况设计关注点
新仓库没有 HEAD从空树构造 checkpoint,不假设 parent commit
未跟踪文件add -A 纳入快照;回退时目标外文件可能被清理
.gitignore 文件通常不捕获,不能承诺恢复
Linked worktree正确解析 git dir/common dir,避免 ref/object 错位
大二进制变化diff 上限与不可预览状态,tree 本身仍可捕获
捕获中断临时 index 清理,ref 只在完整 commit 后更新
用户同时编辑快照代表命令执行时读取到的树;上层要协调 turn 边界

测试关注点

GitVcsDriver.test.ts 和 core 测试应验证:用户 index 字节/状态不变、HEAD 不动、未跟踪文件进入 checkpoint、空仓库可用、linked worktree 正确、 diff 稳定、回退恢复预期内容、错误后临时文件被释放。

可迁移经验

  • 需要“保存当前工作区”时优先使用 Git plumbing + GIT_INDEX_FILE,不要劫持 porcelain 工作流。
  • 产品内部历史放在命名空间明确的隐藏 refs,避免污染用户 branch/commit log。
  • 捕获与恢复不是对称风险:恢复会覆盖/删除内容,必须提高授权和确认等级。
  • Checkpoint 只覆盖版本控制能表达的副作用;把边界诚实呈现给用户。

On this page