补: 那条 40 行回填**只清 (b) 那一半** —— (a) 留下的 116 行它一条都选不到

商定的回填 SQL 带 `WHERE m.status='read'`,只覆盖**冗余列已经是 read** 的(成因 (b))。
成因 (a)(`read_mail` 不标已读)留下的是 **`mails.status` 仍 unread + `mail_reads` 无行**
⇒ 那个 SQL **一条都选不到**。

## 判据:不靠"我觉得没读过",靠可观测的因果

**收件人回了这封信 ⇒ 它必然读过** ⇒ 此时若无 `mail_reads` 行,则两条记录都没记上
(父信的 `to_name` 恰是子信的 `from_name`)。实测 **248 封**:

    read      28   ✅ 回填选得到(属于那 40 行的一部分)
    unread   116   ❌ 选不到
    archived 104   ❌ 选不到

实例:`fd375458`(dsh→pi)—— **pi 回了 `494b29e4`**(`parent_mail_id` 指向它)⇒ pi 读过;
但该信 `mail_reads` 零行、`mails.status` 仍是 `unread`。

## 结论

落那 40 行只清掉 **(b)**;**(a) 留下的 116 行原样留着**。
不要把它写成"补完历史缺口"—— 它补的是**冗余列与权威列之间**的差,
不是**"读过"与"没记上"之间**的差。后者要另立一条判据。

(判据口径说明:`收件人回过`是**充分**证据,不是全部 ⇒ 真实漏记量 **≥ 248**。)

★ 与本节开头"计数对了不等于账记上了"是**同一个形状**:这次是"补了 40 行"≠"账平了"。
This commit is contained in:
2026-09-21 06:32:43 +08:00
parent 437be52552
commit f0c7e5cbbc

View File

@ -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 可用的模型范围
```