# Agent Note: Skill 目录热刷新

Status: implemented

[English](2026-07-27-skill-catalog-hot-refresh.md) | 中文

## 问题

skill（技能）摘要是模型的路由输入，但本地 skill 可在会话启动后新增、消失或重命名。IDE、Git 操作、shell 命令和其他进程都可以修改 `.agents/skills`，而不经过 harness 文件系统工具。仅在启动时构建目录，会让模型无法获知新 skill，并且仍能调用已删除的名称。反之，如果把每次指令正文编辑都视为目录修订，就会让渐进式加载与不必要的提示词频繁变化耦合。

从观察方来看，文件系统更新也不是原子完成的。编辑器或 Git 操作可能会短暂移除文件，受监视的根目录在启动时可能不存在，发现也可能暂时失败。把这些中间观察结果发布为权威空目录，比保留最后一个完整视图更糟。

## 决策

skill 服务将目录成员关系与指令正文加载分离。`ctx.skills.snapshot()` 返回摘要及一个完整性位。`ctx.skills.registerProvider(factory)` 会向同步工厂提供一项注册作用域内的 `{ signal, invalidate }` 控制能力：`invalidate()` 只会将该精确活动注册标记为脏，并丢弃已完成目录缓存；注册失败或释放时，信号会中止。提供方返回的数组是完整发现的简写形式；显式的不完整观测可以保留可读候选项供直接加载，但不能缓存，也不能作为面向模型消费方的权威结果。在发现期间，如果提供方或运行时 generation 发生变化，系统会重试一次；如果这次重试也被后续修订取代，则最新候选项会作为不完整且不缓存的观测返回。资源释放或替换后的延迟失效操作不会执行任何操作，因为该能力已被撤销。

`@deepseek-ai/dsh-skill-local` 直接依赖 Chokidar，并观察与目录相关的宿主路径。已有根目录会监视其直属 skill bundle 目录、平铺的 Markdown 条目和直属 `SKILL.md` 条目文件。新增、移除和目录变更会使成员关系失效；文件变更还支持刷新 frontmatter 中的 `name` 和 `description`。bundle 内更深层的资源文件会被忽略。同一微任务批次中的事件会合并为一次失效。项目 watcher 使用有界集合，并按最久未观察顺序淘汰。

系统从缺失根目录最近的现有祖先开始，使用 `fs.watchFile` 每次跟进一层缺失路径片段；真实根目录出现后，再交给 Chokidar。每次发现操作都会在扫描前重新探测所保留的根目录／祖先模式。即使子项移除在根目录 `unlinkDir` 事件之前就触发失效并发布权威空目录，或者该事件根本没有到达，这项独立探测也会在删除后重新建立祖先观察。Chokidar 配置公开原生事件或轮询模式、写入稳定性、轮询间隔、符号链接跟随选项和项目 watcher 容量。第一方 `write` 和 `edit` 工具观察会同步使相关提供方失效，因此下一个模型步骤无需等待宿主事件投递，就能看到自身改动。watcher 启动或运行失败会被记录并触发重试；发现过程仍会返回可读候选项供直接加载，但会报告不完整观测。资源销毁会关闭 watcher，并忽略延迟回调。

`@deepseek-ai/dsh-tool-skill` 在 `agent/step` 首次观察到非空完整目录时，将该目录注入为一条持久且带来源的 `user/message`。每次 `agent/step`，它都会应用 `skill` 工具的精确可见性，对 `<available_skills>` 标签之间精确渲染的文本计算哈希，并从后向前扫描只读会话事件且不复制，以查找该插件发布的最新一条可识别且仍可见的目录。digest 变化时，插件通过 `agent.inject()` 追加一份持久的完整替换目录；所有 skill 消失时，也会追加显式空目录。如果没有目录仍然可见，但历史事件中存在可识别目录，则说明压缩（compaction）已将其遮蔽，下一次完整观察会重新建立当前目录，包括空 tombstone。如果当前目录为空且历史上从未发布目录，则不发送任何内容；不完整快照则保留最后一次完整的模型视图。反向扫描通常在最新且仍可见的目录处停止；当压缩遮蔽所有目录时，它会以一次 O(session-events) 扫描的成本确认这一事实。

TUI 将同一失效通知作为界面状态而非会话历史来消费。`skills/change` 不携带 diff；TUI 会为活动会话的 cwd 重新获取 `snapshot()`，仅应用最新的完整结果，并在观测不完整时保留先前命令。完整的空结果会清除陈旧补全项。pi-tui 在其提供方被替换时会关闭自动补全，因此如果目录在用户输入斜杠命令名称期间到达，还会触发一次仅用于更新建议的当前草稿重查。

