基于 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 阈值触发 | 上下文压缩后恢复 |
三层架构解决不同时间尺度的记忆问题:永久指令 → 跨会话知识 → 会话恢复
发现机制(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 中更靠后 = 模型更关注 = 优先级更高):
后加载 = prompt 中更靠后 = 模型更关注 = 优先级更高。dirs.reverse() 实现从根→CWD 遍历。
关键源码(claudemd.ts:878):dirs.reverse() 让遍历从根目录→CWD,离 CWD 越近的文件越后加载。
优先级设计意图:
IMPORTANT: OVERRIDE 措辞弥补位置劣势注入方式:通过 getUserContext() 一次性加载(记忆化),作为系统提示的一部分,带硬性指令头:
"Codebase and user instructions are shown below. IMPORTANT: These instructions OVERRIDE any default behavior and you MUST follow them exactly as written."
文件系统结构:
| 维度 | Auto Memory | Session Memory |
|---|---|---|
| 生命周期 | 跨会话永久 | 会话结束即删 |
| 结构 | 用户自由创建 | 固定模板(Task / Current State / Files 等) |
| 触发 | 自动提取 + 用户主动 | token ≥ 10K 后启动 |
| 目的 | 知识积累 | 压缩后恢复「我在干什么」 |
| 类型 | 回答的问题 | 衰减速度 | 默认范围 | 结构要求 |
|---|---|---|---|---|
| user | 我在和谁说话? | 慢 | always private | 自由格式 |
| feedback | 怎么做才对? | 中 | private 偏重 | Rule + Why + How to apply |
| project | 现在在干什么、为什么? | 快 | team 偏重 | Fact + Why + How to apply |
| reference | 去哪找信息? | 慢 | usually team | 自由格式(指针) |
闭合四类分类法覆盖「不可从代码推导的上下文」的完整谱系:谁 · 怎么做 · 在做什么 · 去哪看
源码定义(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 理解有限」(判断) |
源码定义(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 + How | Knowing *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]
源码定义(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 维护成本高」→ 仍然有效 |
| 强偏向 team | strongly bias toward team | 项目状态通常是所有参与者需要知道的 |
源码定义(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: ...',
| 场景 | 用什么 | 不用什么 |
|---|---|---|
| 跨会话需要的信息 | Memory | Plan / Task |
| 当前会话的实施方案 | Plan | Memory |
| 当前会话的进度追踪 | Task | Memory |
| 方案变更记录 | 更新 Plan | 存 Memory |
源码(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(在决策点触发),前者太抽象。
源码(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.`
}
源码(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 中存在六种不同来源的记忆内容,各自有独立的产生规则和加载时机:
| 记忆种类 | 产生方式 | 加载时机 | 注入形式 | 生命周期 |
|---|---|---|---|---|
| CLAUDE.md | 用户手写 | 会话启动时一次性加载 | 系统提示正文 | 永久 |
| MEMORY.md 索引 | 模型/提取 agent 写入 | 每次会话注入系统提示 | 系统提示 # auto memory 节 | 跨会话 |
| Relevant Memories | Sonnet 语义选择 | 每轮用户消息后异步预取 | <system-reminder> attachment | 按轮注入 |
| Nested Memory | 工具触发 | 工具使用触发嵌套 CLAUDE.md | <system-reminder> attachment | 按需注入 |
| Auto-extracted | 后台 Fork Agent | 每轮对话结束后 | 写入文件(下次通过上述机制加载) | 跨会话 |
| Session Memory | 后台 Post-sampling Hook | token ≥ 10K 后每增长 5K | 用于压缩替代摘要 | 当前会话 |
六种记忆在会话时序中的加载位置:越靠右越是「按需」加载
产生规则:用户在项目中手写 CLAUDE.md、.claude/CLAUDE.md、.claude/rules/*.md 等文件。
加载机制(src/utils/claudemd.ts → src/context.ts):
关键特性:
@include 指令引用其他文件产生规则:
加载机制(src/memdir/memdir.ts):
关键特性:
产生规则:不主动产生,而是从已有记忆文件中按需选择。
加载机制(src/utils/attachments.ts → src/memdir/findRelevantMemories.ts):
关键特性:
alreadySurfaced Set + readFileState LRU cache 双重去重产生规则:不产生新记忆,而是在模型操作文件时,加载该文件路径相关的 CLAUDE.md 规则。
加载机制(src/utils/attachments.ts):
关键特性:
.claude/rules/*.md 可以带 glob 条件,只对匹配的文件生效loadedNestedMemoryPaths(不驱逐)防止 LRU cache 驱逐后重复注入产生规则:Fork Agent 分析对话内容,自动创建/更新记忆文件。
加载机制:不直接加载——写入文件后,通过 6.2(MEMORY.md 索引)和 6.3(语义召回)间接加载。
产生规则:后台 post-sampling hook 自动维护结构化笔记。
加载机制:不注入对话——压缩时替代 API 摘要调用。
文件: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.`,
...
}
}
系统提示指导模型的保存行为(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 行限制),所以只存索引。实际内容在独立文件中,通过语义召回按需加载。
// 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」——模型浪费对话轮次去检查目录是否存在。现在系统保证目录存在,模型直接写。
文件:src/memdir/findRelevantMemories.ts
语义召回使用 Sonnet 做选择(非 embedding),与主模型流式输出并行执行
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.`
// 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 迁移项目状态
记忆文件通常 < 50 个,frontmatter 的 description 就是人工/AI 写的摘要。用 Sonnet 做几十个候选的语义匹配,比维护 embedding 索引更简单、无额外基础设施依赖。
| 层面 | 限制 | 值 |
|---|---|---|
| 单次召回数量 | 最多 | 5 个文件 |
| 单个文件大小 | 最大 | 200 行 / 4KB |
| 单次注入总量 | 最大 | 20KB |
| 会话累计注入 | 最大 | 60KB(达到后停止预取) |
| 已出现文件 | 跳过 | 通过 alreadySurfaced Set 去重 |
| 已读文件 | 跳过 | 通过 readFileState LRU cache 去重 |
文件:src/services/extractMemories/extractMemories.ts
核心设计:不在主对话中提取,而是 fork 一个后台子 agent。
后台 Fork Agent 不干扰主对话,共享 prompt cache 实现近零额外成本
| 决策 | 理由 |
|---|---|
| Fork 而非内联 | 不占主对话 token,不干扰用户 |
| 共享 prompt cache | fork agent 复用主线程 cache,几乎零额外 cache 成本 |
| 互斥机制 | 主 agent 已写了 memory → 跳过 fork,避免重复 |
| 游标推进 | 只处理增量消息,不重复分析历史 |
| 合并去抖 | 提取中有新轮结束 → 存入 pendingContext,当前完成后再运行 |
| 5 轮限制 | 防止子 agent 在记忆目录中不停整理 |
| Flag | 控制 |
|---|---|
tengu_passport_quail | 启用/禁用提取 |
tengu_bramble_lintel | 节流(N 轮提取一次) |
tengu_moth_copse | 跳过 MEMORY.md 索引(append-only 模式) |
文件:src/services/teamMemorySync/
路径遍历防护(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 等)。发现密钥的文件静默跳过,永远不上传到服务器。
完整生命周期:创建(3 种方式)→ 存储 → 召回(3 种方式)→ 注入 → 使用 → 清理(3 种方式)
Session Memory 在上下文压缩中扮演关键角色——它是预计算的摘要。当 auto-compact 触发时:
| 模式 | 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)