压缩与回到旧分支怎样改变上下文
分别追踪 compact() 与 navigateTree(),解释摘要屏障、retained tail、active leaf 移动和 abandoned branch summary 怎样改变下一次 buildContext。
Session tree 让历史可以分叉,但 provider 的上下文窗口仍然有限。Pi 在 Harness 里提供两种看起来都像“换一段上下文”的操作:compact() 缩短当前分支的模型可见前缀;navigateTree() 把 active leaf 移到树中已有位置,还可以概括刚离开的分支。它们都会让下一次 buildContext() 返回不同消息,却没有共用同一种拓扑变更。
压缩始终沿当前 branch 向前走,在尾部添加一个 checkpoint。导航则可能横跨分支,把 pointer 跳回祖先或另一个已有节点。旧 entry 在两种操作后都还存在;变化发生在 active path 的选择和投影层。
Compaction 是新 entry,不是历史改写
AgentHarness.compact() 要求 phase 为 idle,随后切到 compaction。它读取当前 branch,调用 prepareCompaction(),允许 session_before_compact Hook 取消或直接提供结果;没有 Hook 结果时才请求模型生成摘要。最后,Harness 调 session.appendCompaction(),把 summary、first kept id、tokens before、usage、retained tail 和 details 作为一条新 entry 追加。
准备阶段先找最新旧 compaction,拿出 previous summary 与新的边界;再对 buildSessionContext(pathEntries).messages 估算当前 token。findCutPoint() 从尾部累加消息估算量,选择适合保留最近内容的切点,还会避免从孤立 tool result 开始。若切点落在一个 turn 中间,准备结果把历史前缀、turn 前缀和 retained tail 分开,摘要生成可能需要两次模型调用。
旧消息没有被删。普通 append 让 compaction entry 成为 current leaf,原来的 parent chain 仍留在 storage。随后 defaultContextEntryTransform() 找 active path 上最新 compaction,把它放到 context entries 首位;有 retainedTail 时直接使用 entry 内保存的最近消息,再加 compaction 之后的新 entry。兼容旧格式时,则从 firstKeptEntryId 在旧 path 中找要保留的尾部。
flowchart LR
accTitle: Compaction 如何改变模型上下文
accDescr: 原始树条目保持不变;当前分支尾部新增 compaction entry,后续上下文由摘要与保留尾部构成。
RAW["原始 branch\nu1 -> a1 -> u2 -> a2"]
PREP["prepare\nold prefix / retained tail"]
SUMMARY["summary request"]
ENTRY["append compaction entry\nparent = a2"]
CONTEXT["next context\ncompactionSummary + retainedTail"]
RAW --> PREP --> SUMMARY --> ENTRY --> CONTEXT
RAW -.->|"仍在 append log"| ENTRY
所以“压缩后 token 变少”是对下一次 context projection 的描述,不是文件大小变化,也不是累计使用量清零。getSessionStats() 仍扫描所有 entries 的 assistant、compaction 与 branch summary usage;历史成本依然可以统计。
还有一个产品边界:v0.83.0 的 AgentHarness 只实现手动 compact(),没有把 shouldCompact() 接进 agent loop 的自动决策。pi-coding-agent 自己有另一套自动 compaction 逻辑,不能把产品能力倒推成 Harness 已经自动触发。
Navigation 改的是 leaf
navigateTree(targetId) 同样要求 idle,但 phase 变为 branch_summary。它先记录 old leaf,校验 target,随后找 old branch 与 target branch 的共同祖先。待摘要 entries 只来自 old leaf 回溯到共同祖先之前的那段,也就是用户准备离开的工作,而不是整棵树。
Hook session_before_tree 可以取消,可以直接给 summary,也可以调整自定义指令。只有调用方设置 summarize: true、离开分支确有内容且 Hook 没提供 summary 时,Harness 才调用模型。branch summary 的输入会过滤 toolResult 和纯配置 entry,保留消息、custom message、已有摘要,并在 token budget 内从较近内容向前选择。
如果 target 本身是 user message,Harness 不把它作为新 leaf,而是移动到它的 parent,并把该 user 文本作为 editorText 返回。这样交互层可以把旧 prompt 放回编辑器,让用户修改后重新提交。custom message 也采用相同的“移到它之前”规则;其他 entry 则直接成为目标 leaf。
Session.moveTo() 先追加 leaf entry,使 active pointer 指向目标;若有摘要,再以目标为 parent 追加 branch_summary。下一次 context 沿新 leaf 回溯时,既能看到目标分支的旧前缀,也能看到“刚才离开的分支做了什么”。这不是把旧分支 entry 复制过来,只增加了一条摘要节点。
before:
u1 -> a1 -> u2 -> a2 (old leaf)
\
u3 -> a3 (target branch)
after navigate + summarize:
u1 -> a1 -> u2 -> a2 (still stored)
\
u3 -> a3 -> branch_summary(old u2/a2) (new leaf)
共同祖先的实际位置取决于 target;上图只是说明拓扑,不能拿固定 id 当作所有导航结果。
两种操作的失败边界不一样
compaction 在 summary 成功后才追加一条 compaction entry。provider 失败或 Hook 取消时,branch 没有结构变化;storage append 失败时,内存中的生成结果丢失,但旧 leaf 和旧上下文仍然可读。
navigation 可能包含两次持久写:先 setLeafId(),后 append branch summary。二者没有跨 backend 事务。摘要生成发生在 leaf move 之前,因此生成失败不会先跳走;但 leaf entry 已追加、后续 summary append 失败时,新 leaf 已生效,用户只会缺少离开分支摘要。JSONL 无法原子追加两行,当前 SessionStorage 接口也没有 transaction 方法。
这不是凭空推测。moveTo() 源码就是先 await setLeafId(entryId),无 summary 时直接返回,有 summary 才调用第二次 append。实现提供了明确的部分完成状态,调用方不能把 navigateTree({ summarize: true }) 宣称为“要么全成,要么不动”。
两条测试分别验证投影与导航
compaction 测试构造 u1/a1/u2/a2 -> compaction -> u3/a3,并在 compaction entry 中保存 u2/a2 retained tail。buildSessionContext() 返回的 role 顺序是 compactionSummary, user, assistant, user, assistant,说明 context 使用摘要加保留尾部,而不是全量旧消息。
branch navigation 测试则创建一段“first branch / abandoned work”,让 faux provider 第一次 summary 失败、第二次成功,最后确认生成的 summary entry 和 retry event 序列。它证明 branch summary retry 与写入路径,不证明外部副作用可恢复。
下面两条命令都在官方 pi-0.83.0-source.tar.gz 的一次性解压目录中运行。先按本部导读核验 SHA256 f225b87ec3b4825dd5b94e922a8629558addca31a1b4d2c206ae598a8e2692c0 并安装 lockfile 依赖,再设置 PI_RELEASE_SOURCE_DIR;不要修改固定证据 checkout,也不要重新执行读取当前上游数据的 hydrate-model-data。如果输出 0 test,不能拿退出前的日志当作通过证据。
repo="${PI_RELEASE_SOURCE_DIR:?先按第四部导读校验并解压官方 release source archive}"
cd "$repo"
test -f packages/ai/src/providers/data/.manifest.json
npm run test:harness --workspace @earendil-works/pi-agent-core -- \
test/harness/compaction.test.ts \
-t 'builds session context with a compaction entry'
npm run test:harness --workspace @earendil-works/pi-agent-core -- \
test/harness/agent-harness.test.ts \
-t 'retries transient branch summary errors and emits retry events'
至此,Session 可以保存消息树、配置、摘要和 leaf 移动,也能在重开 storage 后重建 active context。很容易顺势写出“运行已经持久化”。这个结论仍然早了一步:正在执行的 provider request、未完成工具、队列和 pending writes 并没有同样进入 Session。第 24 章专门划出“持久会话”与“持久运行”的分界线。