青雲的博客
深入浅出 Pi 第四部:会话、快照与 Harness 第 24 章

持久会话为什么还不等于持久运行

对照 SessionStorage、JSONL、SQLite 与 AgentHarness 内存字段,检查进程在 provider、工具或保存点前退出时还能重建什么,并划清 Harness v2 设计的版本边界。

源码版本
v0.83.0
验证日期
Commit
845d6ff1f6643aba440341cce877ce1c43ebbc39

到上一章为止,我们已经看到 JSONL 能重新打开、leaf 能重建、compaction 和 branch summary 也能再次投影。把这些事实合在一起,很容易得出一句过头的结论:“Agent 运行已经持久化。”

检验这句话最有效的方法不是继续看正常结束,而是任意切断进程。provider stream 进行到一半时退出,Session 里有没有 request intent?assistant 已经发出 tool call、工具尚未返回时退出,新进程能否判断工具到底执行过没有?setModel() 已经 resolve、pending write 尚未 flush 时退出,哪个值算被接受?v0.83.0 对这些问题没有 durable operation log。它保存的是会话树,不是运行状态机。

Storage 接口只认识 Session tree

SessionStorage 的写接口很短:创建 entry id、追加 SessionTreeEntry、设置 leaf;读接口返回 metadata、leaf、entry、按类型查找、路径、统计与分页 entries。联合类型中也只有 message、配置、摘要、custom、label、session info 与 leaf。没有 run_startedprovider_request_startedtool_started、queue receipt 或 pending-write receipt。

JSONL 文件第一行是 version 3 session header,后续每一行都按 SessionTreeEntry 解析。open 时 loader 逐行构建 entries,并用普通 entry id 或 leaf target 重建 pointer。append 时,backend 先调用宿主 appendFile();成功后才更新内存数组、id map 与 current leaf。

这给出一种明确但有限的耐久性:一次 entry append Promise 成功后,重新打开该文件可以再次读到这行。接口并未声明 fsync 或断电保证,具体 appendFile()ExecutionEnv backend 实现;所以更准确的表述是“可跨进程重开”,而不是在不了解文件系统与宿主策略时宣称抗掉电。

SQLite 让 entry 内部一致,没有扩大运行合同

SQLite backend 给一次 appendEntry() 加了事务。它在同一 transaction 中写 session_entries、推进 sequence、更新 materialized summary、写 materialized entry、更新 active leaf 与 branch index;失败时还原当前对象的内存缓存。这比 JSONL 单行加内存索引的实现更适合查询和分支物化。

数据库 schema 仍然只有 sessions、session_entries、sequence、branch_entries 和两类 materialized table。没有 run 或 tool invocation table。一个 entry 的数据库事务不能覆盖外部 provider 计费、bash 副作用或另一个稍后才发生的 append,更不提供工具 exactly-once。

因此 JSONL 和 SQLite 的差别主要在 backend 的原子性、查询和物化策略;它们对 SessionStorage 暴露的是同一棵树。更换 backend 不会让 Harness 突然知道中断前的工具是否已经触发副作用。

一次 crash 会留下哪些前缀

AgentHarness.prompt("fix it") 切成几个节点:

flowchart TD
  START["prompt() / phase=turn"]
  USER["append user message"]
  REQ["provider request in flight"]
  ASSIST["append assistant message\npossibly with tool calls"]
  TOOL["tool effect in flight"]
  RESULT["append tool result"]
  SAVE["flush pending writes / save_point"]
  END["agent_end / settled"]

  START --> USER --> REQ --> ASSIST --> TOOL --> RESULT --> SAVE --> END

在 user message 追加后、provider response 前退出,Session 只知道 branch 尾部有一条 user message;没有 record 说明它属于一条已接受但未完成的 run,也没有 request attempt。新建 AgentHarness 的 phase 默认 idle,队列为空。公开 API 没有从 operation log resume() 的实现。

在 assistant tool-call message 追加后、工具结果前退出,情况更危险。树保留了 tool call,却没有 tool_started intent。新进程无法从 Session 判断:工具从未开始、正在执行时进程被杀,还是副作用已经完成但结果尚未写入。自动重跑写文件、发请求或执行 shell 都可能重复副作用。

在运行中 setter resolve 后、保存点前退出,Harness 的最新字段已经变化,相应 entry 仍在 pendingSessionWrites 数组。这个数组与 phase、abort controller、三个消息队列一起属于对象内存;constructor 每次都从空数组与 idle phase 开始。Session branch 只保留上一个成功 flush 的前缀。

abort() 也只清当前内存中的 steering/follow-up 队列,触发当前 abort controller,等待 run settlement,再发事件。Session 中没有 abort request entry。进程在 abort 与 settlement 之间再次退出,新进程同样只能看到已经成功追加的 tree prefix。

重开 Session 和恢复 Run 是两套动作

JsonlSessionRepo.open() 检查文件存在,创建 JsonlSessionStorage 并包成 Session。它返回的对象能 buildContext()、查 entry 和继续追加,但没有同时构造 AgentHarness,也没有推断一条未完成 run。宿主仍需提供 Models、工具实现、认证、资源与 Hook;这些 JavaScript 函数本来也不能序列化进会话文件。

稳定版 pi-coding-agent 的 resume 也应按同一证据标准描述。它从自己的 SessionManager 得到 existing session,恢复 agent.state.messages,必要时补 thinking entry,然后继续创建 AgentSession。这叫恢复会话上下文与产品配置,不是从中断的 provider/tool checkpoint 继续同一 run。

Harness v2 文档写的是目标,不是现状

固定 tag 内已经有 harness-v2.md。它提出 durable run、lane operation log、operation_started、tool intent、queued message receipt 与 crash resume;开头就把“accepted prompt 在 crash 后从安全边界继续”列为 Goals。另一个 durable-harness.md 也明确说实际目标只能是 semi-durable:Session 保存 Harness 自己拥有的可序列化状态,宿主在 resume 时重建工具、模型、扩展、资源、认证和 Hook。

这些文档很有价值,因为它们准确指出当前树模型缺少什么。但文件名里的 design、plan 和 goals 不能被改写成 v0.83.0 的 shipped behavior。判断是否交付,要回到联合类型、storage schema、constructor 与 public methods;它们还没有 operation log 和 restore/resume 状态机。

最小验证不需要制造真实崩溃。先检查一份 JSONL 重开测试,再静态确认 storage 类型与 schema 中不存在 orchestration records:

repo="${PI_SOURCE_DIR:-/tmp/pi-handbook-qbQTcA}"
cd "$repo"

npm run test:harness --workspace @earendil-works/pi-agent-core -- \
  test/harness/storage.test.ts \
  -t 'loads existing entries and reconstructs leaf'

git grep -nE 'operation_started|tool_started|queue_enqueued' v0.83.0 -- \
  packages/agent/src/harness \
  packages/storage/sqlite-node/src || true

第一条应证明 tree entry 与 leaf 可以重开;第二条在实现目录应无命中。设计文档有这些词,所以搜索范围故意只放 src。将来若实现出现命中,本章结论需要随新版重新审计,不能机械沿用。

第四部到这里完成了两次拆分:AgentHarness 与稳定版 Agent + AgentSession 是并列路径;Session durability 与 run durability 也是两层合同。第五部回到当前 pi 产品真正使用的 AgentSession,看它怎样把底层 Agent、SessionManager、扩展、自动压缩、重试与多种 I/O mode 组织成 Coding Agent 的控制面。