Skip to content

从泄漏源码看 Claude Code:Memory系统深度解析

Published:  at  03:13 PM

核心文件:memdir/memdir.ts · memdir/paths.ts · memdir/memoryTypes.ts · memdir/memoryScan.ts · memdir/memoryAge.ts · memdir/findRelevantMemories.ts · memdir/teamMemPaths.ts · services/extractMemories/extractMemories.ts · services/extractMemories/prompts.ts


1. 系统定位

Claude Code 的记忆系统是一套基于文件的持久化知识库,使 Claude 能在跨会话、跨时间的场景下保留对用户、项目和工作偏好的理解。

与”聊天历史”不同,记忆系统的目标是保存不可从代码库或 git 历史直接推导的信息:用户的工作方式偏好、项目的非技术背景决策、指向外部系统的指针等。这些内容会在每次会话启动时通过系统 prompt 注入,让 Claude 无需每轮重新学习就能保持连贯性。

记忆的三个层次

在深入各子机制之前,需要区分 Claude Code 中三类不同性质的”记忆”:

层次载体作用域核心文件
指令记忆CLAUDE.md(项目/全局)跨会话,静态src/utils/claudemd.ts
语义记忆memory/ 目录下 topic 文件跨会话,动态增长src/memdir/
会话记忆session memory 文件当前会话,随会话消亡src/services/SessionMemory/

整个语义记忆系统由以下四个子机制共同构成:


2. 文件系统结构

2.1 路径体系

记忆目录的解析遵循以下优先级链(先匹配先生效):

1. CLAUDE_COWORK_MEMORY_PATH_OVERRIDE 环境变量(Cowork 全路径覆盖)
2. settings.json 中的 autoMemoryDirectory 字段(支持 ~/ 展开)
   ← 仅信任 policy / local / user 级别;projectSettings 被排除(安全考量)
3. 默认路径:<memoryBase>/projects/<sanitized-git-root>/memory/
   其中 memoryBase = CLAUDE_CODE_REMOTE_MEMORY_DIR 或 ~/.claude

sanitized-git-rootsanitizePath(findCanonicalGitRoot(cwd)) 生成:优先使用 git 仓库的规范根目录(而非 worktree 路径),确保同一仓库的所有 worktree 共享同一个记忆目录。

路径解析函数 getAutoMemPath() 使用 memoizeprojectRoot 为键缓存,避免渲染路径中每次 tool-use 消息重建时产生重复的文件系统调用。

2.2 目录布局

~/.claude/
└── projects/
    └── <sanitized-git-root>/        ← 例:__Users_alice_myproject
        └── memory/                  ← getAutoMemPath() 返回值(带尾部 /)
            ├── MEMORY.md            ← 入口索引(≤200行,≤25KB)
            ├── .consolidate-lock    ← AutoDream 锁文件(mtime = 上次整合时间)
            ├── user-profile.md      ← topic 文件(类型:user)
            ├── feedback-testing.md  ← topic 文件(类型:feedback)
            ├── project-goals.md     ← topic 文件(类型:project)
            ├── team/                ← 团队记忆子目录(TEAMMEM feature flag)
            │   ├── MEMORY.md
            │   └── <team-topic>.md
            └── logs/                ← KAIROS 模式日志目录
                └── YYYY/MM/YYYY-MM-DD.md

安全设计validateMemoryPath() 对所有路径做严格校验,拒绝相对路径、根目录、Windows 盘符根、UNC 路径、null 字节注入。isAutoMemPath() 在写入时做归属校验,防止路径遍历。


3. 记忆类型分类体系

3.1 四类分类

记忆系统采用封闭式四类分类法,在 memoryTypes.ts 中定义,所有记忆文件必须属于其中一类:

