Ctrl K
ComunidadGuías…

docs/HANDOFF-2026-08-04-E.md

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# 交接说明 — 2026-08-04 (E)

> **⚠️ 这不是最新的一份。** 本文覆盖 v3.9.17 → v3.9.19,**当前一份是
> [HANDOFF-2026-10-15.md](HANDOFF-2026-10-15.md)**。它的状态表与"仍未做"清单请一律以最新那份
> 为准 —— 交接是 append-only 的,旧的不删也不改结论。
> 本文仍值得读的是:§5(三条方法论)、§6(PR #241 的复测与合并)、§7(发版后 fan-out 复核)。
> 另注:**`ls docs/` 的文件名排序不等于时间顺序**,见索引 [README.md](README.md)。


**上一份**:[HANDOFF-2026-08-04-D.md](HANDOFF-2026-08-04-D.md)。它的 §0 与 §4 已过期(见
§1);§5 两条仍然有效。方法论沿用 [AUDIT-LEDGER.md](AUDIT-LEDGER.md) 第十一、十二轮。

---

## 0. 当前状态

```
版本 / tag    3.9.19 / v3.9.19(tag 落在 master 上)
master        == origin/master,已推
门禁          3449 pass / 0 fail(264 个文件,exit 0)← 含合并的 #241,见 §6/§7
突变          六个 spec 共 67 条:63 CAUGHT + 4 条声明的 expectCaught: false
              ← 本文首发时这一行是 60 条且**测不出来**,见 §0.1
secret-scan   EXIT=0  ← 仍然跳过 test/ 且只读被跟踪文件,见 §4.2
工作树        干净,只剩 master;远端也只剩 master
CI runtime    九个 action 已升 node24;deprecation 警告实测 0 次命中
开着的 issue  #236(等报告者数据) · #239(等付费号) · #208
开着的 PR     无 —— #241 已合(§6)
```

### 0.1 上面那行突变数字,本文首发时是测不出来的(2026-08-05 补)

`strict-usage-total.json` 的 `expectBaselinePass` 从 `71127dc`(08-05 00:37,删掉"Cascade
流式路径没有 recordAccountSpend 调用点"那条断言,18 → 17 条)起就是过期的,而 spec 没跟。
`npm run mutate` 的 guard 2 因此在**第一条突变之前**就中止整个 spec:

```
mutate-verify: baseline pass=17 but spec expects 18.
```

本文首发于 08-05 01:50 —— 漂移之后 73 分钟。所以"六个 spec 共 60 条"那行当时**最多只能
跑到 49 条**,那个数字不是测出来的。

已修(`47a1d12`,把 baseline 改成 17)。修完重跑,60 条的分布**如实**是:

| spec | CAUGHT | 声明的漏网 |
|---|---|---|
| `dashboard-connect-parity` | 12 | 1 |
| `retry-rescue-budget-split` | 11 | 1 |
| `shard-tied-prefix` | 7 | 1 |
| `sticky-collapse-bypass` | 3 | 0 |
| `sticky-queue-on-pin` | 12 | 1 |
| `strict-usage-total` | 11 | 0 |
| **合计** | **56** | **4** |

**值得记的不是数字错了 —— 数字恰好是对的。** 错的是它作为"我跑过"的证据被写下来,而那一轮
它跑不起来。一个碰巧正确的未测量数字比一个错的更难发现:下一个人按 §3 跑循环会看到红,然后
去怀疑自己的环境,而不是怀疑这行字。

**动作**:交接里每个数字旁边要能回答"这一次是谁跑出来的"。删掉或新增断言之后,核一遍有没有
spec 钉着旧计数 —— 但**别用静态数 `it(` 的办法**,我试过,它在 `retry-rescue-budget-split`
上报了一个假 MISMATCH(spec=71 / 数出来 63),因为那个 spec 跨两个文件且有循环生成的测试。
可靠的口径是运行时计数:

```bash
# 每个 spec 的 tests[] 逐文件取门禁日志里的 `ℹ pass N` 求和,与 expectBaselinePass 比
```

零成本的替代:**直接跑 `for s in test/mutations/*.json; do npm run mutate -- "$s"; done`** ——
guard 2 就是干这件事的,而且它比任何静态探针都准。这次六个 spec 已全部核过、全部一致。

### 门禁数字权威表

| ref | pass | 新增 |
|---|---|---|
| `v3.9.13` | 3401 | |
| `v3.9.14` | 3418 | +17 |
| `v3.9.15` | 3427 | +9 |
| `v3.9.16` | 3431 | +4 |
| `v3.9.17` | 3433 | +2 |
| `v3.9.18` | **3447** | +14(#241 的 11 + 我补的 3) |

测量口径 `npm run test:release`(逐文件进程隔离)。**文件数 264**(v3.9.18 起,#241 带来
`messages-thinking-contract.test.js`)。

---

## 1. 上一份的过期处

- **#234 已关闭**(v3.9.15)。六条验收判据逐条核过,第六条是真缺陷并已修
- **#235 已关闭**(两半:v3.9.15 + v3.9.16)
- 又发了三个版本,门禁数字变了三次

---

## 2. v3.9.15 / 16 / 17 发了什么

各自的 release notes 里有完整机制。一句话版:

- **v3.9.15** — #234 收尾:Dashboard `/models` 与 `/v1/models` 此前对 connect 命名空间**零
  重叠**(面板列 163 行全调不了、唯一调得了的不在列表里)。改成一个共享谓词两边都调 + 面板
  **标注而非删行**。另修 Cascade 流式不记 per-account 消费;九个 CI action 升 node24
- **v3.9.16** — #235 后半:面板显示每个模型**吃不吃配额**,三态(免费/计费/**未知**)。费率表
  接线 v3.9.8 就发了,但判定函数只有一个生产调用者、在干旱判定内部 —— 数据躺了八个版本没出口
- **v3.9.17** — 裸写 `glm-5-2` 拿到了这个模型**会忽略**的工具方言(`glm47`),后果是不发出任何
  tool call、客户端一直等。其余写法本来都对,所以它一直藏着

---

## 3. 接手第一步

```bash
cd /Users/dwgx/Documents/Project/WindsurfAPI
echo alive
git status --short                             # 期望:干净
git rev-parse master origin/master             # 期望:相同
git describe --tags master                     # 期望:v3.9.19 或 v3.9.19-N-g…(见下)
npm run test:release                           # 期望:3449 / 0(264 个文件)
for s in test/mutations/*.json; do npm run mutate -- "$s"; done   # 期望:六个全绿
```

**不要用 `--exact-match`。** 本文首发时写的是 `git describe --tags --exact-match master`
期望 `v3.9.17`,而 tag 之后又落了文档 commit(`6480f24`、`3d49657`),那条命令必然
`fatal: no tag exactly matches` —— 五步里第三步假报警。带 `-N-g…` 后缀是正常的:说明
master 领先 tag N 个 commit,只要那 N 个不含行为变更就不需要发版。

**两条会立刻绊住你的操作纪律**(本轮各踩了三次):

1. **突变循环启动后就不要碰仓库。** 它每一步都在 `写入 → 跑测试 → git checkout HEAD -- 还原`,
   期间任何别的编辑都会被 guard 6 判脏并中止整轮。要并行干活就去 worktree
2. **commit 前先切分支。** `.githooks/pre-commit` 会拒绝在 master 上 commit,而它拒绝时你那次
   `git add` 已经做了 —— 工作树仍是脏的,紧接着跑突变会被 guard 1 再拒一次

---

## 4. 仍未做的

### 4.1 等外部

- ~~**PR #241(warelik)** —— 等他改两处~~ —— **他 08-04 18:41 UTC 就改完推了,这一行从写下
  的那天起就是过期的。** 三条全部处理:nudge 累积按建议改成从原始 `params` 重建并补了专门
  断言、第 2 条覆盖声明撤下改写、field-0 护栏收进编码器。已 rebase 到 `3d49657`,
  `MERGEABLE`/`CLEAN`。**不在"等外部"这一节**,等的是维护者复测
- **#236** —— 已向报告者列出**四项能相互区分候选机制**的信息(代理 debug 日志里 tool call 的
  原始形态、绕开 subapi 直连、换模型对照、以及确认 `GLM-5-2-NONCE` 那个拼法是不是笔误)。
  标题说"浏览器操作报错"是误导,正文其实是**工具调用停滞**,与 #237/#238 同族。
  **拿到前两项基本就能定位** —— 别在拿到之前从代码倒推着改
- **#239** —— 需付费号跑 `DEVIN_CONNECT_DEBUG_META=1 npm run calibrate:devin`。
  **注意范围**:本文此前把它记成 billing tag 校准,而 zhonxinya 提的 issue 正文是
  **"支持获取真实的 ACUs 计费"这个功能请求** —— 校准是它的前置,不是它本身

### 4.2 已知的记账口径问题

- **`secret-scan` 跳过 `test/`** 且只读被跟踪文件。"secret-scan 干净"要带这个范围说
- **四条有据可查的漏网**:`lastUsed` 项 / `WAITABLE` 白名单 / backoff 倍数 /
  `known()` existence 项。各自的理由写在对应 spec 的 name 里
- **`RELEASE_NOTES_3.9.12.md` 那处 5.6x 错引刻意不改**(append-only,且已作为 Release body 发布)

### 4.3 未 exhaustive 扫描

见台账文末那张表(`Ctrl-F` 搜 `仍未 exhaustive 扫描`,**取最后一个命中**)。

---

## 5. 本轮最该记住的三条

台账第十二轮有完整版。

### 5.1 "更正"比新增声明更危险

我判定台账上一轮那句"已加测试钉住"是假的,写成更正落进台账 —— **而守卫一直在**
(`strict-usage-total.test.js`,源码扫描形态,它的失败消息里甚至预先写好了"哪天流式路径真的
记账了就删掉这条")。

怎么错的:grep 提到 `recordAccountSpend` 的测试文件(它命中了),再对命中文件 grep
`stream|cascade`(它又命中了),然后**看着命中行判断那是 usage 算术测试、没打开读**。两次
grep 都指向了正确的文件,我在最后一步用眼扫过去就下了结论。**是全量门禁抓到的,而且在修复
已经提交之后。**

新增声明会被下一轮审;而"我核过了、上一轮那句是假的"会让下一个人**停止怀疑**那个位置。

**动作**:要断言"这个面没有守卫",grep**被守对象的名字**在 `test/` 下的全部命中并**逐个打开
读**。搜索能证明"存在",证明"不存在"需要穷尽,而穷尽必须包括读。另:源码扫描形态的守卫在按
行为搜索时是隐形的。

### 5.2 一条 SURVIVED 先问"这段代码今天会被执行吗"

本轮两条 SURVIVED,处置**相反**:一条是我自己刚写的死代码(删),一条会执行但没有输入能让它
说话(记 `expectCaught: false` + 钉住让它无害的前提)。**两者读起来一样。** 把死代码当漏网
留着,正是 v3.9.13 那条 medium 缺陷的形状。

### 5.3 三态字段的断言必须钉住是哪一态,不是排除其中一态

#235 那个 `currentlyFree` 三态字段,我第一版断言只检查"没有行被错报成 `false`"。突变验证
证明漏掉的恰好是**危险的那个方向**:把未知拍平成 `true`(免费)时那条断言全绿 —— 而那正是
报告者那次损失本身(pro 号上把计费模型当免费用,烧掉周限)。

**排除一态的写法在另外两态之间是瞎的,而通常有一态明显更危险。**

---

## 6. PR #241 已合(2026-08-05 补)

`1e2aaea`。救援提示词现在引用上一次失败尝试的 reasoning 尾巴(默认取尾 2000 字符),
上游 swe-1-7 在带 tools 的轮次里只发 reasoning 不给 tool call 时,模型能接着自己刚才的
思路把动作发出来。

**已随 v3.9.18 发布**(`0e16817`)。这是一个真实行为变更且默认开,所以单独发了一版而不是
攒着 —— 与 v3.9.12 那次"零行为变更所以不发版"的判断相反,判据是同一条:有没有东西需要用户
去装。

### 6.1 复测了什么

| 项 | 结果 |
|---|---|
| PR head 单独跑门禁 | 3444 / 0(264 文件),与他声明一致 |
| 合并 + 我的收紧后 | **3447 / 0(264 文件)** |
| 六个 spec | 66 条:62 CAUGHT + 4 声明漏网,全部 EXIT=0 |
| secret-scan / `git diff --check` | EXIT=0 / 干净 |

**第 2 条等价性我没采信他的说法**:`chat.js:2844` 与 `chat.js:3088` 是字面相同的表达式、
同一个 `const`、中间未重新赋值;整个 `src/` 只有 `chat.js` 构造 `connectParams`,三处重新
spread(`3229`/`3295`/`3435`/`3454`,**四处**)都保留 `.tools`。门禁从 flag 改成请求派生量
确实等价。

> 这句原本写"三处(`3295`/`3437`/`3454`)"。复核查出两个错:实际是**四处**(漏了 `3229`),
> 而 `3437` 这一行根本不存在(是 `3435`)。**结论不受影响** —— 漏掉的 `3229` 同样是
> `{ ...connectParams, token, deviceSeed }`,照样保留 `.tools`。但枚举错了就是错了,而这句
> 同样写进了 merge commit `1e2aaea` 的 message(那条已是历史,不改)。
> 教训:声称"共 N 处"时要把 N 也当成一个需要测量的数字,`grep -c` 一下就够。

### 6.2 合并后我补的一条真缺陷

`DEVIN_CONNECT_RESCUE_REASONING_MAX_CHARS` 没有上限,而**同一个函数上面十二行的
`RESCUE_MAX` 已经因为同一个洞加过钳**(`RESCUE_MAX_CEILING = 5`,注释写着 `1e9` 会让请求
挂几个小时)。驱动复现:50000 字符 reasoning + `1e9` → nudge **50089 字节**,该请求每一次
救援都送这么大。已加 `DIGEST_MAX_CEILING = 32000`(`eedf274`)。

**32000 是判断,不是实测的上游上限** —— 真实上游 body 上限从来没探过,源码注释里写明了。

### 6.3 一条我自己的误判,记在这里

我一度把"空串静默关掉 digest"也算成这个 PR 的缺陷。驱动核实后**不是**:
`Number('') === 0` 过 `>= 0`,这是这个文件里**四个旋钮共有的既存约定**
(`RETRY_ON_EMPTY_MAX` → 0 次重试、`RETRY_ON_EMPTY_MS` → 0 退避、`RESCUE_MAX` → 救援关)。
新旋钮照抄邻居、一致。所以**没改行为**,只改了两处说谎的文字(源码注释原写"非数值会回落到
默认值而不是静默关掉",以及 `.env.example` 补"留空 = 关,想要默认值就注释掉整行")。

**动作**:说"这个 PR 引入了 X"之前,先看邻居有没有同一个 X。四个旋钮里三个早就这样,
那就不是这个 PR 的缺陷,是仓库的约定 —— 处置完全不同(改文档 vs 改代码)。

### 6.4 合并静默打断了两条既有突变 anchor

`retry-rescue-budget-split` 有两条突变的 anchor 是 rescue 的判据那一行,而 PR 往里插了
`hasTools`。guard 3 在 anchor 匹配次数 != 1 时 die,所以下次跑突变循环会红。

**测试套件不跑 spec,所以他的 3444/0 和我的复测都看不见。** 同一原因它的 baseline 也过期了
(合并那一刻就该从 71 变 76)。已修 anchor + baseline,并补 6 条突变(含"把 #241 那条累积
缺陷原样打回"—— 让评审第 1 条修掉的东西有常驻突变守着,而不是只靠一条断言)。

**动作**:改了 `src/` 之后,除了跑门禁,还要核 `test/mutations/*.json` 里有没有 spec 钉着
被改的那几行。零成本的做法是直接跑一遍突变循环 —— 门禁绿**不代表** spec 还成立。

### 6.5 我在同一个会话里第三次踩了"数字不能算,要测"

我把这个 spec 的 baseline 从 71 加 3 算成 74,guard 2 报出真值 **79**。71 本身是合并前的数,
我拿一个已知会过期的存量当基数。

今早我刚给 §0.1 写下"可靠口径是运行时计数或直接跑 guard 2",然后用了比静态计数更糟的办法。
**存量数字不能当基数。** guard 2 抓了我 —— 这是它今天第三次抓到东西(spec 6 的 18、
这次的 74),三次都是对的。

### 6.6 留给下一轮的一条(零行为,我刻意没做)

`rescueThinkingOnly` 现在是**死配置**:默认从 `false` 翻成了 `true`,而
`devin-connect-openai.js` 的两个调用点(`334`、`559`)都传字面 `true`,真正的门是函数里的
`hasTools`。核实过没有任何测试传 `false`,所以这个参数已经不可能变化。

**这正是 v3.9.13 那条 medium 缺陷的形状**(被导出的旋钮零实际变化,而突变守卫守在到不了线上
的代码上)。收掉它是零行为变更。

**没在 v3.9.18 里做的理由**:`retry-rescue-budget-split` 有三条突变的 anchor 含这个参数名,
收掉它要第二次改那些 anchor —— 而 anchor 今天已经出过两次问题(一次是 #241 打断的、一次是我
把 baseline 算错的)。为一个零行为的清理项在发版前动第三次不划算。

做的时候要一起改:`119` 的默认值、`228` 的条件、`334`/`559` 两个调用点、以及那三条 anchor。
改完必须跑完整突变循环,不能只跑门禁。

### 6.7 门禁口径外的两条既存失败

`test/_research/responses-cache-hit-seq.test.js` 2 条失败。**在 v3.9.18 之前的 `3d49657` 上
同样失败**,而 `run-test-shard.mjs` 只取 `test/*.test.js` 顶层,所以 `_research/` 从来不在
`npm run test:release` 的口径里(`secret-scan` 也跳过它)。既存,本轮没动。

**注意**:那个测试会**写自己的 fixture**(`responses-cache-hit-seq-A/B.json`)。误跑一次就会
让工作树变脏,紧接着跑突变会被 guard 1 拒。本轮踩过一次,已 `git checkout HEAD --` 还原。

---

## 7. v3.9.19 —— 发版后对抗复核(2026-08-05 补)

发版后对 v3.9.18 的全部 11 个 commit + 仓库 192 个 md 文件跑了一轮 fan-out 复核:15 个 lens
(7 代码 + 8 md 分片),每条发现再走一个**独立的对抗验证器**。40 条唯一发现里
**35 CONFIRMED / 5 REFUTED**。

**密度最高的三个文件正是这一轮改得最多的**:交接 E(10 条)、`devin-connect-openai.js`
(8 条)、台账(8 条)。也就是说复核主要在审**这一轮自己的产出**,不是在重扫老代码 ——
那与台账"复核范围优先给新代码"的结论一致。

### 7.1 一条真缺陷:v3.9.18 的钳只堵了一半

`Math.min(0.5, 32000)` 正确地得到 `0.5`,而消费者 `slice(-n)` 对 `0<n<1` 朝零截断:
`slice(-0.5)` === `slice(0)` === 整个字符串。实测 `MAX_CHARS=0.5` 送出全部 50000 字符 ——
**正是那道钳要挡的后果**。已加 `Math.floor`(`2451ec8`),随 v3.9.19 发布。

**判据:钳住取值范围不等于钳住语义。** 加钳之后要问"这个数最终被谁消费,那个消费者对非整数、
对 0、对边界值的行为是什么"。

### 7.2 一处已发布的版本归属错误

`RELEASE_NOTES_3.9.18.md` 把裸救援提示归到 "v3.9.16 起",**实际是 v3.9.8**(`793ed79`,
PR #238)—— 差八个版本,而 v3.9.16 的 notes 里一个 rescue/nudge 字都没有。已就地改正 +
留勘误,GitHub Release body 同步。**版本号直接影响"我该从哪一版升上来",所以不能只留勘误。**

### 7.3 anchor 在一天里被打断三次,第三次是我自己

1. #241 合并往 rescue 判据里插 `hasTools`,打断两条(`9830571` 修)
2. baseline 两次过期(`47a1d12`、`87bd1e9`)
3. **我自己的 `Math.floor` 包住了我自己写的 anchor**(`a916f8e` 修)—— 而这是在我把前两条
   写进台账之后

真正的启示不是"要记得核 anchor",而是 **anchor 按整行文本匹配,所以任何包裹式改写都会打断它**。
处置:把 anchor 锚在**表达式**上而不是整行(去掉行首缩进与 `? `),表达式比整行耐改写。

### 7.4 这轮 workflow 本身的两个教训

- **一次 40 个验证器并发会被网关限流**:前 16 个返回、17–39 全部 stall,而 `pipeline()` 是
  全部并发。改成 4 个一批顺序跑,58 条全部返回。台账第五轮记过"单个 agent 跑太久会丢全部结果",
  这是同一条的另一个形态:**扇出太宽和跑太久一样会丢**
- **验证器必须有 `UNVERIFIED` 这个态**。第一版脚本把"没返回"和"没问题"混进同一个
  `filter(Boolean)`,那正是 `confirmed: 0` 被当成"通过"的形状。已改成显式第三态并在报告里
  单列

---

## 8. 这份交接到此为止(v3.9.19 已发)

按 `docs/README.md` 的约定:**一份交接对应一个版本,发了 tag 就起新文件。** v3.9.19 已打 tag,
所以下一轮请新建 `HANDOFF-<日期>.md`,不要继续往这份里追加,并在
`docs/README.md` 的必读表里把第 1 条指向它、给本文加前向横幅。

起新文件时把这几条带过去(它们是跨版本仍然有效的):

- §3 的接手第一步(**不要用 `--exact-match`**)
- §4.1 的 #236 / #239 状态与 #239 的范围更正(它是 ACUs 计费的功能请求,校准只是前置)
- §4.2 的记账口径问题(`secret-scan` 跳过 `test/`、四条有据可查的漏网)
- §6.6 的 `rescueThinkingOnly` 死配置,以及 §7 的四条(钳住范围≠钳住语义、anchor 锚表达式、扇出宽度、UNVERIFIED 第三态)
- §6.7 的 `_research/` 既存失败 + 那个会写 fixture 的坑