dsh-fast-mode 1.5.1 → 1.5.3

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
@@ -190,6 +190,121 @@ W8b 之后用 `def15` / `pp16` 在 5 个写入型家族上重测 `preferPatch`
190
190
  1. **修复后提醒确实到过模型**:`csvpipe` p1 的 `pp16` 会话里检索到下发文案 `that step sent a whole file` 命中 1 次。W8b 的 20/20 零下发就此失效,`preferPatch` 从「因 liveness 缺陷作废」变成「已下发、效果待测」。
191
191
  2. **4 格不足以下任何比值结论**,尤其 `ledger` 那格 `pp16` 输出翻倍(15 k → 34 k tok),属于单格长尾。按 C 第 3 条的预登记,这个机制的余量上限约等于输出的 0.2 %,所以在拿到整块之前**不许把上面两个比值当效果**。
192
192
 
193
+ ## 筛选块 W10(2026-10-26):`workspaceMap` 首测,10 个单元全绿,逐格中位 0.565×、总量 0.80×,两格长尾反向
194
+
195
+ `workspaceMap` 是 `def15` 之上加一个开关的臂(`bench/arms/map15.yml`):会话第一次工具调用之前,harness 自己把工作目录走一遍(深度 2、≤60 条、跳 `node_modules`/`.git`/`dist`,正文 ≤1.4 kB),把「相对路径 + 字节数」和看得见的测试入口作为一条通知交给模型。它替代的正是 W8b 里最集中的那类调用——`ls`/`find`/`cat`/`grep` 占 221 次调用的 35.7 %,前两次调用里 40 % 是它。
196
+
197
+ ### A. 总量(10 个完整单元,20 次运行)
198
+
199
+ | 臂 | 墙钟 | 墙钟比 | 输出 token | 输出比 | 步数 | 步数比 |
200
+ |---|---|---|---|---|---|---|
201
+ | `def15` | 2181.8 s | 1.00 | 112 597 | 1.00 | 72 | 1.00 |
202
+ | `map15` | 1755.3 s | **0.80×** | 101 388 | **0.90×** | 82 | 1.14× |
203
+
204
+ 逐格墙钟中位 **0.565×**,10 格里 7 格更快;p50 从 101.2 s 降到 122.7 s(反向尾部更重),p90 从 425.3 s 降到 314.3 s。
205
+
206
+ ### B. 分家族与逐格(`map15` ÷ `def15`)
207
+
208
+ | 家族 | 对数 | 墙钟 / 输出 / 步数(2 对加总口径) |
209
+ |---|---|---|
210
+ | `migrate` | 2 | 0.49 / 0.58 / 1.00 |
211
+ | `debugfix` | 2 | 0.58 / 0.60 / 0.77 |
212
+ | `perfrefactor` | 2 | 0.78 / 0.71 / 0.90 |
213
+ | `stategraph` | 2 | 1.11 / 1.30 / 1.50 |
214
+ | `codebase` | 2 | 1.18 / 1.34 / 1.42 |
215
+
216
+ | 格 | `def15` 墙钟/步数/输出 | `map15` 墙钟/步数/输出 | 墙钟比 |
217
+ |---|---|---|---|
218
+ | `codebase` p1 | 85.1 s / 7 / 5 570 | 47.8 s / 6 / 3 002 | 0.56× |
219
+ | `codebase` p2 | 89.1 s / 5 / 3 171 | 157.4 s / 11 / 8 720 | 1.77× |
220
+ | `debugfix` p1 | 54.5 s / 8 / 3 343 | 47.3 s / 4 / 2 575 | 0.87× |
221
+ | `debugfix` p2 | 113.3 s / 5 / 6 735 | 50.1 s / 6 / 3 498 | 0.44× |
222
+ | `migrate` p1 | 389.6 s / 9 / 22 211 | 156.1 s / 7 / 9 485 | 0.40× |
223
+ | `migrate` p2 | 483.9 s / 10 / 23 696 | 274.8 s / 12 / 17 328 | 0.57× |
224
+ | `perfrefactor` p1 | 81.7 s / 5 / 3 885 | 89.3 s / 4 / 4 119 | 1.09× |
225
+ | `perfrefactor` p2 | 83.3 s / 5 / 5 463 | 39.7 s / 5 / 2 502 | 0.48× |
226
+ | `stategraph` p1 | 418.7 s / 6 / 20 820 | 222.5 s / 7 / 17 801 | 0.53× |
227
+ | `stategraph` p2 | 382.5 s / 12 / 17 703 | 670.2 s / 20 / 32 358 | 1.75× |
228
+
229
+ ### C. 质量门禁与投递
230
+
231
+ - **10/10 次运行隐藏测试全绿,零泄漏,零崩溃**;两臂通过率都是 1.000,没有「基线绿而该臂红」。
232
+ - **投递逐格核对**:10 个 `map15` 会话各命中 `Auto map: one inventory` **1 次**,10 个 `def15` 会话各 **0 次**——因此臂与默认确实不是同一个东西,差值不是「提醒没发」造成的噪声。
233
+ - 清单本身很小:`codeimpl` 那格实测 291 字符(fixture 只一个文件),深度与条数上限在 `src/map.js`。
234
+
235
+ ### D. 结论与下一步
236
+
237
+ 1. **主结论:在 7/10 格上更快,逐格中位 0.565×,总量 0.80×,零红**。省的地方对得上机制:省掉的探索步让输出整体降到 0.90×,`migrate`/`debugfix` 这种「先找文件再改」的任务收益最大。
238
+ 2. **两格反向长尾必须点名**:`codebase` p2(1.77×,步数 5→11、输出 3.2 k→8.7 k)与 `stategraph` p2(1.75×,步数 12→20、输出 17.7 k→32.4 k)。两格都是 `map15` 变长而不是变短,说明清单本身不保证省——重任务里它可能让模型更早展开工作范围。**这两格不足以否定机制,但足以否决「只报中位」**。
239
+ 3. **不晋升为默认**:10 个单元、一次块,且总量只有 0.80×,不满足「独立复现」的门槛。按协议开 W11 复现块(7 家族 × 2 对 = 14 个单元,含 `codeimpl`/`csvpipe` 这类短任务用来查固定成本),复现通过才在 1.5.2 里把 `workspaceMap` 设成默认。**W11 没复现(合并 23 个单元 0.927×,尾部更慢),结论见下一节:保持默认关闭。**
240
+
241
+ ## 复现块 W11(2026-10-26):`workspaceMap` 没复现 W10 的收益,合并 23 个单元 0.927×,**不进默认**
242
+
243
+ W10 的 0.565× 逐格中位需要独立复现才能当结论。W11 用 7 个家族 × 2 对(14 个单元,含 `codeimpl`/`csvpipe` 这类短任务查固定成本)同块对照 `def15` 与 `map15`。
244
+
245
+ ### A. 总量(13 个有效单元,26 次有效运行)
246
+
247
+ | 臂 | 墙钟 | 墙钟比 | 输出 token | 输出比 | 步数 | 步数比 |
248
+ |---|---|---|---|---|---|---|
249
+ | `def15` | 2935.2 s | 1.00 | 162 971 | 1.00 | 99 | 1.00 |
250
+ | `map15` | 2988.6 s | **1.018×** | 145 584 | **0.893×** | 106 | 1.071× |
251
+
252
+ 去掉的 1 个单元是 `csvpipe` p1:`def15` 那次运行 `steps=0 / out=0 / wall=806.97`,是会话空转而不是「极快」,按判据作废,`map15` 那半格即使绿也不成对。
253
+
254
+ ### B. 分家族(`map15` ÷ `def15`,2 对加总口径;`csvpipe` 只剩 1 对)
255
+
256
+ | 家族 | 墙钟 / 输出 / 步数 |
257
+ |---|---|
258
+ | `debugfix` | 0.47 / 0.53 / 0.79 |
259
+ | `migrate` | 0.49 / 0.42 / 0.83 |
260
+ | `codebase` | 0.65 / 0.74 / 1.20 |
261
+ | `perfrefactor` | 1.30 / 1.18 / 0.90 |
262
+ | `codeimpl` | 1.32 / 1.40 / 1.29 |
263
+ | `stategraph` | 1.50 / 1.25 / 1.26 |
264
+
265
+ 反向 ≥1.3× 的格点名:`stategraph` p1 1.65×、`perfrefactor` p1 1.58×、`codeimpl` p2 1.40×、`stategraph` p2 1.40×。
266
+
267
+ ### C. 门禁与投递
268
+
269
+ - 13/13 有效单元、26/26 次运行隐藏测试全绿,零泄漏、零崩溃;两臂 pass rate 都是 1.000。
270
+ - 投递按 `bench/audit-delivery.py` 逐格复核:`map15` 每格恰好 1 条 `Auto map:`,`def15` 每格 0 条。
271
+
272
+ ### D. 合并读数与结论
273
+
274
+ W10(10 个有效单元)+ W11(13 个有效单元)合并,23 个单元:
275
+
276
+ | 口径 | 值 |
277
+ |---|---|
278
+ | 总墙钟 | **0.927×** |
279
+ | 总输出 token | **0.896×** |
280
+ | 总步数 | **1.099×** |
281
+ | 逐格墙钟中位 | **0.791×** |
282
+ | 更快的格 | 13 / 23 |
283
+
284
+ 1. **机制方向被证实一半**:输出 token 确实降了 10 %(W10 0.90×、W11 0.89×,两块一致),说明「先给一份目录清单」确实省掉了模型自己找文件的 token。
285
+ 2. **另一半没兑现**:步数两轮都升(W11 1.071×),净墙钟因此只剩 0.927×;而分母是 `def15` 而非 `off`,对外口径不能拿这 7 % 当卖点。
286
+ 3. **否决点是尾部,不是中位**:`map15` 的 p90 558.6 s、最慢 713.8 s,`def15` 是 369.1 s / 510.3 s。也就是说开关打开后**一部分长任务会更慢**,最长格慢了 40 %。W10 与 W11 都出现同一形状(`stategraph` 两轮都反向),不是单次噪声。
287
+ 4. **因此不晋升默认**:按「长任务不能变慢 + 需要两轮同向复现」的门槛,`workspaceMap` 保持默认关闭,作为旋钮发布并附上这两块的全部数字。1.5.2 的默认行为与 1.5.1 相同。
288
+
289
+ ## 已排除的方向(截至 1.5.2,按成本模型排序)
290
+
291
+ | 方向 | 实测 | 判据 | 剩余余量依据 |
292
+ |---|---|---|---|
293
+ | `outputBudget`/`bud8` | 0.920× | 慢于默认,淘汰 | 正文只占输出 6 % |
294
+ | `fanoutWidth: 12`/`wide15` | 1.148× | 慢于默认,淘汰 | 并发不减推理 |
295
+ | `openBatch`/`open15` | 0.837×、头对头 1.136× | 未复现,淘汰 | — |
296
+ | `preferPatch`/`pp16` | 未测出效果 | 余量上限 ≈ 输出 0.2 % | 153 次整文件写入里重复写同路径只占 0.8 % |
297
+ | 工具结果截断 | 未开跑,已测余量 | 无余量 | 236 条工具结果中位 1.2 kB、最大 18 kB、单会话中位 26 kB |
298
+ | 丢弃旧工具结果(上下文裁剪) | 未实现,先测余量 | 无余量 | 193 步里 `corr(上下文 totalTokens, 每步输出)` = **0.067**,每步输出中位 663 token 与上下文长度基本无关 |
299
+ | `workspaceMap` | 0.927×(23 单元) | 尾部变慢,不进默认 | 输出 −10 % 但步数 +10 % |
300
+ | `batchVerify` | 未测(liveness 缺陷已修) | 待测 | 测试/校验调用占 12.2 %,但步数项只占总墙钟 6 % |
301
+
302
+ 最后一行是这一轮算清的账:`batchVerify` 合并的是「重复验证的步」,而步数项只占 1.62 s × 319 步 ≈ 6 % 墙钟(输出项 97 %)。即使把它压到零,总量也只能到 0.94× 左右,**它不是通往 0.67× 的路**。
303
+
304
+ ## 0.67× 在现有约束下的可达性(结论)
305
+
306
+ 三块合并(W3+W5+W7,65 个有效单元)默认臂 0.742× 墙钟 / 0.758× 输出 / 0.585× 步数。要 0.67× 就必须再省 10 % 墙钟,而墙钟 97 % 来自输出 token;输出里 23 % 是不可压的工具参数(重复写只占 0.8 %、重复调用 2.1 %),工具结果本身很小,剩下的 71 % 是模型自己的推理。**在不改 `reasoningEffort`、不加请求字段、不砍完成判定与必要复验的前提下,harness 侧能动的量已经测完:目录清单 −10 % 输出但 +10 % 步数(净 0.93×)、重复验证 ≤6 % 墙钟、并发与预算类旋钮全部反向。** 因此 1.5.2 的诚实口径是:**已测范围内 0.72×–0.74× 是这套约束的地板,0.67× 需要放宽约束(`reasoningEffort` 或请求侧字段),本版不做。** 下一步若允许放宽,最小改动是测 `reasoningEffort` 低一档在同一套门禁下的表现;这需要用户明确授权。
307
+
193
308
  ## 1.5.0 重基线(2026-10-25,**已撤回,仅作历史**):9 家族 54 个单元,总墙钟 0.86×,总输出 0.95×
