Claude Code 上下文压缩系统:完整设计与源码分析

基于 Claude Code npm 包 source map 泄露快照 (2026-03-31) 的源码分析。
适用于团队内部分享,理解 AI Agent 长对话上下文管理的工程范式。

一、核心问题

Claude Code 是一个多轮对话 Agent。用户在一次会话中可能进行 10-50+ 轮工具调用,每轮的工具结果(文件内容、grep 输出、bash 输出)都留在上下文中。

矛盾:上下文窗口有限(Sonnet 4.6: 200K tokens),但对话历史无限增长。

传统做法:截断旧消息或简单摘要。

Claude Code 的做法:一条五层流水线,每层针对不同场景,从最轻量到最重量级逐层升级。


二、五层压缩架构总览

每次 API 调用前的处理链(query.ts 中的执行顺序) Layer 0: History Snip(实验中) 时机:API 调用前最先执行 做法:直接丢弃最早的消息段 代价:信息永久丢失 收益:立即释放 token,为后续层减压 Layer 1: Microcompact(轻量清理) 时机:Snip 之后、Auto-compact 之前 做法:清空旧工具结果的内容(保留消息结构) 变体 A: Time-based — 距上次回复 > 60 分钟,直接改消息 变体 B: Cached — 发 cache_edits 指令让 API 端删除 代价:工具结果内容丢失,但消息骨架在 收益:不破坏 cache(变体 B)/ 快速释放空间 Layer 2: Context Collapse(实验中) 时机:替代 Auto-compact 做法:细粒度 token 预算管理 特点:启用后会抑制 Auto-compact Layer 3: Auto-compact(主力压缩) 时机:上下文达到有效窗口的 ~93% 路径 A: Session Memory → 用预计算摘要替代,零 API 调用 路径 B: Full Compact → Fork Agent 调用 API 生成摘要 代价:旧对话被摘要替代 收益:大幅释放空间(通常 < 30K tokens) Layer 4: Reactive Compact(被动防御) 时机:API 返回 413 Prompt Too Long 做法:从尾部剥离消息,保留 cache 前缀 特点:最后防线,启用后会抑制 Auto-compact Layer 5: API Microcompact(API 原生) 时机:输入达 180K tokens 做法:API 端 context_management 策略 策略:clear_tool_uses / clear_thinking 特点:完全由 API 端处理,客户端仅配置

三、调用链:query.ts 中的压缩执行顺序

文件:src/query.ts

① HISTORY SNIP 直接丢弃最早消息 ② MICROCOMPACT 清空旧工具结果 ③ CONTEXT COLLAPSE 细粒度管理(如启用) ④ AUTO-COMPACT 主力压缩 // query.ts 中的执行顺序 snipResult = snipCompactIfNeeded(messages) ↓ snipTokensFreed 传给后续层 microcompactResult = microcompact(messages, toolUseContext) ↓ 更新 messagesForQuery collapseResult = contextCollapse.applyCollapsesIfNeeded(...) ↓ 如果启用 CONTEXT_COLLAPSE compactionResult = autocompact(messages, ..., snipTokensFreed) ↓ 输出到前端 依赖注入 (QueryDeps) microcompact → Layer 1 autocompact → Layer 3

依赖注入src/query/deps.ts):

export type QueryDeps = {
  callModel: typeof queryModelWithStreaming,
  microcompact: typeof microcompactMessages,    // ← Layer 1
  autocompact: typeof autoCompactIfNeeded,      // ← Layer 3
  uuid: () => string,
}

四、Layer 1: Microcompact — 轻量工具结果清理

4.1 入口函数

文件:src/services/compact/microCompact.ts

export async function microcompactMessages(
  messages: Message[],
  toolUseContext?: ToolUseContext,
  querySource?: QuerySource,
): Promise<MicrocompactResult> {

  // ① 优先尝试 Time-based(cache 已冷)
  const timeBasedResult = maybeTimeBasedMicrocompact(messages, querySource)
  if (timeBasedResult) return timeBasedResult

  // ② 其次尝试 Cached MC(cache 仍热)
  if (feature('CACHED_MICROCOMPACT')) {
    if (isCachedMicrocompactEnabled() && isModelSupportedForCacheEditing(model)
        && isMainThreadSource(querySource)) {
      return await cachedMicrocompactPath(messages, querySource)
    }
  }

  // ③ 都不满足 → 不压缩
  return { messages }
}

