dsh-fast-mode 1.5.31 → 1.5.33

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
@@ -2367,6 +2367,31 @@ python3 bench/ablate.py --ci 2000 --anchor def15 # 相对默认配置(
2367
2367
 
2368
2368
  **这张表的两个诚实边界**:不同臂覆盖的任务组合不同(跑一遍命令就能看到 `tasks` 列,`d2` 含 `codeimpl:8`、`map15` 只有 `codebase/debugfix/perfrefactor`),所以行与行之间不能直接排大小;以及每个块只采样一次,`n < 20` 的行只算方向提示。
2369
2369
 
2370
+ ## 直接跑一次「多个开关一起开」:三开 **0.669×**(22 对,18 对更快),两开 0.742×
2371
+
2372
+ 上一节那张消融表是**逐个**开关单独叠在默认上测的,从来没有真的跑过「一起开」的臂,所以「多开几个开关不会更快」只是推理。这一轮补上:把默认之上**唯一三个点估计小于 1** 的可选开关同时打开(`d2g` = 明细律第 2 步 + 第 1 步 ≥2 次调用才提前、`workspaceMap`、`openBatch`),和同块的默认臂按 `(任务, pair)` 配对。
2373
+
2374
+ ```
2375
+ python3 bench/suite-run.py --tasks debugfix,migrate --arms def15,combo2 --pairs 12 --tag COMBO2
2376
+ python3 bench/suite-run.py --tasks debugfix,migrate --arms def15,combo3 --pairs 6 --tag COMBO3
2377
+ ```
2378
+
2379
+ `combo2` = 默认 + `d2g` + `workspaceMap` + `openBatch`;`combo3` = 默认 + `workspaceMap` + `openBatch`(不含时点类臂,用来分清增益是不是全来自那条提醒时点)。
2380
+
2381
+ | 臂 | n | wall 几何比 | CI95(bootstrap 5 000) | 中位 | 更快 | 符号 p | 步数中位 | 输出中位 | 隐藏测试 |
2382
+ |---|---|---|---|---|---|---|---|---|---|
2383
+ | `combo2`(三开) | 22 | **0.669** | **[0.554, 0.799]** | 0.687 | **18/22** | **0.0022** | 7 → 6.5 | 6 770 → 4 957(−27 %) | 22/22 绿 |
2384
+ | `combo3`(两开,无时点臂) | 11 | 0.742 | [0.594, 0.909] | 0.884 | 8/11 | 0.113 | 8 → 7 | 6 223 → 6 497(+4 %) | 11/11 绿 |
2385
+
2386
+ 分族(`combo2`):`debugfix` 12 对 **0.766×**(10/12 更快)、`migrate` 10 对 **0.567×**(8/10 更快)。两族方向一致,没有 `d2` 那种「一开就反」的家族。
2387
+
2388
+ 读法,以及它和上一节的矛盾怎么算:
2389
+
2390
+ - **叠加是真的有效,不是推理出来的。** 单个开关各自只有 0.80–0.88 且 CI 跨 1(`map15` 23 对 0.802、`open15` 53 对 0.882、`d2g` 27 对 0.816),三个一起开 22 对 CI 上限 **0.799 < 1**、符号检验 p = 0.0022。单个都测不出显著,合起来能测出,说明增益不在任何一个开关本身,而在它们**减少的是同一类东西的不同份**:少开的文件、少来回的读、少写的散文——每一份单独都不足以顶穿 ±20 % 的单轮噪声。
2391
+ - **但别把它读成「把开关都打开就好」。** 表里 `bv15`、`pp16`、`bud8`、`wide15`、`nodisc` 单独叠都是 0.96–1.10,没有理由相信它们进组合会变好;这一轮只验了上面三个。
2392
+ - **时点臂仍在 HOLD,不进默认。** `combo2` 比 `combo3` 多出来的那一截(0.669 vs 0.742)指向 `d2g`,但两个臂的样本不是同一批 pair,n = 22 vs 11 也不足以给「多 7 个百分点」定论;`d2g` 在 `codeimpl` 上的负收益(见上一节结论 2)没有被这一轮推翻,默认仍是 `disciplineAfterSteps: 4`。
2393
+ - **诚实边界**:各一个块、两个任务族(`debugfix` 短、`migrate` 长),`combo3` 的符号检验 p = 0.113 只算方向提示;锚点臂有 2 格(`migrate` pair 2、11)判 `leak` 被剔除,剔除后仍配对 22 对,`combo2` 24 格全绿、无一格 `leak`。想复现就重跑上面两条命令,`bench/arms/combo2.yml`、`combo3.yml` 在仓库里。
2394
+
2370
2395
  ## 同一配置再跑一遍会差多少:块级差 4 %,任务级差 15–31 %(REP2,10 对)
