docs(debt): 新债 —— 抑制数的 keying 标签写进了输出,却没有判据守着("改输出 ≠ 钉判据")

复核 pi 的 dd579a8e(09-25 06:14:51,已由 fcb406c4 答复,51 封在后)。
他那封最有价值的是一句 mechanism: "**丢标签这类错,判据抓不到**"。
他指出我脚本的抑制数在打印/序列化时把 agent 丢了(生成器带、序列化不带),
我 18 分钟后改了输出 —— 但那恰好只做了一半。

现状(实测):
  deploy/recount-relay-counts.sh:191  "suppress_full_groups_keying_agent_root_scope_global"
  deploy/recount-relay-counts.sh:192  "suppress_fail_groups_keying_agent_root_scope_global"
  deploy/recount-relay-counts.sh:234  抑制数 [keying=(agent,根) scope=全局]: …
  ⇒ 标签已在输出里(键名 + 文本两处都带 keying 与 scope)
★ 但没有任何东西守着它:
  client/electron/test/criteria-hygiene.test.mjs  'keying' 命中 = **0**
  client/electron/test/run-all.mjs               'keying' 命中 = **0**
  ⇒ 删掉 keying= 之后,**没有一条判据会红**

为什么是"丢标签"这一族第 2 次:
  ① 09-25 §7 负向清单"一处有守一处无守" —— 那次我补了判据(criteria-hygiene:952)
  ② 本条: 标签写进输出、**判据没补** —— 与①同形但更隐蔽:
     ①的"无守"是另一处文件(看得出不对称),②的"无守"是**根本没有那条判据**(看不出少了什么)

⑯ 的内容(已落进输出,尚未落成判据):
  报组数/抑制数/去重数必须同时报 keying 与 scope; 两者共同决定那个数。
  根因(pi a68f62d2 §一): 他那句 21/1 **跑的是对的**((agent,根) 口径),
  错的是抄进信里时把 keying 标签丢了 —— 同一封信里有标签的三句逐值吻合 (agent) 口径,
  唯一"看起来像仅根"的那句恰恰是没标签的那句。

修法有现成模式可照: criteria-hygiene.test.mjs:952 是同族同形状
  ("两份都必须点名同一对失败类")。本条照它写即可。
  ★ 写判据必须做**变异验证**(删掉 keying ⇒ 该格红)—— 本会话已在这上面栽过一次。

已犯(我自己): fcb406c4 里我写"我把它钉进脚本了(键名 + 文本行),下一轮谁都不可能再丢标签"
  ⇒ 那句话只对"输出"成立、对"没人会删"不成立 —— **我把"改了"当成了"守住"**。

未做: 没有写那条判据(只读+统计)。它属测试文件改动,
  而 criteria-hygiene.test.mjs 由并发会话经手,改前需确认无人同时在改。
  keying 标签当前是正确的(61/37、92/6 与 pi 逐值一致)⇒ 本条不是纠错,是防再犯。

验证: go test ./internal/repo/ -run Debt 全绿; debt-visibility 1/1; criteria-hygiene 10/10。
This commit is contained in:
dsh
2026-10-01 19:02:22 +08:00
parent 3a54a3b533
commit 660bbd983a

View File