4.2 变体 A: Time-based Microcompact

触发条件:距上次助手回复超过 60 分钟(假设 API 端 cache 已过期)。

function maybeTimeBasedMicrocompact(messages, querySource) {
  const trigger = evaluateTimeBasedTrigger(messages, querySource)
  if (!trigger) return null

  // 找出所有可压缩工具的 tool_use ID
  const toolIds = collectCompactableToolIds(messages)

  // 保留最近 N 个(config.keepRecent,最少 1 个)
  const toKeep = toolIds.slice(-config.keepRecent)
  const toClear = toolIds.filter(id => !toKeep.includes(id))

  // 直接修改消息内容
  for (const msg of messages) {
    if (msg.type === 'user') {
      for (const block of msg.message.content) {
        if (block.type === 'tool_result' && toClear.includes(block.tool_use_id)) {
          block.content = TIME_BASED_MC_CLEARED_MESSAGE
          // → '[Old tool result content cleared]'
        }
      }
    }
  }

  // 重置 Cached MC 状态(server 端引用已失效)
  resetMicrocompactState()
  notifyCacheDeletion()  // 通知 cache-break 检测器
}

可压缩工具集合

const COMPACTABLE_TOOLS = new Set([
  'Read',           // FileReadTool
  'Bash',           // BashTool (+ PowerShell)
  'Grep',           // GrepTool
  'Glob',           // GlobTool
  'WebSearch',      // WebSearchTool
  'WebFetch',       // WebFetchTool
  'Edit',           // FileEditTool
  'Write',          // FileWriteTool
])

4.3 变体 B: Cached Microcompact

触发条件:工具结果数量超过阈值(GrowthBook 配置)。

核心设计不修改本地消息,而是生成 cache_edits 指令发给 API。

async function cachedMicrocompactPath(messages, querySource) {
  // 收集所有可压缩工具 ID
  const toolIds = collectCompactableToolIds(messages)

  // 按用户消息分组注册
  registerToolResultsGroupedByUserMessage(toolIds)

  // 根据配置决定删除哪些
  const toDelete = getToolResultsToDelete(config)

  // 生成 cache_edits 块(不改本地消息!)
  const cacheEditsBlock = createCacheEditsBlock(toDelete)

  // 存入 pending 状态,等 API 调用时附带
  pendingCacheEdits = cacheEditsBlock

  // 通知 cache-break 检测器
  notifyCacheDeletion()

  // 返回原始消息(未修改)
  return { messages, compactionInfo: { pendingCacheEdits } }
}

API 端消费

// 在 API 调用前
const edits = consumePendingCacheEdits()  // 取出并清空
if (edits) {
  // 附加到 API 请求中
  requestBody.cache_edits = edits
}

两种变体的效果图

本地消息(不变) [user] 读取文件 [tool_result] 内容 [assistant] 分析... [user] 执行命令 [tool_result] 输出 [assistant] 结果... UI 显示完整历史 prompt cache 前缀不变 cache_edits API 端(执行删除) [user] 读取文件 [deleted by edit] ← cache_edits [assistant] 分析... [user] 执行命令 [deleted by edit] ← cache_edits [assistant] 结果... 模型看到精简版 token 消耗减少

4.4 两种变体的选择逻辑

维度Time-basedCached
触发条件距上次回复 > 60 分钟工具结果数 > 阈值
假设API cache 已过期API cache 仍有效
修改方式直接改消息内容生成 cache_edits 指令
Cache 影响破坏(但反正已过期)不破坏
适用场景隔夜/午休后恢复对话持续对话中的增量清理
Feature gate默认启用CACHED_MICROCOMPACT(内部用户)

五、Layer 3: Auto-compact — 主力压缩

5.1 触发阈值计算

文件:src/services/compact/autoCompact.ts

// 常量
const MAX_OUTPUT_TOKENS_FOR_SUMMARY = 20_000  // 预留给摘要输出
const AUTOCOMPACT_BUFFER_TOKENS = 13_000      // 触发缓冲区
const WARNING_THRESHOLD_BUFFER_TOKENS = 20_000 // UI 警告缓冲
const ERROR_THRESHOLD_BUFFER_TOKENS = 20_000   // 错误态缓冲