2371
2396
 
2372
2397
  外部评审提的第一条是稳定性。这一块不新增任何开关,只把 `migrate`+`debugfix` 的同一批格子**用同一套配置再跑一遍**(`REP2`,20 次运行,顺序 ABBA 与之前的块一致),看同配置的两次独立测量互相差多少:
package/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.5.33(把「多个开关一起开」里那个真正省时的开关做进面板:第五行「一次读多个文件」)
4
+
5
+ - **面板多了第五行,`openBatch` 第一次能被用户点到。** 1.5.32 实测的 `combo3`(默认 + `workspaceMap` + `openBatch`,11 对 0.742×)里有一半来自 `openBatch`,但它之前只在配置里,面板够不着——等于「测出来了但用户开不了」。这一版把它做成和「先列文件清单」同级的逐会话开关:写 `sessions` 列里本会话条目的 `batch`,宿主侧映射成 `openBatch`(`src/state.js` 的 `configOverridesFor`),不动共享默认值,只影响本会话。
6
+ - **默认仍然关。** 单独开 `openBatch` 测不到显著(0.882×,CI 跨 1),只有和清单一起开才有效;面板 tooltip 写的就是这句后果,不写成「更快」。默认值、`d2g` 的 HOLD 状态都不变。
7
+ - **两侧字段表对齐。** `client.js` 的 `sanitiseChoices` 与 `src/state.js` 的 `sanitise` 同步接受 `batch`,避免出现「客户端写进去、宿主读出来丢掉」的假开关(这正是 1.5.17 那批死按钮的成因)。测试补了三处:状态层的 `batch → openBatch` 映射、面板行的默认值/逐会话写入、以及「共享 `openBatch` 为真但本会话条目为假」时面板按条目画。
8
+ - 动 `client.js`、`src/state.js`、`src/config.js`(描述文案)、两份 README、`docs/REFERENCE.md`;测试 306 → 308 全绿。
9
+
10
+ ## 1.5.32(先回答「多个开关一起开」:三开 **0.669×**、22 对里 18 对更快、隐藏测试 22/22 绿;附带一个内存上限修复)
11
+
12
+ - **新增两个组合臂并真的跑了一遍**(此前只有逐个开关的消融,从没跑过「一起开」):`combo2` = 默认 + `d2g` + `workspaceMap` + `openBatch`,`combo3` = 默认 + `workspaceMap` + `openBatch`。同块内与默认臂按 `(任务, pair)` 配对:
13
+ - `combo2`:22 对,wall 几何 **0.669×**、CI95 **[0.554, 0.799]**、中位 0.687、**18/22 更快**、符号检验 **p = 0.0022**,输出中位 −27 %,步数中位 7 → 6.5,隐藏测试 22/22 绿;
14
+ - `combo3`(去掉时点臂):11 对,几何 0.742×、CI95 [0.594, 0.909]、8/11 更快(p = 0.113,方向提示)。
15
+ - 单开时三个开关各自都测不到显著(0.802 / 0.882 / 0.816,CI 全跨 1),合起来才顶穿噪声——**叠加是真的**,但只验了这三个,其余开关(`batchVerify` / `preferPatch` / 步数预算 / 更宽并行 / 关明细律)单独叠都是 0.96–1.10,没有理由跟着一起开。
16
+ - **默认不变**:`combo2` 比 `combo3` 多出的那一截指向时点臂 `d2g`,但两臂不是同一批 pair、n = 22 vs 11,不足以给「多 7 个百分点」定论;`d2g` 在 `codeimpl` 上的负收益没被推翻,`disciplineAfterSteps` 仍是 4。证据与读法写进 `BENCHMARK.md` 对应新章节。
17
+ - **稳定性修复:`autoBudgets` 加了上限。** 它按会话存自适应超时上限,原来只写不删——长驻宿主进程里每个会话留一格,永不回收。现在与 `compacted` 一样保留最近 200 条(超出丢最旧的);只有「当前会话下一步」会读它,丢弃旧会话不影响行为。这一条是给长时间运行的宿主用的,不改任何默认值。
18
+ - 只动 `index.js`(8 行)与文档;`src/` 无改动。
19
+
3
20
  ## 1.5.31(`turns.py` 顺手审另一条提醒:压缩后重述规则的那条,会不会无理由地多出来)
