🧠

记忆系统

Claude Code 的持久化记忆机制 —— memdir 目录扫描、AI 驱动的记忆检索、年龄衰减、自动做梦整合

核心架构

记忆系统概述

持久化文件记忆

Claude Code 采用基于文件的持久化记忆系统,让 AI 在跨会话中保持连贯性。记忆存储在 ~/.claude/projects/<sanitized-git-root>/memory/ 目录下,每个记忆是一个独立的 Markdown 文件,通过 MEMORY.md 索引文件进行快速导航。

记忆系统包含两个核心能力:显式记忆(用户通过 /remember 命令或对话中主动要求保存)和 隐式记忆(后台 extract-memories agent 自动提取重要信息)。 更高级的 自动做梦(autoDream)机制在积累足够会话后, 自动整合、去重、修剪记忆文件。

记忆系统架构

对话结束写入索引写入记忆记忆清单注入上下文扫描获取锁整合修剪更新索引同步用户对话extractMemories 后台 AgentMEMORY.md 索引文件Topic Files 记忆文件scanMemoryFiles 目录扫描Sonnet 选择器 findRelevantMemoriesautoDream 记忆整合合并锁 consolidationLockTeam Memory 团队共享记忆

memdir — 记忆目录路径

paths.ts 路径解析与安全验证

记忆目录路径解析遵循严格的优先级链,同时内置多层安全验证防止路径遍历攻击:

路径解析优先级

  1. CLAUDE_COWORK_MEMORY_PATH_OVERRIDE — 环境变量覆盖(Cowork SDK)
  2. settings.json autoMemoryDirectory — 仅限 policy/local/user 信任源
  3. 默认路径:~/.claude/projects/<sanitized-git-root>/memory/

安全防护机制

  • 拒绝相对路径、根路径、UNC 路径
  • 拒绝 null 字节和 Windows 驱动器根
  • projectSettings 不参与路径覆盖(防恶意仓库)
  • 路径经过 normalize() + NFC 规范化

KAIROS 助手模式布局

在 KAIROS 模式下,记忆目录使用按日期归档的日志布局:

memory/
├── MEMORY.md              # 主索引(≤25行,≤25KB)
├── topic-1.md             # 按主题组织的记忆文件
├── topic-2.md
├── logs/
│   └── 2026/
│       └── 04/
│           ├── 2026-04-01.md   # 每日追加日志
│           ├── 2026-04-02.md
│           └── 2026-04-03.md
└── team/                   # 团队共享记忆
    ├── MEMORY.md
    └── *.md

记忆类型分类

四类型封闭分类法 — memoryTypes.ts

记忆系统采用严格的四类型分类,每种类型都明确界定应该存什么 不该存什么。核心原则:只存储无法从当前代码库状态推导的信息。

export const MEMORY_TYPES = [
  'user',      // 用户画像:角色、偏好、知识水平
  'feedback',  // 反馈指导:纠正与确认的行为准则
  'project',   // 项目上下文:目标、里程碑、事件
  'reference', // 外部资源:Linear、Grafana 等系统指针
] as const
userscope: always private

用户角色、目标、偏好、知识水平

📌 了解用户角色、偏好、知识时保存

例:用户是资深 Go 工程师,首次接触 React

feedbackscope: default private / team

纠正与确认的行为准则,含 Why 和 How to apply

📌 用户纠正或确认方案时保存(含非显而易见的确认)

例:集成测试禁止 mock 数据库,因上次 mock 通过但生产迁移失败

projectscope: private / team

项目目标、里程碑、事件(代码/git 不可推导的部分)

📌 了解谁做什么、为什么、什么时候时保存

例:3月5日起合并冻结,mobile 团队切 release 分支

referencescope: usually team

外部系统指针(Linear、Grafana、Slack 等)

📌 了解外部资源及其用途时保存

例:Pipeline bug 追踪在 Linear INGEST 项目

⚠️ 不应保存的内容

  • 代码模式、约定、架构、文件路径 — 可从代码库推导
  • Git 历史 — git log 才是权威来源
  • 调试方案 — 修复已在代码中,commit message 有上下文
  • CLAUDE.md 中已记录的内容
  • 临时状态:进行中的工作、当前对话上下文

记忆文件格式

Frontmatter 规范与索引结构

