# Agent Note: 加载全部指令候选并按目录去重

Status: implemented

[English](2026-07-21-instruction-load-all-dedup.md) | 中文

## 问题

[agent-instructions 插件](2026-06-24-workspace-context.zh.md)在每个目录中为每个候选列表只解析出一个胜出文件：`instructionFileCandidates` 中第一个存在的名字赢得基础槽位，[本地覆盖层](2026-07-21-local-instruction-overlay.zh.md)再追加一个胜出者。但 `AGENTS.md` 与 `CLAUDE.md` 经常共处同一目录。在多数仓库里其中一个是另一个的符号链接，因此内容完全相同；在迁移中的仓库里它们则是两个已经产生分歧的、彼此独立的真实文件。先到先得会悄悄丢弃未胜出的已提交文件，于是一个合理地携带两个不同指令文件的目录最终只暴露其中一个——而暴露哪一个取决于候选顺序，而非内容。需求是把两者都读取，仅在它们实质上是同一文件时才去重。

## 决策

每个列表中每个存在的候选都会被加载——先基础列表，再本地列表——按配置顺序进行。在同一目录内，内容在去除首尾空白后逐字节相同的候选会合并到该顺序中最靠前的候选，并渲染被保留文件的原始字节。去重是按目录进行的，而非全局，并且在基础列表与本地列表之间对称。比较前先做去空白处理，可以容忍某文件与其近似副本之间的末尾换行或缩进差异，同时仍逐字节渲染保留下来的文件——这正是需求所要求的「格外稳妥」的比较。

符号链接现在会统一经此流转。指令发现会解析每个候选并对其目标做 stat，而非拒绝末段的符号链接，因此一个符号链接指向其同级 `AGENTS.md` 的 `CLAUDE.md` 会解析到相同内容，并在此像任何逐字节相同的真实副本一样被合并。因此内容去重会通过与真实副本相同的路径把常见的符号链接镜像只渲染一次。[跟随符号链接说明](2026-07-21-follow-instruction-symlinks.zh.md) 拥有该反转决策及其残余的信任边界风险。

## scope 键改为按候选划分

现在每个 `(directory, candidateName)` 对都是各自独立的逻辑 scope，编码为 `directory\u0000candidateName`，其中 NUL 分隔符在真实路径中不可能出现。`candidateScopeKey` / `decodeScopeKey` 负责这套编码，`probeScopeInstruction` 则解码候选名以精确读取该文件。这取代了覆盖层 note 引入的层级哨兵 scope 键：一个目录不再有「基础 scope」和「本地 scope」，而是每个候选名一个 scope，因此同一目录中的 `AGENTS.md` 与 `CLAUDE.md` 是各自独立协调的 scope。

由于一个 scope 现在只对应一个固定文件，此前的「同一 scope 内的候选切换」——即一个 `AGENTS.md` scope 回退到 `CLAUDE.md` 并把旧名字记录在 `previousPath` 中——不再可能发生。`previousPath` 已从变更记录、序列化的 `context/message` 元数据以及渲染文本中移除；一次变更现在要么是 `set`、要么是同一文件的 `replace`、要么是 `remove`。移除某个候选会为该候选自己的 scope 发出一个 `remove`，而把不同的同级文件留作独立的 scope。

去重在协调过程中强制执行，而不仅仅在基线组合时。每一轮协调都会按候选顺序重建一个按目录的「已保留去空白摘要」集合，因此当更靠前的候选收敛到某文件的内容时，一个未变更的文件也会被移除，而新出现的重复同级文件会被丢弃或移除。版本缓存在完整内容摘要之外还存储一个 `trimmedDigest`，使快速路径无需重新读取内容即可重新判定是否重复。

## 备选方案

**每个候选列表保持先到先得。** 否决：这会悄悄丢弃一个目录的第二个已提交指令文件，并使胜出者取决于候选顺序、而非文件是否真的不同，而这恰恰是需求要消除的意外。

**全局的、跨目录的去重。** 否决：两个不同目录下相同的样板内容对各自而言都合理地在作用域内，而更深层的文件对于该更深目录下的工作仍必须暴露。跨目录合并会隐藏模型本应看到的指令。

**不做去空白、直接比较原始字节。** 否决：一个添加末尾换行的编辑器，或一个重排缩进的副本，都会让实质相同的文件无法去重。比较前去空白正是需求所要求的宽容键，而保留下来的文件仍渲染其原始字节。

**跟随符号链接，从而让镜像通过内容去重。** 为本次改动否决以保留「不跟随」不变式，随后另行采纳：[跟随符号链接说明](2026-07-21-follow-instruction-symlinks.zh.md) 反转了该不变式，此后符号链接镜像会被解析，并像真实副本一样通过内容去重。

## 后果

一个携带两个不同真实指令文件的目录现在会把两者都暴露；一个第二个文件仅仅是镜像的目录仍只渲染一次，而无处不在的符号链接场景保持不变。可见的行为差异被限定在携带两个不同真实文件的迁移期仓库中。scope 键的形态从层级哨兵改为按候选划分，`previousPath` 也从持久化的变更元数据中消失；`dsh-session` 对旧会话不作兼容承诺，因此两者都是无成本的改动。版本缓存行新增了一个 `trimmedDigest` 字段，协调过程现在按目录比较去空白后的内容，因此一个未变更的文件可以被同级文件的收敛所移除——这是[状态模型](2026-06-24-workspace-context.zh.md)此前无法产生的转换。
