A hand-damaged preset was silent until the worst moment. An unparsable composition listed as an ordinary selectable row and failed only at the next session start — set as default, every new session failed. A directory whose composition file was deleted vanished from the roster while still occupying its id: copy answered "delete the existing preset first" while remove answered "not found", a dead end. Discovery now owns health: every id-shaped directory is a roster slot, broken when its composition is missing or unloadable, checked with the loader's own entryListSchema dialect (!!js included) so health never rejects what the loader accepts. `broken` rides AgentPreset, the agentPreset.list entry, and the UI row; mount/recompose/standingKeyFor refuse broken up front with the discovery-reported reason, while resolve/read/remove still answer. The section renders marked red cards — unselectable, uncopyable, deletable, location kept on custom rows — and both pickers drop broken rows entirely. The cordis preset's persona now forbids editing the shipped install (corrupting cordis would disable the mode itself) and points authoring at $DSH_HOME/.agent-presets; its skill teaches preset.yml metadata, the copy-first workflow, the one-escalation sandbox reality, and honest verification. Exercised live: asked to edit the shipped composition the composed agent refuses citing both rules; asked for real presets (simple and complex) it lands them under the user root with one approved escalation each and self-checks with the loader dialect.
4.9 KiB
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 落地:
cordispreset 的 persona 现在禁止编辑随附安装(损坏cordis会禁用这一模式本身),并把创作指向${DSH_HOME:-$HOME/.dsh}/.agent-presets/<id>/;其技能新教了preset.yml元信息、先复制再改的流程、一次升级的沙箱现实(preset 根目录在会话工作区之外)与诚实的验证方式(agent 无法自己启动会话;设置页的红色标记是用户的检查项)。已实测:被要求直接改随附cordis组装时,组装出的 agent 援引两条规则拒绝并给出复制路径;被要求真正创建 preset 时,它落在$DSH_HOME下、把写入合并为一次升级、用加载器方言自查、并把验证交还用户。
曾考虑的替代方案
隐藏损坏 preset 但在复制时用更好的报错拒绝该 id:幽灵仍然无法从任何界面清除。深度校验(读名单时解析每一行的模块):挂载已经拥有这一失败并带回滚,每次读名单逐行 import 既不便宜也不更可操作。阻止 settings 写入指向损坏默认值:settings 领域是通用的,而名单是活目录——此刻缺失或损坏的名字到下一个会话可能已经有效,挂载的响亮失败才是拥有那一刻的强制点。