Skip to content

[InProgress][NO-MERGE] fix(file): state the read-before-edit boundary for searches (#360) - #362

Draft
Richeir wants to merge 1 commit into
T-Auto:mainfrom
Richeir:fix/policy-search-not-a-read
Draft

Richeir wants to merge 1 commit into
T-Auto:mainfrom
Richeir:fix/policy-search-not-a-read

Conversation

@Richeir

@Richeir Richeir commented Oct 21, 2026 •

Copy link
Copy Markdown
Collaborator

PR 的内容还在测试中,暂时先不合并;等测试稳定之后,再去掉 Draft;

Closes #360

What was wrong

模型用搜索通道拿到文件内容后就动手改,但宿主的 read-before-edit 表只认读取类工具登记的观测,于是每个从未读过的文件各产生一条拒绝:

Error: cannot modify "…\gates\gate_01.js": file has not been read — read the file, then retry

最小复现(gate-repair-loop fixture,ops 侧逐调):read gate_00.js → edit gate_00.js ✅ → 一次 grep 拿到全部 24 个 stage 的契约与错值 → edit gate_01…23(23 个并行下发)全部被拒 → ops_read_files(23 个文件,offset:77,limit:7)→ 内容相同的 23 个 edit 全部成功。47 次 edit 里 23 次被拒,toolErrors = 23;内容一直是对的,缺的只是出处。

这不是 ops 侧独有。base 侧用两次 pwsh Get-ChildItem gates\*.js | ForEach-Object {…} 批量 dump 踩的是同一个门禁:

run arm 内容通道 toolErrors 其中「has not been read」 文件集合
full-sweep-28 bench-ops 1× 宿主 grep 23 23 gate_01…23
full-sweep-10 bench-ops 3× 宿主 grep 21 21 gate_03…23
full-sweep-16 bench-base 2× pwsh 批量 dump 24 23 gate_01…23
full-sweep-23 bench-base 2× pwsh 批量 dump 23 23 gate_01…23

四例的错误数都精确等于「未登记读取就编辑的文件数」(full-sweep-16 多出的 1 条是 ReplaceFileW EIO (Win32 1175) 的 Windows 瞬时失败,与出处无关)。所以这是批处理发现方式与宿主出处门禁的交互:base 的自然节奏「读 4 个 → 改 4 个」让账本始终同步,ops 工具让一次调用替代 24 次读取,分歧一次性放大到满量级。两侧单独看都不构成缺陷。

根因是 docs/component-reference.md:52 已写明的既定行为:ops_grep、ops_glob 与 dryRun 不登记任何观测。完整证据(callId 级归因、原始 JSON、ops_read_files 返回中 23 个 [revision=…] 标记)见 #360 的证据评论。

The change

  • src/policy.ts:工具指引新增一行说明,只要有一个会登记观测的读取工具可见就输出,并动态点名真正可见的读取工具。会话开局(只有常驻的 ops_read_files 可见)渲染为:

    • A search hit or a command's output is not a read: the host edit tool refuses a file it has no recorded read for ("file has not been read"). Read such a file first with ops_read_files — a narrow range is enough.
  • test/policy.test.mjs:断言覆盖条件矩阵与「不得点名未发布的读取工具」,并复用既有约束确保文件区段不点名 shell 工具。其中**「只有 ops_read_files 可见时也必须输出」是关键回归断言**,理由见下。

  • docs/CHANGELOG.md(## [Unreleased] 双语条目)、docs/troubleshooting.md、docs/常见问题.md:把边界与代价写进排障文档。

为什么条件不能挂在「搜索工具也可见」上

初版把条件写成「搜索工具(grep/glob)与读取工具同时可见」,这是错的,而且错得很隐蔽:visibleTools() 取的是 ctx.tools.schemas(agent),即此刻真正对模型可见的工具。原生文件面只用常驻集合启动(src/index.ts:88 传入 NATIVE_RESIDENT_FILE_TOOLS),而注册表里只有 read_files 是 resident、ops_grep/ops_glob/ops_inspect_local_file 都是 discoverable(src/registry.ts:19,且 discovery 默认 alwaysLoaded: [])。

于是在 gate-repair-loop 那种会话里(模型从未调用 ops_discover_tools),开局可见集合只有 ops_read_files,条件恒为 false——这行根本不会渲染,修复会在它唯一要修的场景里失效。宿主 grep 反而是可见的,正是制造 23 条拒绝的那条通道。

现在的条件只依赖读取工具,第 4 节的断言就是为这个坑设的(把旧条件加回去,该断言立刻变红,见 Validation)。

刻意没做:不给 ops_grep / ops_glob 补发 fs/observed。grep 返回的是片段,补发等于把局部扫描声称为完整读取,与本仓硬规则冲突,所以这条路明确排除。补救取向是「读窄范围」——一次 ops_read_files 调用即可覆盖整批,对照的是每个文件一条拒绝。

刻意保留的边界:只挂 shell 组件、没有文件工具时该行不输出——那里点不出任何读取工具,给不出可执行的补救。若维护者希望 shell 侧也覆盖(改为提示用宿主 read),建议另开一条。

依赖、package.json 版本与 optionalDependencies 均未改动。

Validation

面向模型的文本变化:会话开局形态下该行 216 字节;两个读取工具都可见时点名两者,为 242 字节。条件矩阵实测:

read_files only(会话开局形态)           -> 输出,点名 ops_read_files      ← 关键
inspect_local_file only                  -> 输出,点名 ops_inspect_local_file
read_files + inspect_local_file          -> 输出,点名两者
read_files + grep                        -> 输出
grep only / glob only(无读取器)         -> 不输出(无从点名补救手段)
shell-only (ops_bash + pwsh)             -> 不输出
单个工具可见                              -> 不输出,且不泄漏任何未发布工具名
$ npm run verify
> node scripts/build.mjs && node scripts/validate-manifest.mjs && node test/run.mjs
build: bundled 9 browser modules (33669 bytes)
build: generated PTC declarations for 18 capabilities (113342 bytes)
build: compiled src/ to lib/ and copied 35 assets
validate-manifest: all 73 checks passed
dsh-ops build: all 4 checks passed
dsh-ops policy: all 14 checks passed
dsh-ops test: 123 suite(s) passed, 4 skipped in 327s        (exit 0)

$ npm run check:size
check-file-size: 385 code file(s) checked, all at or under 1000 lines (longest: bench/setup/profile.mjs (924))

$ git checkout HEAD~1 -- src/policy.ts && npm run build && node test/run.mjs policy
  FAIL a search hit is not a read: the prompt names the readers that clear the host gate (#360)
dsh-ops policy: 1 of 14 check(s) failed                      (exit 1)
$ git checkout HEAD -- src/policy.ts && npm run build && node test/run.mjs policy
dsh-ops policy: all 14 checks passed

# 行为级红绿,且专打上面那个隐蔽条件错误:把初版条件(额外要求 grep/glob 可见)加回去
$ git diff src/policy.ts    # if (observing.length)  ->  if (observing.length && ['grep','glob'].some(…))
  FAIL a search hit is not a read: the prompt names the readers that clear the host gate (#360)
       AssertionError [ERR_ASSERTION]: The input did not match the regular expression
       /A search hit or a command's output is not a read/. Input:
         '- ops file tools never throw: check `status` and `error.code`/`error.nextAction`; …'
dsh-ops policy: 1 of 14 check(s) failed                      (exit 1)

4 个跳过均为环境前提、非失败:linux-attributes、linux-shell(POSIX-only 概念),tui-host、tui-native(需要 DSH_OPS_TUI_ROOT)。

  • 撤掉本次实现后相关检查变红(见上,行为级红绿,非结构检查)
  • docs/CHANGELOG.md 的 ## [Unreleased] 已加双语条目
  • 行为变化已同步相关 docs(troubleshooting.md + 常见问题.md)
  • 未新增依赖,未改 package.json 版本与 optionalDependencies
  • 改了面向模型的文本:上方已说明字节影响(会话开局 216 字节 / 两个读取器时 242 字节)并同步文档
  • pull_request 事件触发的 CI required run URL:
$ gh run view 39638250604 --json url,event,headSha,conclusion --jq '"(\(.url)) \(.event) \(.headSha[0:7]) \(.conclusion)"'
(https://github.com/T-Auto/dsh-ops/actions/runs/39638250604) pull_request adca4a2 success

$ gh run view 39638250604 --json jobs --jq '.jobs[] | "\(.conclusion)\t\(.name)"'
success  source-size
success  workflow-lint
success  validate-windows (windows-latest, win32-x64)
success  validate-posix (ubuntu-24.04, linux-x64)
success  node-floor
success  tools-trial
success  validate-posix (macos-latest, darwin-arm64)
skipped  bench-smoke
success  CI required

Environment

  • Windows 10.0.26100 x64(AMD64)、Node v24.11.0、npm 11.12.1
  • 基线:T-Auto/dsh-ops main b963fb2(分支已变基到它之上,与 #306 的 fix(policy) 改动无冲突:本 PR 只增行,未动它修改的 heading 与 prefer-bash 行)
  • 安装通道:本地 checkout(npm run build 后由 bench profile 链接)
  • 观测环境:dsh-ops 0.4.1、DSH 0.2.1-rc.2、profile bench-ops / bench-base、file policy danger-full-access
  • 本地环境注意(与本 PR 无关):NODE_ENV=production 会让 settings-ui 套件 8/8 失败(production 版 React 没有可用的 act)。该现象在干净 main 上同样复现,清掉 NODE_ENV 后 8/8 通过。

A search hit, a command's output or a directory search shows file content
without recording an observation, so the host read-before-edit table still
refuses an `edit` on that file with "file has not been read". The model
learned this by having a whole batch rejected: one error per file it never
read. In one benchmark run of gate-repair-loop a single `grep` stood in for
24 reads and the following 23 edits were each refused; the same signature
reproduces on both arms (21 errors after `grep`, 23-24 after a `pwsh` bulk
dump), so it is an interaction between batch discovery and the host gate
rather than a defect in either side alone.

The line names only the readers that actually clear the gate, and is emitted
whenever one of them is visible — deliberately not only beside ops_grep and
ops_glob. Those two are discoverable, so a session starts with `read_files`
alone while the host grep stays callable, which is exactly the shape that
produced the rejected batch; gating the line on a visible ops search tool
silences it in the one session that needs it. A narrow range is enough: one
`read_files` call covers a whole batch, against one rejection per file that
skips it.

`ops_grep` and `ops_glob` keep recording nothing. A grep returns excerpts, and
reporting them as a read would claim a partial scan as a complete one, which
this repository forbids. The shell section names no reader, so it carries no
remedy and gets no line.
@Richeir
Richeir force-pushed the fix/policy-search-not-a-read branch from 7e83dcd to adca4a2 Compare October 21, 2026 16:49
@Richeir Richeir changed the title fix(file): state the read-before-edit boundary for searches (#360) [InProgress][NO-MERGE] fix(file): state the read-before-edit boundary for searches (#360) Oct 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant