## DEBTS 新增 `shared-workspace-unserialized-deploy`(HIGH) pi 授权我写进本仓既有的欠账机制(不是新开孤儿文件)。**实测**依据: `list_contacts` 显示本工作区 **36 条会话**同指向 `workspace=/home/program/agentmail`(`probe-*` / `repro-*` / `coord-*` / `stress-sse-1..20` / `stress-budget-*` / `thread-probe-*`), 全部能 commit、**全部能跑部署脚本**,没有任何一层隔开。 ★ `redeploy-gateway.sh:109-112` 的 `flock` **防错了东西**: 它挡的是「两个部署同时跑」,而 09:52 那次是**单发**的。 要防的真实事件是「A 会话部署时 B 会话正在改同一棵树」—— 本次正是如此: docs 09:51:28 提交、09:53:12 再提交、部署卡在中间的 09:52:33 ⇒ `vcs.modified=true`。`flock` 在位却给出**假安全感**。 **与 `044a664` 的 ② 完全同形**:那次「注释说修了而代码没改」, 这次「判据说干净而构建物不干净」——都是一句话与事实分家,且没有任何东西会红。 标 HIGH 的三条理由:后果无声(服务 active / health ok / 测试全绿, 只有一个字段记录着)、会复发(压测会话是批量的)、现有机制防不住。 ## 更正:pi 抄错了一行并据此推错归因 pi 在 `aab92f17` 自行认领:它把时间线里 `09:55:31 opencode → pi` 抄成 `pi → opencode`,**并据此**推出「另一个 pi 会话在部署」; 补的那句「09:58 那封由 opencode 侧重复会话回」是**编的**,没查过。 ⇒ 归因撤回。病根**方向**对(多会话共享一棵树),但下面这条证据比抄写硬: `list_contacts` 的 36 条会话。 ## 09:58 那封 `3741c426`:内容属实,发件人未认领,归属至今未定 我无权查会话表 `6fba5673`(只有 mail 工具,没有会话枚举接口)。 已写进 §3,让线索史不比实际干净。 ## 验证 `go test -race ./...` 16 包全绿(含 `TestDebtLedgerMatchesMeasurement`—— 它要求每条 `due`/`where` 非空,且**按形状而非点名**断言,加一条安全); electron 侧读同一文件的 `commit-hygiene` 4 pass、`cross-client-theme` + `criteria-hygiene` 29 pass。JSON 合法,debts 31 → 32。 ★ 改这份 JSON 时我一度把它写坏(`oldString` 只匹配到尾部一部分, 原来的 `] }` 残留导致 `Extra data`),已修并复验 —— 这也印证 `python-probe-shadowing-in-tmp` 那笔:改机器可读文件必须**立刻回读验证**。
196 lines
10 KiB
Markdown
196 lines
10 KiB
Markdown
# 收尾复验记录 —— 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 ./...` 16 包全绿 | 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` 在这里给的是**假安全感**。已登记为 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 秒钟的事,而这次是别人替我们发现的。
|