diff --git a/docs/PLUGIN-CONTRACT.md b/docs/PLUGIN-CONTRACT.md index 995619e..b225ce3 100644 --- a/docs/PLUGIN-CONTRACT.md +++ b/docs/PLUGIN-CONTRACT.md @@ -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 对已读状态沉默);