From ecf98d7d3659cb41e0822564136c8c4251a6ab37 Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Mon, 21 Sep 2026 04:44:32 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=87=E6=A1=A3:=20=E8=AE=B0=E4=B8=8B=20B-7.?= =?UTF-8?q?7=20=E9=82=A3=E6=9D=A1=E5=80=BA=E7=9A=84**=E5=AE=9E=E6=B5=8B?= =?UTF-8?q?=E5=AE=9E=E4=BE=8B**=20=E2=80=94=E2=80=94=20=E6=AF=8F=E6=97=A5?= =?UTF-8?q?=2004:00=20=E7=9A=84=20dsh=20=E9=87=8D=E5=90=AF=E8=AE=A9?= =?UTF-8?q?=E9=87=8D=E6=8A=95=E6=AF=8F=E5=A4=A9=E5=A4=8D=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 与契约里 2026-09-04 那个事故**同形**,只是换成了"宿主被定时重启"。链条完整量到: 1. root crontab `0 4 * * * /usr/bin/systemctl restart dsh.service`(第 6 行) 2. 每天 04:00:03 `dsh.service` Stop/Start(journalctl 逐日可见) 3. 新进程 ⇒ `deliveredMails` 空(index.ts:469,内存 BoundedSet) 4. `caughtUp` 重置 ⇒ 首个心跳后 catchUp 跑一次(:470 / :524) 5. 拉 `/mail/inbox?status=unread&limit=20`(:481)⇒ 整个未读积压当天重投一遍 实测(以 `mails` 表为判据):真邮件投递 **107 次 / 81 封**不同邮件, 04 点整点占 09-18:4 / 09-19:3 / 09-20:3 / 09-21:7;投递时刻落在 04:00:23 / 04:01:23 / 04:03:23… 的 ≤5 封批次节奏上(B-7.2)。 `session/end-seed` 也逐日落在 04:00。 ★ 关键判别用**时序**:这 107 次里「投递时该读者已有已读行」= **0 次** ⇒ catchUp 从不重投已读邮件,它投的是"当时确实未读"的。"未读"有两个独立成因: (a) `read_mail` 不标已读(T-12 对已读状态沉默);(b) `markReadFor` 占位符错位 (2026-09-20 修)。两者都让"读过了"没变成"已读"。 ⚠️ 我把这里算错过两次,都写进文里当纪律: ① `agent/inbox/spliced` 里混着**非邮件**注入(后台作业完成通知、续跑提示), 按"事件数"统计会当成邮件(我一度报 09-20 有 18 次,真值 **3**); ⇒ 要拿 id 去问 `mails` 表在不在,才算真邮件。**同一事件类型 ≠ 同一种载荷。** ② 判断"有没有重投已读的信"**不能看当前 `mail_reads`**:11 封现在是"已读+曾重投", 看着像反例,其实是**先被重投、后来才补标**的。必须拿**每次投递的时刻**比 `read_at` —— `mail_reads` 是当前状态,"投递时是否已读"是历史事实,两者不是一回事。 dsh 桥去重仍是内存态 ⇒ B-7.7 要求的落盘账本这条债**仍未销**(已在文里写明)。 --- docs/PLUGIN-CONTRACT.md | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/docs/PLUGIN-CONTRACT.md b/docs/PLUGIN-CONTRACT.md index 22c67f8..bd9a5a4 100644 --- a/docs/PLUGIN-CONTRACT.md +++ b/docs/PLUGIN-CONTRACT.md @@ -398,6 +398,44 @@ SSE 只推连上之后的事件。插件重启前发来的邮件不会再推一 > > 模型上下文里因此出现两段几乎相同的指令。 > +> ★★ **同一条规律在 dsh 宿主上的实测实例(2026-09-21 量到,非推测)**: +> 与上面那个 09-04 事故**同形**,只是换成了"宿主被定时重启"。完整链条: +> +> 1. root crontab 有一条 `0 4 * * * /usr/bin/systemctl restart dsh.service` +> (`/var/spool/cron/crontabs/root` 第 6 行) +> 2. 于是每天 **04:00:03** `dsh.service` 被 Stop/Start(`journalctl -u dsh` 逐日可见) +> 3. **新宿主进程 ⇒ `deliveredMails` 是空的**(`plugins/dsh-mail-bridge/src/index.ts:469`,内存 `BoundedSet`) +> 4. `caughtUp`(`:470`)在新进程里重置 ⇒ 首个成功心跳后 `catchUp` 跑一次(`:524`) +> 5. 它拉 `/mail/inbox?status=unread&limit=20`(`:481`)⇒ **把整个未读积压当天重投一遍** +> +> 实测:单条会话日志里**真邮件**投递(以 `mails` 表为判据,见下方警告)共 **107 次**、 +> 涉及 **81 封**不同邮件;其中 **04:00 那个整点**占 +> 09-18:4 / 09-19:3 / 09-20:3 / 09-21:7 次;投递时刻精确落在 +> 04:00:23 / 04:01:23 / 04:03:23… 这类 ≤5 封一批的节奏上(B-7.2 的每批上限)。 +> 会话内的 `session/end-seed` 也逐日落在 04:00。 +> +> 关键判别(用**时序**,不是用"现在有没有已读行"):这 **107 次投递中, +> 「投递时该读者已有已读行」= 0 次**。也就是说 **`catchUp` 从不重投已读邮件 —— +> 它投的是"当时确实未读"的那些**。"未读"的成因有两条,互相独立: +> **(a)** `read_mail` 不标已读(契约 T-12 对已读状态沉默); +> **(b)** `markReadFor` 的占位符错位导致权威列漏写(2026-09-20 修,见 `docs/API.md`)。 +> 两者都让"读过了"没变成"已读",于是下次宿主重启时被当成新信再投一遍。 +> +> ⇒ 只要"每日重启 + 内存去重 + 任何一条让已读没记上的路径"三者同时存在, +> 重投就会**每天复发**,且看起来像"对方又发了一遍"。B-7.7 要求的落盘账本正是 +> 为这条链准备的;dsh 桥目前仍是内存态 ⇒ **这条债仍未销**。 +> +> ⚠️ **我第一版把这里算错过两次,都记下来**(同一个 `agent/inbox/spliced` 事件类型里 +> 混着**非邮件**的注入 —— 后台作业完成通知、session 续跑提示等): +> ① 按"事件数"统计会把这批噪声算成邮件(我一度报出 09-20 有 18 次,真值是 **3**); +> ② 只看正文里有没有 `邮件 ID` 还不够 —— 要拿 id 去问 **`mails` 表**在不在, +> 才算"真的是一封邮件被投递"。**同一个事件类型 ≠ 同一种载荷。** +> +> ⚠️ 另一条**取证纪律**(我第一版也算错过):判断"有没有重投已读的信"**不能看当前 +> `mail_reads`**。有 11 封现在是"已读 + 曾重投",看着像反例 —— 其实它们是**先被重投、 +> 后来才被某次 `read_inbox` 补标上**的。必须拿**每次投递的时刻**去和 `read_at` 比: +> `mail_reads` 是**当前状态**,而"投递时是否已读"是**历史事实**,两者不是一回事。 +> > **为什么不能只记「投过没有」**:那会把「重复」换成「丢件」。上面第 2 步里 > 那一轮被掉断,发件人**没有**收到回信,而落盘记录说「已投过」→ 永远跳过。 > 丢件比重复严重:重复至少人能看出来,丢件是静默的。