docs(欠账): 共享工作树无互斥 ⇒ HIGH;并更正 pi 的归因错误

## 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` 那笔:改机器可读文件必须**立刻回读验证**。
This commit is contained in:
2026-09-28 10:08:25 +08:00
parent f9193e9e39
commit d3a7873258
2 changed files with 41 additions and 4 deletions

View File

@ -249,7 +249,15 @@
"due": "有人决定把 zcode 真正部署回来时(豁免自动失效,无需手工改)",
"where": "deploy/check-deploy-drift.mjs 的 RETIRED_HOSTS(豁免本体)",
"kind": "scope",
"note": "2026-09-28 新登记。zcode 宿主已在本机删除,但插件与单元**故意留在仓库里**以便恢复;\ncheck-deploy-drift.mjs 的 HOSTS 是硬编码四家,于是两条判据永久红,且结论区建议的\n`redeploy-plugin.sh zcode` 是**错的动作**(会部署一个用户已决定不运行的宿主)。\n⇒ 加了显式豁免表 RETIRED_HOSTS,但**豁免只换表述、不压红**:\n · checkHost 仍逐条列出「已退场」+ 理由,只是不计入 stale;\n · checkLayout 的 note 里明写「已退场宿主、故意不装:<单元>」,不静默跳过。\n\n★★ 本条欠的是**真实踩过的坑**(两次,都是我自己写的检查自己漏):\n ① 第一版 isRetiredHost 只查 units[0],实测「只装 zcode-mail-bridge.service、\n 不装快照」时**仍被判成已退场**,且那个新装上的单元在 note 里被写成\n 「故意不装」—— 自相矛盾且完全静默。⇒ 改为 units.every 逐个查。\n ② 第二版用 existsSync 判软链存在,而 `current` 是**相对**软链:快照目录被删、\n 软链残留成**断链**时 existsSync 返回 false ⇒ 「装过又删剩」与「从没装过」\n 读数**完全同形**,实测断链场景仍被判「已退场」。⇒ 改用 lstatSync\n (看得见软链本身,含断链)。\n 同族:与 python-probe-shadowing、baseline-residue 一致 ——「没检查到」≠「不存在」。\n\n已实测四态:A 全没装⇒豁免;B 有效软链⇒报漂移;C 断链⇒报漂移;D 只装一个单元⇒报漂移。\n欠的是:豁免逻辑本身**没有自检样本**进 `--self-check`(现靠人测,\n 而判据的纪律是「能自证就别靠人测」⇒ 应补:注入假 fs 让四态各跑一遍,\n 且必须有一格是**断链**样本(第一/二版都恰好漏在断链上)。"
"note": "2026-09-28 新登记。zcode 宿主已在本机删除,但插件与单元**故意留在仓库里**以便恢复;\ncheck-deploy-drift.mjs 的 HO"
},
{
"id": "shared-workspace-unserialized-deploy",
"count": 1,
"kind": "并发**不受控**:多会话共享一棵工作树,部署与改代码之间无任何互斥(`flock` 只挡同时跑的两个部署)",
"due": "给 workspace 加**跨会话互斥**(哪怕只是「部署时若有未提交改动就拒绝」这一条保守判据)时。★ 到期动作不是'再小心一点'——本次已经证明了小心没用:pi 与 opencode 各自按流程走,仍产出带 `vcs.modified=true` 的二进制。",
"where": "`deploy/redeploy-gateway.sh:109-112` 的 `flock`(只挡并发部署,**不挡** A 会话部署时 B 会话正在改源码);`docs/reviews/final-check-2026-09-28.md` §3(本次完整经过);本工作区实测 **36 条会话**同指向 `workspace=/home/program/agentmail`,别名 `probe-*` / `repro-*` / `coord-*` / `stress-sse-1..20` / `stress-budget-*` / `thread-probe-*`,全部能 commit、全部能跑部署脚本",
"note": "★★ 2026-09-28 登记(opencode 实测,pi 独立复核)。\n\n## 形状:**能无声地产出一个「无法证明来源」的二进制**\n\n`redeploy-gateway.sh:109-112` 有 `flock`,注释里把它列为环境前提表里\n「同时性」那一类,写得对 —— **但它防的是错的东西**。\n\n要防的真实事件是:**A 会话 09:52 跑部署,B 会话正在同一棵树上改文档**。\n`flock` 只在两个部署**同时**跑时生效,而 09:52 那次是**单发**的,没人跟它抢。\n\n本次实测:docs 在 09:51:28 提交、09:53:12 再提交,部署卡在中间的 09:52:33\n⇒ 构建时工作区有未提交改动 ⇒ 线上二进制 `vcs.modified=true`。\n\n★ **与 `044a664` 的 ② 完全同形**:那次是「注释说修了而代码没改」,\n这次是「判据说干净而构建物不干净」,**都是一句话与事实分家,且没有任何东西会红**。\n\n## 为什么标 HIGH\n\n① 后果**无声**:服务照常 active、`/health` ok、测试全绿,\n只有 `vcs.modified` 一个字段记录着「这个构建物无法证明等于某次提交」;\n② **会复发**:压测会话(`stress-sse-1..20`、`stress-budget-*`)是**批量**的,\n下一次批量跑就会再压一次;\n③ 现有机制**防不住**:`flock` 在位却给了「已经防住并发了」的**假安全感**。\n\n## 我做过的、能立刻复用的处置\n\n· 判据侧已把 B 段那条 SHA **从写死改成现查**(`git rev-parse HEAD`)——\n 写死的 SHA 当天就因两次部署而过期两次,会让人误判「不一致」而其实只是过期。\n· 部署前先 `git status --short` 空,再跑;不空就**先别跑**。\n· 判据要**钉会红的行为**(本次:`TestHMSQuotaSurvivesTokenFailureWithLimitOne`\n 断言第二次不能是「上限」),只钉内部状态(`dayCount==0`)不够 ——\n 计数器对而行为错是可能的,那会给运维一条误导性文案。\n\n## 一条更贵的建议(未做,需平台侧)\n\n会话级互斥或工作树独占。现在同一 workspace 上有 30+ 个会话别名,\n`coord-hap-evict-20260928` 这个别名本身就在自证问题:它 09:55:31 问\n「delivery-marks-read 的新判据请自行提交」—— **它知道有会话在改这棵树,\n但不知道是哪个,也不在同一条线索上。**"
}
]
}