每个记忆文件使用 YAML Frontmatter 声明元数据,body 部分对于 feedback/project 类型要求包含 Why(原因)和 How to apply(适用场景)结构化信息:

memory-example.md
---
name: integration-test-policy
description: 集成测试必须使用真实数据库,禁止 mock
type: feedback
---

# 集成测试数据库策略

## 规则
所有集成测试必须连接真实数据库进行验证。

## Why
上季度 mock 测试通过但生产迁移失败,
mock/真实环境的差异掩盖了关键 bug。

## How to apply
编写集成测试时直接连接测试数据库,
不使用 sqlite-memory 或 mock provider。

MEMORY.md 索引中每个条目不超过 ~150 字符:- [Title](file.md) — one-line hook。 索引上限 25 行 / 25KB,超出会被截断。

记忆扫描与检索

memoryScan.ts + findRelevantMemories.ts

记忆检索分为两步:扫描(读取所有 .md 文件的 frontmatter 头部) 和 选择(用 Sonnet 模型从清单中选出最相关的 ≤5 条)。

scanMemoryFiles — 单次扫描

export async function scanMemoryFiles(
  memoryDir: string,
  signal: AbortSignal,
): Promise<MemoryHeader[]> {
  const entries = await readdir(memoryDir, { recursive: true })
  const mdFiles = entries.filter(
    f => f.endsWith('.md') && basename(f) !== 'MEMORY.md'
  )
  // 读取 frontmatter,按 mtime 降序排列,最多 200 个
  const headers = await Promise.allSettled(
    mdFiles.map(async (relPath) => {
      const filePath = join(memoryDir, relPath)
      const { content, mtimeMs } = await readFileInRange(filePath, 0, 30)
      const { frontmatter } = parseFrontmatter(content)
      return {
        filename: relPath, filePath, mtimeMs,
        description: frontmatter.description || null,
        type: parseMemoryType(frontmatter.type),
      }
    })
  )
  return headers
    .filter(r => r.status === 'fulfilled')
    .map(r => r.value)
    .sort((a, b) => b.mtimeMs - a.mtimeMs)
    .slice(0, 200)
}

findRelevantMemories — AI 选择

export async function findRelevantMemories(
  query: string,
  memoryDir: string,
  signal: AbortSignal,
): Promise<RelevantMemory[]> {
  // 1. 扫描所有记忆文件头部
  const memories = await scanMemoryFiles(memoryDir, signal)
  // 2. 用 Sonnet 模型选择最相关的 ≤5 条
  const selectedFilenames = await selectRelevantMemories(
    query, memories, signal
  )
  // 3. 返回路径 + mtime(用于新鲜度检查)
  return selected.map(m => ({
    path: m.filePath,
    mtimeMs: m.mtimeMs,
  }))
}

检索流程要点

  • 单次扫描优化:先读后排序,避免双 stat 调用
  • 已去重过滤alreadySurfaced 避免重复选择前轮已展示的记忆
  • 工具噪声抑制:当用户正在使用某工具时,不选择该工具的参考文档
  • 最多 200 个文件,按 mtime 降序排列

年龄衰减与新鲜度

memoryAge.ts — 记忆过时检测

记忆是时间点的快照,不是实时状态。系统通过 年龄计算 新鲜度警告帮助模型正确处理过时记忆:

export function memoryAgeDays(mtimeMs: number): number {
  return Math.max(0, Math.floor((Date.now() - mtimeMs) / 86_400_000))
}

export function memoryAge(mtimeMs: number): string {
  const d = memoryAgeDays(mtimeMs)
  if (d === 0) return 'today'
  if (d === 1) return 'yesterday'
  return `${d} days ago`
}

export function memoryFreshnessText(mtimeMs: number): string {
  const d = memoryAgeDays(mtimeMs)
  if (d <= 1) return ''
  return `This memory is ${d} days old. Memories are point-in-time
  observations, not live state — verify against current code.`
}
0 天
today
无警告
1 天
yesterday
无警告
2+ 天
"1days old"
注入 system-reminder 警告

⚠️ 记忆漂移防护

对于引用特定函数、文件路径的记忆,系统要求模型在使用前验证其仍然存在: 文件路径 → 检查文件是否存在;函数名 → grep 搜索;文件:行号引用 → 特别容易过时。 这是经过 eval 验证的防护机制(memory-prompt-iteration case study)。

自动做梦(autoDream)

