dsh-fast-mode 1.5.26 → 1.5.28

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
@@ -6,6 +6,26 @@
6
6
  >
7
7
  > **口径(1.5.1 起)**:一个单元只在 `exit == 0 && 步数 ≥ 1 && 输出 > 0 && 判分完成 && 未泄漏` 时计数;运行崩溃的单元整格作废并点名,不算红、也不算快。所以「全绿」一律指**有效单元全绿**(1.5.1 29/29、W5 19/19、W7 17/17)。
8
8
 
9
+ ## 提醒投递的稳定性(2026-10-29):压缩重入会让同一轮收到两条
10
+
11
+ `bench/turns.py` 按 `turn/start` 切轮,把「够长却没收到提醒」「同一轮收到两次以上」「后续轮是否重新上膛」从会话记录里数出来,不看墙钟、不看主观印象。
12
+
13
+ 72 小时窗口、68 个真实桌面会话、1446 轮(`--dir='--Users-*' --since-hours 72`):
14
+
15
+ | 指标 | 读数 |
16
+ | --- | --- |
17
+ | 合格轮(≥ 4 步) | 647 |
18
+ | 收到压缩提醒 | 646(**99.8 %**) |
19
+ | 同一轮收到 ≥ 2 条 | 114 |
20
+ | 其中两次之间夹着 `compaction` | **105(92 %)** |
21
+ | 多轮会话后续轮重新上膛 | 599 / 600 |
22
+
23
+ 前两行说明闸门本身没有漏投;最后一行说明「每轮重新上膛」是真的在工作。问题只在第三行:**轮内上下文压缩之后,同一轮会再收到一次同一条提醒**。
24
+
25
+ 根因在宿主侧的重入:压缩时该轮没有结束,宿主用**同一个 turn 号**再次进入(`turn/start` 不落盘,所以只会在插件的事件流里看到)。`noteTurn` 原先只要看到 `turn/start` 就清空每轮闩锁,`disciplineSent` 被复位,所以压缩后第 4 步又发一次。
26
+
27
+ 1.5.28 用 turn 号判别:同号 = 续跑,不清闩锁、不清计数器;号变大才是新轮。事件流没有 turn 号时保持旧行为。压缩后本就有独立的 reassert 提醒,所以压掉这条重复提醒不降低纪律覆盖。
28
+
9
29
  ## 1.5.1 干净重基线(2026-10-26):10 家族 30 个单元,总墙钟 0.72×、逐格中位 0.64×、隐藏测试 60/60 全绿
10
30
 
11
31
  1.5.0 那一节的数字(0.86× / 0.95× / 0.85×)因两个测量缺陷整体撤回,这一节是修好缺陷之后、用当前默认(`def15`,即插件 `enabled: true` 的出厂配置)重跑的结果。**这是 1.5.x 唯一的速度声明。**
@@ -1944,6 +1964,23 @@ python3 bench/suite-run.py --tasks debugfix,ledger,codeimpl --arms off,def15,bud
1944
1964
 
1945
1965
  任何一条不过 ⇒ 这条线判为「时点不可救」,默认永久停在 4,不再追。
1946
1966
 
