Code Reader
首页
帮助
设计文档
首页
帮助
设计文档
  • Claude Code 持久化记忆系统分析

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):

  1. 先行截断:取前 200 行(自然边界)
  2. 再字节截断:如果截断后仍超 25KB,在最后一个换行符处截断
  3. 追加警告:在截断内容后附加 > 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 MemoryPersistent 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:500Sonnet 选出的相关记忆文件内容
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_quailextractMemories 功能开关
tengu_moth_copse启用 prefetch 替代 MEMORY.md 索引注入
tengu_herring_clock团队记忆功能开关
tengu_coral_fern"Searching past context" 指引段
tengu_bramble_lintel提取节流(每 N 个 eligible turn 提取一次)
tengu_session_memorysession memory 功能开关

关键代码路径速查

模块文件核心函数
路径解析src/memdir/paths.tsgetAutoMemPath(), isAutoMemoryEnabled(), isAutoMemPath()
记忆类型src/memdir/memoryTypes.tsMEMORY_TYPES, TYPES_SECTION_*, WHAT_NOT_TO_SAVE_SECTION
提示构建src/memdir/memdir.tsloadMemoryPrompt(), buildMemoryLines(), truncateEntrypointContent()
目录扫描src/memdir/memoryScan.tsscanMemoryFiles(), formatMemoryManifest()
相关性选择src/memdir/findRelevantMemories.tsfindRelevantMemories(), selectRelevantMemories()
年龄计算src/memdir/memoryAge.tsmemoryAge(), memoryFreshnessText()
团队路径src/memdir/teamMemPaths.tsgetTeamMemPath(), validateTeamMemWritePath()
团队提示src/memdir/teamMemPrompts.tsbuildCombinedMemoryPrompt()
提取触发src/services/extractMemories/extractMemories.tsinitExtractMemories(), executeExtractMemories(), createAutoMemCanUseTool()
提取提示src/services/extractMemories/prompts.tsbuildExtractAutoOnlyPrompt(), buildExtractCombinedPrompt()
Session Memorysrc/services/SessionMemory/sessionMemory.tsinitSessionMemory(), shouldExtractMemory()
预取src/utils/attachments.tsstartRelevantMemoryPrefetch(), getRelevantMemoryAttachments()
Stop hookssrc/query/stopHooks.tshandleStopHooks()
文件检测src/utils/memoryFileDetection.tsisAutoManagedMemoryFile()
系统提示src/constants/prompts.tsgetSystemPrompt() (内含 loadMemoryPrompt())
Query 引擎src/QueryEngine.tsSDK 自定义提示路径的 memory 注入
启动初始化src/utils/backgroundHousekeeping.tsstartBackgroundHousekeeping() → initExtractMemories()