AutoDream 深度解读:Claude Code 跨会话记忆整合引擎

0 阅读11分钟

源码位置:src/services/autoDream/
相关文件:autoDream.ts · consolidationLock.ts · consolidationPrompt.ts · config.ts


1. 是什么

AutoDream 是 Claude Code 的跨会话记忆整合引擎
每轮对话结束时,它静默地在后台检查:「是否应该触发一次深度记忆整理?」
一旦条件满足,它启动一个独立的 forked subagent,让 AI 自己回顾历史会话、刮除陈旧内容、压缩合并新知识,最终更新到 memory/ 目录下。

类比理解:

extractMemories ≈ 每天做笔记(增量,当轮触发)
autoDream ≈ 周末整理笔记本(批量,跨会话)

用户手动执行 /dream 命令的效果与它完全相同——autoDream 只是让这件事自动发生。

时序图:

PNG


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() 的优先级链:

  1. settings.jsonautoDreamEnabled 字段(用户显式配置)
  2. GrowthBook flag tengu_onyx_ploverenabled 字段(远程下发默认值)

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 复用)

源码:consolidationLock.ts

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. 配置参数一览

参数来源默认值说明
autoDreamEnabledsettings.jsonundefined (fall through)用户开关
tengu_onyx_plover.enabledGrowthBook远程下发总开关
tengu_onyx_plover.minHoursGrowthBook24最小整合间隔(小时)
tengu_onyx_plover.minSessionsGrowthBook5最少新会话数
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: 示例

私信我,一起它的设计思路装进你自己的项目里。