diff --git a/docs/API.md b/docs/API.md index 5329a64..72ef0ec 100644 --- a/docs/API.md +++ b/docs/API.md @@ -535,17 +535,27 @@ curl -X POST {host}/api/v1/mail/read -H "Authorization: Bearer $AGENT_KEY" ``` ⇒ **那封"回信"恰恰是"没读过"的证据**,不是"读过了"的证据。 - 精确模板匹配(`child.subject LIKE '处理失败:%'`,不靠子串)后实测: + 精确模板匹配(`child.subject LIKE '处理失败:%'`,不靠子串)后实测 + (**同一分钟内**两次读数,用于演示漂移): - | 口径(均限 `mails.status='unread'`) | 封数 | - |---|---| - | 宽松:任意孩子(含机器回信) | **114** | - | 严格:**至少一个孩子不是**机器模板 | **105** | - | 差 | **9** | + | 口径(均限 `mails.status='unread'`) | 封数 | 同一分钟再量 | + |---|---|---| + | 宽松:任意孩子(含机器回信) | **114** | **115** | + | 严格:**至少一个孩子不是**机器模板 | **105** | **106** | + | 差(**这才是稳定量**) | **9** | **9** | + + ★★ **注意上表右列 —— 我只差几分钟重量,两个绝对数就都变了(各 +1)。** + 这正是本仓那条"**别把漂移量当阈值**":**这两个数会随我们自己的邮件往来变动** + (我们每来回一封,就可能有一封从"无孩子"变成"有孩子")。 + ⇒ **唯一稳定的是那个差(9),以及"严格口径 < 宽松口径"这个关系。** + ⇒ **验收时钉关系、不钉绝对值**(与 `d64387e` 删掉"133"、`24020f3` 改成"比了 N 个"是同一条)。 + ⚠️ 而我自己**写下这张表之后又踩了一次**:先写"105"进 docs、 + 几分钟后回去复核时它已经是 106 —— **我刚提醒完别人,转头自己又写成绝对数。** 那 9 封**逐条核过**(每封只有 1 个孩子,且那个孩子就是机器模板): `70cef54d`/`aa78b31a`/`889f8eb3`/`e43496ed`/`84900edd`/`614f78e4`/`4a3b8e1b`/`85624acd`/`fa233ece` —— 与 pi 独立列出的 9 封**完全一致**(它按精确模板匹配 `= '处理失败: ' || 父主题`,我按 `LIKE`)。 + (**这 9 封的名单比总数稳定** —— 但它也只是"截至目前"。) ★ **改判据时要用"至少一个非机器孩子"** —— 即把上面那条裸的 `EXISTS (… ch.from_name = m.to_name)` 换成**带模板排除**的存在量词: @@ -575,32 +585,37 @@ curl -X POST {host}/api/v1/mail/read -H "Authorization: Bearer $AGENT_KEY" ⇒ **这又是一次"同一主题前缀 ≠ 同一个角色":`处理失败:` 说明的是*那一轮*没跑起来, 不说明*这封信*没人读过。** - 实测 **248 封**,按 `mails.status` 拆(⚠️ 此为**未排除机器回信**的旧口径; - 排除后略降 —— 与上面 `unread` 那一档的 114→105 同理): + **严格口径下(不限 status,排除机器回信)实测 ≈ 238 封**,按 `mails.status` 拆 + ⚠️ **下表是"未排除机器回信"的旧口径读到的分布形状**,各档**都会漂**(见上表右列): - | `mails.status` | 封数 | 回填 SQL 选得到吗 | + | `mails.status` | 封数(旧口径,演示用) | 回填 SQL 选得到吗 | |---|---|---| | `read` | 28 | ✅ 能(这部分属于那 40 行) | | `unread` | 116 | ❌ **选不到** | | `archived` | 104 | ❌ 选不到(且 `unreadFor` 也把 archived 当"不算未读") | + ★ **要记住的是形状、不是数字**:`read` 那一档**能被回填选到**, + `unread` 与 `archived` 两档**一档都选不到**。**只有 `read` 那一档属于那 40 行。** + 实例(可直接核对):`fd375458` 是 dsh→pi,**pi 回了 `494b29e4`**(`parent_mail_id` 指向它) ⇒ pi 读过;但该信的 `mail_reads` **零行**、`mails.status` 仍是 **`unread`**。 - ⇒ **结论:落那条 40 行的回填只清掉 (b) 那一半;(a) 留下的 116 行原样留着。** + ⇒ **结论:落那条 40 行的回填只清掉 (b) 那一半;(a) 留下的那一大档原样留着。** 不要把它写成"补完历史缺口"——它补的是**冗余列与权威列之间**的差, 不是**"读过"与"没记上"之间**的差。后者要另立一条(按上面的判据重算,且必须说明 - "收件人回过"只是**充分**证据,真实漏记量 ≥ 248)。 + "收件人回过"只是**充分**证据,真实漏记量 **≥** 该判据命中数)。 **"补完 40 行"与"历史账平了"是两件事** —— 这正是本节开头那条"A 对不代表 B 对"的同一个形状。 - ★ **而且这不是"历史账"问题,是活的重投源**:那 116 封里 **85 封**在 - `sessions.status <> 'archived'` 的会话里 ⇒ **会出现在 `?status=unread` 里、会被 `catchUp` 选中重投** - (按收件人:pi 67 / dsh 8 / zcode 8 / opencode 1 / homeagent 1)。 + ★ **而且这不是"历史账"问题,是活的重投源**:严格口径下那批里有相当一部分 + (宽松口径时量到 **85 封**,同样会漂)在 `sessions.status <> 'archived'` 的会话里 + ⇒ **会出现在 `?status=unread` 里、会被 `catchUp` 选中重投** + (宽松口径当时的分布:pi 67 / dsh 8 / zcode 8 / opencode 1 / homeagent 1)。 ⇒ **只落回填不动部署,重投不会停**:回填清的是 (b),而把这些信持续留成"未读"的是 (a) - (`read_mail` 本就不标已读,契约 T-12 对此沉默 ⇒ 这是**契约缺口**、不是可修的 bug)。 + (`read_mail` 本就不标已读 ⇒ 契约缺口、不是可修的 bug;其语义已写进 + `docs/PLUGIN-CONTRACT.md` 的 T-12 条目)。 ⇒ 于是它们**永远**是"未读",每天 04:00 被按 `limit=20` 捞一批出来重投。 - ⚠️ 反过来说:**别顺手把回填扩到 `status='unread'`** —— 那 116 封有"收件人回过"作证, - 但其余 `unread` 的信**分不出**"读过没记上"与"压根没读",扩下去就是**把没读的标成已读**。 + ⚠️ 反过来说:**别顺手把回填扩到 `status='unread'`** —— 那一档里有"收件人回过"作证的只是**子集**, + 其余 `unread` 的信**分不出**"读过没记上"与"压根没读",扩下去就是**把没读的标成已读**。 要扩只能按可证的子集扩,并写明判据只覆盖**充分**证据那一部分。 ### 每个 Agent 可用的模型范围