Claude Code 持久化记忆系统分析
系统概览
Claude Code 的记忆系统是一个多层、文件持久化的记忆架构,包含四个核心组件:
| 组件 | 职责 | 存储位置 |
|---|---|---|
| Auto Memory (memdir) | 跨会话持久化记忆,MEMORY.md 索引 + 主题文件 | ~/.claude/projects/<slug>/memory/ |
| Team Memory | 团队共享记忆,auto memory 的子目录 | ~/.claude/projects/<slug>/memory/team/ |
| Session Memory | 单会话内的对话摘要笔记 | ~/.claude/session-memory/ |
| Extract Memories | 后台 Agent,从对话中自动提取记忆 | 写入 auto memory 目录 |
MEMORY.md 文件格式与生命周期
文件组织:两层架构
记忆系统采用 索引 + 文件 的两层设计:
~/.claude/projects/<slug>/memory/
├── MEMORY.md ← 索引文件(始终加载到上下文)
├── user_role.md ← 记忆文件(按需加载)
├── feedback_testing.md
├── project_deadline.md
├── reference_linear.md
└── team/ ← 团队记忆子目录
├── MEMORY.md ← 团队记忆索引
└── ...
MEMORY.md是纯索引,每条一行,格式:- [Title](file.md) — one-line hook- 索引条目应控制在 ~150 字符以内
- 实际记忆内容写入独立
.md文件,使用 frontmatter 格式
记忆文件 Frontmatter 格式
src/memdir/memoryTypes.ts:261-270
---
name: {{memory name}}
description: {{one-line description — used to decide relevance in future conversations}}
type: {{user, feedback, project, reference}}
---
{{memory content — for feedback/project types, structure as: rule/fact, then **Why:** and **How to apply:** lines}}
四种记忆类型
src/memdir/memoryTypes.ts:14-19
| 类型 | 说明 | 团队模式 Scope |
|---|---|---|
user | 用户角色、偏好、知识背景 | always private |
feedback | 用户纠正/确认的行为指导 | 默认 private,项目级约定可 team |
project | 项目上下文、目标、决策 | 强烈倾向 team |
reference | 外部系统指针(Linear、Grafana 等) | usually team |
明确排除的内容
src/memdir/memoryTypes.ts:183-195
- 代码模式、架构、文件路径(可从项目状态推导)
- Git 历史(
git log是权威来源) - 调试解决方案(修复已在代码中)
- CLAUDE.md 中已有文档的内容
- 临时任务细节
记忆加载与截断策略
双重截断上限
src/memdir/memdir.ts:35-38
export const MAX_ENTRYPOINT_LINES = 200
export const MAX_ENTRYPOINT_BYTES = 25_000
截断逻辑(truncateEntrypointContent,src/memdir/memdir.ts:57-103):
- 先行截断:取前 200 行(自然边界)
- 再字节截断:如果截断后仍超 25KB,在最后一个换行符处截断
- 追加警告:在截断内容后附加
> WARNING:说明,提示用户将详细内容移入主题文件
截断原因组合:
- 仅行数超:
"N lines (limit: 200)" - 仅字节超:
"X KB (limit: 25 KB) — index entries are too long" - 两者都超:
"N lines and X KB"
系统提示注入路径
loadMemoryPrompt()(src/memdir/memdir.ts:419-507)是系统提示中注入记忆指令的入口:
loadMemoryPrompt()
├── KAIROS 模式 → buildAssistantDailyLogPrompt() // append-only 日志模式
├── TEAMMEM 启用 → buildCombinedMemoryPrompt() // 私有 + 团队双目录
└── auto 启用 → buildMemoryLines() // 单目录模式
系统提示中通过 systemPromptSection('memory', ...) 缓存(src/constants/prompts.ts:495),保证跨 turn 的 prompt cache 命中率。
SDK 自定义提示的特殊路径
src/QueryEngine.ts:316-319:当 SDK 调用者提供自定义系统提示并设置了 CLAUDE_COWORK_MEMORY_PATH_OVERRIDE 时,额外注入记忆机制提示文本。
记忆相关性扫描(Recall)
两阶段架构
记忆召回不是简单地把所有记忆注入上下文,而是采用 Sonnet 选择器 做相关性过滤:
用户输入 query
→ startRelevantMemoryPrefetch() 异步预取 [src/query.ts:301]
→ scanMemoryFiles() 扫描记忆目录 frontmatter [src/memdir/memoryScan.ts:35]
→ Sonnet sideQuery 选择最相关的 5 个文件 [src/memdir/findRelevantMemories.ts:77]
→ readMemoriesForSurfacing() 读取选中文件内容 [src/utils/attachments.ts:2279]
→ 作为 relevant_memories attachment 注入上下文
扫描原语
src/memdir/memoryScan.ts:35-77
- 递归读取 memory 目录下所有
.md文件(排除 MEMORY.md) - 仅读取前 30 行 frontmatter(
FRONTMATTER_MAX_LINES = 30) - 提取
filename、mtimeMs、description、type - 按 mtime 降序排列,上限 200 个文件(
MAX_MEMORY_FILES = 200) - 格式化为 manifest 文本:
- [type] filename (timestamp): description
Sonnet 选择器
src/memdir/findRelevantMemories.ts:17-24
选择器的 system prompt 强调 高精确度、低召回:
- 只选择"确信对处理 query 有帮助"的记忆,最多 5 个
- 不确定就不选(宁缺毋滥)
- 排除最近使用工具的 API 文档(对话中已有上下文)
- 但保留这些工具的 warnings/gotchas/known issues
选择器使用 sideQuery 以 Sonnet 模型运行(低成本),输出 JSON schema 约束的 selected_memories 数组。
预取机制
src/utils/attachments.ts:2361-2424
using pendingMemoryPrefetch = startRelevantMemoryPrefetch(state.messages, state.toolUseContext)
- 在 query loop 启动时异步触发,不阻塞主流程
- 使用
using语法确保 generator 退出时自动 dispose(取消 AbortController) - 会话级节流:累计已 surface 的记忆字节数超过
MAX_SESSION_BYTES后停止预取 - 去重:跳过已在
readFileState中或之前 surface 过的文件
记忆文件的截断与 Age 标注
src/utils/attachments.ts:2279-2309:读取选中记忆时施加额外限制:
MAX_MEMORY_LINES行数限制MAX_MEMORY_BYTES字节限制- 超限时截断并附加提示,引导用户用 FileReadTool 查看完整文件
src/memdir/memoryAge.ts:33-42:超过 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.
自动记忆提取(Write-side)
触发时机
src/query/stopHooks.ts:141-153
每次 query loop 的 turn 结束时(模型产出最终回复、无 tool calls),handleStopHooks fire-and-forget 地调用 executeExtractMemories。
初始化
src/utils/backgroundHousekeeping.ts:34-36:会话启动时调用 initExtractMemories(),创建闭包作用域的状态(cursor、inProgress、pendingContext 等)。
提取流程
src/services/extractMemories/extractMemories.ts:329-523
每个 turn 结束
→ isExtractModeActive()? [feature gate]
→ hasMemoryWritesSince()? [主 Agent 已写 → 跳过]
→ 节流检查 (turnsSinceLastExtraction < N)? [tengu_bramble_lintel]
→ scanMemoryFiles() 获取现有记忆清单
→ runForkedAgent() 运行后台提取 Agent
→ 读取记忆目录现有文件
→ 分析最近 N 条消息
→ 写入/更新记忆文件 + MEMORY.md 索引
→ advance cursor, 记录遥测
Forked Agent 权限
src/services/extractMemories/extractMemories.ts:171-222
提取 Agent 使用 createAutoMemCanUseTool 限制权限:
- 允许:Read / Grep / Glob(无限制)、只读 Bash、Edit/Write(仅限 memory 目录内)
- 拒绝:rm、写入 memory 目录外的文件、MCP、Agent 等
互斥机制
主 Agent 和后台提取 Agent 是互斥的(src/services/extractMemories/extractMemories.ts:121-148):
- 如果主 Agent 在对话中直接写了记忆文件(
hasMemoryWritesSince检测),后台 Agent 跳过该轮 - 后台 Agent 有硬性 turn 上限(
maxTurns: 5),通常 2-4 个 turn 完成(read → write)
Session Memory(会话级记忆)
与 Persistent Memory 的区别
| 维度 | Session Memory | Persistent Memory (memdir) |
|---|---|---|
| 生命周期 | 单次会话 | 跨会话持久化 |
| 用途 | 会话摘要/笔记(用于 compaction 后恢复上下文) | 长期记忆(用户偏好、项目上下文) |
| 触发方式 | Post-sampling hook(每 N 个 tool call 或 token 增长) | Stop hook(每次 turn 结束) |
| 存储位置 | ~/.claude/session-memory/ | ~/.claude/projects/<slug>/memory/ |
提取阈值
src/services/SessionMemory/sessionMemoryUtils.ts:32-36
export const DEFAULT_SESSION_MEMORY_CONFIG: SessionMemoryConfig = {
minimumMessageTokensToInit: 10000, // 首次初始化的 token 阈值
minimumTokensBetweenUpdate: 5000, // 两次更新间的最小 token 增长
toolCallsBetweenUpdates: 3, // 两次更新间的最小 tool call 数
}
触发条件(src/services/SessionMemory/sessionMemory.ts:134-181):
- Token 阈值 AND tool call 阈值同时满足,或
- Token 阈值满足 AND 最后一个 turn 无 tool call(自然对话断点)
Session Memory 文件结构
src/services/SessionMemory/prompts.ts:11-41(默认模板):
# Session Title
# Current State
# Task specification
# Files and Functions
# Workflow
# Errors & Corrections
# Codebase and System Documentation
# Learnings
# Key results
# Worklog
每个 section 有上限(MAX_SECTION_LENGTH = 2000 tokens),总计上限 MAX_TOTAL_SESSION_MEMORY_TOKENS = 12000 tokens。
团队记忆共享
路径与启用条件
src/memdir/teamMemPaths.ts:73-86
团队记忆路径: <autoMemPath>/team/
启用条件: isAutoMemoryEnabled() && feature('tengu_herring_clock')
团队记忆是 auto memory 的子目录,共享同一项目 key。
路径安全
src/memdir/teamMemPaths.ts:109-256 实现了严格的路径安全检查:
sanitizePathKey:拒绝 null 字节、URL 编码遍历、Unicode 归一化攻击、反斜杠realpathDeepestExisting:解析符号链接到最深层存在的祖先,检测悬空符号链接和循环validateTeamMemWritePath/validateTeamMemKey:双重检查(字符串级 + realpath 级)
Combined Prompt
src/memdir/teamMemPrompts.ts:22-99:当团队记忆启用时,系统提示包含:
- 两个 scope 级别:private(个人)和 team(团队共享)
- 四种记忆类型各自声明 scope 倾向
- 团队记忆在每个 session 开始时同步
- 禁止在团队记忆中保存敏感数据(API keys、凭证等)
记忆与 Query Loop 的集成
完整生命周期
┌─────────────────────────────────────────────────┐
│ Session Start │
│ backgroundHousekeeping → initExtractMemories() │
│ prompts.ts → loadMemoryPrompt() → system prompt │
└──────────────────────┬──────────────────────────┘
│
┌──────────────────────▼──────────────────────────┐
│ Each User Turn │
│ query.ts → startRelevantMemoryPrefetch() │
│ ├── scanMemoryFiles() │
│ ├── Sonnet sideQuery → select top 5 │
│ └── readMemoriesForSurfacing() │
│ attachments → relevant_memories attachment │
└──────────────────────┬──────────────────────────┘
│
┌──────────────────────▼──────────────────────────┐
│ Each Turn End (stopHooks) │
│ executeExtractMemories() [fire-and-forget] │
│ ├── hasMemoryWritesSince? → skip if true │
│ └── runForkedAgent() → write memory files │
│ executeAutoDream() [fire-and-forget] │
└─────────────────────────────────────────────────┘
关键注入点
| 注入点 | 位置 | 内容 |
|---|---|---|
| 系统提示 | src/constants/prompts.ts:495 | 记忆行为指令 + MEMORY.md 索引内容 |
| 相关记忆附件 | src/utils/attachments.ts:500 | Sonnet 选出的相关记忆文件内容 |
| Memory file 检测 | src/utils/memoryFileDetection.ts:133 | 识别 Claude 管理的记忆文件(collapse/badge 逻辑) |
| 读取工具集成 | src/utils/claudemd.ts:1142 | 当 tengu_moth_copse 开启时跳过 MEMORY.md 索引注入(由 prefetch 替代) |
Feature Gates 总结
| Gate | 作用 |
|---|---|
CLAUDE_CODE_DISABLE_AUTO_MEMORY | 全局关闭 auto memory |
CLAUDE_CODE_SIMPLE (--bare) | 关闭 auto memory + 提取 |
tengu_passport_quail | extractMemories 功能开关 |
tengu_moth_copse | 启用 prefetch 替代 MEMORY.md 索引注入 |
tengu_herring_clock | 团队记忆功能开关 |
tengu_coral_fern | "Searching past context" 指引段 |
tengu_bramble_lintel | 提取节流(每 N 个 eligible turn 提取一次) |
tengu_session_memory | session memory 功能开关 |
关键代码路径速查
| 模块 | 文件 | 核心函数 |
|---|---|---|
| 路径解析 | src/memdir/paths.ts | getAutoMemPath(), isAutoMemoryEnabled(), isAutoMemPath() |
| 记忆类型 | src/memdir/memoryTypes.ts | MEMORY_TYPES, TYPES_SECTION_*, WHAT_NOT_TO_SAVE_SECTION |
| 提示构建 | src/memdir/memdir.ts | loadMemoryPrompt(), buildMemoryLines(), truncateEntrypointContent() |
| 目录扫描 | src/memdir/memoryScan.ts | scanMemoryFiles(), formatMemoryManifest() |
| 相关性选择 | src/memdir/findRelevantMemories.ts | findRelevantMemories(), selectRelevantMemories() |
| 年龄计算 | src/memdir/memoryAge.ts | memoryAge(), memoryFreshnessText() |
| 团队路径 | src/memdir/teamMemPaths.ts | getTeamMemPath(), validateTeamMemWritePath() |
| 团队提示 | src/memdir/teamMemPrompts.ts | buildCombinedMemoryPrompt() |
| 提取触发 | src/services/extractMemories/extractMemories.ts | initExtractMemories(), executeExtractMemories(), createAutoMemCanUseTool() |
| 提取提示 | src/services/extractMemories/prompts.ts | buildExtractAutoOnlyPrompt(), buildExtractCombinedPrompt() |
| Session Memory | src/services/SessionMemory/sessionMemory.ts | initSessionMemory(), shouldExtractMemory() |
| 预取 | src/utils/attachments.ts | startRelevantMemoryPrefetch(), getRelevantMemoryAttachments() |
| Stop hooks | src/query/stopHooks.ts | handleStopHooks() |
| 文件检测 | src/utils/memoryFileDetection.ts | isAutoManagedMemoryFile() |
| 系统提示 | src/constants/prompts.ts | getSystemPrompt() (内含 loadMemoryPrompt()) |
| Query 引擎 | src/QueryEngine.ts | SDK 自定义提示路径的 memory 注入 |
| 启动初始化 | src/utils/backgroundHousekeeping.ts | startBackgroundHousekeeping() → initExtractMemories() |