1967
+ ### 补测(2026-10-29,`D2G2`):闸门没覆盖过的两族补上,方向相反
1968
+
1969
+ 上面的 21 对只覆盖 `codeimpl`/`debugfix`/`migrate`。闸门的常数 2 在 `ledger`/`shellpipeline` 上一次都没验过,而这两族恰好是 `d2`(无闸门提前)最两极的地方(`ledger` 0.506×、`shellpipeline` 1.111×)。`D2G2` 用这两族各 3 对补齐(`def15` 为基线,12 行全部可用、丢弃 0、质量零倒退):
1970
+
1971
+ ```
1972
+ python3 bench/suite-run.py --tasks ledger,shellpipeline --arms def15,d2g --pairs 3 --tag D2G2
1973
+ python3 bench/block_score.py D2G2 def15 d2g
1974
+ ```
1975
+
1976
+ | 族 | 对 | 几何 | 逐对比值 | 备注 |
1977
+ |---|---|---|---|---|
1978
+ | `ledger` | 3 | **1.898×** | 2.68 / 0.95 / **2.70** | 输的两对步数 8→10、7→13,输出 12.1k→34.0k、15.9k→29.1k token |
1979
+ | `shellpipeline` | 3 | **0.597×** | 0.47 / 0.69 / 0.66 | 赢的三对步数 23→19、27→10、24→17 |
1980
+ | 合并(= 上节 21 对 + 6 对) | 27 | **0.816×** CI95 [0.650, 1.023] | 18/27 更快 | 中位 0.719×、步数 0.892×、输出 0.777×、符号 p=0.122 |
1981
+
1982
+ **判定:HOLD 不变,默认继续停在第 4 步。** 补测不是把 CI 收窄,而是把结论说得更准:同一个时点在两族上方向相反——`shellpipeline` 这种「一条命令跑完再读结果」的任务提前提醒很值(0.597×),`ledger` 这种要逐笔对账的任务提前提醒会让它重做并超写(输出 token 涨到 1.9×,单对 2.7×)。所以「第一步调了几次」不是这个时点该用的信号,**时点类旋钮到此收档**:默认 4 不再动,也不再为新时点补块。
1983
+
1947
1984
  ## 预注册:`disciplineRepeatAtSteps`(第二次压缩提醒)的通过判据(2026-10-28,先写判据再跑)
1948
1985
 
1949
1986
  **为什么是它。** 合并块里唯一被证明有效的机制,是 `outputDiscipline: late` 在第 4 步注入的那一句「剩下的话只写下一步要用的」。它**每回合只发一次**。可是合池的逐格数据显示,回合越长,赢面越小:`debugfix` 1.032×、`perfrefactor` 1.009×、`csvpipe` 0.982×,而 `shellpipeline` 0.699×、`codeimpl` 0.710× —— 长的回合里,第一次提醒之后的那些步又把输出写长了。这不是新机制,同一个机制的第二枪:同一句话,在更晚的一步再说一次,每回合仍然最多一次。
@@ -2313,7 +2350,7 @@ python3 bench/ablate.py --ci 2000 --anchor def15 # 相对默认配置(
2313
2350
  | 臂 = 默认 + 打开 | n | wall 几何比 | CI95 | 中位 | 步数 | 输出 | 更快 | p |
2314
2351
  |---|---|---|---|---|---|---|---|---|
2315
2352
  | `d2`:明细律提前到第 2 步 | 41 | **0.695** | [0.565, 0.863] | 0.700 | 0.960 | 0.723 | 27/41 | 0.060 |
2316
- | `d2g`:同上 + 第 1 步 ≥2 次调用才提前 | 21 | **0.756** | [0.605, 0.939] | 0.719 | 0.885 | 0.704 | 14/21 | 0.189 |
2353
+ | `d2g`:同上 + 第 1 步 ≥2 次调用才提前 | 27 | **0.816** | [0.650, 1.023] | 0.719 | 0.892 | 0.777 | 18/27 | 0.122 |
2317
2354
  | `map15` +`workspaceMap` | 23 | 0.802 | [0.648, 0.999] | 0.790 | 1.048 | 0.824 | 13/23 | 0.678 |
2318
2355
  | `open15` +`openBatch` | 53 | 0.882 | [0.740, 1.049] | 0.911 | 0.831 | 0.915 | 30/53 | 0.410 |
2319
2356
  | `bv15` +`batchVerify` | 39 | 0.963 | [0.813, 1.144] | 0.968 | 0.935 | 0.961 | 20/39 | 1.000 |
@@ -2323,8 +2360,10 @@ python3 bench/ablate.py --ci 2000 --anchor def15 # 相对默认配置(
2323
2360
  | `nodisc` 关掉明细律 | 19 | 1.101 | [0.808, 1.492] | 1.110 | 0.998 | 1.049 | 8/19 | 0.648 |
2324
2361
 
