# 升级指南

本章只列**用户实际会被坑**的兼容边界——不是新增的可选字段，是**写错代码会报错或行为变化**的位置。
新增字段、新增可选参数、文档改写等"无影响"的变化请看 [Changelog](changelog.html)。

---

## v0.4.0 → v0.4.1（当前版本）

**纯文档 patch**，零代码 / 零行为变更。

- 主库 `mix.exs` extras 重组、内核模块加 `@moduledoc false`、清理 hex doc 内的内部任务编号
- 1219 tests + 21 doctests 不变，credo 与 v0.4.0 基线完全一致
- 子库 `cmdc_gateway` 的 0.4.1 见其自己的 [CHANGELOG](https://hex.pm/packages/cmdc_gateway)

**无升级动作**。直接改 `mix.exs` 依赖到 `~> 0.4.1` 即可。

---

## v0.3.x → v0.4.0

### 新增公共模块（向后兼容，纯增量）

- `CMDC.Backend` behaviour + `Backend.{State, Filesystem, Composite}` 三个内置实现
- `CMDC.Checkpoint` + `Checkpoint.Backend` behaviour + ETS / DETS 两个内置 backend
- `CMDC.Telemetry` 标准 `:telemetry` 事件契约
- 4 个新内置 Plugin：`LargeResultOffload` / `ContentPolicy` / `EpisodicMemory` / `MemoryFlush`

旧代码不需要改任何东西就能拿到这些能力——按需挂载即可。

### Plugin Pipeline 新 action `:replace_tool_result`

仅 `:after_tool` hook 接受。让 plugin 在 raw_result 写进 message history
**之前**替换它（`LargeResultOffload` 用这个 action 把 200KB 结果换成
preview）。

无破坏性影响——旧 plugin 完全不受影响。

### `HumanApproval` 加 `:approve_always` 第三态

```elixir
# v0.3：approve(session, id) 只放当次
CMDC.approve(session, id)

# v0.4：可加 :kind 走 session-scoped 永久白名单
CMDC.approve(session, id, kind: :approve_always)
```

旧代码 `CMDC.approve(session, id)` 默认 `:approve_once`，行为不变。

### Sandbox `virtual_mode`（推荐生产开启）

`Backend.Filesystem.new/1` 支持 `:virtual_mode` 选项：

```elixir
# v0.3 行为（默认 false，0 安全保护）
backend = CMDC.Backend.Filesystem.new(root_dir: "/tmp/work")

# v0.4 推荐生产配置
backend = CMDC.Backend.Filesystem.new(
  root_dir: "/tmp/work",
  virtual_mode: true     # 拦 .. / ~ traversal + O_NOFOLLOW symlink 防护
)
```

`virtual_mode: true` 会拒绝路径逃逸 `root_dir`，**有可能**让原本依赖
绝对路径的代码失败。如果你的 Agent 之前依赖访问 `root_dir` 外的文件
（不推荐），开启前要先改造业务代码。

未来 v0.5 默认值会切换为 `true`，到时是 breaking change。

---

## v0.2.x → v0.3.0（**1 条 breaking change**）

### #1 公共 API 全部改 `{:ok, _} | {:error, _}` 返回（**breaking**）

v0.2 的 `CMDC.monitor` / `abort` / `attach_tool` / `detach_tool` /
`status` / `messages` / `agent_pid` / `steer` / `stop` / `switch_model` /
`replace_tools` / `attach_tools` / `detach_tools` 在传非法 session 时**会 raise**。

v0.3 起改为返回 `{:error, :invalid_session | :not_alive}`。

**迁移**：把所有 `CMDC.xxx(session, ...)` 改为 with 链或 case 匹配：

```elixir
# v0.2 写法（v0.3 仍能跑，但只在 session 合法时工作）
CMDC.attach_tool(session, MyTool)

# v0.3 推荐写法
case CMDC.attach_tool(session, MyTool) do
  {:ok, _name} -> :ok
  {:error, :invalid_session} -> handle_dead_session()
  {:error, {:validation_failed, failures}} -> handle_invalid_tool(failures)
end
```

成功路径的返回值（`:ok` / map / Message struct 等）保持不变，所以多数业务
代码只需要在最外层套个 `case` 即可。

### `abort/2` `:reason` 接受 string

v0.2 只接受 atom，v0.3 起接受下列 6 个标准 string 自动归一为 atom，防止
前端通过 JSON 反序列化注入任意 atom 进 BEAM atom table：

```
"user_cancelled" / "timeout" / "shutdown" /
"budget_exceeded" / "permission_denied" / "provider_error"
```

其他 string 一律归并为 `:unknown` 并 `Logger.warning`。Atom 入参保持原样
透传。

### `pending_tools` 加 `started_at_ms` 字段

`CMDC.status/1` 返回的 `pending_tools` 列表每项新增 `started_at_ms` 字段
（`System.system_time(:millisecond)`）。如果你之前对 pending_tools 做了
strict map match `%{name: _, call_id: _, args: _}`，会失败：

```elixir
# v0.2 strict match
%{name: name, call_id: id, args: args} = tool

# v0.3 兼容写法
%{name: name, call_id: id, args: args, started_at_ms: _} = tool
# 或更稳妥：
name = tool.name
```

### Plugin emit 自动注入 user_data

emit 出来的 `{:plugin_event, name, payload}` 当 payload 是 map 时，Pipeline
会自动 merge `state.user_data` 到 `:user_data` 字段。如果你的订阅方做了
strict map match 不期待 `:user_data`，要么改 match，要么在 plugin 给
payload 加 `:_no_user_data` opt out。

### `:after_turn` 新 hook

新增 `{:after_turn, payload}` Plugin hook，每 turn 回 idle 前触发（finish
+ abort 双路径）。比 `:session_end` 触发更频繁，**新 plugin 推荐用它**写
审计 / 长期记忆 / 计费等。

旧 plugin 不受影响。

### `attach_tools / detach_tools / replace_tools` 批量原子 API

新增三个批量 API。dry-run + 全回滚语义：任一失败全部不动。**单次的
`attach_tool/2` / `detach_tool/2` 行为完全不变**，可继续用。

### EventBus replay 加 `:types` 白名单

`subscribe/2` 加 `:types` 选项：

```elixir
# v0.3 起：只 replay stream / agent_end 类事件
{:ok, _} = CMDC.subscribe(session, since: 100, types: [:message_delta, :agent_end])
```

`:since` 一直存在；`:types` 是新增可选项。

---

## v0.1.x → v0.2.0

### `{:agent_end, messages, token_usage}` 第三参数改 struct

v0.1.x 的 `token_usage` 是 plain map（`%{prompt_tokens, completion_tokens, ...}`），
v0.2 起统一为 `%CMDC.TokenUsage{}` struct：

```elixir
# v0.1.x 兼容写法
%{total_tokens: tt} = usage

# v0.2 推荐写法（也兼容 v0.1 字段名）
%CMDC.TokenUsage{total_tokens: tt, cost_usd: cost} = usage
```

字段名保留 `prompt_tokens / completion_tokens / total_tokens`（OpenAI 行业
事实标准），同时归一化 Anthropic 风格的 `input_tokens` / `output_tokens`。

### Steering 软中断（新功能）

新增 `CMDC.steer/2` 公开 API + 3 个新事件 `:steering_received` /
`:steering_applied` / `:tool_skipped_for_steering`。

### SubAgent `prompt_mode` 默认改 `:task`

v0.1 子代理用完整 BasePrompt（同主 Agent）。v0.2 起 SubAgent 默认 `prompt_mode:
:task`（精简），节省 30-50% system prompt token。

如果你的子代理依赖完整 BasePrompt 行为：

```elixir
%CMDC.SubAgent{
  name: "...",
  prompt_mode: :full  # 显式指定回 v0.1 行为
}
```

### MemoryFlush Plugin（新功能）

新增 `CMDC.Plugin.Builtin.MemoryFlush`，在压缩前把关键事实持久化到
`MEMORY.md`，下次会话由 `MemoryLoader` 自动加载回 system prompt——解决
长会话失忆问题。

### Ring Buffer + replay（新功能）

`Options.event_buffer_size > 0` 启用 per-session 事件 ring buffer，
`subscribe(session, since: idx)` 重连补帧。默认 `0` = 关闭，零内存开销。

---

## 完整变更摘要

各版本完整 changelog 见仓库 [CHANGELOG.md](https://github.com/tuplehq/cmdc/blob/main/CHANGELOG.md)。
