# Changelog

## 1.7.28（三档改成无状态预设：按一下载入开关，不再留一个会撒谎的档位）

两处报障：点「标准」和点「激进」，下面五行一模一样；「激进」按过之后手动把开关关掉，上面仍写着
激进。根因是档位被当成一个存下来的标签——点一下只写 `gear`，从来不碰下面五行；高亮又直接从存下来
的标签反推，于是标签和行内容各说各话。

现在三档是无状态预设，按一下就把该档的开关值一次性写进本会话条目，面板不再记录「当前是哪档」：

- 保守：精简输出、开局附文件清单、批量读文件、截短工具输出全关（`directive: false` 本来也会全关，写进去只是让状态一致）；
- 标准：只开精简输出（`late`），其余三关；
- 激进：精简输出 + 开局附文件清单 + 批量读文件 + 截短工具输出。

步数上限不归任何档管：它是用户自己的护栏，预设不增不减。高亮改成拿当前开关值反推——完全匹配
哪一档就亮哪一档，手动改过任何一个开关就不再亮任何一档（组里仍留一个 tab 停靠点）；再点一次已
匹配的档不写入。宽度（`parallel`/`level`）仍随 `gear` 进条目。工具提示同步改成「点一下载入这一档
的开关……手动改过就不再属于任何一档」。

测试改写：点激进一次写五个键、五行随之变开；手动关掉「截短工具输出」后无档高亮；已匹配的档再点
不写；旧 `fast` 别名要配合激进档的开关值才高亮，单有 `gear` 不算。测试 425 通过。

## 1.7.27（保守档下四行置灰：面板不再给写进去却没效果的开关）

标题行那三档原本的提示是「只调并行宽度，机制不变」——这句话是错的。`safe` 声明了
`directive: false`，`resolveConfig` 因此把并行压到 1、提示段整段消失，并把输出纪律、
批量读文件、截短工具输出、步数上限全部关掉。结果是：选了「保守」，下面四行仍显示为开、
仍可点，点完写进本会话的 `sessions` 条目却毫无效果——就是死开关。

现在保守档下这四行置灰、显示为关，步数上限显示「不限」，点击不会写入任何值；切回标准
或激进立刻恢复可点。提示改成「保守关掉整套机制，下面四行随之失效；标准与激进只差并行宽度」。
文档里同一句错话在 `docs/REFERENCE.md` 与 `docs/REFERENCE.en.md` 的 `gear` 行一并改正。

新增两条测试：保守档下四行 `disabled` 且读作关、文件清单行不受影响（它不被 gate），
以及切回激进后全部恢复可点。测试 422 通过。运行时注入行为不变。

## 1.7.26（面板只留量过的开关：没验证的三行收回，配置键保留）

右键面板从八行降到五行，收回「不确定先查证」（`probeFirst`）、「先执行后解释」
（`actBeforePlan`）、「重复时换方法」（`repeatNudge`）。这三个都只是往提示里加一句话，
各自的单独增益从没测出能压过测量噪声（同一配置单跑之间就差三成），摆在面板上等于暗示
「打开就更快」。留着的五行都有依据：精简输出 0.781×/0.787×、截短工具输出 0.756×（30 对）
和 0.805×（24 对）、文件清单+批量读文件 0.742×（11 对，小样本）、步数上限是护栏不是加速。

三个配置键照旧存在，老会话 `sessions` 里已经写过的值继续生效，只是前端不再给新开关；
默认值仍全关。测试从「八行冻结」改成「五行冻结」。运行时注入行为不变。

## 1.7.25（同池第三次独立跑：单跑之间差三成，72 对合池才勉强有个方向）

同一套 8 条官方 SWE-bench Lite 实例、同一配置（`compactResults=true`），第三次独立跑 24 对，
三次合池 **72 对**：墙钟 **0.933×** [0.785, 1.107]、token **0.903×** [0.815, 1.003]，墙钟跨 1，
token 上界贴到 1.003。三次单跑各自 1.109× / 0.914× / 0.800×——**同一配置、同一池子，单跑之间
就能差三成**，所以以后单跑读数一律不发结论。三轮 off 臂 2 对、on 臂 5 对校验没过，两侧都会挂，
不是插件把题做坏。只为文档，运行时行为不变。

## 1.7.24（把 SWE-bench 池子从 6 条补到 8 条，48 对读数还是贴在 1.0）

补 `sympy-24909`、`pytest-8906` 两条过门实例，8×3 跑两次、合池 48 对：墙钟 **1.007×**
[0.808, 1.255]、token 0.905× [0.789, 1.040]，都跨 1。扩池没有推翻也没有翻案「这类任务上
量不出加速」。只为文档，运行时行为不变。

## 1.7.23（往截短上再叠开关：每叠一个就更差，叠到第三个方向都反了）

同一套 30 对自造任务，都在 `compactResults=true` 上叠加：只开截短 0.756× / 0.786×；
`+ gear=fast` 0.937× / 0.973×；`+ workspaceMap` 0.862× / 0.827×；`+ gear=fast + workspaceMap`
0.966× / 1.138×（token 方向变差，均跨 1）。结论：**想省时间只开截短一个开关，多开不叠加收益**。

## 1.7.22（每类命中保留几行：4 / 12 / 30 都扫了，默认 12 不动）

30 对自造任务，只改 `compactKeepMatches`：4 → 墙钟 0.785× [0.626, 0.973]、token 0.820×（跨 1）；
12（默认）→ 0.756× / 0.786×；30 → 0.875× / 0.950×（token 跨 1）。方向符合直觉——保留越多省得越少，
但 4 与 12 的差在噪声里，**没理由改默认值**。运行时行为不变。

## 1.7.21（截多少字也量了：4000 和 8000 一样，2000 开始费 token）

同一套 30 对自造任务，只改 `compactMaxChars`：2000（两轮合池 60 对）墙钟 0.855×、token 0.892×
（跨 1）；4000 → 0.785× [0.677, 0.915]、0.782× [0.646, 0.953]；8000（默认）→ 0.756× / 0.786×。
4000 与 8000 差 3 个百分点、区间大幅重叠，2000 砍太狠反而要重跑命令拿回被截掉的细节。
**默认保持 8000**，没有理由往下调。运行时行为不变。

## 1.7.20（真 PR 24 对再验一遍：加速还是截短开关一个人的）

六个真实 PR 任务 × 4 轮 = 24 对，同一套 A/B：`compactResults=true` 单独开 → 墙钟 0.805×
[0.656, 0.985]、token 0.768× [0.618, 0.960]，都量到了（第二次独立复现）；`gear=fast` 单独开 →
0.918× [0.766, 1.095]、0.861× [0.711, 1.040]，都跨 1。加上自造任务那组（0.756× / 0.867×），
结论固定：**两次独立测量里只有截短开关稳定有效，`gear=fast` 两次都没量到**。要开就只开它。

## 1.7.19（开关矩阵：单独开 / 一起开 / 什么都不开，一张表看完）

`docs/AUDIT.md` 新增「开关矩阵」：同一套 30 对自造任务上，把每个提速开关单独开、两个一起开、
`compactMaxChars=2000` 两轮合池 60 对，再加两行对照——只开 `enabled: true` 不加覆盖（0.945× /
0.968×，都跨 1，说明"开开关"本身没成本），和 `workspaceMap=true`（0.943× / 0.969×，跨 1）。
结论没变：**只有 `compactResults` 在长输出任务上量到了（0.756× / 0.786×），两个一起开反而不显著
（0.937×，重叠 + 噪声），2000 字那档省墙钟但 `run-tests` token 1.414× 更差**。
`gear=aggressive` 记了 1.076× / 1.122× 且砍上下文多，标注不建议开。运行时行为不变。

## 1.7.18（SWE-bench 扩到 6 条题：仍然跨 1；两臂失败集合完全一样）

第四轮把 `pytest-7490` 加进官方 SWE-bench Lite 任务池（验收口径改成「f2p 在 gold 后全绿 +
p2p 零回归，base 上本来就红的算噪声但必须计数」，7490 是 6 个噪声、0 回归），6 条题 × 3 轮
= 18 对：**墙钟 0.912× [0.676, 1.246]、token 1.007× [0.895, 1.136]，都跨 1**；两臂各 15/18
通过，没通过的都是同一条 7490（两臂三轮全没修好）——去掉它重算 0.993× / 1.032×，还是跨 1。
所以这不是插件让模型变差，是那条题本身太难。运行时行为不变，只追加数据与验收口径。

## 1.7.17（官方 SWE-bench Lite 上量了一次：量不出加速，顺手修掉审计的顺序偏差）

把官方 `princeton-nlp/SWE-bench_Lite` 的题目定义接上了（走代理拿 parquet，本机 `uv` 建环境，
按官方 `FAIL_TO_PASS` / `PASS_TO_PASS` 差分判分），5 条题跑了 3 轮两臂、30 对，两臂校验全绿；
**合池墙钟 1.016× [0.823, 1.261]、输出 token 1.055× [0.915, 1.229]，都跨 1**，只开
`enabled: true` 的 15 对也一样（0.881× / 1.081×）。原因是这类任务的工具输出本来就短，截短
开关没有可截的东西。方法和逐题数字在 `docs/SWEBENCH.md`。

这次还量出一个比效应更大的问题：**两臂串行时谁先跑会影响结果**。每轮固定 off 先跑读出
token 1.208×（更差），逐轮换序后 0.922×。`audit/run.mjs` 现在单数轮 off→on、双数轮 on→off，
报告头写明臂顺序；`docs/AUDIT.md` 补了对应小节。插件运行时行为不变。

## 1.7.16（真实 PR 上只开截短开关：24 对，两个指标都过了）

把真实 PR 的任务从 3 个扩到 6 个（`pallets/click`，每个 4 轮、共 24 对），而且只开发布时默认关着的
`compactResults`（`compactMaxChars=8000`），不再叠加 `gear=fast`——合池墙钟 `0.805×`（CI 0.656–0.985，
省 19.5%）、输出 token `0.768×`（CI 0.618–0.960，省 23.2%），两臂校验各 24/24。方向与量级和自造任务
上的 0.756× / 0.786× 一致，说明那条结论不是自造任务特有的。仍有一个真实任务 token 变多
（`click-3805` 1.086×），文档照实写。运行时行为不变。

## 1.7.15（修 1.7.12–1.7.14 的版本号）

发 1.7.13 / 1.7.14 时只改了 `package.json`，忘了 `index.js` 里导出的 `VERSION`，于是这两版的 trace 行还写着 v1.7.12——日志上的版本号和装的东西对不上，正是这条测试要防的事（`the traced version is the version the package ships` 在 1.7.14 的上传里是红的）。这一版把 `VERSION` 对齐到 1.7.15，行为和 1.7.14 一致。

## 1.7.14（真实 PR 的 18 对，量不出快慢）

`docs/AUDIT.md` 补上真实 PR 那一节：本地重建的 3 个 `pallets/click` PR，每轮先 `setup` 把仓库放回原样，`compactResults=true` + `gear=fast` 跑 6 轮共 18 对——合池墙钟 `0.796×`（CI 0.587–1.065）、token `0.888×`（CI 0.774–1.024），都跨过 1。能读出的只有质量没掉：on 臂校验 18/18、off 臂 17/18。文档同时写明为什么这里跑不了官方 SWE-bench / Terminal-Bench（Docker Hub 与 HuggingFace 都连不上），以及这批任务是手挑的小子集、不能和任何排行榜对齐。运行时行为不变。

## 1.7.13（审计工具能跑真实 PR 的任务清单）

`audit/run.mjs` 的任务行新增可选 `cwd`：写了的任务，它的调用和 `check` 都在那个目录里跑，没写的仍用 `--cwd`。以前一张清单只能对着一个仓库，真实 PR 任务（每个 PR 一个 checkout）和自造任务没法放进同一次对比；现在可以，报告的配对数按任务自然对齐。路径不存在在启动前就报错，不给一个跑完才发现的错目录。同时加了可选 `setup`：真实 PR 的仓库每轮都会被模型改掉，不重置的话第二轮量的是「已经修好的树」，数字会凭空变快；`setup` 每轮先跑，失败的那一轮记失败、不进统计。

这一版把等待很久的数字落了地，也把上一版的说法修对：

- 默认预算（`maxChars=8000`）对照跑，6 轮 24–30 对：墙钟 `0.756×`（CI 0.611–0.921），输出 token `0.786×`（CI 0.623–0.990）。两个区间都在 1 以下，所以「省时间是因为把预算调小了」这个说法不成立，已从 `docs/AUDIT.md` 撤回。
- 预算调到 2000，合池 60 对：墙钟 `0.855×`（CI 0.747–0.971），token 无法区分；`run-tests` 的 token 反而 `1.414×`（CI 1.049–1.939）更差，报告里每次都写。
- 压缩 + 闪电组合开，默认预算 30 对：墙钟 `0.937×`（CI 0.763–1.151）、token `0.973×`（CI 0.815–1.165），都跨过 1，这次量不出组合的额外收益。

真实 PR 侧只完成了准备，没有结论：从 GitHub 直接取已合并 PR，按 `base.sha` 还原 checkout、只保留测试补丁，先证明基线失败（`click#3884` 是 9 failed / 667 passed），再把源码补丁贴回去证明全绿（676 passed）。`click#3884`、`click#3865`、`click#3860`、`itsdangerous#378` 四个任务已通过这套校验，单轮试跑两臂都是 3/3 通过；样本只有一对，不能当结果。容器类评测（Terminal-Bench、SWE-bench 官方 harness）在这台机器上跑不起来——Docker Hub 拉镜像直接 EOF，HuggingFace 也连不上，这不是选择，是网络到不了。

## 1.7.12（压缩开关进面板）

右键面板多一行「截短工具输出」（`compactResults`），逐会话可开可关，和配置里的同名键写同一个值。此前压缩只能在 YAML 或设置页打开，面板上没有关掉它的地方——「每个提速开关都要能自己关掉」这件事在压缩上缺了一格，这一版补上。面板行数因此从七行变八行，`docs/REFERENCE.md`、`docs/REFERENCE.en.md`、`README.md` 同步改对，并写明 1.7.12 之前确实是七行。

## 1.7.11（`--on-config` 覆盖 `gear` 时不再写出重复的 YAML 键）

1.7.10 的 `--on-config gear=fast` 会在 on 臂的 patch 里再写一遍 `gear:`，同一个键出现两次——严格解析器直接报错，宽松的只留最后一个。结果是 on 臂每次调用 70 毫秒就失败，报告里写成"快得离谱"。现在覆盖值顶掉基础键，`gear` 只出现一次；新增一条断言盯住这点。

## 1.7.10（审计工具能一次多开一个开关，量组合效果）

`audit/run.mjs` 新增 `--on-config key=value`（可重复），只写进 on 臂的配置里，off 臂保持原样。以前 on 臂固定 `enabled: true` + `gear: auto`，想量「压缩和省 token 一起开」只能手改 patch 文件；现在一条命令就能跑一个组合，值按 YAML 原生类型写（`true` 是布尔、`4000` 是数字），键名写错在启动前就报错，不会跑完一轮才发现 on 臂用的是默认值。

## 1.7.9（Windows 上审计工具不再找 /bin/sh）

`audit/run.mjs` 的校验命令一直写死 `/bin/sh -c`，在 Windows 上直接 `spawn /bin/sh ENOENT`，CI 的 windows 作业因此全红。现在按平台选 shell（Windows 走 `ComSpec /d /s /c`），dry-run 打印的命令行与实跑一致；测试里那句 `ls -A` 也换成 `readdirSync`，不再依赖 POSIX shell。macOS/Linux 行为不变。

## 1.7.8（shell 输出被省略时，把报错行带出来）

`compactResults` 对 shell 类工具（`bash` / `shell` / `exec` / `run` / `terminal` / `command`）换了一种省略方式。以前一个 8 万字符的构建日志被砍成头 40% + 尾 25%，`FAIL src/a.test.js` 那行如果正好落在中间，模型看到的就是「一切正常」的假象。现在标记会从被省略的区间里把匹配报错的行挑出来一起带走，仍是一行标记：

`… [fast-mode: elided 61044 chars, lines 41–812; kept 2 matching lines 121:FAIL src/a.test.js: expected 1 got 2 | 400:error: 3 tests failed] …`

1.7.8 没有单独发布：修完 Windows 上审计工具找 `/bin/sh` 的问题后直接发了 1.7.9，npm 上不存在
`1.7.8` 这个版本。

匹配的是 error / failed / fatal / panic / exception / traceback / denied / refused / timeout / cannot / unable / not found 这些词，以及 `✖`/`✗`/`FAIL`/`ERR!` 开头的行，默认最多 12 条、总量不超过压缩预算的四分之一，行号在行首，模型可以直接按行回读。非 shell 工具（read、grep）行为完全不变，开关仍默认关闭；`kind` 多了一个 `shell`。

## 1.7.7（闪电换到并排槽位 + 页脚读数修好）

闪电从会话输入区的单值槽换到模型选择器旁的 list 槽。单值槽只渲染第一个注册者，计划按钮和语音麦克风谁先注册谁占位，后到的被静默丢掉；list 槽按 `id`/`order` 并排，三件控件可以同时在场。落点、外观、左键开关、右键七行、三档控件全都没变，只是不再和别的控件抢同一个座位。

页脚那行读数以前基本读不到：`sessionStats` 与 `tokenUsage` 是两个投影，它们挂在会话对象上，不在会话服务的同名方法上，按方法名去取只会永远落空。现在改成从会话列表快照里按会话 id 取投影值，订阅的也是这份快照；取不到时照旧回退到「只影响本会话」，不显示 0。

## 1.7.6（省略标记给出可回读的行区间）

`compactResults` 的省略标记从「省略了多少字符」变成「省略了多少字符、第几行到第几行」：`… [fast-mode: elided 4210 chars, lines 88–412] …`。模型不必再在「整份重读」和「相信残缺版本」之间二选一，可以直接按行范围回读那一段。单行正文没有行号，仍然只给字符数；开关仍默认关闭，行为与 1.7.5 一致，只是标记更可用。

## 1.7.5（面板三档 + 两个默认关闭的新开关 + 可复算的审计工具）

面板标题行右侧多了一组三档：保守 2 路并行，标准 4 路并行（出厂默认，按最近几步的表现自动收放），激进 8 路并行。三档只调并行宽度，机制、开关、阈值都不变；档位按会话保存，旧的 `fast` 写法仍读作激进档。面板底部改为真实读数（最近一步号与累计输出 token），取不到时该行不出现，不显示 0。

两个新开关都在设置页里、默认关闭、可反悔：`compactResults` 在结果超过 `compactMaxChars` 时只留头尾并标明省略量，`readCache` 让同一文件在 `mtime`/大小/inode 都没变时复用上次读取结果。两者都不改工具本身、不改文件，关掉即恢复原样。

