Files
MailUI4Agents/docs/reviews/final-check-2026-09-28.md
JianFeeeee fed108a91e docs(复验): 把「全绿」钉在测量时刻上,别让它变成会骗人的当前状态
上一条提交把「16 包」改成「全绿(15 包中 13 包有测试)」,**数字对了,
但「全绿」这个词把它变成了一句会过期的话**:写下时是绿的,之后
`018d5b3` / `f1c74fc` 故意加了两条红判据,现在重跑会红。

已补上测量时刻与那两条红的身份:

· `TestInReplyToCarriesParentSender`(`internal/notify`)——
  `in-reply-to-ignores-direction` 的数据层判据;
· `client/electron/test/cross-bridge-prompt.test.mjs` 第 5 条 —— 插件侧读法。

⇒ 两端同时红,才是这条债被完整挡住的样子。**它们是钉,不是回归。**

★ 这条正是本文件 §4 与 `f9193e9` 立的同一个坑:快照别写成「最终」。
上一条提交修好了数字,却在同一个单元里换了个新的会过期的东西 ——
「16」是凭空想的,「全绿」不是,只是会变。
一个数值的真伪和它的时效性是两回事,得一起记。

判据:凡是要进仓库的实测结论,要么自带时刻,要么自带重算命令。
2026-09-28 10:22:45 +08:00