// 有效窗口 = 上下文窗口 - 摘要输出预留
export function getEffectiveContextWindowSize(model: string): number {
  const contextWindow = getContextWindowForModel(model)
  const maxOutputTokens = Math.min(getMaxOutputTokensForModel(model), 20_000)
  return contextWindow - maxOutputTokens
}

// 触发阈值 = 有效窗口 - 缓冲区
export function getAutoCompactThreshold(model: string): number {
  return getEffectiveContextWindowSize(model) - AUTOCOMPACT_BUFFER_TOKENS
}

以 Sonnet 4.6 (200K context) 为例

上下文窗口:    200,000 tokens
- 摘要预留:   - 20,000 tokens
= 有效窗口:    180,000 tokens
- 缓冲区:     - 13,000 tokens
= 触发阈值:    167,000 tokens (83.5%)

警告阈值:      167,000 - 20,000 = 147,000 tokens (73.5%)
错误阈值:      167,000 - 20,000 = 147,000 tokens (73.5%)

5.2 决策函数

export async function shouldAutoCompact(
  messages: Message[],
  model: string,
  querySource?: QuerySource,
  snipTokensFreed = 0,
): Promise<boolean> {

  // 守卫检查(按顺序短路)
  if (querySource === 'session_memory' || querySource === 'compact')
    return false   // 防递归:压缩/Session Memory 的 fork agent 不触发压缩

  if (isDisableAutoCompact() || isDisableCompact())
    return false   // 环境变量 kill switch

  if (feature('REACTIVE_COMPACT') && tengu_cobalt_raccoon)
    return false   // Reactive Compact 模式:等 API 返回 413 再处理

  if (feature('CONTEXT_COLLAPSE') && isContextCollapseEnabled())
    return false   // Context Collapse 自己管理上下文

  // 实际计算
  const tokenCount = tokenCountWithEstimation(messages) - snipTokensFreed
  const { isAboveAutoCompactThreshold } = calculateTokenWarningState(tokenCount, model)
  return isAboveAutoCompactThreshold
}

5.3 执行编排(含熔断器)

export async function autoCompactIfNeeded(
  messages, toolUseContext, cacheSafeParams, querySource, tracking, snipTokensFreed
) {
  // 熔断器:连续失败 3 次后停止尝试
  if (tracking?.consecutiveFailures >= MAX_CONSECUTIVE_AUTOCOMPACT_FAILURES) {
    return { wasCompacted: false }
  }

  if (!(await shouldAutoCompact(messages, model, querySource, snipTokensFreed))) {
    return { wasCompacted: false }
  }

  // ① 优先尝试 Session Memory Compact(零 API 调用)
  const sessionMemoryResult = await trySessionMemoryCompaction(
    messages, toolUseContext, cacheSafeParams, querySource
  )
  if (sessionMemoryResult) {
    setLastSummarizedMessageId(undefined)
    runPostCompactCleanup(querySource)
    return { wasCompacted: true, compactionResult: sessionMemoryResult }
  }

  // ② 降级到 Full Compact(调用 API 生成摘要)
  try {
    const compactionResult = await compactConversation(
      messages, toolUseContext, cacheSafeParams,
      true,       // suppressFollowUpQuestions
      undefined,  // customInstructions
      true,       // isAutoCompact
      recompactionInfo,
    )
    runPostCompactCleanup(querySource)
    return { wasCompacted: true, compactionResult, consecutiveFailures: 0 }
  } catch (error) {
    // 熔断器计数
    return {
      wasCompacted: false,
      consecutiveFailures: (tracking?.consecutiveFailures ?? 0) + 1,
    }
  }
}

5.4 Full Compact 核心实现

文件:src/services/compact/compact.ts