2325
2362
  - **结论 1:默认配置已经在平台上。** 除时点类两臂外,每个可选开关的 CI 都跨 1,几何比落在 0.80–1.08;把 `openBatch`、`workspaceMap`、`batchVerify`、`preferPatch`、更宽并行、步数预算一起打开,一个百分点都拿不到。所以「多开几个开关会更快」是错的——它们的收益在默认里已经兑现,或者从来就不存在。
2326
- - **结论 2:唯一在池化下站得住的杠杆是「同一句提醒的时点」。** `d2` 41 对 0.695×(CI 上限 0.863 仍 < 1),但它是任务相关的:分族 `debugfix` 0.619、`ledger` 0.506、`migrate` 0.569,`codeimpl` **1.073**、`shellpipeline` **1.111**。加条件闸门后 `d2g` 把 `codeimpl` 收到 0.889,同时把 `debugfix` 的 0.619 抬到 0.903 ⇒ 池化 0.756×(21 对)——**净收益变小的同时方差也变小**。
2327
- - **结论 3:这两臂仍是 HOLD,不改默认。** `d2` 在 `codeimpl` 上是负收益,`d2g` 的闸门常量为 2、只在 3 个家族验过;按预注册判据(`D2_IMPL` 1.073×、3/8 更快)默认保持 `disciplineAfterSteps: 4`。这张表的作用是说明**没有别的开关可以在默认之上继续叠加**,以及下一版要做的是把 `d2g` 的闸门在 `ledger`/`shellpipeline` 上补测,而不是再加开关。
2363
+ - **结论 2:唯一在池化下站得住的杠杆是「同一句提醒的时点」。** `d2` 41 对 0.695×(CI 上限 0.863 仍 < 1),但它是任务相关的:分族 `debugfix` 0.619、`ledger` 0.506、`migrate` 0.569,`codeimpl` **1.073**、`shellpipeline` **1.111**。加条件闸门后 `d2g` 把 `codeimpl` 收到 0.889,同时把 `debugfix` 的 0.619 抬到 0.903 ⇒ 池化一度是 0.756×(21 对)。把闸门从没测过的 `ledger`/`shellpipeline` 补上后(`D2G2`,6 对)这条也塌了:27 对 **0.816×**、CI95 **[0.650, 1.023]**、18/27 更快。
2364
+ - **结论 3:这两臂仍是 HOLD,不改默认。** `d2` 在 `codeimpl` 上是负收益,`d2g` 的闸门常量为 2、只在 3 个家族验过;按预注册判据(`D2_IMPL` 1.073×、3/8 更快)默认保持 `disciplineAfterSteps: 4`。这张表的作用是说明**没有别的开关可以在默认之上继续叠加**;`ledger`/`shellpipeline` 的补测(`D2G2`)已经做完,见下一节,闸门在这两族上方向相反,时点类两臂到此可以收档。
2365
+
2366
+ **顺带把「只挑了 12 个干净块」这个质疑也答了。** `ablate.py` 不挑块(59 个全进),默认臂对 `off` 的池化读数是 **183 对、几何 0.816×、CI95 [0.755, 0.881]、122/183 更快、符号 p < 0.0001**——与 `pool.py` 那 12 个干净块的 138 对 0.8234×(CI [0.744, 0.918])落在同一点上,置信区间还更窄。所以主结论不是选块选出来的。
2328
2367
 
2329
2368
  **这张表的两个诚实边界**:不同臂覆盖的任务组合不同(跑一遍命令就能看到 `tasks` 列,`d2` 含 `codeimpl:8`、`map15` 只有 `codebase/debugfix/perfrefactor`),所以行与行之间不能直接排大小;以及每个块只采样一次,`n < 20` 的行只算方向提示。
2330
2369
 
