6 min read教程

M10. 会话:JSONL 如何变成可分叉的历史

从 JSONL 父子关系理解会话的追加、回退、分支与恢复。

M10. 会话:JSONL 如何变成可分叉的历史#

如果会话只是一个数组,回退就意味着删除后面的消息;删除会让调试和恢复失去证据。Pi 选择把会话保存成带父子关系的 JSONL 树:回退只移动当前 leaf,从旧节点继续工作就会自然产生分支。

1. 文件里保存什么#

首行是 session header,描述会话 ID、时间、工作目录和可选的父会话;后续每行是一个 entry。entry 有自己的 ID、parentId、时间戳和类型。

text
header
  └─ model_change
      └─ user message
          └─ assistant tool call
              └─ tool result
                  └─ assistant answer

消息只是 entry 的一种。压缩、分支摘要、标签、模型变化和自定义数据也可以拥有自己的类型。这样,恢复过程不需要猜测某一行字符串代表什么。

2. 追加、回退、分支#

追加

新 entry 的 parentId 指向当前 leaf,旧节点不修改。追加是最常见、也最容易保证正确性的操作。

回退

把活动 leaf 移到历史节点。旧分支仍然存在,用户可以查看或再次从那里开始。

分支

回退后追加新消息,新节点就会挂在旧节点下面形成另一条路径。分支不是复制整个文件,而是共享共同祖先。

3. SessionManager 的阅读入口#

官方 SDK 提供的管理操作包括:创建、打开、列出会话;读取 entries、tree、path 和 leaf;追加标签;branch、branchWithSummary,以及从某个 leaf 创建新会话。

ts
import { SessionManager } from "@earendil-works/pi-coding-agent";

const sm = SessionManager.open("/path/to/session.jsonl");
const entries = sm.getEntries();
const path = sm.getPath();
const leaf = sm.getLeafEntry();

if (leaf) sm.appendLabelChange(leaf.id, "checkpoint");

这些方法把树的不变量集中在管理器里。业务代码不应直接编辑 JSONL 里的 parentId 或 leaf 指针。

4. 从树到模型上下文#

模型需要的是当前路径上的可见消息,不是整棵树。构建上下文时,系统从当前 leaf 沿 parentId 回到根,再反转为正序,按 entry 类型处理:普通消息进入消息列表,压缩 entry 注入摘要和保留路径,分支摘要提供语义桥,标签和模型变化更新运行状态。

text
整棵树
  → 当前 leaf 的祖先路径
  → 按 entry 类型投影
  → AgentMessage[]
  → transformContext / convertToLlm

这一步解释了为什么树可以很丰富,而模型上下文仍然是线性的:树负责保存选择空间,路径负责定义本次运行看到的世界。

5. JSONL 的工程优点与限制#

每行一个 entry 便于追加、调试、备份和局部读取;树结构也能保留完整尝试。但 JSONL 不是数据库:需要考虑文件权限、并发写入、损坏恢复、清理和大文件索引。

在长时间运行的服务里,应避免多个进程无协调地写同一个 session file。必要时用锁、单写者队列或把 SessionManager 放进拥有明确所有权的服务。

6. 会话操作的产品语义#

“回退”不一定等于“撤销外部副作用”。如果旧分支已经发邮件、写数据库或修改远程资源,回到旧 entry 只改变 Agent 的历史路径,不会自动把外部世界恢复原状。产品 UI 应把“历史分支”与“外部回滚”明确区分。

同样,cloneforknew session 的区别要在用户界面中说明:它们可能共享祖先、复制当前路径或建立新的父会话关系,清理策略也不相同。

7. 恢复测试#

一组基本测试应覆盖:

  • 在 user message 后崩溃,重启后能否继续。
  • 工具结果已经写入后崩溃,是否会重复外部副作用。
  • 从旧节点回退并继续,旧分支是否仍可读。
  • 压缩 entry 存在时,当前上下文是否包含摘要和保留区。
  • session file 损坏一行时,系统是拒绝、修复还是降级。

8. Session Tree 的核心不变量#

树结构至少要保持三条关系:每个 entry 的 parentId 指向一个更早的节点;当前 leaf 能沿父链回到 header;任意分支都不会修改共同祖先。只要这三条关系成立,回退、分支和上下文重建就可以独立实现。外部副作用则必须另行建模,因为改变历史 leaf 不会自动撤销已经发生的现实动作。

资料

本文依据 Pi SDK 文档SessionManager 源码重新创作。

相关文章