类型内容何时保存如何使用
user用户的角色、目标、知识背景了解用户身份/偏好时裁剪回答的深度和角度
feedback工作方式的正反馈(纠正与确认)用户纠正行为 明确认可某个非显然选择时保持行为一致性,不需用户重复指导
project进行中的工作、决策背景、截止日期了解代码/git 历史无法推导的项目信息时更准确地理解需求背后的动机
reference指向外部系统的指针(Linear、Grafana 等)了解特定外部资源的位置时知道去哪里查找信息

团队模式扩展(TYPES_SECTION_COMBINED:每种类型额外添加 <scope> 字段,区分 private(仅本人可见)与 team(项目协作者共享)。user 类型始终私有;project 类型强烈偏向 team;feedback 类型默认私有,仅在明确的项目级约定时才设为 team。

3.2 不应保存的内容

WHAT_NOT_TO_SAVE_SECTION 定义了明确排除项,即使用户明确要求保存也不应写入记忆:

设计动机:这条边界确保记忆系统存储的是”人类判断”而非”机器可推导事实”,避免记忆膨胀为代码库副本。

3.3 记忆文件格式

每个 topic 文件使用 frontmatter 标注元信息:

---
name: {{记忆名称}}
description: {{一行描述——用于相关性判断,需具体}}
type: {{user | feedback | project | reference}}
---

{{记忆正文——feedback/project 类型推荐结构:结论 + **Why:** + **How to apply:**}}

description 字段至关重要:findRelevantMemories() 的选择器模型仅凭 filename + description 判断相关性,不读取正文。描述越精确,被正确召回的概率越高。


4. 入口索引 MEMORY.md

4.1 索引结构与约束

MEMORY.md 是整个记忆系统的导航索引,而非正文内容存储:

- [记忆名称](filename.md) — 一句话摘要
- [另一条记忆](another.md) — 另一句话摘要

每条索引项 ≤150 字符,不写正文,正文在各 topic 文件中。MEMORY.md 在每次会话启动时被加载进系统 prompt,因此必须保持精简。

4.2 双重截断保护机制

truncateEntrypointContent()MEMORY.md 被加载时同时执行行数和字节数双重截断:

MAX_ENTRYPOINT_LINES = 200  (行数上限)
MAX_ENTRYPOINT_BYTES = 25_000  (约 25KB,字节上限)

截断策略:

  1. 先按行截断(自然边界),取前 200 行
  2. 再按字节截断:若截断后仍超 25KB,在最后一个换行符处切断,不截断行中间
  3. 任一限制触发时,追加 > WARNING: 提示行,说明触发原因

字节上限的设计动机:防止少量极长索引行(>200字符/行)绕过行数检查但仍撑爆上下文(实测曾观察到 <200 行却高达 197KB 的情况)。


5. 记忆注入:System Prompt 中的记忆

5.1 loadMemoryPrompt 的分发逻辑

loadMemoryPrompt() 是每次会话启动时构建记忆 prompt 的核心入口,根据当前启用的 feature 决定走哪条路径:

所有路径在创建目录前先调用 ensureMemoryDirExists() 确保目录存在,保证 agent 可以直接写入而无需先检查或创建。

5.2 buildMemoryLines 构建的 Prompt 结构

buildMemoryLines() 生成的记忆指引 Prompt 包含以下固定段落(按序):

段落来源内容
# auto memory 头部硬编码记忆目录路径 + 系统存在声明
## Types of memoryTYPES_SECTION_INDIVIDUAL四类分类详细说明 + 示例
## What NOT to saveWHAT_NOT_TO_SAVE_SECTION明确排除项
## How to save memories条件生成两步保存流程 / frontmatter 格式
## When to access memoriesWHEN_TO_ACCESS_SECTION触发时机 + 忽略指令处理
## Before recommending from memoryTRUSTING_RECALL_SECTION记忆验证要求(文件/函数存在性)
## Memory and other forms of persistence硬编码与 Plan/Tasks 的边界区分
## Searching past contextbuildSearchingPastContextSection()grep memory 和 transcript 的命令(feature flag 控制)
## MEMORY.mdbuildMemoryPrompt()截断后的 MEMORY.md 内容(或”当前为空”提示)

skipIndex 变体:当 feature flag tengu_moth_copse 开启时,How to save memories 段改为单步保存(只写 topic 文件,不要求维护 MEMORY.md 索引),对应在线实验中测试无索引时的记忆质量。

5.3 KAIROS 模式:日志追加变体

KAIROS 模式(长期存在的 assistant 会话)下,记忆系统切换为日志追加而非实时索引更新:

记忆写入路径:<autoMemPath>/logs/YYYY/MM/YYYY-MM-DD.md

日志路径使用模式字符串(而非今天的实际日期)注入 Prompt,目的是保持系统 Prompt 的 cache prefix 在跨午夜时保持不变,避免因日期字符串变化导致 prompt cache 失效。


6. 记忆的写入

6.1 主 agent 直接写入

系统 Prompt 中的记忆指引让主 agent(Claude 的主对话实例)在对话中随时可以直接写入记忆文件。写入使用 FileWriteToolFileEditTool,路径必须在 autoMemPath 内(由 isAutoMemPath() 校验)。

直接写入的路径绕过了常规的 DANGEROUS_DIRECTORIES 检查,这是记忆目录作为”受信任写入区域”的特权。

互斥机制extractMemories 子 agent 在检查到主 agent 已在当轮写过记忆时,会跳过本轮提取(hasMemoryWritesSince()),确保两者不会重复写入同一内容。

6.2 extractMemories 自动后台提取

extractMemories 是记忆系统最重要的自动写入机制:在每轮对话结束后(handleStopHooks 触发),以后台 forked agent 形式分析对话内容,提取值得持久化的记忆。

执行流程

闭包作用域状态

initExtractMemories() 将所有可变状态封装在闭包中,避免模块级变量污染测试:

状态变量类型作用
lastMemoryMessageUuidstring | undefined游标:记录上次提取处理到哪条消息
inProgressboolean互斥锁:防止同时运行两次提取
pendingContextobject | undefined暂存:inProgress 时收到的新触发请求
turnsSinceLastExtractionnumber节流计数:配合 tengu_bramble_lintel flag
inFlightExtractionsSet<Promise>追踪所有未完成的提取 Promise

Trailing run 机制:若提取正在进行时收到新触发,新上下文被 stash;当前提取完成后的 finally 块自动执行一次 trailing run,处理两次调用之间新增的消息——始终以最新上下文为准(多次 stash 只保留最后一个)。

提取 Prompt 设计

buildExtractAutoOnlyPrompt() 生成的 prompt 要求 forked agent:

  1. 预注入 manifestscanMemoryFiles() 的结果在 prompt 构造时已列入(agent 不需要自己 ls
  2. 高效轮次策略:第1轮并行发出所有 FileRead,第2轮并行发出所有 FileWrite/Edit,硬上限 5 轮
  3. 范围限制:只能使用最近 N 条消息的内容,不得 grep 源码或 git 查询验证
  4. 工具约束:允许 Read/Grep/Glob/只读 Bash,以及目录内的 FileEdit/FileWrite;其他一律拒绝

7. 记忆的召回

7.1 MEMORY.md 的系统级加载

MEMORY.md 是记忆系统的常驻上下文:每次会话启动时,loadMemoryPrompt() 将其截断后的内容注入系统 Prompt 的 ## MEMORY.md 段落,主 agent 无需显式操作即可获得记忆索引。

这是记忆的”被动召回”路径——agent 在理解用户意图、选择工作方式时自然参考记忆,无需特别触发。

7.2 findRelevantMemories 动态相关记忆注入

除了静态注入 MEMORY.md 外,系统还有一条动态相关记忆召回路径:

findRelevantMemories(query, memoryDir, signal, recentTools, alreadySurfaced)

工作流程:

选择器模型(默认 Sonnet)的 system prompt 要求:

scanMemoryFiles() 的单次扫描策略:readFileInRange 内部同时返回内容和 mtime,避免先 stat 排序再读取的双倍 syscall。扫描结果按 mtime 倒序排列,最多返回 200 个文件。

7.3 startRelevantMemoryPrefetch:非阻塞预取模式

tengu_moth_copse(skipIndex 实验)开启时,动态召回切换为非阻塞预取模式:

// src/utils/attachments.ts
export function startRelevantMemoryPrefetch(
  messages: ReadonlyArray<Message>,
  toolUseContext: ToolUseContext,
): MemoryPrefetch | undefined

关键设计:

  1. fire-and-forget 启动:在 query.ts 的 query 循环入口用 using 关键字(Disposable 协议)绑定预取句柄,离开 query 循环时自动取消([Symbol.dispose]
  2. 非阻塞:预取在后台并发执行,主 query 不等待其完成
  3. 结果消费:预取完成后的结果通过 filterDuplicateMemoryAttachments(attachments, readFileState) 去重后注入——readFileState 缓存中已出现过的文件路径不会重复注入
  4. 取消联动:prefetch 使用 createChildAbortController(toolUseContext.abortController) 创建子控制器,用户按 Escape 时立即取消 sideQuery,无需等待 using 销毁

单词 query(无空格)会提前 bail out(不足以提取有意义的搜索词),避免无效 sideQuery 调用。

为什么用 tengu_moth_copse 同时控制 skipIndex 和预取? skipIndex 模式下不维护 MEMORY.md,静态 MEMORY.md 注入失去索引价值; 预取恰好补充了无索引时的相关记忆召回能力,两者形成配套实验。


8. 记忆新鲜度机制

memoryAge.ts 提供了一套记忆过期感知机制,防止 Claude 将陈旧记忆当作当前事实断言:

memoryAge(mtimeMs)          // "today" | "yesterday" | "N days ago"
memoryFreshnessText(mtimeMs) // 超过 1 天时返回明确的过期警告文本
memoryFreshnessNote(mtimeMs) // 同上,包裹在 <system-reminder> 标签中

超过 1 天的记忆会携带如下提示注入上下文:

This memory is N days old. Memories are point-in-time observations, not live state —
claims about code behavior or file:line citations may be outdated.
Verify against current code before asserting as fact.

这一机制的设计动机来自用户反馈:带有 file:line 引用的陈旧记忆会让 Claude 的断言听起来更权威,但实际上代码可能已经变化。通过显式的过期提示,引导 Claude 在推荐前先验证记忆中提到的文件/函数是否仍然存在。

TRUSTING_RECALL_SECTION 进一步强化了这一约束:

“The memory says X exists” is not the same as “X exists now.”


9. 权限沙箱:createAutoMemCanUseTool

记忆系统的所有后台 agent(extractMemories、AutoDream)都使用 createAutoMemCanUseTool(memoryDir) 作为统一的权限沙箱:

工具权限限制条件
FileRead / Grep / Glob✅ 完全允许无(只读,无风险)
Bash✅ 有条件允许必须通过 BashTool.isReadOnly() 检查(ls/find/cat/stat 等)
FileEdit / FileWrite✅ 有条件允许路径必须在 autoMemPath 内(isAutoMemPath() 验证)
REPL✅ 允许(外壳)内部调用的原子工具仍经过同一沙箱检查
其他所有工具❌ 拒绝包括 Agent、TodoWrite、Web、写入型 Bash 等

REPL 放行的原因:在 ant 原生构建中,原子工具(Read/Bash/Edit/Write)被隐藏,agent 通过 REPL 调用它们;如果把 REPL 从工具列表中裁掉,会破坏 prompt cache 共享(工具列表是 cache key 的一部分)。真正的安全边界在原子工具层,REPL 外壳的放行不影响安全性。

每次工具被拒绝时,系统触发 tengu_auto_mem_tool_denied 遥测事件(携带脱敏工具名),为 Anthropic 收集”记忆 agent 在真实场景下尝试使用哪些工具”的观测数据。


10. 团队记忆(Team Memory)

10.1 目录结构

团队记忆是个人记忆目录的子目录,由 TEAMMEM feature flag 控制:

memory/
├── MEMORY.md          ← 个人记忆索引
├── user-profile.md
├── feedback-testing.md
└── team/              ← 团队记忆(getTeamMemPath())
    ├── MEMORY.md      ← 团队记忆索引
    └── team-conventions.md

团队记忆必须在个人记忆开启的前提下才能启用(isTeamMemoryEnabled() 先检查 isAutoMemoryEnabled())。团队目录的 mkdir -p 会顺带创建父级的个人目录。

10.2 Scope 分级

团队模式下,TYPES_SECTION_COMBINED 为每种记忆类型附加 <scope> 指引:

10.3 路径安全

团队记忆目录的写入经过两轮路径校验validateTeamMemWritePath() / validateTeamMemKey()):

  1. 第一轮path.resolve() 消除 .. 段,字符串层面确认路径在 teamDir 内
  2. 第二轮realpathDeepestExisting() 解析符号链接,确认真实文件系统位置在 teamDir 内

第二轮检查专门防御符号链接逃逸(PSR M22186):攻击者若在 team 目录内放置指向 ~/.ssh/authorized_keys 的符号链接,仅靠 path.resolve() 无法检测,必须用 realpath 解析链接目标后再验证。


11. Session Memory(会话记忆压缩)

Session Memory 是一套当前会话内的上下文压缩机制,与跨会话的语义记忆(memdir/)完全独立。

核心区别

11.1 触发阈值

DEFAULT_SESSION_MEMORY_CONFIGsessionMemoryUtils.ts)定义了三个默认触发条件:

参数默认值含义
minimumMessageTokensToInit10,000首次初始化所需最低 token 数
minimumTokensBetweenUpdate5,000两次更新之间的最小 token 增量
toolCallsBetweenUpdates3两次更新之间所需的最少 tool call 次数

三个条件同时满足才触发一次压缩。阈值可通过 GrowthBook 动态配置(tengu_sm_config)覆盖。

11.2 执行机制

触发后,Session Memory 以 forkLabel: 'session_memory' 启动一个 forked subagent:

// src/services/SessionMemory/sessionMemory.ts
runForkedAgent({
  querySource: 'session_memory',
  forkLabel: 'session_memory',
  skipTranscript: true,  // 不写入 session transcript
  ...
})

权限沙箱createMemoryFileCanUseTool(memoryPath)):写入权限被精确限制到单一 session memory 文件路径,完全隔离于语义记忆目录。

