
对 Claude Code 外泄的 harness 内部所有记忆与上下文管理系统做一次完整逆向拆解,从亚毫秒级的轻量 token 修剪,到你睡觉时也会把记忆做整合的“梦境”系统。
1. 问题:无界世界里的有界上下文

LLM 有个根本限制,上下文窗口是固定的。Claude Code 通常运行在 200K token 的窗口里(加上 [1m] 后缀可扩到 1M)。一次编码会话很容易就超了,读几个文件,跑点 grep,来回改几轮,就到顶了。
Claude Code 用一套七层记忆架构解决这个问题,跨度从亚毫秒级 token 修剪到耗时数小时的后台“梦境”整合。每一层越往后越贵,但能力也越强。整个系统的设计目标是,让更便宜的层尽量挡住更昂贵的层,不让它们被触发。
Token 计数:地基
一切从知道用了多少 token 开始。权威函数在 src/utils/tokens.ts 里,叫 tokenCountWithEstimation() :
标准 token 计数 = 上一次 API 响应的 usage.input_tokens + 对之后新增消息的粗略估算
粗估用的是一个很简单的启发式规则:大多数文本按 每 token 4 字节估,JSON(token 更密)按 每 token 2 字节估。图片和文档不管大小,都统一按 2,000 token 估算。
上下文窗口如何确定
系统按一条优先级链路来决定可用的上下文窗口:
[1m] 模型后缀 → 模型能力查询 → 1M beta header → 环境变量覆盖 → 200K 默认值
有效上下文窗口会先扣掉 20K token 作为压缩输出的预留。窗口不能全用满,因为还得留位置生成那段救命的总结。
2. 架构总览:七层记忆
每一层由不同条件触发,成本也不同。系统尽量做到 第 N 层能阻止第 N+1 层被触发。
# Session Title
_A short and distinctive 5-10 word descriptive title_
# Current State
_What is actively being worked on right now?_
# Task specification
_What did the user ask to build?_
# Files and Functions
_Important files and their relevance_
# Workflow
_Bash commands usually run and their interpretation_
# Errors & Corrections
_Errors encountered and how they were fixed_
# Codebase and System Documentation
_Important system components and how they fit together_
# Learnings
_What has worked well? What has not?_
# Key results
_If the user asked for specific output, repeat it here_
# Worklog
_Step by step, what was attempted and done_
3. 第 1 层:工具结果存储
-
File: src/utils/toolResultStorage.ts
-
Cost: 只有磁盘 I/O,没有 API 调用
-
When: 每次产生工具结果,立刻执行
问题
在代码库里跑一次 grep,很容易就返回 100KB+ 文本。大文件 cat 一下也可能有 50KB。这些结果会吞掉大量上下文,而且对话推进几分钟后就变得不再重要。
解法
每个工具结果在进上下文前都会走一套预算系统:
当结果超过阈值时:
-
把完整结果写到磁盘 tool-results/<sessionId>/<toolUseId>.txt
-
把预览(前 ~2KB)放进上下文,并包在 <persisted-output> 标签里
-
之后如果需要,模型可以用 Read 再把完整结果读回来
ContentReplacementState:缓存稳定的决策
这里有个关键细节:一旦某个工具结果被替换成预览,这个决定就会被 ContentReplacementState 冻结。后续每次 API 调用,同一个结果都会拿到同一个预览,这样才能保证提示前缀字节级一致,从而命中 prompt cache。这个状态还会被写进 transcript,所以即使恢复会话也能延续。
GrowthBook 覆盖

每个工具的阈值可以通过 tengu_satin_quoll 特性开关远程调整,让 Anthropic 不用发版就能给特定工具改持久化阈值。
4. 第 2 层:微压缩(Microcompaction)
-
File: src/services/compact/microCompact.ts
-
Cost: 零到极低的 API 成本
-
When: 每一轮对话,在 API 调用前
微压缩是最轻量的上下文减负。它不做总结,只是清掉那些大概率不会再用到的旧工具结果。
三种不同机制
a) 基于时间的微压缩
Trigger: 距离上一条 assistant 消息的空闲间隔超过阈值(默认:60 分钟)
Rationale: Anthropic 的服务端 prompt cache TTL 大概是 1 小时。如果 1 小时没发请求,缓存就过期了,整个提示前缀会被重新 tokenize。既然反正要重写,不如先把旧工具结果清掉,缩小要重写的体积。
Action: 把工具结果内容替换成 [Old tool result content cleared],同时至少保留最近 N 条结果(下限为 1)。
Configuration(通过 GrowthBook tengu_slate_heron):
b) 缓存微压缩(Cache-Editing API)

