Compact 摘要的存储和清理机制

1 阅读3分钟

涵盖:摘要存储与 REPL 展示、JSONL 文件大小管理


4. compact 摘要存储与 REPL 展示

摘要存在哪里

摘要不独立存储。CompactionResult包含 summaryMessages,通过 buildPostCompactMessages 组装后直接成为消息列表的一部分:

// compact.ts:330-337
const postCompactMessages = [
  boundaryMarker,          // SystemCompactBoundaryMessage
  ...summaryMessages,      // 摘要(isCompactSummary, isVisibleInTranscriptOnly)
  ...messagesToKeep,       // 保留的最近消息
  ...attachments,
  ...hookResults,
]

这些消息被:

  1. yield 到 QueryEngine(query.ts:530-532
  2. 持久化到 JSONL transcript(recordTranscript
  3. 设置为新的 messagesForQuery(发给 API 的消息列表)

是否替换原消息列表

替换的是 messagesForQuery(发给 API 的消息),不直接删除 state.messages 中的原始消息。

压缩前 messagesForQuery:
  [msg0][msg1][msg2]...[msgN-1][msgN]

压缩后 messagesForQuery:
  [boundary][summary][msgN-1][msgN]
                    ↑               ↑
              被摘要替换        保留的消息
              (不再发给 API)

每次 query loop 迭代开始时:

// query.ts:365
let messagesForQuery = [...getMessagesAfterCompactBoundary(messages)]

最后一个 compact boundary 之后取消息,之前的原始消息不再出现在 API 请求中。

是否影响 REPL 显示

不影响用户看到的聊天记录。 两个机制保证:

机制 1:摘要消息对用户不可见

// sessionMemoryCompact.ts:476-482
const summaryMessages = [createUserMessage({
  content: summaryContent,
  isCompactSummary: true,
  isVisibleInTranscriptOnly: true,  // ← 关键
})]
// messages.ts:4675
if (message.isVisibleInTranscriptOnly && !isTranscriptMode) return false

机制 2:REPL 消息列表在 compact 时被裁剪

收到 compact boundary 时:

// REPL.tsx:2594-2599
if (isCompactBoundaryMessage(newMessage)) {
  if (isFullscreenEnvEnabled()) {
    // 全屏:保留从上一个 boundary 开始的消息(含旧摘要、保留消息等)
    setMessages(old => [...getMessagesAfterCompactBoundary(old, { includeSnipped: true }), newMessage])
  } else {
    // 终端:直接替换为 boundary(旧消息全部丢弃)
    setMessages(() => [newMessage])
  }
}

总结:各视角的可见性

视角能看到什么
API 请求(messagesForQuery)boundary + 摘要 + 保留消息
REPL 用户窗口原始消息列表(摘要被 isVisibleInTranscriptOnly 过滤)
JSONL 转录文件全部历史(boundary 前后的消息都在)

5. JSONL 转录文件大小管理

没有大小限制

JSONL 文件(~/.claude/projects/<project>/sessions/<sessionId>.jsonl没有硬性大小上限。每次 compact 只追加写入,不物理截断文件:

// sessionStorage.ts:1432-1439
await getProject().insertMessageChain(newMessages, ...)

读取时的性能优化

当文件超过 5MBSKIP_PRECOMPACT_THRESHOLD),readTranscriptForLoad 在加载时跳过 compact boundary 之前的旧内容:

// sessionStoragePortable.ts:636-643
if (boundaryAt >= lineStart && ...) {
  const hit = parseBoundaryLine(...)
  if (hit) {
    s.out.len = 0           // 清空缓冲区,丢弃 boundary 前的内容
    s.boundaryStartOffset = s.bufFileOff + lineStart
  }
}

这只影响加载性能,不影响文件大小。旧数据仍留在磁盘上。

实际观测

代码注释中记录的实际文件大小:

// sessionStorage.ts:3234-3235
// 41 MB, 99% dead: parseJSONL 56.0 ms -> 3.9 ms (-93%)
// 151 MB, 92% dead: 47.3 ms -> 9.4 ms (-80%)

跨多天重度使用的会话文件可达到 150MB+,其中 90%+ 是 compact boundary 之前的废弃内容。

清理机制

方式说明
物理截断无自动截断机制
手动删除用户手动删除 sessions/ 下旧 .jsonl 文件
/resume 列表限制limit 参数控制显示数量(如最近 20 个),但不删除文件
Tombstone 保护50MB 上限(MAX_TOMBSTONE_REWRITE_BYTES),超过后跳过重写

取舍

这是有意为之的设计——保留完整历史用于恢复和调试,以磁盘空间换取数据安全。如果磁盘空间成为问题,用户需要手动清理。