精读: OpenClaw Pre-Compaction Memory Flush
Claude Code 记忆机制:精读: Claude Code 记忆系统 Codex 记忆机制:精读: Codex 记忆系统 Harness 视角:Agent 框架 · Claude Code / Codex 架构
调研元信息
| 项目 | 值 |
|---|---|
| 调研日期 | 2026-06-22 |
| 源码版本 | github.com/openclaw/openclaw main HEAD(depth=1 clone) |
| 本地路径 | /tmp/openclaw/ |
| 涉及研究轴 | B(Coding Agent)+ F(设计模式) |
| 证据等级 | [源码] 为主 |
一句话定位
OpenClaw 的 Pre-Compaction Memory Flush 是一种在上下文压缩之前、由嵌入式 agent 执行的静默持久化轮次——让模型在”记忆被删前”把重要信息写到磁盘,解决了”压缩 = 遗忘”的根本矛盾 [源码]。
记忆类型标注
| 类型 | 涉及 | 说明 |
|---|---|---|
| Working | 是 | flush 的触发条件基于 context window token 使用量 |
| Profile | 否 | MEMORY.md 是用户/系统记忆,不是规则配置 |
| User | 是 | flush 产出写入 memory/YYYY-MM-DD.md(日记层)和 MEMORY.md(长期层) |
| External | 间接 | 通过 Host-Adapter 可对接 DB-Agent-Memory 等外部记忆服务 |
日常类比
想象你在开一个长达 6 小时的研讨会。会议室的白板空间有限(context window),写满了就得擦掉旧内容(compaction)。OpenClaw 的做法是:在擦白板之前,安排一个速记员(嵌入式 agent)快速拍照,把重要内容誊写到笔记本(memory/YYYY-MM-DD.md)里。这样即使白板擦干净了,重要决策和结论都保留在笔记本中。
类比边界:速记员不是拍照留存所有内容,而是由 AI 判断”什么值得记”——这个判断通过 flush prompt 控制,质量取决于 prompt 设计。
架构总览
sequenceDiagram
participant User as 用户
participant AR as Agent Runner
participant Gate as Flush Gate
participant Plan as Flush Plan
participant EA as 嵌入式 Agent
participant FS as 文件系统
participant Compact as Compaction
User->>AR: 发送消息
AR->>Gate: shouldRunMemoryFlush()
Gate->>Gate: tokens >= threshold?<br/>已经 flush 过本轮?
alt 需要 flush
Gate->>Plan: buildMemoryFlushPlan()
Plan-->>Gate: {prompt, path, model}
Gate->>EA: runEmbeddedAgent(silent=true)
EA->>FS: 写入 memory/YYYY-MM-DD.md
EA-->>AR: NO_REPLY (静默)
AR->>AR: 记录 memoryFlushAt
end
AR->>Compact: 正常 compaction(如需要)
Compact->>AR: 压缩后继续
核心机制:三步 Flush 流程
第 1 步:Flush Gate — 何时触发
shouldRunMemoryFlush() 函数决定是否需要 flush [源码]:
触发条件(满足任一即触发):
- Token 阈值:
totalTokens >= contextWindow - reserveTokens - softThresholdTokens(默认 softThreshold = 4000 tokens) - Transcript 体积:
transcriptBytes >= forceFlushTranscriptBytes(默认 2MB)
阻止条件(满足任一则跳过):
- 本轮 compaction 已经 flush 过:
memoryFlushCompactionCount === compactionCount - CLI 后端、心跳请求、只读工作区
去重逻辑通过 hasAlreadyFlushedForCurrentCompaction() 实现——比较 memoryFlushCompactionCount 与 compactionCount,确保每个 compaction 周期只 flush 一次 [源码]。
第 2 步:Flush Plan — 写什么、写到哪
buildMemoryFlushPlan() 构建 flush 计划 [源码]:
目标文件:memory/YYYY-MM-DD.md(基于用户时区),如果文件已存在则追加而非覆盖。
Flush Prompt 的核心指令:
- 只存储”持久性记忆”(durable memories)——跨 session 仍有价值的信息
- 将工作区引导/参考文件视为只读,不修改它们
- 如果
memory/YYYY-MM-DD.md已存在,只追加新内容 - 不创建带时间戳变体的文件(防止文件膨胀)
- 如果没有值得保存的内容,回复
NO_REPLYtoken
安全护栏:系统提示明确标注 MEMORY.md、DREAMS.md、SOUL.md 为只读,防止 flush agent 意外覆盖长期记忆或人格定义文件。
第 3 步:Flush Execution — 静默嵌入式 Agent
runMemoryFlushIfNeeded() 启动一个嵌入式 agent 来执行实际写入 [源码]:
关键设计:
silentExpected: true:用户看不到 flush 过程,不会打断工作流trigger: "memory":标记为记忆触发的 agent turnNO_REPLYtoken:如果没有值得记的内容,agent 回复这个特殊 token 来抑制输出- 模型可独立配置:支持使用不同模型做 flush(如
ollama/qwen3:8b用本地小模型降成本) - 状态持久化:flush 完成后将
memoryFlushAt和memoryFlushCompactionCount写入sessions.json
三层记忆文件布局
OpenClaw 的记忆系统使用三层文件结构(默认在 ~/.openclaw/workspace)[源码]:
| 层 | 文件 | 生命周期 | 加载策略 |
|---|---|---|---|
| 长期记忆 | MEMORY.md |
持久 | 每个 DM session 启动时加载 |
| 日记记忆 | memory/YYYY-MM-DD.md |
按天 | 加载今天和昨天的日记 |
| 整理记录 | DREAMS.md |
持久 | 背景整合(dreaming)的产出 |
与 Claude Code / Codex 的对比
| 维度 | OpenClaw | Claude Code | Codex |
|---|---|---|---|
| 长期记忆 | MEMORY.md(单文件) |
MEMORY.md(索引)+ topic.md(正文) |
MEMORY.md + memory_summary.md |
| 短期记忆 | memory/YYYY-MM-DD.md |
无独立短期文件 | raw_memories.md + rollout_summaries/ |
| 整合机制 | Dreaming(日记→MEMORY.md 晋升) | autoDream(去重+优化) | Phase2(全局合并) |
| 文件预算 | 10,000 字符上限 [源码] |
200 行 / 25KB [源码] |
无硬限制(Phase2 选 top-256) |
Dreaming 系统:从日记到长期记忆
OpenClaw 的 Dreaming 系统负责将 flush 产出的日记内容晋升为长期记忆 [源码]:
flowchart LR
FLUSH["Flush Agent<br/>写入 memory/YYYY-MM-DD.md"] --> COLLECT["Dreaming<br/>收集短期信号"]
COLLECT --> SCORE["候选评分<br/>(短期晋升得分)"]
SCORE --> PROMOTE["晋升到 MEMORY.md"]
PROMOTE --> BUDGET["预算检查<br/>≤10,000 字符"]
BUDGET -->|超预算| DROP["删除最旧的<br/>自动晋升段落"]
BUDGET -->|在预算内| DONE["完成"]
Memory Budget Compaction(memory-budget.ts):当 MEMORY.md 增长超过预算时,按日期顺序删除最旧的自动晋升段落,但保留用户手写的内容。这与 Claude Code 的硬截断(超 200 行直接截断并加 WARNING)形成鲜明对比——OpenClaw 的策略更温和,优先淘汰旧内容而非暴力截断 [源码]。
Pre-Compaction Flush vs 同类机制
这是本文的核心对比——三种生产 Coding Agent 如何在 compaction 前保留关键信息:
| 维度 | OpenClaw flush | Claude Code extractMemories | Codex Phase1 |
|---|---|---|---|
| 触发时机 | compaction 前(主动) | 每次无工具响应后(被动) | root session 启动时(批量) |
| 与 compaction 关系 | 前置——flush 在 compaction 之前运行 | 并行——extract 和 compaction 独立 | 独立——Phase1 不关心 compaction |
| 执行方式 | 嵌入式 agent(静默轮次) | forked agent(fire-and-forget) | 并行 worker(最多 8 个) |
| 写入目标 | memory/YYYY-MM-DD.md(日记) |
~/.claude/projects/*/memory/topic.md |
SQLite stage1_outputs 表 |
| 模型选择 | 可配独立模型(如本地小模型) | 共享 prompt cache 的 Sonnet | reasoning=Low 的模型 |
| 静默性 | NO_REPLY token 抑制输出 |
fire-and-forget 不影响主流 | 异步后台 |
| 去重 | per-compaction 只 flush 一次 | hasMemoryWritesSince() 检查 |
冷却 6h per rollout |
| 内容过滤 | 只存”持久性记忆” | 四类型分类 + 排除可推导信息 | 过滤 developer 消息 + secret 脱敏 |
关键洞察:OpenClaw 的 flush 是唯一一个显式前置于 compaction 的方案。Claude Code 的 extractMemories 虽然也提取记忆,但它的触发与 compaction 无关——它在每次模型响应后触发,即使上下文远未满也会运行。Codex 的 Phase1 更是完全独立于 compaction,在 session 启动时回顾历史 rollout。
这意味着 OpenClaw 的设计动机最直接:”compaction 会删掉上下文,所以在删之前先保存”。而 Claude Code 和 Codex 的设计动机是”持续学习”,compaction 的信息损失是它们的副产品,不是主要驱动力。
与 DB-Agent-Memory 的集成
OpenClaw 是 DB-Agent-Memory 的三个 Adapter 之一(MCP、OpenClaw、Hermes)[已有]:
flowchart TB
subgraph openclaw["OpenClaw Agent"]
FLUSH["Flush Agent"]
DREAM["Dreaming"]
MEM_MD["MEMORY.md"]
DAILY["memory/YYYY-MM-DD.md"]
end
subgraph dbmem["DB-Agent-Memory"]
HOST["Memory Host"]
L0["L0 对话层"]
L1["L1 原子事实"]
L2["L2 场景块"]
L3["L3 用户画像"]
ADAPTER["OpenClaw Adapter"]
end
FLUSH --> DAILY
DREAM --> MEM_MD
openclaw -->|对话流| ADAPTER
ADAPTER --> HOST
HOST --> L0 --> L1 --> L2 --> L3
集成关键问题(犀牛鸟切入点)[源码][已有]:
- Flush 与金字塔的协同:OpenClaw flush 写入
memory/YYYY-MM-DD.md是平面文本,而 DB-Agent-Memory 有 L0→L3 的分层蒸馏。flush 内容应该从哪层注入?直接作为 L0?还是由 Host 判断层级? - MMD 注入与 flush prompt 的关系:DB-Agent-Memory 的 MMD(稳定区+动态区)注入和 OpenClaw 的 flush prompt 注入是否互相干扰?两者都在向 context window 写入记忆相关内容。
- Dreaming vs 金字塔提炼:OpenClaw 的 Dreaming(日记→MEMORY.md 晋升)与 DB-Agent-Memory 的 L1→L2→L3 提炼是并行还是冗余?
配置接口
OpenClaw 通过 agent 配置文件暴露 flush 参数 [源码]:
{
"compaction": {
"enabled": true,
"reserveTokens": 16384,
"keepRecentTokens": 20000,
"reserveTokensFloor": 20000,
"memoryFlush": {
"enabled": true,
"softThresholdTokens": 4000,
"forceFlushTranscriptBytes": "2mb",
"model": "ollama/qwen3:8b",
"prompt": "...",
"systemPrompt": "..."
}
}
}
设计亮点:flush 模型可以独立配置。对于本地部署场景,可以用小模型(如 qwen3:8b)做 flush 以降低成本和延迟,同时主 agent 使用强模型。这在 Claude Code 和 Codex 中没有对应——它们的记忆提取使用固定的模型策略(Claude Code 用 Sonnet,Codex 用 reasoning=Low) [源码]。
局限性
- Flush 质量依赖 prompt:flush prompt 决定了什么被保存,但”什么值得记”本身是主观判断——不同 prompt 设计会导致记忆质量差异显著
[源码]。 - 日记文件膨胀:
memory/YYYY-MM-DD.md只追加不删除,一天内多次 flush 可能导致同一天的日记文件过大。虽然有”不创建时间戳变体”的规则,但同一文件内仍可能有重复信息。 - Flush 与用户响应延迟:flush 是在用户消息处理流程中同步执行的(虽然对用户静默),可能增加响应延迟——特别是使用远程模型时
[源码]。 - MEMORY.md 预算固定:10,000 字符的预算是硬编码的,对于长期运行的复杂项目可能不够用。
- Dreaming 调度不透明:dreaming 的触发条件(何时将日记内容晋升到 MEMORY.md)在配置中不可调,用户无法控制晋升策略。
证据等级汇总
| 论点 | 证据等级 |
|---|---|
| Pre-compaction flush 架构和触发条件 | [源码] flush-plan.ts + memory-flush.ts |
| 嵌入式 agent 静默执行 | [源码] agent-runner-memory.ts |
| 三层文件布局(MEMORY.md / daily / DREAMS.md) | [源码] root-memory-files.ts + docs/concepts/memory.md |
| Dreaming 晋升机制 | [源码] dreaming-phases.ts + short-term-promotion.ts |
| Memory Budget 10,000 字符 | [源码] memory-budget.ts |
| 去重逻辑(per-compaction) | [源码] memory-flush.ts |
| Flush 模型可独立配置 | [源码] flush-plan.ts 配置 schema |
| 与 DB-Agent-Memory 集成 | [已有] deep-dive-tencentdb-agent-memory.md |
| 与 Claude Code extractMemories 对比 | [已有] deep-dive-claude-code-memory.md |
| 与 Codex Phase1 对比 | [已有] deep-dive-codex-memory.md |
与开源记忆项目对照
| 维度 | OpenClaw flush | mem0 | DB-Agent-Memory | Letta |
|---|---|---|---|---|
| 写入触发 | 上下文将满时 | API 调用时 | 对话流 | Agent 工具调用 |
| 写入策略 | 追加日记文件 | 扁平事实去重 | 四层蒸馏 | 自管理 Block |
| 遗忘/淘汰 | Budget compaction | 无 | 无(L0 可清理) | Agent 自行删除 |
| 与 compaction 关系 | 显式前置 | 不适用 | 不适用 | 不适用 |
| 多模型支持 | 可配独立 flush 模型 | 固定 | 固定 | 固定 |
| 文件 vs 数据库 | 文件(Markdown) | 数据库(向量) | 数据库(SQLite) | 数据库(Postgres) |