Claude Code Memory 总体系核心代码逐行解析
英文标题:Claude Code Memory 总体系核心代码逐行解析
目录 第 0 章:先建立一张地图 第 1 章:总开关和目录定位 —— Memory 存到哪里 第 2 章:先告诉模型怎么记 —— loadMemoryPrompt 第 3 章:启动时加载什么 —— g
54.7 KB
中文
目录 第 0 章:先建立一张地图 第 1 章:总开关和目录定位 —— Memory 存到哪里 第 2 章:先告诉模型怎么记 —— loadMemoryPrompt 第 3 章:启动时加载什么 —— g
中文全文翻译正在补齐,可先查看下方英文原文。
English / 原文
上一篇讲的是“上下文快满时,怎样把旧对话压成摘要”;本篇讲的是另一个更容易混淆的问题:
Claude Code 到底怎样把信息保存下来,并在未来的会话里重新想起来?
配合下面这个 demo 一起看:
用户:记住,我看复杂源码时喜欢“角色地图 + 逐行中文注释 + 反事实表”。阅读顺序:先看 [第 0 章] 建立地图,再沿着“开门 → 告知规则 → 加载索引 → 写入 → 召回”的主链路阅读。
目录
第 0 章:先建立一张地图
Claude Code 源码里的 memory 不是一个单独功能,而是五套名字相似、职责不同的机制。
| 角色 | 类比 | 保存时间 | 主要文件 |
|---|---|---|---|
| Instructions Memory | 公司规章 | 跨会话 | CLAUDE.md、.claude/rules/*.md |
| Auto Memory | 个人知识库 | 跨会话 | memory/MEMORY.md + 主题文件 |
| Session Memory | 本次会议纪要 | 当前会话 | session-memory/.../summary.md |
| Agent Memory | 某类专家自己的笔记 | 跨 Agent 调用 | agent-memory/<agentType>/ |
| Team Memory | 团队共享 Wiki | 跨用户、跨机器 | memory/team/ + 服务端同步 |
本篇主角
本篇的主角是 Auto Memory 。它的完整生命线是:
启动 Claude Code
│
├─ loadMemoryPrompt()
│ 告诉模型:什么值得记、写到哪里、格式是什么
│
├─ getMemoryFiles()
│ 加载 CLAUDE.md 与 Auto Memory 的 MEMORY.md 索引
│
▼
用户说:“记住,我喜欢逐行中文注释”
│
├─ 路 A:主 Agent 当场 Write/Edit 记忆
│
└─ 路 B:本轮结束后 extractMemories 后台补记
│
▼
主题文件 feedback_source_walkthrough.md
+
MEMORY.md 中的一行目录
│
▼
未来某次用户又问源码
│
├─ 扫描最多 200 个主题文件的 frontmatter
├─ sideQuery 最多选择 5 个相关文件
├─ 异步读取正文
└─ 作为 relevant_memories attachment 注入主对话
一句话主线
Claude Code Memory 不是“把整段聊天永久塞进模型”,而是把值得长期保留的信息写成 Markdown 文件;平时只加载短索引,真正需要时再选择少量主题文件注入上下文。
先纠正三个常见误解
| 误解 | 实际情况 |
|---|---|
| Memory 是向量数据库 | 不是。主体是本地 Markdown + frontmatter + LLM 相关性选择 |
MEMORY.md 存全部记忆 |
不是。它是目录,正文放在独立主题文件 |
| Session Memory 等于长期记忆 | 不是。它主要服务当前长会话和 compact |
第 1 章:总开关和目录定位 —— Memory 存到哪里
📄 文件:src/memdir/paths.ts 第 21-55、79-89、198-258 行
🎬 对应 demo:Claude Code 启动,先判断这次是否允许使用长期记忆
1.1 总开关 isAutoMemoryEnabled
export function isAutoMemoryEnabled(): boolean {
const envVal = process.env.CLAUDE_CODE_DISABLE_AUTO_MEMORY
if (isEnvTruthy(envVal)) {
return false
}
if (isEnvDefinedFalsy(envVal)) {
return true
}
if (isEnvTruthy(process.env.CLAUDE_CODE_SIMPLE)) {
return false
}
if (
isEnvTruthy(process.env.CLAUDE_CODE_REMOTE) &&
!process.env.CLAUDE_CODE_REMOTE_MEMORY_DIR
) {
return false
}
const settings = getInitialSettings()
if (settings.autoMemoryEnabled !== undefined) {
return settings.autoMemoryEnabled
}
return true
}
这是一条“第一个明确答案获胜”的优先级链:
环境变量
> --bare
> 远程环境是否有持久目录
> settings.json
> 默认开启
1.2 基础目录 getMemoryBaseDir
export function getMemoryBaseDir(): string {
if (process.env.CLAUDE_CODE_REMOTE_MEMORY_DIR) {
return process.env.CLAUDE_CODE_REMOTE_MEMORY_DIR
}
return getClaudeConfigHomeDir()
}
1.3 项目级 Auto Memory 路径
function getAutoMemBase(): string {
return findCanonicalGitRoot(getProjectRoot()) ?? getProjectRoot()
}
export const getAutoMemPath = memoize(
(): string => {
const override = getAutoMemPathOverride() ?? getAutoMemPathSetting()
if (override) {
return override
}
const projectsDir = join(getMemoryBaseDir(), 'projects')
return (
join(projectsDir, sanitizePath(getAutoMemBase()), 'memory') + sep
).normalize('NFC')
},
() => getProjectRoot(),
)
export function getAutoMemEntrypoint(): string {
return join(getAutoMemPath(), 'MEMORY.md')
}
demo 假设项目是 /repo/shop,最终大致得到:
~/.claude/projects/-repo-shop/memory/
├── MEMORY.md
└── ...
🔑 小白重点(第 1 章)
- Memory 是项目隔离的 :不同仓库默认进入不同目录。
- worktree 又是共享的 :通过 canonical git root,避免每个 worktree 各记一套、互相矛盾。
- 没有持久盘就不开 :远程临时容器里写“长期记忆”没有意义。
- 路径覆盖有安全边界 :项目提交的
.claude/settings.json不能把目录偷偷改成~/.ssh;只有可信设置来源可以覆盖。
第 2 章:先告诉模型怎么记 —— loadMemoryPrompt
📄 文件:src/constants/prompts.ts 第 491-506 行
📄 文件:src/memdir/memdir.ts 第 187-315、409-506 行
🎬 对应 demo:模型还没有读任何记忆正文,先收到一份“记忆系统使用说明书”
Memory 要生效,第一步不是读文件,而是让模型知道:
- 哪些内容值得保存;
- 哪些内容绝对不要保存;
- 保存到哪个目录;
- 使用什么文件格式;
- 什么时候应该召回。
2.1 Memory Prompt 被放进系统提示
const dynamicSections = [
systemPromptSection('memory', () => loadMemoryPrompt()),
]
systemPromptSection 会缓存这一段,所以不是每一轮都重新扫描目录、重新拼提示。
2.2 loadMemoryPrompt 选择运行模式
export async function loadMemoryPrompt(): Promise<string | null> {
const autoEnabled = isAutoMemoryEnabled()
const skipIndex = getFeatureValue_CACHED_MAY_BE_STALE(
'tengu_moth_copse',
false,
)
if (feature('KAIROS') && autoEnabled && getKairosActive()) {
return buildAssistantDailyLogPrompt(skipIndex)
}
if (feature('TEAMMEM') && teamMemPaths!.isTeamMemoryEnabled()) {
const autoDir = getAutoMemPath()
const teamDir = teamMemPaths!.getTeamMemPath()
await ensureMemoryDirExists(teamDir)
return teamMemPrompts!.buildCombinedMemoryPrompt(
extraGuidelines,
skipIndex,
)
}
if (autoEnabled) {
const autoDir = getAutoMemPath()
await ensureMemoryDirExists(autoDir)
return buildMemoryLines(
'auto memory',
autoDir,
extraGuidelines,
skipIndex,
).join('\n')
}
return null
}
2.3 buildMemoryLines 告诉模型什么
核心规则可以压缩成下面几段:
# auto memory
你有一个文件型持久记忆系统,目录是 <memoryDir>
用户明确说“记住”时:
立即保存
用户说“忘掉”时:
找到对应文件并删除或更新
允许的类型:
user / feedback / project / reference
保存步骤:
1. 写独立主题文件
2. 在 MEMORY.md 增加一行指针
不要保存:
代码结构、Git 历史、临时任务、CLAUDE.md 已有内容
召回时:
Memory 只是过去某时刻的观察
涉及当前代码、文件、flag 时必须重新验证
生成逻辑见 memdir.ts L187-L265。
2.4 四种长期记忆类型
export const MEMORY_TYPES = [
'user',
'feedback',
'project',
'reference',
] as const
我们的 demo:
“我看复杂源码时喜欢角色地图 + 逐行注释 + 反事实表”
它既描述了用户偏好,也描述了希望助手如何协作。实际保存时最适合 feedback,正文可以写成:
---
name: source_explanation_style
description: 用户阅读复杂源码时偏好角色地图、逐行中文注释和反事实表
type: feedback
---
解释复杂源码时,先给角色地图,再沿调用链逐行添加中文注释,最后补反事实表。
**Why:** 用户需要先建立全局心智模型,再进入复杂控制流。
**How to apply:** 面对跨文件调用链、状态机、压缩或调度逻辑时采用此结构。
🔑 小白重点(第 2 章)
loadMemoryPrompt()注入的是使用规则 ,不是所有记忆正文。- Prompt 明确禁止保存可从代码重新推导的信息,避免 Memory 退化成过期的代码快照。
MEMORY.md是目录,不是正文仓库。- “Memory 说某函数存在”不等于“这个函数现在仍存在”,所以源码要求再次
grep或读文件验证。
第 3 章:启动时加载什么 —— getMemoryFiles 与 getUserContext
📄 文件:src/utils/claudemd.ts 第 790-1007、1136-1195 行
📄 文件:src/context.ts 第 152-188 行
🎬 对应 demo:未来开启一次新会话,CC 先加载规章和短索引
这一章要分清两个动作:
loadMemoryPrompt()
= 告诉模型“怎么使用记忆系统”
getMemoryFiles()
= 真的去磁盘找本次需要注入的文件
3.1 getMemoryFiles 的加载顺序
export const getMemoryFiles = memoize(async () => {
const result: MemoryFileInfo[] = []
const processedPaths = new Set<string>()
result.push(...await processMemoryFile(
getMemoryPath('Managed'),
'Managed',
processedPaths,
includeExternal,
))
if (isSettingSourceEnabled('userSettings')) {
result.push(...await processMemoryFile(
getMemoryPath('User'),
'User',
processedPaths,
true,
))
}
for (const dir of dirs.reverse()) {
result.push(...await processMemoryFile(
join(dir, 'CLAUDE.md'),
'Project',
processedPaths,
includeExternal,
))
result.push(...await processMemoryFile(
join(dir, '.claude', 'CLAUDE.md'),
'Project',
processedPaths,
includeExternal,
))
}
if (isAutoMemoryEnabled()) {
const { info: memdirEntry } = await safelyReadMemoryFileAsync(
getAutoMemEntrypoint(),
'AutoMem',
)
if (memdirEntry) {
result.push(memdirEntry)
}
}
if (feature('TEAMMEM') && teamMemPaths!.isTeamMemoryEnabled()) {
const { info: teamMemEntry } = await safelyReadMemoryFileAsync(
teamMemPaths!.getTeamMemEntrypoint(),
'TeamMem',
)
if (teamMemEntry) {
result.push(teamMemEntry)
}
}
return result
})
3.2 为什么加载顺序很重要
源码从“范围大、优先级低”加载到“范围小、优先级高”:
Managed
↓
User
↓
仓库上层目录
↓
更靠近 CWD 的目录
↓
Local
↓
AutoMem / TeamMem 索引
越靠后的内容越接近当前工作环境,模型通常会给予更高注意力。
3.3 @include 怎么处理
processMemoryFile 会:
- 标准化路径;
- 用
processedPaths防止重复和循环; - 检查
claudeMdExcludes; - 解析 symlink;
- 读取正文并找出
@path; - 递归处理引用文件;
- 达到最大深度时停止。
核心代码见 claudemd.ts L615-L685。
3.4 真正注入 userContext
export const getUserContext = memoize(async () => {
const shouldDisableClaudeMd =
isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_CLAUDE_MDS) ||
(isBareMode() && getAdditionalDirectoriesForClaudeMd().length === 0)
const claudeMd = shouldDisableClaudeMd
? null
: getClaudeMds(
filterInjectedMemoryFiles(await getMemoryFiles()),
)
return {
...(claudeMd && { claudeMd }),
currentDate: `Today's date is ${getLocalISODate()}.`,
}
})
这里有一个很容易讲错的细节:
Memory 的“行为规则”
→ loadMemoryPrompt()
→ system prompt
CLAUDE.md / MEMORY.md 的“文件内容”
→ getMemoryFiles() + getClaudeMds()
→ userContext
不是所有 Memory 内容都直接塞进 system prompt。
3.5 新召回模式下为什么不再注入索引
export function filterInjectedMemoryFiles(files) {
const skipMemoryIndex =
getFeatureValue_CACHED_MAY_BE_STALE('tengu_moth_copse', false)
if (!skipMemoryIndex) return files
return files.filter(
f => f.type !== 'AutoMem' && f.type !== 'TeamMem',
)
}
因此存在两种召回范式:
旧模式:
每次加载 MEMORY.md 短索引
模型根据索引自行决定要不要 Read 主题文件
新模式 tengu_moth_copse:
不注入 MEMORY.md
后台 selector 直接挑相关主题文件并附加正文
🔑 小白重点(第 3 章)
CLAUDE.md是“规章”,Auto Memory 是“经验”;两者共用部分加载基础设施,但语义不同。getMemoryFiles被 memoize,同一会话不会每轮都遍历磁盘。- compact、切换 worktree、设置同步等场景会清缓存,让下一次重新读取。
- 新召回模式的核心变化是:从“总给目录”变成“直接给最多几个相关正文”。
第 4 章:模型亲自写记忆 —— 主题文件 + MEMORY.md
📄 文件:src/memdir/memdir.ts 第 199-265 行
📄 文件:src/memdir/memoryTypes.ts 第 180-270 行
🎬 对应 demo:主 Agent 识别到用户明确要求“记住”,当场保存
4.1 为什么必须拆成两层
假设所有内容都直接追加到 MEMORY.md:
MEMORY.md
├─ 用户偏好 100 行
├─ 项目背景 300 行
├─ 外部系统 200 行
└─ 历史反馈 500 行
每次新会话都会付出整本加载成本,而且越用越大。
所以 CC 使用“图书馆目录 + 独立书籍”:
memory/
├── MEMORY.md # 只放一行一条的目录
├── feedback_source_walkthrough.md # 详细正文
├── user_role.md
└── reference_dashboard.md
4.2 两步保存协议
const howToSave = [
'Saving a memory is a two-step process:',
'**Step 1** — write the memory to its own file ...',
...MEMORY_FRONTMATTER_EXAMPLE,
'**Step 2** — add a pointer to that file in `MEMORY.md`...',
'`MEMORY.md` is an index, not a memory...',
]
demo 最终写成:
memory/
├── MEMORY.md
└── feedback_source_walkthrough.md
MEMORY.md:
- [源码讲解偏好](feedback_source_walkthrough.md) — 复杂源码先给角色地图,再逐行注释并补反事实表
主题文件保存完整的 Why 和 How to apply。
4.3 索引的硬上限
export const MAX_ENTRYPOINT_LINES = 200
export const MAX_ENTRYPOINT_BYTES = 25_000
export function truncateEntrypointContent(raw: string) {
}
4.4 什么不应该写
源码明确排除:
× 代码模式、架构、文件路径、项目结构
原因:应读取当前代码
× Git 历史、谁改了什么
原因:git log / git blame 才是事实源
× 调试过程和修复配方
原因:最终代码和 commit 才是事实源
× CLAUDE.md 已经写过的规则
原因:重复注入
× 当前任务进度、临时状态
原因:只对当前会话有用
4.5 用户说“忘掉”怎么办
Prompt 没有设计单独数据库删除 API,而是让模型:
找到对应主题文件
↓
删除错误条目或修改正文
↓
同步更新 MEMORY.md 指针
也就是说,Memory 的 CRUD 本质上就是受权限控制的文件 CRUD。
🔑 小白重点(第 4 章)
- 主题文件是知识,
MEMORY.md是导航。 - frontmatter 的
description非常关键,未来的相关性选择器主要靠它判断是否值得召回。 - 按“语义主题”组织,而不是按“2026-08-09 第 3 次对话”组织。
- 显式说“记住”也不能突破“不保存临时噪声”的规则。
第 5 章:模型漏记了怎么办 —— extractMemories
📄 文件:src/query/stopHooks.ts 第 133-156 行
📄 文件:src/services/extractMemories/extractMemories.ts 第 296-615 行
🎬 对应 demo:主 Agent 没有当场写文件,本轮结束后后台书记员补记
5.1 触发点:完整一轮结束
if (!isBareMode()) {
if (
feature('EXTRACT_MEMORIES') &&
!toolUseContext.agentId &&
isExtractModeActive()
) {
void extractMemoriesModule!.executeExtractMemories(
stopHookContext,
toolUseContext.appendSystemMessage,
)
}
}
这里的时机不是“每次工具调用后”,而是 query loop 已经给出最终回答、没有继续调工具时。
5.2 initExtractMemories 保存哪些运行状态
export function initExtractMemories(): void {
const inFlightExtractions = new Set<Promise<void>>()
let lastMemoryMessageUuid: string | undefined
let inProgress = false
let turnsSinceLastExtraction = 0
let pendingContext
}
源码:extractMemories.ts L290-L325
这套状态可以想成快递分拣站:
lastMemoryMessageUuid = 上次包裹处理到哪
inProgress = 当前传送带是否有人
pendingContext = 忙碌时新来的最后一批包裹
inFlightExtractions = 关机前仍需等待的任务
5.3 主 Agent 写过就不重复提取
if (hasMemoryWritesSince(messages, lastMemoryMessageUuid)) {
const lastMessage = messages.at(-1)
if (lastMessage?.uuid) {
lastMemoryMessageUuid = lastMessage.uuid
}
return
}
源码:extractMemories.ts L345-L360
于是两条写入路径是同一回合互斥的:
主 Agent 已写
→ 后台不写
主 Agent 没写
→ 后台检查有没有值得保存的内容
5.4 正式启动后台 fork
const existingMemories = formatMemoryManifest(
await scanMemoryFiles(memoryDir, createAbortController().signal),
)
const userPrompt = teamMemoryEnabled
? buildExtractCombinedPrompt(
newMessageCount,
existingMemories,
skipIndex,
)
: buildExtractAutoOnlyPrompt(
newMessageCount,
existingMemories,
skipIndex,
)
const result = await runForkedAgent({
promptMessages: [
createUserMessage({ content: userPrompt }),
],
cacheSafeParams,
canUseTool: createAutoMemCanUseTool(memoryDir),
querySource: 'extract_memories',
forkLabel: 'extract_memories',
skipTranscript: true,
maxTurns: 5,
})
源码:extractMemories.ts L388-L427
5.5 权限沙箱
后台提取 Agent 可以:
✓ Read / Grep / Glob
✓ 只读 Bash
✓ 在 Auto Memory 目录内 Write/Edit
不可以:
× 修改项目源码
× 执行写操作 Bash
× 调 MCP
× 再启动 Agent
× 写到 Memory 目录之外
核心权限判断见 extractMemories.ts L166-L221。
5.6 为什么还要限制 maxTurns: 5
正常提取只需要:
Turn 1:并行 Read 可能要更新的已有文件
Turn 2:并行 Edit/Write
Turn 3:必要时修正
限制为 5 可以防止后台 Agent 因“验证得更彻底”进入搜索兔子洞,无限消耗 token。
5.7 并发触发怎么收敛
提取 A 正在运行
│
├─ 触发 B:不并行,保存 pendingContext = B
├─ 触发 C:覆盖 pendingContext = C
│
提取 A 完成
│
└─ 用最新 C 补跑一次 trailing extraction
因为 C 的 messages 已经包含 B 的历史,所以保留最新上下文即可。
🔑 小白重点(第 5 章)
- 后台提取是 best effort :失败只打日志,不影响主回复。
- 它不是每轮无脑总结全部历史,而是通过 UUID 游标只处理增量。
- 主 Agent 和后台 Agent 不会在同一段消息上重复写。
- fork 复用主会话缓存,降低额外提取成本。
- 退出前
drainPendingExtraction()会软等待仍在运行的任务,避免刚写一半进程就结束。
第 6 章:未来怎么想起来 —— 相关记忆异步召回
📄 文件:src/memdir/memoryScan.ts 第 21-94 行
📄 文件:src/memdir/findRelevantMemories.ts 第 18-140 行
📄 文件:src/utils/attachments.ts 第 2196-2415 行
📄 文件:src/query.ts 第 300-304、1592-1613 行
🎬 对应 demo:几天后,用户再次要求解析复杂源码
这是 Auto Memory 最有意思的一段:它没有向量数据库,而是使用“两级筛选”。
6.1 第一级:只扫描轻量 header
const MAX_MEMORY_FILES = 200
const FRONTMATTER_MAX_LINES = 30
export async function scanMemoryFiles(memoryDir, signal) {
const entries = await readdir(memoryDir, { recursive: true })
const mdFiles = entries.filter(
f => f.endsWith('.md') && basename(f) !== 'MEMORY.md',
)
const headerResults = await Promise.allSettled(
mdFiles.map(async relativePath => {
const { content, mtimeMs } = await readFileInRange(
join(memoryDir, relativePath),
0,
FRONTMATTER_MAX_LINES,
)
const { frontmatter } = parseFrontmatter(content)
return {
filename: relativePath,
description: frontmatter.description || null,
type: parseMemoryType(frontmatter.type),
mtimeMs,
}
}),
)
return fulfilled(headerResults)
.sort((a, b) => b.mtimeMs - a.mtimeMs)
.slice(0, MAX_MEMORY_FILES)
}
扫描得到的 manifest 类似:
- [feedback] feedback_source_walkthrough.md (2026-08-09T...)
用户阅读复杂源码时偏好角色地图、逐行中文注释和反事实表
- [reference] reference_dashboard.md (2026-07-20T...)
线上延迟看板入口
6.2 第二级:让 sideQuery 最多选 5 个
export async function findRelevantMemories(
query,
memoryDir,
signal,
recentTools = [],
alreadySurfaced = new Set(),
) {
const memories = (await scanMemoryFiles(memoryDir, signal))
.filter(m => !alreadySurfaced.has(m.filePath))
if (memories.length === 0) return []
const selectedFilenames = await selectRelevantMemories(
query,
memories,
signal,
recentTools,
)
return selectedFilenames
.map(filename => byFilename.get(filename))
.filter(Boolean)
.map(m => ({ path: m.filePath, mtimeMs: m.mtimeMs }))
}
源码:findRelevantMemories.ts L39-L75
selector 的要求是:
最多返回 5 个
只有“确定有帮助”才选择
不确定就不选
没有相关内容可以返回空数组
这不是传统 embedding Top-K,而是一个结构化 JSON side query。
6.3 读取正文时还有三层预算
候选主题文件数:最多 5
每个文件:最多前 200 行
每个文件:最多 4096 bytes
整个会话已注入相关记忆:最多约 60 KB
对应常量和读取逻辑见 attachments.ts L269-L289 与 attachments.ts L2279-L2321。
超长文件不会整份丢弃,而是注入开头并告诉模型:
这份记忆被截断了,如需完整内容请使用 Read 工具读取原文件。
6.4 为什么在 query() 一开始就启动
using pendingMemoryPrefetch = startRelevantMemoryPrefetch(
state.messages,
state.toolUseContext,
)
它会和主模型流式输出、工具执行并行运行。
6.5 为什么消费时“只看是否已完成,不等待”
if (
pendingMemoryPrefetch &&
pendingMemoryPrefetch.settledAt !== null &&
pendingMemoryPrefetch.consumedOnIteration === -1
) {
const memoryAttachments = filterDuplicateMemoryAttachments(
await pendingMemoryPrefetch.promise,
toolUseContext.readFileState,
)
for (const memAttachment of memoryAttachments) {
const msg = createAttachmentMessage(memAttachment)
yield msg
toolResults.push(msg)
}
pendingMemoryPrefetch.consumedOnIteration = turnCount - 1
}
如果 selector 还没完成:
本次 iteration 不等它
↓
主 Agent 继续工作
↓
下一次 query loop iteration 再检查
这叫 zero-wait consume :记忆召回不能成为首 token 延迟的硬依赖。
6.6 旧记忆的新鲜度警告
两天以上的记忆会带上类似提示:
This memory is 47 days old.
Memories are point-in-time observations, not live state.
Verify against current code before asserting as fact.
🔑 小白重点(第 6 章)
- CC 用的是 Markdown header 检索 + LLM rerank ,不是向量库。
description写得越准确,未来召回质量越高。- 召回最多 5 个主题文件,且有行数、字节数、会话总量限制。
- 召回异步预取,不让 Memory 搜索拖慢主循环。
- “已经展示过”与“模型自己 Read 过”的文件都会去重。
第 7 章:Session Memory —— 当前会话的滚动会议纪要
📄 文件:src/services/SessionMemory/sessionMemory.ts 第 134-180、272-375 行
📄 文件:src/services/SessionMemory/sessionMemoryUtils.ts 第 31-36 行
📄 文件:src/services/compact/sessionMemoryCompact.ts 第 514-620 行
🎬 对应 demo:当前对话很长,书记员定期更新 summary.md
Session Memory 和 Auto Memory 最大区别:
Auto Memory:
为未来其他会话保存长期事实
Session Memory:
为当前这次长会话保存滚动工作状态
7.1 默认触发阈值
export const DEFAULT_SESSION_MEMORY_CONFIG = {
minimumMessageTokensToInit: 10_000,
minimumTokensBetweenUpdate: 5_000,
toolCallsBetweenUpdates: 3,
}
源码:sessionMemoryUtils.ts L31-L36
7.2 shouldExtractMemory
export function shouldExtractMemory(messages) {
const currentTokenCount = tokenCountWithEstimation(messages)
if (!isSessionMemoryInitialized()) {
if (!hasMetInitializationThreshold(currentTokenCount)) {
return false
}
markSessionMemoryInitialized()
}
const hasMetTokenThreshold =
hasMetUpdateThreshold(currentTokenCount)
const hasMetToolCallThreshold =
countToolCallsSince(messages, lastMemoryMessageUuid) >=
getToolCallsBetweenUpdates()
const hasToolCallsInLastTurn =
hasToolCallsInLastAssistantTurn(messages)
return (
hasMetTokenThreshold &&
(hasMetToolCallThreshold || !hasToolCallsInLastTurn)
)
}
7.3 后台书记员
post-sampling hook
↓
只允许主 REPL 线程
↓
sequential 防止两个书记员并发写
↓
读取/创建 summary.md
↓
fork session_memory Agent
↓
权限只允许 Edit 这一个 summary.md
↓
推进 lastSummarizedMessageId 书签
主流程见 sessionMemory.ts L272-L350。
7.4 它为什么能帮助 compact
上下文快满时:
trySessionMemoryCompaction()
↓
等待正在写的 summary.md,最多 15 秒
↓
读取 summary.md
↓
用 lastSummarizedMessageId 找到“纪要覆盖到哪”
↓
summary.md 代替旧消息
+
保留书签之后的近期原始消息
源码:sessionMemoryCompact.ts L514-L620。
拿不到有效 Session Memory 时,才降级到传统 LLM 现场压缩。
🔑 小白重点(第 7 章)
- Session Memory 保存的内容可以包含当前任务进度;Auto Memory 反而明确禁止保存这种临时状态。
- Session Memory 的目标是“压缩后还能无缝继续干活”,不是建立用户长期画像。
lastSummarizedMessageId是摘要覆盖边界,不是长期记忆索引。- 这就是为什么 Session Memory 和 Auto Memory 必须分成两个目录、两套 Prompt。
第 8 章:Agent Memory 与 Team Memory —— 作用域扩展
8.1 Agent Memory:每类专家有自己的笔记
📄 文件:src/tools/AgentTool/agentMemory.ts 第 12-177 行
📄 文件:src/tools/AgentTool/loadAgentsDir.ts 第 593-674、713-746 行
自定义 Agent frontmatter 可以写:
---
name: code-reviewer
memory: project
---
支持三种作用域:
| scope | 路径 | 含义 |
|---|---|---|
user |
~/.claude/agent-memory/<agentType>/ |
跨所有项目 |
project |
.claude/agent-memory/<agentType>/ |
项目共享,可进版本控制 |
local |
.claude/agent-memory-local/<agentType>/ |
当前项目、当前机器私有 |
路径分派:
export function getAgentMemoryDir(agentType, scope) {
const dirName = sanitizeAgentTypeForPath(agentType)
switch (scope) {
case 'project':
return join(getCwd(), '.claude', 'agent-memory', dirName) + sep
case 'local':
return getLocalAgentMemoryDir(dirName)
case 'user':
return join(getMemoryBaseDir(), 'agent-memory', dirName) + sep
}
}
Agent 声明 memory 后,加载器会做两件事:
if (isAutoMemoryEnabled() && memory && tools !== undefined) {
tools = addMissingTools(tools, [
FILE_WRITE_TOOL_NAME,
FILE_EDIT_TOOL_NAME,
FILE_READ_TOOL_NAME,
])
}
getSystemPrompt: () => {
if (isAutoMemoryEnabled() && memory) {
return systemPrompt + '\n\n' +
loadAgentMemoryPrompt(agentType, memory)
}
return systemPrompt
}
源码:loadAgentsDir.ts L659-L674 与 loadAgentsDir.ts L713-L746。
它解决的问题是:
主 Agent 的用户偏好
不应该淹没
code-reviewer 对常见缺陷的专业积累
8.2 Team Memory:把主题文件同步给团队
📄 文件:src/memdir/teamMemPaths.ts 第 66-94 行
📄 文件:src/services/teamMemorySync/watcher.ts 第 231-352 行
📄 文件:src/services/teamMemorySync/index.ts 第 760-1188 行
Team Memory 位于 Auto Memory 子目录:
<auto-memory>/
├── MEMORY.md # 私有索引
├── feedback_user.md
└── team/
├── MEMORY.md # 团队索引
├── project_release.md
└── reference_dashboard.md
入口路径见 teamMemPaths.ts L66-L94。
启动同步系统时:
export async function startTeamMemoryWatcher() {
if (!feature('TEAMMEM')) return
if (!isTeamMemoryEnabled() || !isTeamMemorySyncAvailable()) return
const repoSlug = await getGithubRepo()
if (!repoSlug) return
syncState = createSyncState()
await pullTeamMemory(syncState)
await startFileWatcher(getTeamMemPath())
}
上传采用:
本地文件计算 hash
↓
只上传与 serverChecksums 不同的 delta
↓
服务端返回 412 冲突
↓
只拉最新 hashes,不拉全部正文
↓
重新计算更小的 delta
↓
重试
见 teamMemorySync/index.ts L869-L1138。
安全措施包括:
- 路径穿越校验;
- symlink 逃逸校验;
- 单文件大小限制;
- 上传前 secret scan;
- 检测到密钥的文件跳过同步;
- OAuth 和 GitHub repo scope;
- 冲突重试次数限制。
8.3 /memory 命令只是管理入口
/memory 不负责自动总结,它做的是:
清除并重新预热 Memory 文件缓存
↓
展示 User / Project / Auto / Team / Agent Memory 选项
↓
必要时创建空文件
↓
用 $VISUAL / $EDITOR 打开
入口见 memory.tsx L21-L88,选项组装见 MemoryFileSelector.tsx L44-L152。
🔑 小白重点(第 8 章)
- Agent Memory 解决“不同专家各自积累什么”的隔离问题。
- Team Memory 不是 Git 自动同步,而是本地目录 + 服务端 API + watcher。
- Team Memory 冲突策略偏向保住当前本地编辑,不做复杂内容级自动合并。
/memory是查看和编辑入口,不是 Memory 的核心运行时。
第 9 章:Memory、Compact、Context Collapse 到底什么关系
📄 文件:src/query.ts 第 365-467 行
你当前 IDE 打开的代码是:
if (feature('CONTEXT_COLLAPSE') && contextCollapse) {
const collapseResult =
await contextCollapse.applyCollapsesIfNeeded(
messagesForQuery,
toolUseContext,
querySource,
)
messagesForQuery = collapseResult.messages
}
它处在 query 主循环的这个位置:
原始 messages
↓
Tool Result Budget
↓
History Snip
↓
Microcompact
↓
Context Collapse ← query.ts:440
↓
Auto Compact
├─ Session Memory Compact
└─ 传统 LLM Compact
↓
调用主模型
↓
工具执行
↓
Relevant Memory 附件消费
↓
回合结束后 extractMemories
四者的时间尺度
| 机制 | 处理什么 | 是否跨会话 | 是否改写 messages |
|---|---|---|---|
| Auto Memory | 长期偏好和背景 | 是 | 通过 context/attachment 注入 |
| Session Memory | 当前会话工作纪要 | 通常服务当前会话 | compact 时作为摘要 |
| Context Collapse | 当前历史中的局部阶段 | 否 | 是,做读时投影 |
| Auto Compact | 接近上下文上限的全局换代 | 否 | 是,产生新边界和摘要 |
为什么 Context Collapse 在 Auto Compact 前
源码注释已经给出答案:
先尝试细粒度折叠
↓
如果已经降到 auto-compact 阈值以下
↓
就不必把整个历史压成一份大摘要
也就是:
能局部收纳就先局部收纳,实在装不下才整体换箱。
Memory 会不会被 compact 丢掉
分两种:
CLAUDE.md / MEMORY.md:
getUserContext 有独立加载与缓存
compact 后会清理相关缓存并重新注入
relevant_memories attachment:
属于消息历史的一部分
compact 后旧 attachment 可能消失
collectSurfacedMemories 因此自然重置,未来允许再次召回
这也是 attachments.ts L2244-L2265 特意扫描当前 messages,而不是维护永久全局 Set 的原因。
🔑 小白重点(第 9 章)
- Auto Memory 解决“跨会话记得什么”。
- Session Memory 解决“当前长会话压缩后如何继续”。
- Context Collapse 解决“局部旧阶段如何折叠”。
- Auto Compact 解决“整个上下文即将溢出”。
- 它们不是竞争关系,而是不同时间尺度上的分层治理。
附:五套 Memory 对照表
| 维度 | CLAUDE.md | Auto Memory | Session Memory | Agent Memory | Team Memory |
|---|---|---|---|---|---|
| 核心语义 | 指令 | 长期经验 | 当前会话纪要 | 专家经验 | 团队知识 |
| 主要入口 | getMemoryFiles |
loadMemoryPrompt |
post-sampling hook | Agent frontmatter | watcher + sync API |
| 入口文件 | CLAUDE.md |
MEMORY.md |
summary.md |
MEMORY.md |
team/MEMORY.md |
| 详细正文 | 文件本身 | 独立主题文件 | 同一个结构化 summary | 独立主题文件 | 独立主题文件 |
| 默认注入方式 | eager | 索引 eager / 正文按需 | compact 时使用 | Agent prompt | 索引/正文按需 |
| 写入者 | 用户/仓库 | 主 Agent 或后台提取 Agent | Session Memory fork | 对应 Agent | 主/后台 Agent |
| 典型内容 | 测试命令、编码规范 | 用户偏好、项目背景 | Current State、Worklog | review 常见缺陷 | 发布约束、共享入口 |
| 是否允许临时任务状态 | 不建议 | 明确禁止 | 允许且重要 | 取决于 scope 规则 | 不建议 |
| 是否跨用户 | 项目文件可以 | 默认否 | 否 | project scope 可以 | 是 |
| 过期风险 | 中 | 高,需验证 | 随会话更新 | 中 | 高,需同步 |
附:反事实表 —— 少一层会发生什么
| 如果删掉…… | 会发生什么 | 为什么现有设计要保留 |
|---|---|---|
MEMORY.md 索引 |
旧模式下模型不知道有哪些主题文件 | 用很小 token 成本提供导航 |
| 独立主题文件 | 索引无限膨胀,每次启动都加载全部正文 | 将“发现”和“读取”分离 |
frontmatter description |
selector 只能靠模糊文件名猜相关性 | description 是轻量召回摘要 |
| 200 文件候选上限 | 目录越大,扫描和 selector prompt 越贵 | 对召回成本设硬上限 |
| 最多 5 个召回结果 | 一次普通问题可能注入几十份旧记忆 | 控制噪声和上下文占用 |
| 新鲜度提示 | 旧 file:line 会被模型当成当前事实 | 强迫模型回到代码验证 |
hasMemoryWritesSince |
主 Agent 和后台 Agent 同时写重复记忆 | 两条写入路径按消息区间互斥 |
| UUID 增量游标 | 后台每轮重新分析整个会话 | 只处理上次之后的新消息 |
pendingContext 合并 |
多轮快速结束会并发写同一目录 | 单写者 + 最新上下文补跑 |
| 权限沙箱 | “整理记忆”的子 Agent 可能改项目代码 | 将副作用限制在 Memory 目录 |
| Session Memory | compact 时只能每次现场调用 LLM 总结 | 平时滚动记,临界点直接复用 |
| canonical git root | 每个 worktree 形成互相矛盾的记忆 | 同仓库共享同一知识空间 |
| Team secret scan | API key 可能随团队记忆上传 | 共享前阻断敏感信息 |
一页纸总结
【启动】
isAutoMemoryEnabled
├─ env / --bare / remote persistent dir / settings
└─ 默认开启
getAutoMemPath
└─ ~/.claude/projects/<canonical-git-root>/memory/
【告诉模型规则】
constants/prompts.ts
└─ systemPromptSection('memory', loadMemoryPrompt)
├─ 四种类型:user / feedback / project / reference
├─ 什么不保存
├─ 主题文件格式
└─ MEMORY.md 只做索引
【加载】
getMemoryFiles
├─ Managed CLAUDE.md
├─ User CLAUDE.md
├─ Project CLAUDE.md / .claude/rules
├─ Local CLAUDE.local.md
├─ Auto Memory MEMORY.md
└─ Team Memory MEMORY.md
getUserContext
└─ getClaudeMds(filterInjectedMemoryFiles(...))
【写入】
用户明确说“记住”
├─ 路 A:主 Agent 直接 Write/Edit
└─ 路 B:回合结束后 extractMemories 后台补记
├─ UUID 游标,只看增量
├─ 主 Agent 写过则跳过
├─ fork 复用 prompt cache
├─ 最多 5 turns
└─ 写权限仅限 Memory 目录
【召回】
startRelevantMemoryPrefetch
├─ scanMemoryFiles:最多 200 文件,只读前 30 行 frontmatter
├─ sideQuery:最多选择 5 个
├─ 正文:每个最多 200 行 / 4096 bytes
├─ 会话总注入约 60 KB
├─ settled 才消费,不阻塞主循环
└─ 旧记忆附带“请验证当前代码”提示
【当前会话连续性】
Session Memory
├─ 10k token 初始化
├─ 每增长 5k token 更新
├─ summary.md + lastSummarizedMessageId
└─ compact 时优先拿它代替旧消息
【作用域扩展】
Agent Memory
└─ user / project / local 三种 scope
Team Memory
└─ team/ 目录 + pull/push + watcher + hash delta + secret scan
【和 query.ts:440 的关系】
History Snip
→ Microcompact
→ Context Collapse
→ Auto Compact
→ 主模型与工具
→ Relevant Memory 消费
→ 回合结束后长期记忆提取
一句话总结全篇 :
Claude Code Memory 是一套分层的文件型记忆系统:
CLAUDE.md管“必须遵守什么”,Auto Memory 管“未来值得记住什么”,Session Memory 管“本次长会话做到哪里”,Agent Memory 管“某类专家学到了什么”,Team Memory 管“团队共同知道什么”;它通过短索引、frontmatter 召回、异步预取、权限沙箱和过期校验,把持久化能力控制在可解释、可编辑、可审计的 Markdown 文件上。