M10. 会话:JSONL 如何变成可分叉的历史#
如果会话只是一个数组,回退就意味着删除后面的消息;删除会让调试和恢复失去证据。Pi 选择把会话保存成带父子关系的 JSONL 树:回退只移动当前 leaf,从旧节点继续工作就会自然产生分支。
1. 文件里保存什么#
首行是 session header,描述会话 ID、时间、工作目录和可选的父会话;后续每行是一个 entry。entry 有自己的 ID、parentId、时间戳和类型。
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 创建新会话。
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 注入摘要和保留路径,分支摘要提供语义桥,标签和模型变化更新运行状态。
整棵树
→ 当前 leaf 的祖先路径
→ 按 entry 类型投影
→ AgentMessage[]
→ transformContext / convertToLlm这一步解释了为什么树可以很丰富,而模型上下文仍然是线性的:树负责保存选择空间,路径负责定义本次运行看到的世界。
5. JSONL 的工程优点与限制#
每行一个 entry 便于追加、调试、备份和局部读取;树结构也能保留完整尝试。但 JSONL 不是数据库:需要考虑文件权限、并发写入、损坏恢复、清理和大文件索引。
在长时间运行的服务里,应避免多个进程无协调地写同一个 session file。必要时用锁、单写者队列或把 SessionManager 放进拥有明确所有权的服务。
6. 会话操作的产品语义#
“回退”不一定等于“撤销外部副作用”。如果旧分支已经发邮件、写数据库或修改远程资源,回到旧 entry 只改变 Agent 的历史路径,不会自动把外部世界恢复原状。产品 UI 应把“历史分支”与“外部回滚”明确区分。
同样,clone、fork 和 new session 的区别要在用户界面中说明:它们可能共享祖先、复制当前路径或建立新的父会话关系,清理策略也不相同。
7. 恢复测试#
一组基本测试应覆盖:
- 在 user message 后崩溃,重启后能否继续。
- 工具结果已经写入后崩溃,是否会重复外部副作用。
- 从旧节点回退并继续,旧分支是否仍可读。
- 压缩 entry 存在时,当前上下文是否包含摘要和保留区。
- session file 损坏一行时,系统是拒绝、修复还是降级。
8. Session Tree 的核心不变量#
树结构至少要保持三条关系:每个 entry 的 parentId 指向一个更早的节点;当前 leaf 能沿父链回到 header;任意分支都不会修改共同祖先。只要这三条关系成立,回退、分支和上下文重建就可以独立实现。外部副作用则必须另行建模,因为改变历史 leaf 不会自动撤销已经发生的现实动作。
资料
本文依据 Pi SDK 文档、SessionManager 源码重新创作。