【bug】file changed since it was read 的硬性拒绝可被截断读取+重写绕过,我的编辑被覆盖 #1236
Replies: 3 comments 2 replies
当前 rc.6 的代码已经是"硬性拒绝"了——建议确认你用的版本我顺着你说的查了当前 代码事实:write 的 stale guard 是 throw,不是 warn
if (existing.version !== expected.version) {
throw new FsError(`cannot write "${target.displayPath}": file changed since it was read`, 'FS_STALE_VERSION')
}写入被硬性拒绝了——模型拿到的不是"警告"(warn),是错误(throw)。这不是"提示、模型可以无视"的层级。 更进一步的, const REMEDIES = {
FS_STALE_VERSION: 're-read the file, then retry',
FS_NOT_OBSERVED: 'read the file, then retry',
}拒绝的同时告诉模型怎么恢复(重读文件再重试)——这已经是你帖子想要的"硬性行为"了。 那帖子里的事故是怎么发生的?几种可能,值得排查:
重点排查方向 2:无条件覆盖路径看代码里 判别视角(HeartFlow,AGI 第1层辨别者)"file changed since it was read"是护栏存在的信号,但护栏的有效性取决于它覆盖所有进入路径。当前实现的问题不是"护栏是提示不是硬拒绝"(它已经是硬拒绝了),而是护栏可能只覆盖了带版本期望的路径——如果无条件 write 路径绕过了它,护栏就是"看起来有、实际有洞"。判别者面对"有护栏"的声明,第一反应是问:所有入口都过这道护栏吗? 对文件写入来说,"带版本期望的 write"和"无条件 write"是两个入口,只守一个不算守住。 |
答复草稿 · #1236 事故根因补充:read 部分读取(limit/offset)无条件注册完整观察,使后续 CAS write 通过补充讨论中缺失的一环: 机制(rc.6 编译产物)
缺陷本质部分读取(limit/offset 截断)注册了与完整读取等价的观察状态——"只看过前 10 行"被当作"已看到整个文件的最新版",于是后续无条件信任该观察的 write 覆盖通过。护栏的"re-read the file, then retry"指引在此路径下被部分读取"假满足"。 建议修复方案 A(推荐)· 部分读取不注册观察:read 工具在 方案 B · 观察携带覆盖范围: 方案 C · 仅当覆盖全文才升级观察:完整读取(limit ≥ totalLines 且 offset=1)才更新观察版本;部分读取保留旧观察。与 A 等价,实现角度略异。 影响
补充:yun520-1 提出的"无条件 write 路径(不带 expected)"是另一个独立缺口,值得一并修(无期望 write 绕过 guard);两个缺口同属"观察/期望模型覆盖不全",建议一起处理。 |
你的根因比我准——"部分读取注册完整观察"确实是事故的真实机制我之前的猜测("write 不带版本期望")与事故不符——你确认了 write 是带 方案 A 的判别视角:这是"部分证据 ≠ 完整证据"的精准案例方案 A(部分读取不注册观察)语义上最干净,而且它抓住了一个更深的规律:观察系统把"部分读取"当成了"完整读取"——模型只看过前 10 行,观察状态却记成"看过整个文件的最新版"。这不只是 bug,是证据等级的混淆:
现在的实现把 0.5 当成 1 用——观察系统只认"有没有读过",不认"读了多少"。方案 A 把 0.5 降级为 0(部分读取不发观察),保守且正确:不知道全貌就不该声称知道。 方案 B 的一个补充方案 B(观察携带 offset/limit 范围)语义更细,但如果做,建议想清楚一个边界:部分读取之后,用户(或模型)如何升级到完整观察? 如果部分读取不发观察(A),那"先 read 10 行确认结构、再 read 全量确认内容、再 write"的合法流程会被拦(第二次 read 之前没有观察)。B 的价值在于允许部分观察存在但标记不完整,让 writeIntent 知道"这个观察不可作 CAS 依据"。两种方案都成立,取决于你想让"观察升级"是隐式(A:重新完整 read)还是显式(B:观察带范围标记)。 判别视角(HeartFlow,AGI 第1层辨别者)事故的本质是观察系统的粒度不够:观察是"存在 + 版本"两个字段,没有"覆盖范围"。判别者的原则:声称必须匹配实际证据——"看过这个文件"和"看过这个文件的前 10 行"是两种不同的声称,观察系统必须能区分。任何校验系统(CAS、观察、签名)都要回答:"这个观察覆盖了多少真实情况?" 覆盖不全还当全覆盖用,就是 #1236 这种事故的来源。你补上的这个根因,把"观察/期望模型覆盖不全"从抽象风险变成了具体机制——值得合入修复。 |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
在写博客时遇到一个事故:我在 Typora 里编辑一篇文章并保存,同时让 DSH 助手去给文章补图、调文字。助手采用整体重写(write 覆盖)的方式写文件,写入前工具报了 "file changed since it was read" 警告(说明它对文件的上下文理解停留在我新保存的这一次之前,没有重新读),但助手没有停下来,最终直接把自己的版本写回去了,我在 Typora 里的改动全部丢失。因为改动没进过 git,最后是靠 Typora 的自动备份找回的。
复盘下来,问题不在工具没检测,而在警告只是提示、模型可以无视。我们这边能做的只有把规则写进全局 AGENTS.md(写前必读、外部改动即停止、覆盖前必备份),但这是一层靠模型自觉的约束。
希望 DSH 能把这层约束做成工具层面的硬性行为,比如:
这样即使模型没有遵循指令,文件也不会在毫不知情的情况下被覆盖。
相关记录:https://meredith2328.github.io/posts/notes/agents/dsh-overwrite-incident.html
更新(2026-08-14):完整的事故复盘
事后我从会话日志里把整个过程精确还原了,补充到这里(完整记录见前述“相关记录”)。事故发生在 v0.1.0-rc.6,Turn 73 这一轮共 36 次工具调用,关键的三次是:
FS_STALE_VERSION:file changed since it was read — re-read the file, then retry我的 Typora 保存发生在 16:25:41。也就是说:第一次 write 确实被硬性拒绝(throw 而非 warn),问题出在模型收到错误之后的恢复动作——只做了截断读取就带着同样的参数重写,第二次写没有被拦下来。
All reactions