一句话定位
每个 agent 回合前自动 git stash create 拍代码快照,/fork 时按历史点恢复代码的工作流增强插件。
作用(为什么存在)
它解决的是「会话历史」和「代码状态」错位的问题。 你在 pi 里 fork 一个会话到某个历史点继续对话,但工作区的代码还是最新状态------会话回到了过去,代码却没跟上。
git-checkpoint 用 53 行解决这个错位:fork 到哪个历史点,代码就恢复到哪个点的快照。
注意一个容易混淆的点:它不是 「LLM 改坏了代码,撤销一下」的 undo 工具------触发者是 /fork 操作,语义是「新分支的代码起点对齐」,不是「回退开关」。
关键信息
| 项 | 内容 |
|---|---|
| 源码位置 | examples/extensions/git-checkpoint.ts(53 行) |
| 核心 API | on("tool_result") / on("turn_start") / on("session_before_fork") / on("agent_end") + pi.exec + ctx.sessionManager.getLeafEntry() + ctx.ui.select |
| 插件类型 | 事件钩子型(生命周期) |
触发流程 / 数据流
csharp
tool_result(工具结果回传)
→ getLeafEntry() → currentEntryId = 本轮树尖 entry ← 记落点
turn_start(下一轮开始前)
→ git stash create → 快照 ref → checkpoints.set(entryId, ref) ← 拍快照
session_before_fork(用户 /fork 时)
→ checkpoints.get(event.entryId) → 命中则确认恢复 ← 按坐标取快照
agent_end(整轮 run 结束)
→ checkpoints.clear() ← 清理
核心设计:用 pi 会话树的 entryId 当 key,把「树上的位置」和「git 里的快照」绑定 。tool_result 记落点,turn_start 用上一个落点建快照,session_before_fork 按 fork 起点查快照。
架构 / 流程
关键代码解读
typescript
pi.on("turn_start", async () => {
// ① 在 LLM 改代码之前,git stash create 拍快照
// create 只创建对象,不写 ref、不改工作区(区别于 git stash 入栈)
const { stdout } = await pi.exec("git", ["stash", "create"]);
const ref = stdout.trim();
if (ref && currentEntryId) { // ② 有脏文件 且 有落点,才存
checkpoints.set(currentEntryId, ref);
}
});
pi.on("session_before_fork", async (event, ctx) => {
const ref = checkpoints.get(event.entryId); // ③ 按 fork 起点坐标查快照
if (!ref) return; // ④ 无快照 → 静默跳过
if (!ctx.hasUI) return; // ⑤ 无 UI → 保守不恢复
const choice = await ctx.ui.select("Restore code state?", [
"Yes, restore code to that point",
"No, keep current code",
]);
if (choice?.startsWith("Yes")) {
await pi.exec("git", ["stash", "apply", ref]); // ⑥ 参数数组,防注入
ctx.ui.notify("Code restored to checkpoint", "info");
}
});
亮点 / 踩坑
亮点 1:事件是「时机」,不是只能「拦截」。 session_before_fork 返回 undefined(不拦 fork),只利用「fork 前」这一刻挂自己的逻辑。对比上期的 permission-gate:两者都是可拦截的 before 型事件 ------tool_call 返回 {block:true} 拦截,session_before_fork 返回 undefined 挂副逻辑。事件不同,但「拦截型事件既能拦也能挂逻辑」的用法模式相同。
亮点 2:git stash create 不污染 git history。 它创建的是 dangling 对象(无 ref 指向),不会进 HEAD 历史、不会被 push。代价是对象躺在 .git 里等 gc 回收(默认 2 周 prune)------轻量且安全。
踩坑提示:检查点只覆盖「未提交(dirty)」变更。 已 commit 的代码不受管理;工作区干净时 stash create 返回空串,直接跳过。
边界(Limitations)
| 边界 | 表现 |
|---|---|
| 进程级 | checkpoints Map 是纯内存,重启进程即失效 |
| 机器级 | dangling 对象不推送,换机器 clone 后 fork 不恢复代码 |
| 覆盖范围 | 只对 dirty 变更生效 |
| 首次 turn | 第一个 turn_start 时 currentEntryId 为 undefined,第一轮无检查点 |
| 粒度 | currentEntryId 只在 tool_result 更新,纯对话轮不产生检查点 |
场景(Scenarios)
能恢复:同一进程内 fork 到「工具调用后的历史点」→ 命中 Map → 确认后恢复。
静默跳过 (不报错、不恢复):fork 到第一轮之前、干净工作区、无 UI(CI/headless)、agent_end 之后、重启/换机器。
可借鉴的模式
- 状态生命周期配对 :
turn_start建 →agent_end清。内存状态必须有清理时机(类比 ReactuseEffectsetup/cleanup)。 - 事件即时机 :拦截型事件返回
undefined也能挂副逻辑。 - 命令参数化防注入 :
pi.exec("git", ["stash", "apply", ref])用数组而非字符串拼接。 - 快照-恢复模式:改前存快照 → 关联坐标 → 确认恢复 → 清理。可复用到任何「Agent 改工作区」的插件(自动备份/工作区保护)。
- 无 UI 保守降级 :
!ctx.hasUI时不擅自动作。
一句话总结
53 行把「会话树的坐标」和「git 快照」绑定起来,让 fork 具备代码恢复能力------快照-恢复模式的最小范本。