这是最有技术含量的一种。它不改本地消息(那会让 prompt cache 失效),而是用 API 的 cache_edits 机制,在不破坏前缀的前提下,从服务端缓存里把工具结果删掉。
工作方式:
-
工具结果出现时,会登记到全局 CachedMCState 里
-
当数量超过阈值,就挑出最旧的一批(扣掉“保留最近”的缓冲)准备删除
-
生成一个 cache_edits 块,并跟着下一次 API 请求一起发出去
-
服务端从缓存前缀里删除指定的工具结果
-
本地消息不变,删除只发生在 API 层
关键安全点:只在主线程运行。如果分叉出来的子代理(session_memory、agent_summary 等)去改全局状态,会把主线程的 cache editing 搞坏。
c) API 级上下文管理(apiMicrocompact.ts)
一种更新的服务端方案,通过 context_management 这个 API 参数来做:
compactMetadata = {
type: 'auto' | 'manual',
preCompactTokenCount: number,
compactedMessageUuid: UUID, // Last msg before boundary
preCompactDiscoveredTools: string[], // Loaded deferred tools
preservedSegment?: { // Session memory path only
headUuid, anchorUuid, tailUuid
}
}
这让 API 服务器原生处理上下文管理,客户端不用再自己追踪或清理工具结果。
哪些工具结果允许被清理?
只有这些工具的结果会被清掉:
FileRead, Bash/Shell, Grep, Glob, WebSearch, WebFetch, FileEdit, FileWrite
缺席得很明显的有:thinking blocks、assistant 文本、用户消息、MCP 工具结果。
5. 第 3 层:会话记忆(Session Memory)
SessionMemoryCompactConfig = {
minTokens: 10_000, // Minimum tokens to preserve
minTextBlockMessages: 5, // Minimum messages with text blocks
maxTokens: 40_000 // Hard cap on preserved tokens
}
-
Files: src/services/SessionMemory/
-
Cost: 每次提取要跑一个分叉子代理的 API 调用
-
When: 对话过程中周期性触发(post-sampling hook)
核心想法
不等到上下文快满才慌张总结,而是持续维护笔记。这样一旦需要压缩,摘要已经在那儿了,不用再额外付一次昂贵的总结成本。
会话记忆模板
每个会话都会有一个 markdown 文件,路径是 ~/.claude/projects/<slug>/.claude/session-memory/<sessionId>.md,里面是结构化模板:
触发逻辑

会话记忆提取在同时满足两个条件时触发:
自上次提取以来 token 增长 ≥ minimumTokensBetweenUpdate AND(自上次提取以来工具调用次数 ≥ toolCallsBetweenUpdates OR 上一轮 assistant 没有工具调用)
token 门槛永远必须满足,即使工具调用门槛满足也不例外。上一轮无工具调用这条是为了捕捉自然的对话断点,也就是模型刚好结束了一段工作流程的时候。
提取如何执行
提取通过 runForkedAgent() 以分叉子代理执行:
-
querySource: 'session_memory'
-
只允许用 FileEdit 改记忆文件(其他工具全部禁用)
-
共享父代理的 prompt cache 以节省成本
-
通过 sequential() 包装顺序执行,避免多次提取互相重叠
会话记忆压缩:兑现价值的地方
当 autocompact 触发时,会先尝试 trySessionMemoryCompaction():
-
检查会话记忆是否真的有内容(不是空模板)
-
直接把会话记忆 markdown 当作压缩摘要,不需要 API 调用
-
计算要保留的最近消息范围(从 lastSummarizedMessageId 向后扩,直到满足最小保留要求)
-
返回一个 CompactionResult,包含会话记忆摘要 + 保留的最近消息
Configuration:
关键洞察:会话记忆压缩比完整压缩便宜太多,因为摘要已经存在了。没有 summarizer API 调用,没有额外的 prompt 构造,也没有输出 token 成本,只是把会话记忆文件注入为摘要。

