README.md

# cmdc_memory_pg

> CMDC PostgreSQL backend — Checkpoint + EpisodicMemory 持久化。

让 cmdc Agent 在 BEAM 节点重启 / 跨设备 / 跨进程的场景下,**完整保留对话上下文 + 情景记忆**。

## v0.1 范围(**严控二件套**)

| 模块 | 实现 behaviour | 用途 |
|---|---|---|
| `CMDCMemoryPg.CheckpointBackend` | `CMDC.Checkpoint.Backend` | Agent 会话快照持久化(`CMDC.checkpoint!/2` 后端) |
| `CMDCMemoryPg.EpisodicMemoryBackend` | `CMDC.Memory` | 情景记忆 few-shot 持久化(与 `Plugin.Builtin.EpisodicMemory` 对接) |

## v0.1 **明示不含**

- ❌ **pgvector 真语义检索** — `similarity_search/3` 降级为 ILIKE 文本匹配(与 ETS backend 同行为)
- ❌ **3-tier Memory**(Working / Semantic / Procedural)— 留 v0.2
- ❌ **Composite 路由 backend** — 见 cmdc 主库 `CMDC.Backend.Composite`
- ❌ **KV jsonb backend** — 留 v0.2
- ❌ **Cloak encryption 强制集成** — 给集成方留 `CMDC.Checkpoint.Snapshot.redact/2` hook,
   按需在 wrapper 层接 Cloak / KMS(详见 `CheckpointBackend` moduledoc)

## 安装

```elixir
defp deps do
  [
    {:cmdc, "~> 0.5"},
    {:cmdc_memory_pg, "~> 0.1"}
  ]
end
```

## 配置

```elixir
# config/runtime.exs
config :cmdc_memory_pg, CMDCMemoryPg.Repo,
  database: "cmdc_prod",
  username: System.fetch_env!("PGUSER"),
  password: System.fetch_env!("PGPASSWORD"),
  hostname: System.get_env("PGHOST", "localhost"),
  port: String.to_integer(System.get_env("PGPORT", "5432")),
  pool_size: 10

# 设为 CMDC.Checkpoint 默认 backend
config :cmdc, :checkpoint_backend, CMDCMemoryPg.CheckpointBackend
```

## 启动

```elixir
defmodule MyApp.Application do
  def start(_type, _args) do
    children = [
      CMDCMemoryPg.Repo,
      # ... 其他子进程
    ]
    Supervisor.start_link(children, strategy: :one_for_one)
  end
end
```

## Migration

```bash
$ mix ecto.create
$ mix ecto.migrate
```

migration 创建 2 张表:

| 表 | 用途 |
|---|---|
| `cmdc_checkpoints` | Snapshot bytea 存储(`:erlang.term_to_binary([:compressed])`)+ 索引 `(session_id, checkpoint_id)` |
| `cmdc_episodic_memories` | 情景记忆(按 `user_id` namespace 隔离)+ 索引 `(user_id, episode_id)` |

## 使用

### 1. Checkpoint 持久化

```elixir
# 抓快照
{:ok, snap} = CMDC.checkpoint!(session)

# 跨 BEAM 恢复
{:ok, snap} = CMDC.Checkpoint.load("sess-prod-001")
{:ok, new_session} = CMDC.resume_session!(snap)
```

无需指定 backend — 配置 `:cmdc, :checkpoint_backend` 后默认走 PG。

### 2. 情景记忆 few-shot

```elixir
# 配置 EpisodicMemory Plugin 用 PG backend
{:ok, session} =
  CMDC.create_agent(
    model: "anthropic:claude-sonnet-4-5",
    user_data: %{user_id: "alice"},
    plugins: [
      {CMDC.Plugin.Builtin.EpisodicMemory,
       memory_store: :ignored,
       memory_module: CMDCMemoryPg.EpisodicMemoryBackend}
    ]
  )

# 成功对话自动写入;下次同用户类似 query 自动 few-shot 加载
```

## 与 Cloak 集成(可选 encryption at rest)

cmdc 主库提供 `CMDC.Checkpoint.Snapshot.redact/2` helper — 集成方在 wrapper backend 层接 Cloak:

```elixir
defmodule MyApp.EncryptedCheckpointBackend do
  @behaviour CMDC.Checkpoint.Backend

  @impl true
  def save(sid, snap, opts) do
    sanitized = CMDC.Checkpoint.Snapshot.redact(snap, &MyApp.Vault.encrypt/1)
    CMDCMemoryPg.CheckpointBackend.save(sid, sanitized, opts)
  end

  @impl true
  def load(sid, opts) do
    case CMDCMemoryPg.CheckpointBackend.load(sid, opts) do
      {:ok, snap} ->
        decrypted = CMDC.Checkpoint.Snapshot.redact(snap, &MyApp.Vault.decrypt/1)
        {:ok, decrypted}

      other -> other
    end
  end

  # list / delete 透传
  defdelegate list(sid, opts), to: CMDCMemoryPg.CheckpointBackend
  defdelegate delete(sid, opts), to: CMDCMemoryPg.CheckpointBackend
end
```

## 测试

测试套件分两层:

- **单元测试** — 验证 backend 逻辑 / 序列化 / 路径解析等(不依赖真实 PG)
- **集成测试** — 真实 PG,需 docker:

```bash
$ docker compose up -d
$ mix ecto.setup
$ mix test --include pg
```

不带 `--include pg` 时跳过 PG 集成测,便于纯单元 CI。

## v0.2 路线图

| 项 | 优先级 | 说明 |
|---|---|---|
| pgvector embedding 检索 | P0 | 替换 ILIKE,让 `similarity_search/3` 真实语义匹配 |
| Working Memory backend | P1 | session 短期 KV 存储(区别于 Episodic 长期) |
| Composite 路由配方 | P1 | 在 cmdc 主库 `Backend.Composite` 之上提供推荐配置模板 |
| Cloak ecto_field encryption | P2 | 字段级加密预设(不强制) |

## License

Apache 2.0