补: 「触发条件 ≠ 复发原因」两半拆开(pi 补,我复现后同意)+ 记下 4 vs 7 的口径差

pi 指出我上一笔把一件事写成了一个成因。拆开后是**两条**,缺一不可:

    触发条件  快照在**取件时刻**正确,投递与取件之间被读掉  ⇒ 让**这一批**投出已读的信
    复发原因  deliveredMails 是**内存** BoundedSet,重启即失忆 ⇒ 让它**下一轮**又挑同一批

**关键结论**:B-7.7 要的落盘账本只治"复发原因"那一半;
**快照失效是另一半,账本治不了**(账本挡得住"整封重投",但挡不住"取件后、投递前被读掉")。

## 4 还是 7:我一开始数和 pi 冲突,查下来是**口径不同,两边都对**

    口径A  误投前**有过任何**合法投递                        ⇒ 7/7   ← 我第一版数的
    口径B  前一轮**也是 catchup** 且间隔 >5h(排除 SSE 首投) ⇒ 4/7   ← pi 的 4

差异全在 7a350f9d / 57b0c703 / 18527c6b:前一次是 SSE 实时投递(catchup=False,仅早 1.6h)。
**对"复发原因"这条论证,口径 B 才是相关的那个** —— 要证"重启后整批重来",
得看"上一轮也是补投",而不是"之前投过"。已在 docs 里写明两个口径各自的读数。

## 另一条口径边界(我量的,pi 没提)

那 7 例全在 09-13/09-14;pi 侧 catchup 投递 **09-15 09:45 之后再没出现过**(距 09-21 已 5.9 天),
且轮次时刻分布在 07/09/10/15/16/17/19/20/22/23 点,**不在 04:00**。
⇒ "每晚重来"对 **dsh 侧成立**(crontab `0 4 * * * restart dsh.service`),对 **pi 侧不成立**
(它自己的宿主重启/唤醒,另一个触发器)。那 7 例是**历史反例**,不是"现在每晚都在发生"。
This commit is contained in:
2026-09-21 06:20:03 +08:00
parent 8c320121e1
commit 3fc503235c

View File

@ -448,8 +448,48 @@ SSE 只推连上之后的事件。插件重启前发来的邮件不会再推一
> **"判据没答 ≠ 判据答错了"**:这 0/108 没答错,它答的是另一个问题。
>
> ⇒ 结论要分开写:`catchUp` 的**取件谓词**没有错(`unreadFor` 只看 `mail_reads`,
> 取的时候确实未读);**错的是取件与投递之间的时效假设**。这给了"每天重投"
> **第三条**独立成因(前两条见下)。也说明"修好 (b) 重投就会停"是新的假绿期待。
> 取的时候确实未读);**错的是取件与投递之间的时效假设**。
>
> ★ **这条还要再拆成两半(pi 2026-09-21 补,我复现后同意)—— 触发条件 ≠ 复发原因**:
>
> | | 机制 | 作用 |
> |---|---|---|
> | **触发条件** | 快照在**取件时刻**正确,投递与取件之间被读掉 | 让**这一批**投出已读的信 |
> | **复发原因** | `deliveredMails` 是**内存** `BoundedSet` ⇒ 进程重启即失忆 | 让它**下一轮**又从同一批里挑 |
>
> 两者缺一不可:没有"失忆",快照过期只造成一次误投;没有"快照过期",
> 失忆后重投的也是**真正未读**的那些(那本来就是 `catchUp` 该干的事)。
> 我复核了 pi 给的分布:这 7 例里 **4 例是"第二轮"** —— 同一批在约 10 小时前
> 已**合法**投过一轮(当时确实无读行),第二轮才是在读行已存在时投的。
> 例:`1868127e` 09-13 12:36 投(无读行)→ 09-13 15:10 投(无读行)→
> 09-14 01:25 投(**已有读行**,`read_at` 09-14 01:22:25)。
>
> ⚠️ **"前有合法投递"有两种口径,数出来不一样(我第一次数成 7/7,与 pi 的 4 冲突)**:
>
> | 口径 | 判据 | 结果 |
> |---|---|---|
> | A | 误投那次**之前有过任何**合法投递 | **7/7** |
> | B | 之前那一轮**也是 catchup**(不是 SSE 首投)且间隔 > 5h | **4/7** |
>
> 差异全在那 3 例(`7a350f9d`/`57b0c703`/`18527c6b`):它们前一次是 **SSE 实时投递**
> (`catchup=False`,仅早 1.6h),不是"上一轮 catchup"。**pi 的 4 用的是口径 B,是对的**;
> 我的 7 用的是口径 A,也没错 —— **两个口径的基准不同,不是分歧。**
> 记数时必须写明用哪个(**"量到 ≠ 量对了;任何读数的第一句话应该是'我量的是哪个东西'"**)。
> 对"复发原因"这条论证,**口径 B 才是相关的那个**:要证明"重启后整批重来",
> 得看"上一轮也是补投",而不是"之前投过"。
> ⇒ **B-7.7 要的落盘账本治的是"复发原因"那一半;快照失效是另一半,账本治不了它。**
> (账本能挡住"整封重投",但同一批里"取件后、投递前被读掉"仍会发生 ——
> 除非把去重判据从"投过没有"扩展到"投的这一刻还算不算未读"。)
>
> ⚠️ 另记一条**口径边界**:pi 那 7 例全在 09-13/09-14,pi 侧 catchup 投递
> **09-15 09:45 之后就再没出现过**(我量到距 09-21 已 5.9 天),
> 且 pi 侧的轮次时刻分布在 07/09/10/15/16/17/19/20/22/23 点 —— **不在 04:00**。
> 所以"每晚重来"对 **dsh 侧成立**(crontab `0 4 * * * restart dsh.service`),
> **对 pi 侧不成立**(是它自己的宿主重启/唤醒,另一个触发器)。
> 那 7 例是**历史反例**,不是"现在每晚都在发生"。
>
> 这给了"每天重投"**第三条**独立成因(前两条见下)。也说明"修好 (b) 重投就会停"
> 是新的假绿期待。
>
> "未读"的成因有两条,互相独立(**连同上面那条"快照过期"共三条**):
> **(a)** `read_mail` 不标已读(契约 T-12 对已读状态沉默);