新增 `audit/`：零依赖的配对 A/B 执行器（同一插件只差 `config.enabled`，多轮配对、几何均值比、配对 bootstrap 置信区间），配套 `skills/fast-mode-audit/` 与 `docs/AUDIT.md`；`docs/COMPACT.md`、`docs/PANEL.md` 写清截断规则与面板读数来源。

## 1.7.4（宣传文案：一个模型、一半输出、最快快一倍）

市场卡片与 README 开头改成同一句卖点，并在最显眼处露出 benchmark 链接；末尾明确写「有些情况会变慢」。七行悬浮说明压到一句，`不确定先查证` 与 `重复时换方法` 不再被读成纯提速开关：前者的 tooltip 写清「省的是猜错后的重来」，后者写清触发条件是「同一调用重发、或连续三步结果一样」。标签、机制、阈值、默认值一概不动。

## 1.7.3（README 重写：结论先行，只留能复算的数字）

只改文档，代码与行为零变化。README 从「做了什么」改成「快多少、哪些按钮、边界在哪」，七按钮表逐行给原理与默认值，benchmark 只留三开关全开的四行表（DP17 / DP18 / DP20b / 合池 30 对），英文侧同步。

## 1.7.2（面板七行改名：说清「开关做什么」，行为与数字一概不动）

用户看到的是七行标签，不是文档。旧标签里「少写过程」「先列文件清单」「重复无进展时提醒」说的是内部术语或半句话，不看 README 猜不出效果；这一版只改文案，不改任何机制、阈值、默认值或注入文本。

- **`panel.prose`：少写过程 → 精简输出。** 它控制的是输出纪律（少解释、直接给结论），「过程」容易被读成推理过程。
- **`panel.budget`：最多几步 → 步数上限。** 与右侧取值（`不限` / `15 步` / `30 步`）构成一个字段，而不是一个问句。
- **`panel.map`：先列文件清单 → 开局附文件清单。** 清单是插件在开局附上的，不是模型自己列，旧名把主体说反了。
- **`panel.batch`：一次读多个文件 → 批量读文件。** 四个字，与相邻行同一语法层级。
- **`panel.probe`：不确定时先查一次 → 不确定先查证。** 「查一次」会被误读成次数限制；它要求的是先取证再推理。
- **`panel.act`：先动手再解释 → 先执行后解释。** 与英文 `Act before explaining` 对齐，动词统一为「执行」。
- **`panel.repeat`：重复无进展时提醒 → 重复时换方法。** 旧名只说触发条件，不说该做什么；新名给出动作。
- 英文侧同步：`Concise output` / `Step cap` / `File inventory` / `Batched reads` / `Verify before guessing` / `Act before explaining` / `Nudge on repeats`。行内 tooltip 保持原样（它本来就是解释后果的地方），仍是七行。
- 面板行数、`sessions` 列的写入路径、`outputDiscipline` / `stepBudget` / `workspaceMap` / `openBatch` / `probeFirst` / `actBeforePlan` / `repeatNudge` 七个字段一个都没动；`src/config.js` 里七条描述的面板名与英文摘要同步改名。
- 校验：`node --test test/*.test.js` 337/337 通过；`ui-copy-lint` 干净、`zh-lint` 0 error、`style-lint` 通过。

## 1.7.1（`actBeforePlan` 补上第二个信号：本回合思考已写多长；仍然只改提示词，不加面板行）

1.7.0 用 `repeatNudge` 去抓「没新信息还在想」，但本语料里模型很少把同一个调用原样重发第三遍：`DP17` 十二个会话只发出 3 条提醒，全部来自「连续结果相同」那条分支，「同调用 ≥2 次」一次都没满足。所以第二个信号换成本来就记在会话里的东西——**这一回合已经写了多少字符的思考**。

- **判定与文本。** `assistant/message` 事件里的 `reasoning` 块按字计数（`src/usage.js` 的 `readReasoningChars`），按回合累加（`noteReasoning`）；到 `REASONING_CHARS`（4000）字符时，在下一步之前给一条通知，之后每再翻一个 4000 的台阶可以再来一条，每回合上限 `MAX_REASONING_NUDGES`（2）。文本写明它看到的字数并给两个出口：`You have written about N characters of reasoning in this turn. Past that length, more thinking about facts you already have rarely changes the next action: take the action the evidence supports, or read the one fact that is still missing.` 触发条件只有长度本身，所以文本**不断言**「没有新信息」。
- **它挂在 `actBeforePlan` 上，没有新开关。** 面板固定七行，加第四行会破坏「七行」这个说法；这条和「先动手再解释」是同一个方向（把计划和推理压到能动作为止），所以合在一个开关里。代价必须说清：`ab17` 从此同时量两个行为，读数只能按「两个行为一起」解释。
- **仍然一个请求字段都不碰。** 字数只读不写，`agent/request` 照旧原样返回 `resolved`。路由不吐 reasoning 块时读回 0，开关在那条路由上保持沉默——默认关闭的东西该有的失败方式。
- **`DP18` 复核的是 1.7.0 的读数**（长度提醒还没上）：`codeimpl`/`ledger`/`stategraph` 三族各 3 对、四臂轮转、36 次运行，`trio17` **0.491×** [0.314, 0.788]、8/9 更快，`ab17` **0.643×** [0.427, 0.920]、6/9，`pf17` **0.719×** [0.492, 0.999]、7/9；与 `DP17` 合池 21 对后 `ab17` 0.708× [0.558, 0.898]、`trio17` 0.575× [0.444, 0.748]（18/21 更快）。思考字符几何均值 `off` 71275 → `ab17` 44617 / `trio17` 32463，步数 12.86 → 8.98 / 6.87，判分全绿。计划里的 5 对减到 3 对是资源纪律（同一时刻只跑一个块、不长时间占满 CPU），理由与代价写进 `BENCHMARK.md`。
- 测试 334 → **337 全绿**（`test/deliberation.test.js` +2、`test/usage.test.js` +1）。动 `src/deliberation.js`、`src/usage.js`、`index.js`、两个测试文件。

- **`DP19` 回归复核（1.7.1 的代码，`off` 对 `ab17`，`debugfix`+`migrate` 各 4 对、16 次运行）：** `ab17` **0.701×** [0.516, 0.930]、6/8 更快、判分 16/16 绿；`debugfix` 0.756×（4/4，1.7.0 时 0.582×）、`migrate` 0.649×（2/4，1.7.0 时 0.996×），思考字符几何均值 28275 → 17834、步数 9.27 → 7.62。提醒在真实运行里发出过：4 个 `ab17` 会话各 1–2 条（每回合上限 2），`off` 0 条；同批每回合思考字符 5350–120897。结论只到「加了它没让这个臂变差」，8 对不足以说更快，也没拆开两个行为各自的贡献。
- **`DP20` 发布版本复核（1.7.1 + 当前 Host 的 CLI，`off` 对三开，三族各 3 对、18 次运行）：** 三开 **0.372×** [0.240, 0.563]、8/9 更快、判分 138/138 与 `off` 逐格一致、零泄漏；`codeimpl` 0.601×、`ledger` 0.437×、`stategraph` 0.196×，思考字符几何均值 83823 → 27621（−67%）、输出 token 27636 → 11914（−57%）、步数 13.22 → 6.67。与 `DP17`/`DP18`/`DP19` 合池 30 对 **0.505×** [0.401, 0.635]、26/30 更快；合池质量 `trio17` 30/30 绿 438/438 项、`off` 38/38 绿 546/546 项。这一块因此把「大部分时候推荐三开」从三块的方向证据升级为发布版本上的复现；仍保留 `codeimpl` 一对 1.04× 的反例，推荐措辞是「大多数时候」。另：首次尝试误用旧包 CLI（`exit=1`、`steps=1`、`llm=0`）的 18 行整块作废，已移到 `bench/results/archive/suite-DP20.void.jsonl`，不参与任何数字。

## 1.7.0（删掉 `lazy` 与它改 `reasoningEffort` 的那一半；换成三个默认关闭的纯提示侧开关：`probeFirst`「不确定时先查一次」、`actBeforePlan`「先动手再解释」、`repeatNudge`「重复无进展时提醒」；12 对 5 臂 60 次运行里三开 0.647×，判分全绿）

这一轮的两件事是同一个决定的两面：**插件从此不碰任何请求字段**，**以及**把「别想太久」换成不改答案的做法。

- **`lazy` 整个删掉，不是关掉。** `src/lazy.js`、`test/lazy.test.js`、`lazy`/`lazyEffort`/`lazyVariant` 三个配置键、面板第六行、`bench/arms/laz*.yml`、以及 `agent/request` 里那处唯一的 `{ ...resolved, reasoningEffort }` 全部消失。删的原因是上一轮自己测出来的：`LZ1` 里只发文字是 0.389× [0.252, 0.600]，文字加降档是 0.430× [0.273, 0.677]，两臂直接对打 1.106× [0.682, 1.792]——**置信区间跨过 1，请求侧那一半没有测出任何额外收益**；而且它是全插件唯一会改变答案而不只是改变时延的机制。加上这条路由在所有 9678 行测量里 `reasoning` 用量都报 0，参数是否被上游当真从来没有被证明过。删掉之后「默认契约」升级成绝对承诺：**`agent/request` 原样返回 `resolved`，仓库里不存在任何构造 `reasoningEffort:` 的代码**，不存在「某个开关打开时会改」的例外。
- **三个新开关都是纯提示侧、默认关闭、逐会话、上屏可见。**
  - `probeFirst`「不确定时先查一次」：入场步一条通知——拿不准的代码库事实去读一次（先 grep 再 read，先读区间再读全文），不要用推理去猜。
  - `actBeforePlan`「先动手再解释」：第一个工具调用之前最多一行计划；下一步已经清楚就直接做。
  - `repeatNudge`「重复无进展时提醒」：回合进行中才说话，条件是**本地证据**——同一个调用（工具名 + 稳定的 arguments 键）本回合已经跑过 ≥2 次，或最近三条相邻结果内容完全一致；提醒里写明观察到的次数。每回合上限 2 条，同一份证据不重复提醒，**报错结果永不算作循环、并清掉陈旧计数**（失败是信息，不是原地打转）。
  - probe 与 act 共用**一条**通知和一条 `GUARD`（「信任边界、数据丢失、安全、可访问性上绝不跳过验证」），两个都开也只花一条消息。
- **机制在 `src/deliberation.js`，有界且不可能污染工具结果。** `createProgressWatch`/`beginTurn`/`noteCall`/`planRepeatNudge` 加常量 `MAX_REPEAT_NUDGES = 2`、`REPEAT_CALLS = 2`、`STALE_RESULTS = 2`；内存上限 `RUN_MAX = 64` 个会话、每回合 `HISTORY_MAX = 8` 次调用；全部调用都包在既有的 catch 里，检测失败只会丢一条提醒，不会改变任何一步工具结果。
- **实测（`DP17`，`debugfix` + `migrate`，每族 6 对共 12 对、五个 arm 配置按对轮转，共 60 次运行；比值 = 该臂墙钟 / `off` 墙钟，逐对几何平均，括号是 95 % bootstrap CI）：**

  | 开关 | 汇总墙钟比 | 更快 | 测试 |
  |---|---|---|---|
  | `probeFirst` | 0.883× [0.663, 1.176] | 7/12 对 | 全绿 |
  | `actBeforePlan` | 0.761× [0.578, 1.032] | 8/12 对 | 全绿 |
  | `repeatNudge` | 1.007× [0.839, 1.227] | 7/12 对 | 全绿 |
  | 三个全开（`trio17`） | **0.647× [0.501, 0.893]** | **10/12 对** | 全绿 |

  分家族看差别很大：`debugfix` 上 `actBeforePlan` 0.582×、6/6 更快，思考字符（几何均值）8249 → 3152（**这正对着「思考链过长」这个现象**）；`migrate` 上它 0.996×、步数反而（几何均值）11.0 → 15.65，说明该族需要先规划，硬推会绕路。`probeFirst` 在 `migrate` 上 0.785×（思考字符 61560 → 45499），在 `debugfix` 上 0.993×。`repeatNudge` 单开等于没变，但它在组合里不拖后腿——三开 0.647× 是本次唯一置信区间不跨过 1 的臂。**所有臂、所有对，判分与 `off` 一致，零失败。** 更宽的三族验证（`DP18`）跑在 `codeimpl`/`ledger`/`stategraph` 上，结果补齐后写进 `BENCHMARK.md`。
- **`repeatNudge` 为什么单开等于没变：它基本没被触发。** 把 `rn17` 那 12 个会话的记录翻出来数提醒原文：12 个会话里只有 2 个各收到过提醒（`debugfix-p2` 一条、`p5` 两条），合计 **3 条**，而且全部来自「连续结果内容相同」那条分支（观察到的相同条数是 3、7、7）；「同一调用本回合已跑 ≥2 次」这条分支**一次都没满足**。原因不是逻辑坏了，是阈值对着「极端打转」而不是「日常长链」：本语料里模型很少把同一个调用原样重发第三遍。所以 1.007× 的读数其实是「几乎没发生」的读数，不能当成「提醒没用」。这一条如实写在这里，下一轮要改就改阈值本身（不加开关、不加面板行），并用新的臂重测。

- **没有动的东西：** 默认值一个没改（`enabled: false` 仍是出厂态）、`d2`/`d2g`/`dr8` 仍然 HOLD、没有新增提醒时机或预算旋钮、没有新增任何请求字段或系统提示段落。
- 测试 338 → **334 全绿**（删 15 条 lazy 测试，新增 `test/deliberation.test.js` 10 条 + `test/plugin.test.js` 9 条集成）。动 `index.js`（删流式 reasoning 块、入场接线、`agent/request` 原样返回、版本号）、`src/deliberation.js`（新）、`src/config.js`（三键换三键，32 键）、`src/effort.js`、`src/state.js`、`client.js`（三行面板 + 两个白名单 + 四个写路径）、`src/lazy.js` 与 `test/lazy.test.js`（删除）、`test/{client,config,effort,metrics,plugin,state}.test.js`、`bench/arm-config.mjs`、`bench/suite-run.py`、`bench/arms/{pf17,ab17,rn17,trio17}.yml`（新）、`package.json`、两份 README、`docs/REFERENCE.md`、`BENCHMARK.md`。

- **`DP18` 补齐（三族复核，1.7.0 的臂）：** `codeimpl`/`ledger`/`stategraph` 各 3 对、36 次运行，`trio17` **0.491×** [0.314, 0.788]、8/9 更快，`ab17` 0.643× [0.427, 0.920]，`pf17` 0.719× [0.492, 0.999]，判分全绿；与 `DP17` 合池 21 对：`ab17` 0.708× [0.558, 0.898]、`trio17` 0.575× [0.444, 0.748]。三个 arm 的方向在两族短任务（`DP17`）与三族长任务（`DP18`）上一致，`trio17` 是唯一在两块里 CI 都不跨 1 的臂。详见 `BENCHMARK.md` 的「块 DP18」。

## 1.6.2（修面板：`lazy` 那张卡以前点不开；标签改成能看懂的说法；lazy 的梯子文本做了三版对照，不换默认）

- **「过度思考时降档」以前点不开，是真的坏了。** `client.js` 里浏览器侧的 `sessions` 列有两处白名单：`sanitiseChoices` 和 `withOwn`，它们认识 `enabled/gear/discipline/stepBudget/map/batch` 六个字段，唯独没写 `lazy`。于是点击「打开」时写出去的会话条目里根本没有这个 key，宿主读回来是「没有条目」，面板再读一遍又画成「关」——看起来就是按钮没有反应。宿主侧 `src/state.js` 一直是认识 `lazy` 的，缺的只是浏览器这一半。两处都补上，并加了一条跨侧回归测试（`test/state.test.js`：浏览器序列化 → 宿主 `choiceFor` 读回 `{lazy:true}` → 浏览器 `parseChoices` 再读回 `true`；非布尔值仍被丢弃）。**旧版本里已经点过的人不会留下坏数据**，因为那次点击压根没写进去。
- **标签改名，说明手段而不是口号。** 面板第六行从「想太久就少想一点」改成 **「过度思考时降档」**（英文 `Cap over-thinking`），tooltip 写清条件与保证：「一步里推理超量时，下一个请求降一档；安全校验照旧」。旧说法是建议、听起来像文风；新说法说的是这个开关实际做的事，也交代了降的是什么。
- **`lazy` 的梯子文本做了三版对照（`lazyVariant`，只在 bench 里选，不进面板）。** `v1` 是 1.6.0/1.6.1 已经发布的那份文本；`v2` 在它后面加两条（「不要重述任务或先解释计划」「下一步已经清楚就直接做，不要再权衡一遍不打算采用的方案」）；`v3` 是同样规则的精简重写（629 字符 vs 762）。两轮对照（`LZ2` 12 对 36 次运行、`codeimpl`+`ledger`；`LZ3` 15 对 30 次运行、`debugfix`+`migrate`+`stategraph`）的结论是 **不换文本**：`LZ2` 里 v2 对 v1 是 0.699×（9/12 更快），但 `LZ3` 里反过来是 0.822×、中位 1.014、7/15 更快，`debugfix` 五对**全慢**（1.08–1.23×）——短任务上多出来的两句话是纯成本。v3 在 `LZ2` 是 0.997×，等于没变。两轮判分都和对照臂完全一致（648/648、370/370），所以这不是质量换速度，是**只在长任务上划算、在短任务上倒亏**的取舍；默认仍是 `v1`，`lazyVariant` 保留为复现实验用的隐藏字段，面板上看不到。
- **没有动的东西：** 默认值一个没改（`enabled: false`、`lazy: false` 仍是出厂态）、`lazyEffort` 仍默认 `true`、`HOT_CHARS`/`NUDGE_CHARS`/降档上限全部照旧、`d2`/`d2g`/`dr8` 仍然 HOLD、没有新增任何提醒时机或预算旋钮。
- 测试 336 → **338 全绿**。动 `client.js`（两处白名单 + 标签）、`src/config.js`（`lazyVariant`）、`src/lazy.js`（`DIRECTIVES` 三版）、`src/effort.js`、`index.js`（版本号 + 传 `variant`）、`test/state.test.js`、`test/config.test.js`、`test/lazy.test.js`、`bench/suite-run.py`、`bench/arm-config.mjs`、`bench/arms/lz2v{1,2,3}.yml`（新）、两份 README、`docs/REFERENCE.md`、`BENCHMARK.md`。

## 1.6.1（1.6.0 的补包：npm 页面上那句「从不改 reasoningEffort」在 1.6.0 之后不准确了）