package/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.5.28(稳定性:压缩后同一轮里不再重复发同一条提醒)
4
+
5
+ - **先量后改**:新增 `bench/turns.py`,把留存会话按 `turn/start` 切成轮,统计「够长却没收到提醒」「同一轮收到两次以上」「多轮会话里后续轮是否重新上膛」。72 小时窗口 68 个会话 / 1446 轮:647 个合格轮**646 个收到**提醒(99.8 %)、后续轮重新上膛 599/600;但**同一轮收到两次的有 114 轮**,其中 **105 轮(92 %)两次之间夹着一次上下文压缩**。
6
+ - **根因**:轮内压缩时宿主重入该轮(同一 turn 号再走一次 `turn/start`),`noteTurn` 把它当新轮清空了全部每轮闩锁,`disciplineSent` 被复位,于是第 4 步发过的提醒在压缩后第 4 步又发一次。
7
+ - **修复**:`noteTurn` 接受宿主给的 turn 号,**只在新 turn 号变大时清闩锁**;同号视为续跑,计数器与闩锁全部保留(`src/auto.js`、`index.js` 传 `event.data.turn`)。事件流不带 turn 号时退回旧行为,不会更差。压缩后本来就有独立的 reassert 提醒,所以这里压掉重复的那一条不影响纪律覆盖。
8
+ - **测试**:新增「续跑的轮保住闩锁、新轮重新上膛、无 turn 号仍按旧行为」用例;`node --test test/*.test.js` **305/305**,`style-lint` 0。
9
+ - **没有行为变更**:默认值、提醒时机、提示词与 1.5.27 完全一致;这是投递去重,不是新旋钮。
10
+
11
+ ## 1.5.27(把闸门最后两个没测过的任务族补上:方向相反,时点类旋钮收档)
12
+
13
+ - **补测:`bench/suite-run.py --tasks ledger,shellpipeline --arms def15,d2g --pairs 3 --tag D2G2`**(12 次运行、全部可用、丢弃 0、质量零倒退)。`d2g` 的闸门常量(第 1 步 ≥2 次调用才提前)此前只在 `codeimpl`/`debugfix`/`migrate` 三族验过,`ledger`/`shellpipeline` 一个数都没有。
14
+ - **结果方向相反,池化仍不支持提前**:`shellpipeline` 3 对几何 **0.597×**(0.47 / 0.69 / 0.66,步数 23→19、27→10、24→17),`ledger` 3 对几何 **1.898×**(2.68 / 0.95 / 2.70,输的两对输出 token 12.1k→34.0k、15.9k→29.1k)。并进前 21 对后:27 对 **0.816×**、CI95 **[0.650, 1.023]**、18/27 更快、中位 0.719×、符号 p = 0.122。
15
+ - **判定不变:HOLD,默认继续停在第 4 步。** 结论也更准了:提前提醒的价值取决于任务「一条命令跑完再读」还是「逐笔对账」,而不取决于第一步调了几次;时点类旋钮到此收档,不再为新时点补块。
16
+ - **`BENCHMARK.md` 同步**:`d2g` 的消融行 21 → 27 对、0.756 → 0.816、CI 改为 [0.650, 1.023];新增「补测(`D2G2`)」一节记账两条反向的逐对比值。
17
+ - **没有行为变更**:只跑块、改文档;默认值、提醒时机、提示词与 1.5.26 完全一致。
18
+
3
19
  ## 1.5.26(把「多开几个开关会不会更快」变成一张可复算的表)
4
20
 
5
21
  - **新增 `bench/ablate.py`**:把 59 个块的全部臂按「同块、同任务、同 pair」对锚点臂池化,输出每个开关相对默认配置(`--anchor def15`)或相对 `off` 的几何比、95 % bootstrap CI、中位、步数/输出比、更快对数与符号检验。这是第一次用一张表回答「多个开关一起开的效果」。
package/bench/README.md CHANGED
@@ -136,6 +136,19 @@ node bench/verify-arm-reminders.mjs --arms # 臂 x 场景的提
136
136
 
137
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
138
 
