随笔
Claude Code 深度源码解读 06:Memory 是受控上下文,不是玄学记忆
区分 memory prompt、CLAUDE.md、relevant memory、nested memory 与 session memory 的入口和边界。
简化版入口:Memory 连续性。
我的设问与回应
设问 1:Memory 是不是模型真的记住了?
回应答案: 不是。源码层的 Memory 是外部材料按条件进入当前上下文,包括 memory prompt、CLAUDE.md、relevant memory、nested memory、session memory。
证据与证明路径: 证据来自第 6 期对 loadMemoryPrompt、userContext、relevant memory prefetch、nestedMemoryAttachmentTriggers、session memory forked agent 的拆解。
还值得继续学习或反思: 继续学习 memory 的 source、scope、freshness、visibility 怎样被记录。
设问 2:Memory 为什么可能变成风险?
回应答案: 因为 stale memory、noisy memory、scope leak、compact distortion 都可能污染当前任务。Memory 提供连续性,也会带来长期上下文污染。
证据与证明路径: 证据是第 6 期风险边界部分对上下文污染、写入权限、记忆作用域的讨论。
还值得继续学习或反思: Memory 不是越多越好,而是越可治理越好。
源码调研主体
本组解读基于冻结源码快照进行教育、防御和架构研究。
本期主题:Harness 如何把长期记忆、项目指令、相关记忆召回、嵌套目录规则和 session memory 接入当前任务,同时避免把“记忆”误写成每轮动态事实。
1. 研究问题
本期围绕一个问题展开:
Claude Code Harness 如何让跨回合经验进入当前任务,又不让旧信息、重复信息或场景化记忆污染主循环?
Memory 在 Harness 中不是单一数据库,也不是每轮都强制召回完整历史。源码中至少有五条不同路径:
- system prompt 中的 memory mechanics:告诉模型如何使用持久化文件记忆。
- CLAUDE.md / 规则文件:作为用户/项目上下文进入模型窗口。
- relevant memories:按当前 prompt 异步检索少量相关记忆。
- nested memory:由文件路径触发的目录级 CLAUDE.md / rules 注入。
- session memory:长会话中由 post-sampling hook 抽取当前会话笔记,并可参与 compact。
本期重点是区分这些机制的触发条件和边界。不能把某个 feature gate、agent memory 或 session memory 写成所有会话必经路径。
2. 对应总览节点
对应第 1 期总览图中的节点:
MemoryContext PreparationQuery LoopAttachmentsCompaction / RecoveryTranscript / Session State
这部分机制参考前文「Memory 连续性 机制图」。
3. 核心流程图
路线图中的代表链路是:
memory prompt / relevant memory / nested memory / session memory源码中这几条链路并行存在,颜色标注如下:蓝色是主线前置上下文,绿色是按需召回,橙色是异步/恢复支撑,红色是权限隔离。
这部分机制参考前文「Memory 连续性 机制图」。
4. 关键源码入口
| 职责 | 文件 |
|---|---|
| memory prompt 注入点 | src/QueryEngine.ts |
| memory prompt 构造、auto/team/KAIROS 分支 | src/memdir/memdir.ts |
| memory 类型说明 | src/memdir/memoryTypes.ts |
| relevant memory 检索 | src/memdir/findRelevantMemories.ts |
| relevant memory / nested memory 附件 | src/utils/attachments.ts |
| agent memory 目录 | src/tools/AgentTool/agentMemory.ts |
| agent memory snapshot | src/tools/AgentTool/agentMemorySnapshot.ts |
| session memory 抽取 | src/services/SessionMemory/sessionMemory.ts |
| session memory prompt | src/services/SessionMemory/prompts.ts |
| session memory compact | src/services/compact/sessionMemoryCompact.ts |
| auto compact 中 session memory 优先路径 | src/services/compact/autoCompact.ts |
/memory 命令 | src/commands/memory/memory.tsx |
remember skill | src/skills/bundled/remember.ts |
4.1 主线与场景特殊处理分类
| 机制/模块 | 分类 | 为什么这样归类 |
|---|---|---|
loadMemoryPrompt() | 场景增强 + 主线前缀支撑 | 只有 auto memory 开启或 SDK 自定义 prompt + memory path override 时注入;注入的是使用说明,不是完整动态记忆内容 |
getUserContext() 中 CLAUDE.md | 主线机制 + 场景增强 | Context 构建会读取用户/项目上下文,但具体是否有 CLAUDE.md、local rules、managed rules 取决于文件存在和设置 |
buildMemoryLines() / buildCombinedMemoryPrompt() | 场景增强 | auto/team memory 分支由 feature、settings 和路径状态决定 |
| KAIROS daily-log prompt | 场景增强 | assistant 长会话模式下改变“新记忆写到哪里”,不是普通 REPL 的默认路径 |
startRelevantMemoryPrefetch() | 场景增强 + 可观测/恢复支撑 | 只在 auto memory 与相关 feature gate 开启、prompt 有足够文本且未超过会话 memory 字节阈值时启动;不会阻塞当前 turn |
filterDuplicateMemoryAttachments() | 主线支撑 | 对已经通过工具读取或此前 surfaced 的 memory 去重,避免重复注入 |
nestedMemoryAttachmentTriggers | 场景增强 | 由文件访问路径触发目录级规则加载;没有文件触发时为空 |
loadedNestedMemoryPaths | 可观测/恢复支撑 | QueryEngine 持有跨 turn set,避免同一路径重复注入 |
| agent memory | 场景增强 | 只对定义了 memory 的 agent 生效;相关记忆召回在 @agent mention 时会改查 agent memory dir |
| session memory post-sampling hook | 可观测/恢复支撑 + 场景增强 | 只在主 REPL、auto compact 开启、gate 通过且阈值满足时运行;subagent / teammate / remote 不走这条 |
createMemoryFileCanUseTool() | 安全/治理横切 | session memory forked agent 只能 Edit 指定 memory 文件,其他工具请求被 deny |
/memory 命令 | 场景增强 | 用户主动编辑 memory 文件的交互命令,不是自动召回主链路 |
remember skill | 场景增强 | 只在 auto memory enabled 时启用,用于整理和迁移 memory,不自动修改 |
5. 机制拆解
5.1 Memory prompt 是“机制说明”,不是完整记忆注入
QueryEngine.submitMessage() 在构造 system prompt 时,只在特定条件下调用 loadMemoryPrompt()。普通自定义 SDK system prompt 下,还要求 hasAutoMemPathOverride() 为真,注释明确这是调用方显式接入 memory directory 的信号。
src/memdir/memdir.ts 中 loadMemoryPrompt() 会先检查 isAutoMemoryEnabled()。关闭时返回 null 并记录 tengu_memdir_disabled。开启后,根据 KAIROS、TEAMMEM、team memory enabled 等条件选择不同 prompt:
- KAIROS assistant daily-log mode:提示模型把新记忆追加到按日期组织的 log。
- team memory:构造 auto + team combined prompt。
- auto only:构造单目录 memory 使用说明。
因此,system prompt 层的 memory 更像“使用持久化文件记忆的协议说明”。它不等于每轮把所有 memory 文件全文塞入上下文。
5.2 CLAUDE.md / rules 属于项目上下文,不等同 auto memory
前几期已经确认 fetchSystemPromptParts() 会获取 userContext。CLAUDE.md 类文件通过用户/项目上下文进入模型窗口。它与 memdir 的 auto memory 是两套来源:
- CLAUDE.md 更像项目规则、指令和可共享约束。
- auto memory 更像跨会话持久化的用户偏好、反馈和项目背景。
- team memory 是 auto memory 下的团队共享扩展。
remember skill 的提示也强调:整理 auto-memory 时应判断是否应该提升到 CLAUDE.md、CLAUDE.local.md 或 team memory。这说明源码把 memory 层级作为治理问题处理,而不是单一桶。
5.3 relevant memory 是异步预取,不阻塞当前 turn
startRelevantMemoryPrefetch() 在 query.ts 进入 query session 时启动。它有明确 gate:
- auto memory 必须开启。
tengu_moth_copsefeature gate 必须开启。- 必须存在最后一个非 meta user message。
- prompt 不能只是单词。
- 已 surfaced memory 总字节不能超过阈值。
函数返回 MemoryPrefetch handle,注释明确 promise “started once per user turn”,在 post-tools collect point 只在 settledAt !== null 时消费;未就绪不会阻塞本轮。query.ts 的消费点会调用 filterDuplicateMemoryAttachments(),并把 survivor 标记到 readFileState。
这条链路的稳定性贡献是:相关 memory 可以利用模型 streaming / 工具执行期间的空档预取,但不会因为 memory selector 慢而拖住主循环。
5.4 relevant memory 有召回隔离和体积边界
getRelevantMemoryAttachments() 会先检查 prompt 中是否 @mention agent:
- 如果有 agent mention,且对应 agent 定义带
memory,只搜索该 agent 的 memory dir。 - 否则搜索 auto memory dir。
这说明 agent memory 不是全局混入主 agent 的记忆池,而是有隔离条件。检索结果还会被限制:
findRelevantMemories()结果被readFileState和alreadySurfaced过滤。- 最多 slice 5 个。
readMemoriesForSurfacing()用MAX_MEMORY_LINES和MAX_MEMORY_BYTES截断读取。- 如果截断,会附带提示让模型用 Read 查看完整文件。
因此,相关记忆是“有限候选 + 有界内容 + 可继续读取”的召回,不是无限历史灌入。
5.5 nested memory 由文件路径触发,并跨 turn 去重
ToolUseContext 中有 nestedMemoryAttachmentTriggers 和 loadedNestedMemoryPaths。QueryEngine 持有 loadedNestedMemoryPaths = new Set<string>(),并在每次构造输入/工具上下文时传下去。
getNestedMemoryAttachments() 首先检查 trigger set,空则直接返回。非空时按 filePath 调用 getNestedMemoryAttachmentsForFile(),最后清空 trigger set。memoryFilesToAttachments() 会:
- 跳过已经在
loadedNestedMemoryPaths里的 memory file。 - 跳过已经在
readFileState中的路径。 - 将新 memory file 作为
nested_memoryattachment。 - 把内容写入
readFileState,供后续工具和 memory 去重共享。
这条链路说明 nested memory 是由具体文件访问触发的目录上下文补充,不是每轮全目录扫描。
5.6 session memory 是 post-sampling 后台抽取,不是用户 turn 的前置主线
initSessionMemory() 会先检查 remote mode,remote 下直接返回;再检查 auto compact 是否开启,不开启则不注册 hook。注册的是 post-sampling hook extractSessionMemory()。
extractSessionMemory() 内部再次收窄条件:
querySource必须是repl_main_thread。- session memory gate 必须开启。
shouldExtractMemory(messages)必须满足阈值。
真正抽取时,它会创建 isolated setup context,读取/准备 session memory 文件,然后调用 runForkedAgent(),并传入:
querySource: 'session_memory'forkLabel: 'session_memory'canUseTool: createMemoryFileCanUseTool(memoryPath)overrides: { readFileState: setupContext.readFileState }
createMemoryFileCanUseTool() 只允许 FILE_EDIT_TOOL_NAME 且 file_path === memoryPath,其他全部 deny。这是 session memory 后台写入的关键安全边界。
5.7 session memory 与 compact 有协作,但有递归边界
autoCompact.ts 中有 session memory compaction 优先路径,同时注释明确 session_memory 和 compact 是 forked agents,需要递归 guard。session memory 的目的不是替代 transcript,而是为长会话 compact 提供更结构化的当前状态、错误修正和待办摘要。
这也解释了为什么 session memory 被放在可观测/恢复支撑分类:它帮助长会话在 compact 后保留关键状态,但它不是每个 prompt 进入模型前的必经输入。
5.8 /memory 和 remember 是用户主动治理工具
/memory 命令会打开 MemoryFileSelector,允许用户选择并编辑 memory 文件,完成后清理 memory file caches。它是显式用户操作。
remember skill 的 prompt 要求“Review auto-memory entries and propose promotions”,并明确 “Do NOT apply changes”。这说明 memory 治理也区分“建议整理”和“实际写入”,不是后台自动迁移。
6. 对稳定解决问题能力的贡献
6.1 把长期经验变成可控输入
Memory mechanics、CLAUDE.md、relevant memory、nested memory 和 session memory 分别进入不同上下文层,避免所有历史无差别进入模型窗口。
6.2 减少旧信息重复污染
readFileState、alreadySurfaced、loadedNestedMemoryPaths 和 compact 后扫描机制共同减少重复注入。已经通过工具读取的 memory 不会再作为 relevant memory 反复 surfaced。
6.3 长会话恢复更稳
session memory 把长会话的当前状态抽取到文件,再与 compact 协作。即使 transcript 被压缩,模型仍可通过 session memory 保留关键连续性。
6.4 agent 记忆有隔离
@agent mention 会让相关记忆搜索转向 agent memory dir;未 mention 时才走 auto memory。这个边界防止 specialized agent 的私有工作记忆默认污染主 agent。
6.5 后台写 memory 有权限收缩
session memory forked agent 只能编辑指定 memory file。这个限制把“让模型维护记忆”从泛化文件写入收缩成单文件编辑任务。
7. 风险、边界与待验证问题
| 风险/边界 | 影响 | 防御性解读 |
|---|---|---|
| memory prompt 可能被误读为“记忆已加载” | 研究者容易把机制说明当成内容注入 | 必须区分 loadMemoryPrompt() 的行为说明与 relevant_memories / CLAUDE.md 的实际内容注入 |
| relevant memory 是异步预取 | 未 settled 时本轮不会消费 | 不能声称每个 turn 都一定有相关记忆;最多说“满足 gate 且预取完成后注入” |
| session memory 只在主 REPL 且 gate 满足时运行 | subagent、remote、compact fork 不走普通抽取路径 | 不能把 session memory 写成所有 agent 的跨回合机制 |
| nested memory 由文件触发 | 没有文件路径触发时不会加载目录规则 | 不能把 nested CLAUDE.md 写成 conversation start 全量扫描 |
| memory 文件由模型可编辑 | 可能保存错误、过时或敏感信息 | 源码通过 prompt taxonomy、remember skill、单文件权限和用户命令提供治理,但内容质量仍依赖模型与用户审核 |
8. 下一期衔接
第 7 期进入“多 Agent / Task 协作”。Memory 为多 Agent 提供两类支撑:一是 agent definition 可以声明自己的 memory;二是 background / forked agent 的进度、通知和 sidechain transcript 会进入任务协作与恢复链路。下一期将分析 AgentTool、fork subagent、background task、teammate / swarm、remote agent 和 SendMessage / pendingMessages 如何把复杂任务拆分、跟踪与汇总。
机制主线收束
站在架构师视角,我会把这组源码机制收束为三个问题:为什么需要它,什么时候必须引入它,以及真正要设计的是什么。
- why:Memory 解决的是长期经验如何进入当前任务,同时不破坏上下文清洁度。
- when:任务跨会话、跨目录、跨项目规则,或者需要保留长期操作经验时使用。
- what:把 memory 按类型、作用域、来源、有效期分层;写入 memory 必须有权限和路径约束;session memory 要可追溯。
Memory 连续性 这组源码最值得带走的,不是某个函数或某个配置项,而是它如何把模型能力放进一组可验证、可拒绝、可恢复、可审计的工程边界里。读源码时,如果只记住名词,会很快散;如果抓住 why、when、what,就能把这组机制迁移到自己的 agent 架构判断中。