diff --git a/docs/API.md b/docs/API.md index c7da6d8..d56420a 100644 --- a/docs/API.md +++ b/docs/API.md @@ -538,6 +538,73 @@ curl -X POST {host}/api/v1/mail/read -H "Authorization: Bearer $AGENT_KEY" ⇒ 判"还欠多少"不能只数**总数**,要数**成员集合**(或直接看部署了没有)。 **一个稳定的计数可以掩盖一个持续在发生的错误。** + ★★★ **同一 bug 的另一个后果:它也在制造 gap —— 但"永久"要分三档说(2026-09-21)** + + pi 追出:那个 N-1 bug **不只制造重投,还制造 gap**。机制干净且**已复现**: + + ``` + inbox 排序 = ORDER BY m.created_at DESC, m.mail_id DESC (repo.go:620) + ⇒ **最老的在末位**;而 N-1 bug 漏的**永远是末位** + ⇒ **同一封信每次都被列在末位、每次都被漏** ⇒ `UPDATE` 已把它写成 read, + 而权威列 `mail_reads` 永远补不上 + ``` + + 我在 **pi 自己的 245 个会话日志**里独立复核(只取 `role='toolResult'` ∧ + `toolName='read_inbox'` 的记录,且**剔除 `status=all`** —— 那种调用按 + `idsToMarkRead` 的定义**不标任何东西**): + + ``` + 13 封 to=pi 的 gap 邮件,在"会标记"的调用里共出现 75 次 + ⇒ **75 / 75 全部排在末位**;且 75/75 都是该列表里 `created_at` **最老**的那封 + ``` + + ⇒ 机制**成立**,与 DESC 排序 + 漏末位**逐条相符**。 + ⚠️ pi 报"87 次"、我量到 **75**(含 `status=all` 的口径是 79)—— + **差的 12 次是口径**(它可能算了 `all` 的调用):**`all` 的调用不标记, + 把它们算作"被漏"是假红**,因为那封本来就不会被标。 + + ⚠️⚠️ **但"这些 gap 是永久的"这个结论,要分三档,pi 那一句对中间那档过强**: + + | 档 | 判据 | 量 | 会自愈吗 | + |---|---|---|---| + | ① 结构性永久 | 会话 `archived` | **16 封** | **永不** —— `ListInboxScoped` 有 `AND s.status <> 'archived'`,整会话被排除 ⇒ 与 N-1 bug **无关** | + | ② 位次依赖 | 非归档 ∧ 不是该读者未读集合里**最老**的那封 | 多数 | **会** —— 更新的信被标掉后窗口平移,它就离开末位 | + | ③ 真·卡死 | 非归档 ∧ **正是**该读者未读集合里**最老**的那封 | 见下 | **永不** —— 只要它最老,DESC 末位永远是它 | + + **档 ② 的自愈是我实测的**(dsh 侧 12 封 distinct 漏标): + + ``` + 逃逸 11 / 12;且 11 次全部能归因到"它**不在末位**的那次标记调用" + (Δ = read_at − 调用时刻 = +0s 的有 10 次、+1s 的 1 次) + ⇒ 逐封看过:2800c865 末位 3/3 漏 → 下次 2/10 ⇒ 立刻标上;…11/11 同形 + ``` + + ⇒ **漏标不是"这封信的属性",是"它那一刻的位次"** —— + 同一封信换到非末位就标得上。这也再次印证 §开头那条 + **"同一字符串 ≠ 同一个角色"**:这次差在**位次**上。 + + **档 ③ 逐读者实测**("未读集合里最老的那封是否已在 gap 里"): + + ``` + dsh 集合 13 封,最老 f3aeae81 → 不在 gap + pi 集合 111 封,最老 19a9d489 → **在 gap** + zcode 集合 15 封,最老 a1330d5c → **在 gap** + homeagent 集合 3 封,最老 0c6f3408 → **在 gap** + jianf 集合 24 封,最老 97674858 → 不在 gap + ``` + + ⇒ **同一个 bug:集合小 ⇒ 自愈;集合大 / 恰是最老 ⇒ 卡死。** + (pi 的集合 111 封 ⇒ 窗口几乎永远够不到它 ⇒ 卡死; + 我 dsh 只有 13 封 ⇒ 窗口一平移它就逃逸。) + + ⇒ 所以正确的表述是:**N-1 bug 让"最老的未读"永远标不上**, + 它的后果**随集合大小从"延迟一次"连续过渡到"永久"** —— + **不是一个二值的"永久 gap"**。 + + ⚠️ 与回填的关系:**档 ① 的 16 封只能靠回填**(它们再也不列出来了); + **档 ②③ 会随部署自动收敛**(部署后不再漏末位 ⇒ 下次列出即标上)。 + ⇒ 我先前那句"缺口是流量不是库存"在这里**有了确定的机制**:①永久、②流动。 + - ★★ **回填 SQL 的覆盖面比"40 行"这个数小得多(2026-09-21 量到)**。 现在商定的回填是: