Claude Code Memory 系统:完整设计与源码分析

基于 Claude Code npm 包 source map 泄露快照 (2026-03-31) 的源码分析。
适用于团队内部分享,理解 AI Agent 记忆系统的设计范式。


一、设计哲学

源码注释(src/memdir/memoryTypes.ts:1-12)开宗明义:

Memories are constrained to four types capturing context NOT derivable from the current project state. Code patterns, architecture, git history, and file structure are derivable (via grep/git/CLAUDE.md) and should NOT be saved as memories.

核心原则:记忆只存储「从代码中读不出来」的信息。代码模式、架构、文件结构可以 grep/git 获得,不需要记忆。


二、三层记忆架构

Claude Code 不是一层记忆,而是三个独立系统解决不同时间尺度的问题:

生命周期存储位置触发方式解决的问题
CLAUDE.md永久项目目录用户手写项目级规则和指令
Auto Memory跨会话持久~/.claude/projects/<repo>/memory/自动提取 + 用户要求用户偏好、项目状态、反馈
Session Memory当前会话~/.claude/session-memory/<id>/token 阈值触发上下文压缩后恢复
CLAUDE.md 指令层 生命周期:永久 存储位置:项目目录 触发方式:用户手写 注入形式:系统提示正文 项目级规则和指令 Auto Memory 知识层 生命周期:跨会话持久 存储位置:~/.claude/.../memory/ 触发方式:自动提取 + 用户要求 注入形式:MEMORY.md + 语义召回 用户偏好、项目状态、反馈 Session Memory 恢复层 生命周期:当前会话 存储位置:session-memory/ 触发方式:token > 10K 注入形式:压缩时替代摘要 上下文压缩后恢复

三层架构解决不同时间尺度的记忆问题:永久指令 → 跨会话知识 → 会话恢复

2.1 CLAUDE.md:指令层

发现机制src/utils/claudemd.ts):

从 CWD 向上遍历到根目录,按顺序加载。源码注释(第 9 行)明确说明:

Files are loaded in reverse order of priority, i.e. the latest files are highest priority with the model paying more attention to them.

加载顺序与优先级(后加载 = 在 prompt 中更靠后 = 模型更关注 = 优先级更高):