export async function compactConversation(
  messages, context, cacheSafeParams, suppressFollowUpQuestions,
  customInstructions, isAutoCompact, recompactionInfo
): Promise<CompactionResult> {

  // ① 预处理
  const preCompactTokenCount = tokenCountWithEstimation(messages)
  const hookResult = await executePreCompactHooks(...)

  // ② 预处理消息:移除图片、移除已重新注入的附件
  let messagesToSummarize = stripImagesFromMessages(messages)
  messagesToSummarize = stripReinjectedAttachments(messagesToSummarize)

  // ③ 调用 API 生成摘要(带 PTL 重试)
  for (let attempt = 0; attempt < MAX_PTL_RETRIES; attempt++) {
    summaryResponse = await streamCompactSummary({
      messages: messagesToSummarize,
      summaryRequest,
      appState,
      context,
      preCompactTokenCount,
      cacheSafeParams,
    })
    summary = getAssistantMessageText(summaryResponse)

    // 摘要请求本身也可能触发 prompt-too-long
    if (!summary?.startsWith(PROMPT_TOO_LONG_ERROR_MESSAGE)) break

    // 丢弃最早的 API 轮次重试
    const truncated = truncateHeadForPTLRetry(messagesToSummarize, summaryResponse)
    if (!truncated) throw new Error(ERROR_MESSAGE_PROMPT_TOO_LONG)
    messagesToSummarize = truncated
  }

  // ④ 清理状态
  context.readFileState.clear()
  context.loadedNestedMemoryPaths?.clear()

  // ⑤ 创建压缩后附件(并行)
  const [fileAttachments, asyncAgentAttachments] = await Promise.all([
    createPostCompactFileAttachments(context.readFileState, context, 5, []),
    createAsyncAgentAttachmentsIfNeeded(context),
  ])

  // ⑥ 保存跨压缩状态
  const boundaryMarker = createCompactBoundaryMessage(
    isAutoCompact ? 'auto' : 'manual',
    preCompactTokenCount,
    messages.at(-1)?.uuid,
  )

  // 保存已发现的延迟工具
  const preCompactDiscovered = extractDiscoveredToolNames(messages)
  if (preCompactDiscovered.size > 0) {
    boundaryMarker.compactMetadata.preCompactDiscoveredTools =
      [...preCompactDiscovered].sort()
  }

  // ⑦ 创建摘要消息
  const summaryMessages = [
    createUserMessage({
      content: getCompactUserSummaryMessage(summary, ...),
      isCompactSummary: true,
      isVisibleInTranscriptOnly: true,
    }),
  ]

  // ⑧ 恢复附件
  const planAttachment = createPlanAttachmentIfNeeded(context.agentId)
  const skillAttachment = createSkillAttachmentIfNeeded(context.agentId)
  const planModeAttachment = await createPlanModeAttachmentIfNeeded(context)

  // ⑨ 执行 session_start hooks(恢复 CLAUDE.md 等)
  const hookMessages = await executeSessionStartHooks(...)

  // ⑩ 执行 post_compact hooks
  await executePostCompactHooks(...)

  return {
    boundaryMarker,
    summaryMessages,
    attachments: [...fileAttachments, ...skillAttachment, ...planAttachment, ...],
    hookResults: hookMessages,
    preCompactTokenCount,
    postCompactTokenCount: compactionCallTotalTokens,
    truePostCompactTokenCount,
  }
}

5.5 摘要 Prompt 结构

文件:src/services/compact/prompt.ts

摘要请求包含严格的 9 节结构,每节有 <analysis>(思考草稿)和 <summary>(最终输出):

1. Primary Request and Intent — 用户的根本目标
2. Key Technical Concepts — 讨论过的关键技术概念
3. Files and Code Sections — 涉及的文件和代码片段(含代码)
4. Errors and Fixes — 遇到的错误和修复方案
5. Problem Solving — 问题解决过程
6. All User Messages — 所有用户消息(保留反馈的关键)
7. Pending Tasks — 未完成的任务
8. Current Work — 当前工作状态(最详细的一节)
9. Optional Next Step — 建议的下一步(含直接引用)

关键约束

const NO_TOOLS_PREAMBLE =
  `CRITICAL: Respond with TEXT ONLY. Do NOT call any tools.
   Do NOT use Read, Bash, Grep, Glob, Edit, Write, or ANY other tool.
   You already have all the context you need in this conversation.`

5.6 PTL 重试机制

当摘要请求本身触发 prompt-too-long:

