dsh-fast-mode 1.5.19 → 1.5.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/BENCHMARK.md CHANGED
@@ -2104,3 +2104,80 @@ python3 bench/suite-run.py --tasks debugfix,ledger,codeimpl --arms off,def15,bud
2104
2104
  2. **同一块里 `def15` 也出了一次 620 s / 36 步的长尾**(`valid=true`,交付正常)。两臂各一次超长尾,方向不同,说明 migrate 这一族本身是重尾分布,单看某一对没有意义——这也是继续只报几何 + 胜场、不报单对的原因。
2105
2105
 
2106
2106
  **机制读数(沿用 9 条分解)。** 5 对里赢的四对全部同时压步数与输出(步数 0.622×、输出 0.528×),与 `d2`/`d2g` 在 migrate 上看到的「输出 token 才是主因」一致:p1 的 275.6→96.7 s 里步数贡献 −58 s、每步成本贡献 −120 s;p4 的 620.6→93.7 s 里步数贡献 −368 s。p5 是唯一「步数少了 8 步、每步反而更贵」的一对(9.8→14.6 s/步),净收益只剩 11 %——和「早提醒不是无条件赢」的既有结论吻合。
2107
+
2108
+ ## 结果:`D2_PIPE`(`d2` 在 shellpipeline 上,4 对可用)——**没有信号,1.111×,判「未测到」**
2109
+
2110
+ **数字(`def15` vs `d2`,shellpipeline)。** 计划 6 对,队列在 4 对后暂停,可用 4 对、丢弃 0:几何 **1.111×**、中位 1.006×、2/4 更快、范围 0.641–2.594×;步数几何 **1.527×**、输出几何 1.047×;符号检验 p=1.000。逐对:
2111
+
2112
+ | 对 | `def15` | `d2` | 比值 | 步数 | 输出 token |
2113
+ |---|---|---|---|---|---|
2114
+ | p1 | 92.2 s | 239.1 s | **2.594×** | 7→29 | 10 129→25 071 |
2115
+ | p2 | 218.7 s | 140.2 s | 0.641× | 15→13 | 21 010→13 788 |
2116
+ | p3 | 219.1 s | 153.4 s | 0.700× | 19→23 | 27 113→17 137 |
2117
+ | p4 | 114.0 s | 149.5 s | 1.312× | 12→15 | 12 038→14 065 |
2118
+
2119
+ **判定:不成立,也不反向。** 4 对里 2 对更快、几何 1.111× 由单个离群点(p1 的 7→29 步)拖出,去掉它三对几何 0.837×。n=4 太小,符号检验必然 p=1,这一块**不能写「更快」也不能写「更慢」**,只能记成「在 shellpipeline 上第 2 步提醒没有可测收益」。
2120
+
2121
+ **机制读数。** p1 的 2.594× 里步数贡献 +99 s、每步成本贡献 +48 s:`d2` 在第 2 步就收到「省着写」提醒后,反而在同一族里把 7 步的短流程拖成 29 步——与 `d2` 在 codeimpl/debugfix 上的既有结论(0.903–1.073×)方向一致:**早提醒在「本来就短、本来就要多验证」的形状上是净成本**。p2/p3 是反向证据(同样收到第 2 步提醒却步数不涨、输出降 35–37 %),说明签名不是家族级而是单跑级的。
2122
+
2123
+ **对队列的影响。** 这一族已经有 `d2g`(0.903×)与 `D2_PIPE`(1.111×,n=4)两块都不支持第 2 步,加上 `D3_MIG` 的 PASS 只覆盖 migrate,默认继续停在第 4 步;`D2_PIPE` 不再补对:现有 4 对几何 1.111×,即使补的两对都跑到 0.70×,6 对几何也只有约 0.94×,仍然写不出「更快」;再补 2 对换取一个大概率仍是 0.9× 上下的数字,成本不划算。下一步跑 `D3_DBG`(第 3 步在 debugfix 上掉不掉队),它是「默认时点能不能挪」的最后一个 gate。
2124
+
2125
+ ## 结果:`D3_DBG`(第 3 步提醒在 debugfix 上)判 **FAIL**:6/7 更快、中位 0.876×,但几何 1.128× 被一格模型侧长尾顶穿
2126
+
2127
+ **数字(`def15` vs `d3`,debugfix 7 对,全部可用、丢弃 0)。** 几何 **1.128×**、中位 **0.876×**、6/7 更快、范围 0.585–7.346×;步数几何 1.321×、输出几何 1.116×;符号检验 p=0.125。逐对:
2128
+
2129
+ | 对 | `def15` | `d3` | 比值 | 步数 | 输出 token |
2130
+ |---|---|---|---|---|---|
2131
+ | p1 | 33.5 s | 32.1 s | 0.960× | 5→5 | 3 906→3 683 |
2132
+ | p2 | 75.1 s | 43.9 s | **0.585×** | 6→5 | 5 956→4 317 |
2133
+ | p3 | 49.5 s | 363.9 s | **7.346×** | 4→10 | 4 437→**21 743** |
2134
+ | p4 | 76.8 s | 65.1 s | 0.847× | 7→6 | 5 091→4 733 |
2135
+ | p5 | 69.8 s | 57.4 s | 0.822× | 4→7 | 4 967→3 757 |
2136
+ | p6 | 103.7 s | 96.1 s | 0.926× | 4→4 | 6 154→6 786 |
2137
+ | p7 | 40.4 s | 35.4 s | 0.876× | 4→9 | 3 908→3 235 |
2138
+
2139
+ **判定:FAIL,默认继续停在第 4 步。** 预注册 `D3_DBG` 要求几何 ≤ 0.90 **且** ≥ 5/7 更快;胜场过(6/7),几何不过(1.128×)。按预注册「改默认要 MIG 与 DBG 同时过」的规则,`MIG` 过、`DBG` 不过,所以 `disciplineAfterSteps` 不改,维持 4。
2140
+
2141
+ **p3 的 7.346× 不是 d3 的行为效应,是模型侧输出长度波动。** 该格 `llm` 361 s / 墙钟 364 s(99 % 在模型里),步数与 `def15` 同为有效、输出 21 743 token 对 4 437 token(4.9×)。全部 7 对里只有这一格输出变长,其余 6 格输出全部下降;把这 6 格单独加总几何是 **0.826×**(0.826 = 6 格几何,与胜场一致)。所以准确的说法是「第 3 步在 debugfix 上典型更快、但有一个尾部风险:更早的提醒偶尔与模型自己的长输出叠加」。**这不是把 FAIL 改判成 PASS**:预注册门禁就是几何口径,一格 4.9× 输出属于「真实会发生的分布」,不能用剔除它来通过门禁。
2142
+
2143
+ **方法层的发现(记下来给下一次预注册用)。** 本块和 `D3_MIG` 各出现一格 3–7× 的模型侧长尾,几何均值对这种重尾极敏感:n=7 时单格 7.35× 就能把 0.826× 抬到 1.128×。以后这类「按族看训练信号」的块,预注册应写成**中位 + 胜场为主、几何只作辅助**(例如「中位 ≤ 0.90 且 ≥ 5/7 更快,几何不设上限」),否则门禁测的是尾部分布而不是行为效应。这次不改本块的判定,只记录。
2144
+
2145
+ **工具修复(影响复算)。** 这一块最初被 `~/.local/fm/block_score.py` 判成「0 对可用」——它还在读旧字段 `exact`,而 1.5.18 起 runner 写的是 `hasSessionIds`。已把评分器收进仓库 `bench/block_score.py` 并改成 `row.get('hasSessionIds', row.get('exact'))`;`python3 bench/block_score.py D3_DBG def15 d3` 可复现上表。旧字段读取点只剩 `pool.py`/`report.py`/`d2gscore.py`/`dr8score.py` 已带 fallback,历史结果不需要转换。
2146
+
2147
+ ## 结果:`DEF_MIG`(默认 `def15` vs 完全关闭,migrate,跑在 **1.5.19** 上)——**0.688×,5/5 更快**,默认在长迁移上仍然更快
2148
+
2149
+ **这一块的意义。** 前面的 0.823× 全池口径是在 1.5.15–1.5.17 的构建上测的;1.5.19 改了三处会**减少提示**的行为(检查复用证据不再透支、取消三态改写、`disciplineEarlyCalls` 冻结),理论上「提示更少 ⇒ 复用建议更少 ⇒ 可能变慢」。所以必须用**发布版**重新对一次 `off`(插件完全关闭)。臂:`def15` = 发布默认(`outputDiscipline: late`、第 4 步提醒、其余增强默认关),`off` = `enabled: false`。
2150
+
2151
+ **数字(6 对计划,5 对可用、丢弃 1)。** 几何 **0.688×**、中位 0.655×、**5/5 更快**、范围 0.566–0.850×;步数几何 **0.944×**、输出几何 **0.925×**。逐对:
2152
+
2153
+ | 对 | `off` | `def15` | 比值 | 步数 | 输出 token |
2154
+ |---|---|---|---|---|---|
2155
+ | p1 | 225.4 s | 147.7 s | 0.655× | 9→8 | 14 975→11 362 |
2156
+ | p2 | 253.5 s | 143.6 s | **0.566×** | 9→9 | 16 517→15 351 |
2157
+ | p3 | 221.4 s | 128.8 s | 0.582× | 9→11 | 12 913→16 334 |
2158
+ | p5 | 399.6 s | 339.8 s | 0.850× | 17→13 | 27 262→21 519 |
2159
+ | p6 | 147.9 s | 124.3 s | 0.840× | 10→9 | 13 402→12 867 |
2160
+
2161
+ 丢弃的 p4:`off` 那一格 `leak: true`(53 步、46 988 token、1 149 s,测试没交付),`def15` 那一格 370 s / 11 步、`valid`。按预注册「`hasSessionIds` 且 `green` 且 `valid`」丢弃。注意这个丢弃方向是**对我们不利的**:`off` 那格是失败跑,如果硬算进去 `off` 会更慢、比值更小,所以 0.688× 不是靠丢掉不利格子得来的。
2162
+
2163
+ **判定:过预注册门槛。** `DEF_MIG` 要求几何 ≤ 0.85 且 ≥ 4/6 更快:可用 5 对(p4 丢弃,n=5 < 6 已在上面说明)、几何 0.688×、5/5,两条都过。结论:**发布版 1.5.19 的默认配置在长迁移上对比「完全关闭」依然快约 31 %**,且 5 对新会话方向全一致(符号检验 p=0.031)。
2164
+
2165
+ **机制读数:这一块的收益不是「少走步」,是「每步更省」。** 步数只降到 0.944×、输出只降到 0.925×,但墙钟到 0.688× ⇒ 每步耗时约 **0.73×**(`off` 24.6–28.2 s/步 vs `def15` 11.7–18.5 s/步)。这与全池分解(步数 0.827 × 每步 0.996 = 0.823×)**方向不同**:全池是「少走步」,这一块是「同样步数、每步的模型时间更短」。两种都成立,说明**提速来源随族变化**:migrate 上提醒把每次调用的思考/输出压短(`out` 几乎不变但落盘 token 少),全池里则是靠提前收束减少轮次。对外只报全池 0.823× 不会错,但不要把它当成「只有一条机制」。
2166
+
2167
+ ## 结果:`DEF_DBG`(默认 vs 关闭,debugfix,1.5.19)——几何 **0.770×**,但只有 4/7 更快
2168
+
2169
+ 7 对全部可用。几何 **0.770×**、中位 **0.760×**、**4/7 更快**、范围 0.407–1.323×;步数几何 0.794×、输出几何 0.880×;符号检验 p=1.000。
2170
+
2171
+ | 对 | `off` | `def15` | 比值 | 步数 | 输出 token |
2172
+ |---|---|---|---|---|---|
2173
+ | p1 | 36.5 s | 41.8 s | 1.147× | 4→4 | 4 047→4 773 |
2174
+ | p2 | 60.4 s | 45.9 s | 0.760× | 10→4 | 5 049→6 369 |
2175
+ | p3 | 34.3 s | 44.3 s | **1.294×** | 6→4 | 4 106→4 553 |
2176
+ | p4 | 72.5 s | 34.1 s | **0.470×** | 4→4 | 7 191→3 849 |
2177
+ | p5 | 66.5 s | 27.1 s | **0.407×** | 4→5 | 4 512→2 851 |
2178
+ | p6 | 33.3 s | 44.0 s | **1.323×** | 7→5 | 4 046→5 582 |
2179
+ | p7 | 79.3 s | 44.5 s | 0.560× | 6→5 | 8 408→4 447 |
2180
+
2181
+ **读法:几何与胜场分离。** 快的时候快很多(0.407、0.470、0.560),慢的时候慢得不多(1.147、1.294、1.323,单看都在或接近 ±20 % 噪声带),所以几何 0.770× 而胜场只有 4/7。p3/p6 有一个共同点:`def15` 步数更少但输出 token 更多(4 553 vs 4 106、5 582 vs 4 046)——第 4 步的「少写过程」提醒在这两格里像是让模型**先压缩一次、再补回来**。这是短任务上提醒本身有成本的又一条实例(与 1.5.1 里 `perfrefactor` 1.18×、`csvpipe` 1.12× 同向),不是判 FAIL 的依据:`DEF_DBG` 不承担改默认的职责,它只回答「发布版默认还快不快」。
2182
+
2183
+ **合池(`DEF_MIG` + `DEF_DBG`,发布版 1.5.19,12 对)。** 几何 **0.735×**(省约 26.5 %)、逐格中位 **0.708×**、9/12 更快、范围 0.407–1.323×;两族步数几何 0.886×、输出几何 0.907×。这一组是「发布版对新会话」的最干净对照(`off` = `enabled: false`,同一台机、ABBA 交错),结论:**1.5.19 减少提示之后,默认配置的提速还在,方向和量级与全池 0.823× 一致**;单块 n 小(5 和 7),对外仍只用 138 对全池口径,这组数字作为发布版复核记录。
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.5.20(补丁类编辑重新拥有复用证据:从 diff 头读目标文件)
4
+
5
+ - **补丁编辑不再拖垮整个会话的复用提示。** `apply_patch` / `patch` 的写入目标在补丁正文里,参数里没有 `path` 字段;1.5.19 把「定位不到目标的写入」如实标成不确定,代价是**会话里只要用过一次补丁,后续所有检查复用建议都关掉**——而补丁正是最常见的改文件方式,等于最常见的路径拿到了最少的那部分提速。现在从 `*** Update File:` / `*** Add File:` / `*** Move to:` / `+++ b/<path>` 头里读出目标文件并照常打指纹;只有补丁头里一个路径都读不出来时才保持不确定(保守方向不变)。`*** Delete File:` 不计入(文件已消失,打不了指纹)。
6
+ - 只对补丁形状的工具(`apply_patch` / `patch` / `apply_diff` / `edit_patch`)扫正文:整文件写入的 `content` 里可能恰好含 diff 文本,那种情况仍以它自己的 `path` 字段为准。
7
+ - 附:`bench/block_score.py` 随包发布(74 行,纯标准库),按 `hasSessionIds`/`green`/`valid`/`leak` 过滤后可复算每个块的几何、中位、胜场与符号检验。
8
+
3
9
  ## 1.5.19(稳定性发布:面板三项真正可读写、检查复用证据不透支、取消三态、实验臂与判据对齐)
