模块一 · 第六节:autoDream 自动记忆整合
核心问题
什么是 autoDream?Claude Code 如何在后台自动整合记忆?三重门控机制是如何工作的?分布式锁如何防止并发冲突?
◇ 本节位置
Claude Code 全局架构
┌─────────────────────────────────────────────────────────────────────┐
│ 入口与启动 │
│ │
│ autoDream ← 本节 │
│ ├── autoDream.ts ──> 主逻辑 │
│ ├── config.ts ──> 配置 │
│ └── consolidationLock.ts ──> 分布式锁 │
└─────────────────────────────────────────────────────────────────────┘
一、autoDream 概述
1.1 什么是自动记忆整合
源码位置 :src/services/autoDream/autoDream.ts 第 1-9 行
typescript
// Background memory consolidation. Fires the /dream prompt as a forked
// subagent when time-gate passes AND enough sessions have accumulated.
//
// Gate order (cheapest first):
// 1. Time: hours since lastConsolidatedAt >= minHours (one stat)
// 2. Sessions: transcript count with mtime > lastConsolidatedAt >= minSessions
// 3. Lock: no other process mid-consolidation
核心思想:Claude Code 在后台定期分析会话记忆,将有价值的信息提取到长期记忆中。
1.2 为什么需要 autoDream
┌─────────────────────────────────────────────────────────────────────┐
│ 没有 autoDream: │
│ - 每次会话的临时记忆很快被遗忘 │
│ - 跨会话的学习无法积累 │
└─────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ 有 autoDream: │
│ - 自动分析会话,提取重要信息 │
│ - 更新 CLAUDE.md、CLAUDE.local.md │
│ - 形成长期记忆 │
└─────────────────────────────────────────────────────────────────────┘
二、三重门控机制
2.1 门控顺序
源码位置 :src/services/autoDream/autoDream.ts 第 122 行
typescript
export function initAutoDream(): void {
// 按代价从低到高检查门控
// 1. 时间门(stat 一个文件)
// 2. 会话门(stat 多个文件)
// 3. 锁门(尝试获取锁)
}
2.2 时间门
源码位置 :src/services/autoDream/autoDream.ts 第 55-80 行
typescript
const DEFAULTS: AutoDreamConfig = {
minHours: 24, // 至少 24 小时
minSessions: 5, // 至少 5 个新会话
}
function getConfig(): AutoDreamConfig {
const raw = getFeatureValue_CACHED_MAY_BE_STALE<Partial<AutoDreamConfig> | null>(
'tengu_onyx_plover', // GrowthBook feature key
null,
)
return {
minHours: raw?.minHours ?? DEFAULTS.minHours,
minSessions: raw?.minSessions ?? DEFAULTS.minSessions,
}
}
五问分析:
**问 1:为什么用 mtime 而不是 ctime?
typescript
// mtime(修改时间):文件内容修改时间
// ctime(变化时间):文件属性变化时间
// 选择 mtime 的原因:
// - 会话转录文件在有新消息时会被修改
// - mtime 更准确地反映会话活动
2.3 会话门
源码位置 :src/services/autoDream/consolidationLock.ts 第 30-35 行
typescript
// Stale past this even if the PID is live (PID reuse guard).
const HOLDER_STALE_MS = 60 * 60 * 1000 // 1小时
/**
* mtime of the lock file = lastConsolidatedAt. 0 if absent.
* Per-turn cost: one stat.
*/
export async function readLastConsolidatedAt(): Promise<number> {
try {
const s = await stat(lockPath())
return s.mtimeMs
} catch {
return 0
}
}
五问分析:
**问 1:为什么需要 PID reuse guard?
typescript
// PID(进程 ID)可能被操作系统回收复用
// 如果锁的持有者崩溃,新进程可能获得相同的 PID
// 解决方案:
// - 即使 PID 存在,也要检查是否真的在运行
// - 使用 HOLDER_STALE_MS(1小时)作为额外保护
三、分布式锁实现
3.1 锁文件设计
源码位置 :src/services/autoDream/consolidationLock.ts 第 14-20 行
typescript
const LOCK_FILE = '.consolidate-lock'
function lockPath(): string {
return join(getAutoMemPath(), LOCK_FILE)
}
锁文件特点:
- 存储当前进程的 PID
- mtime = lastConsolidatedAt(上次整合时间)
- 放在 memory 目录下(与记忆文件同目录)
3.2 获取锁
源码位置 :src/services/autoDream/consolidationLock.ts 第 46-80 行
typescript
export async function tryAcquireConsolidationLock(): Promise<number | null> {
const path = lockPath()
// 1. 读取现有锁
let mtimeMs: number | undefined
let holderPid: number | undefined
try {
const [s, raw] = await Promise.all([stat(path), readFile(path, 'utf8')])
mtimeMs = s.mtimeMs
holderPid = parseInt(raw.trim(), 10)
} catch {
// ENOENT --- 没有现有锁
}
// 2. 检查锁是否有效
if (mtimeMs !== undefined && Date.now() - mtimeMs < HOLDER_STALE_MS) {
if (holderPid !== undefined && isProcessRunning(holderPid)) {
// 锁被活跃进程持有
return null
}
}
// 3. 尝试获取锁
await mkdir(getAutoMemPath(), { recursive: true })
await writeFile(path, String(process.pid))
// 4. 验证是否获取成功(可能被其他进程抢走)
const verify = await readFile(path, 'utf8')
if (parseInt(verify.trim(), 10) !== process.pid) {
return null // 获取失败
}
return mtimeMs ?? 0 // 返回之前的 mtime(用于回滚)
}
3.3 锁的语义
| 状态 | 含义 | 操作 |
|---|---|---|
| 文件不存在 | 从未整合过 | 直接获取锁 |
| PID 不存在 | 持有者已退出 | 可以抢占 |
| PID 存在且运行中 | 正在整合 | 等待 |
| 超过 STALE 时间 | 可能是僵尸锁 | 可以抢占 |
五问分析:
**问 1:为什么不使用分布式锁库(如 Redis)?
typescript
// 设计意图:简单、可靠、无额外依赖
// 优点:
// - 零额外服务
// - 文件系统原子操作保证一致性
// - 崩溃后自动恢复(mtime 作为时间戳)
// 缺点:
// - 不支持跨机器协调(但记忆只在本地)
// - 不支持锁超时自动释放(使用 PID 检测代替)
四、整合执行
4.1 启动整合任务
源码位置 :src/services/autoDream/autoDream.ts 第 200-220 行
typescript
// 注册为 Dream Task
const taskId = registerDreamTask(setAppState, {
id: randomUUID(),
description: 'Memory consolidation',
})
// 运行 forked 子代理
const params = createCacheSafeParams({ ... })
await runForkedAgent({
prompt: buildConsolidationPrompt(sessions),
background: true,
params,
})
4.2 整合提示词
源码位置 :src/services/autoDream/consolidationPrompt.ts
typescript
export function buildConsolidationPrompt(sessions: Session[]): string {
return `分析以下会话记录,提取有价值的信息:
${sessions.map(s => s.transcript).join('\n\n')}
请将重要信息分类整理到:
1. CLAUDE.md - 项目规范(团队共享)
2. CLAUDE.local.md - 个人偏好(仅自己可见)
3. Team Memory - 团队知识(跨项目)`
}
五、错误处理
5.1 锁获取失败
源码位置 :src/services/autoDream/autoDream.ts 第 180-190 行
typescript
const priorMtime = await tryAcquireConsolidationLock()
if (priorMtime === null) {
logForDebugging('[autoDream] lock held, skipping')
return // 锁被占用,跳过本次整合
}
5.2 整合失败回滚
源码位置 :src/services/autoDream/autoDream.ts
typescript
// 失败时回滚 mtime
await rollbackConsolidationLock(priorMtime)
export async function rollbackConsolidationLock(priorMtime: number): Promise<void> {
if (priorMtime === 0) {
// 之前没有锁,删除
await unlink(lockPath()).catch(() => {})
} else {
// 恢复之前的 mtime
await utimes(lockPath(), priorMtime, priorMtime)
}
}
六、设计模式识别
6.1 双重检查锁定
typescript
// initAutoDream() 中的双重检查:
if (Date.now() - lastConsolidatedAt >= minHours) {
const sessions = await listSessionsTouchedSince(lastConsolidatedAt)
if (sessions.length >= minSessions) {
const lock = await tryAcquireConsolidationLock()
if (lock !== null) {
await runConsolidation() // 真正执行
}
}
}
6.2 乐观锁
typescript
// 使用 mtime 作为版本号
// - 获取锁时记录 priorMtime
// - 失败时恢复到 priorMtime
// - 类似于数据库的 MVCC
七、思考题
思考题 1:autoDream 和普通记忆提取有什么区别?
答案:
typescript
// 普通记忆提取:即时发生
// - 每次会话结束
// - 分析范围有限
// autoDream:定期批量处理
// - 后台运行,不影响前台
// - 可以处理多个会话
// - 更好地发现跨会话的模式
// 源码证据:
// autoDream.ts 第 1-9 行的注释明确说明:
// "Background memory consolidation"
思考题 2:为什么整合需要用户同意?
答案:
typescript
// autoDream 会修改 CLAUDE.md 等文件
// 用户需要审核整合结果
// 避免自动添加不准确的信息
// 源码证据:
// consolidationPrompt.ts 中的提示词要求:
// "Do NOT apply changes --- present proposals for user approval"
思考题 3:锁文件放在 memory 目录而不是项目根目录的原因?
答案:
typescript
// 1. 与记忆文件同目录,方便管理
// 2. 即使项目目录不可写,memory 目录通常可写
// 3. 符合"按 git-root 分目录"的设计
// 源码证据:
// consolidationLock.ts 第 8-10 行:
// "Lives inside the memory dir (getAutoMemPath) so it keys on git-root
// like memory does, and so it's writable even when the memory path comes
// from an env/settings override whose parent may not be."
八、延伸阅读
| 文件 | 行数 | 核心内容 |
|---|---|---|
src/services/autoDream/autoDream.ts |
~350 | 主逻辑、三重门控 |
src/services/autoDream/consolidationLock.ts |
~150 | 分布式锁实现 |
src/services/autoDream/config.ts |
~30 | 配置管理 |