dsh-fast-mode 1.5.10 → 1.5.12

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
@@ -1523,6 +1523,8 @@ node bin/experiment.mjs stop # end it; later sessions g
1523
1523
  | 再加 `shellpipeline`+`shellops` | 50/132 | 0.8763× |
1524
1524
  | 再加 `migrate`+`codebase` | 76/132 | 0.8414× |
1525
1525
  | **全开(当前默认)** | 132/132 | **0.8247×** |
1526
+
1527
+ 三个独立估计量一致,不是尾部样本撑出来的:逐对中位 **0.8128×**(CI95 0.7095–0.8830)、几何均值 **0.8232×**(CI95 0.7530–0.8971)、总输出 token 比 **0.8221×**;132 对里 89 对更快,符号检验 p = 0.0001(`python3 bench/pool.py --ci 2000` 一行可复算)。
1526
1528
  | 全开但 `debugfix`/`perfrefactor` 关 | 103/132 | 0.8235× |
1527
1529
 
1528
1530
  读法两条,都是负结果,但有用:
@@ -1580,3 +1582,232 @@ python3 bench/suite-run.py --tasks debugfix,ledger,codeimpl --arms off,def15,bud
1580
1582
  5. 通过 → `outputBudget` 默认值从关改为 8000 并进 CHANGELOG;不通过 → 在本文档记录负结果,默认保持关。
1581
1583
 
1582
1584
  注意:`bud8` 的历史 20 行来自**没有 tag 的遗留块**(每家族仅 1 对、无 ABBA 保证),已判定不可用,不得拿来替代 `BUD3`;上面第 2 条的旧值 `out/def15≈0.94` 只作方向参考。
