docs/HANDOFF-2026-08-04-C.md
Rama master · réplica de lectura del repositorio original.
# 交接说明 — 2026-08-04 (C)
> **⚠️ 这不是最新的一份。** 本文写的是 v3.9.14,当前一份是
> [HANDOFF-2026-10-15.md](HANDOFF-2026-10-15.md)。**它的状态表与"仍未做"清单请一律以最新那份为准** ——
> 交接是 append-only 的,旧的不删也不改结论。本文仍值得读的是:§5.1(突变的 CAUGHT 计数是跨文件求和的)。
>
> 另:`ls docs/` 的排序**不等于**时间顺序(`-B.md` 的 `-` 排在 `.md` 的 `.` 之前,而
> `HANDOFF-2026-08-05.md` 的文件名日期是错的)。顺序只看索引 [README.md](README.md)。
**上一份**:[HANDOFF-2026-08-04-B.md](HANDOFF-2026-08-04-B.md)。它的 §0 状态表和 §4 未做
清单都已过期(见 §1);它的 §5 仍然全部有效,是上手前最该读的一节。方法论沿用
[AUDIT-LEDGER.md](AUDIT-LEDGER.md) 第九至十一轮。
---
## 0. 当前状态
```
版本 / tag 3.9.14 / v3.9.14(tag 落在 master 上,CI + Release 全绿)
master == origin/master,已推
门禁 3418 pass / 0 fail(261 个文件,exit 0)
突变 五个 spec 共 47 条:44 CAUGHT + 3 条声明的 expectCaught: false
(lastUsed 项 / WAITABLE 白名单 / 退避倍数,各有记录在案的理由)
secret-scan EXIT=0 ← 仍然跳过 test/ 且只读被跟踪文件,见 §4.3
工作树 干净,只剩 master;远端也只剩 master
发布产物 Docker + macOS arm64/x64 + Windows exe 全部成功;
windsurfapi-macos.zip = 45MB(含 x64;不含时约 22MB)
```
### 门禁数字权威表
| ref | pass | 新增 |
|---|---|---|
| `v3.9.11` | 3302 | |
| `v3.9.12` | 3342 | +40 |
| `v3.9.13` | 3401 | +59 |
| `v3.9.14` | **3418** | +17 |
数字请只引这张表。测量口径是 `npm run test:release`(逐文件进程隔离);全量 `npm test`
会因 `--test-force-exit` 输出竞态波动。
**注意 v3.9.14 起文件数是 261 而非 260** —— 见到 260 是旧数。
---
## 1. 上一份交接的三处过期
1. **`#240` 已修并发布**(v3.9.14)。它的 §4.1 把这条列为"需你拍板";判据后来从代码里找到
了(见 §2),不再需要拍板
2. **它完全没提开放的 PR #241** —— 那个 PR 比 #240 早 22 分钟开,而 #240 的正文引用了它。
已审完并回评,状态见 §3
3. **门禁数字与文件数都变了**(3401/260 → 3418/261)
**动作**:写"未做"清单时按 `gh pr list --state open` 与 `gh issue list --state open` 各核
一遍,不要凭本轮碰过什么来回忆。漏记一个开着的 PR 比漏记一条 issue 更贵 —— 那边有人在等。
---
## 2. v3.9.14 发了什么
### `#240`:一次 thinking-only 救援会吃掉空回复重试的预算
`streamChatWithEmptyRetry` 里两条投机重发的臂各有上限,但共用循环计数器:救援臂一直有独立
的 `rescueAttempt`/`rescueMax`,而空回复臂的判据读 `for (…; ; attempt++)` 的 `attempt`,两条
臂都靠 `continue` 进下一轮 —— 于是每次救援顺带花掉空回复的预算。**单向**。
默认值下:2 次救援后遇空回复 → 客户端收到 `""`;同一个空回复前面无救援 → 治好。
**判定它是非预期耦合而不是刻意策略,依据在代码本身:救援臂自带一对计数器。** 共享一个池
的设计不需要第二个。遇到"这个耦合是不是故意的"先找**冗余** —— 如果按"刻意"的解释某个既有
结构是多余的,那个解释大概是错的。
**总上限刻意不变**(`1 + max + rescueMax`)。它拆分前**就已经够得到**,只在"先空回复"那个
顺序上;所以饿死不是"上限太低",是"上限的花法依赖顺序"。
**一个把饿死"修"成抬高上限的改动,在只测饿死的测试里长得一模一样** —— 所以两个方向各有
断言。
报告者 warelik 附 66 天生产数据(158 次 empty-retry、6 次救援、零次可归因的耗尽)。
### 工具:`FORCE_COLOR` 让 `mutate-verify` 恒报"你的套件不绿"
node 给汇总行套 SGR 码,而计数正则锚在行首行尾 → 恒得 `pass=0` → guard 2 拒绝启动。**失败
在安全侧**(0 计数造不出假 SURVIVED),但报错把责任推给套件。
带 `FORCE_COLOR=3` 时 `test/mutate-verify-harness.test.js` 自己 **6/10 红** —— 门禁此前绿只
因环境恰好没设这个变量。修好后带该变量跑全量门禁确认封口。
**如果你的 shell 或 CI 导出 `FORCE_COLOR`,v3.9.14 之前的 `npm run mutate` 在你机器上从来
没真的跑过。**
---
## 3. 接手第一步
```bash
cd /Users/dwgx/Documents/Project/WindsurfAPI
echo alive # 工具层活着吗
git status --short # 期望:干净
git rev-parse master origin/master # 期望:相同
git describe --tags --exact-match master # 期望:v3.9.14
npm run test:release # 期望:3418 / 0(261 个文件)
for s in test/mutations/*.json; do npm run mutate -- "$s"; done # 期望:五个全绿
```
**可选但建议**:`git config core.hooksPath .githooks`。范围见上一份 §3 那张表(五条路径已
逐个实测)。
---
## 4. 仍未做的
### 4.1 等外部贡献者
- **PR #241(warelik,+439/−26)** —— 已审完并回评
([评审](https://github.com/dwgx/WindsurfAPI/pull/241#issuecomment-5178604707))。测试质量
高:六条审查突变全 CAUGHT、零源码 grep 冒充守卫、零条件断言。回给他三条:
1. **要改**:nudge 会累积,digest 把代价放大 43 倍(`RESCUE_MAX=5` 时 242 → 10457 字符),
且第 3 次调用起模型会看到两条互相矛盾的"你刚才的 reasoning"。修法一行
(`origMsgs` 取 `params.messages` 而非 `attemptParams.messages`),我验证过他 233 条断言
全绿 —— 累积没有断言覆盖,不是刻意契约
2. **理由需补证或改写**:PR 第 2 条声称"rescue 此前仅模拟模式"。connect 路径上
`chat.js:2844` 与 `3088` 是**字面相同**的条件、`connectTools` 是 `const` 从未重新赋值,
所以 `emulateTools` ⟺ `params.tools` 非空,native 模式下老门本来就是真。这是等价改写,
不是修覆盖缺口
3. 备注(不阻塞):字段号 0 在 replay 关闭时不可达,但守卫在调用方而非 `writeStringField`
- 他的 base 是 v3.9.10,需 rebase 到 v3.9.14 后重跑
### 4.2 需要核实后才能关
- **#234 主体已修但 issue 还开着**。`auth.js:358` 现在按 connect 后端切 `freeTierModels`
(`connectBackend ? [...FREE_REACHABLE_SELECTORS] : getTierModels('free')`),正是报告里那个
`swe-1-6-slow` vs `gemini-2.5-flash` 的错配。**但它的验收清单有六条,我没逐条核** ——
关别人的 bug 报告之前要核完。这是最容易接的一条
### 4.3 已知的记账口径问题
- **`secret-scan` 跳过 `test/`** 且只读被跟踪文件。v3.9.14 的 6 个改动文件里 4 个在它的盲
区,已手工扫过增量(无十六进制串、无 session token 形态、无硬编码凭据)。"secret-scan
干净"这句话要带上这个范围说
- **退避倍数是有据可查的漏网**:所有测试设 `RETRY_ON_EMPTY_MS=0`,倍数不可观测。墙钟断言
按第四轮那条 sticky TTL 先例否掉。已记成 `expectCaught: false`
- **`RELEASE_NOTES_3.9.12.md` 里那处 5.6x 错引刻意不改**(append-only 历史,且已作为
GitHub Release body 发布)。记在这里免得被当成新发现
### 4.4 卡在真实上游
- **billing tag 号校准**(#239):需付费号跑 `DEVIN_CONNECT_DEBUG_META=1 npm run calibrate:devin`
- **#236 GLM 工具调用停滞**:判据太模糊,需真实复现
- **Cascade 流式路径不记 per-account 消费**:既有缺口,已加测试钉住。补它要往热流式路径加
一个 `recordAccountSpend` 调用点
- **#235**(如何只用免费模型)是个提问,可以直接回
---
## 5. 本轮学到的
台账第十一轮有完整版。两条最会直接害到下一个人的:
### 5.1 突变的 CAUGHT 计数是跨文件求和的,别用它推断"我这条断言咬住了"
按第六轮的标准逐条查 12 条断言有没有空转,查出四条,其中一条**不是冗余而是坏的**:
"主开关覆盖两条臂"那条,**把主开关从空回复臂删掉它照样通过**。第一轮我传的是
thinking-only,而救援预算此时已是 0,那一轮既不可救援也不算空,请求在第 1 次调用就结束 ——
空回复臂的判据压根没被走到。它对自己命名的那件事零断言。
而工具当时报的是 `CAUGHT 1` —— 那 1 条失败来自**另一个文件**。工具每条突变只打印前 4 个
失败名、且混着 suite 名,所以看不出来。**要按叶子测试名逐条收。**
### 5.2 探针给出"零/空"时,先拿已知非零的输入喂同一个探针
本轮我自己写坏四个探针,全部表现为空输出或零结果:POSIX awk 不支持 `\s`(差点据此断言"没有
重新赋值")、拿一个在旧基线上不存在的测试文件当探针(node 什么都没跑)、`parseFields` 是返回
数组不收回调(差点据此写"解码侧静默丢掉字段 0",实际它原样产出)、门禁计数用 `tail -40`
截掉了汇总。
第五轮就记过"没看到信号先问探针坏没坏"。**四次里有三次我的第一反应仍是"被测对象没有那个
性质"。** 唯一让我快速识破的是第三次 —— 因为那个结论听起来太好,正好支持我想写的那句话。
**动作**:比复查逻辑更快的是喂一个已知非零的输入进去。它同时验证探针和被测对象。