摘要请求发送 返回 PTL? prompt-too-long No 摘要完成 Yes 按 API 轮次分组,从最旧开始丢弃 插入截断标记,重试(最多 3 次)
export function truncateHeadForPTLRetry(
  messages: Message[],
  ptlResponse: AssistantMessage,
): Message[] | null {
  // 按 API 轮次分组
  const groups = groupMessagesByApiRound(messages)

  // 计算需要释放的 token 数
  const tokenGap = extractTokenGapFromPTL(ptlResponse)

  // 从最旧的组开始丢弃,直到释放足够空间
  let freedTokens = 0
  let dropCount = 0
  for (const group of groups) {
    freedTokens += estimateGroupTokens(group)
    dropCount++
    if (freedTokens >= tokenGap) break
  }

  // 如果丢弃了第一组(含系统消息),插入占位标记
  if (dropCount > 0 && droppedGroup0) {
    remaining.unshift(createUserMessage({
      content: PTL_RETRY_MARKER,
      // '[earlier conversation truncated for compaction retry]'
    }))
  }

  return remaining
}

六、Compact Boundary — 跨压缩状态保护

6.1 Boundary 消息结构

文件:src/utils/messages.ts

export function createCompactBoundaryMessage(
  trigger: 'manual' | 'auto',
  preTokens: number,
  lastPreCompactMessageUuid?: UUID,
  userContext?: string,
  messagesSummarized?: number,
): SystemCompactBoundaryMessage {
  return {
    type: 'system',
    subtype: 'compact_boundary',
    content: 'Conversation compacted',
    uuid: randomUUID(),
    timestamp: new Date().toISOString(),
    compactMetadata: {
      trigger,
      preTokens,
      userContext,
      messagesSummarized,
      preCompactDiscoveredTools: undefined,
      preservedSegment: undefined,
    },
    logicalParentUuid: lastPreCompactMessageUuid,
  }
}

6.2 状态保护清单

保护项保存位置恢复方式
已发现的延迟工具compactMetadata.preCompactDiscoveredToolsextractDiscoveredToolNames() 扫描 boundary
保留消息段compactMetadata.preservedSegmentdedup-skip 重链
消息链续接logicalParentUuidUI 线程化展示
最近读过的文件Post-compact file attachments重新注入(5 文件 / 50K token)
已调用的技能Post-compact skill attachment重新注入(25K token)
Plan 文件Post-compact plan attachment完整重新注入
异步 agent 状态Post-compact agent attachments完整重新注入
CLAUDE.md 内容session_start hooksHook 重新执行
工具可用性 deltadeferred_tools_delta attachment重新通知
MCP 指令mcp_instructions_delta attachment重新通知

6.3 Post-compact 消息组装

export function buildPostCompactMessages(result: CompactionResult): Message[] {
  return [
    result.boundaryMarker,          // Compact Boundary(含 metadata)
    ...result.summaryMessages,       // 摘要内容
    ...(result.messagesToKeep ?? []), // 保留的最近消息(SM Compact 路径)
    ...result.attachments,           // 文件/技能/计划附件
    ...result.hookResults,           // Hook 结果(CLAUDE.md 等)
  ]
}

6.4 Post-compact 文件恢复

export async function createPostCompactFileAttachments(
  readFileState, toolUseContext, maxFiles, preservedMessages
) {
  // 从 readFileState 中找出最近读过的文件
  const candidates = Object.entries(readFileState)
    .filter(([path]) =>
      !isPlanFile(path) &&           // 排除 plan 文件(单独恢复)
      !isMemoryFile(path) &&         // 排除 memory 文件(CLAUDE.md 单独恢复)
      !isInPreservedMessages(path)   // 排除保留消息中已有的文件
    )
    .sort((a, b) => b.timestamp - a.timestamp)  // 最近读的优先
    .slice(0, maxFiles)              // 最多 5 个

  // 逐个读取,受 token 预算控制
  let totalTokens = 0
  for (const [path, state] of candidates) {
    const content = await readFile(path)
    const tokens = estimateTokens(content)

    if (tokens > POST_COMPACT_MAX_TOKENS_PER_FILE) continue  // 单文件 > 5K → 跳过
    if (totalTokens + tokens > POST_COMPACT_TOKEN_BUDGET) break  // 总计 > 50K → 停止

    attachments.push(createFileAttachment(path, content))
    totalTokens += tokens
  }
  return attachments
}

七、Session Memory Compact — 零 API 调用的压缩

7.1 核心思路

