auto-memory:基于本地文件系统的自增长项目知识库
对应官方文档:claude-code-docs/docs/memory.md 里的 Auto memory。
它解决了什么问题
auto memory 并不是一个“聊天摘要生成器”的黑盒,也不是把整段旧会话塞回 Prompt 的简单缓存。
在源码层面,它是一个落在本地文件系统、按 Canonical Git Root 归档的“持久记忆层”。 Claude 会在会话运行中将值得跨会话保留的发现、决策、环境特征等信息写入特定的 Markdown 文件中,并在后续启动时自动“召回(Recall)”这些内容。
它与 CLAUDE.md 的区别在于:CLAUDE.md 是显式指令(告诉我该怎么做),而 auto memory 是隐式积累(告诉我上次发生了什么)。
代码里的真实逻辑
实现这套机制的核心在于 src/memdir/ 目录下的几个关键逻辑:
- 分层启停与路径解析:
src/memdir/paths.ts里的isAutoMemoryEnabled()将环境变量、--bare模式、以及settings.json的autoMemoryEnabled串成了一条决策链。最重要的一点是目录定位:它默认定位在~/.claude/projects/<sanitized-git-root>/memory/。由于使用了 Git Root,同一个仓库的不同 worktree 或子目录会自然地共享同一份记忆。 - 记忆协议注入(Memory Protocol):Claude 之所以会主动写记忆,不是因为后台有秘密线程,而是因为
loadMemoryPrompt()在 System Prompt 里注入了一套“行为协议”。它告诉 Claude 什么是值得记的(跨会话有用的事实),什么是不该记的(琐碎的单轮细节),以及如何操作MEMORY.md索引。 - 索引截断与入口加载:在
src/utils/claudemd.ts中,MEMORY.md作为AutoMem被自动加载。为了保护上下文空间,源码强制执行truncateEntrypointContent(...),只保留前 200 行或 25KB 以内的内容。这意味着MEMORY.md必须是一个精简的“地图”,详细内容应由模型按需读取子文件。 - 后台提取器(Post-turn Extractor):
src/services/extractMemories/extractMemories.ts运行在一个 forked subagent 中。当一轮对话结束,且主代理没有直接写 memory 时,这个提取器会异步分析对话,补全缺失的持久记忆。 - 相关性召回(Semantic Recall):
src/memdir/findRelevantMemories.ts实现了除索引外的“按需召回”。它会扫描 memory 目录下的其他 Topic Files(.md),提取 Frontmatter 里的描述,再根据当前 Query 挑选出最多 5 个最相关的记忆,以Attachment形式带回会话。 - 结构化内存目录(memdir Structure):在
src/memdir/memdir.ts中,记忆不再只是一个扁平的MEMORY.md文件,而是一个结构化的目录。MEMORY.md作为索引被严格截断(200 行/25KB),强迫模型通过Read工具按需加载详细的话题文件(Topic Files),有效平衡了上下文消耗与信息召回量。 - 团队记忆共享(Team Memory Sharing):核心逻辑位于
src/services/teamMemorySync/。当开启TEAMMEM特性时,系统会创建memory/team/目录并通过watcher.ts同步成员间的关键决策记录。为了安全,teamMemSecretGuard.ts在同步前会自动拦截泄露的 API Key 等敏感信息。
实战注意事项
- 本地/项目限制:记忆是本地的、按 Git 仓库隔离的。它不是全局云同步的,也不会随账号在不同机器间漂移。
- 团队同步前提:Team Memory 需要显式开启且绑定特定组织。若
autoMemoryEnabled被禁用,团队同步也会失效。 - 索引非全文:自动注入上下文的只有
MEMORY.md索引且有硬截断。不要在索引文件里写长篇大论,那是 Topic Files 该做的事。 - 安全围栏:项目级的
.claude/settings.json不能重定向autoMemoryDirectory。 - KAIROS 模式优先:在 Assistant 模式下,每日日志(Daily Log)模式优先,这种模式目前与 Team Sync 的共享写模式不兼容。
接下来看什么
- 如果你关心
CLAUDE.md与 Memory 的加载顺序,下一篇该看 Instruction Loading Chain。 - 如果你想了解每日日志如何被蒸馏进长期记忆,看 梦境过程:Daily Log 到 Topic Files 的演化。
- 如果你更关心如何手动管理记忆,去看
/memory命令实现(memory.tsx)。
源码锚点
claude-code-opensource/src/memdir/paths.ts:记忆库的路径定位、共享逻辑与安全隔离策略。
📄 src/memdir/paths.ts — 记忆库的路径定位、共享逻辑与安全隔离策略。
typescript
export function isAutoMemoryEnabled(): boolean {
const envVal = process.env.CLAUDE_CODE_DISABLE_AUTO_MEMORY
if (isEnvTruthy(envVal)) {
return false
}
if (isEnvDefinedFalsy(envVal)) {
return true
}
// --bare / SIMPLE: prompts.ts already drops the memory section from the
// system prompt via its SIMPLE early-return; this gate stops the other half
// (extractMemories turn-end fork, autoDream, /remember, /dream, team sync).
if (isEnvTruthy(process.env.CLAUDE_CODE_SIMPLE)) {
return false
}
if (
isEnvTruthy(process.env.CLAUDE_CODE_REMOTE) &&
!process.env.CLAUDE_CODE_REMOTE_MEMORY_DIR
) {
return false
}
const settings = getInitialSettings()
if (settings.autoMemoryEnabled !== undefined) {
return settings.autoMemoryEnabled
}
return true
}claude-code-opensource/src/memdir/memdir.ts:结构化 memdir 的管理、Index 与 Topic Files 的分离逻辑。
📄 src/memdir/memdir.ts — 结构化 memdir 的管理、Index 与 Topic Files 的分离逻辑。
typescript
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`claude-code-opensource/src/services/teamMemorySync/watcher.ts:跨设备/跨团队记忆同步的监听与下发机制。
📄 src/services/teamMemorySync/watcher.ts — 跨设备/跨团队记忆同步的监听与下发机制。
typescript
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
logEvent,
} from '../analytics/index.js'
import {claude-code-opensource/src/services/extractMemories/extractMemories.ts:后台异步提取器的触发与逻辑。
📄 src/services/extractMemories/extractMemories.ts — 后台异步提取器的触发与逻辑。
typescript
const teamMemPaths = feature('TEAMMEM')
? (require('../../memdir/teamMemPaths.js') as typeof import('../../memdir/teamMemPaths.js'))
: null
/* eslint-enable @typescript-eslint/no-require-imports */
// ============================================================================
// Helpers
// ============================================================================
/**
* Returns true if a message is visible to the model (sent in API calls).
* Excludes progress, system, and attachment messages.
*/
function isModelVisibleMessage(message: Message): boolean {
return message.type === 'user' || message.type === 'assistant'
}claude-code-opensource/src/memdir/findRelevantMemories.ts:基于 Frontmatter 的 Topic File 相关性召回算法。
📄 src/memdir/findRelevantMemories.ts — 基于 Frontmatter 的 Topic File 相关性召回算法。
typescript
scanMemoryFiles,
} from './memoryScan.js'
export type RelevantMemory = {claude-code-opensource/src/utils/claudemd.ts:MEMORY.md作为核心 Context 附件的装载链。
📄 src/utils/claudemd.ts — `MEMORY.md` 作为核心 Context 附件的装载链。
typescript
// stripped frontmatter, truncated MEMORY.md) such that it no longer matches
// the bytes on disk. When set, `rawContent` holds the unmodified disk bytes
// so callers can cache a `isPartialView` readFileState entry — presence in
// cache provides dedup + change detection, but Edit/Write still require an
// explicit Read before proceeding.
contentDiffersFromDisk?: boolean
rawContent?: string
}
function pathInOriginalCwd(path: string): boolean {