4
21
 
5
22
  - 插件只主动发两条提醒:入场步的「少写散文」(每轮恰好一次)和压缩后的「规则重申」(每次压缩后一次,因为规则可能被裁掉)。`bench/turns.py` 原来只审第一条,第二条只当噪音忽略——如果它的标志位泄漏,就会在每个合格轮里白送一段规则文本。
package/README.en.md CHANGED
@@ -103,13 +103,16 @@ Legacy `fast` / `turbo` / `max` / `ludicrous` values are read as `auto` with a d
103
103
 
104
104
  ## The switch
105
105
 
106
- The bolt is the switch: one click writes on/off, per conversation, restored on reload. Right-click it for three shared settings:
106
+ The bolt is the switch: one click writes on/off, per conversation, restored on reload. Right-click it for four per-conversation settings:
107
107
 
108
108
  - **Less narration** — master switch for the step-4 reminder; with it off, long tasks take about 2.7x longer and write about twice as much. On by default.
109
109
  - **Step cap** — `15` / `30` / off, off by default. At the cap it reports instead of truncating the task.
110
110
  - **Workspace inventory** — before the turn's first tool call, one inventory notice (relative paths + byte sizes + visible test entry points) so the turn does not open with `ls`/`find`. Off by default; over 23 pairs it measured 0.927x geometric with a slower tail and 10% more steps, so it stays a switch to try rather than a default.
111
+ - **Read several files at once** — batches independent reads into one call. Off by default; the measured gain shows up when it is stacked with the inventory switch (below), not alone.
111
112
 
112
- All three apply from the next request. No dropdown, no multiplier estimate.
113
+ All four apply from the next request, per conversation. No dropdown, no multiplier estimate.
114
+
115
+ **Turn several on at once?** Yes, but only the measured pair. Defaults plus inventory plus batched reads is **0.742x** over 11 pairs; adding the step-2 reminder timing (not a panel row, still on HOLD) reaches **0.669x** over 22 pairs, CI95 [0.554, 0.799], 18/22 faster, hidden tests green. Each switch alone lands at 0.80–0.88 with a confidence interval that crosses 1 — the gain comes from each one cutting a different share of the same cost, not from any single switch. The other knobs (`batchVerify`, `preferPatch`, step budget, wider fan-out) all land at 0.96–1.10; do not stack them.
113
116
 
114
117
  The idea draws on `dsh-batch-tool-calls` and `dsh-ops` (batched calls, bounded execution), but their widths are static while this width comes from the conversation's own call evidence; do not install all three. Context-compaction plugins coexist.
115
118
 
package/README.md CHANGED
@@ -103,14 +103,17 @@
103
103
 
104
104
  ## 开关
105
105
 
106
- 闪电就是开关:开/关即写,按会话独立,重启按会话恢复。右键闪电是一块四行的面板,四行都只影响当前会话:
106
+ 闪电就是开关:开/关即写,按会话独立,重启按会话恢复。右键闪电是一块五行的面板,五行都只影响当前会话:
107
107
 
108
108
  - **更快**:这个会话开不开快模式,和点闪电是同一件事。
109
109
  - **少写过程**:第 4 步那句「省着写」提醒的总开关,关掉后输出会变长、耗时变慢(长任务约 2.7×),默认开。
110
110
  - **最多几步**:`15` / `30` / 不限,默认不限。到上限只先汇报,不截断任务。
111
111
  - **先列文件清单**:开启后,回合的第一次工具调用之前先收到一份工作目录清单(相对路径 + 字节数 + 可见的测试入口),省掉开头的 `ls`/`find` 探路。默认关;23 个配对实测几何 0.927×,尾部变慢、步数 +10%,没有达到进默认的门槛,只作为可试的开关。
112
+ - **一次读多个文件**:把互不依赖的读取合成一批发出,少几个来回。默认关;单独开测不到显著,和上一项一起开才有(见下)。
112
113
 
113
- 四项都写在本会话的条目里,对下一个回合生效;另一个会话开不开、写成什么样,互不影响。没有下拉,没有倍数估算。
114
+ 五项都写在本会话的条目里,对下一个回合生效;另一个会话开不开、写成什么样,互不影响。没有下拉,没有倍数估算。
115
+
116
+ **几个一起开会不会更快?** 会,但只有实测过的那几个。默认 + `workspaceMap` + `openBatch` 两开:11 对 **0.742×**;再把 `disciplineAfterSteps` 提前到 2(面板里没有这一项,属于 HOLD 的时点实验)三开:22 对 **0.669×**、CI95 [0.554, 0.799]、18/22 更快、隐藏测试全绿。单独开其中任何一个都测不到显著(0.80–0.88,置信区间全跨 1)——所以增益来自它们各自省掉的是同一类成本的不同份,不是哪个开关本身。其余开关(`batchVerify`、`preferPatch`、步数预算、更宽并行)叠上去都是 0.96–1.10,没有理由跟着一起开。细节见 [BENCHMARK.md](BENCHMARK.md) 的「直接跑一次多个开关一起开」。
114
117
 