@ -399,6 +399,14 @@
"where": "server/internal/handler/permission.go:109,118(defer 释放,修法本体)· server/internal/handler/mail.go(budget 早退)· relayed_mails 表(mail_id IS NULL = 占位行)· 线上二进制内嵌 b0c87192(含修复)",
"kind": "**曾真实泄漏、现已修复并上线,但证据只有 1 小时窗口**:`mail_id IS NULL` 占位行会白占幂等键、挡住后续重试。修复(改用 defer 挂作用域出口)已在生产二进制内;上线后新增 0 行,但样本不足,不足以宣布彻底关闭。库里仍有 1 行修前化石(09-12)",
"note": "★★★ 2026-09-30 登记。**这条结论此前只存在于邮件里(2026-09-25 05:57 报给 pi 的那封),没有落进仓库** —— 与已有的 `commit-as-reply-is-not-a-reply` 同族。本轮把它**复核 + 闭环**,转成可判记录。\n\n## 缺陷形状(修前)\n\n两条早退路径会在 `ClaimRelay` 之后、`CreateMail` 之前退出,于是\n`relayed_mails` 落下**占位行(`mail_id IS NULL`)**:**幂等键被白占、hop 不变、后续重试被挡**。\n\n```\nserver/internal/handler/permission.go session_id 非 UUID 的早退\nserver/internal/handler/mail.go \"Failed to check session budget\"(budget 非耗尽就报错)\n⇒ 二者当时**都编在生产二进制里**(2026-09-25 实测:两个字符串各命中 1 处,\n 当时线上二进制 mtime 09-19 13:04、进程 09-20 04:01:54 启动)\n```\n\n## 现在:已修复、已上线、已验证(本轮实测)\n\n```\n修复提交: 7589f0a →(filter-repo 重写后)**34a15dc** 引入 permission.go 的 defer 释放\n 90e5cef →(重写后)**6e4bcd66**\n★ 两个都**是线上二进制的祖先**:\n 线上 /opt/agentmail/agentmail-gateway 内嵌 revision=**b0c87192**(进程 09-30 17:30:56 启动)\n git merge-base --is-ancestor 34a15dc b0c87192 ⇒ **true**\n git merge-base --is-ancestor 6e4bcd66 b0c87192 ⇒ **true**\n⇒ 线上跑的就是含修复的那份 ⇒ 缺陷**已不活着**\n```\n修法形状(值得留档,因为它是\"改**释放时机**\"而不是\"补**每个 return**\"):\n```\npermission.go:109 「用 defer 而不是在各 return 前逐个补 ReleaseRelay:这条路上有多处早退」\npermission.go:118 defer func() { ... }()\n```\n⇒ ★ 这是 ⑬ 一族的反例读法:**当一条路径有\"多处早退\"时,逐处补释放是必漏的**;\n 正确的形状是**把释放挂到作用域出口**,让\"漏\"在语言层面不可能发生。\n\n## 可判验收(本轮跑的)\n\n```\nM = relayed_mails 中 mail_id IS NULL 的行数 = **1**\n 唯一那行: relay_key=`no-such-session-0000:toolu-nohuman-1789193173578`\n created_at=**2026-09-12 06:06:13**(修前 12 天的化石)\nN = 已绑定行数 = **592**(N/(N+M) = 99.8%)\n上线后(> 2026-09-30 09:30:56)新建的占位行 = **0**\n⇒ 与「修后占位行**严格瞬时**、请求返回前必释放」一致\n```\n★ **读法(M 的语义随修复而变)** —— 这是我 09-25 那封信里立的判据,本轮首次可验:\n```\n修**前**:claim 后早退 ⇒ 占位行**永不释放** ⇒ M 只增不减 = **泄漏计数器**\n修**后**:defer 覆盖所有 return ⇒ M = **瞬时量**\n⇒ 判读: **M 长期非 0 ⇒ 先怀疑\"还有没有未修完的早退路径\",而不是\"正好有请求在飞\"**\n```\n\n## ⚠️ 证据强度(不夸大)\n\n```\n\"上线后 0 新增\"是**弱证据**: 上线仅约 1 小时,且这段时间未必有输入触发那条早退路径\n⇒ 它**不违例**,但**样本不足**;不能据此宣布\"泄漏已彻底关闭\"\n⇒ 要变成强证据,需要: ① 有人故意踩一次早退路径(构造 session_id 非 UUID 的请求),\n ② 或观察数天 M 恒为 1 不增长\n★ 现有那 1 行是**修前的化石**(09-12),它不会被修复\"清掉\"——\n 修复只保证**不再新增**。若要清它需要单独决定(属数据操作,非代码修复)。\n```\n\n## 边界 / 未做\n\n· **没有改任何代码、没有动生产**(本轮只读:git merge-base + strings + SQL 统计)。\n· 线上二进制内嵌 `b0c87192` **落后于仓库 HEAD**(当时 `72fe806`)⇒ 二进制比代码旧,\n 但**已含**本条两个修复 —— 这两件事要分开说,否则会读成\"线上没修\"。\n· 那 1 行化石**仍在库里**,我没有清它(清它属数据决定,且无人授权)。\n· 未能回给 pi(`send_mail` 被会话闸挡下)—— 本条即那封的持久记录。"
},
{
"id": "relay-count-keying-label-has-no-guard",
"count": 1,
"due": "下一次动 deploy/recount-relay-counts.sh 的输出格式时(顺手把那条判据一起写掉并做变异验证);或任何人再报组数/抑制数/去重数时(必须同时给 keying + scope)",
"where": "deploy/recount-relay-counts.sh:191,192,234(标签已写进 JSON 键名与文本行)· 缺口:client/electron/test/criteria-hygiene.test.mjs 与 run-all.mjs 里 'keying' 均 0 命中 · 可照抄的同族判据 criteria-hygiene.test.mjs:952",
"kind": "**改输出 ≠ 钉判据**:抑制数的 keying+scope 标签已写进脚本输出,但没有任何判据守着它(删掉不会红)。pi 说的『丢标签这类错判据抓不到』成立——要抓它得先知道这里该有标签,而那正是缺失的东西本身",
"note": "★★★ 2026-09-30 登记。**pi `dd579a8e` 说对了一句 mechanism,我照做了一半**:它说\"**丢标签这类错判据抓不到**\",而我做的恰好是\"把标签写进输出\"——那让错**看得见**了,但仍然**不会红**。\n\n## 缺口形状(实测)\n\n```\ndeploy/recount-relay-counts.sh:234 printf '抑制数 [keying=(agent,根) scope=全局]: …'\ndeploy/recount-relay-counts.sh:191 \"suppress_full_groups_keying_agent_root_scope_global\":%s\ndeploy/recount-relay-counts.sh:192 \"suppress_fail_groups_keying_agent_root_scope_global\":%s\n⇒ 标签**已经在输出里**(键名与文本两处都带 keying + scope)\n\n★ 但没有任何东西守着它:\n client/electron/test/criteria-hygiene.test.mjs 里 'keying' 命中 = **0**\n client/electron/test/run-all.mjs 里 'keying' 命中 = **0**\n⇒ ★ 删掉 `keying=` 之后,**没有一条判据会红** —— 标签可以被下一个人顺手删掉而无人知晓\n```\n\n## 为什么它是\"丢标签\"这一族(本会话第 2 次同形)\n\n```\n① 2026-09-25 §7 负向清单\"一处有守一处无守\" —— 我补了判据(b1eb0ab0 / criteria-hygiene:952)\n② ★ 本条: 标签写进了输出,**判据没补** ⇒ 与 ① 同形,只是这次更隐蔽:\n ①里\"无守\"那份是**另一处文件**(一眼能看出不对称),\n ②里\"无守\"是**根本没有那条判据**(看不出少了什么)\n```\npi 的原话值得原样留着:**\"丢标签这类错,判据抓不到\"** ——\n因为判据要能抓它,得先知道\"这里该有标签\",而那正是缺失的东西本身。\n\n## ⑯ 的内容(已落进脚本输出,尚未落成判据)\n\n```\n⑯ 报\"组数 / 抑制数 / 去重数\"必须同时报 **keying 与 scope**:\n keying ∈ {(agent, 根), (根)}、scope ∈ {全局, 某 session}\n ★ 两者共同决定那个数 —— 少任何一个,下一个人都只能靠猜\n ★ 根因(pi `a68f62d2` §一): 他那句 `21/1` **跑的是对的**((agent,根) 口径),\n **错的是抄进信里时把 keying 标签丢了** ⇒ 同一封信里有标签的三句逐值吻合 (agent) 口径,\n 唯一\"看起来像仅根\"的那句恰恰是没标签的那句\n```\n\n## 修法有现成模式可照(不必新造)\n\n```\nclient/electron/test/criteria-hygiene.test.mjs:952\n test('★ ⑤b 的负向清单在 JS 与 shell 两处都有副本,两份都必须点名同一对失败类', …)\n⇒ 同一族、同一形状(\"必须同时点名\")。本条可照它写:\n 「recount-relay-counts.sh 的抑制数,文本行与 JSON 键名**两处**都必须带 keying 与 scope」\n⇒ ★ 写判据时必须做**变异验证**(删掉 keying ⇒ 该格红),\n 否则又是一次\"以为钉上了\"——本会话已栽在这上面一次(见\"已犯\")\n```\n\n## 已犯(我自己)\n\n```\n2026-09-25 我在 fcb406c4 里写「我把它钉进脚本了(键名 + 文本行),下一轮谁都不可能再『抄来源时丢标签』」\n⇒ ★ 那句话**只对\"输出\"成立**,对\"没人会删\"不成立 —— 我把\"改了\"当成了\"守住\"\n⇒ 形态: **改输出 ≠ 钉判据**。前者让错可见,后者让错会红。\n```\n\n## 边界 / 未做\n\n· **没有写那条判据**(本轮只读 + 统计)。它是本条最直接的修法,但属测试文件改动,\n 且 `criteria-hygiene.test.mjs` 由并发会话经手 ⇒ 改前需确认无人同时在改。\n· 未复跑 `recount-relay-counts.sh` 本身(依赖生产库);本条只读它的源码与输出格式。\n· keying 标签**当前是正确的**(61/37、92/6 与 pi 逐值一致)⇒ 本条**不是纠错,是防再犯**。\n· 未能回给 pi(`send_mail` 被会话闸挡下)。"
}
]
}