6. 第 4 层:完整压缩(Full Compaction)
-
File: src/services/compact/compact.ts
-
Cost: 一次完整 API 调用(输入=整段对话,输出=摘要)
-
When: 上下文超过 autocompact 阈值且会话记忆压缩不可用
触发条件
effective context window = context window - 20K(预留给输出) autocompact threshold = effective window - 13K(缓冲)
如果 tokenCountWithEstimation(messages) > autocompact threshold → 触发
熔断器(Circuit Breaker)
连续失败 3 次后,autocompact 本次会话里就不再尝试了。之所以加这一条,是因为发现有 1,279 个会话出现了 50+ 次连续失败(单会话最高 3,272 次),导致全球每天白白浪费大约 25 万次 API 调用。
压缩算法
Step 1: 预处理
-
执行用户配置的 PreCompact hooks
-
从消息里去掉图片(用 [image] 标记替代)
-
去掉 skill discovery/listing 附件(稍后再注入回来)
Step 2: 生成摘要 系统会分叉一个 summarizer 代理,用很详细的提示请求一个 9 段式摘要:
---
name: testing-approach
description: User prefers integration tests over mocks after a prod incident
type: feedback
---
Integration tests must hit a real database, not mocks.
**Why:** Prior incident where mock/prod divergence masked a broken migration.
**How to apply:** When writing tests for database code, always use the test database helper.
提示里用了一种很巧的两段式输出结构:
-
先输出 <analysis> 块,作为草稿区让模型整理思路
-
再输出 <summary> 块,作为最终结构化摘要
-
<analysis> 块在摘要进入上下文前会被剥离,它能提升摘要质量,但不会消耗压缩后的 token 预算
Step 3: 压缩后恢复关键上下文
压缩完成后,会把关键上下文再注入回来:
-
最近读过的前 5 个文件(每个 5K tokens,总预算 50K)
-
已调用的 skill 内容(每个 5K tokens,总预算 25K)
-
Plan 附件(如果在 plan mode)
-
延迟加载的工具 schema、agent 列表、MCP 指令
-
SessionStart hooks 重新执行(恢复 CLAUDE.md 等)
-
会话元数据重新追加,供 --resume 展示
Step 4: 边界消息
用一条 SystemCompactBoundaryMessage 标记压缩点:
部分压缩(Partial Compaction)

两种方向变体,用更“手术式”的方式管理上下文:
-
from direction: 总结 pivot index 之后的消息,保留更早的消息不动。因为保留的前缀不变,所以能保住 prompt cache。
-
up_to direction: 总结 pivot 之前的消息,保留更晚的消息。因为摘要会改写前缀,所以会打破缓存。
Prompt-Too-Long 的恢复策略
如果压缩请求本身就触发 prompt-too-long(对话大到 summarizer 都吃不下):
-
用 groupMessagesByApiRound() 按 API 轮次分组
-
丢掉最老的分组,直到 token 缺口补齐(或者在缺口无法解析时丢掉 20% 的分组)
-
最多重试 3 次
-
全部失败就抛出 ERROR_MESSAGE_PROMPT_TOO_LONG
7. 第 5 层:自动记忆提取(Auto Memory Extraction)
-
File: src/services/extractMemories/extractMemories.ts
-
Cost: 一次分叉代理 API 调用
-
When: 每个完整 query loop 结束时触发(模型给出最终回复且没有工具调用)
目的
会话记忆记录的是当前会话的笔记,而自动记忆提取会把信息沉淀成跨会话可复用的长期知识,持久化在 ~/.claude/projects/<path>/memory/ 里。
记忆类型
有四类记忆,每类都有自己的保存规则:
CacheSafeParams = {
systemPrompt: SystemPrompt, // Must be byte-identical to parent
userContext: { [k: string]: string },
systemContext: { [k: string]: string },
toolUseContext: ToolUseContext, // Contains tools, model, options
forkContextMessages: Message[], // Parent's conversation (cache prefix)
}
记忆文件格式
TimeBasedMCConfig = {
enabled: false, // Master switch
gapThresholdMinutes: 60, // Trigger after 1h idle
keepRecent: 5 // Keep last 5 tool results
}
明确不保存什么
提取提示会明确排除:
-
代码风格、约定、架构(都能从代码推出来)
-
Git 历史(用 git log/git blame)
-
调试结论(修复已经写进代码)
-
任何写在 CLAUDE.md 里的内容
-
临时性任务细节
与主代理互斥
如果主代理在当前回合已经写过记忆文件,就会跳过提取,避免后台代理重复劳动:
function hasMemoryWritesSince(messages, sinceUuid): boolean { // 扫描针对 auto-memory 路径的 Edit/Write tool_use 块 // 如果主代理已经保存过记忆则返回 true }
执行策略
提取提示会要求代理在有限回合预算内高效执行:
Turn 1: 并行发出所有可能需要更新文件的 FileRead Turn 2: 并行发出所有 FileWrite/FileEdit 不要在多个回合之间交错读写。
MEMORY.md:索引文件
MEMORY.md 是索引,不是记忆倾倒。每条索引应该是一行,长度大约小于 150 字符:
硬限制是 200 行或 25KB,先到哪个算哪个。超过 200 行的部分在加载进 system prompt 时会被截断。
8. 第 6 层:梦境(Dreaming)
1. Primary Request and Intent
2. Key Technical Concepts
3. Files and Code Sections (with code snippets)
4. Errors and Fixes
5. Problem Solving
6. All User Messages (verbatim — critical for intent tracking)
7. Pending Tasks
8. Current Work
9. Optional Next Step
-
File: src/services/autoDream/autoDream.ts
-
Cost: 一次分叉代理 API 调用(可能多回合)
-
When: 后台运行,在累积了足够时间与会话之后触发
概念
梦境是一种跨会话记忆整合。后台进程会回顾过去的会话 transcript,持续改进 memory 目录。它很像生物的记忆巩固机制,睡眠时会回放、归类、整合当天经验,写入长期存储。
门禁序列(先便宜后昂贵)
梦境系统用的是级联门禁,每一关都比下一关更便宜,所以大多数回合会很早就退出。
锁机制
<memoryDir>/.consolidate-lock 这个锁文件一物两用:
Path: <autoMemPath>/.consolidate-lock Body: Process PID(单行) mtime: lastConsolidatedAt 时间戳(锁本身就是时间戳)
-
Acquire: 写入 PID → mtime = now。随后重读验证 PID(防竞态)。
-
Success: mtime 保持为 now(标记整合完成时间)。
-
Failure: rollbackConsolidationLock(priorMtime) 用 utimes() 回滚 mtime。
-
Stale: 如果 mtime 超过 60 分钟且 PID 不在运行 → 回收。
-
Crash recovery: 检测到死 PID → 下一个进程回收。
四阶段整合
梦境代理会收到一个结构化提示,定义了四个阶段:
Phase 1 — 定位:
-
ls memory 目录
-
读 MEMORY.md 了解当前索引
-
快速翻已有主题文件,避免造重复
Phase 2 — 收集最近信号:
-
如果有的话,查看日记 logs/YYYY/MM/YYYY-MM-DD.md
-
检查是否出现漂移的记忆(与当前代码库矛盾的事实)
-
针对具体线索,窄范围 grep transcript:grep -rn "<narrow term>" transcripts/ --include="*.jsonl" | tail -50
-
“不要把 transcript 全读一遍。只看那些你已经怀疑确实重要的点。”
Phase 3 — 整合:
-
写入或更新记忆文件
-
把新信号合并进已有主题文件,而不是新建近重复文件
-
把相对日期改成绝对日期("yesterday" → "2026-03-30")
-
在源头删除已被推翻的事实
Phase 4 — 修剪与索引:
-
更新 MEMORY.md,保证不超过 200 行 / 25KB
-
去掉指向过期/错误/被替代记忆的索引
-
把冗长的索引条目缩短(细节应该放进主题文件)
-
解决文件之间的矛盾
工具约束
梦境代理在严格限制下运行:
-
Bash: 只允许只读命令(ls、find、grep、cat、stat、wc、head、tail)
-
Edit/Write: 只能写 memory 目录路径
-
不允许 MCP 工具、不允许 Agent 工具、不允许破坏性操作
UI 展示
梦境会在 footer pill 里作为后台任务显示,有一个两段式状态机:
DreamPhase: 'starting' → 'updating'(第一次 Edit/Write 落地时) Status: running → completed | failed | killed
用户可以在后台任务对话框里杀掉梦境任务,锁的 mtime 会回滚,下次会话还能重试。
进度跟踪
系统会监控梦境代理的每一轮 assistant 输出:
-
抓取文本块用于 UI 展示
-
统计 tool_use 块数量
-
收集 Edit/Write 的文件路径用于完成摘要
-
实时展示最多保留最近 30 轮
9. 第 7 层:跨代理通信
-
Files: src/utils/forkedAgent.ts, src/tools/AgentTool/, src/tools/SendMessageTool/
-
Cost: 随具体模式变化
-
When: 代理生成、后台任务、队友协作
分叉代理模式(Forked Agent Pattern)
Claude Code 里几乎所有后台操作(会话记忆、自动记忆、梦境、压缩、agent summary 等)都建立在分叉代理模式之上,这是底座:
ContextEditStrategy =
| { type: 'clear_tool_uses_20250919', // Clear old tool results
trigger: { type: 'input_tokens', value: 180_000 },
clear_at_least: { type: 'input_tokens', value: 140_000 } }
| { type: 'clear_thinking_20251015', // Clear old thinking blocks
keep: { type: 'thinking_turns', value: 1 } | 'all' }
分叉会创建一个隔离的上下文,并复制可变状态:
-
readFileState: 复制 LRU 缓存(避免互相污染)
-
abortController: 子控制器与父控制器联动
-
denialTracking: 全新追踪状态
-
ContentReplacementState: 复制(保住缓存稳定的替换决策)
但它又通过保持 cache-critical 参数一致来共享 prompt cache。API 看到同一段前缀,就会命中缓存。
Agent 工具:生成子代理
Agent 工具支持多种生成模式:
防递归分叉:分叉出来的子代理仍然带着 Agent 工具(保证工具定义一致以便共享缓存),但会检测对话历史里的 <fork_boilerplate_tag>,从而拒绝递归分叉。

为了共享缓存而构造的分叉消息:
所有分叉子代理都会生成字节级一致的 API 请求前缀: 1. 完整复制父代理上一条 assistant 消息(包含全部 tool_use、thinking、text) 2. 再加一条用户消息,里面包含: - 为每个 tool_use 准备相同的占位结果 - 每个子代理各自的指令文本块(只有这一段不同) → 尽可能让并发分叉之间共享同一段 prompt cache
SendMessage:代理间通信
SendMessage 工具让代理之间能在运行时互相发消息:
SendMessage({ to: 'research-agent', // 或用 '' 广播,或 'uds:<path>'、'bridge:<id>'* message: '检查第 5 节', summary: '请求复核该章节' })
路由逻辑:
-
进程内按名字找子代理 → queuePendingMessage() → 在下一轮工具边界时被 drain
-
环境团队(基于进程) → writeToMailbox() → 文件邮箱
-
跨会话 → 通过 bridge/UDS 调 postInterClaudeMessage()
结构化消息用于生命周期控制:
-
shutdown_request / shutdown_response — 协调代理优雅退出
-
plan_approval_response — leader 同意或拒绝队友的 plan
代理记忆:跨调用的持久状态
代理可以在三种范围里维持跨调用的持久记忆:
Agent Summary:周期性的进度快照

对 coordinator 模式的子代理,会每 30 秒把对话分叉一次,生成一个 3–5 个词的进度摘要:
Good: "Reading runAgent.ts" Good: "Fixing null check in validate.ts" Bad: "Investigating the issue" (too vague) Bad: "Analyzed the branch diff" (past tense)
使用 Haiku(最便宜的模型),并禁用所有工具,这就是纯文本生成任务。
10. Query Loop:如何拼在一起
File: src/query.ts

11. Prompt 缓存优化
Claude Code 架构里最精妙的部分之一,是它对 prompt cache 的偏执优化。几乎每个设计决定都会先看对缓存的影响。
问题是什么
Anthropic 的 API 会在服务端缓存 prompt 前缀(TTL 大约 1 小时)。缓存命中意味着只为新增 token 付费。缓存未命中则要把整个 prompt 重新 tokenize。对于 200K token,这个差别大概是每次请求 ~$0.003 和 ~$0.60 的差距。
保持缓存不破的模式
1. CacheSafeParams:每个分叉代理(会话记忆、压缩、梦境、提取)都会继承父代理完全相同的 system prompt、工具和消息前缀。分叉后的 API 请求前缀一致 → 缓存命中。
2. renderedSystemPrompt:分叉线程直接复用父代理已经渲染好的 system prompt 字节,避免重复渲染带来的差异(比如 GrowthBook flag 在两次渲染之间变了)。
3. ContentReplacementState 复制:工具结果的持久化决策会被冻结。同样的结果每次都会得到同样的预览 → 前缀稳定。
4. 缓存微压缩:用 cache_edits 改服务端缓存而不改本地前缀 → 不破缓存。
5. 分叉消息构造:所有分叉子代理拿到字节级一致的前缀,只有最后一段指令不同 → 并发分叉最大化共享缓存。
6. 压缩后的缓存断裂通知:压缩后调用 notifyCompaction() 重置缓存基线,这样预期中的压缩后缓存未命中不会被误判为异常。
缓存断裂检测
系统会通过 promptCacheBreakDetection.ts 主动监控非预期的缓存未命中,并标记出来排查。已知合理的缓存断裂(压缩、微压缩等)会提前登记,避免误报。
12. 关键数字
上下文阈值
工具结果预算
会话记忆

压缩

梦境

微压缩

13. 设计原则
1. 分层防御,先便宜后昂贵
每一层上下文管理,都是为了不让下一层更贵的机制触发:
-
工具结果存储让微压缩需要清掉的东西更少
-
微压缩避免触发会话记忆压缩
-
会话记忆压缩避免触发完整压缩
-
完整压缩避免上下文溢出错误
2. Prompt 缓存优先
几乎所有设计决定都把 prompt cache 影响放在前面。系统会用尽办法保持 API 请求前缀字节级一致,包括冻结的 ContentReplacementState、复用渲染后的 system prompt、cache_edits API、统一的分叉消息构造。
3. 隔离但共享
分叉代理复制可变状态来隔离(避免互相污染),同时共享 prompt cache 前缀(避免成本爆炸)。这是个微妙平衡,隔离过度会浪费缓存,共享过度会引入 bug。
4. 到处都是熔断器
-
Autocompact:三次失败止损
-
Dream scan:10 分钟节流
-
Dream lock:基于 PID 的互斥锁 + 过期回收
-
会话记忆:顺序执行包装
-
记忆提取:与主代理写入互斥
5. 优雅降级
每套系统失败时都会静默让位给下一层。会话记忆压缩失败就返回 null → 交给完整压缩。梦境拿锁失败 → 下次会话再试。提取记忆出错 → 只记录日志,不抛出。
6. 特性开关作为紧急刹车
几乎每个系统都被 GrowthBook 特性开关门控:
-
tengu_session_memory — 会话记忆
-
tengu_sm_compact — 会话记忆压缩
-
tengu_onyx_plover — 梦境
-
tengu_passport_quail — 自动记忆提取
-
tengu_slate_heron — 基于时间的微压缩
-
CACHED_MICROCOMPACT — 缓存编辑微压缩
-
CONTEXT_COLLAPSE — 上下文折叠
-
HISTORY_SNIP — 消息裁剪
只要某个系统出问题,就能不发版快速回滚。
7. 需要时就互斥
- Context Collapse ↔ Autocompact(collapse 自己管上下文)