1585
+
1586
+ ## 负载前提改了:从「等安静」改成「同块交错 + 记录负载」(2026-10-27,方法学变更)
1587
+
1588
+ 连续 4 小时以上等不到 `load1 < 6.5`。查了是谁在占 CPU:不是外部的抓取任务,而是**这台机器现在的常态**——DSH 自己的两个渲染进程(70 % + 25 %)、SkyLight(71 %)、WebKit、一个虚拟机。也就是说 `load1` 8–10 是本机 GUI 会话的基线,原来的「安静窗口」前提在这台机器上永远不会满足,继续等等于把实验停摆。
1589
+
1590
+ 新的前提(写下来是为了以后不被人拿绝对墙钟数字做文章):
1591
+
1592
+ 1. **成块门槛从「安静」改成「不病态」**:`load1 < 12` 即开跑;只有 `load1 ≥ 14 连续 6 分钟`(真异常)或「24 分钟没有新增结果行」(真挂死)才中止。
1593
+ 2. **抗污染靠设计而不是靠等**:`suite-run.py` 本来就是每对交错跑臂(ABBA),同块内 `off` 与 `def15`(或 `gateonly`/`bud8`)吃同一段负载。**对外报的量是同块比值,负载在时间上的整体缩放对比值近似一阶抵消**;受影响的只有绝对墙钟,而这些块的绝对墙钟一律不许跨块比较、也不许当「本机单任务耗时」引用。
1594
+ 3. **每个块把自己的负载样本落盘**:`bench/results/suite-<TAG>.load.json`(`samples/mean/max`),分析时必须把块所属负载区间一并写出,例如「NX1_IMPL 在 mean 9.8 下测得」。
1595
+ 4. **方差预期**:负载高时单格方差变大,所以结论继续用「同块总和比 + 逐对中位数 + 符号检验」,且**每家族 ≥ 2 个同块有效对**才算判定;只有 1 对的行一律写「未判定」。
1596
+
1597
+ 这一条替换掉此前「`load` 持续 ≥ 8 就整块作废」的规则(那版规则在 20:44–20:53 的 NX1_IMPL 上刚把一个块砍到只剩 1 行,而阈值 8 恰好等于本机空闲基线,等于永久停机)。
1598
+
1599
+ ## 拆开看:快的那 18% 全部来自「输出 token 变少」,不是「每一步变快」
1600
+
1601
+ 132 对干净配对逐对分解,恒等式先验证:`几何均值(wall) = 几何均值(步数) × 几何均值(每步墙钟)`,残差 0.0000。
1602
+
1603
+ - 几何均值:**步数 0.827 × 每步 0.996 = 0.823×**(逐对中位 0.818 × 0.999)。
1604
+ - 也就是说:**每步并没有变快**(0.996×,等于没变),省下来的是**回合数**;而回合数省掉之后,每步的输出反而变长,两者相乘才是那 18%。
1605
+ - 每步墙钟 ≈ 每步的输出 token:`corr(log llm/step, log out/step) = 0.949`(n = 132 对)。块内回归 `wall ≈ 9.45 s / 1k 输出 token`。模型侧解码速度是模型自己的,不归本插件管,所以**插件能动的那一项只有「总共写多少 token」**。
1606
+ - 反过来验证:`corr(log 墙钟比, log 输出比) = 0.940`(n = 145 对,含未入池块),逐对 `wall` 比的 sd 0.52、`out` 比的 sd 0.46 —— **方差几乎全部由输出长度决定,不是负载**。所以之前「负载让区间变宽」的说法只对了一半:负载抬绝对墙钟,但逐对方差的主因是模型每次写多写少。
1607
+
1608
+ 逐家族(同块逐对中位,n 见括号;`每步`=wall/步 的比值,`输出/步`=out/步 的比值):
1609
+
1610
+ | 家族 | 步数 | wall | 每步 | 输出/步 |
1611
+ |---|---|---|---|---|
1612
+ | codebase (11) | 1.000 | 0.586 | 0.806 | 0.835 |
1613
+ | stategraph (10) | 1.085 | 0.667 | 0.639 | 0.671 |
1614
+ | shellops (13) | 0.769 | 0.683 | 1.152 | 1.275 |
1615
+ | codeimpl (15) | 0.778 | 0.709 | 0.860 | 0.960 |
1616
+ | migrate (15) | 0.778 | 0.739 | 0.940 | 1.022 |
1617
+ | ledger (14) | 0.591 | 0.812 | 1.252 | 1.445 |
1618
+ | shellpipeline (12) | 0.807 | 0.844 | 1.027 | 0.932 |
1619
+ | perfrefactor (14) | 1.000 | 0.961 | 0.981 | 0.914 |
1620
+ | debugfix (15) | 1.000 | 0.976 | 0.972 | 0.986 |
1621
+ | csvpipe (13) | 0.923 | 0.983 | 1.039 | 1.040 |
1622
+
1623
+ 两条互斥的省钱路子,家族各走一条:`codebase`/`stategraph` **步数不减甚至变多,但每步写得少**(每步 0.64–0.81);`ledger`/`shellops` **步数砍掉四到六成,但每步写得多**(每步 1.15–1.25)。混合池 0.823× 就是把这两类平均后的结果;把它们混在一起比较「哪个机制强」没有意义。
1624
+
1625
+ **一个被否掉的猜想**:曾怀疑「每步变便宜」来自提示词缓存命中,因为注入文本若放在 prompt 尾部、前缀稳定,cacheRead 会变高。实测否掉:live 遥测 446 个 on 步里 `corr(log cacheRead, log llmMs/输出) = −0.026`(缓存命中不预测每 token 延迟),且 461 行遥测里**没有任何一个会话同时有 on 与 off 步**(0 个),所以「on 的 cacheRead 中位 66k vs off 34k」是上下文长度不同导致的混淆量,不可引用。每 token 时间基本是常数,这也解释了为什么 `每步` 完全跟着 `输出/步` 走(0.949)。
1626
+
1627
+ **这条拆解给出的天花板**:0.60× 需要总输出少 40 %,即模型在同样的任务里少写 40 % 的 token(少写解释、少写重复复验、少贴长 diff)。那不是调度能做到的,是**拿任务质量换的**,与「速度不能以牺牲任务质量为代价」直接冲突。所以声明:在「不动请求体、不动推理档、不牺牲正确率」的约束下,**可达区间是 0.80–0.85×**,逐家族最优 0.59–0.71×;再往下必须走被明令禁止的杠杆(提高 max_tokens 压缩比、砍推理档、跳过复验)。这条写在这里,是为了以后不再有人拿 0.60× 当目标返工。
1628
+
1629
+ ## 质量对等:132 对里断言 1959/1960 对 1960/1960
1630
+
1631
+ 「快了 18%」必须配一条质量口径,否则就是拿正确率换的。同一批 132 个有效配对逐对核对隐藏断言:
1632
+
1633
+ - 断言通过数:`off` **1960/1960(100.00 %)**,`def15` **1959/1960(99.95 %)**,差 **−0.05 个百分点 = 1 条断言**。
1634
+ - 逐对看:131/132 对两臂断言数完全相同;唯一不同的是 `W1 / ledger / pair 1`,`def15` 21/22、`off` 22/22。
1635
+ - 产物精确性 `exact`(入口文件 / 输出与期望逐字节一致):两臂都是 132/132。
1636
+ - 结论只能说到这里:**在这 10 个家族、132 个任务上,速度不是用正确率换来的**;1 条断言的分差在统计上无法与噪声区分(也无法证明「绝对无损」),所以对外表述用「断言对等,1/1960 差异」而不是「零质量损失」。
1637
+
1638
+ 另外记一条运行可靠性的事实,避免以后误读:同批数据里有 **`off` 6 行、`def15` 3 行**是零输出的退化运行(`out=0`、`steps` 为 1、任务只做了一半),按有效单元规则已经全部排除,不参与任何比值。这类行的出现与宿主负载相关,不是某一个臂的固有缺陷;它们也解释了为什么 `suite-run.py` 必须按「有效行」而不是「有行」来判断进度(见 `bench/scrub.py`)。
1639
+
1640
+ ## 机制判定:`shapeGate` 没有可测收益(6 对,3 个家族)
1641
+
1642
+ 1.5.10 之前 `shapeGate` / `outputBudget` 在 `resolveConfig()` 里被丢掉了,所以旧块其实全跑默认配置;1.5.10 修好接线后重新测了 `shapeGate`(块 `GATE3_DBG` / `GATE3_CSV` / `GATE3_IMPL`,每块 2 对、三臂 `off` / `def15` / `gateonly` 同块交错):
1643
+
1644
+ | 家族 | 对 | 逐对 `gateonly/def15` 墙钟 | 几何均值 |
1645
+ | --- | --- | --- | --- |
1646
+ | debugfix | 2 | 0.889 / 2.246 | 1.413 |
1647
+ | csvpipe | 2 | 1.303 / 0.337 | 0.663 |
1648
+ | codeimpl | 2 | 1.269 / 0.978 | 1.114 |
1649
+ | **合计** | **6** | — | **1.014**(中位 1.123,`gateonly` 3/6 更快;输出 token 几何 0.949) |
1650
+
1651
+ 判据在跑之前就写死了:同一家族先看逐对方向是否一致,再合池;只要逐对方向随机就不给结论。这里 6 对里 3 对更快、3 对更慢,几何 1.014 与 1.0 无法区分,输出 token 只少 5 % 也没换来墙钟 ⇒ **`shapeGate` 不是速度杠杆**,与上面的分解一致(省时只能靠少输出/少轮次,而 gate 的输出变化不稳定)。因此它保持默认关闭;它的价值只在「形态约束」本身,不在速度。
1652
+
1653
+ 这也顺带说明 1.5.10 那个接线 bug 的影响面:旧块里 `bud8`/`sg15`/`gateonly`/`gate15` 的分支数据一律标 **untested**,不能当机制证据;上面这张表是修复后的第一次真测量。
1654
+
1655
+ ## 反例:`outputDiscipline: true`(从第 1 步就压输出)比默认 `late` 慢 21 %
1656
+
1657
+ 默认值 `outputDiscipline: 'late'` 是「短任务不付提示成本、第 4 步之后提醒一次」。把规则提前全程生效(实验臂 `odt15`)在同一批 12 个块内配对里(`W1`,`codeimpl`/`csvpipe`/`debugfix`/`ledger`/`migrate`/`shellpipeline` 各 2 对):
1658
+
1659
+ - 几何均值 **1.213×**(即慢 21 %),`odt15` 只赢 4/12 对。
1660
+ - 输的方式不是「话变多」而是 **步数变多**:`debugfix` 4→12 步、`ledger` 8→26 步、`shellpipeline` 19→31 步;输出 token 也跟着涨(`ledger` 2.32×、`shellpipeline` 1.84×)。
1661
+ - 机制解释与主结论一致:把「少写过程」当常驻文风后,模型会改用更多轮次去「确认」,而轮次和输出是绑在一起的(`steps` 0.827 × 每步输出 ≈ 墙钟)。
1662
+
1663
+ 所以默认值不改成 `true`,也不做成「从第一步就提醒」。这条和 `shapeGate` 的空结果一起,构成「不是所有听起来合理的省 token 开关都真省时间」的两个反例——它们都是**先测后定**,不是按直觉定的。
1664
+
1665
+ ## `outputBudget`:同一个 8000 token 上限,长任务省一半、短任务慢一半(6 对,3 个家族)
1666
+
1667
+ 1.5.10 修好接线后测 `outputBudget: 8000`(实验臂 `bud8`,块 `BUD3_DBG` / `BUD3_IMPL` / `BUD3_LEDGER`,每块 2 对,`def15` 与 `bud8` 同块交错):
1668
+
1669
+ | 家族 | 对 | 逐对 `bud8/def15` 墙钟 | 几何均值 | 步数变化 | 输出变化 |
1670
+ | --- | --- | --- | --- | --- | --- |
1671
+ | ledger | 2 | 0.531 / 0.976 | **0.720** | 18→5、16→13 | 0.45×、0.89× |
1672
+ | debugfix | 2 | 1.220 / 1.124 | 1.171 | 5→6、6→4 | 1.31×、0.91× |
1673
+ | codeimpl | 2 | 1.466 / 1.403 | **1.434** | 5→8、4→6 | 1.94×、1.90× |
1674
+ | **合计** | **6** | — | 1.066(`bud8` 2/6 更快) | — | — |
1675
+
1676
+ 读法:预算本身不是速度杠杆,它是一个**收尾扳机**。`ledger`(22 个隐藏断言、本来要写 3–5 万 token)在 8000 处被提醒一次,直接砍到 5 步、输出 0.45×,墙钟 0.53×——真的省。`codeimpl` 相反:任务只需要约 1.2 万 token,预算在它还没写出实现时就把「收掉闭环」的意见插进去,结果多出 2 步重开(4→6 / 5→8),输出和墙钟一起涨 40 %。
1677
+
1678
+ 所以默认值保持 0(关闭),并且不按家族静态开:判据是「这一轮是否已经长到值得收」,不是一个固定 token 数。据此把预算改成 **延迟生效**(`outputBudgetAfterSteps`,默认 0 = 立即),实验臂 `budlate` = `outputBudget 8000` + `outputBudgetAfterSteps 6`,正在跑 `BUDL_DBG`/`BUDL_IMPL`/`BUDL_LEDGER` 各 2 对。
1679
+
1680
+ ## 报价更新:并入 NX1 三块后 138 对(2026-10-27 夜)
1681
+
1682
+ `NX1_IMPL` / `NX1_PIPE` / `NX1_SG` 三块是 1.5.10 修线之后的 `off` vs `def15` 同块配对(每块 2 对、ABBA 交错),与旧的 9 块口径相同,因此并入 `bench/pool.py` 的 `CLEAN`,合池从 132 对变 **138 对**(`python3 bench/pool.py --ci 4000`):
1683
+
1684
+ | 估计量 | 132 对(旧口径) | 138 对(并 NX1) |
1685
+ | --- | --- | --- |
1686
+ | 墙钟总比 | 0.8247×(CI95 0.747–0.914) | **0.8234×(CI95 0.742–0.913)** |
1687
+ | 逐格中位 | 0.8128×(0.710–0.883) | **0.7994×(0.703–0.885)** |
1688
+ | 几何均值 | 0.8232×(0.753–0.897) | **0.8248×(0.753–0.900)** |
1689
+ | 步数总比 / 输出总比 | 0.8085× / 0.8221× | **0.8062× / 0.8179×** |
1690
+ | 更快对数 / 符号检验 | 89/132,p=0.0001 | **93/138,p=0.0001** |
1691
+ | 断言(`off` / `def15`) | 1960/1960 / 1959/1960 | **2038/2038 / 2037/2038** |
1692
+
1693
+ 三点读法:
1694
+
1695
+ 1. **点估计没动**(0.8247 → 0.8234,差 0.13 个百分点,远小于区间宽度 0.18),所以对外仍是「约 1.21× 快、约 17–18 % 省时」,区间照旧报 CI95,不报单点。新增的 6 对确实更偏快(中位从 0.813 掉到 0.799),但样本量只涨 4.5 %,不足以支撑新报价。
1696
+ 2. **唯一的断言差异还是那一个**:`(W1, ledger, 1)` 的 `def15` 21/22 对 `off` 22/22;其余 137 对断言计数完全一致,`exact` 138/138。质量措辞仍是「断言对等」,不是「零质量损失」。
1697
+ 3. 三个估计量互相不一致的程度(0.799 / 0.823 / 0.825)正好说明为什么发布口径必须并列报,而不能挑最小的那个当中位卖点。
1698
+
1699
+ ## 空转对照:开关根本没触发时,同一对能差多少(9 对,纯分析,不新增格子)
1700
+
1701
+ 这一轮 mechanism 实验里有 9 对是**结构上必然空转**的:实验臂多开的那一路开关,在那一跑里一次都没触发,所以两臂的行为完全一样,它们测到的比值就是**测量本身的噪声加偏差**。判定依据不是猜的,是逐跑数会话日志里的注入条数(`zstd -dc session.v4.jsonl.zstd | grep -c`):
1702
+
1703
+ | 块 | 任务 | 逐对 `arm/def15` | 为什么是空转 |
1704
+ | --- | --- | --- | --- |
1705
+ | `BUD3_DBG` | debugfix p1 / p2 | 1.220 / 1.124 | `outputBudget 8000` 从未触发(输出 2526、5137 token < 8000),`Auto schedule` 计数 1/1 |
1706
+ | `BUDL_DBG` | debugfix p1 / p2 | 1.342 / 1.123 | 同上,延迟到第 6 步的预算也没触发,计数 1/1 |
1707
+ | `BUDL_IMPL` | codeimpl p1 | 1.151 | 8119 token 只在最后一步越过阈值,没有下一步可提醒,计数 1/1 |
1708
+ | `GATE3_DBG` | debugfix p1 | 0.889 | `shapeGate` 没有抑制任何条注入,计数 1/1 |
1709
+ | `GATE3_CSV` | csvpipe p1 | 1.303 | 同上,计数 1/1 |
1710
+ | `GATE3_IMPL` | codeimpl p1 / p2 | 1.269 / 0.978 | 同上,计数 1/1 |
1711
+
1712
+ 9 对的分布:**几何均值 1.146×、中位 1.151×、范围 0.889–1.342×、7/9 偏向「实验臂更慢」**。也就是说同一份代码、同一份提示词、同一个任务,换一次跑就能差 ±20 %,最坏一头一尾差 1.51 倍。
1713
+
1714
+ 这条数字的用处是把「不可判定」从口头判断变成可引用的边界:
1715
+
1716
+ 1. **单家族 n=2 的裁决没有信号**。`BUD3_DBG` 报的 `bud8` 慢 17 %、`GATE3_DBG` 报的 `gateonly` 慢 41 %,都落在空转对的范围里——那两块的「结论」本来就不该写成结论。之前 `shapeGate` 合池 1.014×(3/6 胜)也是同一回事:6 对里 4 对根本没抑制过注入。
1717
+ 2. **超过 ±20 % 才算有东西**。真正触发了开关的对给出的是 0.531×(ledger)、1.466× / 1.403×(codeimpl),幅度超出空转带,可以当机制证据;幅度在带内的只能进合池,不能单独成段结论。
1718
+ 3. **偏差是方向性的,不是纯随机**:9 对里 7 对偏向同一边(实验臂慢)。因此逐对比较里出现系统性慢半拍的可能来自那一跑的位置/负载,而不是开关本身——这也是块内交错 + 只报合池比值的理由,不是「再补几对就能救单家族结论」。
1719
+
1720
+ ## 把预算推迟到「回合已经长了」以后:0.956×,但赢面没了(BUDL,6 对)
1721
+
1722
+ `outputBudgetAfterSteps: 6` 的设计假设是:BUD3 里 8000 token 的伤害只发生在短任务(那里预算在第 2–3 步就烧起来,提前喊收尾),而收益发生在真正长的手上任务。于是同一条上限改成「第 6 步起才生效」,再跑三块 `off` 无关的 `def15` vs `budlate` 同块配对:
1723
+
1724
+ | 块 | 家族 | 逐对 `budlate/def15` | 预算触发 | 几何均值 |
1725
+ | --- | --- | --- | --- | --- |
1726
+ | `BUDL_DBG` | debugfix | 1.342 / 1.123 | 0 / 0(输出 4300、5769 token < 8000) | 1.228×(空转) |
1727
+ | `BUDL_IMPL` | codeimpl | 1.151 / 0.839 | 0 / 1 | 0.983× |
1728
+ | `BUDL_LEDGER` | ledger | 0.918 / 0.994 | 1 / 1 | **0.956×** |
1729
+ | 合池 | — | — | 2/6 触发 | **1.048×** |
1730
+
1731
+ 结论三条:
1732
+
1733
+ 1. **推迟消除了收益,没消除噪声。** 同一家族 ledger,BUD3 里 `bud8` 是 0.720×(一对 0.531),推迟后只剩 0.956×——因为这一跑的 `def15` 基线本来就只走 10 和 13 步(BUD3 那两跑是 18 和 16 步),长回合的收益场景没有出现。可见「长任务」不是任务标签给的,是同一标签下跑出来的步数给的,**用步数当扳机等于用上一跑的结果赌这一跑的形状**。
1734
+ 2. **代码实现类的伤害没有随推迟消失**:`BUDL_IMPL` 里触发的那一对 0.839×,没触发的 1.151×,两对之间差 37 %,再次落进空转带。
1735
+ 3. 合池 1.048×(n=6,2 对空转)落在上面 9 对空转对照的 0.89–1.34 带内 ⇒ **判定为「无效果」**,而不是「略慢」。因此 `outputBudget` 家族到此收口:上限不是速度杠杆(BUD3 已证),推迟触发也不是补救(BUDL 已证),`outputBudget` / `outputBudgetAfterSteps` 两个开关**保持默认 0 / 0**,只作为可分家族复现的实验旋钮保留。
1736
+
1737
+ 顺带一个测量警告:同样是 `ledger` + `def15`,BUD3 某跑 830 s / 18 步,BUDL 某跑 717 s / 10 步——**同一任务同一条臂的步数能差 80 %**。所以跨块比较绝对时间没有意义,只有块内交错配对的比值能用。
1738
+
1739
+ ## 开局批量化(openBatch)提示:16 对里 1.022×,家族之间符号相反(OPEN8,纯诊断)
1740
+
1741
+ 机制假设:`def15` 的系统提示已经说了「能并行就并行」,但**没有说「第一件事就把这一轮的调用一次发齐」**。`openBatch` 只在第 1 步补这一句,等于直接砍掉一个往返。为了让 n 足够大,选两个短任务家族各跑 8 对(`def15` vs `open15`,同块交错,共 16 对 32 格)。指令确认在每一条 `open15` 跑里都出现了(`grep` 会话转录,8/8 命中),所以这是「开关真的触发了」的对照,不是空转。
1742
+
1743
+ | 家族 | n | 几何均值 | 中位数 | 更快 | 步数(和对和) |
1744
+ | --- | --- | --- | --- | --- | --- |
1745
+ | debugfix | 8 | **1.228×** | 1.213 | 3/8 | 36→36(8 对里 6 对步数完全不变) |
1746
+ | csvpipe | 8 | **0.850×** | 1.020 | 4/8 | 120→70 |
1747
+ | 合池 | 16 | **1.022×** | 1.110 | 7/16 | 156→106 |
1748
+
1749
+ 三条读数:
1750
+
1751
+ 1. **合池无效果。** 几何均值 1.022×、中位数 1.110×、7/16 更快,落在空转带里;家族之间方向相反(debugfix 慢 23 %、csvpipe 快 15 %),正是 PPX2 那次「跨家族符号翻转」的重演,**任何单家族结论都不能引**。
1752
+ 2. **在 debugfix 上「少一个往返」根本没发生**:8 对里有 6 对步数一模一样(4→4、5→5),当步数不变而总时长变长时,多出来的是**输出**(如 p5 3947→5331、p8 3511→5855 token)。原因很直接:`def15` 的系统提示已经要求并行,再在第 1 步喊一次「一次发齐」,模型会把开局写成一份更大的计划式调用,**没省往返,只多写了几千 token**。
1753
+ 3. **csvpipe 的 0.850× 不是杠杆,是尾部救援**:两对(p2、p7)的 `def15` 跑崩成了 21 步(483 s)和 23 步(369 s),而对应的 `open15` 只走 6 步和 4 步;刨掉这两对,剩下 6 对是几何均值 1.36×。也就是说 `open15` 的收益全部来自「让本来会绕圈的那一跑不绕圈」,这是方差,不是速度——它同时说明 `def15` 在 csvpipe 上有不可忽略的长尾(8 对里 2 对超过 19 步,而中位数是 9–14 步)。
1754
+
1755
+ 结论:`openBatch` **保持默认 `false`**,不进入默认路径。附带一条可用的观察:csvpipe/debugfix 这类的长尾与几何均值同样重要,后面报告「更快多少」时应同时给更快对数与最慢那一对。
1756
+
1757
+ ## 提前收尾线(finishLineSteps: 8):步数砍掉 33 %,墙上时间只动 4 %(FL6,4 对)
1758
+
1759
+ `finishLineSteps` 的意思是「到了第 N 步就对模型说:现在开始收敛到最终交付」,本意是砍掉多余的收尾轮次。在 migrate 家族只跑 4 对(`def15` vs `fl8`),因为这条臂动的是回合结构,先看形状再看倍数:
1760
+
1761
+ | 对 | `fl8/def15` | 步数 | 输出 token |
1762
+ | --- | --- | --- | --- |
1763
+ | p1 | 1.288 | 11→7 | 13 224→14 418 |
1764
+ | p2 | 1.003 | 14→8 | 19 590→16 436 |
1765
+ | p3 | 0.632 | 12→10 | 23 180→17 708 |
1766
+ | p4 | 1.031 | 12→8 | 14 827→17 320 |
1767
+ | 合计 | **0.917**(几何 0.958,中位 1.017) | **49→33 = 0.674×** | 70 821→65 882 = 0.930× |
1768
+
1769
+ 这条结果推翻了一个容易写错的因果:**步数不是杠杆,是伴随变量**。4 对里步数平均砍掉 33 %,墙上时间只降 8 %(合计)/ 4 %(几何),因为被砍掉的那些步并没有消失——它们的内容被塞进了保留下来的步里,于是单步输出变大(p1、p4 的输出反而涨 9–17 %),而墙上时间 ≈ 输出 token(本文件前面测的 9.45 s/1k)。
1770
+
1771
+ 回看主口径就清楚了:`off → def15` 是**步数 0.827× 且输出 0.818× 同时下降**,那才是 0.823× 的来源;单独压缩步数(`fl8`)不降输出,所以不降时间。这条也解释了为什么 BUD3/BUDL 的预算扳机在 ledger 上能到 0.53× 而在 codeimpl 上是 1.4×:它压的是「继续写」这件事本身,不是「多一轮」这件事。
1772
+
1773
+ `finishLineSteps` 保持默认 `0`(不启用)。它作为「回合结构」旋钮的定位保留,但速度证据不支持它进默认路径。
1774
+
1775
+ ## 整文件改写的「只发改动行」提醒(preferPatch):中位数 0.926×,几何 1.104×(PP8,8 对)
1776
+
1777
+ 机制最直白的一条候选:`write` 一个 2000 字符以上的整文件,工具参数里的文件正文本身就是输出 token;提醒一次「改已有文件只发改动行(edit hunk)」,落盘内容不变,省的是解码量。codeimpl 家族 8 对 `def15` vs `pp15`(唯一差别是 `preferPatch: true`),并用会话转录确认提醒在 8/8 条 `pp15` 跑里都真的发了、`def15` 里 0 次:
1778
+
1779
+ | 对 | `pp15/def15` | 输出 token | 步数 |
1780
+ | --- | --- | --- | --- |
1781
+ | p1 | 0.651 | 13 188→9 616 | 6→7 |
1782
+ | p2 | 0.870 | 20 981→16 876 | 8→5 |
1783
+ | p3 | **4.549** | 8 068→31 819 | 4→8 |
1784
+ | p4 | 1.520 | 15 373→22 977 | 7→13 |
1785
+ | p5 | 0.561 | 12 904→7 443 | 5→6 |
1786
+ | p6 | 0.907 | 27 506→21 869 | 14→15 |
1787
+ | p7 | 1.169 | 10 489→16 074 | 5→11 |
1788
+ | p8 | 0.945 | 18 255→17 008 | 7→8 |
1789
+ | 合计/统计 | 几何 **1.104**,中位 **0.926**,5/8 更快 | 126 764→133 682 = 1.055 | 56→73 = 1.30 |
1790
+
1791
+ 读数:
1792
+
1793
+ 1. **机制真的生效了,而且是双向的。** 5 对里输出立降 7–42 %(p1 −27 %、p5 −42 %、p2 −20 %、p6 −20 %),说明「别发整个文件」确实省解码;但另外 3 对里提醒之后模型改成**多次小改**,步数从 4→8、7→13、5→11,输出反而涨到 3.9 倍——省下的单次正文被多出来的回合吃掉了。
1794
+ 2. **这是双峰分布,不是均匀位移。** 中位数 0.926×(多数对更快)与几何均值 1.104×(被一对 4.5× 拖累)同时成立;5/8 更快也成立。此时任何一个单独报数都是误导,三个必须一起出现——这也是本文件反复强调「只用几何/中位而不用比值均值」的边界:它能挡住的是均值陷阱,挡不住重尾。
1795
+ 3. 触发链也解释得通:提醒只在「已经写过一个大整文件之后」才发,这条路径上本来就有两种世界——一个文件改一处(赚 20–40 %),或一个文件要改多处(小改的固定开销每处再付一次)。运行时无法预知是哪一种,所以这不是可门控的杠杆。
1796
+
1797
+ ### migrate 上重跑:同一个方向更明显(PP6,6 对)
1798
+
1799
+ 换到 migrate(大量「同一文件多处小改」的任务)再跑 6 对,提醒同样 6/6 触发:
1800
+
1801
+ | 对 | `pp15/def15` | 步数 | 输出 |
1802
+ | --- | --- | --- | --- |
1803
+ | p1 | 0.971 | 16→8 | 26 172→27 199 |
1804
+ | p2 | 2.901 | 9→33 | 13 757→27 560 |
1805
+ | p3 | 1.036 | 8→11 | 18 473→18 680 |
1806
+ | p4 | 2.029 | 15→16 | 16 963→31 967 |
1807
+ | p5 | 0.616 | 11→12 | 21 399→15 324 |
1808
+ | p6 | 1.642 | 8→15 | 14 181→23 842 |
1809
+ | 合计 | 几何 **1.348**,中位 1.339,2/6 更快 | 67→95 = 1.42 | 110 945→144 572 = 1.30 |
1810
+
1811
+ 两个家族合并(14 对):**几何 1.203×、中位 1.100×、7/14 更快、步数 1.37×、输出 1.21×**。migrate 的 p2(9→33 步)说明小改策略在「多处改动」任务上会失控:每处都付一次完整回合的固定开销(约 32 % 的单步时间不在模型调用里),回合数一涨总时间必涨。
1812
+
1813
+ 结论:`preferPatch` 保持默认 `false`,不进入默认路径;两族合并 1.203× 属于「明确更差」,不是噪声。若要继续,唯一有意义的方向是给它一个「本轮已经出现 ≥2 次小改就停手」的止损条件,而不是默认打开——那属于新机制,需要新的同块诊断,不在这条结论内。
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.5.12
4
+
5
+ - **合池口径定为 138 对(纯数据与文档,无行为变更)**:12 个干净块、138 对有效配对、10 个家族 —— 总墙钟 **0.8234×**(95 % 区间 0.7418–0.9133)、逐格中位 **0.7994×**(0.7026–0.8850)、几何均值 0.8248×(0.7532–0.9001)、输出 0.8179×、步数 0.8062×、**93/138 对更快**、符号检验 **p = 0.0001**。质量同口径结论只有「断言对等」:`off` 2038/2038、默认 2037/2038,唯一差异是一格 21/22 —— 不写成「零质量损失」。README 与市场描述的 132 对 / 0.825× 口径一并更新;算术均值 0.872 及其区间**禁用**(区间上界越 1.0)。
6
+ - **新旋钮 `disciplineAfterSteps`(默认 `0`,行为不变)**。整个插件里唯一被测出真实效果的机制,是 `outputDiscipline: late` 在第 4 步注入一次「省着写」的提醒;它的**注入步数**此前是写死的常量。现在可配:`0` = 用内建默认 4,正数 = 覆盖(`src/config.js` 校验 0–40,`src/effort.js` 只在 `gear: auto` 时才带出去,`src/auto.js` 用 `disciplineAt` 判步)。**这只是把已有常量变成可扫参数,不是新机制**:默认路径逐字节相同,因此不需要新标签重跑合池。`bench/arms/d2.yml`、`d3.yml` 是它的消融臂;数据尚未跑出,本版**不对提速幅度作任何声明**。
7
+ - **`preferPatch` 淘汰(保留默认 `false`)**:它让整文件写入改发 hunk,同块 `pp15` 对 `def15` 跑了两个家族 —— `codeimpl` 8 对几何 1.104×(0.651 / 0.870 / 4.549 / 1.520 / 0.561 / 0.907 / 1.169 / 0.945),`migrate` 6 对几何 1.348×(0.971 / 2.901 / 1.036 / 2.029 / 0.616 / 1.642),合并 14 对 **几何 1.203×、中位 1.100×、7/14 更快、步数 1.37×、输出 1.21×**。提醒确实下发了(逐会话搜确切字符串,`pp15` 8/8 与 6/6、`def15` 0 次),所以问题不在接线而在机制:把一处改动拆成多次小写,每次都要付一次完整回合的固定开销(单步时间约 32 % 不在模型调用里)。双峰分布(0.561 到 4.549)说明它不是稳定杠杆。
8
+ - **`openBatch` 淘汰(保留默认 `false`)**:合并 16 对几何 1.022×、中位 1.110×、7/16 更快 —— 空到负,W6 的 0.77× 再未复现。
9
+ - **延迟输出预算帽 `BUDL` 为空**:每家族几何 `debugfix` 1.228×(两对都空)、`codeimpl` 0.983×、`ledger` 0.956×,合并 6 对 1.048×。`outputBudget` / `outputBudgetAfterSteps` 维持 `0`。
10
+ - **`finishLineSteps` 的步数不是杠杆**:`fl6` 臂把收尾提醒提前到第 8 步,4 对里步数合计 49→33(**0.674×**),但墙钟 **0.958×**(4/4 里 1 对更快)—— 步数与墙钟脱钩,所以「砍步数」不能当提速证据。
11
+ - **空构造对照校准了判定门槛**:同一个默认臂对同一个默认臂跑 9 对(两边配置完全相同),几何 **1.146×**、中位 1.151×、范围 0.889–1.342×、7/9 在 1 以上。结论:单家族 n=2 的胜负全是噪声,只有超过 ±20 % 的差才算机制;偏差是方向性的(后跑的一臂更慢),来自位次与负载。
12
+ - **测试**:263 个用例全绿(`node --test test/*.test.js`,10.5 s),新增 `disciplineAfterSteps` 的「按配置步数提前触发、`0` 回落到内建 4」用例,以及 `batchVerify` 的「第二次未验证写入才触发、每回合一次、新回合可再触发」用例。
13
+ - **版本**:`1.5.12`。`1.5.11` 是元数据/版本对齐发版(无行为变更),未单列条目。
14
+
3
15
  ## 1.5.10