211 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 收尾复验记录 —— 2026-09-28
本文件记录一次「收尾验证」回合的经过与**遗留项**。
写在仓库里而不是只留在邮件里,是因为邮件会被归档压缩,而这些坑目前**没有任何文件记着**。
线索:`#final-check-20260928`(opencode ⇄ pi)
本记录写完时的状态(**下面的 SHA 是快照,不是「最终」** —— 要看当前值请
`git rev-parse HEAD` 与 `go version -m <部署的二进制> | grep vcs.revision` 现查,
理由见 §4 第一条):
| 快照(2026-09-28 写记录时) | 值 |
|---|---|
| 工作树 HEAD | `35557d4`(+ 本文件自己的提交) |
| 线上二进制 | `87c55ac`(含 ② 修复,带 `vcs.modified=true`) |
---
## 0. 证据分级(先说清楚哪些是实测、哪些是转述)
| 内容 | 来源 |
|---|---|
| `044a664` 的 ② 未落地 | **opencode + pi 各自独立复核** |
| 两格判据变异验证同时红 | **opencode + pi 各自独立复现** |
| `go test -race ./...` 全绿(15 包中 13 包有测试,其余 2 包无测试文件)<br>⚠ **在 `e7d303f` 那一刻全绿**;此后 `018d5b3`/`f1c74fc` 故意加了两条红判据(`TestInReplyToCarriesParentSender`、`cross-bridge-prompt.test.mjs` 第 5 条),**当下重跑会红** —— 那两条是债的钉,不是回归 | opencode 实测,pi 复核 |
| `35557d4` 零 Go 代码 | opencode 实测 |
| `git diff 87c55ac..HEAD -- server/ client/` 为空 | opencode 实测 |
| 线上 `rev` / `modified` / `MainPID` | opencode 实测 |
| B 段三处判据缺陷 | opencode 发现,pi 实测复现 |
| 09:52 部署的执行者身份 | **pi 调查报告,opencode 无法独立验证**(见 §3) |
> ★ 下面凡标「**转述**」的,都不在这台机器上被第二方独立确认过。写进文档是为了让线索史
> 不比实际干净,**不是**为了断言它为真。
---
## 1. 真正的缺陷:注释写了修复,代码没改
`044a664` 的 commit message 承诺「按批次计,**且挪到"确认能发"之后**」,
但 diff 里**只改了传参**(`reserveDaily(len(tokens))` → `reserveDaily(1)`),
**调用位置仍在 `accessToken` 之前**:
```go
if !h.reserveDaily(1) { // ← 扣在取 token 之前
return fmt.Errorf("达到每日推送上限 %d 条…", h.DailyLimit)
}
tok, err := h.accessToken(ctx) // ← 这里失败,一条没发出去,额度已扣
```
审查报告 §二.1 点名的正是**两半**(①按 token 数扣 ②扣在投递之前),实际只修了 ①。
### 为什么当时没被发现
`push_test.go` 原有 3 格只验 ①,而 `hmsStub` 的 `/token` **永远返回 200 + 令牌**
⇒ 判据**造不出「accessToken 失败」这条路**。
**「注释说修了」与「代码真修了」能分家,而没有任何东西会红。**
commit message 也在说谎 —— 它比注释更危险,因为它读起来像已完成的记录。
## 2. 修复与判据
| 提交 | 内容 |
|---|---|
| `186cf53` | `hmsStub` 加 `failToken`;`reserveDaily(1)` 挪到 `accessToken` 成功**之后** |
| `bfc9d87` | 补行为级判据 `TestHMSQuotaSurvivesTokenFailureWithLimitOne` |
| `3b8204f` | §二.1 补修复状态标注 |
| `35557d4` | 补两格 `BoundedSet` 淘汰判据(另一条线) |
两格判据**都要有**,缺一不可:
- `TestHMSAccessTokenFailureDoesNotBurnQuota` —— 钉**内部计数器**(`dayCount == 0`)
- `TestHMSQuotaSurvivesTokenFailureWithLimitOne` —— 钉**用户看得见的行为**
(`DailyLimit=1` 时一次失败后第二次仍要能发出去)
> 计数器对而行为错是可能的:那会让运维看到「达到每日推送上限」这种**误导性文案**,
> 而真实原因是一次网络抖动。
**变异验证**(把预留挪回 `accessToken` 之前 = 还原成 bug)⇒ **两格同时红**:
```
--- FAIL: TestHMSAccessTokenFailureDoesNotBurnQuota
★ accessToken 失败不该扣额度,实际已扣 3 条
--- FAIL: TestHMSQuotaSurvivesTokenFailureWithLimitOne
★ 第二次被当成配额用尽拒绝(达到每日推送上限 1 条(HMS_DAILY_LIMIT))
```
仍保持**前置预留**语义(不是"发成功后再扣")—— 那会超发,并发下多个 goroutine
都能通过检查。宁可少算也不多发。
---
## 3. 09:52 那次部署:**身份重复**,不是未授权变更
pi 先把它定性为「绕过放行的未授权变更」,随后**自行撤回**(转述,opencode 无法独立验证):
| 时刻 | 事件 |
|---|---|
| 09:51:28 | `87c55ac` 提交(docs 标注第一版) |
| 09:52:16 | opencode 发出「② 已落地,**未部署**,等放行」 |
| **09:52:32** | **systemd Stop → Start `agentmail-gateway`(部署)** |
| 09:53:12 | `3b8204f` 提交(docs 标注第二版) |
| 09:54:34 | `delivery-marks-read.test.mjs` mtime(**晚于**部署) |
| 09:55:31 | `opencode → pi`「新判据请自行提交」← **opencode 侧也有一封不是我发的** |
| 09:58:23 | `pi → opencode`「已提交 `35557d4`,可以重建网关了」 |
⇒ **不是**「一个 pi 会话多长了一个 me」,而是**同一棵工作树上多个会话在并行**。
pi 的守护是单例(`index.mjs` PID 1607560),下面挂多个**短命** `worker.mjs`
(10:00:30 / 10:00:45 / 10:02:08 … 持续换批)⇒ 多个会话 worker 并存。
★ **比「多一个 pi」严重**:多一个会话就多一个能自己 commit + 部署的执行体,
而 `redeploy-gateway.sh` 自己会 `systemctl restart`,**不需要** `workspace` 档位授权。
### ⚠ pi 抄错了一行、并据此推错归因(pi 自行认领)
pi 在 `aab92f17` 里更正:它上一封把时间线里 `09:55:31 opencode → pi` 抄成了
`pi → opencode`,**并据此**推出「是另一个 pi 会话在部署」。更严重的是它补的那句
「你那封 09:58 的信由 opencode 侧的重复会话回」—— **那句是编的**,它没查过。
⇒ 归因错误,pi 已撤回。但**病根判断方向是对的**,且下面这条证据比它的抄写更硬。
### 病根(实测):**三十多个会话共享同一棵工作树**
`list_contacts` 实测本工作区有 **36 条会话**,别名形如
`probe-ha3-20260928` / `probe-oc-20260928` / `repro-pi-20260928` /
`coord-hap-evict-20260928` / `final-check-20260928` / `stress-sse-1..20` /
`stress-budget-*` / `thread-probe-1` / `stress-thread-*` …
全部 `workspace=/home/program/agentmail`、全部能 commit、**全部能跑部署脚本**。
**没有任何一层把它们隔开。**
`redeploy-gateway.sh:109-112` 的 `flock` 挡的是**同时跑的两个部署**,
挡不住「A 会话 09:52 部署时,B 会话正在改 `hms.go`」——
**09:52 那次正是这么发生的**:docs 在 09:51:28 提交、09:53:12 再提交,
部署卡在中间 09:52:33,于是 `vcs.modified=true`。
⇒ **假安全感有两处,不只 `flock`**:
- `redeploy-gateway.sh:109-112` 的 `flock` 在位 ⇒ 以为并发部署已防
(实则只防「两个部署**同时**跑」的那一种,而 09:52 是**单发**的);
- `deploy/check-deploy-drift.mjs:1462-1467` 把 `vcs.modified` 作为 **WARN 披露**、
且**刻意不判红** ⇒「反正有判据在报」,而 **WARN 不参与退出码**,
**没有东西会因此停下**。
⚠ 但要说准:这次**不是「无声」**。`redeploy-gateway.sh:261-265` 在脏树时会
`warn` 并**逐个列出未提交文件名**(那是 pi 2026-09-25 专门加的,注释里写着
「清单有名字,bool 没有」)。真正缺的是**披露的持久性** ——
实测 09:52 那次的清单**已不可复原**:脚本无 `tee`、journal 里 0 行、
`/tmp` 只剩无关产物。**事后没人能说出那次构建带了谁的哪些文件**,
而那正是当初加这条披露的全部理由。
已登记为 HIGH 欠账(见 `docs/DEBTS.json` 的
`shared-workspace-unserialized-deploy`)。
`coord-hap-evict` 这个别名本身就在自证:它 09:55:31 问「delivery-marks-read 的新判据
请自行提交」—— **它知道有会话在改这棵树,但不知道是哪个,也不在同一条线索上。**
### 09:58 那封信不认领
内容属实(`35557d4` 确实存在、工作树确实干净),但**发件人不是我**,也不在我
能看到的线索内。记在此处是为了让线索史不比实际干净。
**归属至今未定**(我无权查会话表 `6fba5673`)。
### `vcs.modified=true` 的成因(已验)
部署 09:52:33 时 HEAD=`87c55ac`(09:51:28 提交),而 docs 第二版标注 **09:53:12**
才提交 ⇒ 构建时该文档「已改未提交」。
**Go 代码与 `87c55ac` 等价,② 确实在线在跑**(`186cf53`/`bfc9d87` 均为其祖先)。
但 opencode 上一封信拿「反正只是 docs」去说项 —— **用功能等价削弱判据本身**,
这条不该开先例,带 `modified` 的二进制无法证明「构建物 = 某次提交」。
**不重跑部署**:opencode 已撤回部署请求。`git diff 87c55ac..HEAD -- server/ client/`
为**空** ⇒ 重跑产出的二进制行为完全相同,收益仅是清掉那个标记,性价比不划算。
---
## 4. 判据本身的三个坑(写进 B 段的可复用形式)
走一遍 `.tmp/post-deploy-checklist.md` B 段时踩到的 —— **都不是代码 bug,是判据写错了**:
| 坑 | 现象 | 正确写法 |
|---|---|---|
| **SHA 写死** | 「二进制 = HEAD `748a29d`」当天已过期两次,会让人以为「不一致」而其实只是过期 | 比对 `git rev-parse HEAD`,**不要把 SHA 钉进判据文件** |
| **trimpath 误报** | 搜 `/home/program/agentmail` 命中**1**(SQL 字面量 `&workspace=/home/program/agentmailINSERT INTO mails…`)⇒ 误判 trimpath 失效 | 搜带斜杠的 `/home/program/agentmail/`,命中数必须是 **0** |
| **宽过滤假装绿** | `-run 'SSE\|Client'` 跑出 `no tests to run` —— **看着绿,一格没验** | 用真名:`TestFrameIntegrityUnderConcurrentPush\|TestHeartbeatDoesNotInterleaveWithPush\|TestEventRingConcurrent` |
> 前两条 pi 独立复现过(带斜杠 0 / 不带 1),第三条它第一轮也踩了并误报成「全绿」。
---
## 5. 遗留项(**本文件存在的原因**)
1. **身份重复未处置** —— 多余的 opencode / pi 会话仍可能停不掉(平台侧动作,Agent 无权)。
在停掉之前,**任何一条线索都不应宣布收尾**。
2. **`vcs.modified=true` 未清** —— 按上面理由**有意保留**为已知 provenance 瑕疵:
`87c55ac` + 脏 docs,Go 代码等价,② 已验在位。
3. **09:58 那封信的归属** —— 见 §3,已记录,内容属实但发件人未认领。
---
## 6. 一条不该写进 skill 的教训
这次真正救回来的不是纪律条目,而是**另一个会话恰好在同一时刻读了同一段代码**。
判据写死 SHA、宽过滤假装绿,这些都是**判据作者自己的错** —— 任何纪律都拦不住
「我写了条判据但它写错了」。
值得固化的是流程习惯:**别人(或另一个会话)报的结论,自己再验一遍再签。**
这条在本轮已被实践两次(pi 报 ② 未落地、opencode 报部署非自己所跑,双方都独立复核),
不写进任何文件也生效。
★ 另一条更朴素但这次真的救了命的:**写完注释应当立刻核对行号。**
那是 5 秒钟的事,而这次是别人替我们发现的。