194
309
 
195
310
  > **撤回(2026-10-26)**:本节及 TA3/TA4 三块批次的每一步数字都作废,原因是两个测量缺陷:
@@ -468,7 +583,7 @@ E1,7 家族、每臂 14 对、隐藏测试全绿:
468
583
  - 收尾线是**建议**,不是截断:模型可以继续做。它靠的是“别再重复验证”这句话在有界上下文里真的生效,证据是输出 token 中位 −52 %。
469
584
  - 短任务仍无收益:1–5 步桶 0.66×(n=1,绝对值 17 s),4 步以内结束的一轮根本到不了第 10 步,代价为零。
470
585
  - 单一家族曾测到 2.68×(ledger,6 对),那是该家族的乐观值;跨 7 家族的定版数字是 **1.86×**。
471
- - 同一 `(任务, 臂, 对)` 有补跑时按各次运行的中位聚合再配对,避免挑最有利的一次;E1 的 54 行记录压成 42 个单元后 n 仍为 14。
586
+ - 同一 `(任务、臂、对)` 有补跑时按各次运行的中位聚合再配对,避免挑最有利的一次;E1 的 54 行记录压成 42 个单元后 n 仍为 14。
472
587
  - **12 对 vs 14 对**:下面分桶表只覆盖 14 对中落在四个步数桶里的 12 对,另 2 对不在桶内;E1 的原始记录未提交,无法从已提交数据重算那 14 对的比值,桶表不能当作 14 对的完整分解。
473
588
  - **语料与复现边界**:272 次运行里包含 C1/D1 的调参轮(不是每条都进定版结论);7 个家族 fixture 与 runner 未公开发布(只在本机 `/tmp/fm-bench`),因此外部无法逐字复跑;全部数据来自本地 `deepseek-v4.1-flash` 网关,换路由后的绝对值不可假定。
