diff --git a/docs/DEBTS.json b/docs/DEBTS.json index 8bf5ef6..b85d820 100644 --- a/docs/DEBTS.json +++ b/docs/DEBTS.json @@ -439,6 +439,14 @@ "where": "relayed_mails.relay_key(最后一段 = 被指向那封的 mail_id)· 桥侧落点 plugins/*-mail-bridge/src/index.ts(出站生成 key 处)· 实测环 e43496ed→2ffb7dbb→84900edd→b3789a21→41ea9a15 · 载荷侧 notify/mail.go grep relay=0(故加字段要改网关)", "kind": "**『报告的报告』的自激环,判据已在元数据里**:本封 relay_key 指向的那封 ∈ relayed_mails ⇒ 该抑制,网关契约一个字不用改(逐跳实测:第 2 跳起 4/4 命中,第 1 跳是根不命中;链长实测最长 5 层但判据逐跳独立 ⇒ 不需要『整链抑制』的配套机制)。本轮修正 pi 一处口径误读——标题启发式在真值口径下 **10/10 全中、漏 0**,问题是它答的是另一个问题(『是失败报告』59 封 vs 『在报告报告』10 封),拿它抑制会误杀 59 封真报告 本轮把 pi 的『暂未发现反例』升级为**四个否证方向逐条实测,三个为空**", "note": "★★★ 2026-09-30 登记。这条在会话里由 pi `2053c8db` 提出(2026-09-25 04:53:14,我 20 分钟后 `148a4220`/`27fd0135` 两封答复),**账本里此前没有**。本轮把它连同**一处口径修正**一起落进来。\n\n## 问题:失败报告的\"环\"(报告的报告)会自激\n\n```\n一条失败报告本身也被 relay(kind=summary、relay_key=`*-failure:<上游id>`)\n⇒ 它的失败回报又生成一条新失败报告 ⇒ 环\n⇒ 抑制点必须在 **ClaimRelay 之前** return(否则占住幂等键,见 `relay-placeholder-leak-…`)\n```\n\n## ★ pi 找出的位:**回查 `relayed_mails`**,网关契约一个字不用改\n\n```\n载荷里**没有** relay 身份(notify/mail.go 全文件 grep relay = **0**;mail.go:39 的 RelayKey\n只用于入站校验、不下发)⇒ 载荷加字段这条路要改网关\n★ 但元数据里**已经完整存在**: 本封的 `relay_key` 最后一段就是**被指向那封的 mail_id**\n 判据 = 「本封的 relay_key 所指向的那封 ∈ relayed_mails」⇒ 这是一封**报告的报告** ⇒ 抑制\n⇒ 桥只要拿本封来信 id 去查一次 relayed_mails 即可,**不依赖标题、不依赖载荷**\n```\n实测逐跳(2026-09-30,pi 那条环 `f76025c9` 的 5 跳):\n```\n第 1 跳 e43496ed target=b3ce9d0f 在 relayed_mails? **False** ← 根,回报合法\n第 2 跳 2ffb7dbb target=e43496ed **True** ← 该抑制\n第 3 跳 84900edd target=2ffb7dbb **True**\n第 4 跳 b3789a21 target=84900edd **True**\n第 5 跳 41ea9a15 target=b3789a21 **True**\n```\n⚠️ `f76025c9` 那封本身在 mails 表里**已查不到**(归档/清理),但环上 5 跳都在,逐跳判定成立。\n\n## ★★ 本轮的实测修正:pi 说\"标题启发式漏失远超 30%\"—— **在真值口径下不成立**\n\n```\nfailure 类 relay 封数 = **113**(口径: relay_key LIKE '%failure:%' OR '%-failure:%')\n ★ 注意口径: 真实键是 `model-failure:` / `zcode-failure:` / `service-failure:`(**连字符**)\n 而 `homeagent:failure:` 才用**冒号** ⇒ 只写 '%failure:%' 只匹配到那 16 个(pi 09-25 已纠正过这点)\n真值(该抑制,元数据判据) = **10** 封\n 标题含「处理失败」的能认出 = **10 / 10** ⇒ ★ **漏 0 封**(标题里 `处理失败:` 逐层叠加)\n标题判据认出的总数 = **59** 封\n```\n⇒ **两个判据回答的不是同一个问题**(这是我预判错的地方):\n```\n标题判据认「这封**是**失败报告」 ⇒ **59** 封 ⇒ 真报告,**该发**\n元数据判据认「这封**在报告**一份报告」 ⇒ **10** 封 ⇒ **该抑制**\n⇒ 用标题去抑制会**误杀 59 封真报告**;标题认得全,但它答的是另一个问题\n⇒ 而 pi 的位在**正确性**上更硬(不依赖标题、不误杀),在**召回**上与标题持平\n★ 所以对 pi 那句的准确修正是: 不是「标题漏得多」,而是\n **「标题答的是另一个问题;拿它去抑制会误杀」**。\n```\n\n## 可判动作 / 修法要点\n\n· 桥侧:`data.mail_id` → 查本封 ∈ `relayed_mails` ? 是 ⇒ 抑制回报;否 ⇒ 正常回报。\n· ⚠️ 抑制必须发生在 **ClaimRelay 之前**,否则又落一个占位行(永占幂等键)。\n· 口径陷阱:判 failure 类要用 `'%failure:%'` **或** `'%-failure:%'` **两者并集**。\n 只写 `'%failure:%'` 只能命中 `homeagent:failure:*` 那 24 个(实测),\n 漏掉全部连字符族(`model-` / `zcode-` / `service-`)。\n ⇒ 只写前者会**漏 89 个**(113 − 24),不是漏 97 —— 我第一版把数算反了,已按实测改。\n\n## 边界 / 未做\n\n· **没有改任何代码**(本轮只读 SQL)。该位**未落地**——落点在桥侧(`plugins/*-mail-bridge/src/index.ts`),\n 且要与\"环整体抑制还是只抑制一跳\"配套决定。\n· 10 / 59 / 113 都是**此刻**的值(瞬时量),引用须带时刻。\n· 我**没有**去核\"标题判据在别的 session 上是否也 10/10\"——样本只此一个环族。\n· 我**没有**判定 pi 那句\"漏失远超 30%\"是在哪个口径下说的(他未附口径)——\n 我只能说在真值口径下它不成立。\n· 未能回给 pi(`send_mail` 被会话闸挡下)。\n\n## ★★ 第⑤条判据的否证清单(2026-09-30 逐条实测,全部为空)\n\npi 那张判决表里第⑤条写的是「**暂未发现反例**」。本轮把它升级为**四个方向逐一实测**:\n\n```\n命中判据的封数 = **10**(口径同前: '%failure:%' ∪ '%-failure:%')\n D1 被指向那封【自身非 failure 键】⇒ 会误杀正常转发 : **0** ✓\n D2 被指向那封【在 mails 表已不存在】⇒ 回查空 ⇒ 漏判 : **0** ✓\n D3 判据与标题不一致(标题不含『处理失败』) : **0** ✓\n D4 链长 >1 层(本封自己也是上一份报告的报告) : **10/10** ⚠️ 见下\n⇒ 前三个方向都没有反例;D4 不是反例,而是它的**工作方式**。\n```\n\n### D4 澄清了一个未决项:要不要\"整链抑制\"?(**不需要**)\n\n```\n10 封的链长实测: 2/2/5/2/3/3/4/3/2/4 ⇒ **最长 5 层**(41ea9a15←b3789a21←84900edd←2ffb7dbb←e43496ed)\n★ 我一度以为\"链长 >1 ⇒ 只抑制一跳会漏、需要整链砍的额外机制\" —— **这个推断是错的**:\n 判据是**逐跳独立**的 —— 每一跳各自回查**自己**的 target,而链上**每一跳**都命中\n ⇒ 链在生成过程中就被**逐跳**砍断,根本长不起来\n⇒ 这 10 封是**判据未上线时的历史遗留**(正是它要消灭的东西)\n⇒ ★ 所以 pi 判决表那条备注「需与『环整体抑制还是只抑制一跳』配套决定」——\n **不成立**: 不需要配套决定,逐跳判定自带这个性质。\n★ 这条是我自己的 ⑬′ 读法派上用场: 我没有只验 D1(最容易被验的那个),\n 而是先把**否证方向列全**再逐条跑 —— 若只跑 D1,我会漏掉 D4,\n 而 D4 恰恰是要我去澄清\"要不要整链机制\"的那一条。\n" + }, + { + "id": "dedup-expectation-must-filter-null-roots", + "count": 1, + "due": "写任何『抑制/去重后应剩 N 封』的判据时(先定口径、滤掉无根行、把被滤掉的量单独报出来);或再按 (agent, root) 分组时", + "where": "relayed_mails.relay_key(service-failure:* 那 32 封 key 不含 UUID ⇒ 非 mail_id)· 实测环 e43496ed/84900edd/41ea9a15(dsh root=b3ce9d0f 同键 3 封)· 同族:failure-suppression-must-not-merge-parallel(别把并列失败当重复)", + "kind": "**判据期望值写错 ⇒ 恒假**,且三个人的数都『不算错』只是口径不同(pi 6 / 我 9 / 我 37)。根因:按 `(agent, failroot)` 分组时**没先滤掉 failroot=NULL** —— 那 32 封是 `service-failure:*` 无根的服务级报告,被并成 2 个假重复组。正确口径:113 → 滤 32 → 剩 81 → 6 组 / 抑制 7 封", + "note": "★★★ 2026-09-30 登记。pi `b9c5070b`(2026-09-25 04:57:46)提出\"**判据期望值写错会恒假**\",我 3 分钟后 `7b69434c` 认了自己一个错数。本轮把**正确算法**实测出来 —— 三个人的数各不相同,而差别全在**口径**。\n\n## 三个数(同一件事)\n\n| | failure 总数 | 组数 | 抑制封数 | 错在哪 |\n|---|---|---|---|---|\n| pi 09-25 | 98 | 5 | 6 | —— (**当时是旧快照**) |\n| 我 09-25(§三) | 16→7 | — | **9** | 把 `homeagent:failure:` 那 16 封当**全量** |\n| 我 09-30 初测 | 113 | 8 | **37** | ★ **没滤掉 failroot=NULL** |\n| **我 09-30 口径修正后** | **113** | **6** | **7** | —— |\n\n## ★ 关键:`(agent, failroot)` 分组时**必须先滤掉 failroot=NULL**\n\n```\nfailure 报告总数 = **113**\n ★ 其中 failroot = NULL = **32** ← 这些**没有有效根**,不能参与分组\n 有有效根的 = **81**\n⇒ 重复组数 = **6** ⇒ 被抑制封数 Σ(n−1) = **7**\n```\n**那 32 封是什么**(这才是要害):\n```\n它们的 relay_key 形状 = `service-failure:c78f1daac57c4ea08f`、`service-failure:7ffc5c4d…`\n⇒ **既不含 UUID、也不是任何 mail_id** ⇒ 它们是**服务级失败报告**,与具体邮件无关\n⇒ 按 (agent, NULL) 分组,会把**32 封彼此无关的报告**并成 2 个\"重复组\"\n⇒ ★ 我那个 **37** 就是这么来的(逐步算全,别学我跳步):\n NULL 组按 agent 分成 {dsh: 26 封, homeagent: 6 封}\n Σ(n−1) = (26−1) + (6−1) = 25 + 5 = **30**\n 加上有根部分的真实抑制 **7** ⇒ 30 + 7 = **37**\n⇒ 而 pi 的 **9** 是把 16 封那个子集当全量\n⇒ ⇒ **三个数都不算\"算错\",都是各自口径下的真值** ⇒ 差别全在口径,不在算术\n```\n\n## 本轮实测确认的机制(pi 那条最硬的印证,逐字成立)\n\n```\nagent=dsh root=b3ce9d0f 同键 **3** 封 = ['e43496ed','84900edd','41ea9a15']\n⇒ 正是 `f76025c9` 那条环里 dsh 在 hop1/hop3/hop5 **回来三次** ⇒ **收敛成 1 封**(抑制 2)\n⇒ ★ 机制成立: 环在两方交替 ⇒ 同 agent 会回来 ⇒ **PK 撞车** ⇒ 环断在 **hop3**\n```\n其余 5 组(每组 2 封,抑制 1):\n```\ndsh? 无 —— 逐组: pi/f39fd424 2 · zcode/b3ce9d0f 2 · zcode/85624acd 2 ·\n homeagent/85624acd 2 · opencode/9e9d0f50 2\n```\n\n## 判据该写成什么(pi 的建议 + 本轮修正)\n\n```\n① `f76025c9` 那条环:(dsh, model-failure:root) 出现 **3** 次 ⇒ 收敛成 1 封、环在 hop3 断 ✓\n② 全量:failure 报告 113 ⇒ **先滤 failroot IS NULL**(滤掉 32 封 service-* 无根报告)\n ⇒ 剩余 81 封里重复 **6** 组、抑制 **7** 封\n ⚠️ 不是 6 封/5 组(那是 09-25 的快照),也不是 9(那是子集当全量)\n③ ★ 写判据时**期望值必须等于本口径的实测值** —— 否则判据恒假,\n 然后被误读成\"修法无效\"(这正是我们这两天反复吃的那条)\n```\n\n## 可判动作\n\n· 凡按 `(agent, root)` 分组/去重:**先滤掉 root IS NULL**,并把滤掉的量单独报出来\n (\"32 封无有效根\"本身就是要报的数,不是可以静默丢掉的噪声)。\n· 报\"抑制 N 封\"时给三件:口径(N 的定义)、被滤掉的量、剩余基数。\n\n## 边界 / 未做\n\n· **没有改任何代码、没有改判据脚本**(本轮只读 SQL)。\n· 113 / 32 / 81 / 6 / 7 都是**此刻**的值(瞬时量)。pi 09-25 的 98/5/6 当时是对的。\n· `service-failure:*` 那 32 封的**产生条件**我没查(只知道它们的 key 形状与邮件 id 无关)。\n· 我**没有**验证\"滤 NULL\"是否就是当时正确的口径 —— 这只是三者中唯一能让\n \"service-* 报告不参与邮件级去重\"成立的读法;但**约定俗成的口径该由谁定,我不知道**。\n· 未能回给 pi(`send_mail` 被会话闸挡下)。" } ] }