- 1.6.0 加了 `lazy`（默认关闭，唯一会改 `reasoningEffort` 的路径），但 `package.json` 的 `description` 还是旧话「Never changes reasoning effort, the model, or the request body」，npm 首页会照抄这句。这一版只改这一句：**默认**不改推理强度、模型与请求体，例外是默认关闭的 `lazy`（想太久时降一档）。行为、默认值、测试零改动（336 全绿）。

## 1.6.0（一个新开关 `lazy`「想太久就少想一点」：动态收思考强度 + 压过度思考，默认关闭；12 对 36 次运行实测 0.39–0.43×，判分 216/216 全绿）

按这一轮的要求，只开**一个**新开关，不动任何默认值。它的理念照抄 [ponytail](https://github.com/dietrichgebert/ponytail) 的核心：先读懂再动手，然后从「最小能work的那一级」开始做，不为想象中的未来写代码；但**读完之前不许偷懒**，也不许省掉信任边界校验、安全、数据丢失处理、可访问性。`lazy` 就是把这条用在「思考」上：不是让它不读、不想，而是想够了就停。

- **新开关 `lazy`，出现在面板第六行「想太久就少想一点」。** 默认 `false`，逐会话写入 `sessions` 列（`src/state.js` 的 `configOverridesFor` 映射），只影响本会话。它是本插件**唯一**一个会改变答案而不只是时延的开关，所以默认关、tooltip 也只说后果（「想太久时先按已有信息定下去，安全校验照旧」），不说「更快」。
- **两半机制，可分开验证。** 提示侧：入场步在对话里放一段 `src/lazy.js` 的梯子文本（先缩小范围 → 复用现成实现 → 标准库/平台能力 → 已装依赖 → 一行 → 最小实现；并把「绝不省略」的四类写清楚），每轮一次；同一轮里推理字符超过 `HOT_CHARS`（1600）就补一条 nudge（最多 2 条，间隔按轮内累计 4000/8000 字符）。字段侧：那一步确实想太久时，把**下一次**请求的 `reasoningEffort` 降一档（`EFFORT_RUNGS` 里往 `low` 方向走，地板 `low`，从不升档），每步最多一次、每轮最多两次，工具报错后两步内不降。`lazyEffort: false` 只发文字、一个请求字段都不写，是这一块的对照臂。
- **不会重演 0.9.x 把会话钉在低档的事故。** 0.9.x 的问题是改写值活过了它被授权的那个回合：值进了请求头，面板从请求头投影「上次用的强度」，于是一个本来跑在中档的会话会漂到低档并跨重启保留。这一版的基准值是**每个请求现读**的 `resolved.reasoningEffort`（用户此刻的选择），降档只写进这一个请求的返回值，没有任何跨请求/跨会话状态；用户中途换档，下一次就是用户的档。测试里有专门的源码扫描断言：全仓只允许这一处 `reasoningEffort:`，形状是 `planned.reduced ? { ...resolved, reasoningEffort: applied } : resolved`，且不允许出现 `thinking: {` 或任何 `decideEffort`。
- **测量补齐两个此前拿不到的通道。** `src/metrics.js` 每行新增 `reasoningChars` / `textChars`（流式 chunk 的字符数，`text`、`delta`、`delta.text` 三种拼法都认，认不出就算 0，只做下界）。加这个是因为这条路由的 usage 里 `reasoning` token **一直是 0**（750 行 fit 数据、1888 步），`reasoning_effort` 是否被上游当真从未被证明——所以 `lazy` 的收益必须能从「实际流出了多少推理字符」独立读到，而不是相信一个恒为 0 的字段。降档发生时，行里同时有 `effort`（实际带出去的）与 `effortBase`（用户原本选的），两列同现就是降档的机器证据；下一个未降档的请求会显式清掉 `effortBase`，不会把它归因到后来每一步。
- **实测（`LZ1`，12 对、36 次运行、两族任务、ABBA 交替）：** `lazt16`（只发文字）逐对几何墙钟 **0.389×**，95 % CI [0.252, 0.600]，**11/12 更快**，符号检验 p = 0.0032；`laz16`（文字 + 降档）**0.430×**，CI [0.273, 0.677]，同样 11/12。推理字符几何 0.372× / 0.418×，步数中位 8.0 → 5.5 / 4.5，判分 **216/216 全绿**（两臂与 `def15` 完全相同）。把两臂直接对打：`laz16 / lazt16` = **1.106×**（CI [0.682, 1.792]，5/12 更快），即**收益来自提示文字那一半，降 `reasoningEffort` 这一半没有测出额外收益**——这一条如实写进 `BENCHMARK.md`，不做成「双保险都有效」。
- **没有动的东西：** 默认值一个没改（`enabled: false` 仍是出厂态）、`disciplineAfterSteps` 仍是 4、`d2`/`d2g`/`dr8` 仍然 HOLD、没有新增任何提醒时机或预算旋钮。外部评审建议「先做稳定性、别加旋钮」，这一轮只在用户明确要求下加这一个开关，其余仍是稳定性与测量。
- 测试 313 → **336 全绿**（新增 `test/lazy.test.js` 15 条、`test/plugin.test.js` 6 条集成、`test/metrics.test.js` 2 条 `effortBase` 断言；源码扫描测试按「恰好一处、且是这一种形状」放宽）。动 `index.js`、`src/lazy.js`（新）、`src/config.js`、`src/effort.js`、`src/state.js`、`src/metrics.js`、`client.js`、`bench/suite-run.py`、`bench/arm-config.mjs`、`bench/arms/laz16.yml`（新）、`bench/arms/lazt16.yml`（新）、两份 README、`docs/REFERENCE.md`、`BENCHMARK.md`、`CHANGELOG.md`。

## 1.5.35（1.5.34 的补包：把 `bench/soak.mjs` 打进 npm 包）

- 1.5.34 的 `BENCHMARK.md` 让人「自己跑一遍浸泡」，但 `bench/soak.mjs` 没在 `package.json` 的 `files` 里——装了包的人照着跑会发现文件不存在。这一版只补这一行 `files`（自包含：只 import `../index.js`），行为零改动，测试仍 313 全绿。

## 1.5.34（稳定性版：插件里最后 6 个随宿主任期增长的缓存全部封顶，附一个可重跑的浸泡测试）

按外部评审这一轮的建议，不新增任何提醒时机、重复提醒或预算旋钮，默认值与全部开关一律不动。只做一件事：**让插件在「几周不重启的宿主」里不再积累任何东西**，并把这件事变成能重跑的数字。

- **审计了插件自己持有的 8 个缓存，其中 6 个之前是无界的。** `mapSent`（`${sid}${cwd}`）、`startedAt`（`callId`，结果没回来的调用不会被 `delete`）、测量 sink 的 `labels`/`steps`/`open`/`pendingChunk` 都不封顶；`compacted`、`autoBudgets`、`sessions` 有上限但各写一遍淘汰循环。现在全部走同一个 `src/bounded.js` 的 `trim(cache, cap)`（插入顺序即淘汰顺序，`cap <= 0` 不动），上限分别是 500 / 2000 / 2000 / 2000 / 2000 / 1000。上限远高于任何真实并发量，正常使用不会淘汰活条目。
- **sink 关闭时不再记 label。** `agent/request` 每个请求都会调 `label()`，而默认配置根本不写测量文件——1.5.33 及以前照样把每个会话的档位记进一个 Map。20000 次 `label()` 的堆增长因此从 **2.48 MB 降到 0.01 MB**（`/tmp/labelctl.mjs`）。淘汰也不会写错行：被淘汰会话的测量行记 `mode: 'unknown'`，不再可能借用别人的档位，这一条是 `test/bounded.test.js` 的断言。
- **新增 `bench/soak.mjs`：一个从不重启的假宿主。** 用与 `test/plugin.test.js` 相同的假 Cordis 挂载真插件，跑 N 个会话 × 4 轮、每轮 `step/start` → `agent/request` → 工具调用 → `step/end`。20000 会话 / 242400 步：堆增长 **−1.64 MB（−0.1 KB/会话）**；加 `--metrics`（真的写盘，跑完删临时文件）跑 4000 会话是 −0.58 MB。阈值 0.004 KB/会话，超了退出码非 0。
- **这个脚本第一次跑报的是假泄漏，值得记下。** 4000 会话 +63.67 MB，`--off` 一模一样；二分到 `--no-request` 变 −0.55 MB，`--one-id` 仍 +49 MB——根因是脚本自己 `ctx.fire('agent/request', …)` 没 `await`，16000 个 pending Promise 冒充成插件泄漏。改成 `await` 后同规模 −0.61 MB。脚本头注释里写清了这条，免得下次又去插件里找。
- **同时复核了已发布源码没退化（`V1533`，12 次运行）：** `debugfix`+`migrate` 3 对 ABBA，`def15` 6 次合计墙钟 **654.0 s（0.50×）**、步数 0.46×、输出 0.57×、逐对 5/6 更快、两臂 **6/6 全绿**，与历史块同量级。
- 测试 308 → **313 全绿**（新增 `test/bounded.test.js` 5 条）。动 `index.js`、`src/metrics.js`、`src/auto.js`、`src/bounded.js`、`bench/soak.mjs`、`BENCHMARK.md`、`docs/REFERENCE.md`；顺手把 `README.en.md` 里「右键有四行设置」改成五行（1.5.33 加了第五行，英文版漏改）。面板、配置、默认值零改动。

## 1.5.33（把「多个开关一起开」里那个真正省时的开关做进面板：第五行「一次读多个文件」）

- **面板多了第五行，`openBatch` 第一次能被用户点到。** 1.5.32 实测的 `combo3`（默认 + `workspaceMap` + `openBatch`，11 对 0.742×）里有一半来自 `openBatch`，但它之前只在配置里，面板够不着——等于「测出来了但用户开不了」。这一版把它做成和「先列文件清单」同级的逐会话开关：写 `sessions` 列里本会话条目的 `batch`，宿主侧映射成 `openBatch`（`src/state.js` 的 `configOverridesFor`），不动共享默认值，只影响本会话。
- **默认仍然关。** 单独开 `openBatch` 测不到显著（0.882×，CI 跨 1），只有和清单一起开才有效；面板 tooltip 写的就是这句后果，不写成「更快」。默认值、`d2g` 的 HOLD 状态都不变。
- **两侧字段表对齐。** `client.js` 的 `sanitiseChoices` 与 `src/state.js` 的 `sanitise` 同步接受 `batch`，避免出现「客户端写进去、宿主读出来丢掉」的假开关（这正是 1.5.17 那批死按钮的成因）。测试补了三处：状态层的 `batch → openBatch` 映射、面板行的默认值/逐会话写入、以及「共享 `openBatch` 为真但本会话条目为假」时面板按条目画。
- 动 `client.js`、`src/state.js`、`src/config.js`（描述文案）、两份 README、`docs/REFERENCE.md`；测试 306 → 308 全绿。

## 1.5.32（先回答「多个开关一起开」：三开 **0.669×**、22 对里 18 对更快、隐藏测试 22/22 绿；附带一个内存上限修复）

- **新增两个组合臂并真的跑了一遍**（此前只有逐个开关的消融，从没跑过「一起开」）：`combo2` = 默认 + `d2g` + `workspaceMap` + `openBatch`，`combo3` = 默认 + `workspaceMap` + `openBatch`。同块内与默认臂按 `(任务, pair)` 配对：
  - `combo2`：22 对，wall 几何 **0.669×**、CI95 **[0.554, 0.799]**、中位 0.687、**18/22 更快**、符号检验 **p = 0.0022**，输出中位 −27 %，步数中位 7 → 6.5，隐藏测试 22/22 绿；
  - `combo3`（去掉时点臂）：11 对，几何 0.742×、CI95 [0.594, 0.909]、8/11 更快（p = 0.113，方向提示）。
  - 单开时三个开关各自都测不到显著（0.802 / 0.882 / 0.816，CI 全跨 1），合起来才顶穿噪声——**叠加是真的**，但只验了这三个，其余开关（`batchVerify` / `preferPatch` / 步数预算 / 更宽并行 / 关明细律）单独叠都是 0.96–1.10，没有理由跟着一起开。
- **默认不变**：`combo2` 比 `combo3` 多出的那一截指向时点臂 `d2g`，但两臂不是同一批 pair、n = 22 vs 11，不足以给「多 7 个百分点」定论；`d2g` 在 `codeimpl` 上的负收益没被推翻，`disciplineAfterSteps` 仍是 4。证据与读法写进 `BENCHMARK.md` 对应新章节。
- **稳定性修复：`autoBudgets` 加了上限。** 它按会话存自适应超时上限，原来只写不删——长驻宿主进程里每个会话留一格，永不回收。现在与 `compacted` 一样保留最近 200 条（超出丢最旧的）；只有「当前会话下一步」会读它，丢弃旧会话不影响行为。这一条是给长时间运行的宿主用的，不改任何默认值。
- 只动 `index.js`（8 行）与文档；`src/` 无改动。

## 1.5.31（`turns.py` 顺手审另一条提醒：压缩后重述规则的那条，会不会无理由地多出来）

- 插件只主动发两条提醒：入场步的「少写散文」（每轮恰好一次）和压缩后的「规则重申」（每次压缩后一次，因为规则可能被裁掉）。`bench/turns.py` 原来只审第一条，第二条只当噪音忽略——如果它的标志位泄漏，就会在每个合格轮里白送一段规则文本。
- 新增读数：有多少轮带过重述、其中有多少轮的重述条数**超过**该轮的压缩次数 + 1（+1 是允许的：压缩发生在上一轮末尾时，重申落在下一轮第一步）。近 72 小时真实会话：**624 轮**带重述，**0 轮**没有压缩可指。
- 同时把「首次提醒落在第几步」的直方图、`--explain-doubles`（列出每个重复提醒的轮、步号、seq 与压缩点）固化进脚本，方便下次换版本时重跑对照。
- 只动 `bench/turns.py` 与文档，`src/` 无改动。

## 1.5.30（把 1.5.28 的修复拿真实流量做 A/B：同一份会话，同一串事件，两个版本）

- **新增 `bench/replay.mjs`**：把一份留存会话的事件流原样喂给插件自己的钩子（`session/event` + `agent/pre-step`），数出「这一版会发几条提醒、落在第几步」。会话 id 是合成的，`metrics`/`benchmarkFile` 关掉，不碰 `~/.dsh` 状态。
- **量化修复效果**：轮内压缩时宿主用**同一个 turn 号**重入该轮，而这次重入不落盘——所以真实会话记录里看不到它，只看得到「同一轮两条提醒」。`--simulate-reentry` 在每次 `compaction/start` 后按当前 turn 号补发一条 `turn/start`，把这个假设显式化：
  - 1.5.27 的接线（`noteTurn` 不知道 turn 号）：1529 个合格轮里 **940 轮**会被发两次以上；
  - 1.5.28 的接线：同样 1529 轮 **0 轮**；
  - 单会话对照更直观：`session-1ee3a310` 111 个合格轮，旧版 82 轮重复（最长的 121 号轮在第 4/13/38/65/102 步各发一次），新版 0 轮。
- **真实流量侧读数不变**：`bench/turns.py` 近 72 小时 651 个合格轮里 120 轮真收到两条、其中 110 轮两次之间夹着压缩，与重入假设一致。
- **没有行为变更**：只加脚本、改文档与版本号；`src/` 与 1.5.29 逐字相同。

## 1.5.29（1.5.28 的补丁：把 `bench/turns.py` 真的放进包里）

- `package.json` 的 `files` 是一份白名单，1.5.28 新增的 `bench/turns.py` 没进去：`npm pack dsh-fast-mode@1.5.28` 里没有这个脚本，而 `CHANGELOG` 和 `bench/README.md` 都在教读者运行它。1.5.29 只改白名单 + 版本号，**代码与 1.5.28 逐字相同**。
- 复核方式：`npm pack dsh-fast-mode@1.5.29 --silent` 解包后应同时看到 `package/bench/turns.py` 与 `package/index.js` 里的 `VERSION = '1.5.29'`。

## 1.5.28（稳定性：压缩后同一轮里不再重复发同一条提醒）

- **先量后改**：新增 `bench/turns.py`，把留存会话按 `turn/start` 切成轮，统计「够长却没收到提醒」「同一轮收到两次以上」「多轮会话里后续轮是否重新上膛」。72 小时窗口 68 个会话 / 1446 轮：647 个合格轮**646 个收到**提醒（99.8 %）、后续轮重新上膛 599/600；但**同一轮收到两次的有 114 轮**，其中 **105 轮（92 %）两次之间夹着一次上下文压缩**。
- **根因**：轮内压缩时宿主重入该轮（同一 turn 号再走一次 `turn/start`），`noteTurn` 把它当新轮清空了全部每轮闩锁，`disciplineSent` 被复位，于是第 4 步发过的提醒在压缩后第 4 步又发一次。
- **修复**：`noteTurn` 接受宿主给的 turn 号，**只在新 turn 号变大时清闩锁**；同号视为续跑，计数器与闩锁全部保留（`src/auto.js`、`index.js` 传 `event.data.turn`）。事件流不带 turn 号时退回旧行为，不会更差。压缩后本来就有独立的 reassert 提醒，所以这里压掉重复的那一条不影响纪律覆盖。
- **测试**：新增「续跑的轮保住闩锁、新轮重新上膛、无 turn 号仍按旧行为」用例；`node --test test/*.test.js` **305/305**，`style-lint` 0。
- **没有行为变更**：默认值、提醒时机、提示词与 1.5.27 完全一致；这是投递去重，不是新旋钮。

## 1.5.27（把闸门最后两个没测过的任务族补上：方向相反，时点类旋钮收档）

- **补测：`bench/suite-run.py --tasks ledger,shellpipeline --arms def15,d2g --pairs 3 --tag D2G2`**（12 次运行、全部可用、丢弃 0、质量零倒退）。`d2g` 的闸门常量（第 1 步 ≥2 次调用才提前）此前只在 `codeimpl`/`debugfix`/`migrate` 三族验过，`ledger`/`shellpipeline` 一个数都没有。
- **结果方向相反，池化仍不支持提前**：`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。
- **判定不变：HOLD，默认继续停在第 4 步。** 结论也更准了：提前提醒的价值取决于任务「一条命令跑完再读」还是「逐笔对账」，而不取决于第一步调了几次；时点类旋钮到此收档，不再为新时点补块。
- **`BENCHMARK.md` 同步**：`d2g` 的消融行 21 → 27 对、0.756 → 0.816、CI 改为 [0.650, 1.023]；新增「补测（`D2G2`）」一节记账两条反向的逐对比值。
- **没有行为变更**：只跑块、改文档；默认值、提醒时机、提示词与 1.5.26 完全一致。

## 1.5.26（把「多开几个开关会不会更快」变成一张可复算的表）

- **新增 `bench/ablate.py`**：把 59 个块的全部臂按「同块、同任务、同 pair」对锚点臂池化，输出每个开关相对默认配置（`--anchor def15`）或相对 `off` 的几何比、95 % bootstrap CI、中位、步数/输出比、更快对数与符号检验。这是第一次用一张表回答「多个开关一起开的效果」。
- **结论：默认之上再叠任何开关都拿不到收益。** `openBatch` 0.882 [0.740, 1.049]、`batchVerify` 0.963、`stepBudget:8` 0.971、`preferPatch` 0.988、更宽并行 1.075、`workspaceMap` 0.802 [0.648, 0.999]、关掉明细律 1.101 —— 每个 CI 都跨 1。收益已经在默认配置里兑现，或者从来就不存在。
- **唯一站得住的杠杆仍是「同一句提醒的时点」**：`d2`（`disciplineAfterSteps: 2`）41 对 **0.695×** [0.565, 0.863]，但分族方向相反（`ledger` 0.506 / `debugfix` 0.619 / `migrate` 0.569，而 `codeimpl` 1.073、`shellpipeline` 1.111）；带闸门的 `d2g` 把 `codeimpl` 收到 0.889、同时削掉 `debugfix` 的收益 ⇒ 池化 0.756× [0.605, 0.939]。两臂按预注册判据仍是 HOLD，默认不放宽。
- **没有行为变更**：只加一个只读分析脚本与文档；默认值、提醒时机、提示词与 1.5.25 完全一致。

## 1.5.25（收回一个测不出来的结论；两个分析脚本补上推理字符）

- **撤回「纪律提醒 1.54×」这个数字**：`LEN3` 把 `LEN_DBG` 那一块（debugfix、7 对、`def15` vs `nodisc`，两臂只差 `discipline:'full' → 'off'`）同题同配再跑一遍，几何比从 **1.790** 变成 **0.648**——方向相反，符号检验两遍都不显著（p = 0.125 / 0.453），块级比值由一两个离群单元格决定（一遍在 `nodisc`，一遍在 `def15`，单格 165 s / 196 s）。所以纪律提醒的**独立份额目前不可判定**，对外只能报池化主效应 0.823×（138 对）。
- **文档据此改口径**：`docs/REFERENCE.md` / `REFERENCE.en.md` 不再说「单独关掉慢 1.54×、是最大单项杠杆」，改成「份额测不出来 + 机制上它确实把推理压短（池化 `reasoning` 字符 0.75×）」。`BENCHMARK.md` 的纪律一节重写成两遍对照表，并写明单块 n=7 不足以定方向。
- **两个分析脚本补上推理字符**：`bench/textshare.py` 现在把转录里的 `reasoning` 部分单独计数（原来只数可见正文与工具参数），`bench/mechanism.py` 的报告多了 `prose / args / reasoning / emitted` 四行。本机样本上推理占模型全部发出字符的 **99.6 %**，可见正文只占（正文+工具参数）的 1.28 %——这解释了为什么提速来自「步数与每步推理量」，而不是「少说话」。
- **没有行为变更**：不含开关、默认值、提醒时机或提示词改动。

## 1.5.24（稳定性修复：被截断的调用身份不再互相冒充）

- **问题**：调用身份的 `argsKey` 有 400 字符上限，超出就截断。两条参数只在尾部不同的调用（最常见：同一个 heredoc 脚本改了后半段、同一个文件改了后面的内容）会得到**同一个身份**。身份被用来判断「这一步有没有进展」——若两条调用的结果长度还恰好相同（无内容哈希时按字符数当摘要），这一步就会被判成「重复调用、没进展」，模型因此收到不该有的催办；复用检查也会把结论记到同一个键上。
- **修法**：`stableKey` 截断时附上整段序列化文本的 32 位 FNV-1a（8 位十六进制），长度仍是 **399** 字符（上限 401 未变）。前缀相同的长调用从此有不同身份；不截断的短调用字节级不变，缓存与旧行为完全一致。
- **本机暴露量**：3 168 个会话、146 506 次工具调用里，48 293 次（33 %）参数超过 400 字符被截断，其中 **782 组**「同一个 390 字符前缀对应 ≥2 种不同完整参数」——这 782 组在旧身份下是同一个键。触发错误催办还要求结果大小相同，所以线上未必发生过，但这条路径现在不再可能。
- 加回归测试：5000 字符同前缀、尾部不同的两个参数必须得到不同身份，且长度仍 ≤ 401。全套 304 个测试通过。

## 1.5.23（打包：把量测脚本一起发出去）

- 1.5.22 的文档让读者用 `bench/mechanism.py` / `textshare.py` / `reminders.py` / `patchshare.py` 复算结论，但 npm 包里只带了 `block_score.py`，从 npm 装的人复算不了。现在这四个只读分析脚本一起进包（都是 stdlib、只读 `~/.dsh/sessions`）。无行为变更。

## 1.5.22（只加量测与文档：机制拆解、纪律份额、同配置重复性；默认行为不变）

- **机制拆解落地成工具**：新增 `bench/mechanism.py`，把 168 对「同 tag、同任务、同 pair、只差 arm」的会话转录逐项数出来（步数、调用数、并行率、只叙述步、prose/参数/推理字符）。结论写进 BENCHMARK：并行的两条口径都是 1.00（即并行**不是**杠杆）；步数 0.83 / 调用 0.79 / 参数 0.84 / prose 0.80 / 推理 0.75，全都落在 0.75–0.84——插件是「每样都少一点」，不是砍某一种输出。转录里推理字符占输出字符的 99.8 %（每会话中位 47.5 k vs 92），所以只盯可见文字优化、天花板是 0.2 %。
- **纪律提醒的独立份额实测**：`bench/arms/nodisc.yml`（与 `def15` 只差 `discipline: 'full' → 'off'`）在 `LEN_MIG` + `LEN_DBG` 上给出 pool **1.54×**（11 对，中位 1.74×；debugfix 单看 1.79×、6/7 对变慢）⇒ 当时把它报成最大的单项杠杆（**这条已被 1.5.25 的 LEN3 复测推翻，见上**）。据此把 `docs/REFERENCE.md` / `REFERENCE.en.md` 里「这条不是提速主要来源」的旧说法改成实测口径。
- **同配置重复性**：`REP2` 把 `migrate`+`debugfix` 用同一套配置再跑一遍。块级 0.791 vs 首遍 0.823（差 4 %），任务级差 15–31 %，单格跨度 0.28×–1.79× ⇒ 「单块/单格差异在 ±20 % 内算噪声」现在有同配置重复的直接证据，不再只是经验宽限。
- **文档口径刷新**：可见正文占「正文+工具参数」字符的比例按最新全量（385 会话 / 3 659 消息 / 4 007 调用）从 1.18 % 更新为 1.19 %。
- **没有行为变更**：默认值（`outputDiscipline:'late'`、`stepBudget:0`、`workspaceMap:false`、`disciplineRepeatAtSteps:0`、`compactRereadHint:false`、`enabled:true`、`gear:auto`）全部未动；这一版只加量测工具与文档。

## 1.5.21（更正 1.5.20 的说法，并把「补丁路径是否存在」变成可复算的证据）

- 1.5.20 的 CHANGELOG 读起来像「补丁编辑恢复复用证据能提速」。实测不是：本机 2 485 个会话里补丁类调用 0 次，这条修复在本机零收益。代码保留（对其他宿主有防御价值、有测试覆盖），说法改成它实际是防御性修复。
- `bench/patchshare.py` 新增：从保留的会话转录里统计补丁类调用占比、以及「首次补丁之后还剩多少工具调用」——下次再有类似「某条路径影响面多大」的猜测，用这个先量，而不是先上结论。

## 1.5.20（补丁类编辑从 diff 头读目标文件；本机实测 0 命中，属防御性修复）

- **补丁编辑不再拖垮整个会话的复用提示。** `apply_patch` / `patch` 的写入目标在补丁正文里，参数里没有 `path` 字段；1.5.19 把「定位不到目标的写入」如实标成不确定，代价是**会话里只要用过一次补丁，后续所有检查复用建议都关掉**——而补丁正是最常见的改文件方式，等于最常见的路径拿到了最少的那部分提速。现在从 `*** Update File:` / `*** Add File:` / `*** Move to:` / `+++ b/<path>` 头里读出目标文件并照常打指纹；只有补丁头里一个路径都读不出来时才保持不确定（保守方向不变）。`*** Delete File:` 不计入（文件已消失，打不了指纹）。
- 只对补丁形状的工具（`apply_patch` / `patch` / `apply_diff` / `edit_patch`）扫正文：整文件写入的 `content` 里可能恰好含 diff 文本，那种情况仍以它自己的 `path` 字段为准。
- **本机证据：这条路径在本机不存在。** `python3 bench/patchshare.py --limit 0`（2 485 个本地会话、25 508 次工具调用）显示补丁类调用 **0 次**：DSH 宿主暴露的改文件工具是 `write` / `edit`，`apply_patch` 这类名字来自别的宿主。所以 1.5.20 不给本机带来任何提速，它只是把「目标读不出来就永久关掉复用」这个过宽的惩罚收窄到补丁形状的工具上；万一宿主换成带补丁工具的实现，会话不再因为用一次补丁就丢掉全部复用提示。
- 附：`bench/block_score.py` 随包发布（74 行，纯标准库），按 `hasSessionIds`/`green`/`valid`/`leak` 过滤后可复算每个块的几何、中位、胜场与符号检验。

## 1.5.19（稳定性发布：面板三项真正可读写、检查复用证据不透支、取消三态、实验臂与判据对齐）

> 1.5.18 在 npm 上只有文档改动（README 结论先行），代码修复都在 1.5.19；升级请直接装 1.5.19。

- **面板三项设置在官方宿主上真正可读可写**：官方的 `describe()` 只投影 volatile 字段，`outputDiscipline` / `stepBudget` / `workspaceMap` 原先非 volatile，于是面板只能画内置默认（`late` / 0 / off）、写入被宿主以 `Config field "…" is not volatile` 拒绝。三项声明 `.volatile()` 后 describe 投影、write 接受、写后读回同一值；默认值不变，实际写入仍只落在本会话的 `sessions` 条目。
- **检查复用证据按检查独立，且要求路径可信**：`reuse-check` 的命中判定从「同一工具 + 同一参数」收紧到「同一条检查 + 同一份产物」，只有参数里出现可信路径、且该路径在本轮没有被改写过时才算复用；否则只按无进展提醒，不给「结果可复用」的结论。
- **取消三态可信**：取消结果区分「已请求取消」「已确认进程退出」「未确认」三种，只有第二种能当失败结论；后两种都把「进程可能仍在跑」写回上下文，不再把未确认的取消报成已退出。
- **`disciplineEarlyCalls` 的决策在第 1 步冻结**：首次请求时确定本轮的提醒步数，同一轮内不再随步数重算，避免同一轮里提醒时点漂移。
- **输出预算按本轮步数**：`outputBudget` / `outputBudgetAfterSteps` 用本轮步数结算，不再读跨轮累计值。
- **父 signal 监听器清理**：看门狗在调用结束（成功、失败、取消）后移除挂在父 `AbortSignal` 上的监听器，长会话不再累积。

- README 顶部换成结论先行：先说清楚这是 DeepSeek Harness 自己的快速模式、能控制什么（执行策略：减少冗余回合与输出，不改模型与思考强度），再给四行实测——默认 0.72× 总耗时 / 0.74× 输出 token / 0.71× 步数；提醒提前到第 2 步在 `debugfix` 0.535×、`ledger` 0.506×、`migrate` 0.751×，13 对合并 0.625×、输出 0.614×；第 3 步在 `migrate` 到 0.509×、输出 0.528×；短改修与数据整形不承诺。数字全部取自 BENCHMARK.md 已有块，没有新增实验。
- 表述统一：正文里的「墙钟」改成「耗时」（术语表词条同步重写），不再以「墙钟」作为对读者的解释词；结论只讲「快多少、token 少多少」。
- 市场卡面文案同步改写（en / zh）：去掉「墙钟」措辞，速度口径与上面的全池 0.823×（12 块 138 对）一致。
- 英文 README 同步。
- README / REFERENCE 的机制表述与实测对齐：新增「三条边界」（承诺不改模型与 `reasoningEffort`；收益来自冗余回合与输出 token 下降，不归因于删旁白；质量以隐藏测试与真实评测为准，不宣称行为完全等价）。旁白占比有了可复算口径：新增 `bench/textshare.py`（transcript 里 `assistant/message` 可见 text 字符 ÷（text + `tool/call.arguments` 字符），不含 reasoning、工具返回与用户消息），def15 全量 341 会话 / 3,327 消息 / 3,569 调用，正文 51,366 / 参数 4,315,096 = **1.18 %**（中位 0.98 %、p90 4.08 %），`python3 bench/textshare.py --limit 0` 复算。
- 面板默认值与 1.5.17 相同；`compactRereadHint` 默认关闭，不在本版默认行为内。
- **验证**：`node --test test/*.test.js` **302 pass / 0 fail**（新增面板读写的宿主投影/校验用例、`reuse-check` 证据语义用例、取消三态与监听器清理用例）；`ui-copy-lint` clean、`zh-lint` 0 error（37 warning 既有）、`style-lint` 通过；`bench/verify-arm-reminders.mjs` 机制 10/10、臂×场景 8/8 通过。

## 1.5.17（面板四行、按会话隔离、网关用量兼容）

- **四个开关按会话隔离（核心修复）。** 面板此前写的是 `outputDiscipline` / `stepBudget` / `workspaceMap` 三个共享字段，而设置列的写入只接受 volatile 字段：`settings.write` 对这三个路径直接抛 `Config field "<path>" is not volatile`，所以按钮点了不生效。现在所有选择都写进 volatile 的 `sessions` 列里本会话的条目（`{enabled, gear, discipline, stepBudget, map, ts}`），面板与闪电只写自己那条，不碰共享的 `enabled`/`gear`：A 会话「更快 + 少写过程」与 B 会话「更快 + 不写过程」同时成立，没点过的会话沿用配置默认值。条目上限 200，按 `ts` 淘汰最旧的。
- **面板改成四行、名字改成说人话的。** 「更快」（开关本身）、「少写过程」、「最多几步」（`15`/`30`/`不限`）、「先列文件清单」；底注「只影响本会话」。四行都带 tooltip 说明后果，只读 / 写入中禁用，写失败回滚并保留原因。配置项描述同步注明面板名称与「这是没有自己条目的会话的默认值」。
- **自建网关与官方 API 的用量口径统一。** 新 `src/usage.js` 把上游的 token 拼写归一到一组数字：harness 的 `inputTokens/outputTokens/reasoningTokens/cacheReadTokens/cacheWriteTokens`、OpenAI Chat 的 `prompt_tokens/completion_tokens`（含 `prompt_tokens_details.cached_tokens`、`completion_tokens_details.reasoning_tokens`）、新版 OpenAI 的 `input_tokens/output_tokens`、Anthropic 的 `cache_read_input_tokens/cache_creation_input_tokens`、Google 的 `candidatesTokenCount`；usage 挂在 `usage`、`message.usage`、`response.usage` 下都能找到，完全不报 usage 时按 0 处理。收益是真实的：此前只在 `outputTokens` 上取数，走 OpenAI 兼容网关的会话输出量恒为 0，调度器的输出预算和计量报告都会把「网关没按这个拼写报」误读成「这一步没输出」。
- **测试**：`node --test test/*.test.js` **289 pass / 0 fail**。新增 `test/usage.test.js`（9 个方言用例）、`test/metrics.test.js` 的 OpenAI 网关行与「不报 usage 仍计时」行，以及 host 侧 `test/session.test.js`（4 个）——两个会话在同一列里各记各的、没条目的会话用默认值、清单只发一次、提醒按各自开关下发。
- **文案与检查**：`ui-copy-lint` **clean**（此前 21 error / 2 warning，全部是注释里的 `host` 与 store 初始化的状态点亮出；注释改成说行为、状态字段逐个加说明后清零），`zh-lint` **0 error**（37 warning 为既有），`style-lint` 通过。

## 1.5.16（只改文档与判据脚本，代码与 1.5.15 相同）

- `BENCHMARK.md` 补两块实测记录：`D2B_DBG`（`d2` 在 debugfix 上的独立复现：7 对几何 0.717×、中位 0.864×、5/7 更快、符号检验 p=0.45、`steps_geo` 1.118×、`out_geo` 0.787×）与「提醒之后输出会不会重新变长」的预注册前提检查（`def15` 有反弹，`d2g` 没有；fixture 里 assistant 可见文字中位 0–200 字符，提醒动的是工具参数与 reasoning 的 token 量）。
- `D2G` 判据脚本补上判据 5 的执行：`d2gscore.py` 调 `d2gsign.py` 逐臂断言提醒下发步位，`D2G_IMPL` 的 `d2g` 实际在第 3 步（预注册要求第 4 步）判 FAIL，结论仍是 HOLD、默认第 4 步。
- `DR8_MIG` 的判据落成可执行脚本：`dr8sign.py`（dr8 命中 ≥1、`def15` 命中 0，否则记「未测到」）与 `dr8score.py`（预注册六条，含 `out_geo ≤ 0.95`、赢面 4/6、质量不倒退）。
- 无配置项变化，默认 `disciplineRepeatAtSteps: 0` 行为不变。

## 1.5.15（右键面板第三行；`D2G` 预注册判据判 HOLD）

- **面板第三行「工作区清单」**：写共享字段 `workspaceMap`（默认 `false`），三行面板第三项，`readonly` / 写入中禁用，写失败回滚。与另两行一样只写共享字段、不带 `sessions`，不碰按会话的开关状态。`client.test.js` 新增一行测试覆盖「默认关、写入不带 `sessions`、写后按 host 值重绘、再点关掉」，全套 **270 pass**。
- **文案与检查**：`ui-copy-lint` **21 error / 2 warning**（与 HEAD 逐行相同，全是文件级 React props 误报），`zh-lint` **0 error**（37 warning 为既有），`style-lint` 通过。
- **`D2G` 预注册判据：HOLD，默认维持第 4 步**。21 对（`D2G_IMPL` 8 / `D2G_DBG` 7 / `D2G_MIG` 6）里 14/21 更快，合并几何 **0.756×**、中位 **0.719×**、总时长 **0.685×**，质量 0 回归；但第 2 条在 `debugfix` 上是 **0.903×**，没进预设的 **≤ 0.80**。按预注册规则，其余判据全过也不能挑指标发版。分块数字：`IMPL` 0.889× / 5-8 更快 / 步数 0.753×；`DBG` 0.903× / 4-7 / 步数 0.987×；`MIG` **0.495×** / 5-6 / 步数 0.964× —— 迁移类省得最多，调试类基本打平。离群点：`codeimpl p6` 2.093×、`migrate p3` 1.601×、`debugfix p5` 1.368×，都在「本来就没有探索空间」的短直线上多走了几步。
- **交付审计的限度**：报告里的 `delivery audit` 按固定句式找完成通报，命中 0/16、2/14、0/12，这个探针在本套任务上不可用；质量结论只引用 `exact` / `green`（全部 1/1）与判据 5 的 0 个坏格。

## 1.5.14（`D2G` 判据仍在跑，未据此改默认）

- **闪电右键面板（新）**：只有两行，都是共享设置、下一个回合生效 —— 「更少中间输出」（写 `outputDiscipline`，`late` ↔ `false`，默认开）与「步数上限」（写 `stepBudget`，循环 `0 / 15 / 30`，默认 `0` 不限）。面板只读取值、只挑一个已测过的安全上限，没有暴露 `disciplineAfterSteps`、`shapeGate`、`finishLineSteps`、`shellParallel` 这些未证实或已淘汰的旋钮。步数上限的写入不带 `sessions` 字段，避免动到按会话的开关状态；`client.js` 的 `write()` 因此加了 `{ touchSession: false }` 路径。文案按界面规则压到标签/值/后果三件，`ui-copy-lint` 与 HEAD 同为 21 error / 2 warning（全部是文件级 React props 误报），`zh-lint` 0 error。
- **新旋钮 `disciplineEarlyCalls`（默认 `0`，行为不变）**：与 `disciplineAfterSteps` 配合，给一个正数 N 时，提前的压缩提醒只对「进入当前步之前本回合已发出 ≥ N 次工具调用」的回合生效，否则回落到内置第 4 步。计数扣掉当前步自己的调用（`turnCalls - stepCalls`），一步不能先付钱再买票。依据是 `D2_IMPL` 的失败与三家族的信号分离（见下条）；`0` 走的是 1.5.12 的逐字节相同路径。判据块 `D2G_IMPL` / `D2G_DBG` / `D2G_MIG`（21 对）正在跑，**本版不对它的效果作任何声明**。
- **`disciplineAfterSteps: 2` 的预注册判据块 `D2_IMPL` 判为 HOLD，默认维持 4**：`codeimpl` 8 对（16 行）几何 **1.073×**、中位 **1.129×**、3/8 更快、步数几何 **1.232×**（提前反而多走步）、范围 0.374–2.688×，第 2 条判据三项全挂。同一时点在 `debugfix` 是 0.535×、`migrate` 是 0.751× —— 方向按家族翻转。`codeimpl` p4 的调用序列：`def15` 5 步 4 次（`write` → python 校验 → `ls && py_compile` → `rm -rf`），`d2` 8 步 7 次（多出 `pwd && ls`、两次同样的 python 校验、`ls && find`）：在本来就最短的直线上，提前提醒**加**了探索与重复校验。合并 21 对的 0.768× / 0.787× 好看，但不能压过判据 2，不引用为发版理由。
- **提前提醒的分离信号（免费、第 2 步之前可读）**：基线 `def15` 第 1 步工具调用数 —— `debugfix` 7/7 是 2、`migrate` 6/6 是 2、`codeimpl` 8/8 是 1。单文件实现任务第 1 步只发 1 次调用，多文件/调试任务发 2 次，所以「调用密度」就是「这个回合会不会走远」的机械代理。
- **`batchVerify` 淘汰（保留默认 `false`）**：`bvl15` 16 对几何 **1.188×**，把验证合并成一次并没有省下回合，反而多了一次「等这批改完」的往返。
- **`disciplineRepeatAtSteps`（默认 `0`，未证实）**：同一句话在更晚一步再提醒一次，逐格数据见 `BENCHMARK.md`，默认不发。
- **机制拆解入档 `BENCHMARK.md`**：调用/步 90.7 % 是单调用（`{1:479, 2:43, …}`）⇒「合批」这条杠杆已关闭；bash 里 inspect 占 0.171；助手文本只占输出字符 1.2 % ⇒ 文本不是杠杆；`D2` 省的是 `edit` 33→7（−79 %）与回头 `inspect`（`migrate` 63→41，−35 %），验证次数两臂 7/7 都在；每步时长几何 DBG 0.731× / MIG 0.828× 是「调用变少」的后果，不是第三机制。

## 1.5.12

- **合池口径定为 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）。
- **新旋钮 `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` 是它的消融臂；数据尚未跑出，本版**不对提速幅度作任何声明**。
- **`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）说明它不是稳定杠杆。
- **`openBatch` 淘汰（保留默认 `false`）**：合并 16 对几何 1.022×、中位 1.110×、7/16 更快 —— 空到负，W6 的 0.77× 再未复现。
- **延迟输出预算帽 `BUDL` 为空**：每家族几何 `debugfix` 1.228×（两对都空）、`codeimpl` 0.983×、`ledger` 0.956×，合并 6 对 1.048×。`outputBudget` / `outputBudgetAfterSteps` 维持 `0`。
- **`finishLineSteps` 的步数不是杠杆**：`fl6` 臂把收尾提醒提前到第 8 步，4 对里步数合计 49→33（**0.674×**），但墙钟 **0.958×**（4/4 里 1 对更快）—— 步数与墙钟脱钩，所以「砍步数」不能当提速证据。
- **空构造对照校准了判定门槛**：同一个默认臂对同一个默认臂跑 9 对（两边配置完全相同），几何 **1.146×**、中位 1.151×、范围 0.889–1.342×、7/9 在 1 以上。结论：单家族 n=2 的胜负全是噪声，只有超过 ±20 % 的差才算机制；偏差是方向性的（后跑的一臂更慢），来自位次与负载。
- **测试**：263 个用例全绿（`node --test test/*.test.js`，10.5 s），新增 `disciplineAfterSteps` 的「按配置步数提前触发、`0` 回落到内建 4」用例，以及 `batchVerify` 的「第二次未验证写入才触发、每回合一次、新回合可再触发」用例。
- **版本**：`1.5.12`。`1.5.11` 是元数据/版本对齐发版（无行为变更），未单列条目。

## 1.5.10

- **接线审计（先说结论：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`）防回归。
- **复用提醒不再基于猜测（`reuse-check` 重写）**。旧实现把任何“带参数的成功调用”都记成一次通过，靠一张正则白名单判断写入；`python3 mutate_state.py` 这种真会改文件却不匹配任何一条的命令因此不会被判成写入，提醒于是会说“没有相关变化”——那是没有证据的断言。现在：只把**识别得出的验证命令**（`npm/pnpm/yarn/bun test`、`pytest`、`node --test`、`go/cargo test`、`tsc`、`eslint`、`ruff`、`mypy`、`make test|check` 等）记成通过；`shell` 命令按 `readonly` / `verify` / `unknown` 分类，`unknown` 与写入同样作废已存结果；多段命令（`&&`、`;`、`|`、换行）除非每一段都能证明是只读或验证，否则算 `unknown`；对写过的文件记 `{mtimeMs,size}` 指纹，发提醒前重新 stat，指纹变了或 stat 失败就不发并作废；验证失败会撤掉它对上的那次通过。提醒文案只陈述已证明的事实。
- **看门狗取消后确认进程真的退出**。旧版取消后只等一个 250 ms–5 s 的窗口就返回失败，`quiesced: false` 只进了日志，模型侧看到的仍是“已经结束”的口气。现在是两段有界等待：先等收敛（随预算缩放），仍在跑就再等一段退出确认窗口，`settleMs + confirmMs` 由 `confirmWindowFor()` 夹在 15 s 以内；结果同时记 `quiesced`（是否在窗口内收敛）、`confirmed`（是否确认退出）、`waitedMs`。未确认时失败文案明确说“退出未被观察到、进程可能还在跑、不要动它可能写的文件”，并把 `pgrep -fl` / `ps -o pid,stat,command -p` 作为下一步给出去；日志行也带上 `confirmed` 与 `waitedMs`。1.5.5 那条“5 s 窗口”的描述被本版取代。
- **新增 `scheduler` 开关，为干净消融提供第四条臂**。此前输出克制纪律被 `gear.directive` 夹住，而所有提醒也共用同一个通道，于是“只开纪律 / 只开调度”无法表达，四臂消融根本跑不起来。`scheduler: false` 现在只关掉提醒通道，宽度与看门狗预算照旧；新臂 `bench/arms/disc-only.yml` = 默认配置 + `scheduler: false`（纪律仍在、零提醒），配上已有的 `disc-off.yml`（提醒仍在、纪律关）即可在**同一区块**内把 off / 只纪律 / 只调度 / 全开四条臂交错跑完。该四臂数据尚未跑（机器 load 9–10，不满足 ≤ 6.5 的成块前提），跑完才会把数字写进 `BENCHMARK.md`。
- **测试**：260 个用例全绿（`node --test`，10.7 s），含新增的接线传播、复用提醒分类/指纹/撤回、两段等待与 `confirmWindowFor` 边界、以及 `scheduler:false` 静默但保留宽度的用例。
- **版本**：`1.5.10`。以上三项都是 1.5.9 评审指出的可靠性问题，不是新增提速机制；对外速度口径仍是 1.5.9 的 **0.825×（132 对）**，且必须同时给逐格中位 0.813×。

## 1.5.9

- **合池口径再扩一块（无行为变更，纯数据与文档）**：新增 SG2 块（`shellops`/`debugfix`/`perfrefactor`/`ledger` × 1 对 × 3 臂，4 对 off 锚定）后，合池变成 **9 块、132 对、10 个家族**：总墙钟 **0.825×**（省 18 %）、输出 token 0.822×、步数 0.809×、逐格中位 0.813×、132 对里 89 对更快；2000 次自助重抽的总比 95 % 区间 **[0.747, 0.914]**、中位区间 [0.710, 0.883]。上一版对外写的 0.809× / 省 19 % 是 128 对口径，**本版按更全样本改口径**，不是行为变了。
- **SG2 自己就是一块反向样本**：4 对合进来是 **1.235×（2/4 更快）**，其中 `ledger` 2.024×（输出 34.1k vs 18.7k token）、`perfrefactor` 1.427×，正是把「输出会变长的家族」如实计进分母的结果。`sg15` 臂在同块比 `def15` 更差（1.295×、中位 1.392×、1/4 更快），**继续默认关**，不进默认机制。
- **逐家族稳定性重算**：稳的仍然只有 `codeimpl`（13/15 更快、p = 0.007、中位 0.709×）和 `shellpipeline`（10/12、p = 0.039、中位 0.844×）；`ledger`（均值 0.908×、四分位 0.44–1.29×）、`debugfix`（均值 1.287×、输出 1.29×）、`perfrefactor`（均值 1.140×）在扩样本后**平均在变慢**，文档与 README 的「什么时候别开」改按这三类写。
- **DBG2 定向复测（未进合池）**：`debugfix`/`perfrefactor`/`csvpipe` 各 3 对，只跑完 7 对（`csvpipe` 第 3 对卡在 18 分钟未收），且全程机器 load 5–8、墙钟被拉长 2–4 倍，**不满足合池前提**，只作方向参考：`debugfix` 1.021×（三条 1.06/1.06/0.94）、`perfrefactor` 1.207×（1.29/1.12）、`csvpipe` 1.783×（一条 3.30×、一条 0.27×，方差大过机制）。结论与混池一致：这三类默认不承诺收益。
- **复现前置**：`python3` 必须是能 `import yaml` 的解释器（本机 `/usr/bin/python3` 因 Xcode license 返回 rc=69）；`bench/pool.py` 的区块清单已含 `SG2`。

## 1.5.8

- **合池口径再收紧（无行为变更，纯数据与文档）**：新增 PPX2 块（4 家族 × 2 对 × 3 臂，8 对 off 锚定）后，合池变成 **8 块、128 对、10 个家族**：总墙钟 **0.809×**（省 19 %）、输出 0.806×、步数 0.806×、逐格中位 0.813×、87/128 更快，2000 次自助重抽 95 % 区间 **[0.734, 0.894]**（中位 [0.706, 0.885]）。上一版的 0.798× / 120 对是被一块偏乐观的数据抬高的，这版把 PPX2 一起算进去。
- **`preferPatch`（`pp16`）判定为不成立**：PPX 单块 4 对给出 0.426×，看着像强机制；同块复测到 12 对后是 **1.035×**（6/12 更快、输出 1.056×），PPX2 那 8 对单独看是 1.193×。结论：单块好账面是格子运气，`preferPatch` 保持默认关闭。这一条写进 `BENCHMARK.md`，避免后来的人重复踩。
- **逐家族表加了「不可跨块引用」的告示**：`debugfix` 混池中位 1.183×，但在 PPX2 块里是 0.651×（2/2 更快）；`perfrefactor` 混池 0.890×、PPX2 里 0.687×。方向会翻，所以对外只报合池，不报「某家族必然快多少」。
- **`bench/pool.py` 区块清单**加 `PPX2`、`SG2`（后者落地后才参与合池），文档补了「`python3` 必须是可用解释器」的复现前置（本机 `/usr/bin/python3` 因 Xcode license 直接返回 69，会让隐藏测试静默失败）。

## 1.5.7

- **对外口径修正（撤回 26–28%）**：把**所有**有 `off` 锚的干净块合池重算——7 个块、120 对有效配对、10 个任务家族：总墙钟 **0.798×**（省 20%）、输出 token 0.806×、步数 0.800×、逐格中位 0.829×、81/120 对更快。之前公开的 26–28% 来自三个被挑出来的块（1.5.1 0.72×、W5 0.73×、W7 0.73×，65 个有效单元合池 0.74×），样本扩大后不再成立，**撤回**；对外只说约 20%，并同时给逐格中位 0.83×，避免把总量当逐任务承诺。行为无变化，只改数字与文档。
- **分场景读法写进文档**：`codeimpl` 0.608×（省 39%，13 对 12 对更快）、`stategraph` 0.708×、`migrate` 0.748×、`shellpipeline` 0.749×、`shellops` 0.803×、`codebase` 0.808×、`ledger` 0.811×、`csvpipe` 0.982×（打平）、`perfrefactor` 1.077×、`debugfix` 1.245×（更慢）。后两者是 1–5 步、5k token 以内的小改修，没有可省的重复复验，一次通知的注入成本（≈0.66–0.92 s）收不回来；默认仍按请求形状调度，不按任务名做特殊处理。
- **成本模型复算进仓库**：`wall = a + b·out + c·steps`，OLS（W8b 的 `off` 臂，n=19）`a=-8.8 s`、`b=13.48 s/1k`、`c=1.621 s/step`、R²=0.922；NNLS（`a=0`）`b=13.59`、`c=0.739`、R²=0.921 ⇒ **输出项占墙钟 ~97–98%**，步数项 ~3%。跨 task 外推会高估 1.5–2.1×，只能读结构比例。
- **新增 `bench/pool.py`**：一条命令复算上面这些合池数字（只读 `bench/results/*.jsonl`，不跑 bench、不写文件），并把「有效格」判据固定成 `exit == 0 && steps >= 1 && out > 0 && exact && !leak`（`green` 不算计量单位）。
- **上限结论不变**：省 40% 墙钟需要省约 40% 输出（输出占 97%），现有约束内 0.67× 不可达，见 `BENCHMARK.md`。

## 1.5.6

- **发版前复核块 REL16**：默认对 `off` 再跑 5 家族 × 1 对（10 个单元），两臂 **10/10 全绿、逐任务通过数完全相同**、无 leak；墙钟合计 **0.967×**、逐格中位 0.999×、3/5 格更快。单对 n=5 只作复核，不替换当时的 26–28% 总口径（该口径已在 1.5.7 撤回）；原始数据 `bench/results/suite-REL16.jsonl`，分析与读法见 `BENCHMARK.md`、`README.md`。

## 1.5.5

- **市场预检修复（续）**：peer 范围再改成 `>=3.18.5-rc.1 <4.0.0-0 || >=4.0.0-rc.1 <5.0.0-0`。预检规则要求范围里显式出现 `||` 与预发布段，单段范围即使写了 `-rc.1` 也判 error。行为无变化。

## 1.5.4

- **市场预检修复**：`peerDependencies["@deepseek-ai/schemastery"]` 从 `^3.18.5` 改为 `>=3.18.5-rc.1 <4.0.0-0`。原来的范围会静默排除预发布的宿主构建，市场预检判为 error；新范围同时覆盖稳定版 3.18.5 与 3.18.5-rc 系列。行为无变化。

## 1.5.3

- **发布到 npm**：`dsh-fast-mode@1.5.3` 已在 registry 上（`npm i dsh-fast-mode` / 插件管理器目标填 `dsh-fast-mode`），30 个文件、含 `cordis.patch.yml` 与 `client.js`。GitHub 安装方式（`github:regrevia/dsh-fast-mode`）照旧可用。
- **插件市场**：仓库内新增 `market/index.json`（format 1 单插件索引，已用宿主 `@deepseek-ai/dsh-plugin-market-index` 的解析器验证通过），可作为「插件 → 发现 → 索引源」的自托管源；同时向精选列表 `awesome-dsh-plugin` 提了收录 PR [#9650](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin/pull/9650)，合并后 dshmarket 与站点自动收录。
- **对外口径**：总量约快 26–28%（三块独立批次 0.72×/0.73×/0.73×，合计 65 个有效单元）；「典型一格快 36%」已撤回，40% 不支持，见 `README.md` 与 `BENCHMARK.md`。

## 1.5.2

- **新增实验旋钮 `workspaceMap`（默认关闭）与 `mapEntries`（默认 60）**：会话第一次工具调用之前，harness 把工作目录走一遍（深度 2、≤`mapEntries` 条、跳过 `node_modules`/`.git`/`dist`/`build`/`coverage`/`__pycache__`/`.venv`，正文 ≤1.4 kB），把「相对路径 + 字节数」和看得见的测试入口作为一条通知交给模型。它不结束回合、不改完成判定、不动 `reasoningEffort`、不加请求字段；它只是把模型自己会用 `ls`/`find` 换来的同一份清单先给一次。实现 `src/map.js`，接入 `index.js` 的 `agent/pre-step`，配置见 `src/config.js`、`src/effort.js`。
- **为什么默认关闭（两轮实测，23 个有效单元）**：W10 与 W11 两块把 `def15` 与 `def15 + workspaceMap` 同块对照。合并 23 个单元：**墙钟 0.927×、输出 0.896×、步数 1.099×**，逐格墙钟中位 **0.791×**、13/23 格更快、隐藏测试零红。方向是对的（输出确实少 10 %），但**步数反而多 10 %**，且反向尾部很重——`stategraph` 与 `codeimpl` 的个别格 1.4×–1.77×，两臂 p90 从 369 s 变成 559 s、最慢 510 s 变 714 s。按「长任务不能变慢」的门槛，它不够格进默认，留作旋钮并公开数字。
- **修掉一个会让新机制静默失效的缺陷**：运行目录在会话创建之后才放 fixture，第一步读工作目录可能读到空目录；旧写法在这种情况下也把「已下发」标掉，于是该会话再也不会拿到清单。现在只有真正构建出清单才记录已下发（`index.js` 的 `mapSent.add` 前判空），MAPDBG4 单格验证：`built=0`、`built=0`、`built=291`，会话里恰好一条 `Auto map:` 通知。
- **投递审计工具化**：`bench/audit-delivery.py --tag W10 --needle "Auto map: one inventory"` 一条命令按「任务 × 对 × 臂」列出文案命中次数。W10 结果是 `map15` 10/10 命中、`def15` 0/10 —— 对照臂干净、处理臂每格恰好一次，所以那一块的差值不是「提醒没发」的噪声。协议见 `bench/README.md` 第 15–17 条。
- 数字与逐格表见 `BENCHMARK.md` 的「筛选块 W10」与「复现块 W11」；原始结果 `bench/results/suite-W10.jsonl`、`bench/results/suite-W11.jsonl`，`python3 bench/report.py --files bench/results/suite-W11.jsonl --baseline def15` 可原样复算。

## 1.5.1

**干净重基线：10 家族 30 个单元，总墙钟 0.72×、逐格中位 0.64×，有效单元 29/29 全绿。默认配置不变。三块独立复现到 0.72× / 0.73× / 0.73×。**

这一版没有改默认行为，改的是**测量**：把两个会污染数字的缺陷修掉，然后用当前默认（`enabled: true` 的出厂配置，即 1.5.0 的 `def15`）重新跑一遍。1.5.0 承诺的「不按步数收尾」没有变。

- **干净重跑（60 次运行 / 30 个单元 / 10 个家族）**：`off` 3434.7 s / 591 244 输出 token / 390 步；`def15` 2472.5 s（**0.72×**）/ 439 708 token（**0.74×**）/ 276 步（**0.71×**）。逐格墙钟中位 **0.64×**、均值 0.84×、90 % 区间 [0.59, 0.79]、30 格里 24 格更快；输出 token 逐格中位 0.69×。分家族从 `migrate` 0.45×、`shellops` 0.57×、`codebase` 0.55× 到 `perfrefactor` 1.18×、`csvpipe` 1.12×——短任务本来就没有可省的重复验证，收益不稳定。
- **质量门禁：有效单元 29/29 全绿**（30 个单元里 1 个 `debugfix` p1 的 `off` 因会话启动崩溃作废，不算红）：隐藏测试在 agent 看不到的临时副本里执行，两臂通过率都是 1.000，没有「基线绿而该臂红」。有限任务上的结论，不是质保证明。
- **判据收紧为「有效单元」**：从 1.5.1 起，一个单元只在 `exit == 0 && 步数 ≥ 1 && 输出 > 0 && 判分完成 && 未泄漏` 时计数；崩溃行整格作废并点名。旧口径会把崩溃当成「0 步的红」或「输出 0 的极快运行」，两种都会污染比值和门禁。W3/W5/W7 的准确说法因此是 29/29、19/19、17/17，而不是 60/60。
- **三块重复现 + 两个机制被证伪，一个机制撤回为「未测」**：W5 同块对照 `def15` 0.73×；W6 筛查里 `openBatch` 0.77×，W7 独立复测回到 0.91×、对默认头对头 1.136×（W7 会话里确认提醒真的下发过，淘汰成立）；W7 再复现 `def15` 0.73×（三块合并 65 个有效单元 0.74×）。同一块里的 `batchVerify` 臂记到 0.84×，但事后逐会话检索发现它 20 个会话**零下发**，那个数字测的不是机制，所以 `batchVerify` 从「已证伪」撤回到**未测**，默认仍关、修复后重测。三个未晋升的机制都留在 `bench/arms/` 作消融臂。
- **W7 的 7 行崩溃已点名**：`stategraph` p2 三臂、`codebase` p2 的 `def15`+`open15`、`shellpipeline` p2 的 `off`+`def15`，全部 `exit=1` / 1 步 / 0 输出 / ~80 s，工具未执行、产物为空，与机制无关。
- **对照块 W8b（60 次运行 / 20 个单元）复现逐格中位**：`def15` 对 `off` 逐格中位 **0.717×**、几何均值 0.819×、19 格里 13 格更快，输出逐格中位 0.823×、步数逐格中位 1.000×；这一块总量只有 0.934×（反向尾部更重），所以总量与逐格中位要一起看。`ledger` p2 的 `off` 行泄漏（读了隐藏测试）整格作废，**59 个有效单元零红**。
- **新增可选 `preferPatch`（默认关闭）**：一次 ≥ 2 kB 的整文件写入之后，下一步发一次「改已存在的文件时只发改动的行」的提醒。文件结果逐字节相同，所以这条只谈形式，不要求重做、也不提前收尾；每轮最多一次。
- **修掉一个提醒投递缺陷**：同一步里调度结果被多个钩子读取，只有其中一个会真正投递提醒，其余只读钩子也调用了同一个一次性提醒函数，把提醒标成「已用」却从未显示给模型。W8b 的 `pp16` 臂因此 20 个会话里**零下发**，那一臂的数字作废、不计入任何结论（`pp16` 对 `def15` 的中位差是噪声）。修复为只读调用传 `{emit: false}`，提醒只在真正投递时消耗；单格验证同格修复前 0 次下发、修复后 1 次下发，另有回归测试。
- **修掉的测量缺陷**：① 运行目录移出仓库（`$TMPDIR/dsh-runs/`），产物里出现 `hidden/`、`ref/`、`bench/suite/`、`MANIFEST.json` 字样的行标 `leak` 丢弃；② 指标按 `session id` + 开跑前差集取行，不再按字节偏移；③ 判分在 `copytree` 出来的临时目录里做（`shellops` 会把产物目录 `chmod` 成只读，旧写法直接 `EACCES` 中断批次）；④ `bench/MANIFEST.json` 用整套 fixture 的 sha256 校验，改过 fixture 就拒绝开跑。
- **查清 W1 那格「红」是题面歧义**：`ledger` 的 `test_round_half_up` 用 `offset: "0.005"` 要求 Decimal 精确参与四舍五入，题面只写「四舍五入到分」+`parse_amount` 抛 `ValueError`，两种读法都成立。参考实现 22 条断言全绿，说明不是快速模式降质。题面已补明规则里的系数/偏移按 `Decimal` 原样解析、只有最终结果取两位小数；fixture 摘要 `2699cd64…` → `2d7546f8…`。
- **没变的约束**：不改 `reasoningEffort`、不加请求体字段（`service_tier`/`max_tokens`）、不按步数结束回合、不用计数器决定“做完了没有”。快的地方仍然是调度器省下来的重复验证与空转，不是压缩模型思考。
- 数字与单元级表格见 `BENCHMARK.md` 的 `1.5.1`；原始结果 `bench/results/suite-W3.jsonl`，`python3 bench/report.py --files bench/results/suite-W3.jsonl --baseline off` 可原样复算。

## 1.5.0

**默认不再按步数收尾：什么时候结束由任务状态和完成证据决定，不由计数器决定。**

1.4.4 把「第 10 步提醒收尾」设成默认，速度确实上去了，但它对**还在推进的长任务**同样开火：第 10 步一到就要求停止扩大范围，等于用一个和完成度无关的数字替模型决定“做完了没有”。1.5.0 取消这条默认。

- **默认无收尾线、无步数上限**：`finishLineSteps` 与 `stepBudget` 默认都是 `0`（关闭）。只有用户自己把某个值设成正数时，才各发一次对应提醒；`0` 就是关，不再回落到旧默认值。
- **无进展只是诊断，不是判决**：连续若干步没有产出新结果时，只发一次「换方法」提醒（后台跑+轮询、把脚本写到文件再执行、缩小到一条路径），**不结束回合**。任务做完就正常报告，不会被提醒逼停。
- **检查复用绑定产物版本**：同一份产物上逐字重复一条已通过的检查，才提示「这个检查在当前文件上已经通过，结果可复用，把这一步花在剩下的工作上」。任何写操作、编辑、或改写类 shell 命令都会让该版本失效，旧结果立即作废；逐字重复的检查命令本身不会让自己失效（否则 `npm test` 永远无法触发这条提示）。
- **新增 `outputBudget`（默认关闭）**：本轮累计输出 token（上游 `usage.outputTokens`，不是估算）达到该值，调度器发一次「收掉当前闭环、跑一次证明它的检查、汇报」，每轮最多一次。它盯的是真实解码量而不是步数——三步写出三万 token 的任务很贵，二十步只写两千 token 的任务不贵；但 token 同样是成本而非完成度证据，所以默认 `0`，只作对照臂。对照臂 `outputBudget: 8000` 在 `codeimpl`/`migrate`/`codebase` 各 3 对上跑过（9 个完整单元、27 次运行全绿）：总量 0.68× 墙钟 / 0.70× 输出，其中 `codeimpl` 输出再降 23 %（0.60× → 0.46×），而单步写入短的 `codebase` 反而 1.16× 输出——所以它是可配项，不是默认。
- **新增 `shapeGate`（默认关闭）**：本轮累计工具调用 ≥ 4 且写入类（不在只读名单里的工具）占比 ≥ 60 % 时，**整轮不再注入任何提醒**，退回原生。它给写密集型任务一条「别管我」的退路——这类任务没有可并行的调用，每条提醒都只是一次用不上的往返。判据只用已有工具名，不发请求、不加请求体字段；默认关，因为它同时会压掉本可救长尾的收尾提醒，尚未单独跑对照臂。
> **撤回（2026-10-26）**：1.5.0 这一节的实验数字（171 次运行、54 个单元，`fl10` 0.66× / `open15` 0.69×、以及 `def15` 的 0.86× 重基线）来自两个有缺陷的批次——运行目录在仓库内（会话可读到 `hidden/` 与 `ref/`）、指标按字节偏移切片（把同机器其他会话的步数算进来）。**这些数字不再作为速度声明**，只保留当时的决策记录；干净重基线见 1.5.1。
>
- **本轮主实验：9 家族 × 6 对三臂交错 ABBA（171 次运行、54 个完整单元）**。`off` 7841.3 s / 2 035 299 输出 token / 3960 步；`fl10` **0.66× 墙钟 / 0.68× 输出 / 0.67× 步数**（逐对 44/54 更快）；`open15` **0.69× / 0.71× / 0.71×**（40/54）。分家族上两臂互补：`open15` 在 `csvpipe` 0.46×、`perfrefactor` 0.62×、`stategraph` 0.63×、`shellops` 输出 0.37× 上更狠，`fl10` 在 `shellpipeline` 0.52×、`ledger` 0.62×、`codeimpl` 0.68×、`debugfix` 0.69× 上更好；两者都在某处留了输出回升（`open15` 在 `debugfix` 1.24× / `migrate` 1.07×，`fl10` 在 `stategraph` 1.16×）。`ledger` 的 22 条断言对模型偏紧：前 6 对里 p1（`fl10` 红）、p2 与 p6（`off` 红）整对作废，补跑 p7–p9 才凑满每臂 6 个完整单元。`fl10` 在 H2、`open15` 在 H3 各出现一次「基线绿而该臂红」，按规则都不做默认。
- **两个更快的配置都没通过门禁，1.5.0 默认不变**。`fl10`：`ledger` p1 基线与默认都绿、它红 21/22（重跑 5 次全绿，1/6 失败率）；`open15`：补测 5 个家族、30 个单元后相对默认 `def15` 是 **1.03× 墙钟 / 0.99× 输出 / 0.95× 步数**，`shellops` p7 基线绿而它红，`csvpipe` p5 用掉 325 步 / 499 s / 5.2 倍输出 token。按「基线绿而该臂红即不合格」，两者都退回实验旋钮，`finishLineSteps: 0` 与 `openBatch: false` 保持默认——宁慢不降质。分家族与单元级数据见 `BENCHMARK.md` 的 `H2`/`H3`。
- **进展按结果内容判定**：用结果内容的摘要（digest）而不是输出长度判断“这一步有没有新东西”。`completed 10 / 99` → `11 / 99` 这种等长轮询算进展，不会被误判成卡住；判定单位是整步，不是单次调用。
- **证据优先级**：最近三次调用都报错时，扇出宽度直接回到 1，压过“请求措辞像扇出”的分类。
- **看门狗取消后等真实收尾**：取消一个卡住的调用后，先等它真正停下（窗口随预算缩放，250 ms–5 s）再返回失败，并把 `quiesced` 记进日志。以前是“先告诉模型已取消，清理还在跑”，下一个步骤可能和上一个进程并发写同一个目录。
- **`enabled: false` 是真的关**：关闭态下看门狗预算为 0，不再按档位表在两分钟后取消调用。
- **测试与 CI**：223 个测试在 Node 20.20.2、22.23.4、24 上全绿。顺带修掉 Node 20/22 上 `node:test` 报 “Promise resolution is still pending but the event loop has already resolved” 并取消 17 个用例的问题（看门狗定时器不再 `unref`，它本来就会被 `clearTimeout`）。
- **措辞更正**：不再声称“请求体逐字节不变”——真实契约是**不加请求字段**（不改 `reasoningEffort`、不注入 `service_tier` / `max_tokens`）；开着时模型输入里多出的内容是本插件自己的调度提示与提醒，关掉后不存在。
- 移除从未被读取的常量 `AUTO_LIMITS.parallelMin`。
- 1.4.4 的 1.86×（7 家族 14 对、`finishLineSteps: 10`）保留为**已退役默认的消融证据**，不是 1.5.0 的速度声明。~~1.5.0 的重基线：9 家族 54 个完整单元、隐藏测试全绿，总量 0.86× 墙钟 / 0.95× 输出 / 0.85× 步数~~ **（2026-10-26 撤回：运行目录曾放在仓库内、指标按字节偏移切片，两个缺陷都会污染数字；干净重基线见 1.5.1。）**
- **计量日志有上限**：`~/.dsh/fast-mode-metrics.jsonl` 每步一行、只追加，装几个月会无限长（本机 7.6 MB）。现在超过 8 MB 就把当前文件改名成 `.1`、新开一份继续写，最多留两代（约 16 MB）；`bin/report.mjs` 会把两代一起读，轮转不丢历史。

## 1.4.4

**收尾线定版：7 家族 14 对中位 1.86×、输出 −52%、正确性 14/14 零损失。**

- 新增 `finishLineSteps` 并**设为默认 10**：第 10 步由调度器注入一次「别再扩大范围、不重跑已通过的检查、只修还在报的错、然后报告」，不阻断工具、不截断步骤。E1 七家族 14 对 ABBA：中位 **1.86×**、**13/14 更快**（符号检验 **p=0.002**）、输出 **−52 %**、步数 **−10.0**、隐藏测试 **14/14 全绿**；旧默认 `late` 同轮 1.31×（11/14，p=0.057）。
- 同轮直接配对（`fl10` ÷ `late`，14 对）：中位 **1.27×**、11/14 更快（p=0.057）、输出 **−22 %**、步数中位不变——收尾线省的是**重复验证的输出**，不是步骤。
- 新增 `stepBudget`（默认 30，可配 4–80）：硬预算只发一次「停止扩大范围、跑一次验证、直接报告」，仍不阻断工具，因为硬截断会把任务切在编辑中途。
- 被否掉的候选写进 BENCHMARK：`stepBudget: 15`（C1 2.04× / D1 1.26×，短任务桶 0.43–0.77×）、`finishLineSteps: 8`（C1 1.63× / D1 1.50×，6–10 步桶 0.44×）、两条叠加（D1 1.14×）。
- 归因沿用 B1：`late-guid0`（关掉提示段）0.96× vs `late` 1.26×——提示段是载荷；并行宽度 `parallel: 1` 1.10× 不是。
- 语料：5 轮 ABBA、272 次运行、7 个家族、隐藏测试确定性评分，全部 `exit 0`；`migrate` 的 12 次“红”经查是隐藏测试过严（只接受 `address.city`），已归一化为 `missing_field:address`，不是 agent 的错。
- 文档：`docs/REFERENCE.md` / `.en.md` 补上两个旋钮的默认值与实测，`outputDiscipline` 的默认值从旧文案的 `true` 更正为实际的 `late`。
- 205→206 个测试（默认值、第 10 步触发、每轮一次、步边界）。

## 1.4.3

**速度随步数出现：长任务 1.49×（迟到介入 2.68×），正确性 16/16 与 12/12 全对。**

- 步数分桶（80 对 ABBA 语料）：**1–5 步 1.00×**（47 对）、**6–10 步 1.23×**（33 对，符号检验 p=0.035）、11 步以上无数据。`corr(输出 token 变化, 加速) = −0.77`——赢的那些对都是模型写得少，不是并发。
- 输出纪律单独 A/B（12 对，深 fixture）：输出 token **−15.4%**（未达预设 20% 门槛）、墙钟 1.13× 但只有 7/12 对更快（p=0.774）、正确性 **96/96 不变**。结论：单看它自己，与 ±30% 的任务内噪声分不开，不作为独立卖点。
- 新增长任务 fixture：从零实现 `ledger.py`（6 个函数、会计括号负数/货币符号/Decimal/ROUND_HALF_UP/并列取最小等边界），**隐藏 22 条断言**由 runner 事后执行。8 对 ABBA：off 215.2 s / 16.5 步 / 32 718 token → on 144.5 s / 10.5 步 / 24 274 token = **1.49×**、步数 **8/8 对下降**（p=0.008）、输出 **−25.8%**、**16/16 次运行 22/22 全对**。
- 对照：上一版“12 个 bug”的修复型 fixture 4–6 步就被做完，实测 **0.94×、输出 +11%**——它不是长任务，正好印证分桶：步数不够就没有可省的。
- 新增 `outputDiscipline: 'late'` 并**设为默认**：系统提示不再放文风规则（短任务零成本），第 4 步由调度器注入一次压缩提醒（`src/auto.js` 的 `compress-output`，每轮一次）。6 对 A/B：184.2 s → 68.6 s = **2.68×**、步数 10.5→7.5、输出 **−52%**、**12/12 次运行 22/22 全对**。`true` 仍可全程生效，`false` 关闭。
- 短任务对照（4–6 步 fixture，6 对）：late 13.5 s vs 关 17.6 s（中位 1.31×、配对中位 1.26×、4/6 对更快、步数不变）——在 ±30% 噪声内，可读作“短任务没有付出代价”，与“短任务不介入”的产品设想一致。
- 撤回旧说法：配置说明里的“每步输出 640→190 token、墙钟快约三分之一”从未被单独测过，已换成上面的实测数字。
- `src/auto.js`：新增 `notePolicy()` 与 `compress-output` 指令，`AUTO_LIMITS.disciplineAfterSteps = 4`，按轮重置 `disciplineSent`。
- 198→202 个测试（三档取值、每轮只提醒一次、新默认值）。

## 1.4.2

**量化并发，并撤回一个自己造出来的结论。**

- `shell_parallel` 改为**默认关闭**（`shellParallel: false`）。6 个 5 秒作业的 fixture 上三对 ABBA：原生 9.1 s vs 开工具 26.3 s（**0.35×**）。工具本身是并发的（5.1 s 跑完），但基线模型本来就用 `&` + `wait` 自并行，多出的工具诱发一次“再复核”的额外步骤。
- 提示段只在工具真的注册时才提 `shell_parallel`（`src/guidance.js` 拆出 `SHELL_JOBS`），避免指着不存在的工具。
- 新增 `guidance` 开关并**修好透传**：`resolveConfig` 之前没把它（和新加的 `shellParallel`）返回出去，导致一次 54 次运行的三臂实验实际上只有两臂配置相同。该实验降级为**噪声标定**：同配置在 n=6 下能给出 0.75× 与 1.01× 两种“效应” → 小任务每任务中位数噪声约 ±30%。此前“notext 1.01× / text 0.88×”的说法**作废**。
- 新增实测：4 个子代理各 `sleep 20`，整段跨度 **21 s**（串行 80 s，约 3.8×）——并发原语可用，瓶颈在模型是否拆活。
- 新增实测：把 4 个模块的真实分析交给子代理扇出，**0.25×**（中位 36.3 s vs 9.2 s，步数 5.5→28.5，输出 token 1.5 K→9.6 K）。并发是真的（4 个孩子的 llm 时间确实重叠），但每个子代理的固定成本在小任务上吃掉全部收益——扇出只在深子任务上有意义。
- 新增实测（pilot，2 对）：深子任务扇出仍 **0.60×**（中位 48.2 s vs 28.8 s，步数 12.5→43，输出 token 5.1 K→16.9 K），且两次里有一次 4 个模块全算错（串行两次全对）。会话取证：4 个子会话在 3 毫秒内同时创建，派发确实并行；亏在每个孩子要 15–25 步才做完一个模块，而串行 11–14 步做完全部四个。扇出到此结案。
- 196→198 个测试（提示段门控、工具默认值）。
- 新增人群统计，否掉“重复调用短路”：7168 次工具调用里严格重复 0.1%、宽松 4.4%，且都是 `ls`/`cat`/`python3 gen.py` 这类便宜或有状态的命令——省不到每步 0.5 s 的 TTFT，还会把有状态命令误当纯读。不做。

## 1.4.1

**诚实修正：扩大样本后 1.4.0 的 1.86× 被证伪，并修掉由此暴露的回归。**

- `src/shape.js`：新增 `PER_ITEM` 形状（`one object keyed by file` 这类逐项输出表）判**原生**。1.4.0 的“一次抓全”在这个形状上实测 **0.85×**（15 对）：八次读并成一步，但该步要一次解码整张表，输出 token 1069 → 1197。
- 实测扩样（76 对，ABBA，全部 PASS）：**中位 1.06×，CI 0.996–1.155×，45/76 对变快，均值 1.15×**。t1 0.85×→1.02×（修掉回归）、t3 1.77×→1.03×（15 对）、t9 写扇出 0.98×、t10 读写混合 1.11×。
- 结论写在 `BENCHMARK.md` 顶部：墙钟由输出 token 主导，只有“步数下降且单步输出不涨”的形状（shell 扇出）能稳定赚到 ~8%；t6/t8 的 1.4×/1.3× 仍只有 5 对，只能当方向。
- 181→185 个测试（新增逐项输出、聚合报告两个用例）。

## 1.4.0

**三步合并成一步：形状第一步就判、已知目标一次抓全、聚合用一条命令。**

- `src/guidance.js`：新增 GATHER 规则（已知目标必须进第一个块；>10 个目标或求聚合时改用一条 `rg -c` / `grep -r` / 只打印的短脚本；独立 shell 作业可以同块，不许写成 `&&` 长链）。
- `src/auto.js`：分类出的宽度**整轮保持**，不再只在第一步生效；同轮后续步骤不会因为上一次调用是写或单条 shell 就退回原生。
- `src/shape.js`：shell + 扇出（每个目录一个作业）判为宽；纯 shell、串行、聚合之外仍原生。
- 实测 43 对（8 任务，ABBA，120/120 PASS）：**中位加速比 1.09×（CI 1.03–1.33×），30/43 对变快**，平均步数 5.5 → 4.7；t1 1.86×、t3 1.77×、t6 1.43×、t8 1.29×，t2/t4/t7 为设计的无操作对照（约 1.0×）。
- 代价：输入 token 上升（抓全 + 聚合），输出 token 略降；单任务 5–8 对，方差仍大于单任务效应。

## 1.3.0

**第一步就知道该不该宽：形状从请求本身的措辞读出来，不再付探测费。**

- 新增 `src/shape.js`：一次正则遍历判断请求形状。串行词（chain / one by one / 依次 / 逐个 / 依赖链）→ 原生；shell 词（bash / python3 / 执行 / 运行）→ 原生（宽块在 shell 上本来就会串行化）；扇出词（independently / every file / across / per file / 每个 / 所有）或 ≥4 个被指名的目标 → 宽度 8；其余一律原生。
- 分类结果由 `noteEntry` 记入该轮，只被本轮第一步消费；同一轮后续步骤不能中途改判。
- 因此**新会话不再有 4 宽探测**：判不出形状的请求直接原生，和没装插件同宽；判得出扇出的请求第一步就宽。
- 试跑（8 任务 × 4 对 ABBA，32 对，64 次运行，质量 63/64，唯一失败在对照组）：中位加速比 1.08×（CI 0.88–1.21×），18/32 对变快；t1 从 0.78× 回到 1.16×、t4 从 0.71× 回到 1.20×，即“不再对非扇出任务下批处理指令”确实拿回了探测的成本。样本仍小，作为 pilot 而非结论。
- 新增 11 个形状测试（含中文扇出/串行、链式命名多文件的陷阱），全套 183 通过。

## 1.2.1

**把"独立读块"变成程序化判据，并且真的用 8 个任务量过一遍。**

- 调度器新增 `lastStepDistinct`：这一步所有调用都在 `SAFE_CALLS` 里，且参数签名两两不同，才算"独立读块"。两个调用读同一路径会被判成链而不是批，不进入宽模式。
- 新的一轮请求如果没有并行形状的历史，宽度直接是 1（原生、整段提示不渲染），不再每轮先付一次 4 宽的探测；只有会话历史里出现过独立读块或连续纯读才提速。
- 独立读块一旦出现（≥2 个调用）就立刻 → 8，不需要等第二步；单调用步骤即使只读也保持原生。
- 实测（8 任务 × 5 对 ABBA，80 次真实 headless 运行，质量 80/80，同模型同 `effort: medium`）：40 对合并中位加速比 0.96×（CI 0.80–1.14×），19/40 对变快；可并行形状 t2 1.14×、t5 1.14×、t6 1.28×，摘要/审计形状 t1 0.78×、t4 0.71×。**结论：没有普遍变快，收益集中在独立读。**
- 局限已写进 README：判据只能用"已经发生的调用"，单轮任务只看到新会话那一次 4 宽探测，形状证据来不及积累。下一步是把请求入口的形状（独立文件/目录数）也变成信号，并对宽度做在线选择。

## 1.2.0

**只在值得的地方提速。** 调度器不再固定给一个"快"的宽度，而是每一步先判断形状：只有证据表明这一步是"多个互不依赖的读"才要宽，其余保持原生（宽度 1 = 整段指导不渲染）。

- 新会话先用 4 做一次探测；连续两步纯读 → 8；上一步同块 ≥3 个读 → 8；跨轮记住上一轮末尾的宽读，下一轮开局直接 8。
- 出现写调用、单调用步骤、或连续 3 步无进展 / 连续 3 次报错 → 回到原生，不再要求批处理，也不再付这段提示的输入钱。
- 实测（4 任务 × 6 对 ABBA，同模型同 effort，质量 24/24）：可并行的 t2 墙钟 0.69×、输出 0.72×；t1 0.89×；t4 1.04×；无可并行形状的 t3 1.24×。24 对中位 0.96×，方差大于效应 —— 不发布单一加速比。
- 不新增配置项；旧 `auto` 含义保持不变，行为按上面的证据规则细化。

## 1.1.0

**只剩一个开关。** 闪电本身就是开/关：点一下开，再点一下关。196 px 的面板整个删除——没有弹层、没有下拉、没有第二层界面，也没有一件只有面板里才能做的事。

- `client.js`：删除 `FastPanel`、菜单开关逻辑、点击外部/Esc 关闭、`aria-haspopup`/`aria-expanded`、`data-open`、全部 `.fm-menu*` 样式与 `menu.*` 词条（941 → 716 行）。按钮保留 `aria-pressed` 与 tooltip；未就绪时先重读再切换，拒绝写入仍在 tooltip 说明原因。
- **说明搬进插件介绍**：`meta.description` 与配置项描述现在完整讲清“同强度、并行 2–8、压输出、卡顿上限、每会话独立、重启记得”，不再占输入区空间。
- **每会话记忆**：一次点击只写本会话在 `sessions` 列里的条目连同 `enabled`/`gear`；`settings` 服务把 volatile 配置原子写回 profile patch，所以重启、重开会话、切标签页都回到该会话自己的选择（例如 medium + fast）。
- 测试重写为 20 个直点开关的用例（单击即写、二次关闭、写出的三个字段里没有 effort、拒绝回滚、只读不写、字典无 `menu.*`、CSS 无 `.fm-menu`、按会话/重启恢复）。全套 168 通过。

## 1.0.1

**修复"每轮 30 步"实际变成"整段会话 30 步"。**

- `index.js` 现在在 `session/event` 的 `turn/start` 上调用 `noteTurn()`：每轮清零 `turnSteps`、`stepCalls`、`lastStepCalls`，并重新武装停止线；跨轮保留调度记忆（试过什么、卡了多久）。
- 修掉一个连带问题：`stop-and-report` 的去重原先按"上一次指令种类"跨轮生效，导致第二条长轮次永远不再触发。去重现在只依赖本轮的 `stopped` 标志，`turn/start` 会把它复位。
- 新增回归测试：一轮 30 步触发一次停止提示 → `turn/start` 后下一步不再立即停止 → 再走 30 步仍会触发。
- `docs/REFERENCE*.md` 中原先记录的"已知实现缺口"随之删除。

## 1.0.0

**Fast 模式从此不碰思考强度。** 0.9.x 之前它按档位改写 `reasoningEffort`，最高档甚至发 `off`——这是错的。用户选了多深的思考，就应该是多深；开启快模式改变的是往返次数，不是智力。1.0.0 把整条降档机制**删掉**（不是关掉、不是加开关），`agent/request` 钩子现在是纯透传：`return resolved`。

### 删掉了什么

- 整套 effort 策略：`floor`、`decideEffort`、每步降一档的阶梯、`ludicrous` 发 `reasoningEffort: 'off'`、以及所有按档位写死的 effort 表。配置里再写 `floor`/`ladder`/`aliases` 不会生效，源码里也不再有 `reasoningEffort:` 赋值（有一条测试扫描源码来守住这点）。
- 面板里的五档列表、速度/倍数估算、每档的等待上限表。它们展示的是已经不存在的选择。

### 现在靠什么变快

同样的思考强度、同样的模型、同样的供应商，只改三件事：

1. **更宽的工具并行**：一步里允许发多个互不依赖的调用，宽度 2–8 由调度器按最近几步的表现自动决定（默认 4，同一步内连续两步顺利后放宽，报错或卡住时收窄）。
2. **更少过程文字**：输出纪律只收叙述性文字，答案本身一个字不删。
3. **更少的卡顿**：前台命令有可取消的上限（`auto` 下 120 s 起、按最近 p90 实际延迟在 2–5 分钟自适应；`safe` 不限），卡住会取消并把"换个做法"写回给模型；另有 6 步无进展告警、12 步停止、每轮 30 步后不再放宽的循环保护。

代价是 token 可能更多（并行调用、一次读多处），这是明确的交易：**用 token 换墙钟时间，不用智力换**。

### 面板与档位

- 闪电点开只有一个开关、一行调度状态（引用的上限从配置里读，不再写死）、两行说明。196 px 不变，仍然跟着会话走。
- `gear` 对外只剩 `auto` 有意义；旧配置里的 `fast`/`turbo`/`max`/`ludicrous` 会被当作 `auto` 并打一条弃用日志，而不是静默降思考强度。`safe` 保留为实验对照臂（固定并行 2、不设上限、不注入提示）。
- 升级后不会再有"每次进来思考强度都变了"：胶囊显示的是你自己选的档，重启也一样。

### 保留

- 0.9.1 的 v4 会话格式修复（`kind: 'plugin:fast-mode'`）与回归测试。
- 每会话开关、指标日志、报告与实验 CLI、压缩类插件共存、与 `dsh-batch-tool-calls`/`dsh-ops` 的互斥声明。

## 0.9.1

**Upgrade if you installed 0.9.0.** Its post-compaction reminder could fail a
whole turn. Nothing else changed.

- **The fix.** The two notices were built with `source: { kind: 'plugin', plugin: 'fast-mode' }`,
  the pre-v4 shape. DSH's v4 session encoder refuses that pair —
  `SessionFormatError: format v4 message requires a producer-owned source kind`
  (`@deepseek-ai/dsh-session-format-v3-to-v4`, `assertV4MessageSources`) — and it
  refuses it while persisting the event, outside the `agent/pre-step` handler
  that built the message, so the failure landed on the turn rather than on the
  notice. A conversation that compacted and then resumed died at the first step
  after the checkpoint. The source is now `{ kind: 'plugin:fast-mode' }`, which is
  also the kind v3→v4 migration derives for rows written by older versions, so
  old transcripts read back under one shape.
- **Why it cannot come back quietly.** `createNoticeMessage` now validates its
  source against the mirrored v4 rule before returning, and throws *there* — the
  caller's `try/catch` then drops the notice and the step proceeds, which is the
  failure mode this plugin is allowed to have. `isProducerOwnedSource` mirrors
  the encoder's rule, and `test/notices.test.js` pins it: the retired
  `{ kind: 'plugin', plugin }` shape must be refused, `plugin:fast-mode` accepted,
  no leftover `plugin` field. Verified against the released codec extracted from
  `app.asar`: both notices encode, both retired shapes throw the exact error above.
- **Existing sessions are unaffected.** The bad row was only ever written by the
  v4 encoder, which refused it, so nothing was persisted. v0-format sessions under
  `--private-tmp--` that still carry `kind: "plugin"` rows go through the
  documented v3→v4 migration, which rewrites them to `plugin:<name>`.

## 0.9.0

Tool wait is the third lever, and the panel stops claiming measurements it never
made.

- **A stall guard bounds one tool call.** `tools/execute` now races a guarded
  shell-style call (`bash`, `script`, `monitor`, `pwsh`…) against a per-gear
  budget: 300 / 180 / 120 / 90 s for Cruise / Sport / Track / Redline, and no
  watch at all on Eco. On expiry the plugin aborts *its own* controller — never
  the caller's signal, which the registry re-fuses anyway — gives the tool 250 ms
  to unwind, restores the caller's signal, and returns a `ToolExecutionFailure`
  telling the model to move the work to `run_in_background`, pass a `script_path`
  instead of a heredoc, narrow the probe, and never re-issue the same command
  unchanged. Waiting tools (`ask_user_question`, `wait_agent`, `job_output`,
  `job_list`, `task_board_run`, `workflow`, `subagent`, `subagent_fork`) and the
  file tools are exempt. The ceiling is a **policy budget, not a measurement**;
  `stallGuard: false` or `toolTimeoutMs: -1` disables it, `toolTimeoutMs: 90000`
  applies one number to every gear.
- **The prompt half ships with it.** Four rules joined the execution-speed
  section: bound any command that can hang, prefer `run_in_background`, write
  long/quoted/multi-line commands to a file instead of a heredoc, and change the
  command before retrying one that was cancelled or came back malformed.
- **`dsh-batch-tool-calls` and `dsh-ops` must not be enabled together with it.**
  The batch plugin is a static, gear-blind subset of this guidance; the ops plugin
  adds two shells plus Windows-only binaries and conflicting timeout advice. The
  host reads the profile's dependency files once on load (and probes for live
  `ops_*` tools); if either is present it logs one warning and appends one notice
  to the next step, then goes quiet. It never hard-fails the user. Both reference
  manuals document the conflict.
- **The panel says "estimate", and shows the one number that is not.** The footer
  now reads `倍率预估 · 单次调用 ≤ 3 分` instead of `倍率 = 相对 Eco 的实测中位数`:
  our own runs do not support a per-gear speed or token ratio — on single-file
  tasks every enabled gear lands within noise of `safe` — so the multipliers are
  labelled as estimates while the stall ceiling is quoted straight from the
  host-side table for the selected gear.
- **The five gears have names now: Eco / Cruise / Sport / Track / Redline.** The
  ids are unchanged (`safe` / `fast` / `turbo` / `max` / `ludicrous`), so existing
  patches, settings and experiment files keep working — only the labels moved.
- **The gear menu labels its own columns.** The 0.8.0 popover printed
  `1.6x · 1.5x` with no header, so nothing said which number was the speed and
  which was the token cost. A `Gear / speed / token` header row now shares one
  CSS grid with the five gear rows: the numbers sit under their labels, right
  aligned in tabular figures, in both locales. Each row's `aria-label` names the
  columns too, so the pair is not opaque to a screen reader either.
- **The fine print wraps instead of running off the edge.** The old single line
  (`倍率预估：速度 · token * 关闭思考，质量会下降`) was `white-space: nowrap`
  inside a 196 px popover and got clipped mid-sentence. It is two short lines in
  a footer above a hairline now — the estimate note (which also carries the
  selected gear's stall ceiling) and the warning — and both wrap.
- **README: 484 lines to 98.** The front page keeps the principle, the measured
  table, thirty-second setup, the five gears, the coexistence rules and what is
  next, one short paragraph each. Configuration, logging, measurement instruments,
  experiment methodology, safety boundary, limits and the test guide moved to
  `docs/REFERENCE.md`. The result table states the honest part in the open:
  turbo is 537 ms slower and 5 % fatter per step — it wins on step count.

198 tests, green. `test/stallguard.test.js` (12) covers the budget table, the
signal dance, the failure text, the frozen notice messages and the peer scan;
`test/plugin.test.js` adds the wiring cases (registration, a hung `bash`
cancelled at `toolTimeoutMs: 20`, passthrough, the post-compaction reminder);
one `test/client.test.js` case pins the footer to the host-side budget table so
the two halves cannot drift.

## 0.8.0

Everything up to 0.7.0 optimised effort, and Experiment E showed effort is not the
lever on a self-hosted route: raising it does not shorten a session. What does
shorten a session is fewer round trips and less visible text. 0.8.0 pulls both,
and it measures the result instead of asserting it.

- **The execution-speed guidance is one system-prompt section, not a rewritten
  prompt.** `fast-mode:execution-speed` at order 520 states the batching rules,
  the gear's parallel target, the speculative-prefetch line and the
  output-discipline layers. It is injected only for gears marked `directive`, so
  `safe` injects nothing at all — it stays the pre-0.8.0 behaviour and works as
  the control arm.
- **Output discipline is the largest single lever.** `outputDiscipline` (default
  `true`, not volatile, so changing it needs a reload) tells the model not to
  narrate around tool calls, not to restate file contents, and not to write
  end-of-step progress reports. Measured: per-step output **640 → 190 tokens**.
- **Gears gain a `parallel` target and a `discipline` level**: safe 2 / fast 4 /
  turbo 6 / max 8 / ludicrous 8 per step; safe injects no discipline, fast gets
  the light version (three prohibitions), turbo and above the full version plus
  speculative prefetch. An explicit `parallel` overrides the target — `gear:
  turbo` + `parallel: 8` fits two more independent calls into the same round
  trip and never adds a step.
- **A warning when the profile caps the target.** If `agentLoop.maxParallelToolCalls`
  sits below what the gear asks for, one warn names the cap and the line to
  change in `cordis.patch.yml`; it fires once per distinct `parallel` value, not
  once per request.
- **Measurement: `bin/experiment.mjs`, and Experiment E.** 16 same-task sessions
  assigned by hash rather than clicked, `safe` vs `turbo`: median Σ step time
  25.9 s → 15.6 s (−40 %), median steps 7.5 → 4.5, per-session output tokens
  3,798 → 2,180 (−43 %), Mann–Whitney p = 0.008, and all ten answer facts agreed.
  Raw data and the caveats (uneven arms, small n) are in BENCHMARK.md.
- **The trade, stated plainly**: this plugin does not save tokens; it spends
  tokens to buy time. Every lever here adds tokens to the same route's request,
  and none of them works by making the model dumber.

173 tests (8 new in `test/guidance.test.js`); the suite is green.

## 0.7.0

Fast mode was a global switch wearing a per-conversation costume: the bolt sits in
one composer, so clicking it read as "this conversation", and it changed every
conversation in the app. Two sessions side by side, one clicked, both accelerated —
and nothing in the UI said so.

- **The switch and the gear are per conversation now.** A click writes that
  conversation's own `{enabled, gear}` entry; the panel says `仅本会话生效` /
  `This conversation only` under the switch, so the scope is stated where the
  decision is made instead of being inferred.
- **A conversation with no entry inherits the shared pair**, which doubles as the
  record of the last choice made anywhere. A brand-new conversation therefore
  starts from the last selection rather than from the schema default — the
  behaviour asked for, and the reason the shared fields still exist.
- **Precedence is explicit**: experiment assignment > that conversation's entry >
  shared default. An assigned trial still outranks the UI, because a click that
  quietly overrode the arm would empty the control group of the very trial the
  user asked for.
- **One click, one patch.** The shared `enabled` / `gear` and the `sessions` entry
  travel in a single `settings/update` carrying the revision that was read. Two
  writes would mean a window where the panel shows one gear and the request runs
  on another, plus a second revision race; a refused write rolls back all three
  fields and states the reason in the panel.
- **The column is bounded and forgiving.** `sessions` holds at most 200 entries,
  oldest pruned first by `ts` (a pruned conversation just falls back to the
  shared default). A hand-edited or truncated value degrades to "no overrides"
  and never takes the chip down.
- **The two copies of the format are tested against each other.** The host reads
  the column (`src/state.js`), the browser half writes it (`client.js`), and a
  drift between them is a switch that looks on and changes nothing — so the
  shipped `client.js` is loaded in a `vm` and fed the same fixtures as the host
  module. Writing the test found one real drift (the browser copy accepted an
  empty session id the host drops), which is now fixed on both sides.
- The conversation id reaches the client half through the slot's own scoped
  `inject(sessionId)`; the chip also accepts a standard `sessionId` prop, so a
  release that stops injecting one spelling still behaves.
- One caveat, stated where a user will hit it: `sessions` renders as a text input
  on the plugin settings page, because this build of schemastery has no
  `.hidden()`. Its description says it is maintained automatically; editing it by
  hand is how a conversation ends up running a gear nobody chose.

165 tests.

## 0.6.2

The measurement was wrong in the one way that matters most: it counted time but
not tokens, and it lost which step each row belonged to.

- **Read the event payload where it is.** `session/event` carries its payload in
  `event.data` (`{turn, step, usage, …}`); the recorder was reading `event.turn`
  and `event.usage` off the envelope, so every row from 0.6.0 and 0.6.1 had
  `turn`/`step` missing and `input`/`output` at zero while timings looked fine.
  Rows now carry step numbers and real token counts, and both the harness
  spelling (`inputTokens`, `reasoningTokens`, `cacheReadTokens`) and the gateway
  spelling (`uncachedInputTokens`, `thoughtTokens`, `cachedReadTokens`) are
  folded.
- Two regression tests feed the wrapped shape, so this cannot come back quietly.
- Rows already written to `~/.dsh/fast-mode-metrics.jsonl` keep their zeros;
  delete the file or filter by `ts` when comparing against new data.

147 tests.

## 0.6.1

0.6.0 could compare gears, but not against the thing they are supposed to beat.
An assignment always switched the mode on, so a randomized trial had no baseline:
the only "off" sessions were ones somebody happened to run before starting the
experiment, which is not an arm — it is a convenience sample.

- **`"off"` is a legal arm.** The assignment file and
  `dsh-fast-mode-experiment --gears` now accept `off`, meaning the control group:
  the session runs with the plugin switched off **because the experiment said so**,
  not because a switch was left alone. `off` is always accepted even when the
  options do not list it, and the CLI now defaults to
  `off,safe,fast,turbo,max`, so a fresh `start` is a controlled trial rather than
  a ranking. `start` says which kind of run it wrote: with `off` it promises a
  baseline, without it it warns that the report can only rank arms against each
  other.
- **The control arm is deliberately invisible to the switch.** A session assigned
  to `off` returns the request untouched even when the global switch is on — the
  one place where the UI can disagree with what a session actually does, which
  the experiment output and both READMEs now state plainly.
- Also lands in this release: `bin/experiment.mjs` is documented and exposed as
  the `dsh-fast-mode-experiment` bin with an npm script, and `--lan` follows
  `--since` into the dashboard's own `1d`/`7d`/`30d` window (default `7d`) rather
  than always asking for one day and quietly printing nothing.

145 tests.

## 0.6.0

0.5.2 made the switch work. 0.6.0 makes the claim checkable: fast mode measures
itself, assigns gears to sessions for comparison, and reports the result from a
terminal. It also adds one gear above `max`, because on the official route that
is the only honest way past the 2× ceiling.

- **Measurement is built in and on by default.** A `session/event` tap folds
  every step into `~/.dsh/fast-mode-metrics.jsonl`, one JSON line per completed
  step, for sessions with the switch **off** as well as on — a comparison needs
  both sides, and nobody remembers to enable measurement before an A/B. A row
  carries `v, ts, sid, turn, step, mode` (`on|off|unknown`), `gear` (or `off`),
  `effort`, `route`, `ttftMs` (step start → first assistant message), `llmMs`
  (step start → last assistant message), `stepMs` (the whole step), `calls`,
  `errors`, `input`, `output`, `reasoning`, `cacheRead`, `cacheWrite` and
  `assistantMessages`. `metrics: false` disables it; a write failure is
  swallowed and never fails a request.
- **Experiment assignment without a UI.** An optional
  `~/.dsh/fast-mode-bench.json` (`benchmarkFile`) assigns gears per session:
  `{"enabled": true, "gears": ["safe","fast","turbo","max"], "sessions": {"<session id>": "turbo"}}`.
  An explicit pin wins; otherwise the gear is `FNV-1a(session id) mod gears`.
  The file is re-read on a 1 s TTL, so an edit applies to the next request, and
  deleting it returns control to the configured switch. A session an experiment
  assigns runs on that assignment even with the switch off — letting a forgotten
  click decide the arm would silently put the session in the control group.
  Unknown gear names are dropped with one warning.
- **The experiment has a command, not a JSON file you edit by hand.**
  `bin/experiment.mjs` (package bin `dsh-fast-mode-experiment`) writes the
  assignment file, reports how many sessions and steps each arm has collected so
  far (`status`, including whether a disabled-state sample exists at all), and
  refuses to overwrite a running experiment without `--force`. An experiment is
  what makes the numbers trustworthy: the arm is chosen by the session-id hash,
  not by whoever wants the better result.
- **A report command instead of a spreadsheet.** `bin/report.mjs`
  (package bin `dsh-fast-mode-report`) reads the JSONL and prints a per-stance
  table plus a speedup table against the `off` baseline. The unit is the step;
  session wall clock is not used, because it mostly measures the human. Flags:
  `--file`, `--since 2d|12h|90m`, `--json`, `--lan`, `--help`. `--lan` joins
  local rows to the self-hosted usage dashboard by session UUID using
  `USAGE_TOKEN` (optional `USAGE_BASE`): read-only, nothing leaves the LAN, and
  the token is never printed. The dashboard's own window (`1d`/`7d`/`30d`)
  follows `--since` instead of always asking for one day. An unreachable,
  slow or unauthorised dashboard degrades to one note; it never costs you the
  local report.
- **New gear `ludicrous` (极速), above `max`.** It is the only gear whose floor
  is `off`, i.e. it may set `reasoningEffort: 'off'`, with a fan-out target of 8
  and `deepAfter: 1`. That is not "a bit faster than max": the official route
  declares `off/low/high/max`, so with thinking on the only legal cut is
  `high → low` (≈2×) and 3–4× requires turning thinking off, which this gear
  does. The panel therefore marks it `极速*` / `Ludicrous*` and the footer says
  `* 关闭思考，质量会下降`. The plugin still never emits an id the route does
  not declare — on a route without `off`, the gear degrades to what the ladder
  offers.
- **The chip is one bolt, and the panel is behind it.** The caret and its inline
  dropdown are gone. Clicking the bolt opens a panel holding the on/off switch,
  the five gear rows (each with its estimated multipliers, the current one
  marked) and the fine print, plus a status line that states the reason when a
  write is refused. Clicking does not toggle; picking a gear while fast mode is
  off still arms `enabled` in the same write.
- **The gear is a name.** `gear` is a schema union of
  `safe/fast/turbo/max/ludicrous`; a number or a typo is a load-time error, as
  `src/config.js` already documented. The policy layer still normalises
  defensively, so a bad value can never become "unlimited", but it no longer
  silently becomes `safe` either.
- **Measurement paths are composition knobs.** `metricsFile` and
  `benchmarkFile` are deliberately not `.volatile()`: they are read at load and
  do not belong on a live form. Moving the JSONL is a `cordis.patch.yml` edit.

Tests 142: the measurement tap proves off sessions are measured too (one row per
step, first-token vs last-message latency in a multi-message step, token and
error sums, per-session isolation, a missing directory created on first write,
an empty path disabling the sink); the assignment proves a stable hash that
covers every gear, an explicit pin, a single-session pin, a live 1 s re-read,
inertness without a file or with `enabled: false`, and one complaint for a
broken file; the report proves both tables, a one-sided refusal in words,
`--since` including the wider dashboard window, `--json`, `--help` reading no
file, `--lan` without a token and with one (the token is never printed), and an
unreachable dashboard degrading to a note; the client proves the contract after
the caret removal — one icon, clicking opens the panel and writes nothing by
itself, the switch writes once and a second click turns it back off, five rows
with estimates and an asterisk on `ludicrous`, and picking a gear while already
on leaves the toggle alone; and the schema proves an unknown gear name is a
load-time error, measurement is on by default, and exactly
`enabled`/`gear`/`parallel` are volatile.

## 0.5.2

0.5.1 shipped the switch but left it inert, and the reason was one wrong field
access:

- **The remote protocol wraps every answer in `{ ok, value }`.** The chip read
  `remote.settings.describe().namespaces` straight off the envelope, so it always
  got `undefined`, classified this plugin as "not configured", and rendered a
  `disabled` button. Chrome suppresses `title` on a disabled button, so hovering
  said nothing, and the click handler's guard swallowed every click — a control
  that was fully explained inside the code and completely dead on screen.
  `unwrap()` now handles the envelope (and a bare payload, so a service stub
  still works), `ok: false` surfaces `error.message`, and every remote call has a
  deadline so a pending bridge becomes a visible error instead of an endless
  "loading".
- **The chip is never disabled.** Read-only, unknown and missing-namespace states
  all stay clickable and open the menu with the reason in words.
- **The gear lives on the chip.** The caret opens a dropdown listing the four
  gears, each row showing its estimated speed and token multiplier
  (`速度 ~1.6× · token ~1.5×`) and marking the current one. Picking a gear while
  fast mode is off turns it on in the same write. There is deliberately no gear
  form on the plugin page: this harness renders no generic schema form, so one
  working entry point beats a second half-working one.
- **Host-side traceability.** With `debug` on, `apply` writes one line per load
  before the `enabled` check (`apply v0.5.2 entered enabled=… gear=…`), then one
  more saying it registered nothing; after two seconds it also reports whether
  `settings.describe()` actually serves this namespace. A future "the chip does
  nothing" report is now one log line away from an answer.

Tests 89: the client suite runs the real bundle against the real wire envelope,
including the bare-payload tolerance, the refused write, the read-only chip, the
missing namespace, and the gear multipliers.

## 0.5.1

0.5.0 shipped a settings switch that could not be switched. Two defects, both
about how the `settings` service treats a schema field:

- **The three form fields were not `.volatile()`.** `settings.describe()` projects
  a Config through `volatileForm`, which keeps only nodes carrying
  `meta.volatile`; with none, the whole entry disappeared from the form — so
  there was no settings page, the composer chip read it as "absent" and stayed
  greyed out, and `settings.update()` would have refused the write with
  `Config field "enabled" is not volatile`. `enabled`, `gear` and `parallel` are
  now declared volatile, which is also what lets an edit land on a running fiber
  without remounting the plugin.
- **A volatile field is a reference, not a value.** The Loader hands a running
  plugin a frozen `{get()}` reference so it can swap the value in place;
  `config.enabled === true` would therefore have been false with the switch on.
  `resolveConfig` now dereferences at that single boundary (and still accepts a
  plain config object), and the module documents the protocol.

Tests 80: the schema now has to prove every form field is volatile, and that a
reference from the Loader reads as an armed switch.

## 0.5.0

Fast mode becomes something you switch, not something an install decides for you.

- **Opt-in.** `enabled` now defaults to `false`, and `apply()` registers nothing —
  no hooks, no prompt section, no discovery — until something sets it to `true`.
  Only a literal `true` arms it; `1` or `"yes"` do not.
- **A lightning switch in the composer.** New client half (`client.js`, served
  through `dsh.client`) registers on `conversation.input.activity`, beside the
  model selector: hollow bolt = off, filled black = on, the tooltip carries the
  gear and the reason when it is not clickable. It reads and writes the plugin's
  own `enabled` through `remote.settings.describe()` / `update()`, so the chip,
  the generated Settings page, and `cordis.patch.yml` are three views of one
  field. Clicks are optimistic and then verified by polling `describe()`; a failed
  write reverts the icon and surfaces the error in the tooltip. A registration
  taken by another plugin, an unreadable document, or a read-only layer degrades
  to a disabled chip, never to an error thrown into the composer.
- **A config schema.** `Config` (schemastery) declares only the three fields a
  form should show — `enabled`, `gear`, `parallel`. Undeclared patch-level keys
  (`floor`, `holdTools`, `debug`, …) pass through untouched, so existing
  compositions keep working.
- **One dependency.** `@deepseek-ai/schemastery` (`~3.18.5`), which every shipped
  DSH plugin that exposes a settings page already declares; the profile already
  has it hoisted, so this adds no new runtime.
- **Tests.** 78 (up from 64): the default-off contract, non-boolean configs not
  arming the policy, real schemastery validation of the schema (`gear: warp`
  fails loudly, unknown keys survive), the client-half manifest contract
  (`dsh.client.platform`, `exports["./client"]`, packed `client.js`, bundle id,
  and the slot being the one that never shadows the model selector), and eight
  tests that run the shipped `client.js` verbatim in a `vm` sandbox with a stub
  `__ModuleLoader__` and a fake `settings` remote — proving the bundle parses,
  declares its loader contract, paints hollow when off and filled when on, sends
  exactly one `settings/update` with the revision it read, rolls back and explains
  itself when that write is refused, and never renders a clickable chip over a
  read-only layer or an absent entry.

## 0.4.0

Speed becomes a stance, not a single threshold, and the plugin now installs itself ON.

- **Gears.** `gear: 1..4` (`safe` / `fast` / `turbo` / `max`) selects a coherent
  stance: how early a clean streak earns the second effort notch (`deepAfter`
  6/4/3/2), how many observations count (`window` 8/6/5/4), and how many
  independent calls the model is asked to batch per step (none/4/6/8).
  `fast` is the shipped default; `safe` is byte-for-byte 0.3.0 behaviour.
- **An explicit fan-out directive.** Gears 2–4 register a short system-prompt
  section (`fast-mode:execution-speed`, order 520) that tells the model to batch
  independent calls. The number in the text follows the gear, or `parallel`.
- **A loud cap warning.** If `agentLoop.maxParallelToolCalls` is below the gear's
  target, the plugin logs the exact `cordis.patch.yml` line that raises it, once.
- **Display metadata.** `icon.png` at the package root (PNG, 11,607 bytes) plus
  `locale/zh.json` and `locale/en.json` with `meta.title` / `meta.description`, so
  the Plugin Manager and Settings show a real icon and localized copy.
- **Docs.** Chinese README is now the default, English moved to `README.en.md`,
  and both document the gears, the parallel lever, official-API support, and how
  to compose with context-compression plugins.
- **Tests.** 64 (up from 55), including the manifest checks (icon size and type,
  locale metadata, `files[]` completeness) and the guidance registration path.

## 0.3.0

- Discover the route's real effort ladder through `llm.resolveModelInfo()` instead
  of assuming a vocabulary; never emit a level the route did not declare.
- Explicit `ladder`, documented-alias folding, per-route startup logging, and a
  `route=` field in the debug line.
- `holdTools` keeps full thinking after context-management tools, which look
  "clean" from the outside while being anything but.

## 0.2.0

- Observe `tools/pre-execute` / `tools/post-execute` for struggle signals (error,
  slow call, oversized result, repeated identical call) and lower the effort only
  while the streak stays clean.

## 0.1.0

- First working cut: per-request reasoning-effort adjustment for DSH, zero
  dependencies, no session writes.
