# Agent Note: 可回溯压缩（compaction）：索引检查点、状态检查点与会话内历史回溯

Status: proposed

[English](2026-07-06-recallable-compaction.md) | 中文

## 问题

压缩是一扇单向门。模型看到的摘要没有指向被其遮蔽内容的引用，因为 `shadowedRange` 来源只存在于仅写入日志、模型不可见的 `compact/summary` 事件上，也没有工具能让模型重新读取被遮蔽的区段。即使仅追加日志仍保存每一个字节，摘要器丢弃的内容也会离开模型可触达的世界。重复压缩会进一步放大问题：每一轮都会重写头部检查点，因此请求前缀每次都会完全失去提示词缓存命中，而更早的摘要也会一代又一代地被重新摘要。

根本原因是一个产物承担了两个互相冲突的角色。**索引** 需要冻结、按时间排序且成本低廉；模型的**工作记忆** 则需要全局视图、重新确定优先级并且可变。单一摘要无法同时胜任两者。

主流编码 harness 都没有让模型在循环内回溯，而且调研过的实现均未让压缩感知前缀缓存。事件溯源会话具备持久原文、可按 seq 寻址和精确回放的特征，是支持这两项功能的天然底座。

## 提案

把检查点拆为两类，并让被遮蔽的历史重新可达。

### 冻结的索引检查点

新近变为陈旧的历史按确定性策略拆分为分片：向 `chunkTokens` 累积；使用 `toolPairingBalancedBefore`／`toolPairingBalancedAfter` 对齐边缘；优先选择轮次边界；在平衡允许的范围内，把最终边界放在尽量接近保留边界的位置，使尾随切片缩小到大约一个轮次。每个分片通过一次 `compactRegion` 调用压缩为一个**索引存根**（`stubTokens`，约 100–200 个 token）：

- 用两三行说明发生了什么；
- 用一行关键词记录低频字面锚点，例如确切的错误字符串、值和配置键，并按类别分组；
- 由代码组装页脚：`[checkpoint c<summarySeq>: shadows conversation span #<start>–#<end>; originals retrievable via history_read]`。指针根据来源组装，绝不由模型编写。

已经提交的存根永不重写，也绝不再次进入之后的压缩区域。存根调用采用分层输入：固定前导内容与逐字节相同的本轮开始状态检查点（该阶段所有调用共享的前缀）；随后是先前所有已提交存根的关键词行，使新条目索引其分片的独特内容，而不是重复整个目录；再加最近一两个已提交存根以维持时间连续性；最后是切片本身。同一轮中的同级存根不作为输入，因为并发阶段禁止这种依赖，而与轮次对齐的边界已经维持局部连续性。状态检查点只作为背景，绝不能成为存根需要总结的材料。完全由回溯内容构成的切片只通过代码生成存根，即只写一行指针，不调用 LLM（大语言模型）。存根调用失败时采用相同降级方式：其切片获得一个仅包含代码指针的存根，本轮继续执行，使状态重写成为一轮中唯一的强制 LLM 依赖。

### 状态检查点

系统维护一份可变的工作记忆文档（最多一份；第一次压缩前为零份），位于所有存根之后、保留尾部之前。每一轮根据先前状态与本轮变为陈旧的内容重写它，成本为 O(previous + new)；过程遵守摘要提示词中已有的「合并而不重复陈述」规则，并覆盖决策、当前状态、约束和后续步骤。它带有自己的页脚，其大小上限与当前摘要处于同一量级。

膨胀保护会约束整轮操作：如果压缩后大小没有严格小于压缩前大小，就不提交任何内容，并继续当前轮次；本次尝试延后至积累更多陈旧历史后再进行。保护逻辑在两侧比较同一项度量：优先使用请求路径上提供方报告的用量；如果不可用，则两侧都回退为字符估算器。

### 一轮的执行过程