4
10
 
5
11
  > 1.5.18 在 npm 上只有文档改动(README 结论先行),代码修复都在 1.5.19;升级请直接装 1.5.19。
package/README.en.md CHANGED
@@ -27,6 +27,7 @@
27
27
  Across 10 task families and 30 complete units (hidden tests 60/60 green):
28
28
 
29
29
  - **Default (whole pool, no block picking)**: 12 clean blocks, 10 task families, 138 valid pairs, added as whole tasks — **0.823x** total time (about 18 % saved), 0.818x output tokens, 0.806x steps, 93 of 138 pairs faster (recompute with `python3 bench/pool.py`). The earlier 0.72-0.74x came from three hand-picked blocks and has been withdrawn; why is in BENCHMARK.md.
30
+ - **Re-checked on the shipped build (1.5.19, default vs fully off)**: `migrate` 5 usable pairs at 0.688x (5/5 faster), `debugfix` 7 pairs at 0.770x (4/7 faster; three reverse cells pull the geometric mean down while the wins are much larger), 12 pairs pooled **0.735x**, per-cell median 0.708x, 9/12 faster (`bench/block_score.py` recomputes). 1.5.19 ships fewer hints, so this run confirms the release did not eat the gain; n per block is small, so the headline stays the 138-pair pool above.
30
31
  - **Moving the "write it once" reminder from step 4 to step 2**: `debugfix` 0.535x, `ledger` 0.506x, `migrate` 0.751x; 13 pairs pooled **0.625x** with output tokens **0.614x**. At step 3, `migrate` alone reaches **0.509x** with output tokens **0.528x**.