离线记忆整合 — 四阶段合并流程

自动做梦是记忆系统的核心维护机制。当满足条件时,系统会 fork 一个后台 Agent 执行记忆整合: 浏览最近会话、合并新信息、修剪过期记忆、更新索引。

autoDream 触发流程

每轮结束 stopHooks时间门 ≥24h会话门 ≥5 sessions合并锁 PID-basedForked Agent 后台执行Phase 1 OrientPhase 2 GatherPhase 3 ConsolidatePhase 4 Prune & Index

三重门控

  1. 时间门:距上次整合 ≥ 24 小时(一次 stat 调用)
  2. 会话门:有 ≥ 5 个新会话(扫描节流 10 分钟)
  3. 合并锁:PID-based 文件锁,防止并发整合

合并锁机制

  • 锁文件:.consolidate-lock
  • mtime = lastConsolidatedAt 时间戳
  • body = holder PID,用于检测存活进程
  • 过期阈值 1 小时(PID 复用防护)
  • 失败时回滚 mtime(下次可重试)

整合提示词(四阶段)

# Dream: Memory Consolidation

## Phase 1 — Orient
- ls 记忆目录,读取 MEMORY.md 索引
- 浏览现有 topic files 避免重复

## Phase 2 — Gather recent signal
- 读取 daily logs (logs/YYYY/MM/YYYY-MM-DD.md)
- 检查与代码库矛盾的旧记忆
- 精准 grep JSONL transcripts

## Phase 3 — Consolidate
- 合并新信号到现有 topic files
- 转换相对日期为绝对日期
- 删除已被推翻的事实

## Phase 4 — Prune and index
- 更新 MEMORY.md(≤25 行,≤25KB)
- 移除过期指针,缩短冗长条目

DreamTask — 做梦任务管理

UI 可见性与生命周期

DreamTask 是 autoDream 的 UI 抽象层,将不可见的 forked agent 暴露为底部任务栏可见的后台任务, 用户可以在 Shift+Down 对话框中查看进度、取消任务。

🟡 开始中
phase: "starting"
读取记忆目录,浏览现有文件
🟢 更新中
phase: "updating"
首次 Edit/Write 操作后切换
✅ 已完成
phase: "completed"
整合完成,显示修改文件列表

DreamTask 状态结构

type DreamTaskState = {
  type: 'dream'
  status: 'running' | 'completed' | 'failed' | 'killed'
  phase: 'starting' | 'updating'
  sessionsReviewing: number    // 正在审查的会话数
  filesTouched: string[]       // Edit/Write 操作的文件路径
  turns: DreamTurn[]           // 助手回复(工具调用折叠为计数)
  priorMtime: number           // 锁的原始 mtime(用于 kill 回滚)
  abortController?: AbortController
}

生命周期事件

  • registerDreamTask:创建任务,status=running, phase=starting
  • addDreamTurn:每轮助手回复,phase 首次 Edit/Write 时切为 updating
  • completeDreamTask:status=completed,inline 显示修改摘要
  • failDreamTask:status=failed,回滚合并锁 mtime
  • kill:abort 控制器 + 回滚锁 + status=killed

团队记忆(Team Memory)

跨用户共享记忆 — teamMemPaths.ts

团队记忆是个人记忆的扩展,存储在 memory/team/ 子目录中。 所有在同一项目目录工作的用户共享团队记忆,每个会话开始时自动同步。

安全验证(双层检查)

  1. 字符串级:path.resolve() 消除 .. 段
  2. 文件系统级:realpath() 解析符号链接

防御悬挂符号链接、符号链接循环、Unicode 规范化攻击

作用域规则

  • user → always private
  • feedback → 默认 private,项目级惯例存 team
  • project → 偏向 team
  • reference → usually team

extractMemories — 提取记忆 Agent

后台自动提取对话中的有价值信息

主 Agent 的 prompt 中始终包含完整的保存指令。后台 extract-memories agent 作为安全网: 当主 Agent 主动写入了记忆时后台跳过该范围,未写入时后台 agent 捕获遗漏。

启用条件

  • 功能开关 feature('EXTRACT_MEMORIES') 必须开启
  • 交互式会话 OR GrowthBook flag tengu_slate_thimble
  • autoDream 限制工具权限:Bash 仅允许只读命令(ls, find, grep, cat 等)