- 分片切片由表面位置范围表示。一轮分两个阶段运行：所有摘要调用先并发执行并在表面之外缓冲；随后严格从左到右提交区域，先提交各分片，最后提交尾随切片，使状态检查点通过连续的单节点替换落在所有存根之后。墙钟时间维持在接近一次摘要调用的水平。
- 被取代的状态检查点会作为普通历史折入下一轮的第一个分片，不需要墓碑或新原语。其存根省略该状态，`history_read` 会把它渲染为 `[prior state checkpoint]`，并让页脚随渲染文本一同传递，使每个尾随切片都可通过两跳链路触达。
- 范围选择会感知冻结边界：可压缩区段从最后一个已提交索引检查点之后开始；只有在不存在索引检查点时，才从表面头部开始。旧会话现有的头部检查点会被视为状态类检查点：其文本作为合并基线，其节点则像其他被取代状态一样折入历史。
- 摘要阶段发生崩溃时不会提交任何内容；提交中途崩溃会留下一个从左到右的已提交前缀，恢复后的一轮从日志中最新的状态类 `compact/summary` 事件读取合并基线，并无条件提交剩余区域。恢复 `[stubs…][state][tail]` 的优先级高于缩减大小。

### 回溯工具

新增包（package）`@deepseek-ai/dsh-tool-recall`，它只是 `dsh-session` 与 `dsh-compact` 词汇之上的消费方，注册两个面向模型的工具：

- `history_read(checkpoint, offset?)`：把日志中任意检查点（包括已被取代的检查点）遮蔽的区段渲染为 `User:`／`Assistant:`／`Tool result:` transcript（文本记录），并按配置预算分页，提供续传游标。
- `history_search(query, checkpoint?, limit?)`：对每个被遮蔽区段进行不区分大小写的字面量扫描；返回带检查点 id 的片段与覆盖元数据（`scanned`／`matched`／`truncated`）。零匹配提示会说明扫描按字面量执行，并建议对可能的检查点直接使用 `history_read`。

两个工具都读取 `exec.agent.session.events`（沿用 tool-todo 访问模式；拒绝非 agent（智能体）调用方），只渲染表面类型的消息事件，并返回普通 `tool/result`：回溯字节会进入上下文尾部并记录到日志，因此无需特殊处理即可满足可重建性。系统不增加新存储或伴随索引：会话日志是归档，`compact/summary` 来源是索引元数据，而这些工具是两者之上的读取路径。工具 schema 与该包唯一的系统提示词章节都是静态字符串；检查点 id 只会通过页脚抵达模型。transcript 渲染器从 `compact-basic` 移入 `dsh-session`，供摘要器与工具共享。

### 缓存与成本

一轮后的请求前缀为 `[system][stubs…][state][tail]`。冻结存根在各轮之间逐字节稳定，因此缓存缺失从替换先前状态检查点的 token 才开始，规模保持 O(new chunks + state + tail)，而当前实现会从位置零开始缺失。回溯输出落在尾部，不会改变前缀。每轮摘要输入大约是当前实现的两倍，另加一个 m·S 背景项；该成本受到 `chunkTokens` 下限（状态上限的小倍数）以及经过校验的 `stubTokens`／`chunkTokens` 比例上限约束。共享前缀输入布局依次为前导内容、逐字节相同的本轮开始状态、位于尾部的切片内容，使同级调用可以按缓存费率重复读取。

### 打包方式

该设计以新的后端 `dsh-compact-recallable` 交付，挂在现有 `ctx.compact` seam 上，并在已交付的示例配置中默认启用。`compact-basic` 保留为参考实现和该 seam 的设计对照，与成对 LLM 适配器的模式一致。seam JSDoc 中「最多一个自动生成的检查点，始终位于头部」这一条会放宽，改为说明两个后端各自的行为。

### 与进行中工作的关系

- **工具结果裁剪**（进行中的裁剪服务）：其替换节点携带 `sourceEventSeqs`；同一注册表折叠会把经过裁剪的结果列为可回溯。它属于后续范围，两项工作互不阻塞。
- **提供方 token 用量核算**（正在把压缩压力迁移至提供方报告用量的工作）：为保护逻辑提供核算基础；本实现堆叠在它之后。
- **「查询会话」backlog（待翻清单）条目**：它是跨会话的泛化方案；本 Agent Note（agent 决策记录）把范围限定在实时会话内，并选择工具名称与渲染方式，使该工作能够扩展本设计而不产生冲突。
- **训练**：何时回溯属于学习到的行为。确定性页脚与关键词锚点为训练提供稳定目标，而回溯使用情况在会话日志中完全可见，可供轨迹导出；基准测试与 RL 设计由后训练侧推进。

### 后续事项

以下项目在评审期间已经明确，但会延后至观察结果证明需要时再实现：

- 保护逻辑降级阶梯（通过代码汇总最早的存根前缀，保留页脚，已汇总 id 仍可作为回溯目标；随后在冻结边界后生成一份摘要）：触发条件是观察到保护逻辑活锁或存根区域压力。
- 存根输出回声检测（句子级 n-gram，豁免短字面量；先重试，再剥离）：触发条件是观察到职责分工泄漏。
- 定期使用分片原文刷新状态：触发条件是交接探针观察到漂移。
- `stateFallbackThreshold`（存根数量低于阈值时使用完整细节的状态提示词）：触发条件是短会话回归。
- 延迟注册回溯工具：触发条件是在从不进行压缩的会话中测得上下文开销。
- 在 pre-step 分摊存根起草工作：一旦已经陈旧但尚未压缩的内容积累超过 `chunkTokens`，就在下一个 pre-step 起草该分片的存根（一个仅写入日志的草稿事件，在分片周围的上下文仍然存活时写入），让压缩轮提交草稿，而不是集中执行摘要。这是后台压缩的确定性、精确回放等价形式（Claude Code 会话记忆采用这种模式；OpenClaw 证明同步语义完全相同）。触发条件是观察到一轮延迟，或近实时起草带来的存根质量收益得到验证。
- 拆分摘要模型；由模型选择分片边界；跨会话回溯；语义搜索回退：每项都必须由各自证据支持。
- 更丰富的 `history_search` 查询形式：正则表达式，以及对日志 JSON 工具结果执行的结构化查询（sql／jq 风格，或由 agent 针对索引存储编写查询）。触发条件是观察到搜索漏检；首版先交付字面量匹配，使回溯路径保持为日志的纯函数。

## 考虑过的替代方案

- **分阶段交付**（先在当前后端之上单独交付回溯工具；观察到回溯使用后，再决定是否拆分检查点）：不予采纳。未经训练的模型会低频使用任何新工具，因此该条件测量的是训练缺失，而不是设计价值；训练侧需要完整机制来构建环境；预发布阶段修改持久化格式的成本最低；缓存经济性则属于第一方已经掌握的知识，不是等待遥测验证的假设。实现仍以堆叠 PR 方式落地，并先交付回溯工具，但这只是构建顺序，不是决策门槛。
- **只保留冻结的全尺寸摘要，不设状态检查点**：不予采纳，因为永久前缀会无界增长、自我加速并最终发生颠簸，而且没有任何内容可以重新确定优先级。
- **只保留纯存根，不设状态检查点**：不予采纳，因为这假定模型知道自己缺少什么，在面对未知的未知时会失败。
- **由 LLM 老化／整合冻结分片**：不作为常规机制，因为摘要的摘要会丢失信息，并使冻结前缀频繁变化；其保留下来的形式是由代码汇总，且延后实现。
- **把完整前缀作为分片摘要器输入**：不予采纳，因为成本为 O(N²)；状态文档以 O(state) 提供相同背景。
- **一次摘要调用输出全部结果**：不予采纳，因为摘要路径没有结构化输出约束；解析一份自由文本响应并将其拆开，正是保守失败设计要避免的脆弱 seam。
- **由模型选择分片边界**：延后实现，因为相对于未经证明的收益，解析与校验成本过高；分片策略位于配置之后。
- **由模型编写指针**：不予采纳，因为指针必须精确，应由确定性代码组装。
- **FTS／向量索引伴随存储**：在会话内不予采纳，因为实时日志已在内存中且大小有界，在预算内进行字面量扫描已经足够；只有跨会话范围才能证明索引的价值。
- **回溯路径中的语义搜索回退／次级模型提取**：不予采纳，因为其中的 LLM 或嵌入调用会破坏无密钥回放的确定性；回溯必须保持为日志的纯函数。
- **使用原始事件而不是渲染后的 transcript**：不予采纳，因为这会泄漏仅日志可见的词汇与分片噪声；模型应读取模型曾经看到的内容。
- **什么都不做（用 resume／fork 恢复）**：不予采纳，因为这会把恢复变成人工操作。

## 验收标准

- 长会话自动压缩后，每一轮完成时都会得到 `[stubs…][state][tail]`；先前存根在各轮之间保持逐字节相同；已提交存根绝不落入后续区域；被取代的状态检查点无需墓碑即可折入历史，渲染时带有标签，并可通过两跳链路触达和搜索。
- 每个检查点的表面文本都以确定性页脚结束；页脚通过回放逐字节往返；状态检查点的来源记录其更宽的输入范围。
- 在全部摘要就绪且保护逻辑通过同类核算前，不提交任何内容；保护失败不会提交任何内容，也不会让轮次失败；提交中途被终止后，下一次 pre-step 会恢复处理，从日志读取合并基线，并无条件提交状态区域以完成本轮；旧版头部检查点会被视为状态类。
- `history_read` 在预算内渲染任意已记录检查点的区段，并提供可用游标；`history_search` 覆盖每个被遮蔽区段，返回带检查点 id 的片段与覆盖元数据，测试尤其要能找到只存在于被已取代状态检查点遮蔽区段中的内容，这是锁定尾随切片可达性的回归用例；两个工具都会拒绝非 agent 调用方，并对从未存在的 id 或遗留 `compact/start` 返回类型化错误；回溯内容作为普通 `tool/result` 出现；请求重建不变量会在同时包含压缩与回溯的会话上通过；一项无密钥快照场景端到端覆盖先压缩再回溯；工具 schema 与提示词章节在各轮之间逐字节相同。
- 在长时间跨度 bench 套件中：任务成功率在预算相同的条件下不低于 `compact-basic`；交接保真探针（在一轮后重新陈述 K 项已知决策和约束）的得分不降低；每次运行都通过 dsh bench 报告流水线汇报回溯使用频率和命中有效性，并同时报告存根目录注意力度量与缓存命中遥测。
- seam JSDoc、压缩能力 seam Agent Note、`architecture.md`，以及生成的工具、配置、持久化与模块图目录都在同一改动中更新；全部预算位于配置中；新源码目录具备逐文件 100% 覆盖率与 HMR（热模块替换）释放测试。

## 风险

- **回溯属于学习到的行为**：未经训练的模型会低频使用它，bench 报告会持续追踪这项差距，直至训练弥合问题。在此之前，状态检查点会让质量下限保持在当前摘要水平。
- **未知的未知仍然存在**：如果某项细节既未出现在摘要中，也未出现在关键词中，就不会触发回溯。回溯把「即使已经怀疑也无法触达」变成「怀疑时可以触达」。
- **存根目录会占用注意力**：每次请求中包含数十张稳定的索引卡，可能稀释模型关注点；验收标准中的 bench 度量会将其与 `compact-basic` 对比。
- **成本**：每轮摘要输入大约是当前实现的两倍；短会话的成本和质量接近当前水平，而设计收益随会话长度增长。
- **状态漂移与职责分工泄漏** 可以通过交接探针和存根评审观察；对应措施已列为后续事项。
- **两个后端** 会增加维护接口；seam 契约和共享回溯消费方会约束该成本，bench 对比则用于逐步决定默认实现。
