# `CMDC.Plugin.Builtin.MemoryFlush`
[🔗](https://github.com/tupleyun/cmdc/blob/v0.5.3/lib/cmdc/plugin/builtin/memory_flush.ex#L1)

压缩前持久化关键事实插件。

在 `Compactor` 触发前将即将被压缩丢掉的消息里的关键事实**提取并追加**到
`working_dir/MEMORY.md`，下一次会话由 `MemoryLoader` 自动加载回 system prompt，
实现"长会话不失忆"。

## 工作流程

1. 订阅 `:before_compact` 事件（Agent 在 compact 触发前 emit）
2. 调用 `extract_fn.(messages, opts)`（2 元）或 `extract_fn.(messages, ctx, opts)`
   （3 元）从待压缩消息里提取 N 条关键事实。
   3 元签名让提取器拿到 `ctx.user_data` 等业务标识，便于按租户路由不同 LLM。
   - 默认内置启发式 `default_extract/2`（无 LLM 依赖，完全离线可测）
   - 可覆盖为真实 LLM 提取函数（签名：`([Message.t()], keyword()) -> {:ok, [String.t()]} | {:error, any}`）
3. 用 `:crypto.hash(:sha256, fact)` 去重（与已写 facts 比对）
4. 追加到 `working_dir/MEMORY.md` 末尾，格式为 `- fact_text`
5. emit 内部事件 `{:memory_flushed, %{facts, count, session_id}}`
6. emit `{:plugin_event, :memory_flush, payload}`
   便于集成方订阅并持久化到数据库 / 长期记忆系统

## 配置

    {CMDC.Plugin.Builtin.MemoryFlush,
      file: "MEMORY.md",            # 持久化目标文件（相对 working_dir）
      max_facts_per_flush: 10,      # 单次 flush 最多提取多少条
      extract_fn: &MyApp.extract/2, # 自定义提取器（返回 {:ok, [fact]} | {:error, _}）
      dedupe: true                  # 是否 sha256 去重（默认 true）
    }

## 失败降级

`extract_fn` 抛异常 / 返回 `{:error, _}` 时：

- Plugin 记录 `Logger.warning`
- 不阻塞 compact（返回 `:continue`）
- 不写文件

保证"MemoryFlush 出问题也不影响主 Agent Loop"。

## 与 MemoryLoader 闭环

    # 第一次会话
    opts = [
      plugins: [
        CMDC.Plugin.Builtin.MemoryLoader,
        CMDC.Plugin.Builtin.MemoryFlush
      ],
      working_dir: "/project/path"
    ]
    # 长对话 → 触发 compact → MemoryFlush 追加 facts 到 MEMORY.md

    # 第二次会话（新进程）
    # MemoryLoader 自动读取 MEMORY.md 并注入 <agent_memory> 到 system prompt
    # → Agent 仍然记得第一次的关键事实

## emit 事件协议

- `{:memory_flushed, %{facts: [String.t()], count: pos_integer, session_id: String.t()}}`
- `{:plugin_event, %{kind: :memory_flush, facts, count, session_id, occurred_at, file, v: 1}}`

# `default_extract`

```elixir
@spec default_extract(
  [CMDC.Message.t()],
  keyword()
) :: {:ok, [String.t()]}
```

默认事实提取器：从消息列表里抽启发式关键句子。

**不依赖 LLM**，只做简单规则匹配，方便单元测试。生产建议覆盖：

    extract_fn: fn messages, opts ->
      ReqLLM.chat(
        model: "anthropic:claude-haiku-4",
        messages: messages,
        system: opts[:extract_prompt]
      )
    end

启发式规则：

1. 只看 role 为 `:user` 的消息（用户显式声明的事实最重要）
2. 按换行切分，保留非空行
3. 匹配关键字模式：
   - 以"记住 | remember | 注意 | 约定 | 以后"开头
   - 含"必须 | 一定 | always | never | must"
   - 长度 20-200 字符（过短无信息量，过长是完整指令不是 fact）

---

*Consult [api-reference.md](api-reference.md) for complete listing*
