# 可观测批(A 批)设计:读窗扩展 · 团队状态卡 · 台账收件视图 > **状态**:✅ **已实施**(2026-09-26,eng_coder,三阶段一次交付;文件 `lib/index.js` + `host-half.test.mjs`;**当次实测** `node host-half.test.mjs` → `1117 (failed: 0)`、`node client-half.test.mjs` → `269 (failed: 0)`;红相 60 条新判据打在**实施前**的 `lib/index.js`(`89074fe`)上 → `1117 (failed: 55)`,读数账本见 [`docs/verification-log.md`](verification-log.md) 的「可观测批(A 批)」一节;提交哈希 `ac3a831d74fb64ae4f6577b3f79a9cc0cc7fb371`(回填于 2026-09-27))。跨文档同步十条见 §10,逐条已完成。 > **来源**:《台账 + 形态设计》§7 的**步 2 / 步 4 / 步 6**(用户 2026-09-26 把这三步合并为「A 批」,与 B 批「形态」并行开工)。会诊 #9 的 Q1 采纳项(只读按任务分组收件视图)即本批的步 6。 > **落点核对**:§3 的行号按 `89074fe` 的 `lib/index.js` **逐条实读**。 > **后续回填**:2026-09-27 的 **DIVERGENCE(12) 分歧审计修复轮**在此之上回填了本档 §4.1(`team_read` 的点名读边界)· §4.2(注记三态与聚合口径 · 台账尾成本列 · 会话面服务名)· §4.3(归组面 · 点名读上限)· §4.4(投递事实的两类载体 · `callId` 配对 · 窗口单位 · 第 4 态),并新增 **10** 条宿主断言(套件自报 `1127`);逐条读数见 [`docs/verification-log.md`](verification-log.md) 的「分歧审计修复轮」一节。 ## 1. 白话版 **问题一(半盲)**:团队一过 12 个会话,列表里第 13 行起的活性行就写「未读(超出快照窗口 12)」。这不是懒 —— 每读一个会话日志都要解一次 zstd,**50 行全读会把工具跑超时**。所以「12」是**预算**,不是能力上限。 **问题二(只有模型看得见)**:谁在位、谁在跑、哪儿有在飞的换届令牌、看门狗在盯谁、台账最后几条 —— 这些事实分散在四个地方,只有模型连着调几次工具才拼得出来。 **本批做两件事**:① 把「固定 12」变成「**默认 12 + 我自己点名再多读几个 + 分页走完**」,成本不变式不动;② 加一张**只读状态卡**:一次调用一屏讲清团队现状,贵的那半边(活性)照旧有界读、超出照旧如实标「未读」。 再加 ③ 台账的**收件视图**(按任务号看「谁对我说了什么」)与 ④ **派生回执**(「我发出去之后,对方动了没有」)—— 两者默认都只读**自己**的会话面,一次读、成本有界,**零新增持久状态**。 **一句话**:读得更多、看得更全,但**那条「成本有界 + 未读必标注」的纪律一寸不动**。 ## 2. 需求 | # | 角色 | 故事 | | --- | --- | --- | | US1 | 协调者 | 团队超过 12 个会话时,我能**点名**读窗口外某几个会话的活性,而不是只被告知「未判定」 | | US2 | 协调者 | 我能**分页**走完整个列表(最多 50 行),不必靠重开调用碰运气 | | US3 | 人 + 模型 | 一次调用看到**一张状态卡**:谁在位/谁空缺 · 谁在跑 · 谁静默多久 · 有没有在飞的换届令牌 · 看门狗在盯谁 · 台账最后几条 | | US4 | 协调者 | 我能按任务号看到**收件视图**(谁在什么时候对 `t-7` 说了什么),不必回读历史消息 | | US5 | 任何人 | 任何超出有界读的项**如实标注未读**,绝不给出「用空数据算出来的读数」 | | US6 | 协调者 | 我能看到**派生回执**:「已投递、而目标日志里没有后续反应」的时刻 —— **纯派生**,零新增状态(会诊 Q4 采纳项,父档 §7 步 6 的后半件) | | # | 非功能标准 | | --- | --- | | N1 | **成本有界**:单次调用的 surface 读数有显式上限,且上限与工具超时预算挂钩(写死成不变量 + 断言) | | N2 | **不静默**:超界即**拒绝并说明**,或如实标注未读 —— 不做静默截断 | | N3 | **零新信任面**:本批**全部只读**;不新增写路径、不新增持久状态、不加缓存 | | N4 | **措辞纪律**:读数与裁决分离(沿用台账批 §9.3.4 / N4 的口径) | ## 3. 现状与成本模型(as-of `89074fe`) | 现有件 | 位置(`lib/index.js`) | 事实 | | --- | --- | --- | | `LIST_LIMIT = 50` | :110 | **列出**上限 50 行 | | `PREVIEW_SESSIONS = 12` | :111–116 | **读 surface** 的上限;原注释写明理由:「一次冷日志 = 一次 zstd 解压 + 一次投影」,按 50 行全读**会超时** | | 并行读 | :9080–9090 | `Promise.allSettled(readWindow.map(readSurface))` —— N 行 ≈ 一次解压的**延迟**,不是 N 次 | | 行外标注 | :121 / :9146–9147 | 窗口外的行**仍然列出**(id / 代理状态 / 创建时间 / provisional 标记),只有活性行换成「未读」 | | 活性构造 | :498 `buildLivenessSignal` → :523 `verdictOf` → :553 `livenessLine` | 依赖 **surface**;没有 surface 就没有 verdict(刻意如此) | | 看门狗注册 | `policy.watchdogs`(:576) | **持久化在 settings**(进程内只是定时器/去抖,:151) | | 换届 pending | `teams[].roles[].pending`(:7610 写入;:2151 / :2500 渲染,**掩码**) | 读面现成 | | `readSurface` 调用点 | :1464 与 :9085 | 全插件只有两处读会话面 —— 本批的新读都从这里走 | **结论**:「12」是预算决定的。本批**不提高默认值**(提高会把超时风险塞回每个调用),而是把「读哪些」的控制权**交回调用方**,并把成本上限定成不变量。 ## 4. 方案 ### 4.1 步 2:读窗从「固定 12」变成「默认 12 + 点名 + 分页」 `team_link_list_sessions` 参数扩展(两个都**可选**、**互斥**): | 参数 | 语义 | 边界 | | --- | --- | --- | | `readIds?: string[]` | **点名读**:把指定会话强制纳入本次读窗(用于窗口外的行) | ① ≤ `PREVIEW_SESSIONS` 个,超出**拒绝**(N2);② 只接受**本次列表内**的会话 id,未知/不属于本列表的 id → **拒绝并说明**;③ 与默认窗口**合计**仍 ≤ 12(见下) | | `offset?: number` | **分页**:从第 N 行开始读,默认 0 | 整数;超界 → 拒绝并给出有效区间 | **成本不变量(N1,按工具分账)**: | 工具 | 上限 | 说明 | | --- | --- | --- | | `team_link_list_sessions` | ≤ `PREVIEW_SESSIONS`(12) | 三条路径(默认 / `offset` / `readIds`)都算在内 | | `team_link_team_read`(收件视图 + 派生回执) | ≤ `min(12, 1 + |readIds|)` | **自读占 1 个额** ⇒ 去重后的 `readIds` **≤ 11**;点名读目标面用掉余下额度,**超出的不拒绝**而是截断到前 11 个、超额逐个标「本次未读」+一行溢出说明(N2 的另一个允许分支,见本节末的边界表) | | `team_link_status` | ≤ `PREVIEW_SESSIONS`(12) | 取窗顺序见 §4.2 | `list_sessions` 的三条路径: - 无参数:读 `shown.slice(0, 12)`(**现状不变**); - `offset=k`:读 `shown.slice(k, k+12)`; - `readIds=[a,b]`:读窗 = `[a,b] + shown 前 (12 − 2) 行`(点名优先占额,其余按原顺序补足)。 **输出尾巴给出可操作的下一步**(把「未读」从终点变成提示): ```text 未读 Y 行(读窗 12/共 X 行):读第 13–24 行 → offset=12;或点名 → readIds=["", …] ``` **边界(逐条给行为,不留白)**:`readIds` 含**重复 id** → 去重后计数;含**本就在默认窗内**的 id → 只占一个额、不重复读;`offset` 为**负数** → 拒绝(是整数但越界,给有效区间)。 | 工具 | `readIds` 的上限与超限行为(**D3 回填**,实现即此) | 为什么两个工具不同 | | --- | --- | --- | | `team_link_list_sessions` | **去重后 ≤ 12**;超出 ⇒ **拒绝 + 零读**(`lib/index.js` 的 readIds 上限分支) | 列表工具**不必**读任何一个会话就能回答,多读纯粹是额外成本 ⇒ 超限没有理由半途而废 | | `team_link_team_read` | **去重后 ≤ 11**(自读占 1 个额);超出 ⇒ **截断到前 11 个**,超额的**逐个点名**标「本次未读」+一行溢出说明(不拒绝、不静默丢) | `team_read` 的**自读无论如何都要发生**(收件视图本身要它)——此时整体拒绝等于把已经能给的读数也一起扣下;N2 允许的两个分支里这里取「如实标注未读」那一支 | 两侧都满足同一条不变量 `≤ min(12, 1+|readIds|)`;`list_sessions` 的拒绝一分钱不读、`team_read` 的截断一分钱不多读,**没有第三条路径**。 ### 4.2 步 4:只读团队状态卡 `team_link_status`(新工具) 一次调用 = 一屏。**来源全部是已有的只读面**(N3): | 屏上的一段 | 来源 | 成本 | | --- | --- | --- | | 团队与角色(在位 / 空缺 / 版本史末条) | `policy.get().teams` | settings 读,零 | | **在飞的换届令牌**(角色 / 继任者 / 到期时刻,token **掩码**) | `teams[].roles[].pending` | settings 读,零 | | **看门狗**(谁盯谁 / 阈值 / 到期) | `policy.watchdogs` | settings 读,零 | | 会话面(id / 代理状态 / 创建时间 / provisional 标记) | `ctx.sessionQuery.listSessions`(**与 `list_sessions` 同源同序**——同一份记录列表、同一个 `cwd` 过滤)+ `ctx.agents.get(id)`(代理状态) | 零(不读日志) | | **活性半边**(verdict / 静默 / goal) | 同一有界读窗(§4.1 的不变量) | 与 list_sessions 同量级 | | **台账最后几条** | `tasks.md` 尾部 | **单团队 1 次文件读;聚合态 N 次**(N = 所示团队数——每个团队各读一次自己的 `tasks.md`;聚合口径见本节「聚合态的 `M`」) | | **反面预警注记** | 从**同一个读窗**的 surface 事件里数「窗口内消息条数」,与台账同期新增行数对比 | **零额外读** | **反面预警注记(判据写死,实现即此)** —— 会诊提的「会话停止说话、只写行」: - **时间锚** `T0` = 本读窗内**最旧的**一条 surface 消息的时间戳(读窗是**会话集合**不是时间窗,锚必须从数据里取); - **两个计数**:`N` = 该锚**之后**、本读窗内的消息条数;`M` = `T0` 之后 `tasks.md` 的新增行数; - **触发条件(只此一条)**:`N == 0` 且 `M > 0` ⇒ 打注记 `⚠ 窗口内 0 条消息 / 台账同期新增 M 行 —— 会话可能已停止说话只写行(读数,不是裁决)`;否则**不打**; - **三种情形逐条写死(D2 回填,实现即此;三态各有断言)**: 1. **读窗为空**(本工作区没有其他会话)⇒ **整段不打印**(连段标题都不出现)——两边都没有,没有什么可对比的; 2. **读窗非空、但窗内没有一条带时间戳的 surface 消息**(锚 `T0` 取不到)⇒ 打一句 `(反面预警注记无法计算:本读窗内没有带时间戳的 surface 消息——不猜。)` —— **诚实优于沉默**:读者知道这里本该有个读数;台账里有多少行都不改这个结论(它属于「取不到锚」,不是「触发」也不是「M == 0」); 3. **锚取到** ⇒ 按上面那条唯一判据判 `N == 0 且 M > 0`; - **聚合态的 `M` 是聚合口径**:省略 `team` 时 `M` = **所示各团队** `tasks.md` 同期新增行数的**和**(单团队时就是那一家);读面上它只是**一个数**(本段**不**逐团队拆分),所以看到数字变大时要知道它数的是「所示全部团队」,不是随便哪一个。 - **成本**:零额外读 —— 两个计数都用已读到的 surface 事件与已读的文件内容; - **它是读数,不是裁决**(N4):只陈述两个计数,不下「互核已死」的结论。 **参数与边界**(对齐 §4.1 的边界风格): | 参数 | 语义 | 边界 | | --- | --- | --- | | `team?: string` | 只看这一个团队 | 省略 ⇒ **聚合设置中的全部团队**(每团队一段,**不按工作区过滤**):本插件里团队是按名**全局寻址**的(`team_link_roster(get)` 本身就返回全体团队概要,`team_read` / `rotate` / `recover` 都只按团队名寻址),只有**会话列表**才按工作区过滤 —— 状态卡与 `roster(get)` 同口径;给了但不存在 → **拒绝并列出已知团队名** | | `tasksTail?: number` | 台账显示最后几条 | 默认 **5**;上限 **20**,超出拒绝(防一屏变十屏) | | 取窗顺序 | 活性半边读哪些行 | 与 `list_sessions` 同一顺序,**前 12 行**;超出如实标注 | | 调用方无会话身份 | —— | 照常出卡;「当前会话」字样改为如实标注(`(当前会话未知)`),**不编造身份** | **贵面照旧有界**:状态卡里的活性行同样受 12 的不变量约束;超出部分如实标注(沿用 `LV_WINDOW_NOTE` 的口径,文案说明「本卡只读了前 12 行」)。 ### 4.3 步 6:台账收件视图(扩 `team_link_team_read`,不加新工具) - **默认**:读**调用方自己**的会话面(一次 `readSurface(selfId)`)→ 抽取正文里带 `ref=t-`(会诊采纳的映射约定)的消息 → 按任务号分组,给出「每条任务收到过谁、什么时候、几个字符的开头」。 - **归组面写死(D12 回填,实现即此)**:归组只认**任何消息文本里第一个** `ref=t-`(`ref=(t-\d+)` 对**整段文本** exec,只取第一个匹配)——**不要求 banner 形状**(`meta.ref` 只是**约定**,不是抽取前提),也**不排除本会话自己**的消息(自己发的正文提到 `ref=…` 一样归组);发送方取不到 `senderSessionId` 时**如实回落**标「(本会话)」,不编造身份。`ref` 是别的值(旧样式的 `slp-…`)不归组、只计数。 - **点名读的上限(D3 回填,实现即此)**:`readIds` 去重后最多 **11** 个(自读占 1 个额,成本不变量见 §4.1);**超出的不拒绝**——截断到前 11 个,超额的**逐个点名**标「本次未读」并给一行溢出说明,行上照常按「未读」标注(N2 的「如实标注未读」那一支)。 - **要读目标面时**:走 §4.1 的**点名读**(`readIds`)—— **未点名时目标面零读**(写成断言,防悄悄变贵)。 - 输出形如: ```text --- 收件视图(按任务号;来源:本会话最近 20 条消息 · 常量 TASK_INBOX_MESSAGES)--- - t-7 ← worker-b(09-26 21:19):核出 3 处错(依据 lib/index.js:3548) - t-7 ← worker-c(09-26 21:31):我这边的读数不同… - (无 ref 的消息 12 条:未归任务,未参与本视图) ``` - 与台账派生视图的分工:**台账 = 谁声称了什么;收件视图 = 谁对我说了什么**。两者都是读数。 - **两个尺寸常量**:读调用方最近 **20** 条消息(`TASK_INBOX_MESSAGES = 20`);每条预览 **80** 码点(`TASK_INBOX_PREVIEW = 80`),越界截断并**标注省略**(沿用本仓「截断必标注」的既有规矩)。 - **边界**:`ref` 指向**从未登记过**的任务号时**照样分组显示**(不隐藏),并在该组标注 `(该号在台账里未登记 —— 可能是笔误,也可能是先发后记)`;**不静默丢弃**。 ### 4.4 步 6 的另一半:派生回执(同一新块里) 收件视图回答**谁对我说了什么**;派生回执回答**我说出去之后,对方到底动了没有**(会诊 Q4 采纳项)。**纯派生、零新增持久状态**: - **投递事实从调用方自己的 surface 里取**(既有事件,**不新增任何事件类型**)——**D4 回填,实现即此**: - **两类事件载体**:一等工具调用的 `tool/call`,以及从 `run_code` 程序里发出的桥记 `tool/ptc-dispatch`(那一类**没有** `tool/result.meta`);两者都要求 `data.name === "team_link_send"`,且事件自带时间戳(拿不到时间的那条不参与视图); - **结构化回执按 `callId` 配对**:`tool/result` 的 `meta`(判别符 + 版本认)用 `data.message.source.callId` 挂回**那一次**调用,取回执里**投递当时**解析出的逐目标 `sessionId` / `expr` / `outcome`(全队表达式在那一刻已经展开,换届之后仍指向当时那个会话);形状不认 ⇒ 当作没有回执,**不编**一个投递事实; - **没有回执** ⇒ 按**实参**兜底解析寻址表达式。其中 `team:/` / `team:/*` 是**按读取时的名册**解析出来的 ⇒ 该行**如实标注「按当前名册解析」**(换届之后表达式指向的人未必是投递当时那一个);裸 id 是字面量,不带这个标注。这是读数,不是裁决; - **窗口的单位是「最近 N 次 send」,不是 N 条消息**:`send` 记录取 `.slice(-TASK_INBOX_MESSAGES)`(N = 20,与收件视图**共用同一个屏界常量**);**渲染行再截同一个常量**,超出时给一行 `(本次还有 M 行未显示:一行 = 一次投递里的一个目标;同一屏界常量 TASK_INBOX_MESSAGES)`(M = 真实差额),**不静默截断**; - **反应判据(写死,不许自由发挥)**:对「目标 `T`、投递时刻 `t`」—— - **未点名读 `T`** ⇒ 标 `未读`(N2,不猜); - **点名读了 `T`** ⇒ 扫 `T` 的 surface:`t` 之后**存在任何 assistant 事件** ⇒ `✅ 有反应`;否则 ⇒ `⚠ 无后续反应`; - **回执里 outcome 不是 `delivered` 的那一笔** ⇒ `:未投递(outcome=<回执里的原值>)` —— **第 4 态**(D8 回填):措辞**只陈述回执自己的 `outcome`**,不下结论(不写「失败」「被拒」这类裁决词),也**不冒充**成上面三态里的任何一个(那一笔根本没送到,谈不上「有没有后续反应」); - **这次寻址没解析到具体会话** ⇒ `:未读(这次寻址没解析到具体会话,本视图不猜目标是谁)`; - 输出形如: ```text --- 派生回执(我发出去之后,对方动了没有)--- - t-7 → worker-b(09-26 21:12):✅ 有反应(21:19 起有 assistant 事件) - t-8 → worker-c(09-26 21:30):未读(未点名读该会话;要读 → readIds=[""]) - t-9 → worker-d(09-26 21:44):⚠ 无后续反应(该时刻之后无 assistant 事件) - t-10 → worker-e(09-26 21:40):未投递(outcome=refused) ``` - **它是读数,不是裁决**(N4):「无后续反应」既可能是**没接住**,也可能是**还没想完** —— 卡里只陈述事实(有没有后续事件),不下结论。 ## 5. 红线 1. **成本不变量**:单次调用 surface 读数 ≤ `PREVIEW_SESSIONS`(断言锁死,含点名与分页两条路径)。 2. **未读必标注**:任何被跳过的行都给文案;**禁止**用空数据算 verdict(沿用 §3.1 的既有口径)。 3. **全只读**:本批不新增任何写路径;`team_link_send` / `team_link_team_append` / `team_link_roster` 的写面**一行不动**。 4. **既有行格式不破**:`list_sessions` 的**每行结构**逐字保持;本次**唯一的格式新增**是列表末尾那段「未读 Y 行 + 怎么读」提示(默认调用也带 —— 它是提示不是载荷),以及新参数。既有断言零回归。 5. **措辞**:不出现「状态=完成」这类把主张写成事实的词(N4)。 ## 6. 验收判据(U1–U14) | # | 判据 | 覆盖 | | --- | --- | --- | | U1 | 默认调用:**每行结构与现状逐字一致**(读前 12 行、行外标未读);末尾新增「未读 Y 行 + 怎么读」提示段(**本次唯一格式新增**) | 4.1 / 红线 4 | | U2 | `offset=12`:第 13 行起给出真实 verdict,前 12 行不再是「已读」那一批 | 4.1 | | U3 | `readIds`:窗口外的 id 被点名后给出真实 verdict,且返回体**标明本次是点名读** | 4.1 | | U4 | `readIds` 超上限(>12)→ **拒绝**,且**零读**(stub 计数为 0) | N2 | | U5 | `readIds` 含未知 / 不属于本列表的 id → 拒绝并指出该 id | N2 | | U6 | `readIds` 与 `offset` 同现 → 拒绝;`offset` 非整数 / 超界 → 拒绝并给有效区间 | 4.1 | | U7 | **成本不变量按工具分账**:`list_sessions` 三路径 ≤ 12;`team_read` ≤ `min(12, 1+|readIds|)` 且**自读占额**;`team_link_status` ≤ 12(stub 计数逐一断言) | N1 | | U8 | 状态卡六段齐:角色在位 / 换届 pending(token **掩码**)/ 看门狗 / 会话面 / 活性(有界)/ 台账尾 | 4.2 | | U9 | 状态卡**零写入**:调用前后 `settings` 与磁盘文件逐字节不变 | N3 | | U10 | 收件视图:从调用方 surface 抽出 `ref=t-` 并分组;无 ref 的消息**不进视图但如实计数** | 4.3 | | U11 | 收件视图**未点名时目标面零读**(stub 计数);点名后才读 | N1 | | U12 | 回归:`list_sessions` 既有行格式与既有断言零回归;`team_read` 的**既有四块**(roster + decisions + discipline + tasks)零回归,**唯一新增为收件视图 / 派生回执块** | 红线 4 | | U13 | 预警注记:`N == 0 且 M > 0` 时出现、否则**不出现**;且**零额外 surface 读**(stub 计数) | 4.2 / N1 | | U14 | 派生回执三态正确:`✅ 有反应` / `⚠ 无后续反应` / `未读`;**第 4 态**(回执 `outcome != delivered` ⇒ `:未投递(outcome=…)`,只陈述 outcome)同样只陈述、不冒充反应读数;**未点名时目标面零读** | 4.4 / N2 | ## 7. 明确不做(本批) 1. **不做客户端面板渲染** —— 本批只做**数据面**(工具输出即人可见的一屏,与 `list_sessions` 同一形态);面板留后续批。 2. **不做缓存 / 索引**(不加缓存文件、不加内存索引、不持久化任何读数)—— N3。 3. **不做自动后台巡检**(看门狗仍是既有机制;本批只**显示**它,不改它的触发逻辑)。 4. **不做跨进程 / 跨机聚合**(本批都在本进程可读的面上)。 5. **不提高 `PREVIEW_SESSIONS` 的默认值**(要读更多 = 点名或分页,把成本决定权交回调用方)。 6. **不定形态(agent-team 档)下的状态卡行为** —— 本批实现时**形态字段尚不存在**;该段**由 B 批(形态)实施**(跨批接缝,显式声明,不留悬空):状态卡**追加第 ⑦ 段「形态」**(形态 + Lead + 成员名册投影 + 完整能力矩阵 + 形态诊断行),A 批的六段一字未动 —— 见 [team-mode-batch-design-2026-09-26.md](team-mode-batch-design-2026-09-26.md) §4.3。 ## 8. 显式假设与待验 | # | 假设 | 不成立的后果 | 验证方式 | | --- | --- | --- | --- | | A1 | `ctx.sessionQuery.readSurface(selfId)` 对**调用方自己**的会话可读 | 收件视图默认路径不成立,需改为读台账 + 目标面 | 实施期实测 | | A2 | 点名读 12 个仍在预算内:`list_sessions` 的预算是 **60s**(`lib/index.js`:9047 实读;README:498 记 26 会话串行读满 50 行实测超 60s、修后约 15s);新工具 `team_link_status` 用 **30s**(与 `team_read` 同级,常量便于调) | 点名上限要下调 | 实施期实测一个真实工作区 | | A5 | 读窗内的 surface 事件**带可用的时间戳**(预警注记的锚与计数都依赖它) | 注记降级为「无法计算」并**如实标注**,不猜 | 实施期用一个真实会话面验证 | | A3 | 消息的 `ref` 在 surface 事件里**可稳定抽取**(banner 首行格式固定) | 收件视图需改为读台账侧记录 | 实施期用真实投递样本验证 | | A4 | 状态卡输出长度**有界**(一屏) | 需要分节 / 截断并标注 | 实测(上限写进断言) | ### 8.1 实施期实测结果(2026-09-26,逐条如实标注) | # | 结论 | 证据 | | --- | --- | --- | | A1 | **harness 内成立**;真机未复验 | `team_read` 的自读走的正是列表工具一直在用的同一个 `readSurface` 调用面,U10/U14 的三态(✅ / ⚠ / 未读)全部由它派生;**「DSH 进程内对调用方自己可读」这一条待在真机/宿主复验**,不当作已证 | | A2 | **读数已锁死;墙钟未实测** | 三工具的 surface 读数上限逐条断言:`list_sessions` 三路径 12 / 2 / 12 · `status` 12 · `team_read` 4(1+3)与 12(1+11,15 个点名时);三个 `timeoutMs` 常量一字未动(60s / 30s / 30s)。**真实工作区的墙钟实测**留给宿主 | | A3 | **成立**(对 §3.4 banner 形状的样本) | `ref=t-` 从既有事件文本里稳定抽出并分组;`ref` 是别的值(旧样式的 `slp-…`)**不归组、只计数**——有意的归组面收窄,已断言 | | A4 | **成立** | 六段 + 活性 12 行(超出如实标注)+ 台账尾 `tasksTail`(默认 5、**上限 20,超出拒绝**)+ 会话面 ≤ `LIST_LIMIT`(50);界是常量与拒绝路径。**未做真机像素级「一屏」测量** | | A5 | **成立** | 注记的锚 `T0` 取自真实事件的 `time`;三态(触发 / 不触发 / **无时间戳时如实标「无法计算」**)都有断言 —— 其中**「无时间戳」那一条由 2026-09-27 的分歧审计修复轮(D1)补齐**(此前本行与账本都写「都有断言」,而 `host-half.test.mjs` 里 grep `无法计算` = **0 命中**;补齐后三态各有一条,读数见 `docs/verification-log.md` 的修复轮一节) | **已知残留(代码评审 🔵#3,2026-09-27)**:「无法计算」那一句把两种成因并在一起 ——「本读窗内没有带时间戳的消息」与「本读窗的 surface 全部没读到」。当前措辞已诚实(不猜、不下结论,测试钉住了「事件无时间戳」这一档),但**区分成因属于输出文案的行为改动**(需新增一档断言 + 红相证据)。**留待下一轮**(与「测试沉默点」类残留同批处理)。 ## 9. 实施分批(3 阶段) | 阶段 | 目标 | 文件 | 自检 | 实测 | | --- | --- | --- | --- | --- | | 1 | 读窗:`readIds` / `offset` / 成本不变量 / 尾巴提示 | `lib/index.js` + `host-half.test.mjs` | U1–U7 转绿(新断言**实施前必红**) | ✅ 新增 23 条;红相 19 条 | | 2 | 状态卡:新工具 `team_link_status`(六段 + 预警注记 U13) | `lib/index.js` + `host-half.test.mjs` | U8–U9 / U13 转绿 | ✅ 新增 20 条;红相 19 条 | | 3 | 收件视图 + 派生回执:`team_link_team_read` 新块 | `lib/index.js` + `host-half.test.mjs` | U10–U11 / U14 转绿;U12 全程回归 | ✅ 新增 17 条;红相 16 条 | (合计新增 **60** 条宿主断言,其中 **54** 条在**实施前**的 `lib/index.js` 上实测红;另 6 条是刻意在两种实现上都通过的**回归锁**,逐条列在 `docs/verification-log.md` 的对应一节。) ## 10. 跨文档同步清单(与实施**同批**必改) 1. `README.md` §一:`list_sessions` 的新参数(`readIds` / `offset`)与成本不变量的说明 2. `README.md` §二:列表/活性段补「点名读与分页」的用法 3. `README.md` **新增一段「团队状态卡」**(放在 §三 看门狗之后或 §四 之前) 4. `README.md` §四:黑板读面补收件视图 5. `README.md` 工具一览表:新增 `team_link_status` 一行 6. `CHANGELOG.md`:新增条目(按「修了什么 → 为什么 → 怎么验证」体例) 7. `docs/verification-log.md`:本批红相/绿相读数 8. 《台账 + 形态设计》§7:步 2 / 4 标 ✅ 已实施;**步 6 标「收件视图 + 派生回执 均 ✅」**(两半都在本批)+ 提交哈希(父会话回填) 9. `README.md` **设计文档索引**:新增本档一行(不然索引与仓库实际不符) 10. 《台账 + 形态设计》§7 的步 2 / 4 / 6:加指向本档的链接(**「每批一档」** 自此成为本仓既定约定 —— 第一批的内嵌式(父档 §9)是特例,不再复制)