# Agent Note: 将 acp-agent 回放配置改为单一来源

Status: implemented
Archived: 2026-07-26

[English](2026-07-04-single-source-acp-replay-config.md) | 中文

## 问题

`examples/acp-agent` 发布了两份手工维护的配置：`cordis.yml`（实时树）和逐条镜像它、只替换 llm 后端的 `cordis.snapshot.yml`——去除注释后，两者的全部差异就是八行 `llm-deepseek` stanza 与两行 `llm-replay` stanza。每次应用形状变化都必须修改两遍，也没有任何机制约束对称性：如果副本发生漂移，快照层会悄然覆盖与已发布应用不同的应用——快照层本就是为了弥合[“单元测试绿色，产品损坏”这类缺口](../../../../docs/postmortem/0001-acp-default-export-drops-inject.md)，如今同类缺口在上一层重新出现，只能依靠评审者警惕。

## 决策

`cordis.snapshot.yml` include 正式配置，通过 id 和 name 禁用指定的 DeepSeek 适配器，并插入回放适配器。其余所有条目因此来自正式运行树。回放时选择 overlay；录制仍然启动 `cordis.yml`，加载守卫允许被有意禁用的条目。

overlay 有意依赖一项 vendored 插件事实：include 加载文件时会应用 `patches`，而其 `refresh()`/`internal/update` 路径会重新读取但不重新打补丁——这恰好足以满足一次性重放启动（重放应用不加载 `hmr`，运行中也没有内容重写配置）。快照套件就是证明：所有场景都能在 overlay 上原样通过，包括逐字节相同的预期输出。

## 曾考虑的替代方案

### 为何不采用这些替代方案？

保留完整的双副本并加一道对称性校验门禁是记录在案的退路——它能消除静默漂移这一类问题，但仍保留一份 125 行的近乎复制品，其全部内容只是一个条目的差异，且随应用每增加一个插件而增长。在 bin 侧做替换（解析配置、替换条目、删除文件）则会把 YAML 手术放进发布产物，并把回放差异藏到视线之外；overlay 让差异保持声明式、可读，且紧邻基础配置——这正是双副本支持者真正看重的教学价值。

## 后果

- 向 `cordis.yml` 添加插件即自动进入回放树，无需第二次编辑；漂移这一类问题从结构上消失，而非靠门禁拦截。
- overlay 依赖条目携带稳定的 `id:`。禁用补丁上的 `name` 断言防止误定位（id 被复用时补丁跳过而非禁用错误的插件）。如果 id 被重命名，补丁退化为跳过，其警告需要一个回放应用有意不具备的 logger——可观测结果是一条无效的无密钥 `llm-deepseek` 条目与 `llm-replay` 并存，回放输出仍然正确（`llm-replay` 拥有流的短路权）；这属于配置腐烂，留给评审发现，不会产生错误的快照。顶层插入一个 id 与既有条目冲突的新条目时，loader 的 id map 以后者为准；当前配置无冲突，新增补丁行才是引入冲突的场所。
- 如果未来回放树需要第二处差异（另一个后端被替换），只需多加一行补丁，而非再 fork 一份文件。