4
16
 
5
17
  - **接线审计(先说结论:1.5.9 有两条机制从未真正生效)**。按 schema 的 23 个键逐个对照 `resolveConfig()` 返回的 policy,发现 `shapeGate`、`outputBudget` 在 `src/config.js` 里声明、却**没有**被 `resolveConfig` 带出去:钩子读到的永远是 `undefined`,`bud8`、`sg15`、`gateonly`、`gate15` 四个臂跑的都是 `def15` 的行为。所以这些块的数字**不是“机制没用”,而是“机制没被测到”**——一律标 **untested**,不写成胜负;`BENCHMARK.md` 里受影响的表格行、G 段与 SG2 的对照结论已就地撤回。这两个键现在真的接上了,并有传播测试(`test/effort.test.js`)防回归。
package/README.en.md CHANGED
@@ -40,7 +40,7 @@ No model switch, no `reasoningEffort` rewrite, no request-field injection (`serv
40
40
 
41
41
  ## Measured
42
42
 
43
- **Toggle it on and the headline is: about 18% faster in total, and 15–25% faster on the shapes it fits.** That is every clean block with an `off` anchor pooled (9 blocks, 132 valid pairs, 10 task families; recompute with `python3 bench/pool.py`): total wall clock **0.825×** (18% less), output tokens 0.822×, steps 0.809×, per-cell median 0.813×, faster in 89 of 132 pairs. Bootstrapping those 132 pairs (`python3 bench/pool.py --ci 2000`) puts the total ratio at a 95% interval of **[0.747, 0.914]** (9% to 25% faster) and the per-cell median at [0.710, 0.883] — "about 18%" is a point estimate, not a floor. Per-family stability and sign tests are in [BENCHMARK.md](BENCHMARK.md), section "合池的自助区间与逐任务稳定性", which also explains why per-family numbers cannot be quoted across blocks.
43
+ **Toggle it on and the headline is: about 18% faster in total, and 25–30% faster on the shapes it fits.** That is every clean block with an `off` anchor pooled (12 blocks, 138 valid pairs, 10 task families; recompute with `python3 bench/pool.py`): total wall clock **0.823×** (18% less), output tokens 0.818×, steps 0.806×, per-cell median 0.799×, faster in 93 of 138 pairs (sign test p = 0.0001). Bootstrapping those 138 pairs 2000 times puts the total ratio at a 95% interval of **[0.742, 0.913]** (9% to 26% faster), the per-cell median at [0.703, 0.885], and the geometric mean at 0.825× (interval 0.753–0.900) — "about 18%" is a point estimate, not a floor. The only quality statement at the same denominator: hidden assertions matched cell for cell across the 138 pairs (`off` 2038/2038, default 2037/2038, the one difference being a 21/22 cell) — that is **assertion parity**, not "zero quality loss". Per-family stability and sign tests are in [BENCHMARK.md](BENCHMARK.md), section "合池的自助区间与逐任务稳定性", which also explains why per-family numbers cannot be quoted across blocks.
44
44
 
45
45
  **When to turn it on, and when not to.** On: mid-length tasks with several artifacts and repeated re-checks — `codeimpl` 0.663x (faster in 13 of 15 pairs, sign test p = 0.007) and `shellpipeline` 0.749x (p = 0.039), with `stategraph` 0.708x, `shellops` 0.748x, `codebase` 0.808x and `migrate` 0.822x on the same side but with too few pairs to stand alone. Off: one-to-five-step repairs and data reshaping — `debugfix` 1.032x (output actually 1.29x longer) and `perfrefactor` 1.009x, where the injected reminder cannot pay for itself. `ledger` 0.915x and `csvpipe` 0.982x are a wash. The switch is the lightning bolt per conversation; it remembers your last choice and applies from the next request.
46
46
 
package/README.md CHANGED
@@ -40,7 +40,7 @@
40
40
 
41
41
  ## 实测
42
42
 
43
- **打开小闪电,排头的一句结论:整任务总量约快 18 %,适合的形状快 15–25 %。** 这是把**所有**有 `off` 锚的干净块一起加总的结果(9 个块、132 对有效配对、10 个任务家族,`python3 bench/pool.py` 可复算):总墙钟 **0.825×**(省 18 %)、输出 token 0.822×、步数 0.809×、逐格中位 0.813×、132 对里 89 对更快。对这 132 对做自助重抽(`python3 bench/pool.py --ci 2000`),总比 95 % 区间是 **[0.747, 0.914]**(即省 9 %–25 %),逐格中位区间 [0.710, 0.883]——「约 18 %」是点估计、不是下限承诺;逐家族稳定性与符号检验见 [BENCHMARK.md](BENCHMARK.md) 的「合池的自助区间与逐任务稳定性」一节,那里也写了为什么逐家族数字不能跨块引用。
43
+ **打开小闪电,排头的一句结论:整任务总量约快 18 %,适合的形状快 25–30 %。** 这是把**所有**有 `off` 锚的干净块一起加总的结果(12 个块、138 对有效配对、10 个任务家族,`python3 bench/pool.py` 可复算):总墙钟 **0.823×**(省 18 %)、输出 token 0.818×、步数 0.806×、逐格中位 0.799×、138 对里 93 对更快(符号检验 p = 0.0001)。对这 138 对自助重抽 2000 次,总比 95 % 区间是 **[0.742, 0.913]**(即省 9 %–26 %),逐格中位区间 [0.703, 0.885],几何均值 0.825×(区间 0.753–0.900)——「约 18 %」是点估计、不是下限承诺。质量的同口径结论只有一句:138 对里两臂隐藏断言逐格完全相同(`off` 2038/2038、默认 2037/2038,唯一差的是一格 21/22),这是**断言对等**,不是「零质量损失」。逐家族稳定性与符号检验见 [BENCHMARK.md](BENCHMARK.md) 的「合池的自助区间与逐任务稳定性」一节,那里也写了为什么逐家族数字不能跨块引用。
44
44
 
45
45
  **什么时候开、什么时候别开(按实测形状,不按任务名)。** 开:有多份产物、要反复跑同一份检查的中长任务——`codeimpl` 0.663×(15 对里 13 对更快,符号检验 p = 0.007)、`shellpipeline` 0.749×(p = 0.039),`stategraph` 0.708×、`shellops` 0.748×、`codebase` 0.808×、`migrate` 0.822× 也在同一侧但样本撑不起单列结论。别开:1–5 步的小改修与数据整形——`debugfix` 1.032×(输出反而长 1.29×)、`perfrefactor` 1.009×,提醒本身的注入成本收不回来。`ledger` 0.915×、`csvpipe` 0.982× 打平,开不开都行。开关就是每会话的小闪电,默认记住上次选择,下一个请求生效。
46
46
 
package/index.js CHANGED
@@ -95,7 +95,7 @@ const TAG = '[fast-mode]'
95
95
  * Shipped identity of this build. Traced into the debug log so a stale install is
96
96
  * visible at a glance, and pinned by a test against `package.json`.
97
97
  */
98
- export const VERSION = '1.5.10'
98
+ export const VERSION = '1.5.12'
99
99
  const GLOBAL_AGENT = '__global__'
100
100
 
101
101
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-fast-mode",
3
- "version": "1.5.10",
3
+ "version": "1.5.12",
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",
@@ -77,7 +77,10 @@
77
77
  "immediately": true
78
78
  }
79
79
  },
80
- "author": "regrevia",
80
+ "author": {
81
+ "name": "regrevia",
82
+ "url": "https://github.com/regrevia"
83
+ },
81
84
  "icon": "./icon.png",
82
85
  "bin": {
83
86
  "dsh-fast-mode-experiment": "./bin/experiment.mjs",
package/src/auto.js CHANGED
@@ -119,7 +119,7 @@ export function noteEntry(state, agent, width) {
119
119
  *
120
120
  * @param {ReturnType<typeof createAutoState>} state
121
121
  * @param {string} agent
122
- * @param {{lateDiscipline?: boolean, stepBudget?: number, finishLineSteps?: number, outputBudget?: number, openBatch?: boolean, shapeGate?: boolean, scheduler?: boolean, fanoutWidth?: number, batchVerify?: boolean, preferPatch?: boolean}} policy
122
+ * @param {{lateDiscipline?: boolean, stepBudget?: number, finishLineSteps?: number, outputBudget?: number, outputBudgetAfterSteps?: number, openBatch?: boolean, shapeGate?: boolean, scheduler?: boolean, fanoutWidth?: number, batchVerify?: boolean, preferPatch?: boolean}} policy
123
123
  */
124
124
  export function notePolicy(state, agent, policy) {
125
125
  const entry = entryOf(state, agent)
@@ -127,6 +127,10 @@ export function notePolicy(state, agent, policy) {
127
127
  // A token budget counts decode work, which is what the wall clock is actually
128
128
  // made of; 0 is the off switch, exactly like stepBudget.
129
129
  entry.outBudget = stepCount(policy?.outputBudget, 0, 0)
130
+ entry.outBudgetAfterSteps = stepCount(policy?.outputBudgetAfterSteps, 0, 0)
131
+ // 0 keeps the built-in entry step; a positive value moves the one reminder that
132
+ // has a measured effect (see BENCHMARK: compress-output) earlier or later.
133
+ entry.disciplineAfterSteps = stepCount(policy?.disciplineAfterSteps, 0, 0)
130
134
  // 0 is a value, not "unset": it is how a caller asks for no hard stop at all.
131
135
  entry.turnStepBudget = stepCount(policy?.stepBudget, AUTO_LIMITS.turnStepBudget, 0)
132
136
  entry.finishLineSteps = stepCount(policy?.finishLineSteps, AUTO_LIMITS.finishLineSteps, 0)
@@ -882,7 +886,8 @@ function directiveFor(entry, stats) {
882
886
  summary: 'auto: group the verification of this edit batch',
883
887
  }
884
888
  }
885
- if (entry.lateDiscipline && !entry.disciplineSent && stats.turnSteps >= AUTO_LIMITS.disciplineAfterSteps) {
889
+ const disciplineAt = entry.disciplineAfterSteps || AUTO_LIMITS.disciplineAfterSteps
890
+ if (entry.lateDiscipline && !entry.disciplineSent && stats.turnSteps >= disciplineAt) {
886
891
  // Late engagement: the prompt carried no prose rules, so the only cost of a
887
892
  // short task was its own work. By this step the task is demonstrably long,
888
893
  // and every remaining step re-pays its output tokens in decode time.
@@ -899,7 +904,8 @@ function directiveFor(entry, stats) {
899
904
  }
900
905
  }
901
906
  const outBudget = entry.outBudget
902
- if (outBudget > 0 && !entry.outBudgetSent && stats.turnOutTokens >= outBudget) {
907
+ const budgetArmed = entry.steps >= (entry.outBudgetAfterSteps || 0)
908
+ if (outBudget > 0 && budgetArmed && !entry.outBudgetSent && stats.turnOutTokens >= outBudget) {
903
909
  // Evidence that the turn is expensive, not that it is long: the counter is
904
910
  // the provider's own usage. A three-step turn that wrote 30 000 tokens is a
905
911
  // cost problem the step counter cannot see, and a twenty-step turn that
package/src/config.js CHANGED
@@ -128,6 +128,15 @@ export const Config = z.object({
128
128
  '输出 token 预算 / Output token budget: 本轮累计输出 token(上游 usage 的 outputTokens)达到该值时,调度器提醒一次「收掉当前闭环、跑一次证明它的检查、汇报」;0 = 关闭(默认)。触发的是真实解码量而非步数:三步写出三万 token 的任务很贵,二十步只写两千 token 的任务不贵。属于成本上限,不是完成度证据——默认关闭,避免在仍在推进的任务上提前收尾。',
129
129
  ),
130
130
 
131
+ outputBudgetAfterSteps: z
132
+ .number()
133
+ .step(1)
134
+ .min(0)
135
+ .max(80)
136
+ .default(0)
137
+ .description(
138
+ '输出预算生效步数 / Budget arm delay: 只有本轮已经跑了这么多步之后,`outputBudget` 才允许提醒一次;0 = 立即生效(沿旧行为)。逐家族实测中同一个 8000 token 预算在长任务(ledger)上省 47 % 墙钟,在短任务(codeimpl/debugfix)上因为提前收尾反而慢 12–47 %;这条延迟让预算只对「已经变长」的轮次起作用。',
139
+ ),
131
140
  openBatch: z
132
141
  .boolean()
133
142
  .default(false)
@@ -142,6 +151,16 @@ export const Config = z.object({
142
151
  '整文件改写提醒 / Prefer hunks: 一次整文件 write(参数 ≥ 2000 字符)之后,提醒一次「改已有文件时只发改动的行(edit hunk),不发整个文件」。落盘内容相同,省的是工具参数的输出 token(约占输出字符 28%)。只发一次,不改完成判定,不改复验。',
143
152
  ),
144
153
 
154
+ disciplineAfterSteps: z
155
+ .number()
156
+ .step(1)
157
+ .min(0)
158
+ .max(40)
159
+ .default(0)
160
+ .description(
161
+ '压缩指令的入场步 / Compression entry step: `outputDiscipline: late` 时,第几步才提醒「剩下的话只写下一步要用的」;0 = 用内置值(4)。这条提醒是本插件唯一被证明有效的机制(回合变少 + 每步输出变短),所以它的入场点值得单独调:太早会在任务还没展开时就把话掐掉,太晚则前几步的散文已经花掉。0 保持旧行为。',
162
+ ),
163
+
145
164
  workspaceMap: z
146
165
  .boolean()
147
166
  .default(false)
package/src/effort.js CHANGED
@@ -250,6 +250,14 @@ export function resolveConfig(raw, overrides) {
250
250
  scheduler: gear.directive && config.scheduler !== false,
251
251
  /** Optional ceiling on decoded output per turn, from upstream usage; 0 = off. */
252
252
  outputBudget: gear.directive ? Math.max(0, Math.floor(number(config.outputBudget, 0))) : 0,
253
+ /** Steps a turn must have run before the output budget may fire; 0 = immediately. */
254
+ outputBudgetAfterSteps: gear.directive
255
+ ? Math.max(0, Math.floor(number(config.outputBudgetAfterSteps, 0)))
256
+ : 0,
257
+ /** Step at which the late compression reminder fires; 0 = built-in default. */
258
+ disciplineAfterSteps: gear.directive
259
+ ? Math.max(0, Math.floor(number(config.disciplineAfterSteps, 0)))
260
+ : 0,
253
261
  /** One "batch the opening step" reminder per turn, for short tasks. */
254
262
  openBatch: gear.directive && config.openBatch === true,
255
263
  /** One "send hunks, not whole files" reminder after a large whole-file write. */