31
32
  - **Quality**: in every pair both arms produced identical hidden assertions, all green; the saving is fewer edit-recheck loops, not fewer checks.
32
33
  - **Where it does not pay**: 1-5 step repairs and data reshaping (`perfrefactor` 1.18x, `csvpipe` 1.12x), so try it once per task shape.
package/README.md CHANGED
@@ -27,6 +27,7 @@
27
27
  在 10 类任务、30 个完整单元上(隐藏测试 60/60 全绿):
28
28
 
29
29
  - **默认配置(全池口径,不挑块)**:12 个干净块、10 类任务、138 对有效配对的整任务总量 —— 总耗时 **0.823×**(省约 18 %)、输出 token 0.818×、步数 0.806×,138 对里 93 对更快(`python3 bench/pool.py` 可复算)。之前对外说的 0.72–0.74× 只来自挑出来的三块,已撤回,原因写在 BENCHMARK.md。
30
+ - **发布版复核(1.5.19,默认 vs 完全关闭)**:`migrate` 5 对可用 0.688×(5/5 更快)、`debugfix` 7 对 0.770×(4/7 更快、几何被三格反向拉低但赢面更大),12 对合池 **0.735×**、逐格中位 0.708×、9/12 更快(`bench/block_score.py` 可复算)。1.5.19 减少了提示,这一轮就是用发布版确认「提示变少没把收益吃掉」;单块 n 小,对外仍只用上面的 138 对全池口径。
30
31
  - **把「省着写」提醒从第 4 步提前到第 2 步**:`debugfix` 0.535×(快约 47%)、`ledger` 0.506×(快约 49%)、`migrate` 0.751×;13 对合并 **0.625×**(快约 38%)、输出 token **0.614×**(少约 39%)。再提前到第 3 步,`migrate` 单族 **0.509×**(快约 49%)、输出 token **0.528×**(少约 47%)。
31
32
  - **质量**:上述每一对里两臂的隐藏断言逐格相同、全绿;快的是少做几遍「改一版→再读一遍→再改一版」,不是少验证。