139
+ ### 提醒是否每轮重新上膛、且每轮只发一次
140
+
141
+ `bench/turns.py` 读留存会话,按 `turn/start` 切轮,输出:轮数/会话、合格轮(默认 ≥ 4 步)、**没收到提醒**的合格轮、**收到两次以上**的轮、多轮会话里后续轮的重新上膛率。只有 `user/message` / `agent/inbox/spliced` / `system/message` 里的文案算投递,模型在自己正文里复述的不算。
142
+
143
+ ```bash
144
+ python3 bench/turns.py # 跑批会话(默认 suite 目录)
145
+ python3 bench/turns.py --since-hours 48 # 真实会话近 48 小时
146
+ python3 bench/turns.py --exclude-arm nodisc # 排除某个臂的会话后缀
147
+ python3 bench/turns.py --min-steps 4 --json # 只算够长的轮,机器可读
148
+ ```
149
+
150
+ 2026-10-29 用它量出「轮内压缩后同一轮收到两条」,据此修了 1.5.28;读数与根因见 `BENCHMARK.md` 的「提醒投递的稳定性」一节。
151
+
139
152
  ## 正文占比与预注册门禁脚本
140
153
 
141
154
  ```bash
package/index.js CHANGED
@@ -97,7 +97,7 @@ const TAG = '[fast-mode]'
97
97
  * Shipped identity of this build. Traced into the debug log so a stale install is
98
98
  * visible at a glance, and pinned by a test against `package.json`.
99
99
  */
100
- export const VERSION = '1.5.26'
100
+ export const VERSION = '1.5.28'
101
101
  const GLOBAL_AGENT = '__global__'
102
102
 
103
103
  /**
@@ -326,7 +326,10 @@ export function apply(ctx, rawConfig) {
326
326
  }
327
327
  if (event && event.type === 'turn/start') {
328
328
  try {
329
- noteTurn(auto, agentKey(session))
329
+ // The turn number is what separates a real new turn from the harness
330
+ // re-entering a turn it compacted mid-flight: the latter must not reset
331
+ // the per-turn latches, or the model gets the same reminder twice.
332
+ noteTurn(auto, agentKey(session), event.data?.turn ?? event.turn)
330
333
  } catch {
331
334
  // A scheduler that can fail a turn is worse than no scheduler.
332
335
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-fast-mode",
3
- "version": "1.5.26",
3
+ "version": "1.5.28",
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/auto.js CHANGED
@@ -200,6 +200,8 @@ function entryOf(state, agent) {
200
200
  }
201
201
  entry = {
202
202
  steps: 0,
203
+ /** The harness turn number this entry's latches belong to; 0 = unknown. */
204
+ turn: 0,
203
205
  turnSteps: 0,
204
206
  noProgress: 0,
205
207
  calls: 0,
@@ -309,11 +311,25 @@ function entryOf(state, agent) {
309
311
  * session-level memory (what has already been tried) must survive, because that
310
312
  * is exactly what a stuck loop re-reads.
311
313
  *
314
+ * A turn that is *resumed* — mid-turn context compaction makes the harness
315
+ * re-enter the turn without ending it — must not get the slate: resetting here
316
+ * would clear `disciplineSent` and hand the model the same "prose is the cost"
317
+ * reminder a second time in the same turn (measured: 227 turns with a doubled
318
+ * reminder in retained transcripts, 214 of them with a compaction between the
319
+ * two). The turn number is the discriminator: same number as the entry's current
320
+ * turn means resume, larger means a real new turn. `0`/absent keeps the old
321
+ * behaviour, so an event stream that omits the number is never worse off.
322
+ *
312
323
  * @param {ReturnType<typeof createAutoState>} state
313
324
  * @param {string} agent
325
+ * @param {number} [turn] the harness turn number from `turn/start`
314
326
  */
315
- export function noteTurn(state, agent) {
327
+ export function noteTurn(state, agent, turn) {
316
328
  const entry = entryOf(state, agent)
329
+ if (typeof turn === 'number' && Number.isFinite(turn) && turn > 0) {
330
+ if (entry.turn === turn) return
331
+ entry.turn = turn
332
+ }
317
333
  entry.turnSteps = 0
318
334
  entry.stepCalls = 0
319
335
  entry.stepUnsafe = 0