指令正文继续采用渐进式披露。每次调用 `skill(name)` 时，系统都会要求提供方重新读取并解析当前文件；不存在正文缓存、哈希、修订或主动通知。先前记录的工具结果保持不变。如果加载后的 frontmatter 名称不再匹配所选候选项，注册表会拒绝这个陈旧名称，并使该提供方失效，以便后续目录观察发布新名称。

## 验证

注册表测试固定了注册作用域内的失效、能力撤销、信号中止、监听器失败隔离、不完整候选项、有界 generation 重试和陈旧名称拒绝。local-provider 测试覆盖 bundle 与平铺文件的创建、移除和重命名，以及根目录创建／删除／重建（包括未观测到根目录 `unlinkDir` 事件的情形）、描述变更、仅正文编辑、第一方观察、符号链接、轮询选项、候选项仍可加载的持续 watcher 失败、事件合并、项目 watcher 容量上限、资源销毁和暂时读取。工具测试固定了完整替换消息、空 tombstone、仅修改正文时 digest 稳定、不完整状态保留、可见性和恢复元数据。TUI 测试固定了上一份完整结果保留、权威空结果清除、刷新时以最新结果为准、资源销毁和已打开斜杠草稿的竞态；一项使用真实 Loader/PTY 的 smoke 测试会在启动后添加本地 skill，并观察其补全项出现，而无需重启。一个无密钥、装配完成的 agent-spine 快照测试通过面向模型的文件系统工具创建项目 skill，观察下一次请求中的替换目录，并使用真实 `skill` 工具加载当前正文。

## 考虑过的替代方案

- **将实时目录放入 World State**：不予采纳，因为目录替换是模型可见的会话输入，必须能够从事件日志重建。持久注入历史已经提供回放、恢复、fork 和压缩语义，无需再引入一套可变状态层。
- **只依赖 `fs/observed`**：不予采纳，因为 IDE、Git、shell 命令和外部进程都不会经过该 seam。该事件仍作为第一方工具的低延迟快速路径，宿主监视则补齐覆盖。
- **为每个 `SKILL.md` 正文计算哈希或版本**：不予采纳，因为模型最初只看到名称和描述，提供方已经在每次工具调用时重新读取正文。正文修订会产生目录流量，却不会改变路由，也不足以成为改写历史工具结果的理由。
- **监视每个 bundle 资源**：不予采纳，因为参考资料、脚本和产物都是按需加载的，不影响类别列表。宽泛的递归监视会增加失效、描述符压力和平台差异，却不能改善路由。
- **将部分发现或失败发现发布为新目录**：不予采纳，因为暂时读取失败不能证明文件已删除。完整性位让面向模型的消费方保留最后一次完整目录，直到完整观察成功。
- **保留公开的 `invalidateProvider(provider)`**：不予采纳，因为这会公开一项注册表变更方法，并要求调用方重复提供注册表已经持有的身份。发给工厂的闭包会将失效绑定到单个注册，并在释放后失去作用，因此观察方既不需要访问注册表，也不需要提供方身份。
- **现在提取通用 Cordis 文件监视服务**：暂缓，直到另一个消费方确立可复用的服务契约。本地提供方标出了其 Chokidar 和缺失根目录观测边界，以便后续提取；skill 路径过滤以及对提供方失效闭包的调用仍属于 skill 专用逻辑。

## 影响

- 新增、删除和重命名的本地 skill 会在模型步骤边界变得可见，无需重启 agent（智能体），即使 skill 根目录在启动时不存在也一样。
- TUI 的 `/skill:` 补全会收敛到同一份完整目录，而不会让每次按键都阻塞于发现；打开的斜杠命令名称草稿会在目录到达时刷新。
- 目录消息采用仅追加、日志记录和全量列表快照。它们会保留较早的可重用 token；替换目录会显式停用陈旧名称，每次 digest 实际变化时，token 成本与当前目录大小成正比。
- 仅修改正文不会产生目录消息。后续工具调用会看到当前内容，而先前工具结果仍准确记录模型之前加载的内容。
- 缺失根目录轮询和 Chokidar 引入一个有人维护的运行时依赖、宿主 watcher 资源、有界检测延迟和部署可调参数。有界项目集合与资源销毁契约会限制这些成本。
- 远程或未来的可变提供方会保留各自注册作用域内的失效闭包，并通过自身观察机制调用它；注册表不会强制采用通用 watcher 或 TTL。
