01Memory System 是什么
一句话定义:让模型在每次新对话中"记住"用户偏好、项目上下文、反馈历史的、基于文件系统的持久化知识库。 与对话上下文(轮内)、CLAUDE.md(项目级人工编辑)相比,记忆系统的核心特征是:跨会话、模型自动写入、类型受限、按需召回。
1.1 核心设计理念
| 设计维度 | 选择 | 原因 |
|---|---|---|
| 存储介质 | .md 文件 + 文件系统 | 透明、可审计、可 git 管理、人类可读 |
| 索引机制 | MEMORY.md 入口文件 | 一次加载全部索引,按需读取详情 |
| 类型约束 | 封闭四类型(user / feedback / project / reference) | 防止记忆膨胀,排除可推导信息 |
| 写入方式 | 主代理直接写 + 后台提取代理补漏 | 双重保障,既不遗漏也不重复 |
| 召回方式 | 索引全量加载 + 相关性检索按需注入 | 兼顾覆盖面和 token 效率 |
| 缓存策略 | 系统提示词缓存 + 用户上下文动态注入 | 兼顾稳定性和实时性 |
1.2 五条核心设计原则
封闭类型法(Closed Taxonomy)
记忆只能是 user / feedback / project / reference 四种。可由代码推导的信息(架构、git 历史、CLAUDE.md 内容)被明确排除,防止记忆库膨胀成"项目知识库的劣化复制品"。
索引/详情两层架构
MEMORY.md 是常驻索引(≤200 行),具体记忆是按需加载的详情文件。索引始终可见 → 模型知道有哪些记忆;详情按需加载 → 节省 token。
双路径互斥写入
主代理在对话中可直接写记忆(FileWrite/Edit)。本轮主代理已写则后台提取代理跳过;未写则后台代理补漏。hasMemoryWritesSince 守卫互斥,避免重复写。
三通道注入
(1) system 提示词段落:使用说明(缓存)。(2) 用户上下文:MEMORY.md 索引(每轮)。(3) relevant_memories 附件:相关性检索的具体记忆(按需)。
新鲜度提示
超过 1 天的记忆自动附加"This memory is N days old"提示,明确告知模型记忆是过往观察、不是实时状态。专门应对"过时代码状态被当作事实复述"的失效模式。
分叉代理隔离
后台提取代理使用 runForkedAgent:共享主对话提示词缓存(cache-safe)、独立工具权限(仅允许 memory 目录内写入)、最多 5 回合(防验证兔子洞)、跳过 transcript(避免与主线程竞争)。
1.3 记忆系统在整体架构中的位置
动态内容块] MSG0["messages[0]
<system-reminder> userContext"] MSGN["messages[N]
<system-reminder> relevant_memories"] end subgraph "记忆系统三条注入通道" CH1["通道 1:systemPromptSection('memory')
记忆使用说明(缓存型)"] CH2["通道 2:claudemd.getMemoryFiles()
MEMORY.md 索引(动态)"] CH3["通道 3:startRelevantMemoryPrefetch()
具体记忆(按需)"] end subgraph "记忆存储" DIR["~/.claude/projects/<sanitized>/memory/
MEMORY.md + *.md"] end CH1 --> SYS CH2 --> MSG0 CH3 --> MSGN DIR -.读取.-> CH2 DIR -.读取.-> CH3 style CH1 fill:#FFF6F0,stroke:#D77757 style CH2 fill:#EAF1FF,stroke:#5769F7 style CH3 fill:#E8F5EE,stroke:#2D8659 style DIR fill:#FCE8DC,stroke:#B85C3D,stroke-width:2px
图 1.1 — 记忆系统通过三条独立通道向模型传递信息,分别对应不同位置和缓存策略
关键理解
很多人以为 loadMemoryPrompt() 返回的就是 MEMORY.md 的内容。实际上:
loadMemoryPrompt()返回的是记忆系统使用说明(类型定义、保存方法、召回规则)MEMORY.md的实际内容通过claudemd.ts的getMemoryFiles()注入- 两者通过不同的通道传递给模型,有不同的缓存策略
02存储 · 目录结构 + 路径解析
记忆持久化在文件系统中,目录结构按"用户级根 → 项目隔离 → 子模式"三层组织。 路径解析有严格的优先级链路与安全约束,能抵御目录遍历攻击与跨项目污染。
2.1 默认目录结构
2.2 路径解析优先级(getAutoMemPath())
位于 src/memdir/paths.ts,是一个 memoize(by projectRoot) 函数:
PATH_OVERRIDE 环境变量?"} B -->|有| C["validateMemoryPath(raw, false)
不展开 ~"] B -->|无| D{"settings.json
autoMemoryDirectory?"} D -->|有| E["仅读 policy / flag / local / user
排除 projectSettings
validateMemoryPath(raw, true)"] D -->|无| F["join(memoryBase,
'projects',
sanitizePath(gitRoot),
'memory') + sep"] C --> G["规范化为单尾分隔符 + NFC"] E --> G F --> G G --> H["返回完整路径"] style B fill:#FFF6F0,stroke:#D77757 style D fill:#FFF6F0,stroke:#D77757 style F fill:#E8F5EE,stroke:#2D8659 style G fill:#EAF1FF,stroke:#5769F7
图 2.1 — 路径解析优先级链路(src/memdir/paths.ts:216-228)
| 优先级 | 来源 | 使用场景 |
|---|---|---|
| 1(最高) | CLAUDE_COWORK_MEMORY_PATH_OVERRIDE 环境变量 |
Cowork 场景:把记忆重定向到按空间隔离的挂载点。否则按会话变化的 cwd(含 VM 进程名)会导致每次产生不同 project-key |
| 2 | settings.json 的 autoMemoryDirectory(仅信任 policy/flag/local/user 来源) |
用户显式配置的自定义目录。有意排除 projectSettings:否则恶意仓库可将该项指向 ~/.ssh 等敏感目录 |
| 3(默认) | <memoryBase>/projects/<sanitized-git-root>/memory/ |
memoryBase 由 getMemoryBaseDir() 解析:CCR 场景读 CLAUDE_CODE_REMOTE_MEMORY_DIR,否则 ~/.claude |
2.3 路径校验:六道安全约束
validateMemoryPath() 拒绝以下情形(paths.ts:106-145):
!isAbsolute)如
"../foo",会被解释为相对 CWD
"/" 去尾后为空,"/a" 过短
"C:\" 去尾后变 "C:"
\\server\share 网络边界不透明
normalize 后仍可存活,可能在系统调用中导致截断
~/、~/.、~/.. 等展开后会指向
$HOME 本身或祖先目录
2.4 启用判定链(isAutoMemoryEnabled())
// src/memdir/paths.ts:30-54
export function isAutoMemoryEnabled(): boolean {
const envVal = process.env.CLAUDE_CODE_DISABLE_AUTO_MEMORY
if (isEnvTruthy(envVal)) return false // 1. 显式禁用
if (isEnvDefinedFalsy(envVal)) return true // 2. 显式启用
if (isEnvTruthy(process.env.CLAUDE_CODE_SIMPLE)) // 3. --bare 模式关闭
return false
if (isEnvTruthy(process.env.CLAUDE_CODE_REMOTE) // 4. CCR 无持久存储
&& !process.env.CLAUDE_CODE_REMOTE_MEMORY_DIR)
return false
const settings = getInitialSettings() // 5. settings.json
if (settings.autoMemoryEnabled !== undefined)
return settings.autoMemoryEnabled
return true // 6. 默认开启
}
2.5 文件格式
具体记忆文件
每个记忆文件使用 frontmatter 格式:
---
name: 用户角色偏好
description: 数据科学家,关注可观测性
type: user
---
用户是数据科学家,目前专注于
可观测性/日志方向。
在解释代码时,应从数据分析的角度
出发,利用用户已有的领域知识构建
心智模型。
MEMORY.md 索引
无 frontmatter,每行一条记忆指针(≤150 字符):
- [用户角色偏好](user_role.md) —
数据科学家,关注可观测性
- [集成测试策略](feedback_testing.md) —
必须用真实数据库
- [Auth 重写背景](project_auth_rewrite.md) —
合规驱动
- [Pipeline Bug 追踪](reference_linear_ingest.md) —
Linear INGEST 项目
03类型 · 封闭四类型分类法
记忆被严格限制为四种类型,专门承载"无法从当前项目状态直接推导"的上下文信息。
定义在 src/memdir/memoryTypes.ts,是整个系统最核心的设计约束。
3.1 四种记忆类型
用户角色与偏好
用途:用户的角色、目标、责任、知识水平。
何时保存:了解到用户角色/偏好/责任/知识的任何细节。
如何使用:当工作需要根据用户视角调整时,比如解释代码时考虑用户领域知识。
示例:"用户是数据科学家,关注可观测性"
用户对工作方式的反馈
用途:用户对工作方式的指导(正面 + 负面)。
何时保存:用户纠正方式("不要 X")或确认非显而易见的方法("对,就这样做")时。纠正显眼,确认隐蔽,二者都要捕捉。
结构:规则本身 → **Why:** 行 → **How to apply:** 行。知道 为什么 才能判断边界情况。
示例:"集成测试必须用真实数据库,不要 mock。Why: 上季度因 mock/prod 分歧导致迁移失败"
项目上下文
用途:正在进行的工作、目标、bug、事件等不可从代码推导的项目信息。
何时保存:了解到谁在做什么、为什么、何时做。状态变化快,要保持更新。
关键约束:用户消息中的相对日期必须转为绝对日期("Thursday" → "2026-03-05"),否则时间一过就失去可解释性。
示例:"merge freeze 始于 2026-03-05,因 mobile 团队切发布分支"
外部系统指针
用途:外部系统中信息所在位置的指针,让模型记住去哪里查最新信息。
何时保存:了解到外部资源的位置和用途时。
如何使用:当用户引用外部系统或信息可能在外部系统时。
示例:"pipeline bug 在 Linear 项目 INGEST 中追踪","grafana.internal/d/api-latency 是 oncall 监控面板"
3.2 什么不应该保存(WHAT_NOT_TO_SAVE_SECTION)
禁止保存的信息
- 代码模式、约定、架构、文件路径、项目结构 → 可从当前项目状态推导
- Git 历史、最近变更、谁改了什么 →
git log/git blame是权威来源 - 调试方案或修复配方 → 修复在代码里,commit message 有上下文
- CLAUDE.md 中已有的内容 → 避免重复
- 临时任务细节:进行中的工作、临时状态、当前对话上下文 → 用 TodoWrite 而非记忆
关键约束:"这些排除规则即使用户明确要求保存也适用。如果用户要求保存 PR 列表或活动摘要,应反问什么是惊讶或非显而易见的,那才是值得保留的部分。"
3.3 召回侧约束(WHEN_TO_ACCESS_SECTION)
MEMORY.md 为空,不要应用、引用、对比、提及记忆内容(哪怕作为反例)
3.4 召回后的"信任校验"约束(TRUSTING_RECALL_SECTION)
// 标题措辞影响明显(来自 memory-prompt-iteration.eval.ts 评测验证):
// "Before recommending"(决策点动作提示)3/3 通过
// 抽象标题 "Trusting what you recall" 仅 0/3
## 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
(not just asking about history), 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.
04写入 · 主代理 + 后台提取双路径
记忆写入有两条互补路径:主代理在对话中根据系统提示词的指导主动写入;
后台提取代理在每个回合结束时补漏分析。两者通过 hasMemoryWritesSince 守卫互斥,
保证不重复也不遗漏。
feedback_package_manager.md"] W1 --> E1["FileEditTool
MEMORY.md 添加索引行"] end subgraph 路径二["路径二:后台 extractMemories"] P2 --> CHECK{"hasMemoryWritesSince?"} CHECK -->|是| SKIP["跳过本轮
推进游标"] CHECK -->|否| FORK["runForkedAgent
分析对话 → 写记忆"] end style P1 fill:#FFF6F0,stroke:#D77757 style P2 fill:#EAF1FF,stroke:#5769F7 style SKIP fill:#E8F5EE,stroke:#2D8659 style FORK fill:#F3EAFF,stroke:#6B4A8E
图 4.1 — 双路径写入机制:主代理直接写 + 后台代理补漏,互斥守卫
4.1 路径一:主代理直接写入
触发条件(来自系统提示词)
- 用户明确要求记住某事 → 立即保存
- 用户纠正模型行为 → 保存 feedback 类型
- 用户确认非显而易见的方法有效 → 保存 feedback 类型
- 了解到用户角色/偏好 → 保存 user 类型
- 了解到项目不可推导的上下文 → 保存 project 类型
- 了解到外部系统位置 → 保存 reference 类型
写入步骤(两步流程)
- Step 1:写入记忆文件(如
user_role.md),包含 frontmatter - Step 2:在
MEMORY.md中添加一行索引指针
当 tengu_moth_copse feature 启用时(skipIndex=true),跳过 Step 2,不再维护 MEMORY.md 索引。
4.2 路径二:后台提取代理(extractMemories)
4.2.1 运行时机与门控
位于 src/services/extractMemories/extractMemories.ts,在每个完整查询循环结束时(模型产出最终响应且无工具调用时),通过 handleStopHooks → stopHooks.ts 触发。
| 门控 | 条件 | 不通过的影响 |
|---|---|---|
feature('EXTRACT_MEMORIES') | build 必须启用此 feature | extractor 函数从未被注册 |
tengu_passport_quail | GrowthBook 实验门控 | 提取代理不运行(一次性记录 disabled 事件) |
isAutoMemoryEnabled() | auto memory 总开关 | 提取代理不运行 |
!getIsRemoteMode() | 非远程会话 | 远程模式下不运行 |
!context.toolUseContext.agentId | 仅主代理触发,子代理不触发 | 避免子代理意外触发提取 |
!hasMemoryWritesSince() | 本轮主代理未写入记忆 | 跳过提取(避免重复),推进游标 |
tengu_bramble_lintel | 每 N 个回合执行一次(默认 1) | 未到次数则跳过本轮 |
4.2.2 完美分叉模式(runForkedAgent)
图 4.2 — 后台提取代理的完整执行序列
4.2.3 工具权限收紧(createAutoMemCanUseTool)
分叉代理使用受限的 canUseTool 函数(extractMemories.ts:171-222):
FileReadTool、GrepTool、GlobTool(read-only,不限范围)
BashTool:仅 tool.isReadOnly(input) 返回 true 的命令(ls/find/grep/cat/stat/wc/head/tail)
FileEditTool、FileWriteTool:仅当 file_path 在 isAutoMemPath() 范围内
REPLTool 默认 allow(VM 内会再次调用 canUseTool 把控原始 Read/Bash/Edit/Write)
MCP / Agent / 写入 Bash / WebFetch / ...,全部记录 tengu_auto_mem_tool_denied 事件
4.2.4 节流与协调
turnsSinceLastExtraction
每次合格回合 +1,达到 tengu_bramble_lintel(默认 1)才执行。trailing run 跳过该检查(已暂存的工作不应被节流)。
pendingContext + inProgress
正在执行时新到的请求会被暂存(仅保留最新一份,因为它包含最多消息),当前执行完后再补跑一次 trailing extraction。
hasMemoryWritesSince
检查游标后是否有任何 assistant 消息包含 FileEdit/FileWrite tool_use 且 file_path 在 memory 目录内。如果有,跳过本轮提取并推进游标。
drainPendingExtraction
print.ts 在响应 flush 后、graceful shutdown 前调用,等待所有 in-flight 提取(含 trailing run)完成,soft timeout 60s。
4.2.5 高效写入策略(提取提示词)
提取提示词(prompts.ts 的 opener())显式指导分叉代理"两轮完成":
// extractMemories/prompts.ts:39
You have a limited turn budget. FileEditTool requires a prior
FileReadTool of the same file, so the efficient strategy is:
turn 1 — issue all FileReadTool calls in parallel for
every file you might update;
turn 2 — issue all FileWriteTool/FileEditTool
calls in parallel.
Do not interleave reads and writes across multiple turns.
You MUST only use content from the last ~N messages to update your
persistent memories. Do not waste any turns attempting to investigate
or verify that content further — no grepping source files, no reading
code to confirm a pattern exists, no git commands.
同时预注入 memory 目录清单(formatMemoryManifest 输出),避免分叉代理浪费一回合执行 ls。
05KAIROS · 按日追加日志模式
当 feature('KAIROS') 启用且 getKairosActive() 为 true 时,记忆写入方式改变:
不再实时维护 MEMORY.md 索引,改为按日期追加日志,夜间由独立 /dream 流程蒸馏。
5.1 设计动机
为什么需要 KAIROS 模式
Assistant 长驻会话中,频繁编辑 MEMORY.md 会反复打破系统提示词缓存前缀(虽然 MEMORY.md 在用户上下文,但相邻引用文件的变更会扰动 attachment 链)。
append-only 日志避免了重写带来的缓存失效,同时把"蒸馏 + 重组织"成本集中到夜间 batch 处理。
5.2 路径模式 vs 具体日期路径
// memdir/memdir.ts:325-345 buildAssistantDailyLogPrompt
const logPathPattern = join(memoryDir,
'logs', 'YYYY', 'MM', 'YYYY-MM-DD.md')
// 这里用"路径模式"而非"当天具体路径":
// 该提示词会被 systemPromptSection('memory', ...) 缓存,
// 不会在日期变化时自动失效。
// 模型应从 date_change 附件(午夜滚动时追加在尾部)
// 获取当前日期,而不是依赖 user-context 消息;
// 后者故意保持滞后以保住跨午夜的提示词缓存前缀。
5.3 KAIROS 写入流程
+ getKairosActive()?"} B -->|否| C["走标准 buildMemoryLines
实时维护 MEMORY.md"] B -->|是| D["buildAssistantDailyLogPrompt"] D --> E["告知模型:append 到
logs/YYYY/MM/YYYY-MM-DD.md"] E --> F["模型从 date_change
附件读取当天日期"] F --> G["首次写入:自动创建
父目录 + 日志文件"] G --> H["每条记录:短时间戳要点
append-only,不重写"] H -.夜间 batch.-> I["独立 /dream 流程
蒸馏为 topic 文件 + MEMORY.md"] style B fill:#F3EAFF,stroke:#6B4A8E style I fill:#FFEFD0,stroke:#C8862C
图 5.1 — KAIROS 模式:写入实时化 → 蒸馏批量化
5.4 何时记录什么(KAIROS 提示词节选)
用户的纠正与偏好("用 bun 不用 npm";"停止总结 diff")
不可从代码推导的项目上下文:deadlines、incidents、决策与原因
关于用户角色、目标的事实
外部系统指针(dashboard、Linear 项目、Slack 频道)
另外:用户明确要求记住的任何事。
KAIROS 与 TEAMMEM 互斥
"按日追加日志"的范式与 team sync 不可直接组合(team sync 依赖双方共享读写 MEMORY.md)。memdir.ts:426 中 KAIROS 分支优先于 TEAMMEM 分支,自然保证互斥。
06索引 · MEMORY.md 的双层架构
MEMORY.md 是记忆系统的入口文件,始终被加载到模型上下文中。
采用"索引/详情"两层架构:索引始终可见,详情按需加载。
6.1 双层架构
每行一条指针 - [Title](file.md) — hook,无 frontmatter,≤ 200 行 / ≤ 25KB。
注入位置:messages[0] 的 <system-reminder> 包裹。
user_role.md / feedback_*.md / project_*.md / reference_*.md
包含 frontmatter(name / description / type)。
通过 relevant_memories 附件或 FileReadTool 按需加载。
6.2 截断保护(truncateEntrypointContent)
位于 src/memdir/memdir.ts:58-104,采用双重截断策略:
或 字节 > 25KB?"} D -->|否| E["原样返回"] D -->|是| F["先按行截断
contentLines.slice(0, 200)"] F --> G{"截断后字节 > 25KB?"} G -->|是| H["在最后一个换行处切断
lastIndexOf('\\n', 25000)"] G -->|否| I["保持现状"] H --> J["追加 WARNING 提示"] I --> J J --> K["返回 EntrypointTruncation"] style D fill:#FFF6F0,stroke:#D77757 style F fill:#EAF1FF,stroke:#5769F7 style H fill:#FFE5E0,stroke:#C23B22 style J fill:#FFF7E6,stroke:#C8862C
图 6.1 — 双重截断保护机制:先按行 → 再按字节
| 失效模式 | 触发条件 | 截断策略 |
|---|---|---|
| 行数过多 | 常规索引膨胀 | 保留前 200 行 |
| 单行过长 | 恶意或误操作(曾观测 200 行内约 197KB) | 在 25KB 字节处切到最后一个换行 |
| 同时超限 | 极端情况 | 先行截断 → 再字节截断 |
6.3 截断后的 WARNING 提示
// memdir/memdir.ts:88-94
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)}`
// 末尾追加:
> WARNING: MEMORY.md is {reason}. Only part of it was loaded.
Keep index entries to one line under ~200 chars; move detail
into topic files.
6.4 索引加载链路
通过 claudemd.ts 的 getMemoryFiles() 函数(memoize),作为 AutoMem 类型的记忆文件,注入到用户上下文的 claudeMd 字段中:
// claudemd.ts:980-998
if (isAutoMemoryEnabled()) {
files.push({ path: getAutoMemEntrypoint(),
type: 'AutoMem', ... })
}
if (feature('TEAMMEM') && teamMemPaths!.isTeamMemoryEnabled()) {
files.push({ path: teamMemPaths!.getTeamMemEntrypoint(),
type: 'TeamMem', ... })
}
格式化为文本时,AutoMem 会标注 "(user's auto-memory, persists across conversations)",TeamMem 会标注 "(shared team memory, synced across the organization)"。
tengu_moth_copse 启用时的行为变化
当 GrowthBook flag tengu_moth_copse 启用时,filterInjectedMemoryFiles(claudemd.ts:1142-1151)会过滤掉 AutoMem 和 TeamMem 类型:
MEMORY.md不再注入claudeMd- 改由 relevant_memories 附件系统按需注入(详见第 7 节)
- 同时
buildMemoryLines的skipIndex=true,跳过 Step 2 索引维护
07召回 · 三种触发机制
记忆召回有三种独立机制,分别服务于不同的场景。它们互为补充,不会冗余。
索引全量加载(被动)
路径:MEMORY.md → claudemd.ts → userContext.claudeMd → messages[0]
特点:始终加载,每轮重注入
相关性检索(主动 / 异步预取)
路径:startRelevantMemoryPrefetch → findRelevantMemories → Sonnet 筛选 → relevant_memories 附件
特点:每轮触发一次预取,最多 5 个文件
嵌套记忆(伴随 FileRead)
路径:FileReadTool 触发 → nestedMemoryAttachmentTriggers → getNestedMemoryAttachments
特点:读到某文件时自动加载该目录的 CLAUDE.md
7.1 机制一:索引全量加载
扫描所有 CLAUDE.md"] B --> C{"AutoMem / TeamMem
类型?"} C -->|是| D["读取 MEMORY.md 内容"] D --> E["truncateEntrypointContent
双重截断保护"] E --> F["filterInjectedMemoryFiles
tengu_moth_copse 时过滤"] F --> G["getClaudeMds 格式化"] G --> H["注入 userContext.claudeMd"] H --> I["prependUserContext
包裹 <system-reminder>"] I --> J["messages[0]"] style C fill:#FFF6F0,stroke:#D77757 style F fill:#FFF7E6,stroke:#C8862C style J fill:#E8F5EE,stroke:#2D8659
图 7.1 — 索引全量加载链路
7.2 机制二:相关性检索(异步预取)
7.2.1 触发与门控
定义在 src/utils/attachments.ts:2380-2440 的 startRelevantMemoryPrefetch,每轮查询循环开始时通过 using 绑定,作为 Disposable 异步运行。
| 检查项 | 不通过的影响 |
|---|---|
isAutoMemoryEnabled() | 返回 undefined,不触发 |
tengu_moth_copse GrowthBook flag | 该机制是 moth_copse 实验组的核心 |
!isPoorModeActive() | 穷鬼模式跳过,节省 token |
| 找到最后一条非 isMeta 的 user 消息 | 没有用户消息则不触发 |
| 消息文本包含空格(多词) | 单词提示缺乏足够上下文,不触发 |
surfaced.totalBytes < MAX_SESSION_BYTES | 会话累计 60KB 后停止预取 |
7.2.2 检索流程详解
已展示路径 + 已用字节 Pre->>Scan: scanMemoryFiles(memoryDir) Scan->>Scan: readdir(recursive) 排除 MEMORY.md Scan->>Scan: 每文件读 30 行 frontmatter Scan->>Scan: 按 mtime 最新优先排序,最多 200 Scan-->>Pre: MemoryHeader[] Pre->>Pre: 过滤已展示路径 Pre->>Sonnet: sideQuery + JSON schema
{selected_memories: string[]} Sonnet-->>Pre: 最多 5 个文件名 Pre->>Read: readMemoriesForSurfacing(selected) Read->>Read: readFileInRange
200 行 + 4KB 双限
truncateOnByteLimit Read-->>Pre: [{path, content, mtimeMs, header}] Pre-->>Q: relevant_memories Attachment Q->>Msg: 注入 messages[N]
<system-reminder> 包裹
图 7.2 — 相关性检索的完整链路
7.2.3 Sonnet 选择器(核心智能筛选)
不是简单关键词匹配,而是用 Sonnet 做语义筛选(findRelevantMemories.ts):
// findRelevantMemories.ts:19-25 SELECT_MEMORIES_SYSTEM_PROMPT
You are selecting memories that will be useful to Claude Code as
it processes a user's query. You will be given the user's query
and a list of available memory files with their filenames and
descriptions.
Return a list of filenames for the memories that will clearly be
useful to Claude Code as it processes the user's query (up to 5).
Only include memories that you are certain will be helpful based
on their name and description.
- If you are unsure if a memory will be useful in processing the
user's query, then do not include it in your list. Be selective
and discerning.
- If there are no memories in the list that would clearly be useful,
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 (Claude Code is already exercising them). DO still select
memories containing warnings, gotchas, or known issues about
those tools — active use is exactly when those matter.
// sideQuery 调用:独立 API 调用,不消耗主对话 token 预算
const result = await sideQuery({
model: getDefaultSonnetModel(),
system: SELECT_MEMORIES_SYSTEM_PROMPT,
skipSystemPromptPrefix: true, // 跳过主提示词
messages: [{ role: 'user', content:
`Query: ${query}\n\nAvailable memories:\n${manifest}${toolsSection}` }],
max_tokens: 256,
output_format: { // 结构化输出
type: 'json_schema',
schema: { type: 'object',
properties: { selected_memories: { type: 'array',
items: { type: 'string' } } },
required: ['selected_memories'] } },
signal,
querySource: 'memdir_relevance',
optional: true,
})
7.2.4 双重去重(alreadySurfaced + readFileState)
为什么是双重
alreadySurfaced 在调用 Sonnet 之前过滤,让 Sonnet 的 5 个名额优先用于新候选;
readFileState 在调用 Sonnet 之后过滤(同时再次过滤 alreadySurfaced),是 belt-and-suspenders 守卫,防止多目录返回结果重复引入已展示路径。
compact 后旧附件消失,collectSurfacedMemories 自然重置 → 允许重新召回。用消息扫描而非 toolUseContext 字段正是为了利用这一性质(attachments.ts:2264-2285)。
7.2.5 文件内容截断(双限)
// attachments.ts:269-289
const MAX_MEMORY_LINES = 200
// Line cap alone doesn't bound size (200 × 500-char lines = 100KB).
// 5 files × 4KB = 20KB/turn 才是真正的硬上限
const MAX_MEMORY_BYTES = 4096
export const RELEVANT_MEMORIES_CONFIG = {
// 会话累计 60KB(约 3 次完整注入),
// 之后停止预取 — 最相关的记忆已经在上下文中了。
// 扫描消息(而非 toolUseContext)能让 compact 自然重置计数。
MAX_SESSION_BYTES: 60 * 1024,
}
截断时附加提示,模型可决定是否用 FileReadTool 拉取完整文件:
// attachments.ts:2323-2326
const content = truncated
? result.content +
`\n\n> This memory file was truncated (${reason}).
Use the FileReadTool to view the complete file at: ${filePath}`
: result.content
7.2.6 新鲜度提示(memoryHeader)
每条记忆都附带头信息,超过 1 天的记忆会带"过时警告":
// attachments.ts:2346-2351 + memoryAge.ts:32-41
function memoryHeader(path: string, mtimeMs: number) {
const staleness = memoryFreshnessText(mtimeMs)
return staleness
? `${staleness}\n\nMemory: ${path}:`
: `Memory (saved ${memoryAge(mtimeMs)}): ${path}:`
}
// memoryFreshnessText 对超 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."
7.3 机制三:嵌套记忆(Nested Memory)
7.3.1 触发机制
nestedMemoryAttachmentTriggers"] B --> C["getNestedMemoryAttachments"] C --> D{"trigger set 为空?"} D -->|是| E["立即返回 []"] D -->|否| F["遍历每个 trigger 路径"] F --> G["查找该目录的
CLAUDE.md / .claude/rules/*.md"] G --> H{"loadedNestedMemoryPaths
已加载?"} H -->|是| I["跳过(去重)"] H -->|否| J["读取并加入 nested_memory
attachments"] J --> K["加入 loadedNestedMemoryPaths"] K --> L["clear() trigger set"] style D fill:#FFF6F0,stroke:#D77757 style H fill:#EAF1FF,stroke:#5769F7
图 7.3 — 嵌套记忆触发机制
7.3.2 关键字段
| 字段 | 类型 | 用途 |
|---|---|---|
nestedMemoryAttachmentTriggers | Set<string> | 记录需要检查嵌套记忆的路径,每轮处理后 clear() |
loadedNestedMemoryPaths | Set<string> | 记录已加载过的 CLAUDE.md 路径,避免重复注入 |
08API 请求 · 三条注入通道详解
记忆系统的三层架构在 API 请求中体现为三条独立的注入通道, 各有不同的位置、内容、缓存策略和 compact 行为。
8.1 三通道全景对比
| 通道 | 位置 | 内容 | 缓存 | compact 影响 |
|---|---|---|---|---|
1. system 段落 9systemPromptSection('memory') |
system 参数 Block 4 |
记忆使用说明(类型定义、保存方法、召回指导) | 缓存(cacheBreak: false) |
❌ 不丢失(system 不受 compact 影响) |
2. userContext.claudeMdgetMemoryFiles() |
messages[0] |
MEMORY.md 索引全文 |
无(用户上下文动态消息) | ❌ 不丢失(每轮重新注入) |
3. relevant_memories 附件startRelevantMemoryPrefetch |
messages[N] |
具体记忆文件内容(最多 5 个) | 无(动态附件) | ⚠️ 可能丢失,但可重新召回 |
8.2 通道一:systemPromptSection('memory') 的缓存机制
// constants/prompts.ts:770
const dynamicSections = [
systemPromptSection('session_guidance', () => ...),
systemPromptSection('memory', () => loadMemoryPrompt()), // ← 这里
...
]
// systemPromptSections.ts
export function systemPromptSection(name, compute) {
return { name, compute, cacheBreak: false } // ← 关键
}
缓存语义
- 首次调用执行
loadMemoryPrompt(),结果缓存到systemPromptSectionCache - 后续轮次直接读取缓存,不再重算
/clear或/compact时通过clearSystemPromptSections()清除
含义:用户在对话中手动编辑了记忆文件,系统提示词中的"记忆使用说明"不会立即更新(但 MEMORY.md 索引会在下一轮通过 claudemd.ts 重新加载,因为不走 systemPromptSection)。
8.3 通道二:MEMORY.md 索引的双路径
+ team enabled?"} E -->|是| F["push TeamMem MemoryFileInfo"] E -->|否| G[skip] F --> H["filterInjectedMemoryFiles"] G --> H H --> I{"tengu_moth_copse?"} I -->|是 skipIndex| J["过滤掉 AutoMem + TeamMem"] I -->|否| K["全部保留"] J --> L["不再走索引通道
改由 relevant_memories 承担"] K --> M["getClaudeMds 格式化"] M --> N["userContext.claudeMd"] N --> O["prependUserContext"] O --> P["messages[0] <system-reminder>"] style I fill:#FFF7E6,stroke:#C8862C style L fill:#FFE5E0,stroke:#C23B22 style P fill:#E8F5EE,stroke:#2D8659
图 8.1 — 索引通道有"传统模式"和"moth_copse 模式"两条分支
8.4 通道三:relevant_memories 附件的注入位置
每轮查询循环开始时通过 using 启动预取,在 collect 点(post-tools)消费:
// query.ts 中的简化伪代码
while (true) {
using prefetch = startRelevantMemoryPrefetch(messages, toolUseContext)
// prefetch 在主模型 streaming + 工具执行期间异步运行
// ... 模型采样 + 工具执行 ...
// collect 点:consume-if-ready or skip-and-retry-next-iteration
const attachments = prefetch?.settledAt
? await prefetch.promise
: []
// prefetch never blocks the turn
// [Symbol.dispose] 在 using 作用域退出时触发
// — abort 飞行中的请求 + 发出终态遥测
}
8.5 QueryEngine 中的第二处 loadMemoryPrompt 调用
// QueryEngine.ts:325-332
const memoryMechanicsPrompt =
customPrompt !== undefined && hasAutoMemPathOverride()
? await loadMemoryPrompt()
: null
// ...
const systemPromptParts = [
customPrompt,
...(memoryMechanicsPrompt ? [memoryMechanicsPrompt] : []),
...
]
为什么需要这处额外调用
当 SDK 调用方提供了 customPrompt(自定义系统提示词)时,默认的 memory 段落不会被加入。但如果 SDK 调用方显式设置了 CLAUDE_COWORK_MEMORY_PATH_OVERRIDE,说明他们仍然需要记忆功能。
因此 QueryEngine 在这种"customPrompt + override"组合下,额外注入 memoryMechanicsPrompt,确保模型仍然知道如何读写记忆。
8.6 compact 对记忆的影响
系统提示词中的指导
system 参数中的 memory 段落 → 每轮都完整存在;messages[0] 中的 MEMORY.md 索引 → 每轮重新注入;systemContext 中的 git 状态 → 每轮重新注入。
relevant_memories 附件
compact 移除旧消息,附件随之消失。
但 collectSurfacedMemories 通过扫描消息统计已展示路径,compact 后自然重置 → 模型可重新召回相同记忆,不会永久失忆。
三层架构的鲁棒性
即使 compact 发生:
- 系统提示词中的行为指导始终完整 → 模型知道如何使用记忆系统
- MEMORY.md 索引始终可见 → 模型知道有哪些记忆可用
- 具体记忆文件可重新召回 → compact 后不会永久丢失信息
09启用开关 · 五道判定链
记忆系统的不同子能力由多层 feature flag 控制 —— 总开关、build feature、GrowthBook 实验、本地设置共同决定运行时行为。
9.1 主判定链 isAutoMemoryEnabled()
AUTO_MEMORY 设置?"} B -->|truthy| X1["❌ 关闭"] B -->|falsy| X2["✅ 开启"] B -->|未设置| C{"CLAUDE_CODE_SIMPLE
(--bare)?"} C -->|是| X1 C -->|否| D{"CCR + 无
REMOTE_MEMORY_DIR?"} D -->|是| X1 D -->|否| E{"settings.json
autoMemoryEnabled 设置?"} E -->|true| X2 E -->|false| X1 E -->|未设置| X2 style X1 fill:#FFE5E0,stroke:#C23B22 style X2 fill:#E8F5EE,stroke:#2D8659
图 9.1 — auto-memory 总开关判定链(paths.ts:30-54)
9.2 各 feature flag 对记忆的影响
| Flag | 层级 | 影响范围 |
|---|---|---|
EXTRACT_MEMORIES |
build feature | 启用后台提取代理(无此 feature 时 extractor 函数从未注册) |
TEAMMEM |
build feature | 启用 team memory(双目录模式,team 子目录 + 合并提示词) |
KAIROS |
build feature | 启用日志模式(按日期追加,非实时索引)— 优先级高于 TEAMMEM |
MEMORY_SHAPE_TELEMETRY |
build feature | 启用记忆召回形态遥测(命中率、年龄分布等) |
tengu_moth_copse |
GrowthBook | 启用相关性检索 + 跳过 MEMORY.md 索引注入(skipIndex=true) |
tengu_coral_fern |
GrowthBook | 启用 "Searching past context" 段落(教模型如何搜索历史会话) |
tengu_passport_quail |
GrowthBook | 提取代理的运行门控(isExtractModeActive) |
tengu_slate_thimble |
GrowthBook | 非交互会话也允许提取(默认仅交互会话) |
tengu_bramble_lintel |
GrowthBook | 提取代理的节流间隔(默认 1 = 每回合) |
tengu_paper_halyard |
GrowthBook | 跳过 Project / Local 类型的 CLAUDE.md(不影响 AutoMem/TeamMem) |
tengu_herring_clock |
GrowthBook | 记录 team-memory disabled 事件(仅遥测,不影响功能) |
9.3 加载分发逻辑(loadMemoryPrompt())
tengu_memdir_disabled"] B -->|是| C["读 tengu_moth_copse → skipIndex"] C --> D{"feature(KAIROS)
+ getKairosActive?"} D -->|是| E["buildAssistantDailyLogPrompt"] D -->|否| F{"feature(TEAMMEM)
+ isTeamMemoryEnabled?"} F -->|是| G["ensureMemoryDirExists(teamDir)
buildCombinedMemoryPrompt"] F -->|否| H["ensureMemoryDirExists(autoDir)
buildMemoryLines
('auto memory', autoDir, ...)"] style Z1 fill:#FFE5E0,stroke:#C23B22 style E fill:#F3EAFF,stroke:#6B4A8E style G fill:#FFF7E6,stroke:#C8862C style H fill:#E8F5EE,stroke:#2D8659
图 9.2 — loadMemoryPrompt() 的分发逻辑(memdir.ts:414-497)
9.4 穷鬼模式(Poor Mode)的特殊处理
isPoorModeActive() 跳过的内容
- extractMemories:跳过后台提取代理
- startRelevantMemoryPrefetch:跳过相关性检索(节省 sideQuery token)
- prompt_suggestion:跳过 prompt 建议
- verification_agent:跳过验证代理
但不会跳过:MEMORY.md 索引注入、主代理写入记忆能力。即穷鬼模式下记忆系统从"自动 + 召回"降级为"被动加载 + 手动引用"。
10全链路 · 写入 → 召回完整数据流
把前面的分散概念串起来,看一个完整的端到端故事: 用户说一句话 → 记忆被写入 → 跨会话被召回。
10.1 写入链路(一次完整对话)
图 10.1 — 写入链路:主代理直接写或后台提取代理补漏
10.2 召回链路(新对话开始)
(tengu_moth_copse 时过滤) CMD-->>Q: AutoMem MemoryFileInfo[] Q->>Q: 注入 userContext.claudeMd
messages[0] and 通道 3:relevant_memories(异步) Q->>Pre: startRelevantMemoryPrefetch Pre->>Disk: scanMemoryFiles Pre->>Sonnet: sideQuery + JSON schema Sonnet-->>Pre: 5 个最相关文件名 Pre->>Disk: readMemoriesForSurfacing Pre-->>Q: relevant_memories Attachment
messages[N] end Q->>API: 流式 API 调用
(system + messages) API-->>Q: 模型响应(看到三层记忆 → 用 bun 不用 npm) Q-->>U: "运行 bun install..."
图 10.2 — 召回链路:三通道并行注入
10.3 完整数据流总览
"用 bun 不要用 npm"
FileEditTool → MEMORY.md
turn 1: Read · turn 2: Write
~/.claude/projects/<sanitized>/memory/
feedback_package_manager.md + MEMORY.md
缓存型,每会话一次
每轮注入 messages[0]
注入 messages[N]
11关键细节 · 你可能不知道但应该知道的
以下这些细节散落在源码各处,如果不通读相关文件很难发现。它们解释了系统的"为什么这样设计"。
"记忆行为指导"和"记忆实际内容"是分离的
loadMemoryPrompt() 返回的是使用说明文本(类型定义、保存方法、召回规则),不包含 MEMORY.md 的内容。
MEMORY.md 的实际内容通过 claudemd.ts 的 getMemoryFiles() → getClaudeMds() 注入。两者通过不同的通道传递给模型,不同的缓存策略。
systemPromptSection 是缓存型段落
// systemPromptSections.ts
export function systemPromptSection(name, compute) {
return { name, compute, cacheBreak: false }
}
首次调用执行 compute(),结果缓存。/clear / /compact 时清除。含义:用户在对话中手动编辑了记忆文件,系统提示词中的"记忆使用说明"不会立即更新。
相关性检索使用 Sonnet(不是关键词匹配)
用户查询 + 记忆清单 → Sonnet 做语义筛选,输出最多 5 个文件名。使用 sideQuery(独立 API 调用,不消耗主对话 token),结构化输出(JSON schema)保证可解析。
关键巧思:排除"正在使用工具的用法文档类记忆"—— 如果当前对话已经在用某个工具,再召回它的用法文档通常是噪音。
主代理和提取代理的互斥
if (hasMemoryWritesSince(messages, lastMemoryMessageUuid)) {
const lastMessage = messages.at(-1)
if (lastMessage?.uuid)
lastMemoryMessageUuid = lastMessage.uuid
return
}
当主代理在本轮已经写入了记忆文件时,提取代理会跳过本轮并推进游标。这避免了:
- 主代理和提取代理写入重复记忆
- 提取代理覆盖主代理的写入
- 浪费 token 做重复工作
记忆的新鲜度提示
超过 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.
这是对"过时代码状态记忆被当作事实复述"问题的直接应对。
MEMORY.md 索引的双重截断
- 先按行截断(更自然的语义边界):超过 200 行则截断
- 再按字节截断:超过 25,000 字节则在上一个换行处切断
- 追加 WARNING:告知模型索引被截断,要求保持简洁
解决两种失效模式:行数过多(正常索引膨胀)+ 单行过长(曾观测到 200 行内约 197KB)。
team memory 的目录关系
getTeamMemPath() = join(getAutoMemPath(), 'team')
team memory 目录是 auto memory 目录的子目录。因此 ensureMemoryDirExists(teamDir) 会连带创建 auto memory 目录(递归 mkdir)。如果未来 team 目录不再位于 auto 目录下,需要在此额外确保 auto 目录的存在。
记忆文件扫描的边界
- 最多扫描 200 个文件(
MAX_MEMORY_FILES) - 每个文件只读取前 30 行 frontmatter(
FRONTMATTER_MAX_LINES) - 排除
MEMORY.md本身 - 按 mtime 最新优先排序
- 使用
Promise.allSettled容错(单个文件读取失败不影响整体) - "读再排"而非"先 stat 再读":
readFileInRange内部已包含 stat 并返回 mtimeMs,对常见规模(N ≤ 200)可显著减少系统调用
Cowork 场景的记忆隔离
Cowork 通过 CLAUDE_COWORK_MEMORY_PATH_OVERRIDE 将记忆重定向到按空间隔离的挂载点:
- 避免按会话变化的 cwd(含 VM 进程名)导致每次产生不同 project-key
hasAutoMemPathOverride()作为"SDK 调用方已显式选择 auto-memory 机制"的信号- 当自定义系统提示词覆盖默认提示词时,仍可通过此信号注入 memoryMechanicsPrompt
提取代理的"两轮策略"
提示词显式指导:
turn 1 — issue all FileReadTool calls in parallel
for every file you might update
turn 2 — issue all FileWriteTool/FileEditTool calls
in parallel
Do not interleave reads and writes across multiple turns.
原因:FileEditTool 要求同一文件先 Read 再 Edit。串行交替会浪费回合数。两轮并行策略让 5 回合预算(maxTurns: 5)足够覆盖典型场景。
双重去重:alreadySurfaced + readFileState
alreadySurfaced在调用 Sonnet 之前过滤 → 让 Sonnet 5 个名额优先用于新候选readFileState在调用 Sonnet 之后过滤(同时再次过滤 alreadySurfaced)→ belt-and-suspenders 守卫- compact 后旧附件消失,
collectSurfacedMemories自然重置 → 允许重新召回 - 关键巧思:用消息扫描而非 toolUseContext 字段,正是为了让 compact 自动重置
会话级累计字节限制
const RELEVANT_MEMORIES_CONFIG = {
MAX_SESSION_BYTES: 60 * 1024, // ~3 次完整注入
}
每轮 cap:5 文件 × 4KB = 20KB;会话 cap:60KB(约 3 次)。一旦累计达到上限,后续轮次停止预取 —— "最相关的记忆已经在上下文中了,再注入边际效益递减"。
compact 后扫描消息发现累计字节归零,预取自然恢复。
提示词标题的措辞影响显著
来自 memory-prompt-iteration.eval.ts 评测的发现:
- "Before recommending from memory"(决策点动作提示)→ 评测 3/3 通过
- 抽象标题 "Trusting what you recall" → 评测 0/3
主体文案完全相同,仅标题不同。说明在系统提示词工程中,"动作触发型标题" 远优于 "抽象描述型标题"。
KAIROS 模式用"路径模式"而非"具体日期路径"
const logPathPattern = join(memoryDir,
'logs', 'YYYY', 'MM', 'YYYY-MM-DD.md')
该提示词被 systemPromptSection('memory') 缓存,不会在日期变化时自动失效。模型应从 date_change 附件(午夜滚动时追加)获取当前日期,而不是依赖 user-context 消息(后者故意保持滞后以保住跨午夜的提示词缓存前缀)。
settings.autoMemoryDirectory 排除 projectSettings
配置来源仅信任 policy / flag / local / user,排除 projectSettings(仓库内可提交的 .claude/settings.json):
否则恶意仓库可将 autoMemoryDirectory 指向 ~/.ssh 等敏感目录,并借由 filesystem.ts 的写入豁口(触发条件:isAutoMemPath() 命中且 !hasAutoMemPathOverride())获得静默写权限。
12源码文件索引
读源码时按这个顺序展开,最容易建立心智模型。
核心模块(按依赖顺序)
| 文件 | 行数 | 职责 |
|---|---|---|
src/memdir/paths.ts |
268 | 路径解析、启用判定、安全校验、Cowork 覆盖 |
src/memdir/memoryTypes.ts |
268 | 四类型定义、TYPES_SECTION_INDIVIDUAL/COMBINED、WHAT_NOT_TO_SAVE、WHEN_TO_ACCESS、TRUSTING_RECALL |
src/memdir/memoryScan.ts |
95 | 扫描 memory 目录、解析 frontmatter、formatMemoryManifest |
src/memdir/memoryAge.ts |
52 | 新鲜度计算、memoryFreshnessText / memoryFreshnessNote |
src/memdir/findRelevantMemories.ts |
143 | 相关性检索(Sonnet 筛选 + sideQuery + JSON schema) |
src/memdir/memdir.ts |
497 | buildMemoryLines / loadMemoryPrompt / truncateEntrypointContent / KAIROS daily-log prompt |
src/memdir/teamMemPaths.ts |
281 | team memory 路径解析(auto 子目录 + 启用判定) |
src/memdir/teamMemPrompts.ts |
100 | team memory 合并提示词(buildCombinedMemoryPrompt) |
src/memdir/memoryShapeTelemetry.ts |
7 | 召回形态遥测(命中率 / 年龄分布)— stub 占位 |
src/services/extractMemories/extractMemories.ts |
615 | 后台提取代理:closure-scoped 状态、互斥、节流、合并、分叉 |
src/services/extractMemories/prompts.ts |
154 | 提取代理提示词(auto-only / combined 两种) |
src/constants/prompts.ts |
~800 | systemPromptSection('memory') 注册 + simple-proactive 路径 |
src/constants/systemPromptSections.ts |
~150 | 段落缓存(systemPromptSectionCache)与解析框架 |
src/utils/claudemd.ts |
~1500 | CLAUDE.md + MEMORY.md 加载管道、AutoMem/TeamMem 注入、filterInjectedMemoryFiles |
src/utils/attachments.ts |
~2700 | relevant_memories 预取 + nested_memory 注入 + collectSurfacedMemories + readMemoriesForSurfacing |
src/QueryEngine.ts |
~600 | memoryMechanicsPrompt(customPrompt + override 时注入)+ nestedMemoryAttachmentTriggers 初始化 |
12.1 阅读建议
- 从
memoryTypes.ts开始 —— 理解四类型分类法和提示词文案 - 读
paths.ts—— 理解目录结构、路径优先级、安全约束 - 读
memdir.ts的buildMemoryLines和loadMemoryPrompt—— 理解使用说明如何构建 - 读
memoryScan.ts+findRelevantMemories.ts—— 理解相关性检索机制 - 读
attachments.ts的startRelevantMemoryPrefetch段落 —— 理解三通道中的"通道 3" - 读
extractMemories.ts—— 理解后台提取代理的完整生命周期 - 最后读
claudemd.ts—— 理解 MEMORY.md 索引如何作为 AutoMem 类型注入用户上下文
至此你已掌握
- 记忆系统的四种类型 + 何时保存 + 何时不应保存
- 双路径写入 + 互斥守卫 + 后台代理的完美分叉
- 三通道注入 + 缓存策略 + compact 行为
- 路径解析的优先级链路 + 六道安全约束
- 双层索引/详情架构 + 双重截断保护
- Sonnet 相关性筛选 + 双重去重 + 会话级累计限制
- KAIROS 日志模式 + 提示词缓存的微妙平衡
- 15 个分散在源码中的关键设计细节