Session Memory 是一个后台 hook 持续维护的「会议纪要」。当需要压缩时,直接用它替代 API 摘要——省去一次完整的 API 调用

文件:src/services/compact/sessionMemoryCompact.ts

7.2 消息保留计算

export function calculateMessagesToKeepIndex(
  messages: Message[],
  lastSummarizedIndex: number,
  config: SessionMemoryCompactConfig,
): number {
  // 从 lastSummarizedIndex + 1 开始,向后扩展
  let startIndex = lastSummarizedIndex + 1
  let totalTokens = 0
  let textBlockMessageCount = 0

  // 向前扩展,直到满足最小要求
  while (startIndex > 0) {
    startIndex--
    totalTokens += estimateMessageTokens(messages[startIndex])
    if (hasTextBlock(messages[startIndex])) textBlockMessageCount++

    // 同时满足:最少 10K token + 最少 5 条文本消息
    if (totalTokens >= config.minTokens &&
        textBlockMessageCount >= config.minTextBlockMessages)
      break

    // 硬上限:40K token
    if (totalTokens >= config.maxTokens) break
  }

  // 不越过上一个 compact boundary
  const lastBoundaryIndex = findLastCompactBoundaryIndex(messages)
  if (lastBoundaryIndex >= 0) startIndex = Math.max(startIndex, lastBoundaryIndex + 1)

  return startIndex
}

7.3 API 不变量保护

export function adjustIndexToPreserveAPIInvariants(
  messages: Message[],
  startIndex: number,
): number {
  // ① 防止 tool_result 失去对应的 tool_use
  while (startIndex > 0) {
    const msg = messages[startIndex]
    if (msg.type === 'user' &&
        hasOrphanedToolResult(msg, messages.slice(0, startIndex))) {
      startIndex--
    } else break
  }

  // ② 防止 thinking block 与 kept assistant 消息共享 message.id

  return startIndex
}

7.4 配置

type SessionMemoryCompactConfig = {
  minTokens: 10_000,          // 最少保留的 token 数
  minTextBlockMessages: 5,    // 最少保留的文本消息数
  maxTokens: 40_000,         // 最多保留的 token 数(硬上限)
}
// GrowthBook key: tengu_sm_compact_config

八、API Microcompact — API 原生上下文管理

文件:src/services/compact/apiMicrocompact.ts

// 策略:清除旧工具结果
{
  type: 'clear_tool_uses_20250919',
  trigger: { token_count: 180_000 },      // 输入达 180K 时触发
  keep: { tail_tool_use_count: N },        // 保留最近 N 个
  clear_tool_inputs: boolean,              // 是否也清除工具输入
}

// 策略:清除 thinking blocks
{
  type: 'clear_thinking_20251015',
  keep: 'all' | { type: 'thinking_turns', value: N },
}

工具清除分类

// 结果可清除的工具(只清 tool_result 内容)
TOOLS_CLEARABLE_RESULTS = [Read, Bash, Grep, Glob, WebSearch, WebFetch, Edit, Write]

// 使用记录可清除的工具(清 tool_use + tool_result)
TOOLS_CLEARABLE_USES = [Edit, Write]

默认阈值

参数默认值环境变量覆盖
触发阈值180,000 tokensAPI_MAX_INPUT_TOKENS
目标 token40,000 tokensAPI_TARGET_INPUT_TOKENS

九、Reactive Compact — 最后防线

9.1 触发条件

query.ts 中,当 API 返回 413 或 media 错误:

if ((isWithheld413 || isWithheldMedia) && reactiveCompact) {
  const compacted = await reactiveCompact.tryReactiveCompact({...})
  if (compacted) {
    const postCompactMessages = buildPostCompactMessages(compacted)
    yield* postCompactMessages
    messagesForQuery = postCompactMessages
    autoCompactTracking = undefined  // 重置追踪
  }
}

9.2 与 Auto-compact 的关系

启用 Reactive Compact 后,Auto-compact 被抑制

// autoCompact.ts
if (feature('REACTIVE_COMPACT')) {
  if (getFeatureValue_CACHED_MAY_BE_STALE('tengu_cobalt_raccoon', false)) {
    return false  // 不主动压缩
  }
}

