Stale-version errors append the recovery instruction at the model boundary
陈旧版本错误在模型边界追加恢复指令
Guarded `write` and `edit` failures reach the model with messages that state the condition but not the only correct recovery: `FS_STALE_VERSION` ("file changed since it was read") and `FS_NOT_OBSERVED` ("edit requires reading … first"). The model must guess that the recovery is a re-read (or a first read) followed by a retry, and the retry/permission/UI layers that route on the structured code see the same message te
English
Problem
Guarded write and edit failures reach the model with messages that state the condition but not the only correct recovery: FS_STALE_VERSION ("file changed since it was read") and FS_NOT_OBSERVED ("edit requires reading … first"). The model must guess that the recovery is a re-read (or a first read) followed by a retry, and the retry/permission/UI layers that route on the structured code see the same message text. The provider-owned messages are part of the storage seam's machine-oriented vocabulary (filesystem capability seam), so the remedy cannot live there without leaking model-facing wording into every consumer of FsError.
Decision
dsh-tool-fs owns a model-facing error wrapper, remediateFsError in src/error.ts, applied in write.ts and edit.ts after the sandbox denial mapping. It appends the recovery instruction to stale-version failures and passes unrelated errors through untouched. The normalized unread-mutation diagnostic supersedes this note's original FS_NOT_OBSERVED text treatment.
FS_STALE_VERSION(including a missing edit target, which shares the stale code) gains— re-read the file, then retry.
The structured FsError code is preserved so retry/permission/UI layers keep routing on it, and the original error chains as cause. Provider messages stay machine-oriented and unchanged.
In edit.ts the fs/edit-intent waterfall sits inside the same try as the provider mutation, so the policy plugin's FS_NOT_OBSERVED refusal and the provider refusal both pass through the model-facing wrapper.
Alternatives considered
- Append the remedy to the provider messages in
dsh-fs/dsh-fs-local. Rejected because those messages are machine-oriented seam vocabulary consumed by retry, permission, UI, and model-facing layers; model-facing wording belongs at the model boundary, wheredsh-tool-fsalready owns result formatting (filesystem capability seam). - Add the recovery to prompt guidance instead. Rejected because the failure arrives mid-task; a static instruction does not reliably reach the retry decision, while the error message is present exactly when the model must act.
- Signal the remedy with a new
FsErrorcode. Rejected because the two failures are the same conditions retry layers already handle; splitting the code would fork routing on identical semantics.
Consequences
The FS_STALE_VERSION model-visible text includes its appended remedy. Unit tests cover its text, code preservation, cause chaining, and passthrough of unrelated values; assembled tool paths assert that the remedy reaches the model.
The filesystem absence-observation follow-up makes the stale remedy actionable for external deletion. The failed reread still returns FS_NOT_FOUND, but records confirmed absence: edit then returns FS_NOT_FOUND without another stale remedy, while write retries as an atomic createIfAbsent and preserves any concurrent creator.
中文
问题
受防护的 write 与 edit 失败以只陈述条件、不给出唯一正确恢复方式的消息到达模型:FS_STALE_VERSION(「file changed since it was read」)与 FS_NOT_OBSERVED(「edit requires reading … first」)。模型必须自行猜测恢复方式是重新读取(或首次读取)后重试,而基于结构化错误码路由的重试/权限/UI 层看到的也是同一段消息文本。提供方拥有的消息属于存储 seam 的面向机器词汇(文件系统能力 seam),因此恢复指令不能放在那里,否则会把面向模型的措辞泄漏给 FsError 的每个消费方。
决策
dsh-tool-fs 拥有一个面向模型的错误包装层 remediateFsError(位于 src/error.ts),在 write.ts 与 edit.ts 中于沙箱拒绝映射之后应用。它为陈旧版本错误追加恢复指令,其余无关错误原样透传。未读取变更的统一诊断取代本记录最初对 FS_NOT_OBSERVED 文本的处理方式。
FS_STALE_VERSION(包括缺失的编辑目标——它与陈旧错误共用同一错误码)追加— re-read the file, then retry。
结构化 FsError 错误码保持不变,使重试/权限/UI 层继续基于它路由;原始错误作为 cause 链入。提供方消息保持面向机器且不变。
在 edit.ts 中,fs/edit-intent waterfall(瀑布式事件)与提供方变更位于同一个 try 内,因此策略插件的 FS_NOT_OBSERVED 拒绝和提供方拒绝都会经过面向模型的包装层。
考虑过的替代方案
- 在
dsh-fs/dsh-fs-local的提供方消息中追加恢复指令。 被拒绝:这些消息是面向机器的 seam 词汇,被重试、权限、UI 和面向模型的各层消费;面向模型的措辞应位于模型边界,即dsh-tool-fs已经拥有结果格式化之处(文件系统能力 seam)。 - 改为在提示词引导中加入恢复方式。 被拒绝:失败发生在任务中途;静态指令无法可靠地影响重试决策,而错误消息恰好在模型必须行动时出现。
- 用新的
FsError错误码表达恢复指令。 被拒绝:这两种失败对应的条件,重试层本就已经处理;拆分错误码会使相同语义采用不同路由。
后果
FS_STALE_VERSION 的模型可见文本包含追加的恢复指令。单元测试覆盖该文本、错误码保留、cause 链和无关值透传;组装后的工具路径断言恢复指令到达模型。
文件系统缺失观测后续决策使外部删除场景下的陈旧恢复指令能够生效。失败的重新读取仍返回 FS_NOT_FOUND,但会记录确认缺失:随后 edit 返回 FS_NOT_FOUND,不再附加陈旧恢复指令;write 则以原子 createIfAbsent 重试,并保留任何并发创建者写入的文件。