docs/HANDOFF-2026-08-03-B.md
Rama master · réplica de lectura del repositorio original.
# 交接说明 — 2026-08-03 第二会话(接 HANDOFF-2026-08-03.md)
> **⚠️ 这不是最新的一份。** 本文写的是 v3.9.8,当前一份是
> [HANDOFF-2026-10-15.md](HANDOFF-2026-10-15.md)。**它的状态表与"仍未做"清单请一律以最新那份为准** ——
> 交接是 append-only 的,旧的不删也不改结论。本文仍值得读的是:末尾"工具失效"一节:哪些结论是实测的、哪些不是。
>
> 另:`ls docs/` 的排序**不等于**时间顺序(`-B.md` 的 `-` 排在 `.md` 的 `.` 之前,而
> `HANDOFF-2026-08-05.md` 的文件名日期是错的)。顺序只看索引 [README.md](README.md)。
**先读这一条:本文末尾的"工具失效"一节说明了哪些结论是我实测过的、哪些不是。
不要把两类混着信。**
上一份:[HANDOFF-2026-08-03.md](HANDOFF-2026-08-03.md)。那份的 §3.1–§3.5 已在本会话
做完(它里面已标 ✅),§3.6 起仍然有效,方法论 §5 全部仍然有效。
---
## 0. 门禁数字权威表(2026-08-03 在 tag 上逐个重测)
散落在各文档里的数字曾经互相矛盾 —— 中间分支的读数被当成了发布基线。以下是在 tag 上
实测的值,**引用数字请用这张表,不要从散文里推**:
| ref | pass | 备注 |
|---|---|---|
| `v3.9.6` | 3119 | v3.9.7/3.9.8 那一轮的真实基线 |
| `v3.9.7` | — | **从未打 tag**,折进 v3.9.8 发的。`RELEASE_NOTES_3.9.7.md` 记录的是一个不存在的发布 |
| `v3.9.8` | 3246 | |
| `v3.9.9` | 3264 | = master |
测量方式:`git checkout <tag> && npm run test:release`,逐 tag 单独跑。
**测量脚本必须在 ref 不存在时中止** —— 我第一次测时 `git checkout v3.9.7` 因无此 tag 而
失败,脚本没停,把 master 的 3264 报成了 v3.9.7 的数字,靠"两行数字一模一样"才发现。
---
## 1. v3.9.8 已发布(实测确认)
```
master 6a954d73d7cf == origin/master
tag v3.9.8 0df2f2c5ac8b7ae9164d01f10967fcc45e667e97
release run 30808601758 completed / success(6 个 job 全绿)
GitHub Release 已发布,非 draft,4 个产物
门禁(master 上跑) 3246 pass / 0 fail,secret-scan EXIT=0
package.json / lock 3.9.8
```
产物:`windsurfapi-macos-arm64` 61.7MB · `windsurfapi-macos.zip` 45.5MB ·
`windsurfapi-windows.zip` 22.7MB · `windsurfapi.exe` 58.9MB。
**macOS x64 job 这次是 success** —— 那是 @andya1lan #233 修掉的那条(v3.9.1–v3.9.4
连续四个版本终态是 cancelled)。这是它修复后第一次真正产出。
v3.9.7 **没有单独发**,两轮合并成 v3.9.8 一个版本。所以仓库里不存在 v3.9.7 tag,
这是有意的,不是漏了。
---
## 2. 本会话做完了什么
按 issue:
| Issue | 状态 | 说明 |
|---|---|---|
| #234 | 主体完成 | §3.1–§3.4 全部修掉并带突变验证 |
| #235 | 主体完成 | 费率表接线,`rate === 0` 判当前免费 |
| #239 | 接线完成 | 逐请求 credit/ACU 通道打通;tag 号待付费校准 |
| #236 | 未动 | 判据太模糊,需真实上游复现 |
commit(全部已在 master 上):
```
6a954d7 docs(release): v3.9.8 — #234/#235/#239 主体 + 台账补第三轮
8e9f8bd test(rate-table): 一条断言钉的是没有行为后果的实现细节
d032386 feat(auth,connect): 接上 credit 费率表与逐请求计费 — 此前零消费者
310c1db fix(auth): connect 目录同步继承了 Cascade 的 latch + 两条自查假测试
b0989ac fix(models,auth): connect 目录永久 latch + 发现视图只宣传不过滤
1c5274e fix(chat): 干旱门禁在生产默认后端结构性不可达 — 接进 connect 短路块
a8b9af2 fix(auth): 干旱谓词跨了命名空间 — 新增 connect selector 专用判定
d9ed81f fix(auth): 单个账号即可宣布全池干旱 — 干旱判定要求最低已知覆盖率
f9c8ca2 docs(handoff): 三处自失效的交接事实 — commit 计数、行号转置、zsh 突变陷阱
```
新增测试文件(基线 3119 → 3246,共 +127 条):
```
test/drought-coverage-floor.test.js 8 条 4 突变全 CAUGHT
test/drought-connect-namespace.test.js 10 条 3 突变全 CAUGHT
test/drought-gate-reachability.test.js 9 条 行为+结构双层,3 突变全 CAUGHT
test/connect-discovery-rebuild.test.js 7 条
test/connect-catalog-delatch.test.js 5 条
test/connect-rate-table-wiring.test.js 12 条 6 突变全 CAUGHT
```
### issue 回复(已发出,公开可见)
- #239 → comment `5165488746`,并移除 `needs-triage` label(现在只有 `enhancement`)
- #235 → comment `5165502836`
- #234 → comment `5165510486`
三条回复里都写明了"哪一部分我验不了"(逐请求 billing tag 号需付费 token 校准),
并在 #239 请社区跑 `DEVIN_CONNECT_DEBUG_META=1 npm run calibrate:devin`。
**如果有人回帖给了 tag 号,那是 #239 的最后一环,接线已经就绪,不用改代码。**
### PR 分流结论:一个都没合
- #238 早已在本地分支合过(台账记 S),GitHub 上还开着只是因为当时分支没推。
**现在 master 已推,#238 应该会自动关闭或可以手动关掉 —— 请核实。**
- #232 / #233 之前已合,#215–#219 全部 closed
- 没有新 PR。作者要求"只要 S 级黄金 PR",所以没有硬凑
---
## 2.5 v3.9.9 — v3.9.8 之后又做完的
release notes 在 [RELEASE_NOTES_3.9.9.md](releases/RELEASE_NOTES_3.9.9.md)。
门禁 **3264 pass / 0 fail**(v3.9.8 tag 实测 = 3246,新增 18 条)。每条都带突变验证。
| 项 | 状态 | 说明 |
|---|---|---|
| §3.8 MODELS 表注入泄漏 | ✅ 已修 | 已删除账号的模型不再被宣传 |
| §3.10-4 sticky 无 selector 维度 | ✅ 已修 | connect 每个 selector 不再互相驱逐 |
| §3.9 dashboard 调用点缺口 | ✅ 已补守卫 | 208 个调用点此前零覆盖 |
| sticky TTL 墙钟偶发失败 | ✅ 已修 | 门禁 3 次里红 1 次,现在连跑 3 次全绿 |
**§3.6 作者本会话明确说跳过**,我开始做了之后回退,未提交任何东西。
### §3.8 的两条自查(形态值得记住)
第一版按"谁注入的"记账,**错的**。applyCloudModels 跳过已存在的 key,所以只有第一个
报告该 UID 的账号有注入记录 —— 移除第一个账号时正确保留(第二个还能到达),但移除
第二个(最后的真实持有者)时什么都不驱逐,照样泄漏。**实测确认了这个失败**,然后改成
全局集合 + 反查"还有账号的目录包含这个 UID 吗"。
调用顺序与第一版相反:必须**先丢目录、再驱逐**;先驱逐会永远看到该账号自己的 UID 仍然
可达。而且这条顺序**只在直接调用 forgetCloudModelCatalog 时承重** —— 经由 removeAccount
的路径上 invalidateModelCatalogForAccount 已经先丢了目录,顺序无关。突变验证发现这条没有
任何断言钉住,补了一条直接调用的测试。
复现时还踩了一个坑:**单条目快照会被缩水确认守卫隔离,applyCloudModels 压根不执行** ——
我第一次复现因此得到假的"no leak"。测试用的快照必须是增长型的。测试里的 staticUids 也
刻意在模块加载时固定,不能实时读 MODELS,否则会把别的账号刚注入的 phantom 算成本账号
合法持有(这个错误让一条正确实现下的测试失败过)。
### §3.10-4 的关键设计点
**不能把 selector 塞进 modelKey。** 两个是不同命名空间:Cascade 走真 modelKey +
selector=null,connect 走 modelKey=null + 真 selector,合并会撞进同槽。所以 bindingKey 是
三维 `caller\0(modelKey||*)\0(selector||*)`。
getApiKey 一直就收着 connectSelector,只是绑定键忽略了它 —— 两侧现在一致,没有新增参数
传递链。
**新引入的 API 陷阱(已用测试钉住)**:2 参数查询 `getStickyBinding(caller, null)` 读的是
`caller\0*\0*`,与 connect 写入的槽不同。生产能解析是因为 getApiKey 传了 selector;直接
调用者必须自己传。connect-sticky-e2e 有三处 2 参数查询因此要改。
**模块加载陷阱**:sticky-session.js 把 `STICKY_SESSION_ENABLED` 读进模块加载期的 `const`,
env 必须在 import 之前设好。我第一版测试用静态 import + beforeEach 设 env,7 条全失败且
失败原因与断言要测的东西毫无关系。用既有的 `loadFresh` 每测新导入模式。
### §3.9 dashboard 守卫的审计结论
**当前没有任何用户数据字段有未转义的 innerHTML sink。** 唯一可疑的一处(probe 汇总插值
`x.email`)走 `toast()`,而 toast 赋的是 `textContent` —— 安全靠上下文而非转义。所以那条
守卫钉的是一个当前成立的性质,职责是在性质失效时叫出来。
自查两条:第一版字段表里的 `labelHash` 在整个 dashboard 里**根本不存在**(我凭印象写的),
守卫的守卫当场抓到 —— 这就是它存在的理由;守卫的守卫计数时 `break` 在第一个匹配字段上,
而 `keyPrefix` 与 `apiKey_masked` 共享同一表达式,于是 keyPrefix 永远记不到、看起来没被
覆盖。
---
## 3. 明确没做的(按建议优先级)
### 3.6 `WINDSURFAPI_STRICT_MODEL=0` 静默改写 + 响应撒谎 —— **作者本会话明确说跳过**
我开始做了,作者说"不用管这个"并要我回退。**已回退,未提交任何东西。**
但本会话对上一份交接的描述做了一处**修正,下一个人接手时很重要**:
上一份说 `test/devin-connect-strict-model.test.js:62-77` "把这个行为断言成期望行为
—— 套件在强制执行 AC5 的违反"。**这个描述不准确。** 那条测试只断言:
```js
assert.notEqual(result.status === 400 && result.body?.error?.code === 'model_not_found', true)
```
也就是"opt-out 不得 400" —— 这正是 opt-out 的正当用途,断言本身是对的。
它**没有**断言响应里的 `model` 字段。所以那个谎言是**没有断言覆盖的**,不是被
固化成契约的。**修它不需要跟测试打架。**
缺陷本体(实测确认):
```
resolveConnectSelector('claude-opus-4-9-medium') → { selector: 'swe-1-6-slow', mapped: false }
chat.js:2989 connectMeta = { ..., displayModel: reqModelName, ... }
devin-connect-openai.js:244 / :416 const model = displayModel || params.model
→ 响应报告 "claude-opus-4-9-medium",实际跑的是 "swe-1-6-slow"
```
修法(我当时打算做的,未落地):`mapped === false` 时 `displayModel` 报实际跑的
selector,而不是请求名。降级本身保留(那是 operator 显式选的),只是不再撒谎。
锚点:`chat.js` 里 `connectMeta = { id: ccId` 那一行,`{ selector, mapped }` 已在作用域内。
### 3.8 ✅ 已修 模块级 MODELS 表注入,无人移除
上一份交接记的实测:`removeAccount` 后 key 仍在、`listModels` 仍列、池 total=0。
**我本会话尝试重新复现,工具返回空,所以这条我没有自己验过。**(见 §5)
我在对话里说过"完全按交接描述复现了",**那句话没有依据,请不要采信** —— 当时
工具输出是空的。缺陷描述本身来自上一份交接的实测,那个可信度独立于我。
注入点在 `models.js` 的 `applyCloudModels`(约 :833),它往模块级 `MODELS` 与
`_lookup` 写入;`clearCloudModelCatalogs` 只清 catalog 状态,不回滚注入。
### 3.9 三条守卫覆盖缺口(第一条已补,余两条仍缺)
- `dashboard-escape-behaviour` 验证 helper 但不验证 207 个调用点
- `connect-dimension-guard` 只检查 `finalizeConnectAccount` 那一段源码
- `handler-route-parity-guard` 的"绑定名被读到"可被一行 debug log 满足
都需要静态分析级改造。上一份说"不适合塞进发版" —— 现在发版已完成,有空间了。
### 3.10 sticky 线 7 条积压(第 4 条已修,余 6 条仍在)
上一份 §3.10 逐条仍然成立,本会话未动。
---
## 4. 本会话新增的方法论(已写进 AUDIT-LEDGER 第三轮)
### 4.1 突变验证成为默认手段,而且它查的东西和对抗复核不重叠
对抗复核查"我的修复有没有引入新缺陷";突变验证查"我的测试有没有在测东西"。
本轮靠它查出**我自己写的 4 条测试是空转的**:
1. "两个行产出器都过滤" —— live 目录在新进程里是空的,producer 2 压根不产行
2. "空响应不缩小并集" —— 带空贡献者的并集算术上本来就不变,**断言错了后果**
3. `sawTable ? free : null` 的区分没有任何断言钉住(谓词对 null 与空集合回答相同)
4. 一条恒真断言 `assert.ok(x.length >= 0)`
**新判别标准(比旧的更早生效)**:问"这条断言在什么输入下会失败?"答不出具体输入,
它就没在测东西。旧标准"功能坏掉这条断言会失败吗"需要先想象一个缺陷,新的只需要
看断言本身。
### 4.2 突变验证前必须先证明 baseline 非零 —— 本轮连拿 4 次假 SURVIVED
全部是 harness 一个测试都没跑。本机 shell 是 **zsh**:
```
node --test $TESTS # 不做词分裂,两个路径当一个参数 → Could not find 'a.js b.js'
read A B < <(cmd) # 进程替换配 read 读不到值
grep --include=*.js # 未加引号被当 glob 吃掉
```
`pass=0 fail=0` **不是**"守卫没抓到",是"没测量"。用 `${=VAR}` 或数组。
### 4.3 去掉一个 latch 不够,外面可能还有一层同类门禁
`trySyncConnectCatalog` 只能从 `fetchAndMergeModelCatalog` 里被调到,而那个函数被
**Cascade** 的 per-key 门禁挡着 —— 一旦某账号 Cascade 目录记为已同步,connect 同步
对它永久不可达,它自己的 per-key 资格压根没被查过。
"只覆盖部分路径"第五次,形态是新的。**修完一个 latch 后问:到达这个 latch 的路径上
还有几个 latch?**
### 4.4 未提交的修复会被突变循环的 git checkout 吃掉
本轮发生**两次**:我在 mutation loop 里用 `git checkout -- <file>` 还原突变,
把同一文件里**尚未提交的真修复**一起还原了,然后下一个突变测的是"修复缺失"而不是
"突变生效",读数完全是假的。
**纪律:先提交修复,再跑突变验证。** 或者用 `git stash` 之外的还原方式。
### 4.5 元守卫按逃逸点枚举,不按 flow
ACP vision 重路由不是 `selectBackend` 的 flow,是 `devin_connect` 逃进
`special_agent`,按 flow 枚举的守卫看不见它。`drought-gate-reachability` 因此枚举
**块内门禁之上的每个 `return`** 并要求每个登记豁免理由,且带"守卫的守卫"(枚举
结果为 0 即失败)—— 一个不再匹配任何真实 `return` 的枚举会永远通过。
---
## 5. 工具失效 —— 下一个人接手前必读
本会话末尾 `Bash` 与 `Read` **都开始返回空输出**(不是报错,是空)。先后确认:
- `echo alive` 一次成功,之后 `echo probe` / `/bin/echo` / `Read package.json` 全部空
- 因此**会话最后一段的所有"验证"都没有依据**
具体受影响的结论:
| 结论 | 可信度 |
|---|---|
| v3.9.8 发布全绿、产物齐全、门禁 3246/0 | **实测确认**(工具失效之前) |
| §3.1–§3.5 的代码与测试、突变验证结果 | **实测确认** |
| 三条 issue 回复已发出 | **实测确认**(返回了 comment URL) |
| §3.6 的 `displayModel` 谎言与那条测试只断言"不得 400" | **实测确认** |
| §3.6 的编辑已回退、树干净、在 master 上 HEAD=6a954d7 | **实测确认** |
| §3.8 的注入泄漏"本会话复现成功" | **没有依据,不要采信** |
| 分支 `fix/models-injection-and-guard-gaps` | **工具恢复后已核:从未创建** |
工具层随后恢复了,上表最后一条已经核实。补充实测确认的收尾状态:
```
分支 master(未创建任何 fix 分支)
工作树 干净(只有本文未跟踪)
master vs origin in sync
tag v3.9.8 0df2f2c5ac8b
src/handlers/chat.js 与 HEAD 一致(§3.6 编辑已完全回退)
```
**所以 §3.8 / §3.9 / §3.10 是完全没开始的状态,不存在半成品分支要清理。**
### 接手第一步(必须先做)
```bash
cd /Users/dwgx/Documents/Project/WindsurfAPI
echo alive # 先确认工具层活着
git branch --show-current
git status --short # 期望:干净
git log --oneline -1 # 期望:6a954d7
git rev-parse master origin/master # 期望:相同
git ls-remote --tags origin v3.9.8 # 期望:0df2f2c5ac8b...
git branch --list 'fix/models-injection-and-guard-gaps'
```
若那个分支存在:**它上面零 commit**,我什么都没提交进去。可以直接删掉重开,
或者留着接 §3.8。若工作树有 `src/handlers/chat.js` 的改动,那是我 §3.6 的残留,
**作者说过跳过 §3.6,直接 `git checkout --` 掉。**
`npm run test:release` 应该 3246 pass / 0 fail。**如果不是,先查这个,再动别的。**
---
## 6. 需要作者拍板的
1. **#238 现在能不能关**(master 已推,它的内容早就在里面了)
2. **`total_tokens` 含 `cache_write`** —— 上一份交接的遗留,仍未决。复核认为破坏
OpenAI 契约;但 Cascade 路径(#118)早已刻意如此并写明理由。改它会同时改两条
路径并推翻既有决定,这是产品取舍
3. **#236 的 A 支要不要救**(上一份 §3.7)。判据模糊,需真实复现
4. **§3.6 要不要做** —— 本会话作者说跳过,但它是 #234 验收标准里最强调的一条,
而且现在已知"没有测试固化那个谎言",修起来比上一份交接描述的容易