32
33
  - **不承诺的地方**:1–5 步的小改修与数据整形收不回提醒成本(`perfrefactor` 1.18×、`csvpipe` 1.12×),所以按任务形状先试一次再定。
@@ -0,0 +1,149 @@
1
+ # 基准:确定性长任务套件
2
+
3
+ `BENCHMARK.md` 里所有 1.5.0 的数字都由这个目录里的三样东西生成:**fixture**(`suite/`)、**跑批器**(`suite-run.py`)、**汇总器**(`report.py`)。把它们放进仓库,是为了让「快了多少、有没有掉正确性」这件事可以被别人重新跑一遍,而不是只有一段自述。
4
+
5
+ ## 目录
6
+
7
+ ```
8
+ bench/
9
+ suite/<family>/ 10 个家族,每个一个可独立跑的 fixture(见 suite/CONTRACT.md)
10
+ PROMPT.txt 交给 agent 的任务描述(agent 的工作目录是空的 run 目录)
11
+ hidden/test_<id>.py 隐藏测试,agent 看不到,跑完一轮由 runner 复制进去执行
12
+ ref/<entrypoint> 参考实现,必须 100% 通过隐藏测试
13
+ meta.json id / entrypoint / minSteps / maxSteps / seed(可选起始仓库)
14
+ arms/<arm>.yml 每个臂一行 fast-mode 配置,作为 `dsh bench --patch` 的补丁
15
+ suite-run.py 跑批:一次 (task, pair, arm) 一行 JSON,断点续跑
16
+ report.py 汇总:总量、分家族、逐对中位、尾部、门禁判定
17
+ results/suite-*.jsonl 上面三块主实验的原始行(可重算 BENCHMARK 的每个数字)
18
+ ```
19
+
20
+ ## 协议(为什么数字能拿来比)
21
+
22
+ 1. **一个单元 = 一次完整任务。** 一个 turn 跑完,隐藏测试事后执行。判定只看隐藏测试,不看模型自己说「做完了」。
23
+ 2. **ABBA 配对。** 第 `p` 对里奇数对按 `--arms` 给定顺序跑,偶数对倒序。机器负载在块内漂移时,两个臂在时间上对称分布。
24
+ 3. **只统计完整单元。** 总量只加「所有臂都在该单元里跑完且隐藏测试全绿」的那些 `(task, pair)`;被剔掉的单元、红掉的运行都单独列出来,不参与比值。
25
+ 4. **门禁规则。** 基线绿、某个臂红 ⇒ 该臂**不合格**,不许做默认值,哪怕总量更快。这是「宁慢不降质」的可执行版本,`report.py` 会打印 `GATE VIOLATIONS`。
26
+ 5. **指标是整任务总量**:墙钟(含排队/等待)、输出 token、步数。逐对中位只是辅助,不做结论。
27
+ 6. **重跑覆盖。** 同一单元出现多行时取最后一行(`--dedupe last`,默认);想复盘重跑前的块用 `--dedupe first`。
28
+ 7. **断点续跑。** 结果追加写入 `results/suite-<tag>.jsonl`,已存在的 `(task, pair, arm)` 会跳过,所以 app 重启、机器休眠都不会毁掉一整块。
29
+ 8. **运行目录要离 fixture 远。** run 目录默认放 `$TMPDIR/dsh-runs/`(macOS 上是 `/var/folders/.../T/`)。这条协议修补过两次:先是 run 目录在仓库内(`bench/.runs/`,fixture 只隔两层 `../../suite/<task>/hidden`,探索型运行读到过答案);后来 run 目录搬到 `/tmp`,但 `/tmp` 里留着上一轮的 fixture 镜像 `/tmp/fm-bench/suite/`,一次 `migrate` 运行列了 `/tmp` 就找到它,读了 `hidden/test_migrate.py` 和 `ref/migrate.py`(`W1` 里被 canary 抓到,那一行已作废)。现在:任何 `bench/suite` 的外部副本都不许留在临时目录,run 根在 OS 临时根下。每次运行结束会扫自己的会话记录,命中 `/hidden/`、`/ref/`、`bench/suite/`、`fm-bench/suite`、`MANIFEST.json` 就在行里记 `leak: true`,这类行必须丢弃重跑。`report.py` 默认会删掉它们并打印 `leak: dropped`。
30
+ 9. **步数与 token 只算本会话。** `~/.dsh/fast-mode-metrics.jsonl` 是全机共享的;旧实现按字节窗口切分,会把同一时刻别的会话(例如正在交互的会话)的行算进来,`steps` 因此虚高(实测一次约 12 步的运行被记成 156 步)。现在按 run 目录对应的 session id 过滤:`steps` 是精确值,`stepsRaw` 保留旧窗口值,`hasSessionIds` 说明是否命中 session id(历史行用旧名 `exact`,读取时回退)。墙钟由 runner 直接计时,不受影响。
31
+
32
+ 10. **判分在产物目录的副本里做。** 任务可能把自己的工作目录改成只读(`shellops` 家族真的会 `chmod -R a-w`),旧写法直接把隐藏测试写进产物目录,于是 `EACCES` 让整块批次中断(`W3` 在 31/60 行处崩过一次)。现在 `verify()` 先 `copytree` 到临时目录、跑完删掉,隐藏测试与测试产生的文件都不会落进被统计的产物里。
33
+ 11. **一块对应一个 fixture 摘要。** `bench/MANIFEST.json` 的 `aggregate` 是整套 fixture 的 sha256,跑批器开跑前校验,对不上就 `fixture drift` 拒绝运行。改过题面/隐藏测试后必须先重生成摘要,新旧摘要的批次不要混在一起报数。`W3` 是 `2699cd64…`,`W4` 起是 `2d7546f8…`(`ledger` 题面补明系数按 Decimal 解析)。
34
+
35
+ 12. **跑批期间对模型隐藏判分与参考解。** 模型和跑批器用同一个用户、同一台机器,用户能读的路径模型都能读。`W5` 的 `shellpipeline p1 bv15` 就从临时工作目录一路 `find` 到仓库,读了 `hidden/test_shellpipeline.py` 和 `ref/`(这一行已按泄漏剔除,不计入任何比值)。现在跑批器开跑前先把每个任务的 `hidden/`、`ref/` 改成 `chmod 000`:题面和测试模块此时已读进内存,`verify()` 从内存把测试写进临时副本再判分,`seed` 复制不受影响;块结束、`SIGINT` 或 `SIGTERM` 时恢复原权限。仓库内旧的 `bench/.runs/` 已删除并写进 `.gitignore`,仓库外的历史批次归档也一并置为不可读。泄漏哨兵(`LEAK_MARKERS`)保留,任何一块出现泄漏行都必须点名,不能静默丢弃。
36
+
37
+ 13. **一个臂必须先证明自己的提醒真的到过模型,才能拿来比。** `W8b` 的 `pp16` 与 `W5` 的 `bv15` 都栽在这上面:跑批正常、门禁全绿、数字齐全,但提醒一条都没下发,于是「臂」和「默认」其实是同一个东西,差值只是噪声。判据分两层:① 跑批器/插件日志里出现该指令的 `directive=<kind>`;② 该次运行的会话记录里出现该指令的下发文案(例如 `that step sent a whole file`)。两层都要有,缺一层就当**未测**,不能当「证伪」也不能当「变慢」。改完这类机制的代码之后,先在单格上验证「修复前 0 次、修复后 ≥1 次」,再开整块。
38
+ 14. **一次性提醒只能由真正投递的那一次消耗。** 同一步里调度结果会被多个钩子读取(动手前的投递点、系统提示解析、策略钩子),只有投递点会显示提醒。只读的调用方必须传 `{emit: false}`,否则它们会把「已发」标记写掉却不显示——提醒在日志里像发过,在会话里根本没有。`index.js` 里三处 `autoSchedule` 调用:只有 `agent/pre-step` 那处投递,另外两处是只读。
39
+
40
+ 15. **先量一个机制的余量,再为它开一块。** 新机制上线前先在已有会话记录上估值:它要替代的那类动作在一次运行里占多少。`workspaceMap` 就是这么定下来的——W8b 的 `def15` 会话里 `ls`/`find`/`cat`/`grep` 这类探索调用 79/221(35.7 %),一个会话前两次调用里 40 % 是它,中位 11.5 次调用/会话;相比之下整文件重写重复率只有 0.8 %(`preferPatch` 的上限),精确重复调用 2.1 %(去重没有余量)。余量不足一个百分点的机制不值得用 20 格去测,先在 transcript 上算清楚能省多少。
41
+ 16. **工作目录落地有先后,空读不能记成「已发」。** 基准的 fixture 在会话建好之后才拷进运行目录,所以第 1 步读到的目录是空的;`workspaceMap` 因此只有在真正建出清单后才标记为已发,空读留到下一步重试(`MAPDBG4` 上第 1、2 步为空,第 3 步建出 291 字符的清单)。一次性提醒如果在这一层被消耗掉,整臂就会像没下发一样安静。
42
+
43
+ 16. **投递审计用脚本,不用眼数。** `python3 bench/audit-delivery.py --tag W10 --needle "Auto map: one inventory"` 按「任务 × 对 × 臂」列出该文案在会话记录里出现的次数:对照臂必须 0、处理臂必须 ≥ 1,处理臂某格 0 就把那格记成**未测**。W10 的结果是 10/20 命中(10 个 `map15` 全 1、10 个 `def15` 全 0),一句命令就能复核,不用再逐目录 grep。
44
+
45
+ ## 怎么跑
46
+
47
+ ```bash
48
+ # 一条命令跑一块:10 个家族 × 2 个臂 × 3 对 = 60 次运行(约 1.5–2 小时)
49
+ python3 bench/suite-run.py --tasks codebase,codeimpl,csvpipe,debugfix,ledger,migrate,perfrefactor,shellops,shellpipeline,stategraph \
50
+ --arms off,def15 --pairs 3 --tag LOCAL1
51
+
52
+ # 汇总(--baseline 必填;--arm 不给就报所有臂)
53
+ python3 bench/report.py --files bench/results/suite-LOCAL1.jsonl --baseline off
54
+ ```
55
+
56
+ 依赖与约定:
57
+
58
+ - macOS 上的 DeepSeek Harness 应用;默认路径写在 `suite-run.py` 顶部,可用 `--dsh-exe` / `--dsh-cli` 或环境变量 `DSH_BENCH_EXE` / `DSH_BENCH_CLI` 覆盖。
59
+ - 模型凭据从 `COMPANY_LLM_TOKEN` 或 `~/.dsh/.credentials.yaml` 的 `refs.COMPANY_LLM_TOKEN` 取。
60
+ - 每轮运行的逐步数据追加在 `~/.dsh/fast-mode-metrics.jsonl`;`suite-run.py` 按本次运行的 session id 切出步数与 token(`stepsRaw` 是旧的字节窗口口径)。
61
+ - 单轮上限 1500 秒,超时记为 `exit: -9`,该单元判红。
62
+
63
+ ## 已发布实验(本目录可复算)
64
+
65
+ > **撤回(2026-10-26)**:下表前三块(`V15`/`TA3`/`TA4`)的数字全部作废。两个测量缺陷:运行目录曾在仓库内,会话能读到 `../../suite/<task>/hidden` 与 `ref/`;指标按字节偏移切片,把同机器其他会话的行算进来(12 步被记成 156 步)。数字保留仅为记录,**不要引用**。
66
+
67
+ | tag | 设计 | 状态与结果 |
68
+ |---|---|---|
69
+ | `V15` / `TA3` / `TA4` | 见旧记录 | **已撤回**(fixture 泄漏 + 跨会话指标混合) |
70
+ | `W1` | 6 家族 × 2 对,`off`/`def15`/`odt15` 三臂 | 干净复测首块:36 次运行、12 个完整格、1 行泄漏作废、1 红(`ledger` p1 `def15` 21/22)。`def15` 总和 **0.65× 墙钟 / 0.68× 输出 / 0.64× 步数**(10 个过门禁的格);单格中位 **0.98×**。`odt15` 0.85× / 0.83×(常开“压缩输出”只有微弱收益,且是步数带来的,不是 token) |
71
+ | `W3` | 10 家族 × 3 对,`off` vs `def15` | **当前声明来源**:60 次运行、30 个完整格、0 行泄漏、60/60 全绿。`def15` 总和 **0.72× 墙钟 / 0.74× 输出 / 0.71× 步数**;逐格墙钟中位 **0.64×**、均值 0.84×、90 % 区间 [0.59, 0.79]、30 格里 24 格更快。`off` 3434.7 s / 591 244 token / 390 步 → `def15` 2472.5 s / 439 708 token / 276 步。批次中途因 `shellops` 把产物目录 `chmod` 成只读、判分 `EACCES` 中断过一次(31/60 行),修好判分后从断点续跑补齐。 |
72
+
73
+ `W1` 的总和比与单格中位差得很远(0.65× vs 0.98×),而 `W3` 把两者收拢到 0.72× / 0.64×:**总和比被少数反向尾部抬起,单格中位代表典型收益,两个必须一起报**,只报一个都会误导。`W3` 用的是旧版 `ledger` 题面(摘要 `2699cd64…`);`ledger` 题面在 `W3` 之后补明了「规则里的系数/偏移按 Decimal 原样解析」,摘要变为 `2d7546f8…`,`W4` 起全部用新版题面,因此 `W3` 的 `ledger` 三格与之后的批次不逐字可比(两臂看到的是同一份题面,结论方向不变)。
74
+
75
+ ```bash
76
+ python3 bench/report.py --files bench/results/suite-W1.jsonl --baseline off
77
+ python3 bench/report.py --files bench/results/suite-W3.jsonl --baseline off
78
+ ```
79
+
80
+ `report.py` 会把红行与门禁违规逐条列出来,不做静默剔除;已撤回的 `suite-{SG1,SG2,V15,TA3,TA4}.jsonl` 不再保留在仓库里。
81
+
82
+ ## 加一个家族
83
+
84
+ 照 `suite/CONTRACT.md` 写:`PROMPT.txt`(规格完整到只有一个合理解)、`hidden/test_<id>.py`(≥ 12 条断言,参考实现全绿、改坏一个边界必须红)、`ref/`、`meta.json`(10–25 步难度)。然后把这个家族加进跑批的 `--tasks`,至少在两个臂上各跑 6 对,再看 `report.py` 的门禁那一行。
85
+
86
+ ## 读这些数字时要知道
87
+
88
+ - **绝对墙钟不可跨日比较**:机器是共享的,本轮期间同机还有另一个基准在跑(约 20 load / 12 核)。比值与同单元配对是有效的,绝对值不是。
89
+ - **门禁只覆盖隐藏测试**:解释质量、代码风格、可维护性不在断言里。
90
+ - **家族是合成的**:10 个家族覆盖写文件、读文件、状态机、shell 运维、调试和性能重构,但不等于你的真实仓库。
91
+ - **总和比与单格中位必须一起看。** `W1` 里 `def15` 总和 0.65×、单格中位 0.98×:加速集中在少数「基线自己绕远」的格子上。只报总和会把少数长尾当成人人受益;只报中位会抹掉真实存在的大幅加速。两个都写,并给出格数。
92
+
93
+ ## 臂配置与提醒投递核对
94
+
95
+ 两条命令回答「这一臂到底被允许发什么提醒、实际发没发」:
96
+
97
+ ```bash
98
+ node bench/arm-config.mjs bench/arms/disc-only.yml # 臂 -> resolveConfig 解析结果
99
+ node bench/verify-arm-reminders.mjs --mechanisms # 每个开关能开/能关,10 项
100
+ node bench/verify-arm-reminders.mjs --arms # 臂 x 场景的提醒矩阵,8 项
101
+ ```
102
+
103
+ 两者都不发请求、不花钱:`arm-config.mjs` 直接 import `src/effort.js` 的 `resolveConfig`,`verify-arm-reminders.mjs` 直接 import `src/auto.js` 的 `createAutoState/noteTurn/noteEntry/noteStep/notePolicy/noteOutputTokens/autoSchedule/noteTool`,在本地按 `index.js` 的调用顺序驱动一遍,记录每次 `autoSchedule` 返回的 `directive`。不依赖 `yaml` 包(仓库 runtime 依赖为空,臂 YAML 由 `arm-config.mjs` 自带的小解析器读)。
104
+
105
+ ### 每个机制对应哪个开关
106
+
107
+ | 机制(提醒 `kind`) | 打开 | 关掉 |
108
+ |---|---|---|
109
+ | 并行/引导(`guidance`,非提醒) | `guidance: true`、`parallel > 1` | `guidance: false`,`parallel: 1` |
110
+ | 输出压缩提醒(`compress-output`) | `outputDiscipline: 'late'`(或 `true`)+ `disciplineAfterSteps` / `disciplineEarlyCalls` | `outputDiscipline: false`(解析成 `discipline: 'off'`),或 `scheduler: false` |
111
+ | 二次提醒(同 `compress-output`,第二次) | `disciplineRepeatAtSteps: N > 0` | `disciplineRepeatAtSteps: 0` |
112
+ | 提前提醒的额外条件 | `disciplineEarlyCalls: k > 0` | `disciplineEarlyCalls: 0` |
113
+ | 复用校验提醒(`reuse-check`) | 无独立开关,开了 scheduler 就可能发 | 只有 `scheduler: false` |
114
+ | 卡住换法提醒(`change-approach`) | 无独立开关,开了 scheduler 就可能发 | 只有 `scheduler: false`;`stallGuard`/`toolTimeoutMs` 只影响停顿判定,不是提醒开关 |
115
+ | 批量校验(`batch-verify`) | `batchVerify: true` | `batchVerify: false` |
116
+ | 先补丁后整写(`prefer-patch`) | `preferPatch: true` | `preferPatch: false` |
117
+ | 一次开放多文件(`open-batch`) | `openBatch: true` | `openBatch: false` |
118
+ | 收尾提示(`finish-line`) | `finishLineSteps: N > 0` | `finishLineSteps: 0` |
119
+ | 输出预算(`output-budget`) | `outputBudget: N > 0` | `outputBudget: 0` |
120
+ | 整轮步数预算(`stop-and-report`) | `stepBudget: N > 0` | `stepBudget: 0` |
121
+ | 工作区地图(非提醒,注入 prompt) | `workspaceMap: true` | `workspaceMap: false` |
122
+
123
+ **由此得到的结论**:想测「只有输出纪律」就必须至少关掉 `guidance`/`parallel`/`openBatch`/`preferPatch`/`batchVerify`/`finishLineSteps`/`stepBudget`/`outputBudget`/`shapeGate`,并把提醒本身设成 `outputDiscipline: 'late'`;`reuse-check` 与 `change-approach` 没有开关,只能整块关 scheduler,所以「输出纪律单独」在真实跑批里只能靠 transcript 里的固定文案(`prose is now the cost` / `still long. Keep`)筛出来,不能靠臂差。
124
+
125
+ ### 旧 `disc-only.yml` 为什么是空转
126
+
127
+ 旧臂设了 `outputDiscipline: 'late'` 却又写了 `scheduler: false`。`'late'` 这种「到第 N 步才发」的提醒**只由 scheduler 投递**(`src/auto.js` 的 `directiveFor` 第一行就是 `if (!entry.scheduler) return undefined`),所以那一臂一次提醒都没发,「只测输出纪律」这个假设从未被测到。现在的 `disc-only.yml` 保留 scheduler、只关其余机制,`verify-arm-reminders.mjs --arms` 用它跑长读场景会拿到 `compress-output@step4`,跑卡住场景会拿到 `compress-output@step4, change-approach@step8`。
128
+
129
+ ## 每行 result 现在带提醒证据
130
+
131
+ `bench/suite-run.py` 写给 `bench/results/suite-<tag>.jsonl` 的每一行多三个字段:
132
+
133
+ - `armConfig`:该臂经 `resolveConfig` 解析后的提醒相关子集(`POLICY_KEYS`),行自身就能说明「这一臂被允许发什么」;
134
+ - `reminders`:`[{step, turnStep, kind}]`,本次运行实际投递的每条提醒(`kind` 就是 `directive` 名,`step` 是该 turn 内的步序);
135
+ - `reminderCounts`:`{kind: n}` 汇总;另有 `hasSessionIds` 取代旧名 `exact`。
136
+
137
+ 采集方式:跑这一格时把臂补丁另存为 `<tag>.arm.yml` 并加 `debug: true` + `debugFile: <runs>/<tag>.debug`,跑完从调试日志里按 `step=N ... turnSteps=M ... directive=kind` 抽行,然后删掉日志与补丁。`debug` 只在步边界写一行已经格式化好的文本,不参与任何调度判断、不额外发请求;日志写在 run 目录之外,所以 `leak_scan`/session id 统计不受影响。老 result 行没有这些字段,读取方一律用 `.get()`,缺字段等于「没有证据」,不是「零提醒」。
138
+
139
+ ## 正文占比与预注册门禁脚本
140
+
141
+ ```bash
142
+ python3 bench/textshare.py --limit 0 # 全量 def15:正文占 (正文+工具参数) 的 1.18 %
143
+ python3 bench/d2gsign.py D2G_IMPL # 每个会话第一条压缩提醒落在第几步
144
+ python3 bench/d2gscore.py # d2g 预注册五条判据 + 下发步位
145
+ python3 bench/dr8sign.py DR8_MIG # 二次提醒是否真的发出去
146
+ python3 bench/dr8score.py # dr8 预注册判据 + 下发核对
147
+ ```
148
+
149
+ 这四个 `d2g*`/`dr8*` 脚本原先只在 `~/.local/fm/`,现在搬进 `bench/`:路径改成相对仓库(`pathlib.Path(__file__).resolve().parent`),会话检索只依赖 `~/.dsh/sessions`(不要求临时 run 目录还在),且同时认 `hasSessionIds` 与历史 `exact`。在现有 result 上跑出来的判定与 BENCHMARK.md 的 HOLD 结论一致(`D2G_IMPL d2g step 4` 一行 FAIL,实际第 3 步)。
@@ -0,0 +1,74 @@
1
+ #!/usr/bin/env python3
2
+ """Paired wall-clock analysis for one two-arm block.
3
+
4
+ Usage: block_score.py TAG BASE ARM [--min-steps N] [--json]
5
+
6
+ Ratios are computed inside each (task, pair) cell so machine load cancels.
7
+ A row is usable only when the runner marked it as attributable to its own
8
+ session ids (field `hasSessionIds`, formerly `exact`) and green with a real transcript; probe-dead rows (one step, zero output, ~2 s wall) are dropped and
9
+ counted separately, never averaged in.
10
+ """
11
+ import json
12
+ import math
13
+ import pathlib
14
+ import statistics
15
+ import sys
16
+
17
+ ROOT = pathlib.Path('/Users/apple/Desktop/DSH/dsh-fast-mode')
18
+
19
+
20
+ def usable(row, min_steps):
21
+ if not row:
22
+ return False
23
+ # steps/out are attributable only if the run reported its own session ids;
24
+ # the legacy field name for that boolean was `exact`.
25
+ if row.get('exit') != 0 or not row.get('hasSessionIds', row.get('exact')) or row.get('leak'):
26
+ return False
27
+ if not row.get('green', True):
28
+ return False
29
+ return row.get('steps', 0) >= min_steps and row.get('out', 0) > 0
30
+
31
+
32
+ def geo(values):
33
+ return math.exp(sum(math.log(v) for v in values) / len(values)) if values else float('nan')
34
+
35
+
36
+ def main():
37
+ argv = [a for a in sys.argv[1:] if not a.startswith('--')]
38
+ tag, base_arm, arm = argv[0], argv[1], argv[2]
39
+ min_steps = 3
40
+ if '--min-steps' in sys.argv:
41
+ min_steps = int(sys.argv[sys.argv.index('--min-steps') + 1])
42
+ rows = [json.loads(l) for l in (ROOT / f'bench/results/suite-{tag}.jsonl').read_text().splitlines() if l.strip()]
43
+ cells = {}
44
+ for row in rows:
45
+ cells.setdefault((row['task'], row['pair']), {})[row['arm']] = row
46
+ wall, steps, out = [], [], []
47
+ dropped = []
48
+ for (task, pair), arms in sorted(cells.items()):
49
+ b, a = arms.get(base_arm), arms.get(arm)
50
+ if not (usable(b, min_steps) and usable(a, min_steps)):
51
+ dropped.append(f'{task} p{pair}')
52
+ continue
53
+ wall.append(a['wall'] / b['wall'])
54
+ steps.append(a['steps'] / b['steps'])
55
+ out.append(a['out'] / b['out'])
56
+ print(f'{task:14s} p{pair} {base_arm}={b["wall"]:7.1f}s {arm}={a["wall"]:7.1f}s '
57
+ f'wall={wall[-1]:.3f} steps {b["steps"]}->{a["steps"]} out {b["out"]}->{a["out"]}')
58
+ n = len(wall)
59
+ print(f'\n{tag}: {n} usable pair(s), dropped {len(dropped)}'
60
+ + (f' ({", ".join(dropped)})' if dropped else ''))
61
+ if not n:
62
+ sys.exit(1)
63
+ wins = sum(1 for v in wall if v < 1)
64
+ print(f'wall geo={geo(wall):.3f} median={statistics.median(wall):.3f} '
65
+ f'wins={wins}/{n} min={min(wall):.3f} max={max(wall):.3f}')
66
+ print(f'steps geo={geo(steps):.3f} out geo={geo(out):.3f}')
67
+ # exact sign test, two-sided
68
+ if 0 < wins < n:
69
+ tail = sum(math.comb(n, k) for k in range(min(wins, n - wins) + 1)) / 2 ** n
70
+ print(f'sign test p={min(1.0, 2 * tail):.3f}')
71
+
72
+
73
+ if __name__ == '__main__':
74
+ main()
package/index.js CHANGED
@@ -97,7 +97,7 @@ const TAG = '[fast-mode]'
97
97
  * Shipped identity of this build. Traced into the debug log so a stale install is