设计意图:Reactive Compact 从消息尾部剥离,保留 prompt cache 前缀。而 Auto-compact 替换全部消息为摘要,会破坏 cache。在 cache 命中率高的场景下,Reactive 更划算。


十、Post-compact Cleanup — 状态重置

文件:src/services/compact/postCompactCleanup.ts

export function runPostCompactCleanup(querySource?: QuerySource): void {
  const isMainThread = querySource === undefined
    || querySource.startsWith('repl_main_thread')
    || querySource === 'sdk'

  // 始终重置
  resetMicrocompactState()       // Cached MC 追踪状态
  clearSystemPromptSections()    // 系统提示缓存
  clearClassifierApprovals()     // Auto-mode 分类器审批缓存
  clearSpeculativeChecks()       // Bash 权限推测缓存
  clearBetaTracingState()        // Beta 遥测状态
  clearSessionMessagesCache()    // 会话消息缓存

  // 仅主线程重置(防止 fork agent 破坏共享状态)
  if (isMainThread) {
    resetContextCollapse()             // Context Collapse 状态
    getUserContext.cache.clear()       // 记忆化的上下文缓存
    resetGetMemoryFilesCache('compact') // Memory 文件缓存
  }

  // 条件重置
  if (feature('COMMIT_ATTRIBUTION')) {
    sweepFileContentCache()            // 文件归因缓存
  }

  // 有意不重置的:
  // - invoked skill content(跨多次压缩存活,用于 skill attachment)
  // - sent skill names(重新注入成本低)
}

主线程保护:fork agent(如 Session Memory、extractMemories)执行压缩时,不重置主线程的模块级状态。否则会导致 getUserContext 缓存被意外清空。


十一、关键数据结构

11.1 CompactionResult

export interface CompactionResult {
  boundaryMarker: SystemMessage       // Compact Boundary 消息
  summaryMessages: UserMessage[]       // 摘要内容
  attachments: AttachmentMessage[]     // 恢复附件(文件/技能/计划)
  hookResults: HookResultMessage[]     // Hook 执行结果
  messagesToKeep?: Message[]           // 保留的原始消息(SM Compact 路径)
  userDisplayMessage?: string          // 给用户看的提示

  // Token 计量
  preCompactTokenCount?: number        // 压缩前 token 数
  postCompactTokenCount?: number       // 摘要 API 调用消耗
  truePostCompactTokenCount?: number   // 压缩后真实上下文大小
  compactionUsage?: TokenUsage         // 详细 token 用量
}

11.2 AutoCompactTrackingState

export type AutoCompactTrackingState = {
  compacted: boolean               // 本轮是否已压缩
  turnCounter: number              // 距上次压缩的轮次数
  turnId: string                   // 当前轮次 ID
  consecutiveFailures?: number     // 连续失败次数(熔断器计数)
}

11.3 Token 预算常量

常量用途
AUTOCOMPACT_BUFFER_TOKENS13,000触发阈值与有效窗口的间距
WARNING_THRESHOLD_BUFFER_TOKENS20,000UI 警告与触发阈值的间距
ERROR_THRESHOLD_BUFFER_TOKENS20,000错误态与触发阈值的间距
MANUAL_COMPACT_BUFFER_TOKENS3,000/compact 手动命令的阻塞限制
MAX_OUTPUT_TOKENS_FOR_SUMMARY20,000预留给摘要输出的 token
POST_COMPACT_TOKEN_BUDGET50,000文件附件总预算
POST_COMPACT_MAX_TOKENS_PER_FILE5,000单文件预算
POST_COMPACT_SKILLS_TOKEN_BUDGET25,000技能附件总预算
POST_COMPACT_MAX_TOKENS_PER_SKILL5,000单技能预算
POST_COMPACT_MAX_FILES_TO_RESTORE5最多恢复文件数
MAX_PTL_RETRIES3PTL 重试次数
MAX_CONSECUTIVE_AUTOCOMPACT_FAILURES3熔断器阈值
MAX_COMPACT_STREAMING_RETRIES2流式请求重试次数

十二、完整决策流程图

用户发送消息 / 工具执行完毕 query.ts 主循环 ① HISTORY SNIP 直接丢弃最早消息(实验中) ② MICROCOMPACT gap > 60min? Yes → 清空旧工具结果内容(cache 已冷) Cached MC 启用? 工具数 > 阈值? Yes → 生成 cache_edits(cache 不破) 都不满足 → 跳过 ③ CONTEXT COLLAPSE(如果启用,抑制 ④) ④ token > 阈值? AUTO-COMPACT No 正常继续 Yes 熔断器? 失败 >= 3 Yes 跳过 No Session Memory 可用? Yes SM 摘要(零 API) No Full Compact Fork Agent 生成摘要 PTL? → 丢弃最旧轮次重试 创建 Boundary / 恢复文件/技能 保存延迟工具状态 / 执行 hooks 发送 API 请求 ⑤ REACTIVE COMPACT API 返回 413? → 从尾部剥离,保留 cache 前缀 ⑥ API MICROCOMPACT 输入 > 180K? → API 端 context_management

十三、Feature Gates 与环境变量

Feature Gates(构建时死代码消除)

Flag控制
CACHED_MICROCOMPACT启用 cache_edits 微压缩
HISTORY_SNIP启用消息丢弃
CONTEXT_COLLAPSE启用细粒度上下文管理
REACTIVE_COMPACT启用被动 413 压缩
PROMPT_CACHE_BREAK_DETECTIONCache 断裂检测
COMMIT_ATTRIBUTION提交归因缓存

GrowthBook Flags(运行时 A/B)

Flag控制
tengu_cobalt_raccoon启用 Reactive Compact 抑制 Auto-compact
tengu_compact_cache_prefix启用 cache 前缀共享压缩
tengu_compact_streaming_retry启用流式压缩重试
tengu_sm_compact_configSession Memory Compact 配置
tengu_slate_heronTime-based MC 配置

环境变量

变量作用
DISABLE_COMPACT完全禁用所有压缩
DISABLE_AUTO_COMPACT仅禁用自动压缩
CLAUDE_CODE_AUTO_COMPACT_WINDOW覆盖上下文窗口大小
CLAUDE_AUTOCOMPACT_PCT_OVERRIDE覆盖触发阈值百分比
CLAUDE_CODE_BLOCKING_LIMIT_OVERRIDE覆盖 /compact 阻塞限制
API_MAX_INPUT_TOKENS覆盖 API MC 触发阈值
API_TARGET_INPUT_TOKENS覆盖 API MC 目标 token

十四、关键源码文件清单

src/services/compact/ compact.ts 核心压缩逻辑(compactConversation、post-compact 恢复) autoCompact.ts 自动压缩触发(阈值计算、熔断器、编排) microCompact.ts 微压缩(Time-based + Cached MC 双路径) cachedMicrocompact.ts Cached MC 配置和模型支持检查 apiMicrocompact.ts API 原生 context_management 策略 sessionMemoryCompact.ts Session Memory 压缩(零 API 调用路径) postCompactCleanup.ts 压缩后状态重置(cache 清理、主线程保护) grouping.ts 消息按 API 轮次分组(PTL 重试用) prompt.ts 摘要 Prompt(9 节结构化模板) src/query.ts 主循环中的压缩调用链 src/query/deps.ts 依赖注入(microcompact、autocompact) src/utils/messages.ts Compact Boundary 消息创建

十五、可借鉴的设计模式

模式Claude Code 做法可借鉴场景
分层压缩流水线5 层从轻到重逐级升级任何需要上下文管理的长对话 Agent
Cache 感知压缩Cached MC 不改本地消息,只发 cache_edits高 cache 命中率场景
冷热判断Time-based MC 检测 gap > 60min 判断 cache 已冷有 cache TTL 的 API 调用
预计算摘要Session Memory 替代 API 摘要调用省 API 调用成本
熔断器连续 3 次失败后停止尝试任何可能反复失败的后台操作
PTL 分组重试按 API 轮次分组丢弃,保留结构完整性处理 prompt-too-long 的通用策略
Boundary 状态快照在边界消息中保存跨压缩状态任何有状态的消息流处理
Post-compact 恢复带 token 预算的文件/技能重新注入压缩后的上下文重建
主线程保护fork agent 不重置主线程模块级状态多线程/多 agent 共享状态场景
依赖注入microcompact/autocompact 通过 QueryDeps 注入测试隔离、策略替换

文件来源:Claude Code npm 包 source map 泄露快照 (2026-03-31)