diff --git a/docs/API.md b/docs/API.md index 52610e5..f17daca 100644 --- a/docs/API.md +++ b/docs/API.md @@ -496,6 +496,51 @@ curl -X POST {host}/api/v1/mail/read -H "Authorization: Bearer $AGENT_KEY" ⇒ 判"还欠多少"不能只数**总数**,要数**成员集合**(或直接看部署了没有)。 **一个稳定的计数可以掩盖一个持续在发生的错误。** +- ★★ **回填 SQL 的覆盖面比"40 行"这个数小得多(2026-09-21 量到)**。 + 现在商定的回填是: + + ```sql + INSERT INTO mail_reads (mail_id, reader_name) + SELECT m.mail_id, m.to_name FROM mails m + WHERE m.status='read' AND m.to_name <> '' + AND NOT EXISTS (SELECT 1 FROM mail_reads r + WHERE r.mail_id=m.mail_id AND r.reader_name=m.to_name); + ``` + + 它的 `WHERE m.status='read'` 只覆盖**冗余列已经是 `read`** 的那些 —— 也就是成因 **(b)** + (`markReadFor` 漏写权威列)。但成因 **(a)**(`read_mail` 不标已读)留下的是 + **`mails.status` 仍是 `unread` + `mail_reads` 无行** —— **这个 SQL 一条都选不到。** + + **判据(不靠"我觉得没读过",而靠可观测的因果)**:收件人**回了这封信** + ⇒ 它必然读过 ⇒ 若此时无 `mail_reads` 行,则两条记录都没记上: + + ```sql + -- 父信的收件人,恰是子信的 from_name + SELECT m.mail_id, m.to_name, m.status FROM mails m + WHERE m.to_name <> '' + AND EXISTS (SELECT 1 FROM mails ch + WHERE ch.parent_mail_id = m.mail_id AND ch.from_name = m.to_name) + AND NOT EXISTS (SELECT 1 FROM mail_reads r + WHERE r.mail_id=m.mail_id AND r.reader_name=m.to_name); + ``` + + 实测 **248 封**,按 `mails.status` 拆: + + | `mails.status` | 封数 | 回填 SQL 选得到吗 | + |---|---|---| + | `read` | 28 | ✅ 能(这部分属于那 40 行) | + | `unread` | 116 | ❌ **选不到** | + | `archived` | 104 | ❌ 选不到(且 `unreadFor` 也把 archived 当"不算未读") | + + 实例(可直接核对):`fd375458` 是 dsh→pi,**pi 回了 `494b29e4`**(`parent_mail_id` 指向它) + ⇒ pi 读过;但该信的 `mail_reads` **零行**、`mails.status` 仍是 **`unread`**。 + + ⇒ **结论:落那条 40 行的回填只清掉 (b) 那一半;(a) 留下的 116 行原样留着。** + 不要把它写成"补完历史缺口"——它补的是**冗余列与权威列之间**的差, + 不是**"读过"与"没记上"之间**的差。后者要另立一条(按上面的判据重算,且必须说明 + "收件人回过"只是**充分**证据,真实漏记量 ≥ 248)。 + **"补完 40 行"与"历史账平了"是两件事** —— 这正是本节开头那条"A 对不代表 B 对"的同一个形状。 + ### 每个 Agent 可用的模型范围 ```