dsh-fast-mode 1.5.18 → 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.
@@ -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()
@@ -4,7 +4,7 @@ This manual describes what **dsh-fast-mode 1.5.17** actually does (injection beh
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
 
7
- Speed comes from three things only: fewer round trips, less narration, less tool wait. Each gets a section below.
7
+ Speed comes from three things only: fewer round trips, less redundant output, less tool wait. Each gets a section below.
8
8
 
9
9
  ## Interaction details: the bolt switch and which conversation it applies to
10
10
 
@@ -90,7 +90,7 @@ Details worth knowing:
90
90
  - **The fan-out number is the width the scheduler computed.** Under `auto` the section and the stall guard share one `autoSchedule` result, so the number the model reads and the number being enforced cannot drift apart.
91
91
  - **Prefetch appears only at width ≥ 4.** Below that, an extra call just competes with the work the step already has.
92
92
  - **The bounded-calls block appears only when `stallGuard !== false`**, because it describes the same policy the guard enforces.
93
- - **Output discipline has two strengths**: `light` for the legacy `fast` gear, `full` for `auto`/`turbo`/`max`/`ludicrous`. Both trim narration only — no announcing a call, no restating a result, no progress report; the last step still writes the answer in full. `full` adds one sentence: send bare calls when a step needs no prose. **It never shortens the answer and never tells the model to skip a check.**
93
+ - **Output discipline has two strengths**: `light` for the legacy `fast` gear, `full` for `auto`/`turbo`/`max`/`ludicrous`. Both ask the model to skip call previews, restatements and progress reports; the last step still writes the answer in full. `full` adds one sentence: send bare calls when a step needs no prose. **It never shortens the answer and never tells the model to skip a check.** Scope matters: visible prose is only 1.18 % of the characters in (prose + tool arguments) (def15, 341 sessions; excludes reasoning and tool results; recompute with `python3 bench/textshare.py --limit 0`), so this is not where the speedup mainly comes from — the gain is fewer round trips and fewer output tokens, and it does not license the claim that "the work itself is unaffected".
94
94
  - **`parallel: 1` is the documented "on but no fan-out" setting**: the switch still applies to the stall guard, and the guidance section disappears entirely.
95
95
 
96
96
  The cost is explicit: about 2,700 characters under `auto`, paid as input tokens on every request. That is the intended trade — input tokens for fewer round trips.
@@ -164,6 +164,7 @@ In the profile's `cordis.patch.yml`:
164
164
  | `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. |
165
165
  | `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. |
166
166
  | `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`. |
167
+ | `compactRereadHint` | false | One extra sentence in the compression reminder: do not re-read whole files this turn already wrote. Experimental, off by default |
167
168
  | `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`. |
168
169
  | `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. |
169
170
  | `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. |
package/docs/REFERENCE.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  **唯一的契约:Fast 模式从不改变思考强度、thinking 预算或模型。** `agent/request` 钩子只做一件事——`await next()`,读一下请求携带的 `reasoningEffort` 记进测量,然后原样返回。用户选的是哪一档(会话里的 UI 选择,或者 profile 里 agent-default-model 固定下来的那一档),每个请求就用哪一档。
6
6
 
7
- 快的部分只来自三件事:更少的往返、更少的过程文字、更少的卡顿等待。下面逐条说明。
7
+ 快的部分只来自三件事:更少的往返、更少的冗余输出、更少的卡顿等待。下面逐条说明。
8
8
 
9
9
  ## 交互细节:闪电开关与会话归属
10
10
 
@@ -94,7 +94,7 @@
94
94
  - **扇出目标里的数字就是调度器算出的宽度。** `auto` 下提示段和看门狗共用同一个 `autoSchedule` 结果,所以模型读到的数字和被执行的数字不会漂移。
95
95
  - **预取只在宽度 ≥ 4 时出现。** 宽度不够时多取一个调用只会挤占别的工作。
96
96
  - **有界调用那一段只在 `stallGuard !== false` 时出现**,因为它描述的规则和看门狗是一套东西。
97
- - **输出纪律有两档**:`light` 给旧档位 `fast` 用,`full` 给 `auto`/`turbo`/`max`/`ludicrous` 用。两档只裁剪过程叙述——不预告调用、不复述结果、不写进度报告;最后一步的答案仍然要完整写出。`full` 只多一句"不需要文字时只发调用"。**它不会让答案变短,更不会让模型跳过检查。**
97
+ - **输出纪律有两档**:`light` 给旧档位 `fast` 用,`full` 给 `auto`/`turbo`/`max`/`ludicrous` 用。两档要求模型不预告调用、不复述结果、不写进度报告;最后一步的答案仍然要完整写出。`full` 只多一句"不需要文字时只发调用"。**它不会让答案变短,更不会让模型跳过检查。** 口径要说清:可见正文只占「正文+工具参数」字符的 1.18 %(def15 全量 341 会话;不含 reasoning 与工具返回;`python3 bench/textshare.py --limit 0` 可复算),所以这一条不是提速的主要来源——收益来自回合数与输出 token 下降;也不能由此宣称"工作内容不受影响"。
98
98
  - **`parallel: 1` 是"开着但不要求扇出"的正式写法**:开关仍然生效于看门狗,提示段整体消失。
99
99
 
100
100
  成本是明码标价的:`auto` 下这一段约 2.7 千字符,每个请求都要付这部分输入 token。这是有意的交换——用输入 token 换更少的往返。
@@ -168,6 +168,7 @@
168
168
  | `openBatch` | `false` | 开局批量:每轮第一步发一次「把已知要做的独立调用放进同一块、shell 命令用 `&&` 串起来」。短任务的钱几乎全是每步固定成本(首字延迟 ~0.5 s/次),少一次往返就省一次;只影响第一轮第一步,长任务可忽略。旧批次里它不优于默认,但那些数字已因测量缺陷撤回(运行目录在仓库内、指标按字节偏移切片),尚无干净重测。保持在 `false`,只作实验旋钮。 |
169
169
  | `shapeGate` | `false` | 形状闸门:本轮累计调用 ≥ 4 且其中写入类(不在只读名单里的工具)占比 ≥ 60 % 时,**整轮不再注入任何提醒**,行为退回原生。判断只用已有的工具名,不发请求、不加请求体字段。它给的是「不适合就别管我」的退路:写密集型的任务没有可并行的调用,每条提醒都只是一次它用不上的往返。显式设了 `stepBudget` 时该收尾线优先于闸门——「别加速」不等于「无视我叫停」。默认关,因为它会同时关掉本可以救回长尾的收尾提醒。 |
170
170
  | `preferPatch` | `false` | 整文件写入之后提醒一次「改已有文件时只发改动的行(edit hunk),不要重发整个文件」。本步 `write` 的正文 ≥ 2000 字符才武装,之后一步发一次,每轮最多一次;文本明确要求结果与原文件逐字节相同,不限制写什么、也不影响何时结束。默认关闭:它只作用于模型的输出形式(工具调用参数),正确性由文件内容决定、与发送形式无关;实测是否真省墙钟见 `BENCHMARK.md` 的 `pp16` 臂。 |
171
+ | `compactRereadHint` | false | 压缩提醒里多一句「本回合写过的文件不要为确认整份重读」;不新增消息。实验旋钮,默认关 |
171
172
  | `workspaceMap` | `false` | 每会话一次工作区清单:在第一次工具调用之前,由 harness 自己走一遍会话的工作目录(深度 2、最多 `mapEntries` 条、跳过 `node_modules`/`.git`/`dist` 等),把「相对路径 + 字节数」和它能看到的测试入口(`npm test` 脚本、`tests/`、pytest 配置、`go`/`cargo`/`make`)作为一条通知交给模型。它只替代「目录里有什么」这一轮探索(W8b 记录里这类调用占 35.7 %),不隐藏任何东西、不限制读什么、也不影响何时结束;清单一字未改地给模型,读哪个文件仍由它自己判断。默认关闭:它是一个提示,不是加速证明,实测见 `BENCHMARK.md` 的 `map15` 臂。 |
172
173
  | `mapEntries` | `60` | `workspaceMap` 的条数上限,10–200。清单正文另有 ~1.4 kB 的上限,超出时给「前 N 条,共 M 条」并保留测试入口那行。 |
173
174
  | `stepBudget` | `0` | 单轮步数硬上限。`0` = 不限,这是默认:步数只说明成本,不说明任务做没做完,所以默认不按步数催停。设成正数时,到线只发一次「按现状汇报」,不阻断工具调用——硬截断会把任务切在编辑中途。 |
package/index.js CHANGED
@@ -73,6 +73,7 @@ import {
73
73
  noteStep,
74
74
  noteTool,
75
75
  noteTurn,
76
+ toolTargets,
76
77
  } from './src/auto.js'
77
78
  import { buildConflictMessage, buildReassertMessage, createNoticeMessage } from './src/notices.js'
78
79
  import { buildWorkspaceMap } from './src/map.js'
@@ -96,7 +97,7 @@ const TAG = '[fast-mode]'
96
97
  * Shipped identity of this build. Traced into the debug log so a stale install is
97
98
  * visible at a glance, and pinned by a test against `package.json`.
98
99
  */
99
- export const VERSION = '1.5.17'
100
+ export const VERSION = '1.5.20'
100
101
  const GLOBAL_AGENT = '__global__'
101
102
 
102
103
  /**
@@ -265,7 +266,7 @@ export function apply(ctx, rawConfig) {
265
266
  if (initial.debug) {
266
267
  appendDebug(
267
268
  initial.debugFile,
268
- `apply v${VERSION} entered enabled=${initial.enabled} gear=${initial.gear} parallel=${initial.parallel} map=${String(rawConfig?.workspaceMap)} mapEntries=${String(rawConfig?.mapEntries)}`,
269
+ `apply v${VERSION} entered enabled=${initial.enabled} gear=${initial.gear} parallel=${initial.parallel} map=${String(initial.workspaceMap)} mapEntries=${String(initial.mapEntries)}`,
269
270
  )
270
271
  appendDebug(initial.debugFile, `measure v${VERSION}: ${metrics.enabled ? `file=${metrics.file}` : 'off'}`)
271
272
  traceSettings(ctx, initial)
@@ -663,9 +664,16 @@ export function apply(ctx, rawConfig) {
663
664
  }
664
665
  const argBody = args.content ?? args.text ?? args.contents ?? args.new_str ?? args.newString
665
666
  const argChars = typeof argBody === 'string' ? argBody.length : 0
667
+ // The dedupe key is sorted and truncated, so it is not evidence about
668
+ // which files a call touched. Hand the scheduler the real targets, plus
669
+ // the session directory a relative target resolves against.
670
+ const targets = toolTargets(args, typeof exec.name === 'string' ? exec.name : '')
666
671
  const observation = {
667
672
  name: typeof exec.name === 'string' ? exec.name : 'unknown',
668
673
  argsKey: stableKey(exec.arguments),
674
+ paths: targets.paths,
675
+ command: targets.command,
676
+ cwd: typeof exec.agent?.session?.header?.cwd === 'string' ? exec.agent.session.header.cwd : '',
669
677
  argChars,
670
678
  isError: result?.isError === true,
671
679
  chars: contentChars(result?.content),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-fast-mode",
3
- "version": "1.5.18",
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
  ],