DSH / Atlas
2026-07-04implementedtestingarchived 2026-07-26

Single-source the acp-agent replay config

将 acp-agent 回放配置改为单一来源

`examples/acp-agent` shipped two hand-maintained configs: `cordis.yml` (the live tree) and a `cordis.snapshot.yml` that mirrored it entry-for-entry with only the llm backend swapped — stripped of comments, the entire difference was the eight-line `llm-deepseek` stanza versus the two-line `llm-replay` stanza. Every app-shape change had to be made twice, and nothing gated the symmetry: if the copies drifted, the snapsh

English

Problem

examples/acp-agent shipped two hand-maintained configs: cordis.yml (the live tree) and a cordis.snapshot.yml that mirrored it entry-for-entry with only the llm backend swapped — stripped of comments, the entire difference was the eight-line llm-deepseek stanza versus the two-line llm-replay stanza. Every app-shape change had to be made twice, and nothing gated the symmetry: if the copies drifted, the snapshot tier would silently exercise a different app than the one that ships — the "green units, broken product" class of gap the snapshot tier exists to close, reintroduced one level up, with reviewer vigilance as the only defense.

Decision

cordis.snapshot.yml includes the live config, disables the named DeepSeek adapter by id and name, and inserts the replay adapter. Every other entry therefore comes from the shipping tree. Replay selects the overlay; recording still boots cordis.yml, and the load guard permits the intentionally disabled entry.

One vendored-plugin fact the overlay depends on, deliberately: the include applies patches when it loads the file — its refresh()/internal/update paths re-read without re-patching — which is exactly enough for a one-shot replay boot (the replay app loads no hmr and nothing rewrites the config mid-run). The snapshot suite is the proof: all scenarios pass unchanged on the overlay, byte-identical expected outputs included.

Alternatives considered

Why not the alternatives?

Keeping the full twin with a symmetry verify-gate was the recorded fallback — it would have removed the silent-drift class but kept a 125-line near-copy whose only content was one entry's difference, growing with every plugin the app gains. A bin-side swap (parse the config, replace the entry, delete the file) would have put YAML surgery inside a published artifact and moved the replay delta out of sight; the overlay keeps the delta declarative, readable, and next to the base config — the teaching value the twin's defenders actually wanted.

Consequences

  • A plugin added to cordis.yml is in the replay tree with no second edit; the drift class is structurally gone rather than gated.
  • The overlay depends on entries carrying stable id:s. The name assertion on the disable patch guards mis-targeting (a reused id skips the patch instead of disabling the wrong plugin). An id RENAME degrades the patch to a skip whose warning needs a logger the replay app deliberately lacks — the observable result is a futile keyless llm-deepseek entry alongside llm-replay, with replay output still correct (llm-replay owns the stream short-circuit); config rot for review to catch, not wrong snapshots. A top-level insert whose id collides with an existing entry resolves last-wins through the loader's id map — the current config has no collision, and a new patch line is where one would be introduced.
  • If a future replay tree needs a second divergence (another backend swapped), it is one more patch line, not a second fork of the file.

中文

问题

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

决策

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 一份文件。