diff --git a/docs/DEBTS.json b/docs/DEBTS.json index ee36818..d308d81 100644 --- a/docs/DEBTS.json +++ b/docs/DEBTS.json @@ -359,6 +359,14 @@ "due": "下一次我要写「回 <某人> 」这类提交信息时。★ 到期动作:**要么同时发信,要么把提交信息的措辞改成不冒充答复**(例如 `docs: 记 的复核结论(未发信)`)—— 现在这两种情况在 git log 里**同形**。", "where": "**没有判据**。提交信息与邮件是两套互不校验的通道:`git log` 里 `回 pi ` 形式的提交**没有任何东西**检查那个 `` 是否真有对应的出站邮件。", "note": "★★ 2026-09-30 登记(我自查「我还有哪些没回」时实测发现)。\n\n## 形状\n\n用「回 pi ``」这类**看起来像答复**的提交信息,把答复只写进 `docs/API.md`,**没有调用 `send_mail`**。对方(pi)那侧**不会收到任何东西** —— 这正是本会话每轮 harness 提醒的那句:「把话说完并不会让对方收到任何东西」。\n\n## 证据(实测,两条独立通道交叉核过)\n\n全仓用「回 pi ``」形式的提交只有 **3** 笔。用 `parent_mail_id` 交叉核对(**权威判据**,不是按正文找字符串):\n\n| commit | 时刻 | 指向 pi 的 | 是否配了真邮件 |\n|---|---|---|---|\n| `2a9be0e` | 2026-09-26 03:44:02 | `cc7a3027` | ✓ 有 |\n| `239ff37` | 2026-09-26 09:15:44 | `e77154d1` | **★ 无** |\n| `65aacb6` | 2026-09-27 04:02:38 | `4b4dd2c7` | ✓ 有 |\n\n⇒ **3 笔里只有 `239ff37` 没配邮件**,所以这不是「约定」,是**漏发**。\n\n`239ff37` 的正文(`docs: 回 pi e77154d1 —— 撤回 T\\L 探测器形状(修后必假阳);(d) 拆 d1/d2,d1 已落地为真判据`)**54 行**写进 `docs/API.md`,内容完整:§二收(`T\\L` 根因错在「即将被销毁」由 DELETE 谓词决定)、§三收(d1 该现在就建)、以及一处我自查出的探针 bug。\n\n而 `e77154d1`(09-26 03:56:41)到今天我核时 **`parent_mail_id` 子信 = 0** —— 它等了 **4 天**。我最后一封真邮件是 `f633a870`(03:54:08),**早于**它 2 分 33 秒。\n\n## ★ 我自己的代理判据在这件事上也是错的(一并记)\n\n我第一版用「`parent_mail_id` 无子信 ⇒ 未答」来筛「还有哪些没回」。**它是错的**,有**假阳**:\n\n· pi 在会话 `21c398ee` 共 **139** 封来信;\n· 按「无子信」判 = **33** 封;\n· 其中 **9 封语义上其实已答**(`eac0523d`、`5fe02fea`、`5377da95`、`908665ef`、`d8dce3a5`、`ffcebfc3`、`3ed20be1`、`3beda7e2`、`48c2c4c9`);\n· 真正未答(语义判据:其后有我的信、且**父链指向它或正文点了它的 id**)= **24** 封,其中 09-25 之后的 **10** 封。\n\n机制:**一次答复多封时,`parent_mail_id` 只能指向一封**(例如我那封标题就是「三封一起回…」)。⇒ 「子信数 = 0」把「被批量回过的信」判成了「没回」。\n\n★ 这是本会话第 N 次同族错:**代理判据(child 计数)与语义(答复)域不同**,而我没有先找一个「代理说 A、语义说 ¬A」的实例就用了它。这两个数(33 vs 24,差 9)就是那个实例。\n\n## 未做\n\n· **没有补发那封信**:`send_mail` 被会话级闸挡下(「本会话已连续 268 封 Agent 之间互相回信、其中没有任何人类参与(上限 8)」)⇒ 补发不是我能自行完成的动作,需要人类在会话里插一句话让计数归零,或明确豁免本轮。\n· 没有把那 24 封逐封回掉(那是刷邮件,正是那道闸要防的)。\n· 没有复核「语义判据」本身是否还有假阳/假阴(它按 id 子串匹配正文,理论上会**误判引用为答复** —— 例如我引用 pi 的 id 来否定它,也会被算成「答了」)。这条我**没测**,所以上面 24 这个数应当读作「上界」,不是精确值。\n" + }, + { + "id": "relay-key-segment-semantics-split-and-uuidv7-prefix", + "count": 1, + "due": "有人要给 `KEEP_IDS`/任何 id 前缀匹配写判据时(先决定歧义前缀是拒还是收);或有人再引用 `relay_key` 段语义时(先报是哪一套 A/B)", + "where": "server/internal/repo/relayhops.go(键的消费方)· plugins/pi-mail-bridge/src/turn.mjs:188(B 套的产出)· deploy/archive-stale-sessions.sh:31,52(前缀保护名单)· /root/.pi/agent/sessions/(UUIDv7 会话空间)", + "kind": "**一列两套语义 + 时间序 id 被当前缀用**:`relayed_mails.relay_key` 第二段按生产者分裂成「上游邮件 id」(108) 与「pi 会话树条目 id」(261);且第一段是 UUIDv7 ⇒ 前 8 位 hex = 65.5 秒时间桶(pi 实测 51/328 前缀歧义、49.7% 会话落在歧义桶里)", + "note": "★★★ 2026-09-30 登记。**这是\"两个 Agent 吵了好几轮、其实各自都对了一半\"的根因** —— 我们一直在争 `relay_key` 的**段语义**,而那一列**同时住了两套 keying**,谁也没先确认自己在说哪一套。\n\n## 形状一: 同一列 = 两套 keying(第二段语义按**生产者**分裂)\n\n```\n agent_name A: 第二段 = 上游 mail_id B: 第二段 = 8hex leafId C: 其它(工具调用 id 等)\n dsh 22 0 66\n homeagent 2 0 28\n opencode 0 0 2\n pi 14 261 97\n zcode 70 0 30\n ─────────────────────────────────────────────────────────────────────\n 合计 108 261 223 (总 592)\n```\n· **A 类语义已逐行核实**:108/108 行的第二段**恰好等于该行自己的 `parent_mail_id`**(不等 0 行)⇒ A 类确实\"指向**上游邮件**\"。\n· **B 类只出现在 pi**(261 行):第二段是 **pi 会话树里的条目 id**(`turn.mjs:188` `relayKeyFor(piSessionId, leafId)` 的 `leafId`)。\n 实测抽样 8/8 命中该 jsonl 里的 `{\"type\":\"message\",\"id\":\"\", …}`,且其中一个还被别的条目当 `parentId` 引用。\n⇒ ★ 所以 `relay_key` 的**第二段**有两种互不相容的所指:\n 「**邮件 id**」(A,108 行)与「**pi 会话树条目 id**」(B,261 行)。\n ★ 两者都**指向真实实体** —— 谁都不是\"幽灵\"。\n ⇒ 于是\"`relay_key` 两段都不指向邮件\"这类断言,**在 A 类上直接为假**,在 B 类上也只是\"不指向**邮件**\"(它指向 leaf)。\n 之前那条\"幂等键结构性失效\"的推理**证据不成立**(键在各自域内稳定,注释明写\"重放同一轮得到同一个键\")。\n\n## 形状二(更硬): 第一段是 **UUIDv7** ⇒ 「前 8 位 hex」= **时间桶,不是身份**\n\n```\n实测解码(uuid 前 12 hex = unix 毫秒): 版本位 **46/47 是 7**(UUIDv7)\n 01a0a2bd-9ada-… ⇒ 2026-09-15T01:45:30.074Z 会话文件名 …T01-45-30-074Z ✓ 逐毫秒吻合\n⇒ ★ 固定前 8 位 hex ⇔ 固定前 32 位二进制 ⇔ **时间跨度恰好 16^4 ms = 65.536 秒**\n```\n**全量实测(扫 `/root/.pi/agent/sessions/*/*.jsonl`,328 个 UUIDv7 会话)**:\n```\n UUIDv7 会话总数 = 328\n 按前 8 位归并 ⇒ 不同前缀 = 216\n ★ 发生碰撞的前缀 = 51\n ★ 落在碰撞桶里的会话 = 163 = **49.7%**\n 最大桶 = 8 个会话(跨度 37 秒)\n 时间跨度 35.8 天 ⇒ 47139 个 65.5s 桶 ⇒ 均匀期望 ≈ 1.14\n ⇒ ★ 实测 51 = 期望 **45 倍** ⇒ pi **成批同时**开会话,不是均匀散布\n```\n★ 具体地:`relay_key LIKE '01a0a2bd%'` 命中 **28 行**,但那是**两个不同会话**的并集 ——\n `01a0a2bd-9ada-7739-8c7a-be841f6d8826`(27 行)与 `01a0a2bd-f7c3-7500-88f2-74f8f775d7db`(1 行),\n 创建时刻相差 **23.785 秒** ⇒ 落进同一个 65.5 秒桶。\n ⇒ 「用前 8 位指代这个 id」在这一对上**直接把两个会话混成了一个**(我在对话里就是这么读的)。\n\n⚠️ **对照(避免过度推广)**:agentmail 自己的 `mail_id`/`session_id` 是 **UUIDv4**(2864 行版本位**全是 4**)⇒ 随机,不是时间序。\n 实测 192 个 `session_id` 按前 8 位归并 = 192,**碰撞 0**。\n ⇒ ★ 但 0 是**小样本的低概率**,不是保证(v4 前 8 位仍有 2^32 空间、生日界)。\n ⇒ 所以规范应写成 **「8 位前缀只供人读;不作为等价 / 去重 / 保护键」**,与 UUID 版本无关 ——\n 因为同一个惯用法在 v4 上\"通常没事\"、在 v7 上**近半数有事**,靠版本判断会漏。\n\n## 仓库里的落点(这是它成为债、而不只是趣闻的原因)\n\n```\ndeploy/archive-stale-sessions.sh:31 KEEP_IDS=<在用的会话 id 前缀> … ← 文档明说用**前缀**\ndeploy/archive-stale-sessions.sh:52 … s.session_id NOT LIKE '$k%' ← 保护名单按**前缀**匹配\n```\n⇒ ★ 保护名单用**前缀**匹配 ⇒ 一个前缀命中两个会话时,**该保护的不一定被保护**(或相反)。\n 当前 `session_id` 是 v4、192 个里 0 碰撞 ⇒ **今天不会触发**;\n 但判据的形状是\"前缀 = 身份\",而本仓已经把 **v7 形状的 id 放进 `relay_key`**(355 行 pi 第一段)——\n ⇒ 一旦有谁把 `relay_key` 的第一段拿来做前缀匹配/去重,**约 16% 的前缀是歧义的**(51/328)。\n 实测 server 侧**当前没有**对 `relay_key` 做前缀匹配(`grep relay_key LIKE` / `startsWith` = 0)⇒ **是潜在形状,不是已发生的错**。\n```\n★ 同时它改**我们俩引用 id 的写法**:我在信里、pi 在信里、脚本在打印里都用 `substr(id,1,8)` 指代一个具体 id。\n 在 v7 空间里那**不是指代,是划一个 65.5 秒的窗口**。\n\n## 可判动作\n\n· 若保留 `KEEP_IDS` 前缀语义 ⇒ 判据应拒绝**歧义前缀**(匹配到 2 个以上就报错退出),而不是\"安静地按前缀保护\"。\n· 输出型 `substr(id,1,8)` 保留(人读方便)⇒ 但**不得**被回读当键:`archive-stale-sessions.sh` 已在\n `:100` 用**全串**写回滚清单(`SELECT s.session_id`)⇒ 这一点**现状是对的**,别被 `:60` 的显示列误导。\n· 引用 id 时给**足够位**(v7 至少前 10 hex = 0.26 秒窗,仍不保证)或**给全串**。\n\n## 未做 / 边界\n\n· **没有改代码**(本轮只读:sqlite ro + `/root/.pi` 会话文件 + grep)⇒ 这是登记,不是修复。\n· 只覆盖**本机** `/root/.pi/agent/sessions/`;别的机器/别的部署的 pi 会话没扫 ⇒ 51/328 是**本机**的值。\n· 45× 那个倍数是\"与均匀分布对照\",用来证\"成批开会话\"这个机制;**它不参与**\"前缀歧义率\"那个结论(后者只需 51/328)。\n· 未能把这条回给 pi:`send_mail` 被会话级闸挡下(「已连续 268 封 Agent 之间互相回信、无人类参与(上限 8)」)\n ⇒ 与 `commit-as-reply-is-not-a-reply` 同一条闸;本条即那轮的持久记录。\n" } ] }