乐于分享
好东西不私藏

Claude Code源码系列:4、2-关键功能模块-记忆系统

Claude Code源码系列:4、2-关键功能模块-记忆系统

1. 系统概述 (System Overview)

一句话核心价值

Claude Code 记忆系统是一个跨会话的、基于文件的持久化上下文管理系统,通过严格的四类型分类法(user/feedback/project/reference)自动提取和存储”不可从当前项目状态推导”的关键信息,确保 AI 在未来对话中能够持续理解用户偏好、项目背景和行为指导。

架构图 (ASCII)

┌───────────────────────────────────────────────────────────────────────┐│                          Claude Code Memory System                           ││                      (跨会话持久化上下文管理架构)                              │└───────────────────────────────────────────────────────────────────────┘                              ┌──────────────┐                              │   用户对话    │                              │  (Messages)  │                              └──────┬───────┘                                     │                                     ▼        ┌────────────────────────────────────────────────────────────┐        │                  Query Loop 结束触发                       │        │              (stopHooks.ts - handleStopHooks)              │        └───────────────────────────┬────────────────────────────────┘                                    │                    ┌───────────────┴───────────────┐                    │                               │                    ▼                               ▼        ┌─────────────────────┐         ┌─────────────────────────────┐        │    主代理直接写入    │         │   后台提取代理 (Forked)     │        │  (hasMemoryWrites   │  ───────│  extractMemories.ts         │        │   Since 检查跳过)   │  互斥    │  runForkedAgent 模式       │        └─────────────────────┘         └─────────────────────────────┘                    │                               │                    │                               ▼                    │                    ┌─────────────────────────────┐                    │                    │   工具权限沙箱              │                    │                    │ createAutoMemCanUseTool     │                    │                    │ (Read/Grep/Glob + 记忆目录   │                    │                    │  内 Edit/Write only)        │                    │                    └─────────────────────────────┘                    │                               │                    └───────────────┬───────────────┘                                    │                                    ▼        ┌────────────────────────────────────────────────────────────┐        │                     记忆文件写入                            │        │              ~/.claude/projects/<slug>/memory/             │        └───────────────────────────┬────────────────────────────────┘                                    │                                    ▼        ┌────────────────────────────────────────────────────────────┐        │                      MEMORY.md 索引                        │        │                (最多 200 行 / 25KB, 截断警告)              │        └───────────────────────────┬────────────────────────────────┘                                    │                    ┌───────────────┴───────────────┐                    │                               │                    ▼                               ▼        ┌─────────────────────┐         ┌─────────────────────────────┐        │     私有记忆        │         │      团队记忆               │        │   memory/*.md       │         │   memory/team/*.md          │        │   (用户个人偏好)    │         │   (项目共享约定)            │        └─────────────────────┘         │   + 文件Watcher同步         │                                        └─────────────────────────────┘                              ┌──────────────┐                              │   新会话启动  │                              └──────┬───────┘                                     │                                     ▼        ┌────────────────────────────────────────────────────────────┐        │                  loadMemoryPrompt()                        │        │                  (memdir.ts - 系统提示注入)                 │        └───────────────────────────┬────────────────────────────────┘                                    │                    ┌───────────────┴───────────────┐                    │                               │                    ▼                               ▼        ┌─────────────────────┐         ┌─────────────────────────────┐        │  buildMemoryLines   │         │  buildCombinedMemoryPrompt  │        │  (单目录模式)        │         │  (私有+团队组合模式)         │        └─────────────────────┘         └─────────────────────────────┘                    │                               │                    └───────────────┬───────────────┘                                    │                                    ▼        ┌────────────────────────────────────────────────────────────┐        │              System Prompt Section 注册                    │        │         prompts.ts - 'memory' 动态提示部分                 │        └────────────────────────────────────────────────────────────┘

核心组件概念 (3-4个)

组件
概念定义
设计意图
Memory Types (记忆类型)
四种分类法:user / feedback / project / reference
严格限定”不可从项目状态推导”的信息边界,防止记忆膨胀
MEMORY.md Index (索引文件)
最多200行/25KB的轻量索引,指向实际记忆内容文件
控制上下文注入成本,让模型快速定位相关记忆
Forked Agent (分支代理)
记忆提取的后台代理,共享主对话的提示缓存
不干扰主对话,高效利用缓存,工具权限被沙箱化
Team Memory Sync (团队同步)
文件Watcher监听团队记忆变更,debounce推送至服务器
多用户协作共享项目约定,防止敏感数据泄露

2. 核心机制拆解

2.1 记忆类型系统 (Memory Type Taxonomy)

2.1.1 核心文件/组件职责

文件路径
职责
关键导出
src/memdir/memoryTypes.ts
四类型定义与分类法约束
MEMORY_TYPES

MemoryTypeparseMemoryType()
src/memdir/memoryTypes.ts
类型描述模板(组合/单独模式)
TYPES_SECTION_COMBINED

TYPES_SECTION_INDIVIDUAL
src/memdir/memoryTypes.ts
禁止保存内容清单
WHAT_NOT_TO_SAVE_SECTION
src/memdir/memoryTypes.ts
记忆新鲜度警告文本
MEMORY_DRIFT_CAVEAT

2.1.2 数据结构与配置

// 文件: src/memdir/memoryTypes.ts (行14-21)// 四种记忆类型的常量数组 — 作为类型约束的基础export const MEMORY_TYPES = ['user',      // 用户角色、偏好、知识'feedback',  // 行为指导(纠正/确认)'project',   // 项目上下文(截止日期、决策)'reference'// 外部系统指针(Linear、Slack)as const// TypeScript 类型推导 — 确保类型字符串只能是这四种export type MemoryType = (typeof MEMORY_TYPES)[number]/** * 解析 frontmatter 中的 type 字段 *  * @design 防范边界情况: *   - legacy文件可能没有type字段 → undefined(继续工作) *   - 未知类型值 → undefined(优雅降级) *   - 非字符串类型 → undefined(类型安全) */export functionparseMemoryType(raw: unknown): MemoryType | undefined{if (typeof raw !== 'string'return undefinedreturn MEMORY_TYPES.find(t => t === raw)}

关键设计特点:

  • as const + typeof 组合实现严格的字符串枚举类型约束
  • parseMemoryType() 对历史文件做兼容处理,避免解析失败阻断记忆加载

2.1.3 触发条件/核心逻辑

记忆类型的保存触发条件由 TYPES_SECTION_* 中的 <when_to_save> 标签定义:

类型
触发条件
代码位置
user
“When you learn any details about the user’s role, preferences, responsibilities, or knowledge”
memoryTypes.ts:47-48
feedback
“Any time the user corrects your approach… OR confirms a non-obvious approach worked”
memoryTypes.ts:61
project
“When you learn who is doing what, why, or by when”
memoryTypes.ts:79
reference
“When you learn about resources in external systems and their purpose”
memoryTypes.ts:94

关键边界条件 (WHAT_NOT_TO_SAVE_SECTION):

// 文件: src/memdir/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',// ❌ Git历史是权威来源'- Git history, recent changes, or who-changed-what — `git log` / `git blame`',// ❌ 修复在代码中,上下文在commit message'- Debugging solutions or fix recipes — the fix is in the code; the commit message',// ❌ 已有CLAUDE.md文档的不要重复'- Anything already documented in CLAUDE.md files.',// ❌ 临时状态不需要跨会话'- Ephemeral task details: in-progress work, temporary state, current conversation','',// ⚠️ 显式保存请求的过滤 — 防止用户把周报当记忆存'These exclusions apply even when the user explicitly asks you to save.',]

2.1.4 执行流程

Step 1: 用户对话产生信息 → 主代理或后台提取代理分析内容Step 2: 代理判断信息是否属于四种类型之一        │        ├─→ user: 学习用户角色/偏好 → 私有目录        ├─→ feedback: 用户纠正/确认 → 私有目录(除非是项目级约定)        ├─→ project: 项目上下文/决策 → 强烈倾向于团队目录        └─→ reference: 外部系统指针 → 通常团队目录Step 3: 检查 WHAT_NOT_TO_SAVE_SECTION 排除项        │        └─→ 如果是可推导内容 → 不保存Step 4: 生成 frontmatter 格式的记忆文件        │        └─→ name, description, type 三字段必须填写Step 5: 更新 MEMORY.md 索引(除非 skipIndex=true

2.2 记忆加载机制 (Memory Loading)

2.2.1 核心文件/组件职责

文件路径
职责
关键导出
src/memdir/memdir.ts
记忆提示构建与加载入口
loadMemoryPrompt()

buildMemoryLines()
src/memdir/paths.ts
路径解析、环境变量读取、安全验证
getAutoMemPath()

isAutoMemPath()
src/memdir/memoryScan.ts
目录扫描、frontmatter头读取
scanMemoryFiles()

MemoryHeader
src/constants/prompts.ts
系统提示注册入口
systemPromptSection('memory', ...)

2.2.2 数据结构与配置

// 文件: src/memdir/memdir.ts (行34-38)export const ENTRYPOINT_NAME = 'MEMORY.md'// 索引文件名export const MAX_ENTRYPOINT_LINES = 200// 行数上限export const MAX_ENTRYPOINT_BYTES = 25_000       // 字节上限 (~25KB)// 文件: src/memdir/memoryScan.ts (行13-19)export type MemoryHeader = {  filename: string// 相对于memory目录的路径  filePath: string// 绝对路径  mtimeMs: number// 最后修改时间(毫秒)— 用于新鲜度追踪  description: string | null// frontmatter中的descriptiontype: MemoryType | undefined// 解析后的类型}const MAX_MEMORY_FILES = 200// 单次扫描最多200个文件const FRONTMATTER_MAX_LINES = 30// 只读前30行获取frontmatter

关键设计特点:

  • 双重限制(行数+字节)防止超长索引文件吞噬上下文
  • mtimeMs 穿透传递,避免二次 stat 调用

2.2.3 触发条件/核心逻辑

// 文件: src/constants/prompts.ts (行495)// 记忆作为动态系统提示部分注册 — 每次构建系统提示时调用systemPromptSection('memory'() => loadMemoryPrompt()),

触发时机:

  1. 新会话启动 → 系统提示构建 → 调用 loadMemoryPrompt()
  2. 提示缓存失效 → 动态部分重新计算
// 文件: src/memdir/memdir.ts (行419-507)export async functionloadMemoryPrompt(): Promise<string | null{const autoEnabled = isAutoMemoryEnabled()// 功能标志: 是否跳过MEMORY.md索引(直接写topic files)const skipIndex = getFeatureValue_CACHED_MAY_BE_STALE('tengu_moth_copse'false)// 分支1: KAIROS模式(助理长会话)— 使用日期日志而非索引if (feature('KAIROS') && autoEnabled && getKairosActive()) {return buildAssistantDailyLogPrompt(skipIndex)  }// 分支2: 团队记忆组合模式if (feature('TEAMMEM') && teamMemPaths!.isTeamMemoryEnabled()) {return teamMemPrompts!.buildCombinedMemoryPrompt(extraGuidelines, skipIndex)  }// 分支3: 仅私有记忆模式if (autoEnabled) {return buildMemoryLines('auto memory', autoDir, extraGuidelines, skipIndex).join('\n')  }// 记忆禁用 → 返回null,系统提示中不包含记忆部分return null}

2.2.4 执行流程

Step 1: 新会话启动 → prompts.ts 构建系统提示Step 2: systemPromptSection('memory') 被调用        │        └─→ loadMemoryPrompt() 入口Step 3: 检查记忆是否启用 (isAutoMemoryEnabled)        │        ├─→ 禁用 → 返回 null(不注入记忆部分)        └─→ 启用 → 继续Step 4: 分支选择模式        │        ├─→ KAIROS模式 → buildAssistantDailyLogPrompt()        ├─→ TEAMMEM模式 → buildCombinedMemoryPrompt()        └─→ 默认模式 → buildMemoryLines()Step 5: 确保记忆目录存在 (ensureMemoryDirExists)        │        └─→ fs.mkdir(memoryDir) — recursive + EEXIST swallowStep 6: 读取 MEMORY.md 索引文件        │        └─→ truncateEntrypointContent() — 截断到200行/25KBStep 7: 组装完整提示文本        │        └─→ 类型说明 + 禁止保存清单 + 如何保存 + 如何访问 + 新鲜度警告Step 8: 注入到系统提示

2.3 记忆提取机制 (Memory Extraction)

2.3.1 核心文件/组件职责

文件路径
职责
关键导出
src/services/extractMemories/extractMemories.ts
后台提取代理主逻辑
executeExtractMemories()

initExtractMemories()
src/services/extractMemories/prompts.ts
提取提示模板
buildExtractAutoOnlyPrompt()

buildExtractCombinedPrompt()
src/query/stopHooks.ts
查询循环结束触发
handleStopHooks()
src/utils/forkedAgent.ts
分支代理运行框架
runForkedAgent()

createCacheSafeParams()

2.3.2 数据结构与配置

// 文件: src/services/extractMemories/extractMemories.ts (行275-326)// Closure-scoped 状态管理 — 避免模块级变量污染/** 正在执行的提取Promise集合 — 用于drain等待 */const inFlightExtractions = new Set<Promise<void>>()/** 上次处理的消息UUID — 游标,避免重复处理 */let lastMemoryMessageUuid: string | undefined/** 提取进行中标志 — 防止重叠执行 */let inProgress = false/** 自上次提取以来的轮次数 — 用于节流 */let turnsSinceLastExtraction = 0/** stash的pending context — 用于trailing extraction */let pendingContext: { context, appendSystemMessage } | undefined

关键设计特点:

  • Closure-scoped 而非 module-level,便于测试时 initExtractMemories() 重置
  • inFlightExtractions 用于进程退出前的 drain 等待

2.3.3 触发条件/核心逻辑

// 文件: src/query/stopHooks.ts (行142-157)// 在每次查询循环结束时触发 — 模型产生最终响应且无工具调用时void extractMemoriesModule!.executeExtractMemories(  stopHookContext,  toolUseContext.appendSystemMessage,)

触发时机:

  1. Query Loop 结束 → handleStopHooks → fire-and-forget 调用提取
  2. 非阻塞执行 → 使用 void 关键字,不等待结果
// 文件: src/services/extractMemories/extractMemories.ts (行527-567)async functionexecuteExtractMemoriesImpl(  context: REPLHookContext,  appendSystemMessage?: AppendSystemMessageFn,): Promise<void{// Gate 1: 只为主代理运行,子代理跳过if (context.toolUseContext.agentId) return// Gate 2: 功能标志检查if (!getFeatureValue_CACHED_MAY_BE_STALE('tengu_passport_quail'false)) return// Gate 3: 记忆功能启用检查if (!isAutoMemoryEnabled()) return// Gate 4: 远程模式跳过if (getIsRemoteMode()) return// Gate 5: 正在进行中 → stash pending contextif (inProgress) {    pendingContext = { context, appendSystemMessage }return  }await runExtraction({ context, appendSystemMessage })}

2.3.4 执行流程

Step 1: Query Loop 结束 → handleStopHooks 触发Step 2: executeExtractMemories() 入口        │        └─→ 检查五个Gate条件Step 3: 检查主代理是否已写入记忆 (hasMemoryWritesSince)        │        ├─→ 已写入 → 跳过,仅更新游标        └─→ 未写入 → 继续后台提取Step 4: 节流检查 (tengu_bramble_lintel)        │        ├─→ 未达到轮次数 → 返回        └─→ 达到 → 执行提取Step 5: 扫描现有记忆文件 (scanMemoryFiles)        │        └─→ 生成 manifest 注入到提取提示Step 6: 构建 forked agent 提示        │        └─→ buildExtractAutoOnlyPrompt() 或 buildExtractCombinedPrompt()Step 7: 运行 forked agent        │        ├─→ 共享主对话的提示缓存 (cacheSafeParams)        ├─→ 工具权限沙箱 (canUseTool)        ├─→ 最大5轮限制 (maxTurns: 5)        └─→ 不写入transcript (skipTranscript: true)Step 8: 提取写入的文件路径 (extractWrittenPaths)        │        └─→ 记录 telemetry,追加系统消息通知Step 9: 更新游标 (lastMemoryMessageUuid)        │        └─→ 下次只处理新增消息Step 10: 处理 stash 的 pending context (trailing extraction)

2.4 相关记忆检索 (Relevance Retrieval)

2.4.1 核心文件/组件职责

文件路径
职责
关键导出
src/memdir/findRelevantMemories.ts
使用Sonnet选择最相关记忆
findRelevantMemories()

RelevantMemory
src/utils/sideQuery.ts
轻量级API调用封装
sideQuery()

2.4.2 数据结构与配置

// 文件: src/memdir/findRelevantMemories.ts (行13-16)exporttype RelevantMemory = {  path: string// 记忆文件绝对路径  mtimeMs: number// 修改时间 — 用于新鲜度提示}// 文件: src/memdir/findRelevantMemories.ts (行18-24)const SELECT_MEMORIES_SYSTEM_PROMPT = `You are selecting memories that will be useful to Claude Code...Return a list of filenames for the memories that will clearly be useful...(up to 5)  // ← 最多选择5个记忆- If you are unsure... then do not include it in your list.- If there are no memories... feel free to return an empty list.- If a list of recently-used tools is provided, do not select memories  that are usage reference or API documentation for those tools...`

关键设计特点:

  • JSON Schema 强制输出格式 { selected_memories: string[] }
  • recentTools 过滤:排除正在使用工具的参考文档

2.4.3 触发条件/核心逻辑

// 文件: src/memdir/findRelevantMemories.ts (行39-75)export async functionfindRelevantMemories(  query: string,                 // 用户查询文本  memoryDir: string,             // 记忆目录路径  signal: AbortSignal,  recentTools: readonly string[] = [],    // 正在使用的工具列表  alreadySurfaced: ReadonlySet<string> = new Set(),  // 已展示的记忆): Promise<RelevantMemory[]> {// Step 1: 扫描记忆文件,排除已展示的const memories = (await scanMemoryFiles(memoryDir, signal))    .filter(m => !alreadySurfaced.has(m.filePath))if (memories.length === 0return []// Step 2: 调用Sonnet选择相关记忆const selectedFilenames = await selectRelevantMemories(query, memories, signal, recentTools)// Step 3: 映射回完整的MemoryHeaderconst byFilename = new Map(memories.map(m => [m.filename, m]))const selected = selectedFilenames    .map(filename => byFilename.get(filename))    .filter((m): m is MemoryHeader => m !== undefined)return selected.map(m => ({ path: m.filePath, mtimeMs: m.mtimeMs }))}

2.4.4 执行流程

Step 1: 扫描记忆目录 → scanMemoryFiles()        │        └─→ 排除 MEMORY.md 和已展示的记忆 (alreadySurfaced)Step 2: 格式化 manifest → formatMemoryManifest()        │        └─→ "- [type] filename (timestamp): description"Step 3: 构建选择提示        │        ├─→ Query: {用户查询}        ├─→ Available memories: {manifest}        └─→ Recently used tools: {recentTools} (如果有)Step 4: 调用 sideQuery (Sonnet模型)        │        ├─→ model: getDefaultSonnetModel()        ├─→ max_tokens: 256        └─→ output_format: JSON SchemaStep 5: 解析 JSON 输出        │        ├─→ { selected_memories: ["file1.md""file2.md"] }        └─→ 过滤无效文件名Step 6: 返回最多5个 RelevantMemory        │        └─→ 包含 path + mtimeMs

2.5 记忆新鲜度机制 (Memory Freshness)

2.5.1 核心文件/组件职责

文件路径
职责
关键导出
src/memdir/memoryAge.ts
新鲜度计算与警告文本
memoryAgeDays()

memoryFreshnessText()
src/memdir/memoryTypes.ts
记忆过期提示模板
MEMORY_DRIFT_CAVEAT

2.5.2 数据结构与配置

// 文件: src/memdir/memoryAge.ts// 一天的毫秒数常量const MS_PER_DAY = 86_400_000  // 24 * 60 * 60 * 1000/** * 计算记忆年龄(天数) *  * @design 边界条件: *   - Floor-rounded: 今天=0, 昨天=1 *   - 未来时间/时钟偏差 → clamp到0 */exportfunctionmemoryAgeDays(mtimeMs: number): number{returnMath.max(0Math.floor((Date.now() - mtimeMs) / 86_400_000))}

2.5.3 触发条件/核心逻辑

// 文件: src/memdir/memoryAge.ts (行33-42)/** * 新鲜度警告文本 — 超过1天的记忆才显示 *  * @design 为什么是1天? *   - 今天的记忆足够新鲜,警告是噪音 *   - 超过1天的记忆可能引用已变更的代码(file:line citations) *   - 用户报告过旧记忆被当作事实断言的问题 */export functionmemoryFreshnessText(mtimeMs: number): string{const d = memoryAgeDays(mtimeMs)if (d <= 1return ''// 新鲜记忆不警告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.`  )}

3. 深度机制/策略系统

记忆类型分类详解

3.1 四类型分类法对比

类型
作用域
保存时机
内容结构
示例
user always private
学习用户角色/偏好/知识时
自由格式描述
“用户是数据科学家,目前关注observability/logging”
feedback default private

 (项目约定→team)
用户纠正/确认时
Rule → Why → How to apply

 三段式
“integration tests必须用真实DB。Why: mock/prod分歧导致上次prod失败。How: 所有DB测试禁用mock”
project bias toward team
学习项目上下文/截止日期时
Fact → Why → How to apply

 三段式
“merge freeze 2026-03-05开始。Why: mobile团队切release分支。How: 非critical PR标记freeze后日期”
reference usually team
学习外部系统指针时
简洁指针描述
“pipeline bugs在Linear项目INGEST追踪”

3.2 类型作用域决策矩阵

                    ┌─────────────────────────────────────┐                    │         Scope Decision Matrix       │                    └─────────────────────────────────────┘    private scope                              team scope    (个人偏好/知识)                             (项目共享约定)    ┌─────────────────┐                       ┌─────────────────┐    │     user        │                       │                 │    │  (always)       │                       │                 │    │                 │                       │                 │    └─────────────────┘                       │                 │                                              │                 │    ┌─────────────────┐                       │                 │    │    feedback     │ ──────────────────────│─→ feedback      │    │  (default)      │  项目级约定时          │  (项目约定)     │    │                 │                       │                 │    └─────────────────┘                       │                 │                                              │                 │    ┌─────────────────┐                       └─────────────────┐    │    project      │ ──────────────────────│─→ project       │    │  (可选)         │  强烈倾向team          │  (强烈倾向)     │    │                 │                       │                 │    └─────────────────┘                       └─────────────────┘                                              ┌─────────────────┐                                              │    reference    │                                              │  (usually)      │                                              │                 │                                              └─────────────────┘

3.3 Frontmatter 格式示例

---name: feedback_testing_policydescription: integration tests must hit real database, not mockstype: feedback---**Rule**: Integration tests must hit a real database, not mocks.**Why**: We got burned last quarter when mocked tests passed but the prod migration failed — mock/prod divergence masked a broken migration.**How to apply**: All database-related tests must use the real test database instance. This is a project testing policy, not a personal preference.

3.4 记忆保存两步流程

┌──────────────────────────────────────────────────────────────────────┐│                      Memory Save Two-Step Process                           │└──────────────────────────────────────────────────────────────────────┘    Step 1: 写入记忆文件                                             Step 2: 更新索引    ┌─────────────────────────────────────┐                        ┌─────────────────────────────────────┐    │                                     │                        │                                     │    │   memory/feedback_testing.md        │                        │   MEMORY.md                         │    │                                     │                        │                                     │    │   ---                               │                        │   - [Testing Policy](feedback_testing.md) │    │   name: feedback_testing_policy     │                        │    — integration tests use real DB  │    │   description: ...                  │                        │                                     │    │   type: feedback                    │                        │   (one line, ~150 chars max)        │    │   ---                               │                        │                                     │    │                                     │                        │                                     │    │   **Rule**: ...                     │                        │                                     │    │   **Why**: ...                      │                        │                                     │    │   **How to apply**: ...             │                        │                                     │    │                                     │                        │                                     │    └─────────────────────────────────────┘                        └─────────────────────────────────────┘                    │                                                │                    │                                                │                    ▼                                                ▼            内容文件(完整记忆)                               索引文件(快速定位)    ✓ 完整的记忆内容                                ✓ 每行一个链接,~150字符    ✓ frontmatter必须                              ✓ 无frontmatter    ✓ 按主题组织,不按时间                         ✓ 超过200行会被截断

4. 代码片段深度解析 (Hardcore Code Snippets)

4.1 parseMemoryType() — 类型解析的优雅降级

【文件定位】src/memdir/memoryTypes.ts:28-31

/** * Parse a raw frontmatter value into a MemoryType. * Invalid or missing values return undefined — legacy files without a * `type:` field keep working, files with unknown types degrade gracefully. */exportfunctionparseMemoryType(raw: unknown): MemoryType | undefined{if (typeof raw !== 'string'returnundefinedreturn MEMORY_TYPES.find(t => t === raw)}

【深度解析】

  1. 防范的边界情况

    • legacy文件兼容:历史记忆文件可能没有 type 字段 → undefined(不阻断加载)
    • 未知类型优雅降级type: "custom" → undefined(不是硬错误)
    • 非字符串输入type: 123 → undefined(类型安全)
  2. 算法巧妙之处

    • MEMORY_TYPES.find() 比 includes() 更语义化
    • undefined 而非 null 的选择:TypeScript中 undefined 是”缺失”的自然表示
  3. 变量演变

    • raw: unknown → 类型不确定的输入
    • typeof raw !== 'string' → 类型收窄为 string 或返回
    • MEMORY_TYPES.find() → 返回 MemoryType | undefined

4.2 truncateEntrypointContent() — 双重截断保护上下文

【文件定位】src/memdir/memdir.ts:57-103

/** * Truncate MEMORY.md content to the line AND byte caps. *  * @design 防止超长索引吞噬上下文: *   - Line-truncates first (natural boundary) *   - Byte-truncates at the last newline before the cap *   - Appends warning explaining which cap fired */export functiontruncateEntrypointContent(raw: string): EntrypointTruncation{const trimmed = raw.trim()const contentLines = trimmed.split('\n')const lineCount = contentLines.lengthconst byteCount = trimmed.length// 检查是否超过任一限制const wasLineTruncated = lineCount > MAX_ENTRYPOINT_LINES  // 200const wasByteTruncated = byteCount > MAX_ENTRYPOINT_BYTES  // 25KBif (!wasLineTruncated && !wasByteTruncated) {return { content: trimmed, lineCount, byteCount, wasLineTruncated, wasByteTruncated }  }// 先按行截断(自然边界)let truncated = wasLineTruncated    ? contentLines.slice(0, MAX_ENTRYPOINT_LINES).join('\n')    : trimmed// 再按字节截断 — 在最后一个换行处切割,避免切断行if (truncated.length > MAX_ENTRYPOINT_BYTES) {const cutAt = truncated.lastIndexOf('\n', MAX_ENTRYPOINT_BYTES)    truncated = truncated.slice(0, cutAt > 0 ? cutAt : MAX_ENTRYPOINT_BYTES)  }// 生成截断原因描述const reason =    wasByteTruncated && !wasLineTruncated      ? `${formatFileSize(byteCount)} (limit: ${formatFileSize(MAX_ENTRYPOINT_BYTES)}) — index entries are too long`      : wasLineTruncated && !wasByteTruncated        ? `${lineCount} lines (limit: ${MAX_ENTRYPOINT_LINES})`        : `${lineCount} lines and ${formatFileSize(byteCount)}`// 返回截断内容 + 警告return {    content: truncated +`\n\n> WARNING: ${ENTRYPOINT_NAME} is ${reason}. Only part of it was loaded. Keep index entries to one line under ~200 chars.`,    lineCount, byteCount, wasLineTruncated, wasByteTruncated  }}

【深度解析】

  1. 防范的边界情况

    • 超长行问题:有些用户把整段内容写进MEMORY.md → 字节限制兜底
    • 截断位置:在换行处切割,避免切断行造成格式混乱
    • 双重检查:先用原始byteCount检查(line-truncated后的内容可能仍超字节)
  2. 算法巧妙之处

    • lastIndexOf('\n', MAX_ENTRYPOINT_BYTES) 找最后一个安全换行
    • cutAt > 0 ? cutAt : MAX_ENTRYPOINT_BYTES 兜底:如果没有换行,硬切
  3. 变量演变

    • raw: string → 用户写入的原始内容
    • trimmed → 去除首尾空白
    • contentLines[] → 按行分割
    • truncated → 经过两次截断后的内容

4.3 scanMemoryFiles() — 高效的单次扫描

【文件定位】src/memdir/memoryScan.ts:35-77

/** * Scan a memory directory for .md files. *  * @design 单次扫描策略: *   - readFileInRange stats internally and returns mtimeMs *   - read-then-sort halves syscalls vs a separate stat round *   - For large N, we read a few extra small files but still avoid double-stat */export async functionscanMemoryFiles(  memoryDir: string,  signal: AbortSignal,): Promise<MemoryHeader[]> {try {// Step 1: 递归读取目录const entries = await readdir(memoryDir, { recursive: true })// Step 2: 过滤.md文件,排除MEMORY.mdconst mdFiles = entries.filter(f => f.endsWith('.md') && basename(f) !== 'MEMORY.md',    )// Step 3: 并行读取所有文件的frontmatter(前30行)const headerResults = await Promise.allSettled(      mdFiles.map(async (relativePath): Promise<MemoryHeader> => {const filePath = join(memoryDir, relativePath)// readFileInRange 内部调用stat获取mtimeMs → 避免二次statconst { content, mtimeMs } = await readFileInRange(          filePath,0,          FRONTMATTER_MAX_LINES,  // 30undefined,          signal,        )const { frontmatter } = parseFrontmatter(content, filePath)return {          filename: relativePath,          filePath,          mtimeMs,          // ← 从readFileInRange穿透          description: frontmatter.description || null,type: parseMemoryType(frontmatter.type),        }      }),    )// Step 4: 过滤成功结果,按时间排序,取前200个return headerResults      .filter((r): r is PromiseFulfilledResult<MemoryHeader> => r.status === 'fulfilled')      .map(r => r.value)      .sort((a, b) => b.mtimeMs - a.mtimeMs)  // 新的在前      .slice(0, MAX_MEMORY_FILES)            // 最多200个  } catch {return []  // 目录不存在或不可读 → 空数组  }}

【深度解析】

  1. 防范的边界情况

    • 目录不存在catch { return [] } — 不阻断记忆加载
    • 文件解析失败Promise.allSettled → 只取成功结果
    • 文件过多slice(0, 200) — 防止内存爆炸
  2. 算法巧妙之处

    • readFileInRange 内部 stat 获取 mtimeMs,避免 stat → read 双重调用
    • Promise.allSettled 而非 Promise.all:部分失败不影响整体
  3. 变量演变

    • entries[] → 目录下所有文件/子目录
    • mdFiles[] → 过滤后的.md文件列表
    • headerResults[] → Promise结果(settled状态)
    • MemoryHeader[] → 最终的记忆头信息列表

4.4 createAutoMemCanUseTool() — 工具权限沙箱

【文件定位】src/services/extractMemories/extractMemories.ts:171-222

/** * Creates a canUseTool function that allows Read/Grep/Glob (unrestricted), * read-only Bash commands, and Edit/Write only for paths within the * auto-memory directory. *  * @security 沙箱边界: *   - REPL允许(内部会重新调用canUseTool) *   - Read/Grep/Glob无限制(只读) *   - Bash只允许isReadOnly命令 *   - Edit/Write只允许记忆目录内路径 */export functioncreateAutoMemCanUseTool(memoryDir: string): CanUseToolFn{return async (tool: Tool, input: Record<string, unknown>) => {// Gate 1: REPL工具允许 — 内部会重新gate子工具if (tool.name === REPL_TOOL_NAME) {return { behavior: 'allow' as const, updatedInput: input }    }// Gate 2: Read/Grep/Glob无限制 — 本质只读if (      tool.name === FILE_READ_TOOL_NAME ||      tool.name === GREP_TOOL_NAME ||      tool.name === GLOB_TOOL_NAME    ) {return { behavior: 'allow' asc onst, updatedInput: input }    }// Gate 3: Bash只允许read-only命令if (tool.name === BASH_TOOL_NAME) {const parsed = tool.inputSchema.safeParse(input)if (parsed.success && tool.isReadOnly(parsed.data)) {return { behavior: 'allow' as const, updatedInput: input }      }return denyAutoMemTool(        tool,'Only read-only shell commands are permitted (ls, find, grep, cat, stat, wc, head, tail)',      )    }// Gate 4: Edit/Write只允许记忆目录内路径if (      (tool.name === FILE_EDIT_TOOL_NAME || tool.name === FILE_WRITE_TOOL_NAME) &&'file_path' in input    ) {const filePath = input.file_path// isAutoMemPath 做了normalize防止路径遍历if (typeof filePath === 'string' && isAutoMemPath(filePath)) {return { behavior: 'allow' as const, updatedInput: input }      }    }// 默认拒绝所有其他工具return denyAutoMemTool(      tool,`only Read/Grep/Glob, read-only Bash, and Edit/Write within ${memoryDir} are allowed`,    )  }}

【深度解析】

  1. 防范的边界情况

    • 路径遍历攻击isAutoMemPath(filePath) 内部 normalize() 防止 ../ 绕过
    • REPL嵌套调用:REPL内部的子工具会重新gate
    • 非字符串路径typeof filePath === 'string' 类型检查
  2. 算法巧妙之处

    • tool.isReadOnly(parsed.data) 利用工具自身的readonly判断
    • denyAutoMemTool 记录telemetry用于分析被拒绝的工具
  3. 安全边界

    • MCP工具、Agent工具、Bash rm — 全部被拒绝
    • 写入权限严格限制在记忆目录内

4.5 runExtraction() — 提取执行的核心循环

【文件定位】src/services/extractMemories/extractMemories.ts:329-523

/** * 执行记忆提取的核心逻辑 *  * @design 关键机制: *   - 互斥检查:主代理已写入时跳过 *   - 节流机制:按轮次数控制执行频率 *   - trailing extraction:处理stash的pending context */async functionrunExtraction({  context,  appendSystemMessage,  isTrailingRun,}: {...}): Promise<void{const { messages } = contextconst memoryDir = getAutoMemPath()// 计算新增消息数(从游标位置开始)const newMessageCount = countModelVisibleMessagesSince(    messages,    lastMemoryMessageUuid,  )// 互斥检查:主代理已写入记忆 → 跳过,只更新游标if (hasMemoryWritesSince(messages, lastMemoryMessageUuid)) {    logForDebugging('[extractMemories] skipping — conversation already wrote to memory')const lastMessage = messages.at(-1)if (lastMessage?.uuid) {      lastMemoryMessageUuid = lastMessage.uuid    }    logEvent('tengu_extract_memories_skipped_direct_write', {      message_count: newMessageCount,    })return  }// 节流检查:非trailing run时检查轮次数if (!isTrailingRun) {    turnsSinceLastExtraction++if (      turnsSinceLastExtraction <      (getFeatureValue_CACHED_MAY_BE_STALE('tengu_bramble_lintel'null) ?? 1)    ) {return// 未达到执行轮次数    }  }  turnsSinceLastExtraction = 0  inProgress = trueconst startTime = Date.now()try {// 预注入现有记忆manifest — 避免代理浪费一轮lsconst existingMemories = formatMemoryManifest(await scanMemoryFiles(memoryDir, createAbortController().signal),    )// 构建提取提示const userPrompt = teamMemoryEnabled      ? buildExtractCombinedPrompt(newMessageCount, existingMemories, skipIndex)      : buildExtractAutoOnlyPrompt(newMessageCount, existingMemories, skipIndex)// 运行forked agentconst result = await runForkedAgent({      promptMessages: [createUserMessage({ content: userPrompt })],      cacheSafeParams,         // 共享主对话缓存      canUseTool,              // 沙箱化工具权限      querySource: 'extract_memories',      forkLabel: 'extract_memories',      skipTranscript: true,    // 不写入transcript      maxTurns: 5,             // 最大5轮    })// 更新游标const lastMessage = messages.at(-1)if (lastMessage?.uuid) {      lastMemoryMessageUuid = lastMessage.uuid    }// 提取写入的文件路径const writtenPaths = extractWrittenPaths(result.messages)const turnCount = count(result.messages, m => m.type === 'assistant')// 记录telemetry    logEvent('tengu_extract_memories_extraction', {      input_tokens: result.totalUsage.input_tokens,      cache_read_input_tokens: result.totalUsage.cache_read_input_tokens,      message_count: newMessageCount,      turn_count: turnCount,      files_written: writtenPaths.length,      duration_ms: Date.now() - startTime,    })// 通知用户记忆已保存if (memoryPaths.length > 0) {const msg = createMemorySavedMessage(memoryPaths)      appendSystemMessage?.(msg)    }  } catch (error) {    logForDebugging(`[extractMemories] error: ${error}`)// 失败不更新游标 → 下次重新处理这些消息  } finally {    inProgress = false// 处理stash的pending context — trailing extractionconst trailing = pendingContext    pendingContext = undefinedif (trailing) {await runExtraction({        context: trailing.context,        appendSystemMessage: trailing.appendSystemMessage,        isTrailingRun: true,  // ← 标记为trailing,跳过节流      })    }  }}

【深度解析】

  1. 防范的边界情况

    • 游标未找到sinceUuid 被compact删除 → fallback到全量计数
    • 提取失败:不更新游标 → 下次重新处理
    • 重叠调用inProgress 标志 + pendingContext stash
  2. 算法巧妙之处

    • hasMemoryWritesSince 互斥检查 → 主代理写入时跳过
    • isTrailingRun 标记 → trailing extraction跳过节流
    • finally 块处理pending context → 确保不丢失
  3. 变量演变

    • newMessageCount → 从游标位置计算的新消息数
    • turnsSinceLastExtraction → 节流计数器
    • inProgress → 互斥标志
    • pendingContext → stash的上下文
    • writtenPaths[] → 提取代理写入的文件列表

4.6 selectRelevantMemories() — Sonnet相关性选择

【文件定位】src/memdir/findRelevantMemories.ts:77-141

/** * 使用Sonnet模型选择最相关的记忆 *  * @design: *   - JSON Schema强制输出格式 *   - recentTools排除正在使用工具的参考文档 */async functionselectRelevantMemories(  query: string,  memories: MemoryHeader[],  signal: AbortSignal,  recentTools: readonly string[],): Promise<string[]> {const validFilenames = new Set(memories.map(m => m.filename))const manifest = formatMemoryManifest(memories)// recentTools过滤:正在使用的工具的参考文档是噪音const toolsSection =    recentTools.length > 0      ? `\n\nRecently used tools: ${recentTools.join(', ')}`      : ''try {const result = await sideQuery({      model: getDefaultSonnetModel(),  // 使用Sonnet而非Opus      system: SELECT_MEMORIES_SYSTEM_PROMPT,      skipSystemPromptPrefix: true,      messages: [        {          role: 'user',          content: `Query: ${query}\n\nAvailable memories:\n${manifest}${toolsSection}`,        },      ],      max_tokens: 256,// JSON Schema强制输出格式      output_format: {type'json_schema',        schema: {type'object',          properties: {            selected_memories: { type'array', items: { type'string' } },          },          required: ['selected_memories'],          additionalProperties: false,        },      },      signal,      querySource: 'memdir_relevance',    })// 解析JSON输出const textBlock = result.content.find(block => block.type === 'text')if (!textBlock || textBlock.type !== 'text'return []const parsed: { selected_memories: string[] } = jsonParse(textBlock.text)// 过滤无效文件名return parsed.selected_memories.filter(f => validFilenames.has(f))  } catch (e) {if (signal.aborted) return []    logForDebugging(`[memdir] selectRelevantMemories failed: ${errorMessage(e)}`)return []  }}

【深度解析】

  1. 防范的边界情况

    • 解析失败catch { return [] } — 不阻断主流程
    • 无效文件名validFilenames.has(f) 过滤
    • AbortSignal:取消时返回空数组
  2. 算法巧妙之处

    • sideQuery 而非完整API调用 — 轻量级
    • additionalProperties: false 严格JSON Schema
    • recentTools 排除正在使用工具的文档
  3. 变量演变

    • manifest → 格式化的记忆列表文本
    • toolsSection → recentTools提示(如果有)
    • parsed.selected_memories[] → Sonnet选择的文件名列表

4.7 validateMemoryPath() — 安全路径验证

【文件定位】src/memdir/paths.ts:109-150

/** * Normalize and validate a candidate auto-memory directory path. * * @security 拒绝危险路径: *   - relative (!isAbsolute): "../foo" *   - root/near-root (length < 3): "/" → "" *   - Windows drive-root (C: regex): "C:\" *   - UNC paths (\\server\share): network paths *   - null byte: survives normalize(), truncates in syscalls */functionvalidateMemoryPath(  raw: string | undefined,  expandTilde: boolean,): string | undefined{if (!raw) return undefinedlet candidate = raw// ~/ expansion (只在settings.json中启用)if (expandTilde && (candidate.startsWith('~/') || candidate.startsWith('~\\'))) {const rest = candidate.slice(2)// 拒绝会展开到$HOME或祖先的路径const restNorm = normalize(rest || '.')if (restNorm === '.' || restNorm === '..') {return undefined    }    candidate = join(homedir(), rest)  }// normalize并去除尾部分隔符const normalized = normalize(candidate).replace(/[/\\]+$/'')// 安全检查:拒绝危险路径if (    !isAbsolute(normalized) ||          // 相对路径    normalized.length < 3 ||            // root或太短    /^[A-Za-z]:$/.test(normalized) ||   // Windows drive-root    normalized.startsWith('\\\\') ||    // UNC path    normalized.startsWith('//') ||      // UNC path (alt)    normalized.includes('\0')           // null byte attack  ) {return undefined  }// 返回规范化路径 + 一个尾部分隔符return (normalized + sep).normalize('NFC')  // Unicode normalization}

【深度解析】

  1. 防范的边界情况

    • 路径遍历../ 通过 normalize() 解决
    • UNC路径:网络路径是 opaque trust boundary
    • null byte攻击\0 在某些syscall中会截断字符串
    • Unicode攻击normalize('NFC') 防止 Unicode normalization attack
    • Windows drive-rootC: → 拒绝
  2. 算法巧妙之处

    • normalize(candidate).replace(/[/\\]+$/, '') 去除尾部分隔符再添加一个
    • restNorm === '.' || restNorm === '..' 检测会展开到$HOME的路径
  3. 安全边界

    • 禁止 ~~/~/.~/.. — 会展开到HOME或祖先
    • projectSettings (仓库内settings.json) 被排除 — 防止恶意仓库攻击

4.8 memoryFreshnessText() — 新鲜度警告生成

【文件定位】src/memdir/memoryAge.ts:33-42

/** * Plain-text staleness caveat for memories >1 day old. * Returns '' for fresh (today/yesterday) memories — warning there is noise. * * @design 用户体验考量: *   - 超过1天的记忆可能引用已变更的代码 *   - file:line citations 让旧记忆看起来更权威(危险) *   - 用户报告过旧记忆被当作事实断言的问题 */export functionmemoryFreshnessText(mtimeMs: number): string{const d = memoryAgeDays(mtimeMs)if (d <= 1return ''// 新鲜记忆不警告(避免噪音)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.`  )}/** * 辅助函数:计算记忆年龄(天数) */export functionmemoryAgeDays(mtimeMs: number): number{// Floor-rounded: 今天=0, 昨天=1// 未来时间/时钟偏差 → Math.max(0, ...) clampreturn Math.max(0Math.floor((Date.now() - mtimeMs) / 86_400_000))}/** * 辅助函数:人类可读年龄字符串 */export functionmemoryAge(mtimeMs: number): string{const d = memoryAgeDays(mtimeMs)if (d === 0return 'today'if (d === 1return 'yesterday'return `${d} days ago`}

【深度解析】

  1. 防范的边界情况

    • 未来时间/时钟偏差Math.max(0, ...) clamp到0
    • 新鲜记忆噪音d <= 1 不警告,避免干扰用户
  2. 算法巧妙之处

    • Floor-rounded:今天=0,昨天=1(自然语义)
    • 三段式警告文本:年龄 + 本质声明 + 行动建议
  3. 设计动机

    • file:line citations 让旧记忆看起来权威(”权威陷阱”)
    • 用户报告:旧记忆被当作事实断言 → 需要明确提醒

5. 小结 (Summary)

架构分层

┌───────────────────────────────────────────────────────────────────────┐│                          Memory System Architecture Layers                   │└───────────────────────────────────────────────────────────────────────┘Layer 5: 同步层 (Team Memory Sync)         │  watcher.ts → File Watcher + 2s debounce push         │  teamMemPaths.ts → 路径安全验证         └─────────────────────────────────────────────┐                                                         │Layer 4: 检索层 (Relevance Retrieval)                  │         │  findRelevantMemories.ts → Sonnet选择(最多5个) │         │  memoryAge.ts → 新鲜度警告                   │         └─────────────────────────────────────────────┐                                                         │Layer 3: 提取层 (Memory Extraction)                    │         │  extractMemories.ts → Forked Agent          │         │  prompts.ts → 提取提示模板                   │         │  createAutoMemCanUseTool → 工具沙箱          │         └─────────────────────────────────────────────┐                                                         │Layer 2: 加载层 (Memory Loading)                       │         │  memdir.ts → loadMemoryPrompt()             │         │  buildMemoryLines() → 提示构建               │         │  truncateEntrypointContent → 截断保护        │         └─────────────────────────────────────────────┐                                                         │Layer 1: 存储层 (File Storage)                         │         │  ~/.claude/projects/<slug>/memory/          │         │  MEMORY.md (索引) + *.md (内容)              │         │  memory/team/ (团队记忆)                     │         └─────────────────────────────────────────────┘

关键设计原则 (Design Principles)

#
原则
体现
1 不可推导性 (Non-Derivability) WHAT_NOT_TO_SAVE_SECTION

 明确排除可从代码/git历史推导的内容;四类型分类法严格限定”不可推导”边界
2 沙箱隔离 (Sandbox Isolation) createAutoMemCanUseTool()

 严格限制提取代理的工具权限:Read/Grep/Glob + 记忆目录内Edit/Write only
3 互斥机制 (Mutual Exclusion) hasMemoryWritesSince()

 检查主代理是否已写入 → 跳过后台提取;inProgress 标志防止重叠执行
4 新鲜度追踪 (Freshness Tracking) mtimeMs

 穿透传递;memoryFreshnessText() 超过1天显示警告;MEMORY_DRIFT_CAVEAT 提醒验证当前状态
5 安全验证 (Security Validation) validateMemoryPath()

 拒绝路径遍历攻击:relative/root/UNC/null byte/Unicode normalization
6 缓存共享 (Cache Sharing) runForkedAgent()

 + createCacheSafeParams() 共享主对话的提示缓存prefix,避免重复计算

核心文件一览

文件
职责
关键导出
src/memdir/memoryTypes.ts
四类型分类法定义
MEMORY_TYPES

TYPES_SECTION_*WHAT_NOT_TO_SAVE_SECTION
src/memdir/memdir.ts
记忆加载与提示构建
loadMemoryPrompt()

buildMemoryLines()truncateEntrypointContent()
src/memdir/memoryScan.ts
目录扫描与frontmatter解析
scanMemoryFiles()

MemoryHeader
src/memdir/paths.ts
路径解析与安全验证
getAutoMemPath()

isAutoMemPath()validateMemoryPath()
src/memdir/findRelevantMemories.ts
相关性检索
findRelevantMemories()

selectRelevantMemories()
src/memdir/memoryAge.ts
新鲜度计算与警告
memoryAgeDays()

memoryFreshnessText()
src/memdir/teamMemPrompts.ts
团队记忆组合提示
buildCombinedMemoryPrompt()
src/services/extractMemories/extractMemories.ts
后台提取代理
executeExtractMemories()

createAutoMemCanUseTool()
src/services/extractMemories/prompts.ts
提取提示模板
buildExtractAutoOnlyPrompt()

buildExtractCombinedPrompt()