115
118
  思路参考了 `dsh-batch-tool-calls` 与 `dsh-ops`(批量调用、有界执行),但它们的规则是静态的,这里的宽度来自会话自身的调用证据;不需要同时装。压缩类插件可以共存。
116
119
 
package/client.js CHANGED
@@ -102,6 +102,8 @@ window.__ModuleLoader__.load({
102
102
  'panel.budgetTitle': '到上限先汇报,不截断任务',
103
103
  'panel.map': '先列文件清单',
104
104
  'panel.mapTitle': '开局把目录里的文件交给模型,省掉先翻一遍目录',
105
+ 'panel.batch': '一次读多个文件',
106
+ 'panel.batchTitle': '把互不依赖的读取合成一批发出,少几个来回',
105
107
  }
106
108
 
107
109
  const en = {
@@ -125,6 +127,8 @@ window.__ModuleLoader__.load({
125
127
  'panel.budgetTitle': 'Reports at the cap; never truncates the task',
126
128
  'panel.map': 'File list first',
127
129
  'panel.mapTitle': 'Hands the directory listing to the model up front, skipping a lookup',
130
+ 'panel.batch': 'Read several files at once',
131
+ 'panel.batchTitle': 'Batches independent reads into one call, fewer round trips',
128
132
  }
129
133
 
130
134
  const CSS = [
@@ -186,6 +190,8 @@ window.__ModuleLoader__.load({
186
190
  discipline: true,
187
191
  /** The workspace inventory switch for this conversation, off by default. */
188
192
  inventory: false,
193
+ /** Batches independent reads into one call for this conversation. */
194
+ batch: false,
189
195
  /** This conversation's step cap; 0 means no cap. */
190
196
  stepBudget: 0,
191
197
  /** The right-click panel is open. Store-backed, like every other paint. */
@@ -314,6 +320,7 @@ window.__ModuleLoader__.load({
314
320
  if (typeof entry.discipline === 'boolean' || entry.discipline === 'late') clean.discipline = entry.discipline
315
321
  if (Number.isFinite(entry.stepBudget) && entry.stepBudget >= 0) clean.stepBudget = Math.floor(Number(entry.stepBudget))
316
322
  if (typeof entry.map === 'boolean') clean.map = entry.map
323
+ if (typeof entry.batch === 'boolean') clean.batch = entry.batch
317
324
  if (Number.isFinite(entry.ts)) clean.ts = Number(entry.ts)
318
325
  if (
319
326
  clean.enabled === undefined
@@ -321,6 +328,7 @@ window.__ModuleLoader__.load({
321
328
  && clean.discipline === undefined
322
329
  && clean.stepBudget === undefined
323
330
  && clean.map === undefined
331
+ && clean.batch === undefined
324
332
  ) continue
325
333
  out[sessionId] = clean
326
334
  }
@@ -369,7 +377,7 @@ window.__ModuleLoader__.load({
369
377
  *
370
378
  * @param {Record<string, any>} map
371
379
  * @param {string|undefined} sessionId
372
- * @param {{enabled?: boolean, gear?: string, discipline?: boolean|'late', stepBudget?: number, map?: boolean}} patch
380
+ * @param {{enabled?: boolean, gear?: string, discipline?: boolean|'late', stepBudget?: number, map?: boolean, batch?: boolean}} patch
373
381
  */
374
382
  function withOwn(map, sessionId, patch) {
375
383
  const next = { ...map }
@@ -382,6 +390,7 @@ window.__ModuleLoader__.load({
382
390
  if (typeof patch.discipline === 'boolean' || patch.discipline === 'late') entry.discipline = patch.discipline
383
391
  if (Number.isFinite(patch.stepBudget) && patch.stepBudget >= 0) entry.stepBudget = Math.floor(Number(patch.stepBudget))
384
392
  if (typeof patch.map === 'boolean') entry.map = patch.map
393
+ if (typeof patch.batch === 'boolean') entry.batch = patch.batch
385
394
  next[sessionId] = entry
386
395
  return next
387
396
  }
@@ -525,6 +534,9 @@ window.__ModuleLoader__.load({
525
534
  const seenMap = own !== undefined && typeof own.map === 'boolean'
526
535
  ? own.map
527
536
  : value.workspaceMap === true
537
+ const seenBatch = own !== undefined && typeof own.batch === 'boolean'
538
+ ? own.batch
539
+ : value.openBatch === true
528
540
  store.phase = 'ready'
529
541
  store.error = ''
530
542
  store.ns = view.ns
@@ -534,6 +546,7 @@ window.__ModuleLoader__.load({
534
546
  store.discipline = seenDiscipline
535
547
  store.stepBudget = seenBudget
536
548
  store.inventory = seenMap
549
+ store.batch = seenBatch
537
550
  // A write we have not seen echoed yet wins: the Loader applies the new
538
551
  // section asynchronously, and flipping back to the old paint in that
539
552
  // window would read as a failed click.
@@ -587,7 +600,7 @@ window.__ModuleLoader__.load({
587
600
  *
588
601
  * @param {{settings: {update: (ns: string, patch: object, revision?: number) => Promise<any>}}} remote
589
602
  * @param {{enabled?: boolean, gear?: string, outputDiscipline?: boolean|'late', stepBudget?: number, workspaceMap?: boolean}} [patch]
590
- * @param {{entry?: {enabled?: boolean, gear?: string, discipline?: boolean|'late', stepBudget?: number, map?: boolean}}} [options]
603
+ * @param {{entry?: {enabled?: boolean, gear?: string, discipline?: boolean|'late', stepBudget?: number, map?: boolean, batch?: boolean}}} [options]
591
604
  * @returns {Promise<boolean>} whether the write was accepted
592
605
  */
593
606
  async function write(remote, patch, options = {}) {
@@ -610,6 +623,7 @@ window.__ModuleLoader__.load({
610
623
  if (typeof entry.discipline === 'boolean' || entry.discipline === 'late') store.discipline = entry.discipline !== false
611
624
  if (typeof entry.stepBudget === 'number') store.stepBudget = entry.stepBudget
612
625
  if (typeof entry.map === 'boolean') store.inventory = entry.map
626
+ if (typeof entry.batch === 'boolean') store.batch = entry.batch
613
627
  }
614
628
  if (typeof shared.enabled === 'boolean') {
615
629
  store.enabled = shared.enabled
@@ -620,6 +634,7 @@ window.__ModuleLoader__.load({
620
634
  else if (shared.outputDiscipline !== undefined) store.discipline = true
621
635
  if (typeof shared.stepBudget === 'number') store.stepBudget = shared.stepBudget
622
636
  if (typeof shared.workspaceMap === 'boolean') store.inventory = shared.workspaceMap
637
+ if (typeof shared.openBatch === 'boolean') store.batch = shared.openBatch
623
638
  emit()
624
639
  try {
625
640
  unwrap(await withTimeout(remote.settings.update(store.ns, outgoing, store.revision), 15000, 'settings/update'))
@@ -635,6 +650,7 @@ window.__ModuleLoader__.load({
635
650
  store.discipline = before.discipline
636
651
  store.stepBudget = before.stepBudget
637
652
  store.inventory = before.inventory
653
+ store.batch = before.batch
638
654
  store.pending = undefined
639
655
  store.writeError = describeError(error)
640
656
  emit()
@@ -843,6 +859,13 @@ window.__ModuleLoader__.load({
843
859
  emit()
844
860
  }
845
861
 
862
+ /** Same per-conversation tier as the file list, one call for many reads. */
863
+ const toggleBatch = () => {
864
+ void write(remote, undefined, { entry: { batch: !store.batch } })
865
+ store.panelOpen = false
866
+ emit()
867
+ }
868
+
846
869
  return react.createElement(
847
870
  'div',
848
871
  { className: 'fm-panel', role: 'menu', 'aria-label': t('panel.title') },
@@ -866,6 +889,15 @@ window.__ModuleLoader__.load({
866
889
  store.inventory,
867
890
  t('panel.mapTitle'),
868
891
  ),
892
+ row(
893
+ 'menuitemcheckbox',
894
+ t('panel.batch'),
895
+ t(store.batch ? 'panel.on' : 'panel.off'),
896
+ canWrite,
897
+ toggleBatch,
898
+ store.batch,
899
+ t('panel.batchTitle'),
900
+ ),
869
901
  react.createElement('div', { className: 'fm-panel__scope' }, t('panel.scope')),
870
902
  )
871
903
  }
package/docs/REFERENCE.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # 参考手册:原理、配置与边界
2
2
 
3
- 本手册描述 **dsh-fast-mode 1.5.17** 的实际行为(默认配置下的注入行为与 1.5.0 一致;1.5.14 起多了右键面板,1.5.17 面板为四行、四行都只影响本会话),对应仓库里的 `index.js`、`src/` 与 `client.js`。它不是宣传页:凡是能测量的地方给出数字,凡是没测量的地方直接说没测量。
3
+ 本手册描述 **dsh-fast-mode 1.5.33** 的实际行为(默认配置下的注入行为与 1.5.0 一致;1.5.14 起多了右键面板,1.5.33 面板为五行、五行都只影响本会话),对应仓库里的 `index.js`、`src/` 与 `client.js`。它不是宣传页:凡是能测量的地方给出数字,凡是没测量的地方直接说没测量。
4
4
 
5
5
  **唯一的契约:Fast 模式从不改变思考强度、thinking 预算或模型。** `agent/request` 钩子只做一件事——`await next()`,读一下请求携带的 `reasoningEffort` 记进测量,然后原样返回。用户选的是哪一档(会话里的 UI 选择,或者 profile 里 agent-default-model 固定下来的那一档),每个请求就用哪一档。
6
6
 
@@ -9,7 +9,7 @@
9
9
  ## 交互细节:闪电开关与会话归属
10
10
 
11
11
  - **一个图标就是开关。** 会话输入区的活动槽(`conversation.input.activity`)里只有一个闪电按钮:关闭时空心,开启时实心。点一下切换,再点一下切回;这一下只写当前会话。没有档位选择器,没有箭头,没有"选择 gear"的二级菜单。
12
- - **右键是面板。** 右键闪电打开四行,四行都只影响当前会话:"更快"(`enabled`)、"少写过程"(`outputDiscipline`)、"最多几步"(`stepBudget`,循环 0/15/30)、"先列文件清单"(`workspaceMap`)。四行都把选择写进 `sessions` 列里本会话的条目,不碰共享默认值,下一个回合生效;只读或写入中时整块禁用,写失败回滚。没有条目的会话沿用配置里的默认值(`enabled`/`gear`/`outputDiscipline`/`stepBudget`/`workspaceMap`)。面板不暴露 `disciplineAfterSteps`、`disciplineEarlyCalls`、`shapeGate`、`finishLineSteps`、`shellParallel`、`disciplineRepeatAtSteps` 这些未证实或已判 HOLD 的旋钮。
12
+ - **右键是面板。** 右键闪电打开五行,五行都只影响当前会话:"更快"(`enabled`)、"少写过程"(`outputDiscipline`)、"最多几步"(`stepBudget`,循环 0/15/30)、"先列文件清单"(`workspaceMap`)、"一次读多个文件"(`openBatch`)。五行都把选择写进 `sessions` 列里本会话的条目,不碰共享默认值,下一个回合生效;只读或写入中时整块禁用,写失败回滚。没有条目的会话沿用配置里的默认值(`enabled`/`gear`/`outputDiscipline`/`stepBudget`/`workspaceMap`/`openBatch`)。面板不暴露 `disciplineAfterSteps`、`disciplineEarlyCalls`、`shapeGate`、`finishLineSteps`、`shellParallel`、`disciplineRepeatAtSteps` 这些未证实或已判 HOLD 的旋钮。
13
13
  - **会话之间互不影响,这是硬约束。** 每个会话的 `enabled`、`gear`、`outputDiscipline`、`stepBudget`、`workspaceMap` 都从 `sessions` 列里自己那一条解析:A 会话 `更快 + 少写过程`、B 会话 `更快 + 不写过程` 同时成立,C 会话没点过就沿用配置默认值。面板和闪电只写本会话的条目,从不写共享默认值;条目最多保留 200 个,按 `ts` 淘汰最旧的,被淘汰的会话回到默认值。
14
14
  - **按钮只表达一个问题:这个会话开不开。** `aria-pressed` 就是状态,tooltip 写清"仅本会话 · 思考强度不变 · 点击关闭"。为什么快、快在哪里、单次调用上限这类说明放在插件介绍(`meta.description` 与配置项描述)里,不再占用输入区。
15
15
  - **实时行引用的是被执行的数字**,而不是产品形容词:
@@ -165,7 +165,7 @@
165
165
  | `parallel` | 由档位推导,`auto` 下为 4 | 并行上限的初始值,1–8。`auto` 只把它当起点;设为 1 表示不注入批量提示段。 |
166
166
  | `sessions` | `''` | 插件自动维护的会话级覆盖,见上一节。**不要手改。** |
167
167
  | `outputDiscipline` | `'late'` | 输出纪律。`late`(默认)前 4 步不写任何输出约束,第 4 步起由调度器提醒一次;`true` 全程生效;`false` 关闭输出纪律,仍注入并行与有界调用规则。 |
168
- | `openBatch` | `false` | 开局批量:每轮第一步发一次「把已知要做的独立调用放进同一块、shell 命令用 `&&` 串起来」。短任务的钱几乎全是每步固定成本(首字延迟 ~0.5 s/次),少一次往返就省一次;只影响第一轮第一步,长任务可忽略。旧批次里它不优于默认,但那些数字已因测量缺陷撤回(运行目录在仓库内、指标按字节偏移切片),尚无干净重测。保持在 `false`,只作实验旋钮。 |
168
+ | `openBatch` | `false` | 开局批量:每轮第一步发一次「把已知要做的独立调用放进同一块、shell 命令用 `&&` 串起来」。短任务的钱几乎全是每步固定成本(首字延迟 ~0.5 s/次),少一次往返就省一次;只影响第一轮第一步,长任务可忽略。旧批次里它不优于默认,但那些数字已因测量缺陷撤回(运行目录在仓库内、指标按字节偏移切片),尚无干净重测。保持在 `false`,但 1.5.33 起进面板(「一次读多个文件」),逐会话可开;和 `workspaceMap` 一起开 11 对 0.742×,再加 `disciplineAfterSteps: 2` 三开 22 对 0.669×〔CI 0.554–0.799〕,单独开仍测不到显著。 |
169
169
  | `shapeGate` | `false` | 形状闸门:本轮累计调用 ≥ 4 且其中写入类(不在只读名单里的工具)占比 ≥ 60 % 时,**整轮不再注入任何提醒**,行为退回原生。判断只用已有的工具名,不发请求、不加请求体字段。它给的是「不适合就别管我」的退路:写密集型的任务没有可并行的调用,每条提醒都只是一次它用不上的往返。显式设了 `stepBudget` 时该收尾线优先于闸门——「别加速」不等于「无视我叫停」。默认关,因为它会同时关掉本可以救回长尾的收尾提醒。 |
170
170
  | `preferPatch` | `false` | 整文件写入之后提醒一次「改已有文件时只发改动的行(edit hunk),不要重发整个文件」。本步 `write` 的正文 ≥ 2000 字符才武装,之后一步发一次,每轮最多一次;文本明确要求结果与原文件逐字节相同,不限制写什么、也不影响何时结束。默认关闭:它只作用于模型的输出形式(工具调用参数),正确性由文件内容决定、与发送形式无关;实测是否真省墙钟见 `BENCHMARK.md` 的 `pp16` 臂。 |
171
171
  | `compactRereadHint` | false | 压缩提醒里多一句「本回合写过的文件不要为确认整份重读」;不新增消息。实验旋钮,默认关 |
@@ -369,7 +369,7 @@ dsh-fast-mode-experiment stop # 结束实验
369
369
  - `src/metrics.js`:JSONL 计量与汇总;
370
370
  - `src/benchmark.js`:按会话哈希分配;
371
371
  - `src/peers.js`:冲突插件探测;
372
- - `client.js`:闪电开关、右键面板(四行,按会话写入 `sessions` 列)。
372
+ - `client.js`:闪电开关、右键面板(五行,按会话写入 `sessions` 列)。
373
373
 
374
374
  跑测试:
375
375
 
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.31'
100
+ export const VERSION = '1.5.33'
101
101
  const GLOBAL_AGENT = '__global__'
102
102
 
103
103
  /**
@@ -383,7 +383,12 @@ export function apply(ctx, rawConfig) {
383
383
  // The scheduler's memory, one entry per conversation: what has been tried, how
384
384
  // long calls have been taking, and how many steps went by without a new result.
385
385
  const auto = createAutoState()
386
- /** Live stall ceilings per conversation, as computed by the scheduler. */
386
+ /**
387
+ * Live stall ceilings per conversation, as computed by the scheduler.
388
+ * Bounded like `compacted`: a long-lived host process sees thousands of
389
+ * sessions and only the next step of the current one reads a ceiling, so the
390
+ * oldest entries are dropped instead of kept forever.
391
+ */
387
392
  const autoBudgets = new Map()
388
393
  /** @type {Map<string, number>} */
389
394
  const startedAt = new Map()
@@ -530,6 +535,7 @@ export function apply(ctx, rawConfig) {
530
535
  noteStep(auto, sid)
531
536
  notePolicy(auto, sid, cfg)
532
537
  const schedule = autoSchedule(auto, sid)
538
+ if (autoBudgets.size >= 200) autoBudgets.delete(autoBudgets.keys().next().value)
533
539
  autoBudgets.set(sid, schedule.stallBudgetMs)
534
540
  if (schedule.directive) {
535
541
  notices.push(createNoticeMessage(schedule.directive.text, schedule.directive.summary))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-fast-mode",
3
- "version": "1.5.31",
3
+ "version": "1.5.33",
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
@@ -157,7 +157,7 @@ export const Config = z.object({
157
157
  .boolean()
158
158
  .default(false)
159
159
  .description(
160
- '开局批量 / Opening batch: 每轮第一步提醒一次「把已知要做的独立调用放进同一块、shell 命令用 && 串起来」。短任务的钱几乎全花在每步固定成本(首字延迟 ~0.5 s/次)上,少一次往返就省一次;只发一次,不改后续调度。',
160
+ '开局批量 / Opening batch(面板名「一次读多个文件」): 每轮第一步提醒一次「把已知要做的独立调用放进同一块、shell 命令用 && 串起来」。短任务的钱几乎全花在每步固定成本(首字延迟 ~0.5 s/次)上,少一次往返就省一次;只发一次,不改后续调度。面板可逐会话开;和 `workspaceMap` 一起开时实测 0.742×(11 对),单独开测不到显著,所以默认仍是 false。',
161
161
  ),
162
162
 
163
163
  preferPatch: z
package/src/state.js CHANGED
@@ -106,6 +106,7 @@ function sanitise(input) {
106
106
  if (typeof entry.discipline === 'boolean' || entry.discipline === 'late') clean.discipline = entry.discipline
107
107
  if (Number.isFinite(entry.stepBudget) && entry.stepBudget >= 0) clean.stepBudget = Math.floor(Number(entry.stepBudget))
108
108
  if (typeof entry.map === 'boolean') clean.map = entry.map
109
+ if (typeof entry.batch === 'boolean') clean.batch = entry.batch
109
110
  if (Number.isFinite(entry.ts)) clean.ts = Number(entry.ts)
110
111
  if (
111
112
  clean.enabled === undefined
@@ -113,6 +114,7 @@ function sanitise(input) {
113
114
  && clean.discipline === undefined
114
115
  && clean.stepBudget === undefined
115
116
  && clean.map === undefined
117
+ && clean.batch === undefined
116
118
  ) continue
117
119
  out[sessionId] = clean
118
120
  }
@@ -137,6 +139,7 @@ export function choiceFor(raw, sessionId) {
137
139
  if (entry.discipline !== undefined) choice.discipline = entry.discipline
138
140
  if (entry.stepBudget !== undefined) choice.stepBudget = entry.stepBudget
139
141
  if (entry.map !== undefined) choice.map = entry.map
142
+ if (entry.batch !== undefined) choice.batch = entry.batch
140
143
  return choice
141
144
  }
142
145
 
@@ -161,6 +164,7 @@ export function configOverridesFor(raw, sessionId) {
161
164
  if (choice.discipline !== undefined) overrides.outputDiscipline = choice.discipline
162
165
  if (choice.stepBudget !== undefined) overrides.stepBudget = choice.stepBudget
163
166
  if (choice.map !== undefined) overrides.workspaceMap = choice.map
167
+ if (choice.batch !== undefined) overrides.openBatch = choice.batch
164
168
  return Object.keys(overrides).length === 0 ? undefined : overrides
165
169
  }
166
170
 
@@ -215,7 +219,7 @@ export function writeChoice(raw, sessionId, choice, now) {
215
219
  return { sessions: encodeChoices(map), map }
216
220
  }
217
221
 
218
- /** @typedef {{enabled?: boolean, gear?: string, discipline?: boolean|'late', stepBudget?: number, map?: boolean, ts?: number}} SessionEntry */
222
+ /** @typedef {{enabled?: boolean, gear?: string, discipline?: boolean|'late', stepBudget?: number, map?: boolean, batch?: boolean, ts?: number}} SessionEntry */
219
223
 
220
224
  /**
221
225
  * Serialise the map, dropping the oldest entries beyond {@link SESSIONS_LIMIT}.