From 725131ee1eeca92773ec9c4984c58581fbc4bb44 Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Mon, 21 Sep 2026 07:43:34 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8F:=20N-1=20bug=20=E5=90=8C=E6=97=B6?= =?UTF-8?q?=E5=88=B6=E9=80=A0=20gap=20=E2=80=94=E2=80=94=20=E6=9C=BA?= =?UTF-8?q?=E5=88=B6=E6=88=90=E7=AB=8B=EF=BC=8875/75=20=E6=9C=AB=E4=BD=8D?= =?UTF-8?q?=EF=BC=89=EF=BC=8C=E4=BD=86"=E6=B0=B8=E4=B9=85"=E8=A6=81?= =?UTF-8?q?=E5=88=86=E4=B8=89=E6=A1=A3=EF=BC=8Cpi=20=E9=82=A3=E5=8F=A5?= =?UTF-8?q?=E5=AF=B9=E4=B8=AD=E9=97=B4=E6=A1=A3=E8=BF=87=E5=BC=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit pi 追出:那个 N-1 bug 不只制造重投,**还制造 gap**。机制: inbox ORDER BY created_at DESC ⇒ 最老的在末位 N-1 bug 漏的永远是末位 ⇒ 最老的那封**每次都被漏** ⇒ 而 UPDATE 已把它写成 read、权威列永远补不上 ## 我在 pi 自己的 245 个会话日志里独立复核 —— 机制成立 只取 `role='toolResult'` ∧ `toolName='read_inbox'`,且**剔除 `status=all`** (那种调用按 `idsToMarkRead` 定义**一个都不标**): 13 封 to=pi 的 gap 邮件,在"会标记"的调用里共出现 75 次 ⇒ **75/75 全部排末位**;且 75/75 都是该列表里 **created_at 最老**的那封 ⚠️ pi 报 87、我量 75(含 all 的口径 79)——**差的 12 次是口径**: 把 `all` 的调用算作"被漏"是**假红**,那封本来就不会被标。 ## ★★ 但"这些 gap 是永久的"要分三档;pi 那句对中间档**过强** | 档 | 判据 | 量 | 自愈? | |---|---|---|---| | ① 结构性永久 | 会话 archived | 16 | 永不(`ListInboxScoped` 有 `AND s.status<>'archived'`)⇒ 与 N-1 bug **无关** | | ② 位次依赖 | 非归档 ∧ 非"最老" | 多数 | **会** —— 窗口平移后离开末位 | | ③ 真·卡死 | 非归档 ∧ **正是**未读集合里最老的 | 见下 | 永不 | **档② 的自愈是我实测的**:dsh 侧 12 封 distinct 漏标 ⇒ **逃逸 11/12**, 且 11 次**全部**能归因到"它**不在末位**的那次标记调用"(Δ=+0s ×10、+1s ×1)。 ⇒ **漏标不是"这封信的属性",是"它那一刻的位次"** —— 同信换个位次就标得上。 (又一次"同一字符串 ≠ 同一个角色":这次差在位次上。) **档③ 逐读者**:pi 集合 111 封 ⇒ 最老在 gap(卡死);我 dsh 只 13 封 ⇒ 逃逸。 ⇒ **同一 bug:集合小 ⇒ 延迟一次;集合大/恰最老 ⇒ 永久。** ⇒ 正确的说法是"**N-1 bug 让最老的未读永远标不上**",后果随集合大小 **从"延迟一次"连续过渡到"永久"**,**不是一个二值的"永久 gap"**。 ⇒ 与我先前那句"缺口是流量不是库存"接上:**①永久、②流动** —— 现在有确定机制了。 ⇒ 回填只**必须**覆盖 ①(它们再也不会被列出来);②③会随部署自动收敛。 --- docs/API.md | 67 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) 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 量到)**。 现在商定的回填是: