Skip to content

Latest commit

 

History

History
124 lines (83 loc) · 4.97 KB

File metadata and controls

124 lines (83 loc) · 4.97 KB

34. History Snip、Replay 与 Projected View

所属专题簇:记忆与上下文算法深挖

建议前读:19. 上下文压缩与历史治理

建议后读:07. QueryEngine 与上下文16. 会话持久化与恢复机制

研究问题

Claude Code 如何在长会话里同时做到“UI 还看得见历史”和“实际运行时不再背着整段历史前进”?

一句话结论

它在 QueryEngine 里预留了一条 HISTORY_SNIP 路径:通过 snip boundary、replay 和 projected view,把“用户能回看的历史”和“运行时真正保留的消息集”拆开。

这篇讲什么

这一章解释当前快照里可见的 snip 机制接口、边界消息语义,以及它与普通 compact 的不同。需要说明的是:snipCompact.tssnipProjection.ts 在当前仓库中由 QueryEngine 条件导入,但实际文件未出现在可见快照中,因此本章只依据调用点和注释解释其协议,不伪造实现细节。

如果你不看源码,只看这一章,应该记住什么

  • Snip 不是普通 compact 的别名。
  • 它更像“历史重放和运行时裁剪协议”。
  • 目标不是只省 token,还要让 SDK / headless 会话真正释放旧消息占用。

源码依据

Mermaid 图:Snip Boundary 与 Replay 协议图

flowchart TD
    A["长会话 mutableMessages"] --> B["yield compact/snip boundary"]
    B --> C["snipReplay hook"]
    C --> D{"是否真的是 snip boundary?"}
    D -- 否 --> E["忽略"]
    D -- 是 --> F["重放 / 投影保留段"]
    F --> G["替换 QueryEngine 内 store"]
    G --> H["释放 pre-boundary messages for GC"]
    H --> I["UI 或 SDK 继续基于投影视图工作"]
Loading

当前快照里能确认什么

src/QueryEngine.ts 明确有:

  • feature('HISTORY_SNIP')
  • 条件导入 snipCompact.js
  • 条件导入 snipProjection.js
  • snipReplay 配置项
  • projectSnippedView 相关注释

这足以证明 snip 是正式设计的一部分,而不是注释残留。

QueryEngine 为什么需要单独的 snipReplay

QueryEngineConfigsnipReplay 的注释已经把语义说得很清楚:

  • REPL 保留 full history 用于 UI scrollback
  • SDK / headless 会话需要在这里真正裁剪历史
  • replay 的目标是让存储中的消息集缩短,而不是只发一个通知消息

这说明 snip 的核心作用是控制运行时消息存储,而不是只生成一个总结。

compact boundary 和 preserved segment 很关键

QueryEngine.ts 中关于 compact_boundary 的处理能确认:

  • boundary 本身会被当作系统消息
  • 会带 compactMetadata
  • 其中有 preservedSegment.tailUuid
  • 在 boundary 写入前后会 flush transcript / 剪掉前缀

这说明系统不是“把旧消息全扔掉”,而是明确维护一个保留段边界。

为什么要按 API round 分组

src/services/compact/grouping.ts 很有价值,因为它已经把 compact 分组策略从 human turn 改成 assistant message.id 驱动的 API round:

  • 同一个 assistant response 的 streaming chunk 保持同组
  • tool_use / tool_result 的配对有效性依赖这个边界
  • malformed input 的 dangling tool use 由后续修复逻辑兜底

这说明 Anthropic 已经接受一个现实:agentic 会话不能只按“人发一轮、模型回一轮”理解。

这和普通 compact 的区别

普通 compact 关注的是:

  • 总结旧消息
  • 重建必要 attachment
  • 替换上下文前缀

而 snip 更像:

  • 产生边界
  • 对历史做投影视图
  • 在 headless path 里真正缩短 store
  • 支持 GC 释放 pre-boundary messages

因此它更接近“运行时内存治理协议”,不是普通摘要流程。

这里最重要的不变量

  • 历史裁剪后仍要保留足够的 boundary 语义,避免 replay 断裂
  • tool_use / tool_result 配对不能在分组过程中被拆坏
  • UI scrollback 和运行时 store 可以不完全相同
  • headless 会话应真正缩短内存占用,而不是只逻辑上“说自己 compact 了”

你真正应该记住的点

  • Snip 是 Claude Code 长会话治理里更激进的一条路径。
  • 它处理的是“怎么保留可理解历史,同时真的裁掉运行时负担”。
  • 即使当前快照缺少 snipCompact.ts 正文,QueryEngine 暴露的接口和注释已经足以证明这套协议存在。

延伸阅读