Files
deepseek-harness/.agents/notes/implemented/bug-fix/2026-08-09-broken-preset-roster-rows.zh.md
T
Yichen Jiang 0b3ac6356b fix(preset): correct the composition-authoring skill and give it a real check
The `cordis` preset's `editing-cordis-compositions` skill is the only guidance
an agent has when it authors a preset, and four of its statements were false.

`tool-bash` was named as the worked example of a row that hides a service; it
provides nothing and injects `bashEnv` from the host's own `bash-env` row, so
following that advice strands the row behind its realm and the preset fails to
mount. The `isolate` example composed `tasks-local` with `tool-tasks`, which
the shipped compositions' own comments say breaks `run_in_background`. A string
realm label was described as pooling one instance; labels join realms and
`provide()` still throws on the second registration. Rows were to be checked
against a package README, which no harness package publishes.

Verification is now the agent's own: `standingKeyFor(id)` runs the same mount a
session start performs and rejects an unresolvable package, an invalid config, a
service in the root realm, and a row that never activated. The skill states that
`list()`'s `broken` field is a shape check that every one of those passes, ships
the `cordis_mount` plugin that reaches the roster service, and names `copy()` as
the authoring write. The prohibition on touching the shipped install is promoted
to its own section and extended to the host composition.

Fixes #2266
2026-08-11 17:43:09 +08:00

5.1 KiB
Raw Blame History

Agent Note:损坏的 preset 是名单行,不是空缺

Status: implemented

English | 中文

问题

文件成为唯一的组装编辑器之后,手动编辑造成的损坏有两种形态,且都要拖到最糟的时刻才暴露。agent.cordis.yml 解析不了的 preset 在名单上是一张完全正常的行——可选择、可复制、可设为默认——直到下一个会话尝试挂载才失败;一旦被设为默认,所有新会话都无法启动。组装文件被整个删掉的目录则从名单上消失,却仍在磁盘上占着它的 id:copy 以「先删除既有 preset」拒绝这个名字,remove 却回答「找不到」——两条互相矛盾的错误,除了手动删目录别无出路。

决定

发现过程负责健康,受损目录是携带 broken 原因的名单行,绝不是空缺。scanRoot 把名字是可用 preset id 的每个目录都当作一个 preset 槽位:组装缺失 → broken(「仍占着该 id;删除目录或恢复文件」),组装不可读/解析失败/不是具名行列表 → broken 并携带解析器的首行。形状检查用加载器自己的 entryListSchema(含 !!js 的方言)解析,因此健康检查绝不会把加载器接受的组装叫作损坏;名字不符合 PRESET_ID 的目录直接跳过,因为复制永远不可能与之相撞。broken 依次落在 AgentPreset、agentPreset.list 的线上条目和 UI 行上。挂载路径(mount/recompose/standingKeyFor)经 resolveMountable 用发现时记下的原因在前置拒绝;resolve 照样应答(删除/读取/上报都需要这一行),而 copy 的名单检查现在看得见幽灵,让「已存在」的拒绝变得可操作——要删的损坏卡片就在同一页上。

界面按职责分开:管理区把损坏行渲染为标记卡片(红边、「已损坏」徽记、原样展示原因、卡片主体与复制禁用,自定义行保留位置与删除——文件正是修复处,删除正是幽灵的出路;损坏的内置行连查看器也不给),而两个选择器(通用设置行、新会话 chip)经 presetOptions 完全不列损坏的 preset——它们选的是下一个会话的组装,端出无法组装的选项只会推迟失败。

后果

  • 幽灵死路端到端消除:目录以损坏行列出,删除即清掉,释放的 id 立刻可用(单测、组件测试与 e2e 各自覆盖)。
  • 事后才损坏的默认值仍会在会话启动处大声失败——选择器隐藏损坏行,但没有任何东西改写已存的默认;resolveMountable 的前置拒绝让每种不可加载形态得到同一条消息,而不是依赖加载器内部的报错。
  • 健康检查随每次 list() 运行:每次读名单对每个 preset 一次读取加解析,接受的理由与不做缓存的发现相同——名单很小,新鲜是契约。
  • 复制损坏 preset 只在 UI 层拒绝(按钮禁用并给出原因);宿主的 copy 保持形状无关。损坏来源产出同样损坏、同样可见的副本——没有能力增益,而宿主侧拒绝需要为一条被禁用按钮挡住的路径专门发明错误词汇。

关键细节

  • PRESET_ID 移到 types.ts,让发现与创作共享同一份包含边界词汇;authoring 原样转发导出。
  • 原因只留一行。 js-yaml 会附上多行代码框摘录;名单卡片不是终端,compositionProblem 只保留首行。
  • mount.spec 的两个竞态用例特意不动:ensureStanding 仍可能拿到删除前一刻解析出的 preset(私有路径测试),其 stamp/unstampable 语义不变——健康检查发生在此之前的公开路径上。
  • 创造模式的引导随同一 PR 落地:cordis preset 的 persona 禁止编辑随附安装(损坏 cordis 会禁用这一模式本身),并把创作指向 ${DSH_HOME:-$HOME/.dsh}/.agent-presets/<id>/;其技能教了 preset.yml 元信息、先复制再改的流程与一次升级的沙箱现实(preset 根目录在会话工作区之外)。已实测:被要求直接改随附 cordis 组装时,组装出的 agent 援引两条规则拒绝并给出复制路径;被要求真正创建 preset 时,它落在 $DSH_HOME 下并把写入合并为一次升级。该引导中关于验证的那一半——agent 无法自己启动会话,因而设置页的红色标记是用户的检查项——已由创作 preset 的 agent 自行挂载校验其组装取代:下文的结构检查不是校验,而 standingKeyFor 才给了 agent 真正的校验手段。本篇的健康检查决策不变。

曾考虑的替代方案

隐藏损坏 preset 但在复制时用更好的报错拒绝该 id:幽灵仍然无法从任何界面清除。深度校验(读名单时解析每一行的模块):挂载已经拥有这一失败并带回滚,每次读名单逐行 import 既不便宜也不更可操作。阻止 settings 写入指向损坏默认值:settings 领域是通用的,而名单是活目录——此刻缺失或损坏的名字到下一个会话可能已经有效,挂载的响亮失败才是拥有那一刻的强制点。