# Agent Note: 文件系统中的缺失是一种观测，带防护的创建绝不执行替换

Status: implemented
Archived: 2026-09-04

[English](2026-08-09-filesystem-absence-observation.md) | 中文

## 问题

事件门控的文件系统策略最初只把成功读取和变更记录成目标版本。如果某个会话读取文件后，外部命令将其删除，第一次带防护的变更会正确地因陈旧而失败，但按指示执行的重新读取会在发出 `fs/observed` 前返回 `FS_NOT_FOUND`。因此，旧的存在版本会一直保留：写入仍不断选择 `replaceIfVersion`，提供方仍不断拒绝缺失目标，而面向模型的「re-read the file, then retry」指令则形成无法恢复的循环。

把一次失败的读取视作创建授权，还会暴露第二个边界。本地与 E2B 提供方都会先探测再暂存，此前随后通过 rename 发布；另一进程可能在两步之间创建目标，即使调用方提供了 `createIfAbsent`，该目标仍会被覆盖。进程内目标锁无法防范这种跨进程发布竞态。

## 决策

`dsh-fs` 拥有一个显式观测联合类型：`{ kind: 'present', version: FsVersion } | { kind: 'absent' }`。`fs/observed` 事件携带该联合类型。成功的读取与变更发出存在观测；`read` 的元数据未命中，或 `str_replace_editor` 的 `view`、`str_replace`、`insert` 命令发生元数据未命中时，都会在返回 `FS_NOT_FOUND` 前同步发出缺失观测。其他读取失败不会产生缺失观测。

`dsh-fs-observation-policy` 按所有者与目标存储三种逻辑状态，既不注入也不调用 `ctx.fs`：映射中无条目即未见，`absent` 表示确认缺失，`present(version)` 是替换/编辑基准。写入把未见和缺失映射到现有 `createIfAbsent` 意图，把存在映射到 `replaceIfVersion`。编辑把未见映射到 `FS_NOT_OBSERVED`，把缺失映射到 `FS_NOT_FOUND`，把存在映射到其版本守卫。成功创建或变更后，系统会用其产生的存在版本取代缺失状态。

每个提供方都必须在发布点执行 `createIfAbsent`，不能只在初始探测时执行。`dsh-fs-local` 在私有同级目录中暂存并执行 fsync，再通过硬链接把暂存文件发布到目标位置；链接失败后，它会检查目标条目：与普通文件冲突时返回 `FS_NOT_OBSERVED`，条目非普通时返回 `FS_NOT_REGULAR_FILE`，目标仍然缺失时返回 `FS_IO_ERROR`。`dsh-fs-e2b` 使用远程 `ln -T` 返回明确的已创建/已存在结果，并根据不可取消提交前取得的元数据推导已提交目标的版本。替换操作和裸无条件写入仍沿用现有发布路径。

本决策不宣称 `replaceIfVersion` 具有跨进程线性一致性：提供方的版本检查与替换仍只能防范被其自身锁纳入协调的写入方，以及能通过元数据检测到的写入方。更窄的保证边界准确且足以支持缺失恢复：带防护的创建绝不会覆盖在发布前出现的目标。本地带防护的创建要求支持硬链接；任何本地发布一旦成功，暂存清理便采用尽力而为语义，因为私有残留无法否定已提交写入的成功。

## 曾考虑的替代方案

- **读取返回未找到时删除缓存版本。** 不予采用，因为这会混淆未见与确认缺失，无法让 edit 返回正确的 `FS_NOT_FOUND` 结果，还会抹去该事件本应传达的状态转换。
- **让 `dsh-fs-observation-policy` 在选择意图前调用 `stat`。** 不予采用，因为这会让只依赖事件的策略转而依赖提供方，为每次决策增加 I/O，并且在发布前仍留下 TOCTOU 间隙。
- **允许 `replaceIfVersion` 在目标消失后执行创建。** 不予采用，因为存在观测是执行替换而非创建的依据；静默改变该提供方意图会绕过必须针对缺失目标执行的重新读取，并削弱陈旧保护。
- **让删除目标后的死路继续保持 fail-closed。** 不予采用，因为这样会使面向模型的恢复指令失实，而且正常的外部清理操作无法在会话内恢复。

## 影响

尚未观测到外部删除时，第一次变更仍以 `FS_STALE_VERSION` 失败；用户或模型必须遵循现有的重新读取恢复指令。该次针对缺失目标的重新读取会返回 `FS_NOT_FOUND` 并同时改变策略状态，此后 edit 仍被禁止，而 write 可以重新创建该路径。如果另一个写入方赢得创建竞态，本次重试会返回 `FS_NOT_OBSERVED`，并保留获胜方写入的文件；若竞态目标是目录、特殊条目或悬空符号链接，则改为返回 `FS_NOT_REGULAR_FILE`，且不会要求再次读取。

观测载荷是由包拥有的事件约定变更，因此所有生产方、监听器、不变式、生成的 Cordis 目录、子系统文档以及两套文件系统工具都必须同步更新。策略保留[事件门禁决策](../architecture/2026-06-26-file-context-as-event-gate.zh.md)确立的 read 一次 `stat`、write/edit 零次 `stat` 预算、所有者隔离、dispose 行为和可选部署边界。

组装后的文件系统快照固定面向模型的恢复链；提供方测试则在暂存后注入一个创建者，以证明发布不会覆盖竞争目标。[带防护变更恢复指令决策](../feature/2026-08-03-fs-tool-error-remedy.zh.md)仍然拥有面向模型的恢复措辞；本 Agent Note 使其中的删除路径能够生效。