系统提示加载顺序(从上到下 = 从先到后 = 优先级递增) 优先级 低注意力 高注意力 ① /etc/claude-code/CLAUDE.md Managed(MDM 企业级策略) ② ~/.claude/CLAUDE.md User(用户全局偏好) ③ <根目录>/CLAUDE.md Project(最远层) ④ <中间目录>/CLAUDE.md Project(逐层向 CWD 靠近) ⑤ <CWD>/CLAUDE.md Project(CWD 层) ⑥ <CWD>/.claude/CLAUDE.md Project(CWD 层) ⑦ <CWD>/.claude/rules/*.md Project(CWD 层规则) ⑧ <CWD>/CLAUDE.local.md Local(最后加载 = 最高优先级)

后加载 = prompt 中更靠后 = 模型更关注 = 优先级更高。dirs.reverse() 实现从根→CWD 遍历。

关键源码claudemd.ts:878):dirs.reverse() 让遍历从根目录→CWD,离 CWD 越近的文件越后加载。

优先级设计意图

注入方式:通过 getUserContext() 一次性加载(记忆化),作为系统提示的一部分,带硬性指令头:

"Codebase and user instructions are shown below. IMPORTANT: These instructions OVERRIDE any default behavior and you MUST follow them exactly as written."

2.2 Auto Memory:知识层

文件系统结构:

~/.claude/projects/<sanitized-git-root>/memory/ MEMORY.md 索引文件(始终注入系统提示) user_seandong.md 用户画像 feedback_design_approach.md 行为反馈 project_plugin_status.md 项目状态 reference_existing_skills.md 外部指针 team/ 团队共享目录(可选):MEMORY.md + ...

2.3 Session Memory:压缩恢复层

维度Auto MemorySession Memory
生命周期跨会话永久会话结束即删
结构用户自由创建固定模板(Task / Current State / Files 等)
触发自动提取 + 用户主动token ≥ 10K 后启动
目的知识积累压缩后恢复「我在干什么」

三、四种记忆类型:闭合分类法

3.1 总览

类型回答的问题衰减速度默认范围结构要求
user我在和谁说话?always private自由格式
feedback怎么做才对?private 偏重Rule + Why + How to apply
project现在在干什么、为什么?team 偏重Fact + Why + How to apply
reference去哪找信息?usually team自由格式(指针)
user 我在和谁说话? 范围:始终 private 衰减:慢 格式:自由 被动学习用户信息 不做负面判断 调整沟通策略和深度 user_seandong.md feedback 怎么做才对? 范围:默认 private 衰减:中 格式:Rule+Why+How 同时记录成功和纠正 监听安静的确认信号 Why 用于边界情况判断 feedback_design.md project 在做什么?为什么? 范围:偏向 team 衰减:快 格式:Fact+Why+How 使用绝对日期 Why 判断记忆是否过期 不存代码可推导的信息 project_status.md reference 去哪找信息? 范围:通常 team 衰减:慢 格式:自由(指针) 指向外部系统 最轻量的类型 无需 Why/How 结构 reference_skills.md

闭合四类分类法覆盖「不可从代码推导的上下文」的完整谱系:谁 · 怎么做 · 在做什么 · 去哪看

3.2 Type 1: User(用户画像)

源码定义memoryTypes.ts:46-56):

<type>
    <name>user</name>
    <scope>always private</scope>
    <description>Contain information about the user's role, goals,
    responsibilities, and knowledge. Your goal is to build up an
    understanding of who the user is and how you can be most helpful
    to them specifically.</description>
    <when_to_save>When you learn any details about the user's role,
    preferences, responsibilities, or knowledge</when_to_save>
    <how_to_use>When your work should be informed by the user's
    profile or perspective.</how_to_use>
</type>

实际样例(ones-testing-infra 项目):

---
name: Sean 用户画像
description: 用户角色、技术背景和协作偏好
type: user
---

- QA/测试工程方向的技术负责人,关注测试质量保障自动化
- 熟悉 ONES 平台(内部产品)、Claude Code、Codex CLI 插件生态
- 偏好先出文档再写代码(Doc First),重视方案设计的严谨性
- 对产品定位要求精确:会纠正范围蔓延
- 习惯追问设计细节和 tradeoff,期望得到有深度的思考而非直接套模板

设计规则

规则源码依据设计意图
永远 private<scope>always private</scope>用户画像是 per-person 的,同项目不同成员背景不同
被动学习when_to_save: When you learn...不主动提问,在对话中被动捕捉用户信息
调整沟通策略collaborate with a senior engineer differently than a student核心价值:根据用户水平定制回答深度和方式
不做负面判断Avoid writing memories that could be viewed as a negative judgement可以存「第一次接触 React」(客观),不能存「对 React 理解有限」(判断)

3.3 Type 2: Feedback(行为反馈)

源码定义memoryTypes.ts:57-73):

<type>
    <name>feedback</name>
    <scope>default to private. Save as team only when the guidance
    is clearly a project-wide convention.</scope>
    <description>Guidance the user has given you about how to approach
    work — both what to avoid and what to keep doing. Record from
    failure AND success.</description>
    <when_to_save>Any time the user corrects your approach OR confirms
    a non-obvious approach worked. Corrections are easy to notice;
    confirmations are quieter — watch for them.</when_to_save>
    <body_structure>Lead with the rule itself, then a **Why:** line
    and a **How to apply:** line. Knowing *why* lets you judge edge
    cases instead of blindly following the rule.</body_structure>
</type>

实际样例

---
name: 设计沟通反馈
description: 用户对方案设计过程的反馈和偏好
type: feedback
---

不要被现有实现限制思路,先想清楚"正确的做法是什么"再考虑兼容。

**Why:** 用户多次要求"不用局限于目前 skill 的限制"、"你认为正确的做法是什么",
期望先从第一性原理思考,再考虑向后兼容。

**How to apply:** 设计新方案时,先提出理想方案,再说明与现有实现的兼容策略。
不要一开始就被现有代码/目录结构束缚。

---

Plugin 不是 MCP Server。用户明确纠正过"我说的是插件形态,不是 mcp"。

**Why:** Claude Code Plugin 和 MCP Server 是不同概念。Plugin 包含 skills、agents、
MCP 声明等;MCP Server 只是 Plugin 的一个组成部分。

**How to apply:** 讨论 Plugin 时使用正确术语,区分 Plugin(整体)和 MCP Server
(Plugin 内的工具服务)。

设计规则

规则源码原文设计意图
记录成功,不只纠正Record from failure AND success: if you only save corrections, you will avoid past mistakes but drift away from approaches the user has already validated, and may grow overly cautious.防止模型逐渐变得保守——只知道不该做什么,不知道该做什么
结构化:Rule + Why + HowKnowing *why* lets you judge edge cases instead of blindly following the rule.有原因才能在边界情况下做出判断,而不是机械执行
监听安静的确认Corrections are easy to notice; confirmations are quieter — watch for them.用户说「perfect」「yes exactly」时容易被忽略,但同样重要
team vs private 决策Save as team only when the guidance is clearly a project-wide convention (e.g., a testing policy), not a personal style preference.「不 mock 数据库」→ team;「不要在末尾总结」→ private
检查团队冲突check that it doesn't contradict a team feedback memory个人偏好不能悄悄覆盖团队共识(仅 COMBINED 模式)

「记录成功」是最精妙的设计洞察。源码给的例子:

user: yeah the single bundled PR was the right call here, splitting
      this one would've just been churn
assistant: [saves feedback memory: for refactors in this area, user
           prefers one bundled PR over many small ones. Confirmed after
           I chose this approach — a validated judgment call, not a
           correction]

3.4 Type 3: Project(项目状态)

源码定义memoryTypes.ts:75-88):

<type>
    <name>project</name>
    <scope>private or team, but strongly bias toward team</scope>
    <description>Information about ongoing work, goals, initiatives,
    bugs, or incidents NOT derivable from code or git history.</description>
    <when_to_save>When you learn who is doing what, why, or by when.
    Always convert relative dates to absolute dates (e.g., "Thursday"
    → "2026-03-05").</when_to_save>
    <body_structure>Lead with the fact or decision, then **Why:** and
    **How to apply:**. Project memories decay fast, so the why helps
    future-you judge whether the memory is still load-bearing.</body_structure>
</type>

实际样例

---
name: QABOT Plugin 项目状态
description: QABOT Plugin 从 CLI 迁移为 Claude Code/Codex Plugin 的项目进展
type: project
---

QABOT 正在从独立 CLI 迁移为 Claude Code / Codex Plugin 形态,聚焦 ONES 测试质量保障。

**Why:** CLI 维护成本高(~3000行基础设施代码),Plugin 形态可复用宿主的 Agent Loop/LLM/REPL。

**How to apply:** Plugin 代码在 /workspace/.../plugin/ 下独立开发,不修改现有 CLI。

## 已完成(截至 2026-03-27)
- [x] plugin/.specs/prd.md — PRD
- [x] plugin/.specs/architecture.md — 技术架构
...

## 待办
### M1:Plugin 骨架 + 环境管理
- [ ] 创建双平台 Plugin 结构
...

设计规则

规则源码原文设计意图
绝对日期Always convert relative dates to absolute dates (e.g., "Thursday" → "2026-03-05")两个月后读到「上周」没有意义
强调 Why 的易腐性Project memories decay fast, so the why helps future-you judge whether the memory is still load-bearing.Why 是「下周 demo」→ 说明已过期;Why 是「CLI 维护成本高」→ 仍然有效
强偏向 teamstrongly bias toward team项目状态通常是所有参与者需要知道的

3.5 Type 4: Reference(外部指针)

源码定义memoryTypes.ts:89-103):

<type>
    <name>reference</name>
    <scope>usually team</scope>
    <description>Stores pointers to where information can be found
    in external systems.</description>
    <when_to_save>When you learn about resources in external systems
    and their purpose.</when_to_save>
</type>

设计规则:最轻量的类型,不需要 Why / How to apply 结构。价值在于「告诉模型去哪找」。默认 team——外部系统的位置信息对整个团队有用。


四、排除规则:什么不该存

源码定义了明确的反面清单(memoryTypes.ts:183-195):

export const WHAT_NOT_TO_SAVE_SECTION: readonly string[] = [
  '## What NOT to save in memory',
  '',
  '- Code patterns, conventions, architecture, file paths, or project structure '
  + '— these can be derived by reading the current project state.',
  '- Git history, recent changes, or who-changed-what — `git log` / `git blame` are authoritative.',
  '- Debugging solutions or fix recipes — the fix is in the code; the commit message has the context.',
  '- Anything already documented in CLAUDE.md files.',
  '- Ephemeral task details: in-progress work, temporary state, current conversation context.',
  '',
  // H2: eval-validated (memory-prompt-iteration case 3, 0/2 → 3/3)
  'These exclusions apply even when the user explicitly asks you to save. '
  + 'If they ask you to save a PR list or activity summary, ask what was '
  + '*surprising* or *non-obvious* about it — that is the part worth keeping.',
]

最后一条是最强硬的:即使用户说「保存这个 PR 列表」,模型也不应该直接存——而是追问「这里面什么是意外的或不明显的?那部分才值得记住。」这条规则经过 eval 验证(0/2 → 3/3),专门针对「活动日志噪声」问题。

与其他持久化机制的边界

// memdir.ts:254-257
'## Memory and other forms of persistence',
'- When to use or update a plan instead of memory: ...',
'- When to use or update tasks instead of memory: ...',
场景用什么不用什么
跨会话需要的信息MemoryPlan / Task
当前会话的实施方案PlanMemory
当前会话的进度追踪TaskMemory
方案变更记录更新 Plan存 Memory

五、召回侧规则:用记忆时的纪律

5.1 验证优于信任

源码(memoryTypes.ts:240-256),eval 验证 H1: 0/2 → 3/3:

export const TRUSTING_RECALL_SECTION: readonly string[] = [
  '## Before recommending from memory',
  '',
  'A memory that names a specific function, file, or flag is a claim that it '
  + 'existed *when the memory was written*. It may have been renamed, removed, '
  + 'or never merged. Before recommending it:',
  '',
  '- If the memory names a file path: check the file exists.',
  '- If the memory names a function or flag: grep for it.',
  '- If the user is about to act on your recommendation, verify first.',
  '',
  '"The memory says X exists" is not the same as "X exists now."',
  '',
  'A memory that summarizes repo state (activity logs, architecture snapshots) '
  + 'is frozen in time. If the user asks about *recent* or *current* state, '
  + 'prefer `git log` or reading the code over recalling the snapshot.',
]

设计意图:源码注释记录了失败模式——模型会直接推荐记忆中的文件路径,即使该文件已被移动/删除。header 措辞从「Trusting what you recall」改为「Before recommending from memory」后通过率从 0/3 → 3/3,因为后者是 action cue(在决策点触发),前者太抽象。

5.2 新鲜度漂移警告

源码(memoryTypes.ts:201-202):

export const MEMORY_DRIFT_CAVEAT =
  'Memory records can become stale over time. Use memory as context for what '
  + 'was true at a given point in time. Before answering the user or building '
  + 'assumptions based solely on information in memory records, verify that '
  + 'the memory is still correct and up-to-date...'

配合 memoryAge.ts 的时间标注系统:

// 模型不善于日期计算——原始 ISO 时间戳不会触发过时推理
// 但 "47 days ago" 会
export function memoryAge(mtimeMs: number): string {
  const d = memoryAgeDays(mtimeMs)
  if (d === 0) return 'today'
  if (d === 1) return 'yesterday'
  return `${d} days ago`
}

// 超过 1 天的记忆附带 staleness caveat
export function memoryFreshnessText(mtimeMs: number): string {
  const d = memoryAgeDays(mtimeMs)
  if (d <= 1) return ''
  return `This memory is ${d} 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.`
}

5.3 忽略指令

源码(memoryTypes.ts:216-222),eval 验证 H6: case 5 1/3:

'- If the user says to *ignore* or *not use* memory: proceed as if '
+ 'MEMORY.md were empty. Do not apply remembered facts, cite, compare '
+ 'against, or mention memory content.',

失败模式:用户说「忽略关于 X 的记忆」→ 模型读代码给出正确答案,但加了一句「not Y as noted in memory」——把记忆内容提了出来。规则要求完全不提及


六、六种记忆的产生规则与加载机制

Claude Code 中存在六种不同来源的记忆内容,各自有独立的产生规则和加载时机:

6.0 总览

记忆种类产生方式加载时机注入形式生命周期
CLAUDE.md用户手写会话启动时一次性加载系统提示正文永久
MEMORY.md 索引模型/提取 agent 写入每次会话注入系统提示系统提示 # auto memory跨会话
Relevant MemoriesSonnet 语义选择每轮用户消息后异步预取<system-reminder> attachment按轮注入
Nested Memory工具触发工具使用触发嵌套 CLAUDE.md<system-reminder> attachment按需注入
Auto-extracted后台 Fork Agent每轮对话结束后写入文件(下次通过上述机制加载)跨会话
Session Memory后台 Post-sampling Hooktoken ≥ 10K 后每增长 5K用于压缩替代摘要当前会话
会话时间线 会话启动 每轮对话 工具使用 轮结束 / 压缩 CLAUDE.md 一次性,记忆化 MEMORY.md 索引 系统提示节 语义召回记忆 异步预取 嵌套记忆 工具触发 自动提取 写入文件,下次会话加载 Session Memory 压缩时替代摘要(零 API 调用)

六种记忆在会话时序中的加载位置:越靠右越是「按需」加载

6.1 CLAUDE.md —— 指令级记忆

产生规则:用户在项目中手写 CLAUDE.md.claude/CLAUDE.md.claude/rules/*.md 等文件。

加载机制src/utils/claudemd.tssrc/context.ts):

会话启动 getUserContext() 记忆化,整个会话只调用一次 getMemoryFiles() 加载顺序(后加载 = 在 prompt 更靠后 = 优先级更高): ① Managed (/etc/claude-code/CLAUDE.md) -- 最先加载,优先级最低 ② User (~/.claude/CLAUDE.md) ③ Project(根目录 → CWD,逐层 CLAUDE.md + .claude/ + rules/) ④ Local(根目录 → CWD,逐层 CLAUDE.local.md) -- 最后加载,最高优先级 getClaudeMds() 格式化为系统提示文本,带指令头:"These instructions OVERRIDE..." 注入系统提示(不在消息中,在 system prompt 中)

关键特性

6.2 MEMORY.md 索引 —— 知识入口

产生规则

加载机制src/memdir/memdir.ts):

会话启动 loadMemoryPrompt() 通过 systemPromptSection 缓存 读取 MEMORY.md 文件(同步 readFileSync) truncateEntrypointContent() 行限制:200 行 / 字节限制:25KB,超出则截断并附加 WARNING 注入系统提示的 # auto memory 节 包含:类型定义 + 保存规则 + 召回纪律 + MEMORY.md 内容

关键特性

6.3 Relevant Memories —— 按需语义召回

产生规则:不主动产生,而是从已有记忆文件中按需选择。

加载机制src/utils/attachments.tssrc/memdir/findRelevantMemories.ts):

用户发送消息 startRelevantMemoryPrefetch() 与模型流式输出并行执行 — 非阻塞 前置检查 auto memory 启用? · 多词查询? GrowthBook 开关? · 会话累计 < 60KB? scanMemoryFiles() 递归扫描 .md · 解析 frontmatter(30行) · 最多 200 文件 selectRelevantMemories() Sonnet 选择 ≤ 5 个文件 · 排除已激活工具文档 · JSON 输出 readMemoriesForSurfacing() 每文件 200 行/4KB · 附加 memoryAge · 新鲜度警告(>1 天) 注入为 <system-reminder> 附件 预算控制 单次:5 x 4KB 会话累计:60KB 去重:双层机制

关键特性

6.4 Nested Memory —— 文件触发的上下文注入

产生规则:不产生新记忆,而是在模型操作文件时,加载该文件路径相关的 CLAUDE.md 规则。

加载机制src/utils/attachments.ts):

模型调用 Read/Edit/Write 工具 文件路径加入 nestedMemoryAttachmentTriggers Set 工具执行时自动收集 getNestedMemoryAttachments(toolUseContext) 本轮工具执行结束后调用 getNestedMemoryAttachmentsForFile(filePath) 按优先级搜索: ① Managed/User 条件规则(glob 匹配文件路径) ② 嵌套目录(CWD→目标文件):CLAUDE.md + 无条件 + 条件规则 去重 loadedNestedMemoryPaths(非 LRU Set,永不驱逐)+ readFileState(LRU cache) 注入为 <system-reminder>: "Contents of {path}"

关键特性

6.5 Auto-extracted Memories —— 后台智能提取

产生规则:Fork Agent 分析对话内容,自动创建/更新记忆文件。

加载机制:不直接加载——写入文件后,通过 6.2(MEMORY.md 索引)和 6.3(语义召回)间接加载。

对话轮结束 守卫检查 节流:每 N 轮运行一次(GrowthBook 配置) 游标:只处理 lastMemoryMessageUuid 之后的消息 互斥:主 agent 已写 memory 则跳过 Fork 后台子 agent(共享 prompt cache) 最多 5 轮 · 工具:Read/Grep/Glob + memory 目录内 Edit/Write 注入现有记忆清单(避免重复创建) 分析新消息中的可记忆内容 写入文件 + 更新 MEMORY.md 无需记忆 — 推进游标 有内容 无内容

6.6 Session Memory —— 压缩恢复专用

产生规则:后台 post-sampling hook 自动维护结构化笔记。

加载机制:不注入对话——压缩时替代 API 摘要调用。

对话达到 10K token registerPostSamplingHook() 启动 Session Memory 维护 每增长 5K+ token 更新 · Fork agent 更新固定模板 · 每节最大 2K token 上下文达 87%,触发压缩 有 Session Memory 无 Session Memory 用它替代 API 摘要 零额外 API 调用 · 保留最近 10K-40K token 调用 API 生成摘要 传统路径(Fork agent,共享 cache)

七、MEMORY.md 索引机制详解

7.1 加载与截断

文件:src/memdir/memdir.ts

export const MAX_ENTRYPOINT_LINES = 200
export const MAX_ENTRYPOINT_BYTES = 25_000  // ~125 chars/line at 200 lines

export function truncateEntrypointContent(raw: string): EntrypointTruncation {
  const contentLines = trimmed.split('\n')

  // ① 先按行截断(200 行上限)
  if (lineCount > MAX_ENTRYPOINT_LINES) {
    truncated = contentLines.slice(0, 200).join('\n')
  }

  // ② 再按字节截断(25KB 上限,在最后一个换行处切断)
  if (truncated.length > MAX_ENTRYPOINT_BYTES) {
    const cutAt = truncated.lastIndexOf('\n', MAX_ENTRYPOINT_BYTES)
    truncated = truncated.slice(0, cutAt > 0 ? cutAt : MAX_ENTRYPOINT_BYTES)
  }

  // ③ 附加警告
  return {
    content: truncated + `\nWARNING: MEMORY.md is ${reason}. Only part of it was loaded.`,
    ...
  }
}

7.2 两步保存流程

系统提示指导模型的保存行为(memdir.ts:218-234):

Step 1: 写入独立文件(如 user_role.md, feedback_testing.md),使用 frontmatter:
  ---
  name: {{memory name}}
  description: {{one-line description — used to decide relevance}}
  type: {{user, feedback, project, reference}}
  ---
  {{content}}

Step 2: 在 MEMORY.md 中添加索引条目,每条一行,~150 字符以内:
  - [Title](file.md) — one-line hook

为什么分两步:MEMORY.md 始终在系统提示中(200 行限制),所以只存索引。实际内容在独立文件中,通过语义召回按需加载。

7.3 目录自动创建

// memdir.ts:129-147
export async function ensureMemoryDirExists(memoryDir: string): Promise<void> {
  const fs = getFsImplementation()
  try {
    await fs.mkdir(memoryDir)  // recursive by default, swallows EEXIST
  } catch (e) {
    // 权限错误只 log,不阻塞。模型的 Write 调用会展示真正的错误
  }
}

系统提示告诉模型目录已存在:

export const DIR_EXISTS_GUIDANCE =
  'This directory already exists — write to it directly with the Write tool '
  + '(do not run mkdir or check for its existence).'

设计意图:源码注释说「Claude was burning turns on ls/mkdir -p before writing」——模型浪费对话轮次去检查目录是否存在。现在系统保证目录存在,模型直接写。


八、语义记忆召回

8.1 召回流程

文件:src/memdir/findRelevantMemories.ts

用户发送新消息 startRelevantMemoryPrefetch() 与模型流式输出并行执行 — 非阻塞 前置检查 auto memory 启用? · 多词查询? GrowthBook 开关? · 会话累计 < 60KB? scanMemoryFiles(memoryDir) 递归扫描 .md(排除 MEMORY.md)· 解析 frontmatter(前 30 行)· 按 mtime 倒序 · 最多 200 文件 selectRelevantMemories(query, memories) 调用 Sonnet · 传入用户查询 + 记忆清单 · 返回最多 5 个文件名 readMemoriesForSurfacing() 每文件 200 行/4KB · 添加 memoryAge · 添加 staleness caveat(> 1 天) 注入为 <system-reminder> attachment · 去重 + 预算:5x4KB / 60KB 累计

语义召回使用 Sonnet 做选择(非 embedding),与主模型流式输出并行执行

8.2 选择器 prompt

const SELECT_MEMORIES_SYSTEM_PROMPT =
  `You are selecting memories that will be useful to Claude Code as it `
  + `processes a user's query. Return a list of filenames for the memories `
  + `that will clearly be useful (up to 5). Only include memories that you `
  + `are certain will be helpful. If there are no memories that would clearly `
  + `be useful, return an empty list.`
  + `If a list of recently-used tools is provided, do not select memories `
  + `that are usage reference for those tools — DO still select warnings, `
  + `gotchas, or known issues about those tools.`

8.3 记忆清单格式

// memoryScan.ts:84-94
export function formatMemoryManifest(memories: MemoryHeader[]): string {
  return memories.map(m => {
    const tag = m.type ? `[${m.type}] ` : ''
    const ts = new Date(m.mtimeMs).toISOString()
    return m.description
      ? `- ${tag}${m.filename} (${ts}): ${m.description}`
      : `- ${tag}${m.filename} (${ts})`
  }).join('\n')
}

输出示例:

- [user] user_seandong.md (2026-03-27T10:30:00Z): 用户角色、技术背景和协作偏好
- [feedback] feedback_design_approach.md (2026-03-27T10:30:00Z): 设计方案时的沟通偏好
- [project] project_plugin_status.md (2026-03-27T10:30:00Z): QABOT Plugin 迁移项目状态

8.4 为什么用 Sonnet 而不是 Embedding

记忆文件通常 < 50 个,frontmatter 的 description 就是人工/AI 写的摘要。用 Sonnet 做几十个候选的语义匹配,比维护 embedding 索引更简单、无额外基础设施依赖。

8.5 去重与预算控制

层面限制
单次召回数量最多5 个文件
单个文件大小最大200 行 / 4KB
单次注入总量最大20KB
会话累计注入最大60KB(达到后停止预取)
已出现文件跳过通过 alreadySurfaced Set 去重
已读文件跳过通过 readFileState LRU cache 去重

九、自动记忆提取

9.1 架构

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

核心设计:不在主对话中提取,而是 fork 一个后台子 agent。

对话轮结束 守卫检查 节流:每 N 轮运行一次(GrowthBook 配置) 游标:只处理 lastMemoryMessageUuid 之后的消息 互斥:主 agent 已写 memory 则跳过 Fork 后台子 agent(共享 prompt cache) 最多 5 轮 · 工具:Read/Grep/Glob + memory 目录内 Edit/Write 注入现有记忆清单(避免重复创建) 分析新消息中的可记忆内容 写入文件 + 更新 MEMORY.md 无需记忆 — 推进游标 有内容 无内容

后台 Fork Agent 不干扰主对话,共享 prompt cache 实现近零额外成本

9.2 关键设计决策

决策理由
Fork 而非内联不占主对话 token,不干扰用户
共享 prompt cachefork agent 复用主线程 cache,几乎零额外 cache 成本
互斥机制主 agent 已写了 memory → 跳过 fork,避免重复
游标推进只处理增量消息,不重复分析历史
合并去抖提取中有新轮结束 → 存入 pendingContext,当前完成后再运行
5 轮限制防止子 agent 在记忆目录中不停整理

9.3 Feature Gates

Flag控制
tengu_passport_quail启用/禁用提取
tengu_bramble_lintel节流(N 轮提取一次)
tengu_moth_copse跳过 MEMORY.md 索引(append-only 模式)

十、团队记忆同步

10.1 架构

文件:src/services/teamMemorySync/

~/.claude/projects/<repo>/ memory/team/ 拉取 拉取(会话启动时) GET /api/claude_code/team_memory ?repo={owner/repo} ETag 未变 → 304 跳过 有变化 → 写入本地(去重、symlink 验证) 推送 推送(文件变化时) fs.watch({recursive: true}) 2s 去抖 → SHA-256 per-key diff 凭据扫描(30+ gitleaks 规则) 增量 PUT(只传变化的文件) 冲突解决 冲突解决流程 PUT → 412 Precondition Failed(ETag 不匹配) → GET ?view=hashes(只取校验和,不取内容) → 重算 diff → 重试 PUT → 最多 3 次 → 最终 local-wins

10.2 安全机制

路径遍历防护teamMemPaths.ts):

// 多层防御
function sanitizePathKey(key: string): string {
  // ① null 字节检测(可在 C 层截断路径)
  if (key.includes('\0')) throw new PathTraversalError(...)
  // ② URL 编码遍历检测(%2e%2e%2f = ../)
  const decoded = decodeURIComponent(key)
  if (decoded !== key && (decoded.includes('..') || decoded.includes('/')))
    throw new PathTraversalError(...)
  // ③ Unicode 标准化攻击(全角 ../ → ASCII ../ under NFKC)
  const normalized = key.normalize('NFKC')
  if (normalized !== key && normalized.includes('..'))
    throw new PathTraversalError(...)
  // ④ 反斜杠、绝对路径拒绝
}

// 写入前 symlink 验证
async function validateTeamMemWritePath(filePath: string): Promise<string> {
  // Pass 1: path.resolve() 消除 .. 段,字符串级检查
  // Pass 2: realpath() 解析 symlink,验证真实路径在 team 目录内
  //   → 防止 symlink 指向 ~/.ssh/authorized_keys 的攻击
}

凭据扫描:推送前用 30+ 条 gitleaks 规则扫描每个文件(AWS key、GitHub PAT、Anthropic key、Slack token 等)。发现密钥的文件静默跳过,永远不上传到服务器。


十一、记忆的完整生命周期

创建 用户说「记住 X」 后台 Fork 自动提取 团队同步拉取(REST API) 存储 独立 .md 文件 + MEMORY.md 索引 Frontmatter: name · description · type (user|feedback|project|reference) 召回 MEMORY.md 始终在系统提示中 语义召回(Sonnet) 每轮异步预取 嵌套记忆 文件触发注入 注入 <system-reminder> 附件消息 使用 引用前先验证 · 新鲜度标注 「记忆说 X 存在」 ≠ 「X 现在存在」 清理 用户手动编辑/删除 模型更新/删除过时记忆 团队同步推送

完整生命周期:创建(3 种方式)→ 存储 → 召回(3 种方式)→ 注入 → 使用 → 清理(3 种方式)


十二、与上下文压缩的协同

12.1 压缩时的记忆保护

上下文达到 87% (167K tokens) 优先使用 Session Memory 做摘要(零 API 成本) 压缩后 MEMORY.md 不受影响(在系统提示中,不在消息中) Compact Boundary 保存 preCompactDiscoveredTools Post-compact 重新注入 CLAUDE.md 内容(session_start hook) 最近读过的 5 个文件(50K token) 已调用的技能(25K token) readFileState cache 清空(强制重新读取) 下一轮 MEMORY.md 仍在系统提示中 语义召回重新运行 Session Memory 继续更新

12.2 Session Memory 替代 API 调用

Session Memory 在上下文压缩中扮演关键角色——它是预计算的摘要。当 auto-compact 触发时:

  1. 有 Session Memory → 用它做摘要,保留最近 10K-40K token 的消息。零额外 API 调用
  2. 无 Session Memory → 调用 API 生成摘要(Fork agent,共享 cache)。

十三、关键源码文件清单

src/memdir/ memdir.ts MEMORY.md 加载、截断、目录创建、prompt 构建 memoryTypes.ts 四种类型定义、排除规则、召回纪律、frontmatter 格式 findRelevantMemories.ts Sonnet 语义选择(最多 5 个文件) memoryScan.ts 目录扫描、frontmatter 解析、清单格式化 memoryAge.ts 新鲜度计算("47 days ago")、staleness caveat paths.ts 路径解析、安全验证、Cowork/Settings override teamMemPaths.ts 团队路径、路径遍历防护、symlink 验证 teamMemPrompts.ts 团队模式 prompt(private/team scope 指导) src/services/ extractMemories/ extractMemories.ts Fork agent 编排、游标追踪、互斥、合并去抖 prompts.ts 提取 agent 的 prompt SessionMemory/ sessionMemory.ts Post-sampling hook、token 阈值触发 prompts.ts 模板(Task / Current State / Files 等节) teamMemorySync/ index.ts · watcher.ts · secretScanner.ts · types.ts 同步、监听、安全

十四、可借鉴的设计模式

模式Claude Code 做法可借鉴场景
闭合分类法只有 4 种类型,不可扩展任何需要结构化记忆的 AI 系统
排除列表 > 包含列表明确什么不该存防止记忆膨胀和噪声
记录成功 + 失败feedback 类型同时记录纠正和确认防止模型逐渐变保守
Why + How to apply结构化 feedback/project,带原因和应用场景让 AI 能判断边界情况
索引 + 独立文件MEMORY.md 只存指针,内容在文件中平衡「始终可用」和「按需加载」
语义召回Sonnet 选择 + 新鲜度标注替代 embedding 搜索的轻量方案
后台 Fork 提取共享 cache、互斥、游标、去抖任何需要 AI 后处理的场景
验证优于信任召回时 grep/check 验证文件/函数是否存在防止过时记忆被当作事实
多层安全null 字节/URL 编码/Unicode/symlink/凭据扫描任何涉及文件系统的共享数据
local-wins 冲突解决用户编辑不被静默覆盖协作编辑系统

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