源码位置:
src/services/autoDream/
相关文件:autoDream.ts·consolidationLock.ts·consolidationPrompt.ts·config.ts
1. 是什么
AutoDream 是 Claude Code 的跨会话记忆整合引擎。
每轮对话结束时,它静默地在后台检查:「是否应该触发一次深度记忆整理?」
一旦条件满足,它启动一个独立的 forked subagent,让 AI 自己回顾历史会话、刮除陈旧内容、压缩合并新知识,最终更新到 memory/ 目录下。
类比理解:
extractMemories ≈ 每天做笔记(增量,当轮触发)
autoDream ≈ 周末整理笔记本(批量,跨会话)
用户手动执行 /dream 命令的效果与它完全相同——autoDream 只是让这件事自动发生。
时序图:
2. 整体架构
每轮对话结束 (stopHooks.ts)
│
▼
executeAutoDream(context, appendSystemMessage)
│
▼ runner?.() [闭包,在 initAutoDream() 中赋值]
┌────────────────────────────────────────────┐
│ runAutoDream() │
│ │
│ 1. isGateOpen() — 环境前置检查 │
│ 2. 时间门控 — 距上次整合 >= minHours │
│ 3. 扫描节流 — 距上次扫描 >= 10min │
│ 4. 会话门控 — 新会话数 >= minSessions │
│ 5. 文件锁 — 多进程互斥 │
│ ↓ (全部通过) │
│ runForkedAgent(dream prompt) │
│ · querySource: 'auto_dream' │
│ · skipTranscript: true │
│ · canUseTool: 受限工具集 │
│ ↓ │
│ completeDreamTask / failDreamTask │
│ ↓ (成功) │
│ appendSystemMessage("Improved N files") │
└────────────────────────────────────────────┘
3. 五层门控机制(廉价到昂贵)
这是 autoDream 最精妙的工程设计——每层检查只有前一层通过才执行,成本递增:
Layer 1 — 环境前置检查(无 I/O)
function isGateOpen(): boolean {
if (getKairosActive()) return false // KAIROS 模式用不同的 dream
if (getIsRemoteMode()) return false // 远程模式不触发
if (!isAutoMemoryEnabled()) return false // 用户开关
return isAutoDreamEnabled() // 配置/Feature Flag
}
isAutoDreamEnabled() 的优先级链:
settings.json的autoDreamEnabled字段(用户显式配置)- GrowthBook flag
tengu_onyx_plover的enabled字段(远程下发默认值)
Layer 2 — 时间门控(1 次 stat 系统调用)
const lastAt = await readLastConsolidatedAt() // stat(.consolidate-lock) 的 mtime
const hoursSince = (Date.now() - lastAt) / 3_600_000
if (hoursSince < cfg.minHours) return // 默认 24h
关键设计:锁文件的 mtime 就是上次整合时间,不需要单独存储时间戳。
Layer 3 — 扫描节流(防止时间门通过后密集触发)
const SESSION_SCAN_INTERVAL_MS = 10 * 60 * 1000 // 10 分钟
const sinceScanMs = Date.now() - lastSessionScanAt
if (sinceScanMs < SESSION_SCAN_INTERVAL_MS) return
lastSessionScanAt = Date.now()
当时间门通过但会话数不够时,锁的 mtime 不会更新,所以时间门下次仍会通过,形成循环触发。扫描节流挡住了这一频繁的无效 stat 代价。
Layer 4 — 会话门控(扫描会话文件目录)
let sessionIds = await listSessionsTouchedSince(lastAt)
const currentSession = getSessionId()
sessionIds = sessionIds.filter(id => id !== currentSession) // 排除当前会话
if (sessionIds.length < cfg.minSessions) return // 默认 5 个会话
listSessionsTouchedSince 扫描 projects/<cwd-hash>/ 下的 JSONL 会话文件,按 mtime 过滤。这确保只有积累了足够多历史信息时才整合——否则整合性价比低。
Layer 5 — 文件锁(多进程互斥)
const priorMtime = await tryAcquireConsolidationLock()
if (priorMtime === null) return // 其他进程正在整合
锁的实现细节见下文「锁机制」章节。
4. 文件锁机制(精妙的 mtime 复用)
memory/
.consolidate-lock ← 其 mtime = lastConsolidatedAt
文件体 = 持有者 PID
获取锁
export async function tryAcquireConsolidationLock(): Promise<number | null> {
// 1. 读取当前持有者 PID 和 mtime
const [s, raw] = await Promise.all([stat(path), readFile(path, 'utf8')])
// 2. 如果锁未超时(< 1h)且 PID 存活,阻塞
if (Date.now() - s.mtimeMs < HOLDER_STALE_MS && isProcessRunning(holderPid)) {
return null
}
// 3. 写入自己的 PID,mtime 自动更新到 now
await writeFile(path, String(process.pid))
// 4. 竞争检测:重读验证 PID 是否是自己写的
const verify = await readFile(path, 'utf8')
if (parseInt(verify.trim(), 10) !== process.pid) return null
return priorMtime // 返回旧 mtime(用于失败回滚)
}
失败回滚
export async function rollbackConsolidationLock(priorMtime: number): Promise<void> {
if (priorMtime === 0) {
await unlink(path) // 之前没有锁文件,删除
return
}
await writeFile(path, '') // 清空 PID(不再"持有")
const t = priorMtime / 1000
await utimes(path, t, t) // 恢复到旧 mtime(恢复 lastConsolidatedAt)
}
核心洞见:失败后把 mtime 恢复到之前,等于「这次整合从未发生」,下次时间门仍然可以通过并重试。
崩溃安全:进程 SIGKILL 后,锁文件还在,但 PID 死了。超过 HOLDER_STALE_MS(1小时)后锁自动过期,下个进程可以重新获取。
5. forked subagent 的工具权限约束
const canUseTool = createAutoMemCanUseTool(memoryRoot)
这个函数定义了 dream subagent 能使用的工具:
| 工具 | 权限 |
|---|---|
| FileRead / Grep / Glob | ✅ 无限制 |
| Bash(只读命令) | ✅ ls/find/cat/stat/wc/head/tail 等 |
| FileEdit / FileWrite | ✅ 仅限 memory/ 目录内路径 |
| Bash(写操作/重定向) | ❌ 拒绝 |
| MCP 工具、Agent 工具 | ❌ 拒绝 |
这是安全边界:dream agent 只能读取历史信息,只能写记忆文件,不能修改代码,不能调用外部服务。
6. Prompt 四阶段(可直接复用)
buildConsolidationPrompt() 是整个系统的智识核心。以下是完整的中文解析:
Phase 1 — Orient(定向)
- ls 记忆目录,了解现有结构
- 读取 MEMORY.md 索引
- 浏览现有主题文件,避免创建重复
- 如果存在 logs/ 或 sessions/ 子目录,查看最近条目
作用:让 AI 先建立对现有知识库的全局认知,再决定要添加什么。
Phase 2 — Gather(采集,按优先级)
1. 日志文件(logs/YYYY/MM/YYYY-MM-DD.md)— 第一优先级
2. 已漂移的记忆(当前事实与记忆矛盾的)
3. Transcript 搜索(grep JSONL,非穷举)
关键约束:不要穷举读 transcript,只 grep 你已知需要找的内容。
Phase 3 — Consolidate(整合)
- 合并到现有主题文件,而非创建新文件
- 相对日期转绝对日期("昨天" → "2026-04-03")
- 删除被推翻的事实
Phase 4 — Prune and index(剪枝与索引)
更新 MEMORY.md:
- 保持 ≤ 200 行,≤ 25KB
- 每行格式:- [Title](file.md) — 一行简介(< 150 字符)
- 删除过时指针
- 解决两个文件之间的矛盾
7. 进度追踪(UI 侧集成)
autoDream 对用户是可观察的,通过 registerDreamTask 在 UI 的 tasks 面板注册一个后台任务:
const taskId = registerDreamTask(setAppState, {
sessionsReviewing: sessionIds.length,
priorMtime,
abortController,
})
makeDreamProgressWatcher 监听每条 assistant 消息,实时更新:
- 已接触的文件列表(
filesTouched) - 每轮的摘要文本和工具调用数
用户可以从 UI 的 Background Tasks 对话框主动 Kill 正在运行的 dream,kill 后会触发 rollbackConsolidationLock,保证 mtime 不被污染。
8. 配置参数一览
| 参数 | 来源 | 默认值 | 说明 |
|---|---|---|---|
autoDreamEnabled | settings.json | undefined (fall through) | 用户开关 |
tengu_onyx_plover.enabled | GrowthBook | — | 远程下发总开关 |
tengu_onyx_plover.minHours | GrowthBook | 24 | 最小整合间隔(小时) |
tengu_onyx_plover.minSessions | GrowthBook | 5 | 最少新会话数 |
SESSION_SCAN_INTERVAL_MS | 硬编码 | 10 分钟 | 会话扫描节流 |
HOLDER_STALE_MS | 硬编码 | 1 小时 | 锁持有者超时 |
9. 哪些内容可以直接用于自己的 Agent
下面按「可拷贝程度」分级标注:
9.1 可直接复制的核心思想
9.1.1 锁文件 mtime 复用模式
不要单独存储「上次运行时间」变量。直接用锁文件的 mtime:
# Python 等效实现
import os, time
LOCK_FILE = "memory/.consolidate-lock"
def read_last_consolidated_at() -> float:
try:
return os.stat(LOCK_FILE).st_mtime * 1000 # ms
except FileNotFoundError:
return 0
def acquire_lock() -> float | None:
"""Returns prior mtime (for rollback), or None if blocked."""
try:
stat = os.stat(LOCK_FILE)
mtime_ms = stat.st_mtime * 1000
with open(LOCK_FILE) as f:
holder_pid = int(f.read().strip())
# 锁未超时且 PID 存活则阻塞
if (time.time() * 1000 - mtime_ms) < 3_600_000:
if is_process_running(holder_pid):
return None
except (FileNotFoundError, ValueError):
mtime_ms = 0 # 没有锁文件
# 写入自己的 PID(自动更新 mtime)
os.makedirs(os.path.dirname(LOCK_FILE), exist_ok=True)
with open(LOCK_FILE, 'w') as f:
f.write(str(os.getpid()))
# 竞争验证
with open(LOCK_FILE) as f:
if int(f.read().strip()) != os.getpid():
return None
return mtime_ms
def rollback_lock(prior_mtime: float):
if prior_mtime == 0:
os.unlink(LOCK_FILE)
return
with open(LOCK_FILE, 'w') as f:
f.write('') # 清空 PID
t = prior_mtime / 1000
os.utime(LOCK_FILE, (t, t)) # 恢复旧 mtime
9.1.2 五层“廉价到昂贵”门控模式
任何「背景任务」都应该按成本递增顺序检查条件,前一层不通过则短路:
async def should_run_consolidation() -> bool:
# Layer 1: 无 I/O 的环境检查
if not is_feature_enabled(): return False
# Layer 2: 单次 stat(极廉价)
last_at = await read_last_consolidated_at()
if (now() - last_at) < MIN_HOURS * 3600_000: return False
# Layer 3: 内存节流(防止 Layer 4 被频繁触发)
if (now() - last_scan_at) < SCAN_INTERVAL_MS: return False
last_scan_at = now()
# Layer 4: 目录扫描(比 stat 贵)
sessions = await list_sessions_since(last_at)
if len(sessions) < MIN_SESSIONS: return False
# Layer 5: 文件锁(有写操作)
prior_mtime = await acquire_lock()
return prior_mtime is not None
9.1.3 整合 Prompt 的四段式结构
这是可以直接拿来用于任何记忆整合 agent 的 prompt pattern:
# Memory Consolidation
You are performing a reflective pass over your memory files.
Memory directory: `{memory_root}`
History directory: `{history_dir}` (grep narrowly, don't read whole files)
## Phase 1 — Orient
- List the memory directory
- Read the index file (MEMORY.md)
- Skim existing topic files to avoid duplication
## Phase 2 — Gather recent signal (priority order)
1. Append-only logs if present
2. Memories that have drifted (contradict current facts)
3. History search — grep for specific terms you know matter
Do NOT exhaustively read history. Look only for things you already suspect matter.
## Phase 3 — Consolidate
- Merge into existing topic files rather than creating duplicates
- Convert relative dates to absolute dates
- Delete contradicted facts at the source
## Phase 4 — Prune and index
Update the index file:
- Keep it under {MAX_LINES} lines and {MAX_SIZE}KB
- Each entry: `- [Title](file.md) — one-line hook (<150 chars)`
- Remove stale pointers, resolve contradictions
Return a brief summary of changes. If nothing changed, say so.
9.1.4 forked subagent 的工具权限沙箱
自己实现 memory agent 时,用白名单约束工具权限:
ALLOWED_READ_TOOLS = {"file_read", "grep", "glob", "bash_readonly"}
ALLOWED_WRITE_PATHS = ["/path/to/memory/"] # 只允许写记忆目录
def can_use_tool(tool_name: str, tool_input: dict) -> bool:
if tool_name in ALLOWED_READ_TOOLS:
return True
if tool_name in {"file_write", "file_edit"}:
path = tool_input.get("file_path", "")
return any(path.startswith(p) for p in ALLOWED_WRITE_PATHS)
return False # 默认拒绝所有其他工具
9.2 可参考设计的模式
9.2.1 skipTranscript: true — 后台 agent 不污染主对话
任何后台整合 agent(不面向用户的)都不应该把自己的中间过程写入主对话历史。否则下次 context 里会有大量无意义的系统内部消息。
9.2.2 失败回滚即「这次从未发生」
try:
result = await run_dream_agent(...)
record_success() # 更新 mtime → 时间门重置
except Exception:
rollback_lock(prior_mtime) # 恢复旧 mtime → 时间门下次仍通过
这个设计让系统自动重试,不需要显式的重试逻辑。
9.2.3 会话文件扫描作为活动量代理
不需要 agent 主动汇报「我处理了多少信息」,直接通过统计历史文件数量/大小来判断「是否值得整合」:
def sessions_since(last_at: float) -> list[str]:
"""返回 mtime > last_at 的会话文件列表"""
candidates = []
for f in os.listdir(history_dir):
if f.endswith('.jsonl'):
mtime = os.stat(os.path.join(history_dir, f)).st_mtime * 1000
if mtime > last_at:
candidates.append(f)
return candidates
9.2.4 记忆索引的大小约束
MEMORY.md 有硬性限制(≤200行 / ≤25KB),且明确区分:
- 索引文件:只存指针,每行一句话
- 主题文件:存具体内容
这两层结构防止索引因内容膨胀失去作用(注入 system prompt 的优先是索引)。
9.3 可借鉴思路的设计
9.3.1 用户可见的可取消后台任务
autoDream 在 UI 注册了一个 Task(DreamTask),用户可以看到进度、可以 Kill。
即使是完全后台的 agent,也应该提供可观察性和控制权。
9.3.2 参数由远程 Feature Flag 控制
minHours/minSessions 不是硬编码常量,而是通过 GrowthBook 远程下发。
这使得 Anthropic 可以不发版本就调整触发频率,是 LLM 系统运营的重要模式。
10. 设计要点总结
autoDream 的核心工程洞见:
1. mtime 即状态 — 锁文件 mtime 替代单独的时间戳存储
2. 廉价先行 — 五层门控按 I/O 成本递增排列
3. 失败即回滚 — 异常恢复等价于「这次从未发生」
4. 后台静默 — skipTranscript,不干扰主对话
5. 权限沙箱 — canUseTool 白名单,只写记忆目录
6. 结构化 prompt — 四段式(Orient→Gather→Consolidate→Prune)
7. 索引与内容分离 — MEMORY.md 只存指针,避免膨胀
8. 参数远程控制 — 触发阈值不硬编码,可热调整
11. 附:数据流全景图
[每轮对话结束]
│
▼
isGateOpen? ──No──> return
│ Yes
▼
stat(.consolidate-lock) ← 1 次 I/O
hoursSince < 24h? ──Yes──> return
│ No
▼
lastSessionScanAt < 10min? ──Yes──> return
│ No
▼
ls projects/<cwd>/ ← N 次 stat
sessions < 5? ──Yes──> return
[记录 lastSessionScanAt]
│ No
▼
stat+read(.consolidate-lock)
pid alive? ──Yes──> return
│ No (或超时)
▼
write(pid) → verify ← 2 次 I/O
lost race? ──Yes──> return
│ No
▼
┌─────────────────────────────────────────────┐
│ runForkedAgent(consolidationPrompt) │
│ · canUseTool: 只读 + 只写 memory/ │
│ · skipTranscript: true │
│ · onMessage: 更新 DreamTask 进度 │
│ │
│ Phase 1: ls + read MEMORY.md │
│ Phase 2: grep transcripts (narrow) │
│ Phase 3: write/edit memory/*.md │
│ Phase 4: update MEMORY.md index │
└─────────────────────────────────────────────┘
│ 成功
▼
completeDreamTask()
appendSystemMessage("Improved N files")
│ 失败
▼
rollbackConsolidationLock(priorMtime)
failDreamTask()
写在最后
Claude Code 的记忆系统,远比你想象的复杂。 它不是把对话存起来那么简单——它有跨会话整合、五层门控、失败回滚、工具沙箱,每一层都有真实的工程权衡在背后。 它的成功不仅仅依靠模型能力,更多的是一系列的工程化设计。
读完 Claude Code 的源码,我最大的感受是:Anthropic 的工程师是一群极其务实的强迫症患者。 代码虽不优雅,但每个设计决策都有道理。Memory 的五层分层、Harness 的门控链路、mtime 复用锁……你把这些搬进自己的 Agent,它立刻就有了"工业味"。
你也想养一只更能干的 🦞 吗?
想深入 Claude Code 的记忆管理?想让你的 Agent 学会"整理记忆"而不是无限堆 context?
我写了一份关于 Claude Code 详细的架构分析,130页PDF,带你深入理解 Claude Code 的设计哲学。
00: 目录
01: 示例
02: 示例
03: 示例
私信我,一起它的设计思路装进你自己的项目里。