98
98
  * visible at a glance, and pinned by a test against `package.json`.
99
99
  */
100
- export const VERSION = '1.5.19'
100
+ export const VERSION = '1.5.20'
101
101
  const GLOBAL_AGENT = '__global__'
102
102
 
103
103
  /**
@@ -667,7 +667,7 @@ export function apply(ctx, rawConfig) {
667
667
  // The dedupe key is sorted and truncated, so it is not evidence about
668
668
  // which files a call touched. Hand the scheduler the real targets, plus
669
669
  // the session directory a relative target resolves against.
670
- const targets = toolTargets(args)
670
+ const targets = toolTargets(args, typeof exec.name === 'string' ? exec.name : '')
671
671
  const observation = {
672
672
  name: typeof exec.name === 'string' ? exec.name : 'unknown',
673
673
  argsKey: stableKey(exec.arguments),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-fast-mode",
3
- "version": "1.5.19",
3
+ "version": "1.5.20",
4
4
  "description": "DeepSeek Harness fast mode: same thinking effort, fewer round trips. An adaptive parallel tool fan-out, output discipline that never trims the answer, and a cancellable stall guard. Never changes reasoning effort, the model, or the request body. Provider-agnostic, one dependency (@deepseek-ai/schemastery).",
5
5
  "repository": {
6
6
  "type": "git",
@@ -38,6 +38,7 @@
38
38
  "README.en.md",
39
39
  "CHANGELOG.md",
40
40
  "BENCHMARK.md",
41
+ "bench/block_score.py",
41
42
  "docs/",
42
43
  "LICENSE"
43
44
  ],
package/src/auto.js CHANGED
@@ -519,12 +519,42 @@ const COMMAND_FIELDS = ['command', 'cmd', 'script', 'shell']
519
519
  /** Argument fields a write call keeps its target paths in. */
520
520
  const PATH_FIELDS = ['path', 'file_path', 'filePath', 'target', 'file', 'filename']
521
521
 
522
+ /** Patch-shaped arguments: the targets live in the diff text, not in a path field. */
523
+ const PATCH_CALLS = Object.freeze(new Set(['apply_patch', 'patch', 'apply_diff', 'edit_patch']))
524
+ const PATCH_TEXT_FIELDS = ['patch', 'patch_text', 'diff', 'input']
525
+ const PATCH_HEADER = /^[ \t]*(?:\*\*\* (?:Update File:|Add File:|Move to:)[ \t]*|[+]{3}[ \t]+b\/)(.+?)[ \t]*$/gm
526
+
522
527
  /** How many written files stay tracked for the fingerprint recheck. */
523
528
  const WRITE_WINDOW = 64
524
529
 
525
530
  const MUTATING_CALLS = Object.freeze(new Set(['write', 'write_file', 'create_file', 'edit', 'multi_edit', 'apply_patch', 'patch']))
526
531
  const SHELL_CALLS = Object.freeze(new Set(['bash', 'shell', 'terminal', 'run_code', 'exec']))
527
532
 
533
+ /**
534
+ * Files a patch names, read from its headers: `*** Update File:` / `*** Add File:` /
535
+ * `*** Move to:` (the agent patch dialect) and `+++ b/<path>` (unified diff).
536
+ *
537
+ * Without this a patch-based edit names no path, the write cannot be fingerprinted,
538
+ * and the session permanently stops offering reuse evidence — the common edit path
539
+ * would pay the whole cost of a feature it never uses.
540
+ *
541
+ * @param {string} text
542
+ * @returns {string[]}
543
+ */
544
+ function patchTargets(text) {
545
+ if (typeof text !== 'string' || text.length === 0) return []
546
+ const paths = []
547
+ PATCH_HEADER.lastIndex = 0
548
+ let match = PATCH_HEADER.exec(text)
549
+ while (match) {
550
+ // A unified-diff `+++ b/` line may carry a trailing tab-separated timestamp.
551
+ const raw = match[1].split('\t')[0].trim()
552
+ if (raw.length > 0 && raw !== '/dev/null') paths.push(raw)
553
+ match = PATCH_HEADER.exec(text)
554
+ }
555
+ return paths
556
+ }
557
+
528
558
  /**
529
559
  * What a tool call actually names: the files a mutation targets, the command text
530
560
  * a shell call runs. Read from the **untruncated** arguments the hook receives.
@@ -536,9 +566,10 @@ const SHELL_CALLS = Object.freeze(new Set(['bash', 'shell', 'terminal', 'run_cod
536
566
  * `noteTool` so the reuse evidence covers the real artifact.
537
567
  *
538
568
  * @param {unknown} args
569
+ * @param {string} [name] - tool name; only patch-shaped tools are scanned for diff headers
539
570
  * @returns {{paths: string[], command: string}}
540
571
  */
541
- export function toolTargets(args) {
572
+ export function toolTargets(args, name) {
542
573
  const paths = []
543
574
  const commands = []
544
575
  if (typeof args === 'string') {
@@ -554,6 +585,12 @@ export function toolTargets(args) {
554
585
  if (typeof value === 'string') commands.push(value)
555
586
  else if (Array.isArray(value)) commands.push(...value.filter(part => typeof part === 'string'))
556
587
  }
588
+ if (PATCH_CALLS.has(typeof name === 'string' ? name : '')) {
589
+ for (const field of PATCH_TEXT_FIELDS) {
590
+ const value = args[field]
591
+ if (typeof value === 'string') paths.push(...patchTargets(value))
592
+ }
593
+ }
557
594
  }
558
595
  return {
559
596
  paths: paths.filter(path => path.length > 0 && path.length <= 4096),