474
589
 
@@ -486,7 +601,7 @@ E1,7 家族、每臂 14 对、隐藏测试全绿:
486
601
  | 6–10 步 | 33 | **1.23×**(符号检验 p = 0.035) | 0.99–1.43 | 23/33 |
487
602
  | 11 步以上 | 0 | — | — | — |
488
603
 
489
- `corr(输出 token 变化, 加速) = −0.77`:快的那些对,都是模型写得少。输出 token 中位 831 → 756。这条规律解释了为什么“总体中位 1.06×”看起来弱——它把 47 对几乎没有可省空间的短任务平均进去了。
604
+ `corr(输出 token 变化,加速)= −0.77`:快的那些对,都是模型写得少。输出 token 中位 831 → 756。这条规律解释了为什么“总体中位 1.06×”看起来弱——它把 47 对几乎没有可省空间的短任务平均进去了。
490
605
 
491
606
  同一份语料还证明**并发不是来源**:短任务上并行宽度早就够用,长任务上的加速与工具扇出无关。
492
607
 
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.5.3
4
+
5
+ - **发布到 npm**:`dsh-fast-mode@1.5.3` 已在 registry 上(`npm i dsh-fast-mode` / 插件管理器目标填 `dsh-fast-mode`),30 个文件、含 `cordis.patch.yml` 与 `client.js`。GitHub 安装方式(`github:regrevia/dsh-fast-mode`)照旧可用。
6
+ - **插件市场**:仓库内新增 `market/index.json`(format 1 单插件索引,已用宿主 `@deepseek-ai/dsh-plugin-market-index` 的解析器验证通过),可作为「插件 → 发现 → 索引源」的自托管源;同时向精选列表 `awesome-dsh-plugin` 提了收录 PR [#9650](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin/pull/9650),合并后 dshmarket 与站点自动收录。
7
+ - **对外口径**:总量约快 26–28%(三块独立批次 0.72×/0.73×/0.73×,合计 65 个有效单元);「典型一格快 36%」已撤回,40% 不支持,见 `README.md` 与 `BENCHMARK.md`。
8
+
9
+ ## 1.5.2
10
+
11
+ - **新增实验旋钮 `workspaceMap`(默认关闭)与 `mapEntries`(默认 60)**:会话第一次工具调用之前,harness 把工作目录走一遍(深度 2、≤`mapEntries` 条、跳过 `node_modules`/`.git`/`dist`/`build`/`coverage`/`__pycache__`/`.venv`,正文 ≤1.4 kB),把「相对路径 + 字节数」和看得见的测试入口作为一条通知交给模型。它不结束回合、不改完成判定、不动 `reasoningEffort`、不加请求字段;它只是把模型自己会用 `ls`/`find` 换来的同一份清单先给一次。实现 `src/map.js`,接入 `index.js` 的 `agent/pre-step`,配置见 `src/config.js`、`src/effort.js`。
12
+ - **为什么默认关闭(两轮实测,23 个有效单元)**:W10 与 W11 两块把 `def15` 与 `def15 + workspaceMap` 同块对照。合并 23 个单元:**墙钟 0.927×、输出 0.896×、步数 1.099×**,逐格墙钟中位 **0.791×**、13/23 格更快、隐藏测试零红。方向是对的(输出确实少 10 %),但**步数反而多 10 %**,且反向尾部很重——`stategraph` 与 `codeimpl` 的个别格 1.4×–1.77×,两臂 p90 从 369 s 变成 559 s、最慢 510 s 变 714 s。按「长任务不能变慢」的门槛,它不够格进默认,留作旋钮并公开数字。
13
+ - **修掉一个会让新机制静默失效的缺陷**:运行目录在会话创建之后才放 fixture,第一步读工作目录可能读到空目录;旧写法在这种情况下也把「已下发」标掉,于是该会话再也不会拿到清单。现在只有真正构建出清单才记录已下发(`index.js` 的 `mapSent.add` 前判空),MAPDBG4 单格验证:`built=0`、`built=0`、`built=291`,会话里恰好一条 `Auto map:` 通知。
14
+ - **投递审计工具化**:`bench/audit-delivery.py --tag W10 --needle "Auto map: one inventory"` 一条命令按「任务 × 对 × 臂」列出文案命中次数。W10 结果是 `map15` 10/10 命中、`def15` 0/10 —— 对照臂干净、处理臂每格恰好一次,所以那一块的差值不是「提醒没发」的噪声。协议见 `bench/README.md` 第 15–17 条。
15
+ - 数字与逐格表见 `BENCHMARK.md` 的「筛选块 W10」与「复现块 W11」;原始结果 `bench/results/suite-W10.jsonl`、`bench/results/suite-W11.jsonl`,`python3 bench/report.py --files bench/results/suite-W11.jsonl --baseline def15` 可原样复算。
16
+
3
17
  ## 1.5.1
4
18
 
5
19
  **干净重基线:10 家族 30 个单元,总墙钟 0.72×、逐格中位 0.64×,有效单元 29/29 全绿。默认配置不变。三块独立复现到 0.72× / 0.73× / 0.73×。**
package/README.en.md CHANGED
@@ -45,9 +45,11 @@ No model switch, no `reasoningEffort` rewrite, no request-field injection (`serv
45
45
  **1.5.0 ships no step-based finish line by default.** `finishLineSteps` and `stepBudget` both default to `0`: there is no "remind me at step 10" and no "stop at step 30". When to finish is decided by task state and completion evidence — repeating an already-passing check verbatim against the same artifact suggests reusing the result and spending the step on what is left; several steps with no new result suggests changing the method (a reminder only, it never ends the turn); a stuck tool is cancelled by the watchdog and awaited until it really stops. A finished task is reported normally; a reminder never votes to stop for you.
46
46
 
47
47
  **1.5.1 clean rebaseline (10 families, 30 complete units, hidden tests 60/60 green).** Totals over whole completed tasks with no runs dropped except rows that leaked fixture material: off **3434.7 s / 591 244 tokens / 390 steps**; the default arm **0.72× wall / 0.74× output / 0.71× steps**; per-cell median **0.64×**, mean 0.84×, 90% interval [0.59, 0.79]. The gain sits in tasks that otherwise re-run the same check (`migrate` 0.45×, `shellops` 0.57×, `codebase` 0.55×, `stategraph` 0.64×, `ledger` 0.64×); short tasks are unstable — `perfrefactor` 1.18×, `csvpipe` 1.12×, and `debugfix` wrote 2.01× the output — because they have no repeated verification to save and the reminder itself costs a turn. This round also fixed two measurement defects that distorted the old numbers (run directories used to live inside the repo; metrics used to be sliced by byte offset). The old `0.86×` is retracted; see `1.5.1` and `1.5.0 rebaseline (retracted)` in [BENCHMARK.md](BENCHMARK.md).
48
- **Replication block W5 (2026-10-26, 19 complete units).** Three arms compared inside one block: `off` 1855.5 s, the default arm 0.73× wall / 0.77× output / 0.89× steps, matching 0.72× from 1.5.1. The `batchVerify` arm in the same block was 0.84× — slower than the default — so W4's 0.76× did not replicate and it is **not promoted**. One row leaked (`shellpipeline` p1 `bv15` read the hidden test and the reference solution) and is named and excluded; the runner now makes `hidden/` and `ref/` unreadable for the duration of a block. See "复现块 W5" in [BENCHMARK.md](BENCHMARK.md) and protocol item 12 in [bench/README.md](bench/README.md).
48
+ **Replication block W5 (2026-10-26, 19 complete units).** Three arms compared inside one block: `off` 1855.5 s, the default arm 0.73× wall / 0.77× output / 0.89× steps, matching 0.72× from 1.5.1. The `batchVerify` arm in the same block recorded 0.84×, but its notice **was never actually delivered** in any of the 20 sessions (0 hits when searched session by session), so `batchVerify` is **untested**, not slower and not falsified; it stays off, to be retested after the fix. One row leaked (`shellpipeline` p1 `bv15` read the hidden test and the reference solution) and is named and excluded; the runner now makes `hidden/` and `ref/` unreadable for the duration of a block. See "复现块 W5" in [BENCHMARK.md](BENCHMARK.md) and protocol item 12 in [bench/README.md](bench/README.md).
49
49
  **Replication block W7 (2026-10-26, 17 valid units).** The default arm measured **0.73×** wall / 0.75× output / 0.59× steps (longest cell 36 steps → 17), matching 0.72× in 1.5.1 and 0.73× in W5; the three blocks pool to **0.74×** over 65 valid units. `openBatch` in the same block was 0.91× and 1.136× head-to-head against the default, so W6's 0.77× did not replicate and it is **dropped**. The block also produced 7 crash rows (`exit=1`, 1 step, 0 output at session startup) which voided 3 units, named in the doc; from this block on a unit only counts when `exit == 0 && steps >= 1 && output > 0 && grading completed && no leak`, so "all green" always means all valid units green (1.5.1 29/29, W5 19/19, W7 17/17). See "复现块 W7" in [BENCHMARK.md](BENCHMARK.md).
50
50
 
51
+ **Screen W10 + replication W11 (2026-10-26, 23 valid units combined): `workspaceMap` only half delivered, stays off by default.** 1.5.2 adds one knob, off by default: before the first action the harness hands the model a one-shot inventory of the working tree (depth 2, ≤60 entries, dependency and build directories skipped) so it does not have to spend a step listing files itself. Both blocks compare it with `def15` inside the same block: across 23 valid units **wall 0.927×, output 0.896×, steps 1.099×**, per-cell median 0.791×, 13/23 faster, zero gate reds. Half of the mechanism is confirmed: output really is 10 % lower (0.90× and 0.89× in the two blocks). But steps rise 10 % and the tail gets worse — p90 559 s versus 369 s and max 714 s versus 510 s, with the same reverse cells in both blocks (`stategraph` at 1.4×–1.77× in each). **Because a long task must not get slower, it is not promoted**: the 1.5.2 default behaves exactly like 1.5.1, and the knob ships with its numbers. The same round accounted for the remaining headroom: repeated verification is only 6 % of wall, tool results have a 1.2 kB median, rewriting an already written file is 0.8 % — so **0.67× is not reachable inside the "do not touch `reasoningEffort`, add no request fields" constraints**. See "复现块 W11" and "0.67× 在现有约束下的可达性" in [BENCHMARK.md](BENCHMARK.md).
52
+
51
53
  **Faster configurations that failed the gate stay ablation arms only.** `fl10` (a step-10 finish reminder) is the fastest configuration measured, but it suppresses re-verification after the tests pass and once went red on a cell where both the baseline and the default were green, so it stays an ablation arm (those numbers came from a retracted batch and are kept only as a decision record). `openBatch` (one opening reminder to put independent calls in one block) beat the default 0.77× vs 0.93× in the W6 screen, but an independent W7 rerun brought it back to 0.91× and 1.136× head-to-head — **not replicated, so dropped**. `batchVerify` was 0.84× against the default inside W5 and is likewise not promoted. **The defaults therefore stay as they are**: no step-based finish line, `stepBudget: 0`, and all three mechanisms off. Per-cell data and gate results are in the `H2`/`H3` sections of BENCHMARK.
52
54
 
53
55
  **The retired default: `finishLineSteps: 10`.** On 7 families and 14 pairs it produced a median **1.86×**, 13/14 pairs faster (sign test **p = 0.002**), output **−52 %**, steps **−10.0**, hidden tests **14/14 green**. That line was genuinely fast, and the cost was real too: it fired at step 10 even on a long task that was still making progress. So 1.5.0 demotes it to an ablation arm (`fl10`), off by default.
@@ -60,7 +62,10 @@ Every raw number, the step buckets, the rejected candidates and the failure mode
60
62
 
61
63
  ## Install
62
64
 
63
- 1. **Install**: DSH desktop → plugin manager → install bundle, target `github:regrevia/dsh-fast-mode`. Terminal equivalent: `pnpm add github:regrevia/dsh-fast-mode --dir ~/.dsh/profiles/desktop`.
65
+ 1. **Install** (any one):
66
+ - **npm** (recommended): plugin manager → install bundle, target `dsh-fast-mode`; terminal equivalent `pnpm add dsh-fast-mode --dir ~/.dsh/profiles/desktop`. Package page <https://www.npmjs.com/package/dsh-fast-mode>.
67
+ - **GitHub**: target `github:regrevia/dsh-fast-mode`; terminal equivalent `pnpm add github:regrevia/dsh-fast-mode --dir ~/.dsh/profiles/desktop`.
68
+ - **Market index**: Plugins → Discover → index sources, add `https://raw.githubusercontent.com/regrevia/dsh-fast-mode/main/market/index.json` (format 1, checked with the host index parser). The curated-list submission is [#9650](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin/pull/9650); once merged, dshmarket and the site pick it up automatically and no manual source is needed.
64
69
  2. **Enable**: reload the page, click the small lightning bolt next to the composer. No mode to pick.
65
70
  3. **Takes effect** on the next request — no restart, no new conversation.
66
71
 
package/README.md CHANGED
@@ -46,6 +46,8 @@
46
46
 
47
47
  **1.5.1 干净重基线(10 家族、30 个完整单元、隐藏测试 60/60 全绿)**:不剔任何运行(只看产物里有没有泄漏标记)、按整任务总量加总 —— 关闭 **3434.7 s / 591 244 token / 390 步**;默认臂 **0.72× 墙钟 / 0.74× 输出 / 0.71× 步数**;逐格中位 **0.64×**、均值 0.84×、90 % 区间 [0.59, 0.79]。收益集中在反复重跑同一条检查的任务(`migrate` 0.45×、`shellops` 0.57×、`codebase` 0.55×、`stategraph` 0.64×、`ledger` 0.64×);短任务收益不稳定,`perfrefactor` 1.18×、`csvpipe` 1.12×、`debugfix` 输出反而 2.01×——短任务本来就没有可省的重复验证,提醒本身也是成本。这一轮同时修掉了两个会让旧数字失真的测量缺陷(运行目录曾在仓库内、指标曾按字节偏移切片),旧口径的 `0.86×` 已撤回,详见 [BENCHMARK.md](BENCHMARK.md) 的「1.5.1」与「1.5.0 重基线(已撤回)」。
48
48
  **复现块 W5(2026-10-26,19 个完整单元)**:同一块里三臂直接对照,`off` 1855.5 s,默认臂 0.73× 墙钟 / 0.77× 输出 / 0.89× 步数,与 1.5.1 的 0.72× 一致。同块的 `batchVerify` 臂记到 0.84×,但那一臂的提醒**实际一次都没下发**(20 个会话逐条检索命中 0),所以 `batchVerify` 是**未测**、不是变慢也不是被证伪,默认保持关闭、修复后重测。这一块还有 1 行泄漏(`shellpipeline` p1 的 `bv15` 读到隐藏测试与参考解),已点名剔除;跑批器现在开跑前会把 `hidden/`、`ref/` 置为不可读,详见 [BENCHMARK.md](BENCHMARK.md) 的「复现块 W5」与 [bench/README.md](bench/README.md) 协议第 12 条。
49
+ **筛选块 W10 + 复现块 W11(2026-10-26,合并 23 个有效单元):`workspaceMap` 只兑现一半,默认关闭。** 1.5.2 加了一个默认关闭的旋钮:会话第一次动手前,把工作目录的清单(深度 2、≤60 条、跳过依赖与构建目录)先交给模型一次,省掉它自己 `ls`/`find` 的那一步。两块同块对照 `def15`:合并 23 个单元 **墙钟 0.927×、输出 0.896×、步数 1.099×**,逐格中位 0.791×、13/23 格更快、隐藏测试零红。方向被证实一半——输出确实少 10 %(两块分别是 0.90×、0.89×);但步数反而多 10 %,而且尾部变差:两臂 p90 是 559 s 对 369 s、最慢 714 s 对 510 s,W10 与 W11 都出现同一形状的反向格(`stategraph` 两轮都 1.4×–1.77×)。**按「长任务不能变慢」的门槛它不进默认**,1.5.2 的默认行为与 1.5.1 完全相同,旋钮与数字一起公开。这一轮同时把余量算清了:重复验证只占 6 % 墙钟、工具结果中位 1.2 kB、重复写文件占 0.8 %,所以 **0.67× 在「不动 `reasoningEffort`、不加请求字段」的约束内不可达**,细节见 [BENCHMARK.md](BENCHMARK.md) 的「复现块 W11」与「0.67× 在现有约束下的可达性」。
50
+
49
51
  **复现块 W7(2026-10-26,17 个有效单元)**:默认臂 **0.73×** 墙钟 / 0.75× 输出 / 0.59× 步数(最长一格 36 步 → 17 步),与 1.5.1 的 0.72×、W5 的 0.73× 一致:三块合并 65 个有效单元 **0.74×**。同块的 `openBatch` 是 0.91×,对默认头对头 1.136×,W6 的 0.77× 没有复现,**淘汰**。这一块还有 7 行运行崩溃(会话启动阶段 `exit=1`、1 步、0 输出),3 个单元因此作废并点名;从这一块起判据要求 `exit == 0 && 步数 ≥ 1 && 输出 > 0 && 判分完成 && 未泄漏` 才计数,所以「全绿」一律指有效单元全绿(1.5.1 29/29、W5 19/19、W7 17/17)。详见 [BENCHMARK.md](BENCHMARK.md) 的「复现块 W7」。
50
52
 
51
53
  **更快但没过门禁的配置只作消融臂。** `fl10`(第 10 步提醒收尾)是实测最快的配置,但它会压掉「测试通过后又改了代码」的必要复验,且出现过基线与默认都绿而它红的一格,所以只是消融臂(这些数字出自已撤回的批次,只保留决策记录)。`openBatch`(开局发一次「把独立调用并进同一块」)在 W6 筛查里以 0.77× 快过默认的 0.93×,但 W7 独立复测回到 0.91×、头对头 1.136×,**没有复现即淘汰**。`batchVerify`(改完一批文件再跑一次覆盖检查)在 W5 同块对照里记到 0.84×,但那一臂的提醒实际零下发,测到的不是机制本身,所以它算**未测**,默认保持关闭。**所以默认不变**:不按步数收尾、`stepBudget: 0`、上述机制全关。单元级数据与门禁逐条见 BENCHMARK 的 `H2`/`H3`。
@@ -60,7 +62,10 @@
60
62
 
61
63
  ## 使用
62
64
 
63
- 1. **装**:DSH 桌面版 → 插件管理器 → 安装 bundle,目标填 `github:regrevia/dsh-fast-mode`。终端等价:`pnpm add github:regrevia/dsh-fast-mode --dir ~/.dsh/profiles/desktop`。
65
+ 1. **装**(任选其一):
66
+ - **npm**(推荐):插件管理器 → 安装 bundle,目标填 `dsh-fast-mode`;终端等价 `pnpm add dsh-fast-mode --dir ~/.dsh/profiles/desktop`。包页 <https://www.npmjs.com/package/dsh-fast-mode>。
67
+ - **GitHub**:目标填 `github:regrevia/dsh-fast-mode`;终端等价 `pnpm add github:regrevia/dsh-fast-mode --dir ~/.dsh/profiles/desktop`。
68
+ - **市场索引**:插件 → 发现 → 索引源,添加 `https://raw.githubusercontent.com/regrevia/dsh-fast-mode/main/market/index.json`(format 1,已用宿主索引解析器验过)。精选列表 `awesome-dsh-plugin` 的收录 PR 是 [#9650](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin/pull/9650),合并后 dshmarket 与站点自动收录,不用手动加源。
64
69
  2. **开**:重载页面,点输入框旁的小闪电。没有档位要选。
65
70
  3. **生效**:下一条请求生效,不用重启、不用新开会话。
66
71
 
@@ -1,6 +1,6 @@
1
1
  # Reference: principles, configuration and limits
2
2
 
3
- This manual describes what **dsh-fast-mode 1.5.1** actually does (compared with 1.5.0 it adds one experimental knob, `preferPatch`, off by default; with the default config behaviour is identical to 1.5.0), matching `index.js`, `src/` and `client.js` in the repository. It is not a brochure: where something can be measured it gets a number, and where it has not been measured it says so.
3
+ This manual describes what **dsh-fast-mode 1.5.3** actually does (compared with 1.5.0 it adds two experimental knobs, `preferPatch` and `workspaceMap`, both off by default; with the default config behaviour is identical to 1.5.0), matching `index.js`, `src/` and `client.js` in the repository. It is not a brochure: where something can be measured it gets a number, and where it has not been measured it says so.
4
4
 
5
5
  **The one contract: fast mode never changes reasoning effort, the thinking budget, or the model.** The `agent/request` hook does exactly one thing — `await next()`, read the `reasoningEffort` the request already carries for measurement, then return it unchanged. Whatever effort the user picked (the per-session choice in the UI, or the effort pinned by the profile's agent-default-model) is the effort every request uses.
6
6
 
@@ -160,6 +160,8 @@ In the profile's `cordis.patch.yml`:
160
160
  | `openBatch` | `false` | Opening batch: one notice on the first step of each turn asking for every independent call in one block and shell commands chained with `&&`. A short task spends its time on per-call fixed cost (TTFT ≈ 0.5 s), so one fewer round trip is one fewer TTFT. It only touches the first step of a turn. In the old batches it measured no faster than the default, but **those numbers are retracted** (run directories lived inside the repo and metrics were sliced by byte offset); it has not been re-measured cleanly yet. Stays `false`, experimental only. |
161
161
  | `shapeGate` | `false` | Shape gate: once a turn has accumulated at least 4 tool calls with 60 % or more of them writes (tools outside the read-only list), **no reminder is injected for the rest of that turn** and behaviour falls back to native. It only reads tool names — no request, no request-body field. It exists so a task the scheduler cannot accelerate gets left alone: write-heavy work has nothing to batch, and every reminder is a round trip it cannot reuse. An explicit `stepBudget` outranks the gate — "don't accelerate this" must not mean "ignore my stop line". Off by default, because it also suppresses the finish-line reminder that sometimes rescues a long tail. |
162
162
  | `preferPatch` | `false` | After a whole-file write, remind the model once to send only the changed lines (an edit hunk) when it edits a file that already exists. It arms on a `write` whose body is ≥ 2000 characters, fires on the following step at most once per turn, and the text asks for a byte-identical result — it does not limit what gets written or when the turn may end. Off by default: it only changes the form of the model's output (tool-call arguments), and correctness is decided by file contents, not by how they were sent. Whether it actually saves wall clock is measured by the `pp16` arm in `BENCHMARK.md`. |
163
+ | `workspaceMap` | `false` | One workspace inventory per conversation: before the first tool call the harness walks the session's working directory itself (depth 2, at most `mapEntries` rows, skipping `node_modules`, `.git`, `dist` and similar) and hands the model one notice with relative paths and byte sizes plus the test entry points it can see (an `npm test` script, `tests/`, pytest config, `go`/`cargo`/`make`). It replaces exactly the "what is in the directory" round trip (35.7 % of recorded calls in W8b) — it hides nothing, limits no read, and has no say in when the turn ends; the inventory is passed through unedited and which file to open stays the model's call. Off by default: it is a hint, not a proof of speed; measured by the `map15` arm in `BENCHMARK.md`. |
164
+ | `mapEntries` | `60` | Row cap for `workspaceMap`, 10–200. The notice text has its own ~1.4 kB ceiling; when it truncates it says "first N of M" and keeps the test-entry line. |
163
165
  | `stepBudget` | `0` | Per-turn hard step ceiling. `0` = unlimited, which is the default: a step count measures cost, not whether the task is done, so nothing is asked to wrap up on a counter. Set above 0, the line sends one "report where you are" notice without blocking tools — a hard cut would land mid-edit. |
164
166
  | `outputBudget` | `0` | Output-token budget: once cumulative `outputTokens` in the turn (the upstream `usage.outputTokens`, not an estimate) reach this value, one reminder to close the current loop, run the check that proves it, and report — at most once per turn. `0` = off, which is the default. It triggers on real decode volume rather than step count — three steps that write 30 000 tokens are expensive, twenty steps that write 2 000 are not — but tokens are still a cost signal, not evidence of completion, so it ships off. |
165
167
  | `finishLineSteps` | `0` | Optional soft finish line: at step N the scheduler asks once to stop widening scope, skip checks that already passed, and fix only what still fails. `0` = off, which is the default. **`10` is the retired 1.4.4 default and is kept only as an ablation arm, not as a recommendation**: it was the fastest configuration measured in the old batches (**those numbers are retracted** for the two measurement defects above), but it fails the gate — on `ledger` p1 both the baseline and the default were green and it was red (21/22; five reruns green, a 1/6 failure rate), so it stays off. The 1.86× over 14 pairs across 7 families was a paired median measured on the 1.4.4 default and does not carry over to this version. It saves re-verification, but on a task that edits after its tests pass it also suppresses the second verification that task needs, so it went back to off. Numbers and boundaries: `BENCHMARK.md`; the 1.5.0 default's clean rebaseline is 10 families and 30 complete units at **0.72× wall / 0.74× output / 0.71× steps** in total, per-cell wall median **0.64×**, hidden tests 60/60 green, broken down in that page's `## 1.5.1` section. |
package/docs/REFERENCE.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # 参考手册:原理、配置与边界
2
2
 
3
- 本手册描述 **dsh-fast-mode 1.5.1** 的实际行为(相对 1.5.0 只多一个默认关闭的实验旋钮 `preferPatch`,默认配置下行为与 1.5.0 完全一致),对应仓库里的 `index.js`、`src/` 与 `client.js`。它不是宣传页:凡是能测量的地方给出数字,凡是没测量的地方直接说没测量。
3
+ 本手册描述 **dsh-fast-mode 1.5.3** 的实际行为(相对 1.5.0 多两个默认关闭的实验旋钮 `preferPatch` 与 `workspaceMap`,默认配置下行为与 1.5.0 完全一致),对应仓库里的 `index.js`、`src/` 与 `client.js`。它不是宣传页:凡是能测量的地方给出数字,凡是没测量的地方直接说没测量。
4
4
 
5
5
  **唯一的契约:Fast 模式从不改变思考强度、thinking 预算或模型。** `agent/request` 钩子只做一件事——`await next()`,读一下请求携带的 `reasoningEffort` 记进测量,然后原样返回。用户选的是哪一档(会话里的 UI 选择,或者 profile 里 agent-default-model 固定下来的那一档),每个请求就用哪一档。
6
6
 
@@ -164,6 +164,8 @@
164
164
  | `openBatch` | `false` | 开局批量:每轮第一步发一次「把已知要做的独立调用放进同一块、shell 命令用 `&&` 串起来」。短任务的钱几乎全是每步固定成本(首字延迟 ~0.5 s/次),少一次往返就省一次;只影响第一轮第一步,长任务可忽略。旧批次里它不优于默认,但那些数字已因测量缺陷撤回(运行目录在仓库内、指标按字节偏移切片),尚无干净重测。保持在 `false`,只作实验旋钮。 |
165
165
  | `shapeGate` | `false` | 形状闸门:本轮累计调用 ≥ 4 且其中写入类(不在只读名单里的工具)占比 ≥ 60 % 时,**整轮不再注入任何提醒**,行为退回原生。判断只用已有的工具名,不发请求、不加请求体字段。它给的是「不适合就别管我」的退路:写密集型的任务没有可并行的调用,每条提醒都只是一次它用不上的往返。显式设了 `stepBudget` 时该收尾线优先于闸门——「别加速」不等于「无视我叫停」。默认关,因为它会同时关掉本可以救回长尾的收尾提醒。 |
166
166
  | `preferPatch` | `false` | 整文件写入之后提醒一次「改已有文件时只发改动的行(edit hunk),不要重发整个文件」。本步 `write` 的正文 ≥ 2000 字符才武装,之后一步发一次,每轮最多一次;文本明确要求结果与原文件逐字节相同,不限制写什么、也不影响何时结束。默认关闭:它只作用于模型的输出形式(工具调用参数),正确性由文件内容决定、与发送形式无关;实测是否真省墙钟见 `BENCHMARK.md` 的 `pp16` 臂。 |
167
+ | `workspaceMap` | `false` | 每会话一次工作区清单:在第一次工具调用之前,由 harness 自己走一遍会话的工作目录(深度 2、最多 `mapEntries` 条、跳过 `node_modules`/`.git`/`dist` 等),把「相对路径 + 字节数」和它能看到的测试入口(`npm test` 脚本、`tests/`、pytest 配置、`go`/`cargo`/`make`)作为一条通知交给模型。它只替代「目录里有什么」这一轮探索(W8b 记录里这类调用占 35.7 %),不隐藏任何东西、不限制读什么、也不影响何时结束;清单一字未改地给模型,读哪个文件仍由它自己判断。默认关闭:它是一个提示,不是加速证明,实测见 `BENCHMARK.md` 的 `map15` 臂。 |
168
+ | `mapEntries` | `60` | `workspaceMap` 的条数上限,10–200。清单正文另有 ~1.4 kB 的上限,超出时给「前 N 条,共 M 条」并保留测试入口那行。 |
167
169
  | `stepBudget` | `0` | 单轮步数硬上限。`0` = 不限,这是默认:步数只说明成本,不说明任务做没做完,所以默认不按步数催停。设成正数时,到线只发一次「按现状汇报」,不阻断工具调用——硬截断会把任务切在编辑中途。 |
168
170
  | `outputBudget` | `0` | 输出 token 预算:本轮累计 `outputTokens`(上游 `usage.outputTokens`,不是估算)达到该值时提醒一次「收掉当前闭环、跑一次证明它的检查、汇报」,每轮最多一次。`0` = 关闭,这是默认。它触发的是真实解码量而不是步数——三步写出三万 token 的任务很贵,二十步只写两千 token 的任务不贵;但 token 数同样是成本而非完成度证据,所以默认不启用。 |
169
171
  | `finishLineSteps` | `0` | 可选软收尾线:第 N 步提醒一次「不再扩大范围、不重跑已通过的检查、只修还在报的错」。`0` = 关闭,这是默认。**`10` 是 1.4.4 的退役默认值,现在只作消融臂,且不建议开启**:它在旧批次里是测到最快的配置(那些数字已因测量缺陷撤回),但按门禁不合格——`ledger` p1 上基线与默认都绿、它红(21/22,重跑 5 次全绿,1/6 失败率),因此仍然关闭。7 家族 14 对的 1.86× 是在 1.4.4 那个默认上测的逐对中位,不适用于本版;它省的是重复验证,但会在「测试通过后又改了代码」的任务上压掉必要复验,所以退回关闭。数据和边界见 `BENCHMARK.md`;1.5.0 自身默认的干净重基线是 10 家族 30 个完整单元:**0.72× 墙钟 / 0.74× 输出 / 0.71× 步数**,逐格墙钟中位 **0.64×**、隐藏测试 60/60 全绿,见该页 `## 1.5.1`。 |
package/index.js CHANGED
@@ -75,6 +75,7 @@ import {
75
75
  noteTurn,
76
76
  } from './src/auto.js'
77
77
  import { buildConflictMessage, buildReassertMessage, createNoticeMessage } from './src/notices.js'
78
+ import { buildWorkspaceMap } from './src/map.js'
78
79
  import { classifyEntry } from './src/shape.js'
79
80
  import { detectPeerPlugins, opsToolsLive } from './src/peers.js'
80
81
  import { createStallGuard, stallBudgetFor } from './src/stallguard.js'
@@ -94,7 +95,7 @@ const TAG = '[fast-mode]'
94
95
  * Shipped identity of this build. Traced into the debug log so a stale install is
95
96
  * visible at a glance, and pinned by a test against `package.json`.
96
97
  */
97
- export const VERSION = '1.5.1'
98
+ export const VERSION = '1.5.3'
98
99
  const GLOBAL_AGENT = '__global__'
99
100
 
100
101
  /**
@@ -263,7 +264,7 @@ export function apply(ctx, rawConfig) {
263
264
  if (initial.debug) {
264
265
  appendDebug(
265
266
  initial.debugFile,
266
- `apply v${VERSION} entered enabled=${initial.enabled} gear=${initial.gear} parallel=${initial.parallel}`,
267
+ `apply v${VERSION} entered enabled=${initial.enabled} gear=${initial.gear} parallel=${initial.parallel} map=${String(rawConfig?.workspaceMap)} mapEntries=${String(rawConfig?.mapEntries)}`,
267
268
  )
268
269
  appendDebug(initial.debugFile, `measure v${VERSION}: ${metrics.enabled ? `file=${metrics.file}` : 'off'}`)
269
270
  traceSettings(ctx, initial)
@@ -277,6 +278,8 @@ export function apply(ctx, rawConfig) {
277
278
  // The same listener remembers the compactions, so the two things that read the
278
279
  // event stream are read in one place and in one order.
279
280
  const compacted = new Set()
281
+ /** `${sid}${cwd}` pairs already given a workspace inventory. */
282
+ const mapSent = new Set()
280
283
  /**
281
284
  * @param {{id?: string}} session
282
285
  * @param {{type?: string, data?: object}} event
@@ -539,6 +542,29 @@ export function apply(ctx, rawConfig) {
539
542
  }
540
543
  }
541
544
  }
545
+ if (cfg.workspaceMap === true) {
546
+ // One inventory per session, before the model spends a step finding out
547
+ // what is in the directory (35.7 % of the recorded calls were exactly
548
+ // that). Bounded, and absent when the map cannot be built.
549
+ const cwd = payload?.agent?.session?.header?.cwd
550
+ const key = `${sid}\u0000${typeof cwd === 'string' ? cwd : ''}`
551
+ // A step-1 call sees the directory as handed over; the fixtures of a
552
+ // benchmark land after it, so an empty read is retried, not remembered.
553
+ if (!mapSent.has(key)) {
554
+ const map = buildWorkspaceMap(cwd, { maxEntries: cfg.mapEntries })
555
+ if (initial.debug) {
556
+ appendDebug(
557
+ initial.debugFile,
558
+ `map v${VERSION}: cwd=${typeof cwd === 'string' ? cwd : '(none)'}`
559
+ + ` built=${map ? map.length : 0}`,
560
+ )
561
+ }
562
+ if (map) {
563
+ mapSent.add(key)
564
+ notices.push(createNoticeMessage(map, 'auto: workspace inventory'))
565
+ }
566
+ }
567
+ }
542
568
  if (notices.length === 0) return decision
543
569
  return { ...decision, messages: insertAfterClaimed(decision.messages, payload?.messages, notices) }
544
570
  } catch {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-fast-mode",
3
- "version": "1.5.1",
3
+ "version": "1.5.3",
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",
package/src/config.js CHANGED
@@ -136,6 +136,23 @@ export const Config = z.object({
136
136
  '整文件改写提醒 / Prefer hunks: 一次整文件 write(参数 ≥ 2000 字符)之后,提醒一次「改已有文件时只发改动的行(edit hunk),不发整个文件」。落盘内容相同,省的是工具参数的输出 token(约占输出字符 28%)。只发一次,不改完成判定,不改复验。',
137
137
  ),
138
138
 
139
+ workspaceMap: z
140
+ .boolean()
141
+ .default(false)
142
+ .description(
143
+ '工作区清单 / Workspace inventory: 会话开始时不调用任何工具,harness 自己走一遍 cwd(深度 2、最多 60 条、跳过 node_modules 等)把路径、大小和现有测试入口作为一条提示交给模型。实测 `def15` 会话里 35.7% 的工具调用是 ls/find/cat 这类探路,其中 40% 出现在前两次调用里——这一步就是把那次往返拿掉。只发一次,不隐藏任何文件,读文件仍由模型自己决定。',
144
+ ),
145
+
146
+ mapEntries: z
147
+ .number()
148
+ .step(1)
149
+ .min(10)
150
+ .max(200)
151
+ .default(60)
152
+ .description(
153
+ '清单条数上限 / Inventory cap: 工作区清单最多列多少条路径,默认 60。超过上限时注明「前 N 条 / 共 M 条」。',
154
+ ),
155
+
139
156
  fanoutWidth: z
140
157
  .number()
141
158
  .step(1)
package/src/effort.js CHANGED
@@ -216,6 +216,10 @@ export function resolveConfig(raw, overrides) {
216
216
  fanoutWidth: gear.directive
217
217
  ? Math.min(12, Math.max(0, Math.floor(number(config.fanoutWidth, 0))))
218
218
  : 0,
219
+ /** One workspace inventory per session, before the first tool call. */
220
+ workspaceMap: config.workspaceMap === true,
221
+ /** Cap on the inventory listing; the default keeps it under ~1.4 kB. */
222
+ mapEntries: Math.min(200, Math.max(10, Math.floor(number(config.mapEntries, 60, { positive: true })))),
219
223
  /** How much visible prose the model is asked to cut. */
220
224
  discipline:
221
225
  gear.directive && config.outputDiscipline !== false && config.outputDiscipline !== 'late'
package/src/map.js ADDED
@@ -0,0 +1,154 @@
1
+ /**
2
+ * A bounded workspace inventory, delivered once per session.
3
+ *
4
+ * The measured reason this exists: in the recorded `def15` sessions of the bench
5
+ * suite, **35.7 % of all tool calls were discovery** (`ls`, `find`, `cat`,
6
+ * `grep`, `head`, …), and **40 % of the first two calls of a session** were the
7
+ * same. Those calls are not work; they are the model paying a round trip to
8
+ * learn what is in a directory it was just pointed at. Each round trip costs one
9
+ * step of reasoning plus one step of TTFT, and the bench fit says wall clock is
10
+ * ~97 % output tokens with steps on top.
11
+ *
12
+ * So the harness can do the inventory itself: walk the session cwd once, hand
13
+ * the model a capped list of paths (with sizes) and the test entry points it
14
+ * would otherwise search for. Nothing is hidden from the model and nothing is
15
+ * summarised away — files it needs are still there to read; this only removes
16
+ * the "what is here" round trip.
17
+ *
18
+ * Design limits, deliberately tight:
19
+ *
20
+ * - depth 2, at most 60 entries, known clutter (`node_modules`, `.git`, build
21
+ * output, caches) skipped;
22
+ * - total text stays well under 1 500 characters, so it costs picture-frame
23
+ * tokens, not a re-read;
24
+ * - every failure path returns `null` (no cwd, unreadable dir, empty dir) and
25
+ * the caller sends nothing. A map is a convenience, never a precondition.
26
+ *
27
+ * @module dsh-fast-mode/map
28
+ */
29
+ import fs from 'node:fs'
30
+ import path from 'node:path'
31
+
32
+ const SKIP = new Set([
33
+ 'node_modules', '.git', '.hg', '.svn', 'dist', 'build', 'out', 'target',
34
+ 'coverage', '__pycache__', '.venv', 'venv', '.pytest_cache', '.mypy_cache',
35
+ '.next', '.turbo', '.cache', '.idea', '.vscode', '.DS_Store',
36
+ ])
37
+
38
+ const MAX_ENTRIES = 60
39
+ const MAX_DEPTH = 2
40
+ const MAX_TEXT = 1400
41
+
42
+ function walk(root, dir, depth, out) {
43
+ let names
44
+ try {
45
+ names = fs.readdirSync(dir, { withFileTypes: true })
46
+ } catch {
47
+ return
48
+ }
49
+ names.sort((a, b) => Number(b.isDirectory()) - Number(a.isDirectory()) || a.name.localeCompare(b.name))
50
+ for (const entry of names) {
51
+ if (out.length >= MAX_ENTRIES) return
52
+ if (SKIP.has(entry.name)) continue
53
+ const abs = path.join(dir, entry.name)
54
+ const rel = path.relative(root, abs)
55
+ if (entry.isDirectory()) {
56
+ out.push(`${rel}/`)
57
+ if (depth < MAX_DEPTH) walk(root, abs, depth + 1, out)
58
+ } else if (entry.isFile()) {
59
+ let size = ''
60
+ try {
61
+ size = ` ${fs.statSync(abs).size}B`
62
+ } catch {
63
+ size = ''
64
+ }
65
+ out.push(`${rel}${size}`)
66
+ }
67
+ }
68
+ }
69
+
70
+ function readText(file, limit = 200_000) {
71
+ try {
72
+ const text = fs.readFileSync(file, 'utf8')
73
+ return text.length > limit ? text.slice(0, limit) : text
74
+ } catch {
75
+ return ''
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Test entry points the model would otherwise probe for. Only signals that are
81
+ * actually present in the tree are reported, so the map never suggests a command
82
+ * that cannot work.
83
+ */
84
+ function testHints(root) {
85
+ const hints = []
86
+ const pkg = readText(path.join(root, 'package.json'))
87
+ if (pkg) {
88
+ try {
89
+ const parsed = JSON.parse(pkg)
90
+ const script = parsed?.scripts?.test
91
+ if (typeof script === 'string' && script.trim() !== '') hints.push(`npm test (${script.trim()})`)
92
+ } catch {
93
+ // A broken package.json is not this module's problem.
94
+ }
95
+ }
96
+ const tryDir = (rel, label) => {
97
+ try {
98
+ if (fs.statSync(path.join(root, rel)).isDirectory()) hints.push(label)
99
+ } catch {
100
+ // absent
101
+ }
102
+ }
103
+ tryDir('tests', 'tests/')
104
+ tryDir('test', 'test/')
105
+ const files = (() => {
106
+ try {
107
+ return fs.readdirSync(root)
108
+ } catch {
109
+ return []
110
+ }
111
+ })()
112
+ if (files.some(f => /^pytest\.ini$|^pyproject\.toml$|^setup\.cfg$|^tox\.ini$/.test(f))) {
113
+ hints.push('pytest')
114
+ }
115
+ if (files.some(f => /^.*_test\.go$|^go\.mod$/.test(f))) hints.push('go test ./...')
116
+ if (files.some(f => /^Cargo\.toml$/.test(f))) hints.push('cargo test')
117
+ if (files.some(f => /^Makefile$/.test(f))) hints.push('make (Makefile present)')
118
+ return hints
119
+ }
120
+
121
+ /**
122
+ * Build the inventory text for a working directory.
123
+ *
124
+ * @param {string|undefined} cwd Absolute path of the session cwd.
125
+ * @param {object} [options]
126
+ * @param {number} [options.maxEntries] Cap on listed paths.
127
+ * @returns {string|null} Text to hand the model, or `null` when unavailable.
128
+ */
129
+ export function buildWorkspaceMap(cwd, options = {}) {
130
+ if (typeof cwd !== 'string' || cwd === '') return null
131
+ let root
132
+ try {
133
+ root = fs.realpathSync(cwd)
134
+ if (!fs.statSync(root).isDirectory()) return null
135
+ } catch {
136
+ return null
137
+ }
138
+ const entries = []
139
+ walk(root, root, 1, entries)
140
+ const hints = testHints(root)
141
+ if (entries.length === 0 && hints.length === 0) return null
142
+ const cap = Math.max(1, Math.floor(Number(options.maxEntries) || MAX_ENTRIES))
143
+ const listed = entries.slice(0, cap)
144
+ const lines = [
145
+ `Auto map: one inventory of ${root} so you do not need a discovery round trip.`,
146
+ `Entries (depth <= ${MAX_DEPTH}${entries.length > listed.length ? `, first ${listed.length} of ${entries.length}` : ''}):`,
147
+ ...listed.map(e => `- ${e}`),
148
+ ]
149
+ if (hints.length > 0) lines.push(`Test entry points present: ${hints.join(', ')}.`)
150
+ lines.push('Read the files you need; this only replaces the "what is in the directory" step.')
151
+ let text = lines.join('\n')
152
+ if (text.length > MAX_TEXT) text = `${text.slice(0, MAX_TEXT - 20).trimEnd()}\n…(truncated)`
153
+ return text
154
+ }
package/src/shape.js CHANGED
@@ -41,8 +41,8 @@ const PER_ITEM = [
41
41
  /\bone\s+(?:object|entry|row|line|record)\s+(?:keyed|per)\b/i,
42
42
  /\bkeyed\s+by\s+(?:file|name|module|path)\b/i,
43
43
  /\bone\s+(?:entry|row|object)\s+(?:for|per)\s+each\b/i,
44
- /每个文件.{0,12}(?:一项|一条|一个对象|汇总|报告)/,
45
- /逐文件.{0,10}(?:汇总|报告|列举)/,
44
+ /每个文件[^\n]{0,12}(?:一项|一条|一个对象|汇总|报告)/,
45
+ /逐文件[^\n]{0,10}(?:汇总|报告|列举)/,
46
46
  ]
47
47
 
48
48
  const FANOUT = [