11.3 生命周期

Session memory 文件随会话创建,会话结束时不自动删除,但路径绑定到当前 session ID(getSessionMemoryPath()),下次新会话不复用。它不会被 AutoDream 扫描,不会出现在语义记忆的 MEMORY.md 索引中。

11.4 Feature Flag

Session Memory 由 tengu_session_memory GrowthBook flag 控制(isSessionMemoryGateEnabled()),默认关闭(false)。


12. 记忆整合:AutoDream(简介)

AutoDream 是记忆系统的后台定期整合机制,独立于实时提取运行:

AutoDream 的详细实现(门控机制、锁设计、Prompt 结构)参见专题报告。


13. 开关与配置体系

13.1 isAutoMemoryEnabled 的优先级链

// Path: src/memdir/paths.ts
// 优先级从高到低,第一个命中的返回:
1. CLAUDE_CODE_DISABLE_AUTO_MEMORY=1false(禁用)
2. CLAUDE_CODE_DISABLE_AUTO_MEMORY=0true(显式启用)
3. CLAUDE_CODE_SIMPLE--bare 模式)  → false
4. CLAUDE_CODE_REMOTE 且无 REMOTE_MEMORY_DIRfalseCCR 无持久存储)
5. settings.json 中 autoMemoryEnabled  → 取设置值
6. 默认                                → true(开启)

13.2 用户可配置项

配置项位置说明
autoMemoryEnabledsettings.json启用/禁用整个记忆系统
autoMemoryDirectorysettings.json(非 projectSettings)自定义记忆目录路径
autoDreamEnabledsettings.json启用/禁用后台 AutoDream

13.3 Feature Flags

Flag类型作用
EXTRACT_MEMORIESbuild-timefeature()extractMemories 模块编译开关;关闭时模块不加载,相关 require() 返回 null
tengu_passport_quailruntime GrowthBookextractMemories 后台提取运行时总开关(isExtractModeActive() 的双重门之一)
tengu_bramble_lintelruntime GrowthBookextractMemories 轮次节流(默认每轮提取一次)
tengu_moth_copseruntime GrowthBookskipIndex 模式(不维护 MEMORY.md 索引)+ 启用 startRelevantMemoryPrefetch
tengu_coral_fernruntime GrowthBook”Searching past context” 段落开关
tengu_onyx_ploverruntime GrowthBookAutoDream 开关 + minHours/minSessions 参数
tengu_session_memoryruntime GrowthBookSession Memory 功能总开关(默认 false)
tengu_sm_configruntime dynamic configSession Memory 阈值配置(tokenInit / tokenUpdate / toolCalls)
KAIROSbuild-time日志追加模式(assistant 永久会话)
TEAMMEMbuild-time团队记忆功能
tengu_herring_clockruntime GrowthBook团队记忆 GrowthBook 实验开关
MEMORY_SHAPE_TELEMETRYbuild-time记忆召回形状遥测

14. 核心数据流总览

14.1 会话启动:记忆注入

14.2 对话中:动态记忆召回

14.3 对话结束:记忆提取与整合


附录:关键文件速查

文件职责
memdir/paths.ts路径解析(getAutoMemPath)、开关判断(isAutoMemoryEnabled)、路径安全校验
memdir/memdir.tsPrompt 构建(buildMemoryLinesbuildMemoryPromptloadMemoryPrompt)、MEMORY.md 截断
memdir/memoryTypes.ts四类分类定义、frontmatter 格式、不保存规则、访问时机、新鲜度校验指引
memdir/memoryScan.ts扫描目录(scanMemoryFiles)、生成 manifest(formatMemoryManifest
memdir/memoryAge.ts记忆新鲜度计算(memoryAgememoryFreshnessTextmemoryFreshnessNote
memdir/findRelevantMemories.ts动态召回选择(findRelevantMemoriesselectRelevantMemories
memdir/teamMemPaths.ts团队记忆路径(getTeamMemPath)、双轮路径安全校验(symlink 防御)
services/extractMemories/extractMemories.ts自动提取主逻辑(initExtractMemoriesexecuteExtractMemories)、工具沙箱(createAutoMemCanUseTool
services/extractMemories/prompts.ts提取 agent prompt 构建(buildExtractAutoOnlyPromptbuildExtractCombinedPrompt
services/autoDream/autoDream.ts后台整合调度(参见 AutoDream 专题报告)
services/SessionMemory/sessionMemory.ts会话记忆压缩主逻辑(shouldExtractMemoryextractSessionMemory)、触发阈值判断
services/SessionMemory/sessionMemoryUtils.tsSession Memory 配置(DEFAULT_SESSION_MEMORY_CONFIG)、状态管理工具函数
utils/attachments.tsstartRelevantMemoryPrefetch(非阻塞预取)、filterDuplicateMemoryAttachments(去重)
utils/memoryFileDetection.ts统一记忆文件检测:isAutoManagedMemoryFilememoryScopeForPathisMemoryDirectoryisShellCommandTargetingMemory
commands/memory/memory.tsx/memory 命令:在外部编辑器中打开 CLAUDE.md 指令记忆文件

Suggest Changes

Previous Post
从泄漏源码看 Claude Code:KAIROS模式-永久助理
Next Post
从泄漏源码看 Claude Code:AutoDream 自进化记忆机制