# Changelog 本文件记录 `dsh-team-link` 的变更史。版本号策略:`package.json` 的版本号**随发布统一 bump**——开发期累积的条目先记为「未发布」,发布时一次性收口(例如 0.3.1–0.3.6 的条目在 0.3.7 发布时一并落定)。 格式:每个版本按 **修了什么 → 为什么 → 怎么验证** 组织。凡涉及行为修复的条目都附**变异验证**证据(修复前必红 / 修复后全绿),这是本仓库的验收文化。 ## 0.5.2 — 2026-10-10(热修:重启后真机复验抓到的三处) **一句话**:0.5.1 让老团队「能」吃到自治那套;这一版修掉三处「**实际上吃不到、或者吃到了也看不见**」的缺陷 —— 全是只在真机上才暴露的那一类。 ### 三段话(用户视角) **① 判读行不再被行数上限吃掉。** 状态卡第 ⑧ 段有 12 行上限;当会话多到 10 行时,末尾的判读行(那段「本节只判上下文/预算…」)会被**静默裁掉** —— 也就是说**团队越忙,越看不到它**。现在先给判读行与轮换标记**留位**,被裁的只能是明细行,而且会明说 `(行数上限 12;已省略 N 行明细)`。 **② 重启后「按队补挂」真的会跑了。** 0.5.1 的补挂挂在插件挂载的后置链上,但那一刻策略数据**还没到位**(代码里原本就写着「activation 那一刻还是空的」)⇒ 补挂空转、**零写入而且一点声都不出**。现在数据一到位就补通知一次。重启后这类「配好了却没生效」不会再静默。 **③ 状态卡多了一行「attach 后置链」,失败不再无声。** 上面那处之所以要靠翻日志才发现,是因为后置链的失败**没有任何出口**(本部署 `ctx.logger?.warn?.` 是空操作)。现在 ③ 段末尾会报最近一次后置链的结果:`rearm 挂上 N 个定时器 · 段同步(计划变化 M 项;在场 S 个段) · 按队补挂:created X · present Y · skip Z(首因 …) · failed W`。四档都不静默:没记录就说「尚无记录」、还在飞就说「尚未落定」、整轮抛错报 failed。 ### 顺带修 / 澄清 - **`skip` 与 `failed` 分家**:此前「没有观察者 / 没有可盯成员」(正常)与「**写入失败**」「定时器没挂上」(真失败)都记成 `skip` ⇒ 一次真的写入失败会**伪装成正常跳过**。现在后者进 `failed` 并报首因;`arm-team` 命令面的输出也随之分档。 - **判据两处补漏**:U26 原本「等一个 tick」在负载下**偶发红** ⇒ 改按墙钟有界等待;U27 的「纯读」断言原本摘掉那一行会**空真**(恒真断言永远绿)⇒ 加「该行真的在场」前置。 ### 怎么验证 - `node host-half.test.mjs` → `ALL PASS / 1477 (failed: 0)`;`node client-half.test.mjs` → `ALL PASS / 310 (failed: 0)`(0.5.0 的 1442 → 0.5.1 的 1466 → **1477**)。 - 每条判据都有「修复前必红」读数(U25 / U26 / U27 + 本轮三条)。 - **真机待验(需重启)**:那一行后置链读数会直接给出「补挂卡在哪一步」。 ### 仍未验(承接 0.5.1) 1. ~~看门狗重挂的真机证据~~ ⇒ **本条已收口**:2026-10-10 **16:33:57** 探针 tick 到达(= 启动 16:23:55 + `intervalMinutes=10`,分秒吻合)。 2. **压缩阈值 0.45 的运行时读数**(阈值不外暴露;用首次压缩的触发压力旁证:≈**79%** 生效、≈**95%** 旧默认仍在)。 ## 0.5.1 — 2026-10-10(发布收口:让**既有团队**也吃到自治那套 + 判读行自证口径) **一句话**:0.5.0 那套(工作台摘要 / 角色文本 / 资源读数)此前只对**新建的团队**生效;这一版补上「老团队也能吃到」,并让状态卡自己说清「我判的是哪一类问题」。 ### 三段话(用户视角) **① 按队补挂:老团队不用重建,重启就自动获得工作台摘要。** 0.5.0 的「工作台摘要」是在**建队那一刻**挂上的,所以 10 月 8 日之前建的团队**从来没有**这条推送 —— 协调者只能靠自己想起来去看谁闲着(真机后果:某 worker 12:32 干完就**停摆 87 分钟**,协调者自己承认「我 feeding 失职」)。现在改成**每次插件挂载都按名册检查一遍**:哪个队缺这条注册就补上(幂等、只在缺时写一次、补挂失败不影响挂载)。⇒ **重启一次,老团队也会被推着干活**。 **② `arm-team`:不想等重启,一条命令补。** `team_link_watch` 新增 `action="arm-team"`:**现任协调者**自己跑一条,就能给自己带的那个队补上工作台摘要(同样幂等;非现任 / 队不在名册 / 现任空缺都会被拒并说明原因,理由按 `policy.writer` 档分别写)。⇒ 给「我现在就要它动起来」的场景一条即刻通道。 **③ 范围界定:状态卡会自己说清「这段读数担保什么」。** 第 ⑧ 段判读行末尾现在**固定**接一句:`(本节只判上下文/预算;宿主工具闸门——如写入自动评审 429——属另一类,见工具报错原文,不在本节口径内。)` 为什么加:真机发生过一次混淆 —— worker 的**写入**被宿主的自动评审配额拦下(原文 `429: 您已达到每周/每月使用上限…2026-10-14 00:27:38 重置`,且报错里写明「工具本体未执行」),而「预算/配额」这个词被拿去解释成「上下文要耗尽」。**两类是不同的东西**:本节只担保前者;混一次就会**既把真事当幻觉、也把真幻觉当真事**。 ### 同版内还改了两处 - **巡逻开始兜「晚启动的协调者」**:段注册此前只在「插件挂载那一刻 + 名册写入」两个时机发生;协调者若在挂载时没在跑,它的岗位文本就一直空着。现在**每轮巡逻顺带对齐一次**(纯读名册 + 活代理表,只在有变化时才写进程内注册表)。 - **跨工作区读窗不再看错人**:状态卡的「会话面 / 活性 / 资源」三段此前按**调用方**所在工作区取窗口 —— 你在 A 工作区看 B 工作区的团队时,那三段列的是 **A 的人**。现在优先按**团队自己的 workspace** 取;取不到或宿主不支持时**如实标注**(例如「读窗=调用方工作区 xxx(回退:该队无 workspace 记录)」),并修掉一处「优先支恒不可达」的调用(`listSessions(workspace)` → 宿主真有的 `filterSessions([{kind:"cwd",values:[workspace]}])`)。 ### 怎么验证 - 套件:`node host-half.test.mjs` → `ALL PASS / 1466 (failed: 0)`;`node client-half.test.mjs` → `ALL PASS / 310 (failed: 0)`(0.5.0 的 1442 → 1466,+24)。 - 每条新判据都附「修复前必红」的读数(U20–U24 各一组定向变异;两轮独立复核 task-25 / task-27 逐项复现,含程序化逐字比对与禁词指纹)。 - **真机待验(需重启)**:两个既有团队是否自动各补一条 `origin=auto` 注册;协调者是否开始收到摘要(不必等用户催);判读行是否带范围句。 ### 仍未验的两条(承接 0.5.0) 1. **看门狗重挂**的真机证据(需「先有注册 + 再重启」;取证注册 `wd-75e4e35f-…` 已就位)。 2. **压缩阈值 0.45 的运行时读数**(阈值不外暴露;用首次压缩的触发压力旁证:≈**79%** 说明生效、≈**95%** 说明旧默认仍在)。 ## 0.5.0 — 2026-10-10(发布收口:团队自治三批 + 一次真机修复) **一句话**:协调者不再"自己闷头干活",也不再"不知道 worker 是死是活";状态卡多了一段,能直接看到每个会话用了多少上下文、跑了多少轮、该不该换人。 ### 你会看到的变化(按场景) | 场景 | 以前 | 现在 | | --- | --- | --- | | **协调者开工** | 它可能自己动手写代码、跑命令,要靠人反复纠正「你是管理、不是实施」 | 它的**系统提示里就写着岗位边界**(只管理 / 协调 / 决策 / 评审 / 设计,不实施);相关工具返回里也会重钉一遍 | | **worker 干完一轮 or 闲着** | 协调者不知道,没人提醒就不派活、也不并派 | 建队时**自动**挂一条「工作台摘要」:把全队按 **运行中 / 空闲待派 / 刚干完 / 需处置** 四组报给协调者;**它自己正忙也照投**(失联告警那类提醒的忙时抑制不变),同一注册 10 分钟内不重复打扰 | | **worker 说「预算耗尽」** | 没有读数可核,只能采信 | 状态卡**第 ⑧ 段**给出每个会话的真实读数(窗口 / 压力 / 累计 / 轮次),末尾一句**判读行**——"预算耗尽"这个词**只允许以否定式出现在那一行** | | **会话跑太久,要不要换人** | 没有判据 | 超过 **100 轮**或 **2 亿 tokens** 的会话会被标 **⚠ 轮换候选**,并给出那句现成命令 —— **只标记,不自动换** | | **DSH 重启之后** | 看门狗注册变成"哑的":注册还在,但没人巡逻 | 每次策略挂载后**自动重挂定时器**;过期条目跳过,清扫一路推进到清空 | | **想彻底关掉这些文本** | 没有开关 | 新增策略键 `sections.{role, resources}`:`role=false` 关角色/宪章文本(工具返回里的重钉一起关),`resources=false` 让资源行在三处全缺 | ### 为什么这么改 > 用户原话(2026-10-08):「协调者三次被纠正『你只负责管理,不要自己实施』」「worker 报『预算耗尽,实现未动』,而它的实测用量是 131M token / 32 轮」。 三批围绕三件事收口:**职责写进提示**(不靠人反复纠正)、**状态自动播报**(不靠人提醒)、**用量有真值**(不靠 worker 自述)。 ### 新依赖两个宿主服务(都做了降级) - `sessionProjections` —— 资源读数(第 ⑧ 段 / 提示段资源行 / 摘要脚注) - `agent.ctx.systemPrompt` —— 挂角色与资源文本段 两者都**晚挂取用 + 特征探测**:宿主没有时**零注册 + 一行警告**,功能回退到「工具返回重钉」,**不报错、不影响既有功能**。 ### 发布前的真机修复(同版内) 第一次上线后实测发现:状态卡**第 ⑧ 段每一行都读不出数**,全部报 `session.snapshotEvents is not a function`。 根因是我们读投影时传了一个**自己拼的假会话对象**,而宿主只认真会话。已修:只取**活动会话真身**,取不到就如实标「非活动会话」(不再编数)。 **更要紧的是修了测试桩**:桩此前"来者不拒",所以这个 bug 一路溜到真机(当时 1438 条断言全绿)。现在桩**照宿主的样子挑剔**——同类断层以后进不了套件。 ### 怎么验证 - 套件:`node host-half.test.mjs` → `ALL PASS / 1442 (failed: 0)`;`node client-half.test.mjs` → `ALL PASS / 310 (failed: 0)`。 - **真机复跑**:第 ⑧ 段活会话给出真实读数(实测 `压力 45.7%(窗口 1.00M) · 累计 ≈149.22M · 第 48 轮`),非活动会话如实标「非活动会话」。 - 每条新判据都附**变异红相**("修复前必红"的读数),逐条见 `docs/verification-log.md` 的批 1/2/3、批 4 与「2026-10-10 第二次重启后的复验」各节。 ### 仍未验的两条(如实) 1. **看门狗重挂的真机验证**:需要「先有注册 + 再重启」;已注册一条取证用注册(`wd-75e4e35f-…`),等下次自然重启。 2. **压缩阈值 0.45 的运行时读数**:阈值不外暴露,用「首次压缩的触发压力」旁证 —— ≈**79%** 说明 0.45 生效;≈**95%** 说明旧默认仍在。 ### DEFECT-5(真机 2026-09-30):`revive` 复活出来的 worker 第一回合必死——resume 面漏了同一份「能跑起来」的组装时序 > 用户原话:「worker会话重启后报错 —— 处理失败 / 本轮运行失败 prompt variable "{{model}}" has no value for this assembly (section "deployment:persona-prefix") / UNKNOWN」。 > > **取证链(每条都可复核)**:策略档 `~/.dsh/team-link/policy.json` 的 `recoveries` 行记着团队 `mal-analyze-cli-20260929` 的 worker-1..4 在本机 **12:47:46 / 12:50:28 / 12:50:33 / 12:50:38** 各被 `team_link_recover action=revive` 复活过;四个会话日志(`~/.dsh/sessions/--D-workspace-mal-analyze-cli--/team-link-…-worker-N-…/session.v4.jsonl.zstd`,逐帧解 zstd)里**复活后的第一回合**(turn 14 / 18 / 18 / 22,12:50:54 / :55 / :55 / :55)逐字报同一个错;同一晚**未被 revive 过**的 threat-intel 四条 worker 日志里,这个错 **0 命中**。 > > **根因(与 DEFECT-1 / DEFECT-3 同一个缺陷类,只是换了一条路径)**:`recoverRevive` 调 `ctx.agents.resume({ resumeSessionId })`,一个键都没多给 —— ① 没有 `agentOptions` ⇒ 复活出来的 agent **没有任何模型选择**,而 `{{model}}` 的唯一来源就是 `context.agent?.options.model`(`dsh-agent-loop:1565`)⇒ 首回合死在 `deployment:persona-prefix`(**DEFECT-3 的机制原样复现,错误文案逐字相同**);② 没有 `setup` ⇒ **preset 组装源没挂**,而 `standard` preset 正是这具 agent 的**工具面**(`dsh-web-app/presets/standard.patch.yml`:bash / pwsh / fs / jobs / skill / goal / plan-mode / compaction / delegation / web 全在组成里)⇒ **DEFECT-1 的机制**。宿主**自己**的 resume(`dsh-api-session-controller:399-410`,也就是人从侧边栏打开一个会话时走的那条路)给的正是三样:`resumeSessionId` + `agentOptions: agentDefaultModel.currentSelection()` + `setup: composeAgent(presetForObservation(obs)).setup`(存下来的 preset → `agentPresets.mount`)。 > > **为什么 0.4.2 没拦住**:DEFECT-1 / DEFECT-3 两轮的修复**全部落在 `agents.create` 面**(`buildTeamSessionCreateOptions`),而 revive 走的是 `agents.resume`;更糟的是旧断言把缺陷**钉成了规格** —— `U26 revive` 原本断言 `Object.keys(resumeCalls[0]).length === 1`(**恰好一个键**)。**「会建 ≠ 能用」第三次以同一种形状出现**:同一份模板时序在插件里有两条入口,当时只对齐了一条。 #### ✨ 改了什么 - **新增 `buildTeamSessionResumeOptions(ctx, sessionId)`**(`lib/index.js`):revive 面按宿主自己那条 resume 路径的时序组装三样 —— ① 模型选择走与 create 面**同一个**解析器(`resolveTeamSessionModelSelection(ctx, {}, sessionId)`;复活面没有调用方可以给 model,所以永远是「两侧都没给」那一支),解析不出来就**拒绝恢复**(fail-closed,与「宁可不建」同口径);② preset 取**这个会话自己存下来的那一个**(`agent.session.header.agentPreset` —— 宿主 resume 的 `meta` 就是 `structuredClone(handle.header)`),读不到就显式传 `undefined` 交给 `resolve` 按宿主缺省解析(与 create 面「缺省也解析」同口径,**不是**「读不到就不挂」);③ 模型选择钩子与 create 面**同一个**(`installTeamSessionModelSelection`),挂在复活这具 agent 自己的 setup 上下文上。 - **revive 调用点**改为 `ctx.agents.resume({ resumeSessionId, agentOptions, setup })`;组装失败 ⇒ **零复活、零写入**,报告点名原因、后果与出路。两档失败面**不是同一档**(如实区分):模型选择解析不出 ⇒ 失败在调用**之前**(`resume` 一次都没被调);存量 preset 解析不出 ⇒ 失败在 `setup` 里、`resume` 调用**已在飞**并在组装阶段回滚(真工厂是 setup 抛错即回滚发布,不留活代理)。 - **两处有意不降级(如实声明)**:`agentPresets` 服务缺席是**可服务降级**(恰一行具名 warn、复活照常,与 create 面同款);而**存量 preset 解析不出来**是**显式失败**——绝不静默换成另一支组成源(一个顶着旧名字、跑着别的工具面的会话比一次可读的失败更难查),与 create 面「`preset=` 指了个解析不出来的 id ⇒ 拒绝创建、不静默降级」同口径。 - **一处与宿主的有意偏差(如实声明)**:宿主那条路的 selection 取值器会优先读「会话上次用的模型」(持久化的 request header);本插件没有「每会话模型」这个概念,这里与 create 面一致地取**当时的缺省**。复活后若缺省已变,模型会跟着缺省走 —— 人设文本与实际路由仍然自洽,这也是本插件在创建时就有一贯的语义。 - **组装器不取 preset 租约**(`acquireScope`):那是 `dsh-webhook` **create** 时序的一环(解析在前、create 在后,中间窗口要把该 generation 钉住);resume 的参照物(宿主的 `composeAgent`)不取 —— `mount` 自己 `retain` → `bind` → 释放,窗口内不会漏。照抄 create 的租约只会多造一处「谁在什么时候还」的状态。 - **模型可见描述面同批同步**(revive 对话框的「这一步做什么」+ `team_link_recover` 的工具描述):写明 revive 现在会把模型选择与这个会话自己的 preset 组装源一起装上、组装不出来就拒绝恢复。 #### 为什么是这样修 修复的参照物不是「让报错消失」,而是**宿主自己那条 resume 路径**:插件与宿主对同一个会话 id 必须交出同一个「能跑起来的代理」,否则「插件复活」与「人在侧边栏打开」就是两种生命周期。DEFECT-1/3/5 是同一份清单上的同一种失误的三个爆点 —— 每次都是在**另一条入口**上重新漏一遍模板时序;因此这一轮把判据按**入口**钉(create 面已有,resume 面本轮补上),并顺手修了夹具里同款的盲区(见下)。 #### 🔍 代码评审修复轮(同批;1 条技术性驳回,6 条已处置) - **🟡#1 README 三处读数滞后** → 已同批同步(徽章 / 稳定性行 / §十 当前读数 → **1324**,并把本轮红绿相与 `1324 = 1314 + 10` 的账目写进去;第 2 轮把复活面口径统一后又多一条断言,计数由 1323 改成 1324,同一处同步)。 - **🟡#2 「零 resume」言过其实** → 已按实现事实改写(见上「改了什么」第 2 条):⑥ 档失败在调用前(零 resume 调用),⑦ 档失败在 `setup` 里(resume 在飞并回滚);并新增 `DEFECT-5 ⑦ 口径` 断言把两档**分开钉住**。 - **🟡#3 复活面复用了创建面的「出路」** → 已修:`resolveTeamSessionModelSelection` 新增 `face` 参数,复活面用自己的判词与出路(`team_link_recover` 没有 `model=` 参数,「传 model=」那条指路在本工具里**执行不了** —— D8 那轮的裁定),**create 面文案逐字未改**。 - **🟡#4 「人设文本与实际路由自洽」被质疑** → **技术性驳回**(证据):宿主每起一个 agent 实例时 `requestHeaderLogged` 都是 `false`(`dsh-agent-loop:761`),而 `prepareRequest` 的路由基线取的是 `this.options.provider` / `this.options.model`(同档 `:1167-1178`,持久化 request header 只在 provider/model 相同那一支里贡献 `reasoningEffort`)⇒ **路由 = `options.model` = `{{model}}` 的取值来源**,两者同源;`installTeamSessionModelSelection` 在已有 header 时早返回只是不再抹掉继承来的 effort,它不改路由。**该论断要求的「真机核对一枚缺省 != 上次模型的 worker」仍列为待验项**(见题末)。 - **🔵#5 新断言直取下标可能在 break 时炸套件** → 已修:`DEFECT-5 ②③④` 全部改成先判 `length === 1` 再取下标(与同组 ⑤ 同形)。 - **🔵#6 resume 桩先发布后 setup** → 已修:发布(进注册表 / 解除 hidden / 记进 `resumedAgents`)挪到 `setup` **成功之后**,并在 ⑦ 补「失败后不留活代理」的读数(`agentFor` 为 undefined + `agentIsLive === false`)。 - **🔵#7 create 面只判 `resolve` 就裸调 `mount`** → 已修:组成面守卫改为**双判**(`resolve` + `mount`),形状断层显式点名(两个读数都写进文案)并拒绝创建,不再是裸 `TypeError: agentPresets.mount is not a function`;新增 1 条断言 `组成面(mount 缺失·形状断层)`(夹具加 `omitMount` 开关)。**第 2 轮评审又指出复活面在同一断层上仍走降级**(同一形状断层两个面两种判词)→ 已同批统一:三分法现在是**服务缺席 ⇒ 降级(一行具名 warn)** / **服务在场但形状断层 ⇒ 两个面都显式拒绝** / 两方法都在 ⇒ 正常组装;新增断言 `DEFECT-5 ⑤b` 钉住复活面这一档。 - **第 2 轮评审的另一条 ≤🔵(文档措辞残留)** → 已清:证据账里那句「组装失败 ⇒ 零 resume、零写入」按两档事实重写。 #### 🧪 怎么验证 - **新增 10 条 + 改写 1 条宿主断言(可 grep)**:`U26 revive:`(改写的形状断言)· `DEFECT-5 ①`(resume 恰三键 + `agentOptions` 逐字等于 `currentSelection()`)· `DEFECT-5 ②`(挂的是**它自己存下来的** preset、恰一次、绑在它自己的 setup 上下文上)· `DEFECT-5 ③`(钩子的**行为**读数:同名路由抹掉继承来的 `reasoningEffort`)· `DEFECT-5 ④`(header 无 preset ⇒ 缺省也解析)· `DEFECT-5 ⑤`(服务缺席 ⇒ 降级 + 恰一行具名 warn)· `DEFECT-5 ⑤b`(服务在场但只有一半 ⇒ 与创建面同一句判词:显式拒绝)· `DEFECT-5 ⑥`(模型解析不出 ⇒ 拒绝恢复、零 resume 调用、零写入)· `DEFECT-5 ⑦`(存量 preset 解析不出 ⇒ 显式失败、点名那个 preset、**失败后不留活代理**)· `DEFECT-5 ⑦ 口径`(两档失败面不同:⑥ 零 resume 调用 vs ⑦ resume 在飞并回滚)· `组成面(mount 缺失·形状断层)`(见评审修复轮)。 - **夹具同批修复(这一课的重点)**:`agents.resume` 桩原先用 `options.sessionId` 当 setup 上下文的 `agentId`,而 resume 面**只有** `options.resumeSessionId` ⇒ 复活面上 `agentId` 恒为 `undefined`、桩的 `mount` 按「无 scope」拒绝,**恰好把 DEFECT-5 要钉的那一层挡在套件外**(与 DEFECT-1 那轮「桩只镜像自家实现的调用面」是同一个盲区);同批两处保真修复:桩的复活 header 从**持久化记录**里取 `agentPreset`,且**发布挪到 `setup` 成功之后**(真工厂是 setup 抛错即回滚发布)——后者让「fail-closed 之后不留活代理」这半条变成可读的读数。 - **红相(实跑)**:`git checkout -- lib/index.js`(夹具不动)⇒ `host 1324 (failed: 11)` —— 恰是改写的 1 条 + 新增的 10 条,其余 1313 条全绿;还原后 `lib/index.js` sha256 `609F9D47A35BE7AFA07B3C6C31CC192E40209B7F86DDF7AFB4F1811E4F7941E0` byte-identical。 - **绿相(实跑)**:`node host-half.test.mjs` → `ALL PASS` / `assertion total: 1324 (failed: 0)`(1313 + 11);`node client-half.test.mjs` → `ALL PASS` / `assertion total: 310 (failed: 0)`;退出码均 0。 - **真机复验(2026-10-01,重启窗口后)**:① **跑的是修复后的代码**有铁证 —— worker-1 的 `request/header` 事件里内嵌的 `team_link_recover` 工具描述,重启前那次不含本轮新增句、重启后那次(13:52:34)含「真机缺陷 #5」与「resumeSessionId + agentOptions + setup」;② **故障不再复发** —— 四个 worker 日志里该错误的**真失败**(`turn/end{kind=error}`)全部落在重启(13:34:17)**之前**,之后 0 条(之后仍命中该字符串的 8 处均为 `request/header` 内嵌文本,逐条按事件类型核对);③ 四个 worker 重启后仍在持续跑(末次 `turn/start` 到本机 02:19–02:22),两个团队 5/5 有活代理。**④ 插件 revive 路径已在生产上走通(更正注记)**:`mal-analyze-cli-20260929` 那四个 worker 确实不是插件带回来的(协调者 13:50:19 连发的四次 revive 都被「该角色的现任……**有活动代理**」拒绝,它们由宿主自己的 resume 路径带回来)—— 但**同一台机器、同一次重启之后**,团队 `0930-threatintel` 的协调者用 `team_link_recover action=revive` **成功复活了 worker-1**:审计行落在 `2026-09-30T05:38:19.847Z`(本机 13:38:19,晚于 boot 13:34:17 ⇒ 执行者是修复后的代码),复活后 `request/header`(13:39:57,`reason=resume`)内嵌的工具描述含「真机缺陷 #5」;该会话 revive 之后的 14 个 `turn/end` **全部 `kind=completed`**(末个本机 03:39),整份日志里那类真失败 **0 条**。**仍未取得的那一格(不冒充)**:反事实(不带 `agentOptions` 时这批会话会不会死)无法在真机直接观测,那一层由套件的 `DEFECT-5 ①/③` 与修复前那四条错误原文共同支撑;🔵#4 登记的「缺省 ≠ 上次用的模型」那枚 worker 仍待验(已取 `0930-threatintel/worker-1` 复活前后 header 均为 `deepseek-official/deepseek-flash` ⇒ **同值、区分不了**,不冒充已验)。 ## 0.4.2 — 2026-09-29(发布收口:DSH 0.2.0-rc.1 两处运行时换代断层 + preset 租约释放 + 客户端团队面板可读性重设计) > 本版一次性收口下列 **3 个「未发布」条目**;package.json 已 bump 到 **0.4.2**。上一版为 **0.4.1(2026-09-29)**。 > > 三条都来自同一天的真机反馈,且是同一条链:0.4.1 让插件在 0.2.0-rc.1 上**装得上**(peer 装载闸门),本版让它**用得了**(preset pin API 换代),并顺手把租约**还干净**(`acquireScope` 是租约,不是可以丢掉的返回值);第三条来自 owner 对团队面板可读性的截图反馈。 ### DSH 0.2.0-rc.1 宿主 API 换代:preset pin `standingKeyFor` → `acquireScope`(2026-09-29) > 同一天的第二处 0.2.0-rc.1 换代断层:上午的 peer 区间闸门(0.4.1)让插件**装得上**,这一处让它**用得了** `/team_session`。真机两次失败(15:35:40 / 15:46:18,团队 mal-analyze-cli-20260929,`n=4 准备接手继续开发`):回执「批量建队未全部成功」,命令输出逐字 `未创建(创建失败(agentPresets.standingKeyFor is not a function))`,worker-2/3/4 按失败即停未尝试、团队零落地(无 roster / 无黑板 / 无会话目录,仅两张 5 分钟 TTL 的 pendingCreate 孤儿票据由过期清扫自理)。取证链:调用方会话日志 `team_link_export` 取 `command/done` 原文 → 宿主 `app.asar` 文本检索 `standingKeyFor` **零命中**、官方模板同一时序已是 `acquireScope`(注册面签名 `async acquireScope(id?): Promise<{ key: ScopeKey } & AsyncDisposable>`)。 #### ✨ 改了什么 `lib/index.js` 的 `buildTeamSessionCreateOptions`(`/team_session` 与 `successor:"auto"` 共用的唯一创建路径):pin preset 的调用改为**双代兼容**——`acquireScope` 在场优先现名;否则回退 `standingKeyFor`(0.1.x 宿主);两个都没有 ⇒ 抛**显式**错误(点名两代已知形状与断层),批次报可行动的原因,不再让裸 TypeError 当错误文案。返回的 scope 沿既有形状只确认「取得到」、不提前 dispose(提前释放可能把仍在运行会话的 standing 组成拆掉)。改动面 = 1 个实现文件 + 宿主测试套件(夹具与断言)。 #### 为什么是兼容而不是只改成新名 peer 区间仍声明支持 `^0.1.0-rc.6 || ^0.1.5-rc.1`(0.4.1 的立场:老宿主装得上),运行时却只认新名会把「装得上」变成「一建队就死」——同一份声明两种真相。回退分支正是给还停在 0.1.x 的宿主的。 #### 🧪 怎么验证 - **夹具换代**:`makeAgentPresets` 默认建模 0.2.0-rc.1 真机形状(只有 `acquireScope`),新增 `pinSurface` 开关(`current` / `legacy` / `both` / `none`)+ `setup` / `teamSessionEnv` 的 `agentPresetsOptions` 透传;`standings` 仍是两代合流的同一读数,DEFECT-1 既有断言口径不变。 - **新增 4 条宿主断言**:现名成立(旧名零调用、preset 绑定判据不变)/ 两名都在只走现名 / 只有旧名回退成立 / 双缺 ⇒ 显式点名(含两个方法名、零创建零 pairs、无 `is not a function`)。 - **变异验证(红相实跑)**:夹具先改、实现未动时套件以**真机同款文案**崩——多条 FAIL 后中止于 U16 端到端(`okEnv.store()[0]` undefined=团队零落地的直接后果),可见逐字相同的 `创建失败(agentPresets.standingKeyFor is not a function)`;套件中止故未取得完整 failed 计数(自报计数只在结尾打印,如实登记)。 - **绿相(实跑)**:`node host-half.test.mjs` → `ALL PASS` / `assertion total: 1310 (failed: 0)`(1306 + 4,退出码 0);`node client-half.test.mjs` → `ALL PASS` / `assertion total: 305 (failed: 0)`(退出码 0)。 - **真机待验(如实登记)**:插件代码随 DSH boot 加载,修复需**重启 DSH** 后由用户重发 `/team_session` 闭环;本轮真机验到的只有故障面本身(两次失败的完整输出)。 ### preset 租约补丁:`acquireScope` 不是可以丢掉的返回值(2026-09-29,同批评审发现) > 上面那条把 pin 调用改成了「双代兼容 + 双缺显式报错」,方向对,但漏了一层:**`acquireScope` 不是 `standingKeyFor` 的纯改名**。宿主注册面的实现是 `retain(id)` + 一个 `[Symbol.asyncDispose]` 句柄(`users--` + `collect()`),文档原话「**A revision lease; dispose it after the scoped read completes**」;官方模板 `dsh-webhook` 是 `await using` 取(`lib/index.js:158`)、在 `finally` 释放(`:216`)。原修复 `await agentPresets.acquireScope(preset.id);` 把返回值丢掉、永不释放 ⇒ **每建一个会话泄一个租约**(该 generation 的 `users` 永不归零、`collect()` 永不执行)。原注释那句「宿主按会话生命周期持有、不提前 dispose」与宿主自己的文档和官方模板都相反,是补出来的理由。 - **修法**:照官方模板同一时序 —— 构建器只**取**,把可释放句柄经 Symbol 带外通道(`PRESET_SCOPE_RELEASE`)随 options 交给调用方;`createRootAgent` 在 `agents.create` 的 `finally` 里释放(`setup` 里那次 `mount` 在 create **期间**跑,租约必须活到那一刻)。释放失败只留一行 warn,绝不覆盖 create 的原始错误。旧代(`standingKeyFor` 返回纯值)**不伪造** dispose。Symbol 对宿主 `create` 不可见(它只读具名键)、对 `Object.keys` / 展开 / `JSON.stringify` 也不可见 ⇒ options 的**可见形状**与既有断言一字不变。 - **夹具同步(这一课的重点)**:桩的 `acquireScope` 原先只返回 `{ key }` —— **只镜像自家实现的调用面**,于是「永不释放」这层缺陷被永久挡在套件外(与上一轮「桩里只有自家在调的方法名」是同一个盲区,只是这次挡的是泄漏)。现在连句柄一起建模(`[Symbol.asyncDispose]` + `released` 读数)。 - **新增 4 条宿主断言**:租约必须释放(取 N 次 ⇒ 还 N 次)/ 失败路径也还(`finally` 不是「建成功了才还」)/ 旧代不伪造释放 / 句柄幂等(★ 控制项,证明「恰好一次」不是靠重复计数凑的)。 - **变异验证(实测红 → 绿)**:摘掉调用方的释放(=只改名不还租约)⇒ `FAIL pin 换代(租约必须释放)` + `FAIL pin 换代(失败也要还)`,`host 1314 (failed: 2)`;另 2 条在两种状态下都该绿(旧代形状 / 幂等控制项),**如实记,不冒充 4/4**。还原后 sha256 `D078611F…` byte-identical;绿相 `host 1314 (failed: 0)`。 ### 客户端团队面板可读性重设计:默认折叠 + 高度上限 + 层级(2026-09-29,owner 截图反馈) > owner 原话:「超长文字弹窗超长,而且文字太密集太多根本不知道应该看哪里」。诊断(本地视觉夹具 + 视觉桥独立复核):不是没渲染,是**信息架构**——五段默认全展开、段标题与正文同为 12px、面板无高度上限(弹窗跟着长)、三行角色文本每行 ~120 字还把同一串 session id 写了两遍。 - **改了什么**(`lib/client.js`,纯呈现,**不解析任何宿主字节**):面板加高度上限 + 内部滚动;段标题升到 13px/600、独立一行、带 `▾` 标记并可点开合(`h3` + `role="button"` + `aria-expanded` + Enter/Space);**默认全部折叠**——这一屏先当索引用(`data-collapsed` + 一条 CSS 规则,节点全在树里,只是不显示);段间加分隔线;结论行加左侧色条;缺数段不折叠(藏起来等于把「这一段读不到」也藏了)。 - **读数**(本地视觉夹具,真实 payload,452px 宽):弹窗 **941px → 406px**(展开态最多 425px);**5 个板块标题一屏全可见**(改前只有 2 个);面板 205px、无需滚动。视觉桥改前判「非常拥挤、有密集文字墙」,改后判「**没有密集文字墙,界面非常简洁**,5 个板块一眼列全」。 - **变异验证(实测红 → 绿)**:新增 **5** 条客户端断言(高度上限 / 段标题字号严格大于正文 / 默认全折叠 / 点标题即展开且展开前后 `panelLines` 逐字相同 / 缺数段不折叠);换回修复前实现 ⇒ `client 310 (failed: 5)`,**五条全红且套件跑完不崩**(首版判据在旧实现上抛 `TypeError` 把套件炸在半路、只报 3 条 —— 正是 `client-half.test.mjs:32-42` 那条 Y7 纪律的坑,已给判据加兜底)。还原后 sha256 byte-identical;绿相 `client 310 (failed: 0)`。 - **已知剩余(如实登记)**:展开「团队与角色」后三行角色文本仍长(宿主给的字节,同一个 session id 出现两次;客户端不得重写)—— 要治得改宿主的行渲染,会同时影响 `team_link_status` 的工具输出,**不在本版**。 ## 0.4.1 — 2026-09-29(发布收口:DSH 0.2.0-rc.1 兼容性闸门 + 部署接线改 `link:` + 策略持久化自持化 + C 批客户端团队面板与形态可视化 + 六轮代码评审/分歧审计修复) > 本版一次性收口下列 **10 个「未发布」条目**(开发期累积、按时间倒序);package.json 已 bump 到 **0.4.1**。上一版为 **0.4.0(2026-09-27)**。 > > **宿主版本支持**:本版声明并实测 **DSH 0.2.0-rc.1**;peer 区间同时保留 `^0.1.0-rc.6 || ^0.1.5-rc.1` 两个历史分支(累加,不撤),故 0.1.x 宿主**仍能通过闸门**——但本版实现相对 0.4.0 有实质改动,而**只在 0.2.0-rc.1 上实测过**。支持矩阵与「低于 0.2.0-rc.1 怎么办」见 [`README.md`](README.md) 的「宿主版本支持」一节。 ### DSH 0.2.0-rc.1 兼容性闸门:peer 区间补 `^0.2.0-rc.1`(2026-09-29) > 桌面版 2026-09-29 自动升级到 **0.2.0-rc.1**,本插件当天**整个没被加载**:工具凭空消失、插件页零提示。根因不在插件代码,而在 `package.json` 的 peer 区间没跟上宿主换代。**改动面 = 1 个清单文件(`package.json`)+ 文档(本文件 / [`docs/verification-log.md`](docs/verification-log.md) / `README.md` 依赖声明一节);实现文件零改动。** 账本见同档的「2026-09-29 DSH 0.2.0-rc.1 兼容性闸门」一节。 ### ✨ 改了什么 `package.json` 里 7 个 `@deepseek-ai/dsh-*` peer 的区间由 `^0.1.0-rc.6 || ^0.1.5-rc.1` 补为 `^0.1.0-rc.6 || ^0.1.5-rc.1 || ^0.2.0-rc.1`。`@deepseek-ai/cordis` **不动**:0.2.0-rc.1 里是 4.0.4,仍满足 `^4.0.1`,且闸门只查 `@deepseek-ai/dsh` / `@deepseek-ai/dsh-*` 名下的 peer。 **为什么是「整个没加载」而不是「报个 peer 警告」**:`dsh-desktop-host` 启动时用 `loadProfileDirectory()` 对 `dsh.profile.bundles` 里每个包跑 `@deepseek-ai/dsh-app-boot` 的 `evaluatePluginCompatibility()`;任一条 `@deepseek-ai/dsh*` peer 不满足 `semver.satisfies(运行时版本, 区间, { includePrerelease: true })` 就 `throw`,被同文件的 `catch` 吞掉后推入 `skippedBundles` ⇒ **该 bundle 的 patch 层完全不参与组合**(`layers.push` 在闸门之后),`cordis.patch.yml` 那条 `- id: team-link` 于是指向一个从未存在的 entry。这个跳过只经 `reportSkippedBundles()` 往宿主 stderr 打一行,而桌面壳只把宿主 stderr 留在内存里(64 KiB 环形缓冲),**仅在宿主异常退出时**才随崩溃报告落盘 ⇒ 用户侧零提示。 ### 🧪 怎么验证 - **修复前(闸门复刻,实跑)**:逐字复刻 `@deepseek-ai/dsh-app-boot/lib/index.js` 的 `evaluatePluginCompatibility()`,运行时版本取 app-boot 自己的 `0.2.0-rc.1`,semver 用 profile 那份 —— `dsh-team-link@0.4.0` 落在 **SKIPPED**,理由 `peerDependencies {"@deepseek-ai/dsh-tools":"^0.1.0-rc.6 || ^0.1.5-rc.1", …}`(7 条)。 - **修复后(同一脚本,实跑)**:`dsh-team-link` 移到 **PASSED**;SKIPPED 由 **14 → 10**(其中 1 个是本条,另 3 个是 `dsh-better-sidebar` 0.24.1 / `dsh-config-manager` 0.1.66 / `dshmarket` 1.66.5 在本轮期间各自更新到 0.2.0 就绪版,与本条无关)。 - **semver 判据本身**:`semver.satisfies('0.1.7-rc.2', '^0.1.0-rc.6 || ^0.1.5-rc.1', { includePrerelease: true })` → `true`(所以旧宿主上一直是好的);同一区间对 `'0.2.0-rc.1'` → `false`。 - **套件(实跑)**:`node host-half.test.mjs` → `ALL PASS` / `assertion total: 1306 (failed: 0)`(退出码 0);`node client-half.test.mjs` → `ALL PASS` / `assertion total: 305 (failed: 0)`(退出码 0)。**断言总数不变**(本轮不新增断言,故 README §十 的两处读数不动)。 - **0.2.0-rc.1 上的兼容性复核(静态,逐项对源码行)**:四项硬注入服务全在 —— `tools`(`dsh-tools/lib/index.js:2704`;`register()` `:2878`)· `sessionQuery`(`dsh-session-query/lib/index.js:1046`;`listSessions` `:1070` / `readSession` `:1079` / `filterSessions` `:1094` / `readTitleSnapshots` `:1127` / `readSurface` `:1166`)· `sessionReferenceResolver`(`dsh-session-reference/lib/index.js:440`;`prepare(agent, content, references, signal)` `:606`,与调用点逐字同形)· `agents`(`dsh-agent/lib/index.js:332`;`create` `:451` / `resume` `:464` / `get` `:594` / `list` `:612` / `roots` `:621`);晚挂服务同样在场(`settings` / `goals` / `webServer` / `connection` + `requestRejection` / `workspaceRegistry` / `systemPrompt` / `agentDefaultModel`);过滤描述符形状未变(`case "id": filter.values.includes(record.header.id)`,`dsh-session-query/lib/index.js:753`)。 - **真机读数(重启后,实调 —— 本节唯一一处非静态证据)**:DSH 以 0.2.0-rc.1 重启后,只读工具 `team_link_status` 成功返回整张状态卡(读数时点 **2026-09-29 15:01:24**):**首行**是三态之一的 `存储:文件 C:\Users\huangchaowen\.dsh\team-link\policy.json`,名册读回 `threat-intel` 的 3 个角色(coordinator / worker-a / worker-b,与 2026-09-28 01:04 的登记一致),形态段读出 `形态=sessions`,宿主 `agentTeams` 投影可读 ⇒ **工具面回归 = 修复生效**(bundle 被跳过时工具根本不在表里,调不到)。 - **未做(如实登记)**:未 commit、未留临时文件;**客户端半边(侧栏团队面板)未做真机 DOM 冒烟** —— 本次验到的是宿主工具面。 ### C 批代码评审修复轮:🟡#1/#2 + 🔵#3/#4/#5/#6(2026-09-27) > C 批交付**代码评审**(结论 **PASS、零 🔴**;6 条 = 2 🟡 + 4 🔵)的逐条收口。**只闭环这 6 条,不夹带**:改动面 = 2 个实现文件(`lib/index.js` / `lib/client.js`)+ 2 个套件(新增 **12** 条断言 —— host 6 / client 6)+ 文档(本文件 / [`docs/verification-log.md`](docs/verification-log.md) / [`docs/client-panel-batch-design-2026-09-27.md`](docs/client-panel-batch-design-2026-09-27.md) §9 / `README.md` 的三处读数 —— 见下「同批文档」)。账本见同档的「C 批代码评审修复轮」一节。 ### ✨ 改了什么 **🟡#1 面板的 13 个 `.dshsl-pn-*` 类此前一条 CSS 规则都没有**(正是本仓自己定性的「a class with no rule is appearance by luck, not by contract」,此前为 `.dshsl-st` / `.dshsl-st-label` 补过一次)。 实际后果不是外观洁癖:台账行与角色行是宿主渲染好的**多空格缩进**字节(`" " + row` 的台账行、`teamRoleLines` 的四空格角色行),落在没有 `white-space:pre-wrap` 的 `

` 上会被浏览器折叠成一句话 ⇒ 直接削弱 R1「一屏可读」。 **修法**:`lib/client.js` 样式数组末尾补一组 `.dshsl-pn-*` 规则(13 个类各有规则;`.dshsl-pn-line` / `.dshsl-pn-placement` 给 `white-space:pre-wrap`,徽标与段落间距照 `.dshsl-relay*` 的既有做法)+ **一条源码级对照判据**(`className:` 里出现的每一个 `dshsl-pn` 类都必须能在样式块里按选择器找到规则;两侧集合都要非空,防止「两边都空所以相等」)。徽标**只给一条规则、不按 `data-badge` 的值分色** —— 三态的区别由宿主给的标签文字承担,把宿主那一层的档位词汇写进 `lib/client.js` 会踩 W4 那条源码级代理锁(「客户端自己一个档位词都没有」)。 **🟡#2 「读更多」翻页在第二页起回卷。** 旧实现取 `firstUnread = shown.findIndex(!read.ids.has(id))` —— 第二页(`offset=12`)的第 0 行永远不在本页读窗里 ⇒ `nextOffset = 0` ⇒ 客户端唯一的翻页控件指回第一页(13–24 个会话两页循环;>24 个时第 25 行起在 UI 上不可达,客户端没有手输 offset 的口子)。 **修法**:`lib/index.js` 的 `sessionSegment.page.nextOffset` 改为「**当前页之后**」—— `windowEnd = 本页 offset + 本页读窗行数`,`windowEnd >= shown.length` 时置 **null**(客户端隐藏按钮)。列表工具段尾那句 `listReadTail` **一字未动**(它是提示不是载荷,且与工具面同源)。 **🔵#3 降级句在分页路径上不准。** `PANEL_LV_WINDOW_NOTE` 写死「本面板只读了前 12 行」,而 `offset=12` 那一页读的是 13–24 行 ⇒ 那句降级句与实况相反。 **修法**:新增 `panelWindowNote(offset)` —— 第一页逐字复用原常量(第一页字节一个都不动),分页那一页换成点名**本次这一页**的措辞(「本面板本次只读了 12 行,从第 13 行起」)。 **🔵#4 多团队时派生读数失去归属。** `tasksDerivedLines()` 的行原先被顺序拼进 `payload.tasksTail.derived`,**一个团队名都不带**(对照同一段 `- /tasks.md:…` 的三处头)⇒ 两支队伍的「最后主张 / 未消解存疑」在面板上分不出谁是谁。 **修法**:新增 `teamDerivedLines(teamName, derivedLines)` —— 块首带 `- /tasks.md:` 归属头、续行四空格缩进(与同段台账尾的排版同形);**行体逐字仍是 `tasksDerivedLines()` 的字节**,面板一个字节都不改写。原有 W6「逐字同形」判据因此**改紧**:剥掉面板自己那一层归属头之后,行体仍必须逐字出现在 `team_read` 的输出里。 **🔵#5 面板行直入 DOM,没过 `wellFormed()`。** 落点行与段行直接 `createElement`,绕过了本仓对**孤立代理对**的整条纪律(§8 字符串安全)—— 卡片、发送行、纯文本行都过,面板不该是新缺口。 **修法**:面板的**每一个宿主字符串出口**都过 `wellFormed()`:落点行 / 段行 / 档位结论 / 徽标标签 / 台账尾 / 派生行 / 会话行 / 段尾 / 缺段句 / 整页降级句(后两处会回显宿主的 reason / error 字段)。判据用**半对代理**(孤立高代理、孤立低代理各一处)打进八处字节,断言渲染出的**每一段**文本都不含孤立代理项。 **🔵#6 面板子树里缺 React `key`。** 评审点名 5 处(`placement` / `fail` / `loading` / `refresh` / `more`,`lib/client.js` as-of 修复前 `:2214` / `:2216` / `:2218` / `:2220` / `:2228`)。 **修法**:补齐这 5 处,并**同一缺陷类**余下的 5 处一并补齐(段标题 `h3`、缺段句、档位结论、徽标 `span`、面板根 `div` —— 它们同属数组子节点,React 对每一个都会报「unique key」)⇒ 判据写成「**整棵面板子树 key 完备**」,比点名的 5 处更紧。评审给的 5 处与同一缺陷类的余下 5 处都在交付报告里点名列清,不当作夹带。 ### 🧪 怎么验证 - **起点读数(实跑)**:`node host-half.test.mjs` → `ALL PASS` / `assertion total: 1300 (failed: 0)`(退出码 0);`node client-half.test.mjs` → `ALL PASS` / `assertion total: 299 (failed: 0)`(退出码 0)。 - **红相(判据先写、实现后到,实跑)**:`node host-half.test.mjs` → `5 FAILURE(S)` / `assertion total: 1306 (failed: 5)`(退出码 1)—— 红的是 🟡#2 翻页不回卷 · 🟡#2 ★ 负相末页 · 🔵#3 降级句随页走 · 🔵#4 派生行带团队归属 · 🔵#4 归属头只是加的那一层;`node client-half.test.mjs` → `4 FAILURE(S)` / `assertion total: 305 (failed: 4)`(退出码 1)—— 红的是 🟡#1 类名↔规则 · 🟡#1 `pre-wrap` · 🔵#6 key 完备 · 🔵#5 `wellFormed`。 **如实分解(不冒充红相)**:本轮 12 条新判据里 **9 条实测红**,另 **3 条是控制 / 对照项(★ 负相,本就绿)**:🔵#6 的 walker 控制项与 🔵#5 的「没有孤立代理项时逐字原样」对照项在修复前即绿;🟡#2 那条「沿**按钮链**走到第 25 行起」的判据是**实现之后补强**的(首跑红相里它还是旧版「三页读窗并集」形态、首跑即绿),**未取得红相读数,如实标注**。 - **绿相(实现后,实跑)**:`node host-half.test.mjs` → `ALL PASS` / `assertion total: 1306 (failed: 0)`(退出码 0);`node client-half.test.mjs` → `ALL PASS` / `assertion total: 305 (failed: 0)`(退出码 0)。 - **本轮的断言账**:host `1306 = 1300 + 6`;client `305 = 299 + 6`。 - **同批文档**:`docs/verification-log.md`(本轮一节:起点/红/绿读数 + 6 条闭环表 + 同批文档)· `docs/client-panel-batch-design-2026-09-27.md`(W8 行读数 + 新增 §9 本轮一节)· `README.md`(tests 徽章 / 稳定性格 / §十「当前读数」三处读数 `1300 → 1306`、`299 → 305`)· 本文件。 - **未做(如实登记)**:**未做真机 DOM 冒烟**(🔵#1 / 🔵#5 / 🔵#6 三条里,只有 🔵#1 的「类名 ↔ 规则」是源码级判据、🔵#5 / 🔵#6 判据走的是 stub 树;浏览器里的实际排版与 React「unique key」警告没有实测);未重启、未 commit、未留临时文件。 --- ### C 批分歧审计修复轮:🔵#2/#3/#4/#5(2026-09-27) > C 批分歧审计(只读,审计 **0 🔴** / 4 条 🔵)的逐条收口。**只收这 4 条,别处一字不动**:改动面 = **1 条新判据** + `agentStateLabel` 的「行为修正」标记与判据面(`__testing` 导出)+ 文档四处(计数 / 口径 / 回填 / 声称)。设计档 = [`docs/client-panel-batch-design-2026-09-27.md`](docs/client-panel-batch-design-2026-09-27.md);账本见 [`docs/verification-log.md`](docs/verification-log.md) 的「C 批分歧审计修复轮」一节。 ### ✨ 改了什么 **① 🔵#2 账本「计数与列表不一致」。** C 批小节写「**三处**段体渲染器收敛为一处」,紧随其后却列了**六个**(`agentStateLabel` / `sessionFaceLine` / `teamRoleLines` / `pendingLines` / `watchdogLines` / `tasksDerivedLines`)—— 代码侧改写点确为 **6 处**。标题与正文同一次编辑改成「**六处**」。 **② 🔵#3 「4 条 🔵」与「3 条 🔵」同批并存。** 设计档**五处**写「4 条」(`:1` 标题 / `:3` 一句话 / `:105` §5 标题 / `:116` §6 分期 / `:131` §7.1,as-of 修复前),README / CHANGELOG / 账本写「3 条(C3-a/b/c)」,而设计档 §5 正文自己说 ① 已在上一批修完。口径统一为「**登记 4 条 / 本批执行 3 条(① 已在上一批修完)**」:设计档(标题 / §1 一句话 / §5 标题 / §6 分期 / §7.1)、README(§十「当前读数」与设计索引行)、CHANGELOG(本标题与 C 批 ④ 的标题)与账本标题同改。 **③ 🔵#4 设计 §8 第 2 问仍是问句,而代码已作答。** 面板的台账尾恒取 `rows.slice(-STATUS_TASKS_TAIL_DEFAULT)`(as-of 本轮 `lib/index.js:11313`,常量在同档 `:140` = **5**),而面板路由只收 `session` / `offset` 两个参数(`:11519–11531`,**不收** `tasksTail`)⇒ **面板 = 状态卡的 5(同值)**。已回填进设计档 §8 第 2 问,并让「(已定)」这个标题与两问都已回填这一事实对齐(标题改为「**两问均已回填**」)。 **④ 🔵#5 「收敛后输出逐字不变」在 `agent === null` 一格不成立 —— 两件都做。** **a) 补 null 判据(先红后绿):** `host-half.test.mjs` 新增 **1** 条 —— 状态词对三种输入逐字(已定义 running / idle 与 `undefined` 保持原样,`null` = 「✕ 未运行」),且「**行为修正**」这句标记钉在 `agentStateLabel` **自己的源码块**里(否则它只是一句无法复核的散文)。为了让判据读**真函数**而不是抄一份复制品,`__testing` 开放 `agentStateLabel`(纯函数,模型可见面一个字未变)。 **b) 标为行为修正 + 把声称收窄:** `null` 一档**不是**静默漂移,是修正 —— 收敛前列表工具写 `running ? "▶ 运行中" : agent !== undefined ? "○ 空闲" : "✕ 未运行"`(`null` ⇒ 「○ 空闲」),状态卡那一份写 `agent === undefined ? … : agent.status === …`(`null` ⇒ 直接抛);收敛后两种「没有这个代理」统一判「✕ 未运行」。账本那句「输出逐字不变」收窄为「**对 `ctx.agents.get` 实际会产生的输入(已定义 / `undefined`)逐字不变**;`null` 一档由旧码的『○ 空闲』改判为更正确的『✕ 未运行』」,`lib/index.js` 的 `agentStateLabel()` 注释里同批写下这四个字。 **⑤ 顺带登记(本批不做):** 审计观察 ③(`tasksTail` 的两种「不可读」只降级到 lines 文本、`present` 仍 `true` 且无断言覆盖 —— 若将来给客户端结构化降级信号,那两支应改 `present: false`)记入设计档 **§8.1 后续项登记**;同处**新登记**一条本轮核实到的收敛残渣(列表工具里 `const running = …` 已是死局部,as-of 本轮 `:10826`,只登记不动手)。 ### 🧪 怎么验证 - **起点读数(实跑)**:`node host-half.test.mjs` → `assertion total: 1299 (failed: 0)`(退出码 0);`node client-half.test.mjs` → `assertion total: 299 (failed: 0)`(退出码 0)。 - **红相(判据先写、实现后到,实跑)**:`node host-half.test.mjs` → `1 FAILURE(S)` / `assertion total: 1300 (failed: 1)`(退出码 1),红的恰是 🔵#5 那一条。**如实分解(不冒充红相)**:该条的**行为半**在修复前**已经成立**(`agentStateLabel` 本就含 `null` 分支 ⇒ ★ 负相),真红的是另外两半 —— `typeof __testing.agentStateLabel` 为 `undefined`(**判据面**缺席)与源码块里没有「行为修正」**标记**;两半都在实现后转绿。 - **绿相(实现后,实跑)**:host `ALL PASS` / `1300 (failed: 0)`(退出码 0);client `ALL PASS` / `299 (failed: 0)`(退出码 0)。 - **同批文档**:`docs/verification-log.md`(C 批小节的计数与口径 + 🔵#5 的声称收窄 + 新增本轮一节)· `README.md`(tests 徽章 / 稳定性格 / §十「当前读数」三处读数 `1299 → 1300`、设计索引行口径)· 设计档(口径**五处** + W8 行的当前读数 + §8 回填 + §8.1 后续项登记)· 本文件。 - **未做(如实登记)**:`docs/policy-persistence-design-2026-09-27.md` 不在本批的可改文件清单内(父侧刚改完)—— 一字未动。 --- ### C 批:客户端团队面板(跨半边取数通道)+ 形态可视化 + 🔵 登记 4 条 / 本批执行 3 条(① 已在上一批修完)(2026-09-27) > 设计档 = [`docs/client-panel-batch-design-2026-09-27.md`](docs/client-panel-batch-design-2026-09-27.md)(评审 PASS)。本批**首次动客户端半边**:判据 W1–W9 + C3-a/b/c;实现 2 个文件(`lib/index.js` 新增只读路由与面板载荷、`lib/client.js` 新增面板分区),新增断言 **65 条**(host +35 / client +30),另**改写既有期望值 4 条**(host 3 条路由计数断言、client 1 条段落类序断言 —— 都是「新增了一条路由/一个分区」逼出来的,断言本身照旧严格),**文档 5 个文件**(见下「同批文档」)。账本见 [`docs/verification-log.md`](docs/verification-log.md) 末节。 ### ✨ 改了什么 **① 跨半边取数通道:`GET /team-link/panel`(只读,与导出路由同一道栅栏)。** 面板在浏览器半边、六段数据全在宿主半边 —— 此前 README §二 明确记着「本插件浏览器半边**没有任何跨半边取数通道**」(这正是状态点只做两态的原因)。通道其实一直在:`GET /team-link/export` 就是它。本批把**读路由**收成一对、**共用同一套门**:挂载期要 `webServer` + `connection` 成对在场(缺一即一条路由都不注册),请求期一律先问 `connection.requestRejection`(无 token ⇒ 401 / Host-Origin 不符 ⇒ 403,裁决原样写回),**仅 GET**(其余 405 + `allow: GET`),被拒时**零数据外泄**(连一次 surface 读都不发生)。 **② 面板分区(六段只读 + 落点行):宿主渲染、客户端只显示。** 面板是**既有侧栏「会话工具」入口**里的一个分区(**不新造 Modal、不新造入口** —— 宿主 Modal 有独占交互契约),排在会话列表之后、底部之前。响应 `{ placementLine, form, teams, pending, watchdogs, tasksTail, sessions }`,每段是 `{ present, lines, unavailable? }`:**落点行**取 `policy.placementLine()`(与 roster / status 两个读面**同一个函数**)、**台账派生读数**取 `tasksDerivedLines()`(与 `team_link_team_read` **同一份字节**)、**活性 verdict** 取 `livenessLine()` —— 「同源」由构造保证,客户端一个字段都不重算。**一次渲染 = 一次路由调用**(与工具面同一个「≤12 次面读」不变量),只在打开 / 手动刷新时拉,**不轮询**(无定时器)。**降级(R4)**:整条路由不可用 ⇒ 面板照常打开并显示「⚠ 面板数据不可用:<原因>」;单段取不到 ⇒ 该段 `present:false` + `unavailable.reason`。**零写(W9)**:读路径走状态卡那一套 + 纯派生扫描,**绝不**调用 `team_read` 的惰性清扫(那条会清 pending、删过期 provisional)—— 夹具里带着**已过期**的换届令牌与**已过期**的 provisional 配对,渲染前后 `policy.json` 字节与 mtime 均不变。 **③ 形态可视化(C2):三态徽标 + 段顶档位结论,投影读不到「不冒充」。** 每个会话格一枚徽标:`agent-team·Lead`(取自 settings 的 Lead 指针 —— 与宿主投影**无关**,投影缺席时这一格照给)· `agent-team·成员`(取自宿主 `agentTeams` 投影)· `多会话`(投影**可读**且两处都不是 —— 这是读数,不是默认值);投影**读不到**时给第四态 `档位未判定(成员名册读不到:<原因>)`,**绝不**写成「多会话」(把一个没读到的名册写成「不是成员」正是设计档禁止的编造)。「团队与角色」段顶部一句**档位结论**,措辞**复用** `TEAM_MODE_CONCLUSION`(「单会话兜底档」那句,同一个常量)。**不改**宿主会话列表 UI。 **④ 🔵 收尾:登记 4 条 / 本批执行 3 条(C3-a/b/c;① 已在上一批修完)。** **a) 快路径归档留痕条件化**:`adoptMemoryWindow` 的归档原本在 `update()` **之前**,于是「快路径 + fold failed」会先留下一句「随后文件会被改写成合并结果并盖 foldedAt」,而那一支的链尾**根本不回写** —— 承诺与实况相反,还多一份冗余归档。现在归档挪到**折叠成功之后**(`archiveMergedFile`):没有折叠落定就没有归档、也没有那句承诺;文件逐字节不变(无数据丢失)。归档本身永不抛(自己的 `try/catch`),所以挪位不会把一次已落定的折叠误报成 `fold failed`。**b)** 补「**快路径 + fold failed**」夹具(此前只覆盖晚挂路径):文件逐字节不变 + `foldedAt` 不重盖 + 归档数与留痕一致且都必须为 0。**c)** `host-half.test.mjs` 的 `setup()` 形参从一整行 1109 字符改为一行一个(纯排版,行为一字未动)。 ### 🧪 怎么验证 - **红相(实现前,实跑)**:`node host-half.test.mjs` → `23 FAILURE(S)` / `assertion total: 1290 (failed: 23)`;`node client-half.test.mjs` → `4 FAILURE(S)` / `assertion total: 275 (failed: 4)`(红的是四条命名守卫:面板面**尚不存在** ⇒ W2/W3/W5/W6/W7 的正相断言整块未执行,按 **★ 覆盖缺口** 如实标注、不冒充红相)。C2 阶段:host `1295 (failed: 4)`(四条徽标/不冒充判据);C3 阶段:host `1299 (failed: 3)`(C3-a/b/c 各一条)。 - **绿相(实现后,实跑)**:`node host-half.test.mjs` → `ALL PASS` / `assertion total: 1299 (failed: 0)`;`node client-half.test.mjs` → `ALL PASS` / `assertion total: 299 (failed: 0)`(基线 host 1264 / client 269 ⇒ 本批 +35 / +30)。 - **同批文档**:`docs/hardening-and-recovery-design-2026-09-21.md` §4.3.2 结构行 + 新增 **§4.3.8 团队面板分区**(入口口径的唯一事实源)· README §二 行为表新增「团队面板分区」行、`:584` 的「没有任何跨半边取数通道」**前提更正**、架构图补两条读路由与面板、设计索引 W1–W8 → **W1–W9**、徽章 / 稳定性行 / §十读数三处 · 设计档 §3.0.1 回填**实际响应形状** · 账本末节新增本轮条目。 - **未做(如实登记)**:`docs/policy-persistence-design-2026-09-27.md` §8 表里 ②③④ 仍写着「⏳ 后续项」—— 该档不在本批的可改文件清单内,**需要父侧同批改准**(否则状态行与事实不同步)。 --- ### 代码评审第 3 轮:`fold failed` 后链尾不回写 / `detach()` 同步置窗口判据(2026-09-27) > 逐项收口代码评审第 3 轮的 2 条 🟡 + 一条文档滞后(报告项):实现 2 处、判据 2 条(先红后绿)、文档 4 处。账本见 [`docs/verification-log.md`](docs/verification-log.md) 末节。 ### ✨ 改了什么 **① `fold failed` 之后链尾不再回写(🟡#1)。** `adoptMemoryWindow` 的 catch 支(`fold failed`)**不抛** ⇒ `.then` 链继续走到 `syncFileFromService`,其门在文件后端部署里全过 ⇒ 内存被整份替换成**服务视图**(折叠失败 ⇒ 服务里没有窗口数据)并写回文件 —— 窗口数据从内存与文件**两边同时消失**,且无归档、还盖新 `foldedAt`(与设计档 §3.4「宁可漏盖不冒盖」相反)。现在那一支**连回写都不做**:文件保留未并入的那一份,与折叠失败那一行留痕(「仍在落点里、本次未能并入」)逐字一致;`memoryDirty` 保持置位,下一次 attach 的折叠会重试;另留一行 info 说明回写被跳过(不静默)。**不用「先归档再覆盖」那条路** —— 归档救不回内存那份镜像。盖戳判据同批收紧为「**确已落定**的两个结局」(`folded into settings` / `not folded`)才为真。 **② `detach()` 同步置「未挂载窗口」判据(🟡#2)。** `detach()` 过去只复位 `attachWarned`;`unattachedWindowKind` / `enteredUnattachedWindow` 只在 activation 写一次。于是「provider 掉线触发 detach → 窗口内有写入 → provider 回来再 attach」这条链上,折叠留痕沿用 activation 那一次写下的措辞(对一个真实发生过的 detach 窗口是假陈述),归档判据 `!enteredUnattachedWindow` 读的也是旧值。现在 `detach()` 同步置 `unattachedWindowKind = "detached"` + `enteredUnattachedWindow = true`(判据与 warn 门**同生命周期**);折叠留痕的措辞随之从三支变四支,`detached` 支如实说「provider 被 detach 后又回来」—— 不是「服务从一开始就在场」的快路径措辞,也不是「服务还没来」的启动窗口;detach 窗口里本进程自己写进文件后端的镜像**不再多归档一份**(与 🔵#4 的 `refused` 窗口同一条裁定:只有真·快路径才存档)。 **③ 文档滞后(🟡#3,报告项)。** README 已写实第 2 轮(`1262 = 1255 + 7`),而设计档 §4 的红相账本与 §5 的 host-half 影响面止步于代码评审修复轮的 11 条 —— 本轮把两处补齐到第 2 轮,并同批写进第 3 轮。 ### 🧪 怎么验证 - **红相(修复前,实跑)**:`node host-half.test.mjs` → `2 FAILURE(S)` / `assertion total: 1264 (failed: 2)`(退出码 1),红的正是两条新判据(基线 `1262` ⇒ 新增 2 条;控制条件并入同条断言,无单独负相项)。 - **绿相(修复后,实跑)**:`node host-half.test.mjs` → `ALL PASS` / `assertion total: 1264 (failed: 0)`(退出码 0);`node client-half.test.mjs` → `ALL PASS` / `assertion total: 269 (failed: 0)`(退出码 0)。浏览器半边一字未改。 - **顺带修掉一处夹具竞速(本轮实测暴露、先于本轮存在,非插件缺陷)**:第 2 轮 h1 夹具(catch 支盖戳门)的文件轮询读与链尾回写的原子 `rename` 并发时,Windows 偶发把 rename 拒成 `EPERM` ⇒ 回写被拒、`savedAt` 永不刷新、两条断言假红(5 连跑中 2 次)。修法:轮询在 `policy.json.tmp` 在场(回写在飞)时不打开目标文件;修后 8 连跑全绿。 - **同批文档**:设计档 §3.4(新 bullet:`fold failed` 不回写 + 盖戳判据收紧)· §4(两条新判据行 + 红相账本补第 2/3 轮)· §5(host-half 影响面补第 2/3 轮,含本轮新加的 `settingsUpdateThrows` 夹具把手);README 徽章 / 稳定性行 / §十「当前读数」三处读数 `1262 → 1264`;账本末节新增本轮条目。 --- ### 代码评审第 2 轮:catch 支盖戳门 / 数组 policy 也判损坏 / 两因并存并陈(2026-09-27) > 逐项收口代码评审第 2 轮的 3 条(1 🟡 + 2 🔵):实现 3 处、文档 4 处。判据(4 红 + 3 ★ 负相)先于本轮写进 `host-half.test.mjs`(上一轮被 DSH 重启打断、未交付报告;本轮从工作区起点实测复核后**只补实现、不新增判据**)。账本见 [`docs/verification-log.md`](docs/verification-log.md) 末节。 ### ✨ 改了什么 **① attach 后置链 catch 支不再盖新 `foldedAt`(🟡#1)。** 链在折叠结果产出**之前**断掉时,catch 那一支读不到 `memoryOutcome`,**没有资格**判定「本次链上确有折叠/裁决落定」;旧实现那里是无参的 `syncFileFromService()`,而盖戳门 `options.foldHappened !== false` 对 `undefined` 取真 ⇒ 照样盖新戳,把「最近一次 attach 时刻」冒充「折叠落定时刻」。现在 catch 支显式传 `{ foldHappened: false }`:**宁可漏盖不冒盖** —— 回写照跑(合并结果照旧落盘),只是文件里那个戳逐字节保留上一次真正落定的值。 **② `policy: []` 也判损坏档(🔵#2)。** 旧的损坏判据 `typeof doc.policy !== "object"` 对**数组**不设防(`typeof [] === "object"`),于是 `{"schema":1,"policy":[]}` 被当成一份合法空档:`fileUsable` 置真、落点行翻成「存储:文件 …」,**下一次任何写入**就用原子 rename 把它整个替换掉 —— 正是 U11 损坏档守卫要防的形状。现在判据补上 `Array.isArray(doc.policy)`(数组不是命名空间镜像,`normalizePolicy` 会把它读成一份全默认的空视图),与「缺失 / null」走**同一条**置坏分支,不复制逻辑。 **③ 两因并存时落点行并陈(🔵#3)。** `policyPlacement()` 里 `serviceObserved`(`no-register`)过去**独占**原因行:「服务在场却拒注册 **且** 文件损坏」时,用户看不到「你的文件坏了、需要人工处理」这条**唯一的行动信号**。现在两因并存时并陈 —— `⚠ 存储:仅进程内存(重启即失)—— 原因:no-register;另:文件损坏(不覆盖;请人工处理)`:主因仍取 `no-register`,文件侧那一因以 `;另:` 附后;**四种单因形态逐字不变**(单一原因时没有「另:」)。roster 与 status 两个读面逐字同形;状态卡段①「来源」三态不受影响(只随落点 `kind` 走)。 ### 🧪 怎么验证 - **红相(修复前,实跑)**:`node host-half.test.mjs` → `4 FAILURE(S)` / `assertion total: 1262 (failed: 4)`(退出码 1),打在**本轮修复前**的实现上。红的 4 条 = 🟡#1 catch 支盖戳门 1 条 + 🔵#2 数组 policy 2 条 + 🔵#3 两因并存 1 条;另 3 条(🟡#1 / 🔵#2 / 🔵#3 各一条控制项)修复前即绿,按 **★ 负相** 如实标注 —— 分别钉住「夹具真的走了链的 catch 支」「数组档真的原样在盘上」「并陈只发生在两因并存时(四种单因逐字不变)」。 - **绿相(修复后,实跑)**:`node host-half.test.mjs` → `ALL PASS` / `assertion total: 1262 (failed: 0)`(退出码 0);`node client-half.test.mjs` → `ALL PASS` / `assertion total: 269 (failed: 0)`(退出码 0)。判据上一轮已就位(`1262 = 1255 + 7`),本轮**未新增断言**、浏览器半边一字未改。 - **同批文档**:设计档 §3.4(损坏判据写明**含数组**;catch 支「宁可漏盖不冒盖」例外)· §3.5(两因并存的并陈形态与优先序)· §5 行号漂移两处(`:1181`→`:1182`、`:1198`→`:1199`);README 徽章 / 稳定性行 / §十「当前读数」三处读数 → `1262`;账本末节新增本轮条目。 --- ### 代码评审修复轮:损坏档守卫 / 写失败 warn 按段复位 / `foldedAt` 盖戳门 / `refused` 窗口(2026-09-27) > 逐项收口代码评审列出的 6 条(2 🟡 + 4 🔵):代码侧 4 处,夹具整洁 2 处,文档 4 处。账本见 [`docs/verification-log.md`](docs/verification-log.md) 末节。 ### ✨ 改了什么 **① 损坏的 `policy.json` 不再被覆盖(🟡#1)。** 此前 `loadPolicyFile()` 解析失败时只留了一行 warn 说「本次不会覆盖该文件」,而 `persistPolicyFile()` **毫无守卫** —— 文件后端下**下一次任何写入**都会用原子 rename 把它整个替换掉,落点行还会从「⚠ 仅进程内存」翻成「存储:文件」(`policyPlacement` 只看 `fileUsable`)。现在载入判定为损坏(读不出 / 非法 JSON / 没有 `policy` 对象)就置 `fileCorrupted`:此后**每一次写**都直接返回并**如实留痕**(不静默,也不吞掉第二次以后),落点行报**第 5 个原因码** `文件损坏(不覆盖;请人工处理)`。**人工处置是唯一出口**:修好或移走该文件后重启(每个 store 只读一次文件)。 **② 写失败的 warn 改成按段复位(🟡#2)。** `fileWriteWarned` 过去是**进程级一次性**门:第一段失败 warn 过之后,再下一次失败**完全静默**(与 `attachWarned` 按窗口复位的既有范式不一致)。现在由写入的**成功分支**复位 ⇒ 一段失败 = 一行,成功后新的一段失败再报一行;warn 文案里「此行不再重复」也改准成「**本段内**此行不再重复(写入一旦成功,本段告警复位,再失败会再报一行)」。 **③ `foldedAt` 只由折叠落定盖戳(🔵#3)。** `syncFileFromService()` 过去**无条件**盖新戳,于是「最近一次 attach 时刻」冒充「折叠落定时刻」。现在盖戳门挂在「**本次链上确有折叠/裁决落定**」上(判据取折叠那一步的结局,`: none (no writes while unattached)` 除外);**回写本身不受影响**(合并结果照旧写回文件,U3/U4/U8/U10 逐字不动),只是没有折叠的那一段保留上一次真正落定的戳。 **④ `refused` 是一个真实的未挂载窗口(🔵#4)。** 判据从 `activationReason !== "attached" && activationReason !== "refused"` 收成 `!== "attached"` —— 「服务在场但**拒绝注册**、后来才挂上」那段时间里的写入**同样由文件后端支撑、同样要折叠并入**。折叠那一行的措辞随之分三支,`refused` 如实说「服务在场但拒绝注册」、**不再**说成「服务从一开始就在场」;由此也不再给本进程自己写的镜像多归档一份 `policy.superseded-*.json`。 **⑤⑤ 夹具整洁两处(🔵#5 / 🔵#6)。** 种子守卫由 `policyHome !== null` 改判 `policyDir !== null`(`policyHome: false` 那一支旧守卫为**真**,`path.join(null, …)` 会抛 TypeError);另去掉 `host-half.test.mjs` 里一个对同步 `readdirSync` 的无效果 `await`。顺带给夹具加两个把手:`policySeed` 收**原文**字符串(U11 的坏 JSON 要在 `apply` 之前落盘)、settings stub 的 `stopRefusing()`(🔵#4 的「拒注册 → 后来才挂上」)。 ### 🧪 怎么验证 - **红相(修复前,实跑)**:`node host-half.test.mjs` → `8 FAILURE(S)` / `assertion total: 1255 (failed: 8)`(退出码 1),打在**本轮修复前**的实现上。红的 8 条 = U11 四条 + U12 一条 + 🔵#3 一条 + 🔵#4 两条;另 3 条(U12 控制项、🔵#3 对照项、🔵#4 对照项)修复前即绿,按 **★ 负相** 如实标注 —— 它们分别钉住「那两次写真的失败了」「那个夹具真的落在没有折叠的那一支」「那个夹具真的挂上并折叠了」,作用是让上面那几条红**不是空转的绿**。 - **绿相(修复后,实跑)**:`node host-half.test.mjs` → `ALL PASS / assertion total: 1255 (failed: 0)`(退出码 0);`node client-half.test.mjs` → `ALL PASS / assertion total: 269 (failed: 0)`(退出码 0)。基线 `1244` / `269` ⇒ **新增 11 条宿主断言**(`1255 = 1244 + 11`);**浏览器半边一字未改**。 --- ### 分歧审计修复轮 `DIVERGENCE(10)`:快路径折叠 + 状态卡「来源」跟随落点(2026-09-27) > 逐项收口父侧审计列出的 10 条;代码侧只动 2 条(🟡#5 · 🔵#9),其余 8 条是文档层。账本见 [`docs/verification-log.md`](docs/verification-log.md) 末节。 ### ✨ 改了什么 **① settings 从一开始就在场时,文件里那一份也会并入(唯一的语义改动)。** 此前 `loadPolicyFile()` 只有一个调用点(`if (scope === null)`)⇒ 设置服务在 activation 就在场时**文件从不被读**:纯设置部署里上一次跑写进 `policy.json` 的团队,在下一次启动里**不可见**。现在:命名空间**还是默认**时,文件里那一份照样折叠并入(并入**前**原样归档成 `policy.superseded-.json`,合并结果回写文件并盖 `foldedAt`);命名空间**非默认**时仍按既有裁定**设置胜、文件连读都不读**(U6 那条断言逐字保留)。晚挂路径(服务后来才挂上)的折叠行为**一字未动**。 **② 状态卡里团队清单那一段的「来源」跟随实际落点。** 此前写死成「来源:设置 team-link 的 teams 键」,文件 / 内存落点下会说谎;现在三态各说一句: `来源:设置 team-link 的 teams 键` / `来源:文件 <绝对路径> 的 teams 键` / `来源:进程内存的 teams 键(重启即失)`。首行「存储:…」那行本来就是真的,一处真一处假是最难发现的形状。 ### 💡 为什么 ① 是同一类缺陷的另一半:文件后端解决了「服务没挂上时团队活在一个进程里」,却留下了「服务一直在场时文件后端写的团队读不到」—— 用户侧的现象仍是「我建的团队不见了」。② 违反 R2 红线的可核性:落点行说真话、来源行说假话。 ### 🧪 怎么验证(实跑原文) - **绿相**:`node host-half.test.mjs` → `ALL PASS / assertion total: 1244 (failed: 0)`(退出码 0);`node client-half.test.mjs` → `ALL PASS / assertion total: 269 (failed: 0)`(退出码 0)。基线 `1237` / `269` ⇒ **新增 7 条宿主断言**(`1244 = 1237 + 7`),浏览器半边一字未改。 - **红相**:`node host-half.test.mjs` → `6 FAILURE(S)` / `assertion total: 1244 (failed: 6)`(退出码 1),打在**本轮修复前**的实现上。红的 6 条 = U10 五条里的四条 + U5 增补两条;第 7 条(折叠后落点是设置服务)修复前即绿,按 **★ 负相** 如实标注 —— 它断言的是折叠**之后**的落点,而缺陷是折叠压根没发生。 - **新增断言名**:`U10 快路径折叠:` ×5、`U5 落点行: 状态卡段①的「来源」` ×2。 --- ### 策略持久化自持化:落点可核 + settings 缺席时不再只活在一个进程里(2026-09-27) > 设计档:[`docs/policy-persistence-design-2026-09-27.md`](docs/policy-persistence-design-2026-09-27.md)(评审 PASS)。证据账本:[`docs/verification-log.md`](docs/verification-log.md) 的「策略持久化自持化」一节。 ### ✨ 改了什么 **① settings 不在场时,状态不再只活在一个进程里。** 此前 `settings` 服务没挂上时,团队注册表与信任数据只存在于**进程内存**(DSH 重启即失,`README.md` 第一张对照表里那一行「DSH 重启会清掉本插件的团队注册表」就是这个后果)。现在这一档落到**插件自己的文件** `/team-link/policy.json`:路径优先取 `DSH_HOME`(实测 `C:\Users\magic\.dsh`),缺失才退回 `os.homedir()/.dsh`;两者都拿不到才退回进程内存 + **一行**告警。文件落**全局目录**而非工作区 —— 团队是按名字**全局**寻址的,落工作区会造成「换个工作区团队就没了」。 **② 文档形状与写入语义。** `{ "schema": 1, "savedAt": "…", "foldedAt": null, "policy": { …与 settings 命名空间同键集… } }`:`policy` 与命名空间**同键**(8 个键,无损 —— 迁移型改动必须一次写全);写入原子(`policy.json.tmp` → `fsync` → 覆盖,目录先建),**写失败保留内存态并告警、下次写入重试**。 **③ 落点可核(新红线)。** `team_link_roster action=get` 与 `team_link_status` 的**首行**现在永远说得出状态此刻存在哪,三态之一:`存储:设置服务(team-link 命名空间)` / `存储:文件 <绝对路径>` / `⚠ 存储:仅进程内存(重启即失)—— 原因:`。 **④ settings 之后再挂上时会折叠。** 文件里的内容并入 settings(服务成为落点),**并立刻把合并结果写回文件、盖上 `foldedAt`** —— 服务下一次缺席(重启)读到的就是合并后的真值,不会表现为「折叠后新建的团队消失」。两边都有数据时按既有 legacy 迁移规则裁决:**服务胜**,文件那一份原样归档为 `policy.superseded-<时间戳>.json` **并留痕**,不做静默合并。回写刻意挂在 attach 后置链的**末尾**(旧命名空间迁移之后):写早了,文件会缺迁移进来的那一份信任数据。 **⑤ 一个时刻只写一处。** settings 在场时**不建、不写**那个文件;读**有条件**:命名空间还是默认时读进来折叠并入(见上方「分歧审计修复轮 `DIVERGENCE(10)`」那节的 ①),命名空间已有数据时**连读都不读** —— 读了就会把文件那侧并进来,与 §3.3「服务胜」的裁定冲突。 ### 💡 为什么 「状态存在哪」此前只能在**源码**里看见:用户看到的是「重启后团队没了」,而插件只留一行 `settings not active…` 的告警。文件后端把那一档从「活在一个进程里」变成「活在一个可读可备份的文件里」,首行落点把「它到底在哪儿」从推断变成**可核**。 ### 🚫 明确不做 **不双写**(服务与文件各写一份);**代码不读宿主配置文件**(`/cordis.patch.yml` 一次都不碰,迁移由父侧手工做一次,步骤见 `AGENTS.md` §七);**不做文件锁**(`policy.json` 是全局单文件、插件跨进程,两进程同写可能丢更新 —— 缓解是原子写 + 每次「读-改-写整份」,冲突窗口很小,且折叠/迁移路径都有留痕);**不改 `session-link-pro` → `team-link` 的迁移**(一行未动)。 ### 🧪 怎么验证 - **命令**:`node host-half.test.mjs`(宿主套件)/`node client-half.test.mjs`(客户端套件)。 - **绿相(实跑原文)**:`node host-half.test.mjs` → `ALL PASS / assertion total: 1237 (failed: 0)`(退出码 0);`node client-half.test.mjs` → `ALL PASS / assertion total: 269 (failed: 0)`(退出码 0)。基线 `1203` / `269` ⇒ **新增 34 条宿主断言**(`1237 = 1203 + 34`);**浏览器半边本轮一字未改**。 - **红相(文件后端档,测试先行)**:`7 FAILURE(S)` / `assertion total: 1214 (failed: 7)`(退出码 1)= 6 条新判据 + 1 条 import 白名单锁;**另 4 条覆盖缺口类 ★ 负相**(纯解析器与恒真路径断言,两种实现上都绿,如实标负相不冒充红相)。 - **折叠/可核/迁移档的 23 条**:问题先于实现写出,但**红相改用变异验证** —— 8 次**单点**变异(写文件时不看 scope / 折叠后不回写 / 归档跳过 / 落点行说谎 / 载入不置可折叠位 / 链尾回写跳过 / 迁移次序颠倒 / 永不读文件)逐条证明这些判据**真会变红**,变异后源码按 sha256 校验**逐字节还原**。 - **一次性与真机可核**:无服务时的写入 → 重启(新进程读同一文件)→ 读回一致;迁移可核 = 导入后 `roster get` **看得见**迁移过来的团队 **且**首行 = `存储:文件 <绝对路径>`(`U9 迁移可核:` 三条断言即该路径的夹具等价物;真机执行步骤见 `AGENTS.md` §七,由父侧做并把两条读数抄进账本)。 ### 部署接线改为 `link:`(2026-09-27) ### ✨ 改了什么 desktop profile 的 `dsh-team-link` 从 pnpm `file:`(整包**拷贝**进 `.pnpm/`,改完代码必须先刷新拷贝再重启)改为 **`link:`**: `/node_modules/dsh-team-link` 成为**指向本仓目录**的 junction,**改完代码只需重启**。 0.4.0 引入的过渡脚本 `deploy-desktop.ps1` 随本条**退役删除**(它解决的是 `file:` 接线自带的拷贝问题)。 ### ✅ 怎么验证 - 切换后经 junction 读 `lib/index.js`:sha256 与本仓**逐字节一致**(`A78C463D…`)。 - 改动前拍手动配置快照 `20260927-111821-07ef`,并另存 `package.json.bak-20260927-pre-link`;旧 `.pnpm` 拷贝**保留未删**(回滚=改回 `file:` 后重跑 `pnpm install`)。 - 安全性核对:三个运行时依赖中 `schemastery` 两侧均为 3.18.0;`@deepseek-ai/dsh-tools` / `dsh-session-reference` 在 desktop profile 中**不存在**(由宿主 asar 提供)⇒ link 不引入版本错配。lockfile 中本就没有 `dsh-team-link` 条目 ⇒ 无「lockfile 与声明不一致」风险。 ## 0.4.0 — 2026-09-27(发布收口:团队任务台账 + 可观测批(A)+ 形态批(B)+ 三轮修复轮) > 本版一次性收口下列 **8 个「未发布」条目**(开发期累积、按时间倒序);package.json 已 bump 到 **0.4.0**。上一版为 **0.3.10(2026-09-22)**。 ### 真机冒烟修复轮(L1 时基 / L2 镜像)(2026-09-27) > 这一轮修的是**真机冒烟**抓到的两条:夹具与审计都抓不到,它们**只在真实运行环境里才看得见**。**不夹带新功能、不扩范围**:A 批设计 §7 与 B 档 §7「明确不做」继续有效;`docs/observability-batch-design-2026-09-26.md` 本轮一字未动;**不碰部署面**;**不动 `\team\smoke-test\`**(真机冒烟的数据,留着当证据)。逐条读数与红/负相拆分见 [`docs/verification-log.md`](docs/verification-log.md) 的同名一节。 ### ✨ 修了什么 **① L1 —— 同屏混用两个时基且未标注**(真机实测:派生视图写 `最后主张 done(2026-09-27 01:45 · session-…)`,而**同一屏**末尾写 `(读数 2026-09-27 09:45:31,>2min 作废)` —— 差 8 小时;原始行是 `2026-09-27T01:45:31.010Z`)。根因:`taskStamp(iso)` 把 ISO 串**切前 16 位**(UTC),`readStamp(now)` 走 `localStamp(new Date(...))`(本地)。处置(父侧裁定:**渲染出来的时间统一到本地时基**):派生视图的时间戳改走**同一套本地格式化**(`localStamp(date, withSeconds = false)`,分钟精度不变),时间的**瞬间**由 `new Date()` 解析而不再做字符串切分;**原始行**仍逐字保留 `toISOString()` 的 UTC 值(事实源不动)。时间列解析不出日期的手写行**单行化后回显该列**(`preview()` 折叠内部连续空白并 trim)—— 原先只切串、换成解析后必须显式兜住这一支,否则会打印出 `NaN-NaN-NaN NaN:NaN`(既不编造时间,也不吐「不是值的值」)。`taskStamp` 的调用点只有派生视图那一处(已核)。 **② L2 —— 镜像在「新建团队」这条路上渲染 `undefined`**(真机实测:`team_link_roster action=upsert-team` 新建的团队,其 `roster.md` 里两行是 `- 形态(mode):undefined(闭集 sessions / agent-team;默认 sessions)` 与 `- Lead 会话(leadSessionId):undefined`)。根因(本轮核实并如实报告):镜像渲染器读的是**未归一化**的团队对象,而 `applyTeamUpsert` **新建**时构造的行**没把这两个新字段写进记录** —— 既有团队走归一化读面(`normalizeTeams` 每次读都补),所以旧团队看不出这个问题;`/team rotate` 的批量建队走同一个函数,属同一病灶。处置:① 渲染器**永不打印 `undefined`**(两字段按 `normalizeTeams` 的**同一套缺字段语义** —— 空串 / 非字符串 = 字段缺席 —— 加闭集规则归一化);② `upsert-team` **显式写入**两个默认值(schema 的默认值只有**读面**会补,而镜像读的是**写进去**的那一行);③ 断言补住**新建路径的镜像**与**两个读面(roster get 形态段 / 状态卡 ⑦ 段)的零 `undefined` 泄漏**(后者如实标 ★ 负相:它们本就读归一化行)。 ### 💡 为什么 两条的共性是**同一份事实被两种方式算了两遍**:L1 里「同一个瞬间」被渲染器切串(UTC)、被同一屏的读数行本地格式化 —— 于是同屏两处可以相差一个时区;L2 里「字段缺席」在**写路径**没归一化、在**读路径**归一化 —— 于是同一个团队在 settings / 读面 / 镜像三处可以长得不一样。处置都收在同一个方向:**一处算、一处用**(L1 同出一套本地格式化;L2 写路径也写全 + 渲染器自带兜底),并且**渲染面永不吐出 `undefined` / `NaN` 这类「不是值的值」**。 ### 🚫 明确不做(本轮) 不加新工具 / 新参数 / 新事件类型 / 新持久状态;不改投递层与黑板三件套的语义;渲染面**只改时基与 `undefined` 两处**,不重排任何既有文案;不动 `docs/observability-batch-design-2026-09-26.md`;**不碰部署面**(`profile/profiles/web/...` 与用户目录里的插件副本);**不动 `\team\smoke-test\`**。相邻发现的一条(`renderRosterMirror` 对**缺 `rotationBackup` 键**的裸对象会取 `undefined.at` 抛错)**本轮不改、仅留档**:该形状在所有调用点都不可达,且不属 L1/L2 的判据面。 ### 🧪 怎么验证 - **命令**:`node host-half.test.mjs && node client-half.test.mjs`; - **绿相(实跑两行原文)**:`node host-half.test.mjs` → `ALL PASS / assertion total: 1203 (failed: 0)`(退出码 0);`node client-half.test.mjs` → `ALL PASS / assertion total: 269 (failed: 0)`(退出码 0)。基线 `1193` / `269` ⇒ **新增 10 条宿主断言**(`1203 = 1193 + 10`)+ **改写 10 条**;**浏览器半边本轮一字未改**(仍 `269`); - **红相(本轮修复前的实现 + 本轮的新断言/改写断言;测试先行)**:`assertion total: 1203 (failed: 17)`(`17 FAILURE(S)`,`nodeExit=1`)—— **17 条真红** = **7 条新断言**(L1 三条 + L2 四条)+ **10 条改写的期望值**(9 条 `§9 U12/U13/U15/D5/D6` 的 `stampOfRow` 期望 + TR 夹具那条把 UTC 截断值写死的 `U12 零回归`); - **★ 负相 3 条**(两种实现上都绿,**不冒充红相**):`L1 坏时间戳原样回显` · `L1 原始行逐字 UTC ISO` · `L2 两个读面零 undefined 泄漏`(覆盖缺口类 / 反向护栏); - **夹具不再自证**:`stampOfRow` 从「切行里的 ISO 串」改为「把那个瞬间按**本地墙钟**独立算出来」⇒ 任何时基改动都会当场变红;判据的**机器无关性**见验证档(前 16 位相同的两行、相差 5 小时的两个瞬间必须渲染不同)。 ### 🔁 同批同步的面 `lib/index.js`(`localStamp` / `taskStamp` / `applyTeamUpsert` / `renderRosterMirror`);`host-half.test.mjs`(10 条新断言 + 10 条改写的期望值);`README.md`(三处读数 `1193 → 1203` · §四**时间基**一条 · 团队形态表的**镜像**行 · 实现约定里的时间戳条目);`docs/team-mode-batch-design-2026-09-26.md`(状态行读数 · §4.1 镜像范围);`docs/team-ledger-and-mode-design-2026-09-26.md`(§9.3.3 时间基 · 状态段与 §7 步 3 读数);本条目;`docs/verification-log.md`(本轮一节)。 --- ### 部署脚本与真机冒烟(2026-09-27) ### ✨ 新增 **部署脚本 `deploy-desktop.ps1`** —— 解决「改完代码重启却不生效」这个静默坑:profile 用 pnpm `file:` 依赖装本插件时,pnpm 会把整包**拷贝**进 `\node_modules\.pnpm\dsh-team-link@file+...\node_modules\dsh-team-link`,而 DSH 加载的是那份拷贝。脚本四步:从本仓库(本地路径,不依赖网络)fast-forward 部署克隆 → 覆盖 `lib/` + `package.json` + `cordis.patch.yml` 进 pnpm 拷贝 → 校验两边 `lib/index.js` sha256 一致 → 提示重启;`-DryRun` 只打印、不落盘、不改 git 状态。 ### ✅ 怎么验证 - 两个壳各跑一遍:`powershell -File deploy-desktop.ps1 -DryRun`(exit 0,四文件报告 `already in sync`,两侧 sha256 打印一致);坏路径参数 `-Clone C:\nope` ⇒ 明确报错 + exit 1(fail-fast)。 - 干跑前后:仓库 `git status` 与部署克隆 HEAD **未变**(零副作用)。 ### 📌 同批记录(真机冒烟的两条发现 —— 修复见上面「真机冒烟修复轮(L1 时基 / L2 镜像)」一节) 在**运行中的壳**里对台账做写入冒烟(建临时团队 → `plan/claim/done/dispute` 四种行 → 读回派生视图)时,抓到两条**夹具测不出**的问题:① 派生视图的时间戳是 UTC(切 ISO 串)而同屏「读数」是本地时间,**同屏混用两个时基且未标注**(真机差 8 小时);② `roster.md` 镜像在 `upsert-team` 新建那条路上把 `形态(mode)` / `Lead 会话(leadSessionId)` 渲染成 `undefined`。两条已另立修复条目(见上面「真机冒烟修复轮(L1 时基 / L2 镜像)」一节:L1 渲染统一到本地时基、L2 镜像与新建路径去 `undefined`)。 --- ### B 批收尾修复轮(代码评审 R1–R6)(2026-09-27) > 形态批(B 批)与分歧审计修复轮 `DIVERGENCE(8)` 之后的**收尾修复轮**:只取代码评审 7 条里的 6 条(R1–R6),**R7 仅留档、代码与夹具一字未动**(父侧裁定:接受顺序耦合,理由见验证档的「已知可接受项」)。**不夹带新功能、不扩范围**:A 批设计 §7 与 B 档 §7「明确不做」继续有效;**不动 `docs/observability-batch-design-2026-09-26.md`**;**不碰部署面**。逐条读数与红/负相拆分见 [`docs/verification-log.md`](docs/verification-log.md) 的「B 批收尾修复轮(R1–R6)」一节。 ### ✨ 修了什么 **① R1 —— 诊断话术仍是断言式归因(D1 同类漏改的一处)** —— `probeAgentTeams` 的 service-absent 分支把原因写成因果断言「宿主没有提供 agentTeams 服务(**本次运行的 profile** 没有装载 …组合包)」。而 cordis 的 `ctx.get` 只返回**已激活**的 provider:一个尚未完成 `[Service.init]` 的 `agentTeams` 也会走到这一支 ⇒ 「profile 没装载」是**假阴性断言**(正是 D1 要消灭的那类错误归因)。现在该 reason 软化为「服务 `agentTeams` 当前不可见(可能未装载 …组合包,也可能尚未激活完成 —— 可稍后重试,或核对当前活动 profile 的 `dsh.profile.bundles`)」:写路径的拒绝照旧刺眼(拒绝 + 指路 + 零写入),**但不替宿主下结论**。引用该文案的既有断言(`D2 首行按分支准确化(反向锁)`)同批改写。 **② R2 —— 同一判据被探测两次** —— `set-mode` 的可用性门与确认框正文的投影各调一次 `probeAgentTeams`。现在「切到 agent-team」那一向的探测结果**直接复用**为确认框正文的投影(`probe ?? probeAgentTeams(...)`);只有「切回 sessions」那一向没有前置探测,才现读一次。两条路的处置仍各自决定(可用性门 vs. 正文那一行),判据仍只此一处。 **③ R3 —— TOCTOU 复检后的不对称(账与镜像可能分家)** —— 写时复检之后,镜像用**复检后**的行,而形态史那行还用**对话框前**的 `team`:用户在确认框打开期间经设置 UI 改了 workspace 时,镜像写新路径、账写旧路径。现在两者**同源**(同一个 `teamWritten = nextTeams.find(...)` 行)。 **④ R4 —— 幂等放宽的语义噪音(文档侧 + 返回体措辞同批)** —— 从未切换过的团队第一次收到 `set-mode mode=sessions`(其默认档)会**补记一条「形态 → sessions」**:为一次从未发生的切换造一行史(属设计裁定的取舍,**行为不动**)。文档与返回体一律改准为:**首次声明也算一行** —— 形态史记的是「**声明过这一档**」,不是「档位发生变化」。旧措辞「形态史记的是「切换」,不是「重复声明」」在 B 档 / README / `lib/index.js` 三处**零残留**。 **⑤ R5 —— 无会话身份时诊断行会「猜」** —— `otherReachableSessions` 在调用方无会话身份时退化为按 `process.cwd()` 过滤 ⇒ 可能触发**基于猜测**的「⚠ 多会话通道看起来不可用」,与同文件「读不到 ⇒ `null` ⇒ 不判、不猜」的纪律不一致。现在 `selfId === undefined` ⇒ **返回 `null`(不判)**,与 `sessionIdSnapshot` 的失败语义对齐;状态卡 ⑦ 段(原先直接传 `pool`)随之收敛到同一个判据。 **⑥ R6 —— 状态行措辞过宽** —— B 档状态行写「**D8 是本轮唯一的行为改动**」,但 D2(读面首行按分支取)与 D3(镜像新增两行 + `set-mode` 落笔后重写镜像)都改了**可观测输出**。同批收窄为「唯一的**门 / 写路径语义**改动(D2 / D3 只改读面文案与镜像渲染)」,B 档 §4.2 与上一条 `DIVERGENCE(8)` 条目里的同句声明一起改准。 ### 💡 为什么 R1 / R5 是同一个病:**把观测到的现象说成了因果结论**(R1:「服务看不见」⇒ 断言「profile 没装载」;R5:没有身份 ⇒ 拿 `process.cwd()` 过滤出来的空列表断言「多会话通道不可用」)。本仓的家规是「**读不到 ⇒ null ⇒ 不判、不猜**」,所以两处都改成只报现象、只在不猜的前提下下判据。R2 / R3 是同一个形状的另一面:**同一份事实被取了两次**(R2 探测、R3 行来源),于是两次取到的东西可以不一样 —— 一次取、一处用,分家就不可能。R4 / R6 是文档与实现的自相矛盾(R4:文档说「切换才记一行」而实现连首次声明也补记;R6:说「唯一的行为改动」而同轮还有两处可观测输出变更)。 ### 🚫 明确不做(本轮) **R7(测试夹具顺序耦合)不接受修复** —— 父侧裁定「可接受的顺序耦合(有注释、teardown 已清理)」,本轮只在验证档留一条「已知可接受项」并给出将来的做法(重排用例时给各用例独立 workspace;D8 的 `nbWs` / `fbWs` 已是该做法),**代码与夹具一字未动**。其余同 `DIVERGENCE(8)`:不加新工具 / 新参数 / 新事件类型 / 新持久状态;不动投递层与黑板三件套的语义;**不动 `docs/observability-batch-design-2026-09-26.md`**;**不碰部署面**(`profile/profiles/web/...` 与用户目录里的插件副本)。 ### 🧪 怎么验证 - **命令**:`node host-half.test.mjs && node client-half.test.mjs`; - **绿相(实跑两行原文)**:`node host-half.test.mjs` → `ALL PASS / assertion total: 1193 (failed: 0)`(退出码 0);`node client-half.test.mjs` → `ALL PASS / assertion total: 269 (failed: 0)`(退出码 0)。基线 `1184` / `269` ⇒ **新增 9 条宿主断言**(`1193 = 1184 + 9`)+ **改写 1 条**;**浏览器半边本轮一字未改**(仍 `269` —— 本轮只动宿主半边); - **红相(本轮修复前的实现 + 本轮的新断言/改写断言;测试先行)**:`assertion total: 1193 (failed: 8)`(`nodeExit=1`)—— **8 条真红**(7 条新断言 + 1 条改写的反向锁);另 **2 条新断言在两种实现上都绿**,如实标 **★ 负相**(`R2 ★ 负相(切回方向)` = 覆盖缺口类;`R5 反面对照` = 反向锁),**不冒充红相**; - **回归**:B 批 41 条、`DIVERGENCE(8)` 16 条与 A 批 / 台账批的既有断言**零回归**(本轮唯一改动的既有断言就是 `D2 首行按分支准确化(反向锁)` 的措辞)。 ### 🔁 同批同步的面 `lib/index.js`(`probeAgentTeams` 的 reason · `set-mode` 的探测复用与 `teamWritten` 同源 · 两处形态史措辞 · `otherReachableSessions` 的 `selfId` 早返回 · 状态卡 ⑦ 段的判据);`host-half.test.mjs`(9 条新断言 + 1 条改写;新夹具各自一份 workspace);`README.md`(三处读数 `1184 → 1193` · 幂等行 · 宿主探测 / 落笔 / 形态诊断行三行的口径);本条目;`docs/verification-log.md`(本轮一节);`docs/team-mode-batch-design-2026-09-26.md`(状态行 · §4.2 幂等口径);`docs/team-ledger-and-mode-design-2026-09-26.md`(§7 步 3 行的本轮交付说明)。 --- ### 分歧审计修复轮 `DIVERGENCE(8)`(2026-09-27) > 只读分歧审计的 **8 条**结论(D1–D8)逐条闭环。**不夹带新功能、不扩范围**:A 批设计 §7 与 B 档 §7「明确不做」继续有效。逐条验证证据见 [`docs/verification-log.md`](docs/verification-log.md) 的「分歧审计修复轮 `DIVERGENCE(8)`」一节。 ### ✨ 修了什么 **① D1 —— 文档把真机读反了(实证):文案通用化 + 补设置 UI 兜底路** —— 旧文案断言「**本部署未装载** `@deepseek-ai/dsh-experimental-agent-team-profile` ⇒ `ctx.get("agentTeams")` 为 undefined ⇒ 切档被拒」。实测事实是:本部署跑的是 **desktop profile**(`DSH_PROFILE=desktop`),其 `dsh.profile.bundles` **含**该组合包;本会话系统提示词里同时有工具包 POLICY 常量与 `spawn_teammate` 工具声明(该工具只由 `@deepseek-ai/dsh-experimental-agent-team-tool-agent-team` 注册,其 `inject` 含 `agentTeams`)⇒ **服务已在运行实例中挂载** ⇒ 探针 `available:true` ⇒ **U11b 的拒绝路径在本部署不会触发、US1 可达**。于是:`AGENT_TEAMS_HOWTO` 改成**通用表述**(按**当前活动 profile** 的 `dsh.profile.bundles` 自查;已经在那里 ⇒ 拦住本次切换的是别的原因),拒绝文案里**补一行设置 UI 的兜底路**(`MODE_SETTINGS_FALLBACK`:形态就是 settings 的 `team-link.teams[].mode` / `leadSessionId`,用户经设置 UI 永远是超级写者)—— 防一次假阴性把用户卡死。读面首行同时按分支准确化(见 ②)。 **② D2 —— 首行与原因行自相矛盾(真机最常走到的那一支)+ 两条断言** —— 名册投影不可读时原先**无条件**先打常量「成员名册不可读(本部署未提供该投影)」,再打原因行;而探针有五条不可读分支(服务缺席 / 缺方法 / 无会话身份 / **调用方非成员** / 读取抛错),真机走的是「服务在场、调用方不是成员」那一支 ⇒ 首行与原因行**互相打脸**。现在探针回一个 `cause` 判别符,渲染器据此取首行:只有 `service-absent` 说「本部署未提供该投影」,其余四条用**中性首行**(`成员名册不可读 —— 仍可读 Lead 与形态(哪一条读不到,见下一行的原因)`)+ 紧随的`(不可读原因:…)`行。**补两条断言**:「调用方非成员」档与「读取抛错」档(此前**零断言**)。 **③ D3 —— roster.md 镜像补 mode / Lead 两行 + `set-mode` 落笔后重写镜像** —— 镜像文件自述「与**设置变更**同一事务内 best-effort 镜像」,但 `set-mode` 既不渲染这两个字段、也不重写镜像 ⇒ 那句自述对本字段是**假话**。现在镜像里出现`- 形态(mode):…`与`- Lead 会话(leadSessionId):…`两行,且 `set-mode` 落笔后与既有调用点(`upsert-team` / `set-role` / `retire`)**同形**地调用同一个 `writeRosterMirror`(best-effort 语义一字不差:镜像坏掉只告警,绝不把整次切换变成失败)。 **④ D4 —— 过期限定词统一(三处)** —— 父档 §2.3 矩阵第 7 行、`lib/index.js` 的能力矩阵常量、README 的同一张表此前仍写「成员部分要读宿主投影(**未复核**)」,而同批已把父档 §8 改成「✅ 已核」;B 批「逐行照抄」把它抄进了代码常量。三处统一为已核口径:`⚠️ 成员部分读宿主投影(2026-09-27 已核:本部署已挂载该服务 ⇒ 探针 available=true)`。 **⑤ D5 —— 补确认框「成员名册不可读」那一档的断言** —— §4.4 写死了两档(可读 ⇒ 列成员 / 不可读 ⇒ 写「(成员名册不可读)」),但套件只断言了可读档。现已补上(含「不会迁移的三件事」与「不阻断」两句的对照)。 **⑥ D6 —— 「只改这两个键」的表述改准 + 多团队夹具** —— README / CHANGELOG 原写「除这两个键以外**一个字不改**」,而实现是 `reopened.teams.map(...)` **整份数组回写** ⇒ 其余团队行也会被**补默认值**(`mode:"sessions"` / `leadSessionId:""`)。现在如实写成「只改**本团队行**的两个键;其余团队行**已有值不被覆盖、缺值被补默认**(与既有 `upsert-team` 同形,语义无变化)」,并补一条**多团队夹具断言**钉住这两半。 **⑦ D7 —— 测试加固三处** —— ① 能力矩阵断言由「4 个 ❌ + 3 个 ⚠️ + 其中两行原文」改为**逐行锁八行原文**(其余六行的文本漂移此前**不会红**,是真盲区);② 删掉 `modeSetEnv({ tag: "rg" })` 的死参数(该函数没有 `tag` 形参);③ 三道门的 `length` 断言在注释里**如实标注**为「函数元数变更探测器」,不是行为锁。 **⑧ D8 —— 唯一的门 / 写路径语义改动(D2 / D3 只改读面文案与镜像渲染):让「补记」这条路真的可执行(原指路与幂等互斥)** —— `decisions.md` 写失败时给出的指路是「补写路径后**重发本次切换以补记**」,但幂等分支(同档同 Lead)会**先命中**并返回「已是该档」、**连 decisions 都不追加** ⇒ 该指路**不可执行**,US6「每次切换留一行」会被永久破坏。现在幂等判据放宽为「同档同 Lead **且账上已有这一档的形态史行** ⇒ 已是该档(零写);**账上缺这一行 ⇒ 补记恰好一行**再返回(返回体如实说明这是补记)」。形态史行的识别走**共享常量函数** `modeHistoryBody`(写读同源:落笔与判据读同一个函数),只比正文、不看时间戳与作者;**读不出来 / 没有 workspace ⇒ 不补记**并在返回体如实说明(fail-closed,不猜)。 ### 💡 为什么 这一轮的八条有同一个形状:**文档/文案断言了没有实测过的结论**(D1 把真机读反了 —— 它以为拒绝路径会被触发,真机其实根本走不到),或者**两条互相矛盾的话被并排写在一起**(D2 的首行与原因行、D6 的「一个字不改」与整份数组回写、D8 的指路与幂等)。处置原则沿用本仓家规:**能实测的就实测**(D1 四处改成真机事实,并让文案只说它在当前 profile 下该说的话)、**能标注的就不沉默**(D2 的中性首行 + 原因行、D8 的「不补记」)、**能红的断言不许留盲区**(D7 逐行锁 + D5 补档 + D8 两条新判据)。 ### 🚫 明确不做(本轮) 不加新工具 / 新参数 / 新事件类型 / 新持久状态;不改投递层与黑板三件套的语义;**不动 `docs/observability-batch-design-2026-09-26.md`**;不做跨形态改投、不做自动切档、不代理宿主的派活/任务板(B 档 §7 继续有效);**不碰部署面**(`profile/profiles/web/...` 与用户目录里的插件副本)。 ### 🧪 怎么验证 - **命令**:`node host-half.test.mjs && node client-half.test.mjs`; - **绿相(实跑两行原文)**:`node host-half.test.mjs` → `ALL PASS / assertion total: 1184 (failed: 0)`(退出码 0);`node client-half.test.mjs` → `ALL PASS / assertion total: 269 (failed: 0)`(退出码 0)。基线(本轮修复前的工作树)`1168` / `269` ⇒ **新增 16 条宿主断言**(`1184 = 1168 + 16`);**浏览器半边本轮一字未改**(仍 `269`); - **红相(本轮修复前的实现 + 本轮新断言;测试先行)**:**`assertion total: 1184 (failed: 14)`**。逐条点名后**做了归因**:**13 条是真红**(D1 两条 · D2 两条分支 · D3 三条 · D8 三条 · U6 两条幂等 · U11 矩阵逐行锁一条),第 14 条(`D6 多团队夹具`)是**夹具假红**(那一版夹具漏了宿主替身 ⇒ 切换先被探测拒掉),补上替身后它**在两种实现上都绿**、改归 ★ 负相;另 **6 条在修复前本来就绿**,如实标 **★ 负相**(覆盖缺口类:`U6 账上已有该行`【改写】· `D2 首行反向锁` · `D5 不可读档` · `D5 两档对照` · `D8 前置(写失败)` · `D6 多团队夹具`)—— 不冒充红相; - **本轮改写的既有断言 3 条**(同批同步,不留旧口径):`U11 完整能力矩阵`(→ 逐行锁八行原文)· `U6 幂等(agent-team)`(→ 加上「账上已有该行」这一半)· `U6 幂等(sessions)`(→ 改成「账上缺该行 ⇒ 补记恰好一行」+「补记之后 ⇒ 零写幂等」两条); - **回归**:B 批 41 条与 A 批 / 台账批的既有断言零回归(唯一的三处改动就是上面那三条改写);模块级 `inject` 仍 4 项;`policy` 命名空间键集不变(零新增持久状态);D8 的两个写路径都只碰 `decisions.md`(只追加一行,不重写)。 ### 🔁 同批同步的面 `lib/index.js`(`AGENT_TEAMS_HOWTO` 通用化 + `MODE_SETTINGS_FALLBACK` + 拒绝文案 + 能力矩阵第 7 行 + `probeAgentTeams` 的 `cause` + `TEAM_MODE_ROSTER_UNREADABLE_NEUTRAL` + 渲染器按分支取首行 + `renderRosterMirror` 两行 + `set-mode` 的镜像调用 / 幂等放宽 / 形态史共享写入 `modeHistoryBody` / `decisionRowBody` / `decisionsHasModeHistory` / `appendModeHistoryRow` / `backfillModeHistoryRow` + roster 工具描述面);`host-half.test.mjs`(16 条新断言 + 3 条改写 + `makeAgentTeams` 的 `listThrows` 与 `modeSetEnv` 的 `workspace` 两个夹具参数);`README.md`(三处读数 `1168 → 1184` · 能力矩阵第 7 行 · 落笔 / 幂等 / 看形态 / `set-mode` 四行 · `agentTeams` 服务表);本条目;`docs/verification-log.md`(本轮一节);`docs/team-mode-batch-design-2026-09-26.md`(§3 未复核面行 · §4.2 幂等口径 · §4.3(3) 第三种文案 · §6 U6 · §8 A1 与 §8.1 A1 · §2.3 第 7 行);`docs/team-ledger-and-mode-design-2026-09-26.md`(§2.3 第 7 行 · §8 已核那条)。 --- ### 形态批(B 批:团队形态 · `set-mode` · 读面形态段 · teammate 交卷提示)(2026-09-27) > 设计(唯一事实源):[`docs/team-mode-batch-design-2026-09-26.md`](docs/team-mode-batch-design-2026-09-26.md)(§4.1 形态字段 / §4.2 `set-mode` / §4.3 读面形态段 / §4.4 交卷提示 / §5 红线六条 / §6 判据 **U1–U14 + U11b** / §7 明确不做 / §8 假设 A1–A4)。来源是[《台账 + 形态设计》](docs/team-ledger-and-mode-design-2026-09-26.md) §7 的**步 3**(形态字段 + agent-team 档)与**步 5**(teammate 交卷提示)。逐条红相/绿相读数见 [`docs/verification-log.md`](docs/verification-log.md) 的「形态批(B 批)」一节。 ### ✨ 修了什么 **① 形态字段(只落在 settings,§4.1)** —— `TeamConfig` 扩两字段:`mode`(闭集 `sessions`(默认)/ `agent-team`)与 `leadSessionId`(agent-team 档的 **Lead 指针**,不是名册)。**既有团队零迁移**:老 settings 行没有这两个键也照常读出 `sessions`;读到**闭集外**的 `mode` ⇒ 落默认档**并在读面留痕**(点名原值、闭集、以及「原值未被改写」),settings 里的原值一个字不动 —— 与「手改出来的行必须降成**可读状态**,绝不降成不可读命名空间」同一条范式。 **② `team_link_roster action=set-mode`(第五个封闭动词,§4.2)** —— 团队形态在多会话档 ↔ agent-team 档之间切换: 1. **闭集**:`mode` 不在闭集内 ⇒ 拒绝并**列出两个值**(拒绝文案渲染的就是校验用的那一个常量); 2. **Lead**:`leadSessionId` **省略 ⇒ 默认取调用方自己的会话 id**,并在确认框正文里**明示该默认值**(人可见即可否决);**显式空串 ⇒ 拒绝**;非空须过**形状**(`^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$`)与**在场校验**(能在本次会话列表快照里找到 —— 防转录错位)两道; 3. **宿主探测只对「切到 agent-team」这一向**:探测不到 ⇒ **拒绝 + 指路怎么开 + 零写入**(切回 `sessions` **不探测**); 4. **双重门(缺一即 fail-closed、零写入)**:① `writer gate` **原样**(现任协调者;空缺 ⇒ 会话路径拒绝,设置 UI 仍是兜底);② **人类确认框**(无确认服务 / 超时 / 取消 ⇒ 拒绝且零写入); 5. **落笔**:settings 的团队行只改 `mode` / `leadSessionId` **两个键**(`teams` 是**整份数组回写**:其余团队行若缺这两个键会被**补默认值**、已有值不被覆盖 —— 与既有 `upsert-team` 同形)+ `decisions.md` 追加**恰好一行**形态史(`形态 → agent-team(Lead=)` / `形态 → sessions`)+ best-effort 重写 `roster.md` 镜像; 6. **幂等**:同档同 Lead **且账上已有这一档的形态史行** ⇒ 「已是该档」,**不弹框、不写 settings、零写入**;账上**缺**那一行 ⇒ **补记恰好一行**再返回(否则那条「补上路径后重发以补记」的指路不可执行),账读不出来 / 没有 workspace ⇒ **不补记**并如实说明;另有写时复检(对话框横跨任意长的人工等待,落笔前的现任条件由宿主**重新观测**)。 **③ 读面形态段(§4.3,`roster get` 与状态卡⑦段同一个渲染器)** —— 形态 + Lead(Lead 为空标「**Lead 未知**」,不编造)+ 成员名册**投影**(**读时现算、一行不落 settings**;来源文案**写死两种**:`来源:宿主 agentTeams 投影(本部署可读)` / `成员名册不可读(本部署未提供该投影)—— 仍可读 Lead 与形态`)+ **完整能力矩阵**(逐行照抄父档 §2.3:四件 ❌ / 三件 ⚠️ / 一件与形态无关)+ 「**单会话兜底档**」那句结论 + **形态诊断行**(除自己外没有其他可达会话时只提示、**绝不自动切**)。 **④ teammate 交卷提示(§4.4,提示式、不阻断)** —— 切换确认框正文列出「**不会迁移的三件事**」(① 信任 ② 成员名册 ③ 任务归属)+ **当前 teammate**(可读则列,不可读如实说)+「请确认它们的结论已落到黑板或文件」。**不逐成员推断有没有交卷**(那要读子代理日志,§7 明确不做)。 ### 💡 为什么 形态**不是功能开关,是通道选择**:多会话档给的是「平级会话之间的信任层」(投递 / 配对 / 换届 / 黑板),agent-team 档给的是「一个会话内部的子代理团队」。两档各有各的主人,所以本批只做四件事:**把选择写成 settings 里的一个字段**、**把切换做成一次由人点一下的动作**、**把「这一档下什么用不上」如实摆到读面上**、**把「切走会丢什么」在切之前说出来**。三条纪律贯穿全批:**只读宿主**(不 spawn / 不派活 / 不改宿主任务状态)、**不悄悄降级**(写路径刺眼报错 + 指路;读路径如实标注并保持其余能力 —— **两条路处置不同且不许混用**)、**指针不拷贝**(只存 Lead 会话 id,成员名册读时现算)。 ### 🚫 明确不做(本批) 不代理宿主的 spawn / 派活 / 任务板;**不读子代理日志**(交卷只做提示式,不逐成员推断);**不做自动切档**(切换永远由人触发);不改 `pairs`/`trustedSenders` 的迁移语义(只**声明**不迁移);不做客户端面板(数据面先行,与 A 批同一取舍);**不做跨形态改投**(切档不改投在飞消息、不建桥 —— 要跨形态说话由模型自己用对应形态的手段说)。 ### 🧪 怎么验证 - **命令**:`node host-half.test.mjs && node client-half.test.mjs`; - **绿相(实跑两行原文)**:`node host-half.test.mjs` → `ALL PASS / assertion total: 1168 (failed: 0)`;`node client-half.test.mjs` → `ALL PASS / assertion total: 269 (failed: 0)`。基线 `1127` / `269` ⇒ **新增 41 条宿主断言**(`1168 = 1127 + 41`);**浏览器半边本批一字未改**(仍 269 —— 设计档 §7 明确不做客户端面板); - **红相(本批 41 条判据 + 实施前的 `lib/index.js`)**:**`assertion total: 1168 (failed: 34)`** —— 33 条 B 判据红 + 1 条**既有锁**红(`U10` 的 roster 参数键集锁:临时状态下测试文件已按设计要求期望 `mode`/`leadSessionId`,而旧实现没有这两个参数 —— 那条红正是这条锁该红的理由,与 A 批的「工具计数同改锁」同源,不是回归); - **红相的安全方法**:把工作树版 `lib/index.js` **读进内存** → 用 `git show HEAD:lib/index.js` 覆写(**换来换去都有 sha 对照**:覆写后的 `git hash-object` == `git rev-parse HEAD:lib/index.js`,即换进去的就是实施前那一份)→ 跑 `node host-half.test.mjs` → **还原内存里的那一份** → **sha256 逐字节复核**(`restoreMatch: true`,工作树版 sha256 `4110893DC5F07C56909EE194B1E6BF0F20F5C674B8233007065CAE81A378FA3B`)。**不留备份文件、不留探针**,工作树零残留; - **分阶段红相(测试先行,逐阶段实跑)**:阶段 1 `1137 (failed: 9)` · 阶段 2 `1157 (failed: 19)` · 阶段 3 `1168 (failed: 2)` —— 每一阶段的新断言都是在**该阶段实现之前**写进去并实测红的(逐条红/负相拆分见验证账本); - **回归**:既有四动词(`get`/`upsert-team`/`set-role`/`retire`)与 `team_link_rotate`/`team_link_recover` 的动词闭集与三道门函数**一字未动**;模块级 `inject` 仍 4 项;`policy` 顶层键集不变。**唯一改动的既有断言**是上面那条 roster 参数键集锁(它必须跟着参数面一起改,这是本仓「改一处即两处」的既有规矩)。 ### 🔁 同批同步的面 `lib/index.js`(形态常量与渲染 / `normalizeTeams` 降级留痕 / 宿主探测 `probeAgentTeams` / 确认框 `askModeDialog` / `set-mode` 分支 / `roster get` 与 `team_link_status` 的形态段 / 两个工具的描述面);`host-half.test.mjs`(41 条断言 + 宿主 `agentTeams` 替身 + `rosterCall` 的 Y7-safe 调用器 + roster 参数键集锁);`README.md`(三处读数 · roster 动词表与工具一览 · **新增「团队形态」段**(两档白话 + 三句要紧的话 + 能力矩阵)· 状态卡 ⑦ 段 · 设置键表与两处 yaml 样例 · 设计文档索引加本档一行);本条目;`docs/verification-log.md`(本批一节);`docs/team-mode-batch-design-2026-09-26.md`(状态行与 §8 假设 A1–A4 回填实测结论);`docs/team-ledger-and-mode-design-2026-09-26.md`(§7 步 3 / 5 标 ✅ + 指向本档的链接;提交哈希 `12f0874`(2026-09-27 由父会话回填));`docs/observability-batch-design-2026-09-26.md`(§7 第 6 条改标「状态卡形态段由 B 批实施」)。 --- ### 分歧审计修复轮 `DIVERGENCE(12)`(2026-09-27) > 只读分歧审计的 **12 条**结论(D1–D12)逐条闭环。**不夹带新功能、不扩范围**:A 批设计 §7「明确不做」继续有效,B 档 `docs/team-mode-batch-design-2026-09-26.md` 一字未动。逐条读数见 `docs/verification-log.md` 的「分歧审计修复轮」一节。 ### ✨ 修了什么 **① 唯一的行为改动:派生回执行标注「按当前名册解析」(D7)** —— 没有结构化回执时按**实参**兜底解析寻址表达式,其中 `team:/` / `team:/*` 是**按读取时的名册**解析出来的;此前注释承诺了标注而实现没做(只特判了「回执截断」),换届之后可能把 ✅/⚠ 判给当时并非收件人的会话且**不告知**。现在这类行如实带上 ` · 按当前名册解析(该表达式按读取时的名册解析,未必是投递当时那一个)`(裸 id 是字面量,不带此标注),工具描述面同批同步。 **② 10 条断言补齐(D1 / D2 / D4 / D5 / D9 / D10 / D12)**: 1. **D1**:无时间戳 ⇒ 打「无法计算」那句、**不打**计数注记 —— 设计 §8.1 A5 与本档都曾写「三态都有断言」,而套件里 `grep 无法计算` = **0 命中**(补的是判据,不是行为); 2. **D2**:读窗为空 ⇒ 注记**整段不打印**(连标题都不出现); 3. **D4**(四条):`run_code` 里发出的 `tool/ptc-dispatch` 那一类投递照样成行 · 实参兜底解析 `team:/` 时表达式与现任一并显示 · 回执被裁过时只报告**回执自己报的总数** · 逐行渲染上限恰 20 行 + 一行「本次还有 N 行未显示」(N = 真实差额); 4. **D5**:`team_read` 的 `readIds` **非数组** ⇒ 拒绝且**零读**(工具面硬形状); 5. **D9**:`U13 零额外读` 那条原先有半边**恒真**(`slice(length).length === 0`),换成真计数 —— 打注记那次与不打注记那次,各自的 surface 读数逐次相等(各恰 12 = 同一个有界读窗); 6. **D10**:聚合态 `M` = **所示各团队** `tasks.md` 同期新增行数之**和**(夹具 3 + 2 = 5,两个团队的台账尾都在卡上); 7. **D12**:归组面 = **任何消息文本里第一个** `ref=t-` —— **非 banner** 的普通消息(本会话自己发的那条)同样归组,发送方如实回落标「(本会话)」。 **③ 设计与 README 口径回填(D1–D4 / D6 / D8 / D10–D12)** —— 设计档 §4.1 新增「点名读上限与超限行为」表(`list_sessions` ≤12 拒绝 / `team_read` ≤11 截断并逐个标注,同一不变量两种形态)· §4.2 注记补**三态**与**聚合口径**、成本列回填「单团队 1 次 / 聚合 N 次(N = 所示团队数)」、会话面来源订正为 `ctx.sessionQuery.listSessions`(与 `list_sessions` **同源同序**)· §4.3 写死**归组面**与点名读上限 · §4.4 写死投递事实的**两类事件载体**(`tool/call` + `tool/ptc-dispatch`)、**`callId` 配对**、**窗口单位是「最近 20 次 `team_link_send`」而不是 20 条消息**,并补**第 4 态**「:未投递(outcome=…)」(措辞只陈述 `outcome`,不下结论);README 同步这些口径,并修正设计文档索引里与父档矛盾的状态行(步 2 / 4 / 6 已实施;步 3 / 5 待实施)。账本 A 批一节 A5 行的「都有断言」也已订正。 ### 💡 为什么 这一批的共性是**读面与描述面脱节**:行为大多已经存在,但(a)有两条断言是**空的**(一条恒真、一条根本不存在),(b) 有若干实现口径**没写进设计**(无锚时的三态、聚合口径、`team_read` 的读额边界、投递事实的载体与窗口、第 4 态、归组面、成本列),(c) 有一处注释**承诺了没实现的标记**(D7)——那才是唯一真会让人读错数的缺陷:换届之后按当前名册解析出来的收件人,会被当成投递当时的收件人来读。处置原则沿用本仓两条家规:**能标注的就不沉默**(无法计算 / 本次未读 / 按当前名册解析),**能红的断言不许恒真**。 ### 🚫 明确不做(本轮) 不加新工具 / 新参数 / 新事件类型 / 新持久状态;不改 `list_sessions`、`team_link_status`、`team_link_send`、`team_link_team_append` 的任何行为;不动 `PREVIEW_SESSIONS`、不动三个 `timeoutMs`;B 档(形态)一字不碰。 ### 🧪 怎么验证 - **命令**:`node host-half.test.mjs && node client-half.test.mjs`; - **绿相(实跑两行原文)**:`node host-half.test.mjs` → `ALL PASS / assertion total: 1127 (failed: 0)`(宿主进程退出码 0);`node client-half.test.mjs` → `ALL PASS / assertion total: 269 (failed: 0)`。基线 `1117` / `269` ⇒ **新增 10 条宿主断言**(`1127 = 1117 + 10`),浏览器半边**一字未改**; - **红相 A(「本轮修复前」的实现 = 工作树、D7 尚未落码)**:`assertion total: 1127 (failed: 1)` —— 唯一红的那条就是 **D7**;其余 9 条在两种实现上都绿,**如实标为负相**(它们补的是判据覆盖缺口:旧实现没有这个 bug,只是没有这条锁); - **红相 B(A 批实施前的 `89074fe`)**:`assertion total: 1127 (failed: 65)` = A 批的 **55** 条(54 新 + 1 既有工具计数锁,与账本的 A 批一节逐条相符)+ **本轮 10 条全红**;安全方法同 A 批:工作树版读进内存 → `git show HEAD:lib/index.js` 覆写 → 跑 → **还原内存里的那份** → **sha256 逐字节复核**(`restoreMatch: true`,本轮工作树版 `d6ae2f78c1d6c13db8fb2d8ad0cc56cfd623702f7a675675d69c074211b9ab1a`),**不留备份 / 探针 / 残留**; - **回归**:A 批 60 条断言全部仍绿(含 `U12` 既有四块零回归、`U7` 三条成本不变量、`§12.5` 跨半边 outcome 短语锁、工具计数锁 10),模块级 `inject` 仍 4 项,`policy` 命名空间键集不变。 ### 🔁 同批同步的面 `lib/index.js`(`ROSTER_RESOLVED_NOTE` + `receiptRowsOf` / `renderReceiptRow` 的标注 + `team_link_team_read` 描述面);`host-half.test.mjs`(10 条断言 + 4 条夹具事件 + 3 个新夹具 + `trTruncatedResult` + `U13 零额外读` 改真计数);`README.md`(三处读数 + 状态卡段 + 收件视图/派生回执段 + 回执四态表 + 设计文档索引状态行);本条目;`docs/verification-log.md`(本轮一节);`docs/observability-batch-design-2026-09-26.md`(§4.1–§4.4 口径回填 + §8.1 A5 订正 + 状态行加「后续回填」)。**`docs/team-ledger-and-mode-design-2026-09-26.md` 与 B 档一字未动。** --- ### 可观测批(A 批:读窗扩展 · 团队状态卡 · 台账收件视图 + 派生回执) > 设计(唯一事实源):`docs/observability-batch-design-2026-09-26.md`(§4.1 读窗 / §4.2 状态卡 / §4.3 收件视图 / §4.4 派生回执 / §5 红线五条 / §6 判据 **U1–U14** / §7 明确不做 / §8 假设 A1–A5);来源是《台账 + 形态设计》§7 的**步 2 / 步 4 / 步 6**(步 6 的两半都在本批)。逐条红相/绿相读数见 `docs/verification-log.md` 的「可观测批(A 批)」一节。 ### ✨ 修了什么 **① 读窗从「固定 12」变成「默认 12 + 点名 + 分页」(§4.1)** —— `team_link_list_sessions` 新增两个**可选且互斥**的参数: 1. `readIds?: string[]`(点名读):去重后 ≤ **12** 个,超出即**拒绝且本次零读**;只接受**本次列表内**的 id,未知 / 不属于本列表的 id → 拒绝并**指出那个 id**;点名**优先占额**,其余按原顺序补足到 12;本就在默认窗内的 id 只占一个额、不重复读;与 `offset` 互斥; 2. `offset?: number`(分页):从第 `offset+1` 行起读 12 行(默认 0);非整数 / 负数 / 越界 → 拒绝并给出**有效区间** `0..行数-1`; 3. **成本不变量按工具分账**(断言锁死):`list_sessions` 三条路径 ≤ **12**;`team_read` ≤ `min(12, 1+|readIds|)`(**自读占 1 个额**);`team_link_status` ≤ **12**; 4. **列表末尾新增「未读 Y 行 + 怎么读」提示段**(默认调用也带):`未读 2 行(读窗 12/共 14 行):读第 13–24 行 → offset=12;或点名 → readIds=["session-x", "session-y"]`。**既有行结构一字未改**——本次唯一的格式新增就是这一段(外加新参数)。**默认值 12 不动**(提高它等于把超时风险塞回每一个调用)。 **② 新工具 `team_link_status`:只读团队状态卡(§4.2)** —— 一次调用一屏,六段:① 团队与角色(在位 / 空缺 / **版本史末条**)② 换届 pending(在飞令牌,token **掩码**)③ 看门狗(`policy.watchdogs`)④ 会话面(id / 代理状态 / 创建时间 / provisional 标记,**不读日志**)⑤ 活性(**有界**:与列表工具同一顺序读前 12 行,超出如实标「本卡只读了前 12 行」)⑥ 台账尾(`tasksTail` 默认 **5**、上限 **20**,超出或非正 → 拒绝并给区间)。参数:`team?`(省略 = 聚合全部团队、每团队一段;给了但不存在 → 拒绝并**列出已知团队名**)、`tasksTail?`。**全只读**:不写设置、不写磁盘、不加缓存、**连换届过期清扫都不跑**(那会写);无会话身份时照常出卡,「当前会话」处如实标 `(当前会话未知)`。**反面预警注记**(会诊提出):锚 `T0` = 本读窗内最旧的一条 surface 消息时间戳、`N` = 其后本读窗内的消息条数、`M` = 其后 `tasks.md` 的新增行数,**只有 `N == 0 且 M > 0` 才打注记**(「会话可能已停止说话只写行」),否则不打;两个计数都取自**已经读到**的数据 ⇒ **零额外读**;锚取不到时如实标「无法计算」,**不猜**;**它是读数,不是裁决**。 **③ 台账收件视图 + 派生回执(§4.3 / §4.4)** —— `team_link_team_read` 新增**一块**(既有四块一字未动): - **收件视图**:读**调用方自己**的会话面,按 `ref=t-` 分组给出「谁 · 什么时候 · 开头几个字符」;两个尺寸常量 `TASK_INBOX_MESSAGES=20` / `TASK_INBOX_PREVIEW=80`(越界截断**必标注**);无 `ref` 的消息**如实计数**(不静默丢弃),**未登记的任务号照样分组并标注** `(该号在台账里未登记 —— 可能是笔误,也可能是先发后记)`; - **派生回执**:投递事实从调用方**自己**会话面的既有事件取(`tool/call`,或 `run_code` 里发出的 `tool/ptc-dispatch`;有结构化回执时用**投递当时**解析出的目标与结果),三态写死:**未点名** ⇒ `未读`(**目标面零读**,断言锁死)/点名且 `t` 之后有 assistant 事件 ⇒ `✅ 有反应`/点名且其后无 assistant 事件 ⇒ `⚠ 无后续反应`;读不到的面一律标「未读」,**绝不用空数据算读数**;`≤ min(12, 1+|readIds|)`,超额的点名逐个标「本次未读」。**纯派生、零新增持久状态、零新增写路径**。 ### 💡 为什么 「12」是**预算**(一行 surface = 一次冷日志解压 + 一次投影;真实工作区里串行读满 50 行会超掉 60s 工具预算),不是能力上限——所以**不提高默认值**,把「读哪些」的决定权交回调用方(点名 / 分页),并把上限定成不变量;同时把「未读」从终点变成**可执行的下一步**。另外两件事都是**只读地**把已经存在的事实摆到一屏里:状态卡回答「人想知道团队还活着没有」,收件视图回答「谁对我说了什么」,派生回执回答「我说出去之后对方动了没有」——后者是**纯派生**,不新增任何持久状态(会诊 Q4 采纳项)。**红线一寸未动**:成本不变量按工具分账、未读必标注、**全只读**(`send` / `team_append` / `roster` 的写面一行未动)、`list_sessions` 既有行结构逐字保持、措辞不出现「状态=完成」这类把主张写成事实的词。 ### 🚫 明确不做(本批,§7) 不做客户端面板渲染(本批只做数据面)· 不做缓存 / 索引 / 自动后台巡检 · 不做跨进程聚合 · **不提高 `PREVIEW_SESSIONS` 默认值** · 不定 agent-team 档下的状态卡行为(形态字段尚不存在,跨批接缝显式声明)。 ### 🧪 怎么验证 - **命令**:`node host-half.test.mjs && node client-half.test.mjs`; - **新增断言 60 条**(全部落在宿主套件;读数 `1117 = 1057 + 60`,基线取本批前的套件自报值 1057):阶段 1(读窗 / 成本不变量 / 尾巴提示 / 三类边界拒绝)23 条 · 阶段 2(状态卡六段 + 参数边界 + U13 注记)20 条 · 阶段 3(收件视图 / 派生回执 / U12 回归)17 条; - **两套件实跑自报读数**:`node host-half.test.mjs` → `assertion total: 1117 (failed: 0)`;`node client-half.test.mjs` → `assertion total: 269 (failed: 0)`(浏览器半边本批**未触碰**:本批只做数据面); - **红相(安全方法:备份 → 换实现 → 跑 → 还原 → sha256 复核)**:把 `lib/index.js` 换成 `git show HEAD:lib/index.js`(`89074fe`)后跑本批的测试 —— `assertion total: 1117 (failed: 55)`,其中**本批新断言 54 条红**(其余 6 条是刻意在两种实现上都应通过的**回归锁**:U1 的三条「行结构 / 读窗 / 窗口内 verdict 不变」、U12 的「派生读数与原始行窗口未动」、U3 的「本就在默认窗内的 id 不重复读」、以及 U9 的 settings 半边在红相上 trivially 成立——旧实现根本没有这个工具,自然不改设置),另 1 条是既有的 `§11.9 工具计数同改锁`(在那个临时状态下 README 写 10 个工具而旧实现注册 9 个——这正是它该红的原因)。**还原后 sha256 逐字节一致**(`fde1b78573f88d392cdf3666c38bf1f70132439209666d590bfc46bf546ff7c1`),工作树无残留; - **回归锁**:`list_sessions` 的既有行结构 / 既有断言零回归;`team_read` 的**既有四块**(roster + decisions + discipline + tasks)零回归,新块只追加; - **红线回归**:模块级 `inject` 仍 4 项、**不新增任何日志事件类型**(新读全部走既有事件:`tool/call` / `tool/ptc-dispatch` / 既有 `tool/result.meta`)、`policy` 命名空间**键集不变**(零新增持久状态)、投递 `source` 仍恰三成员、`§12.5` 跨半边 outcome 短语锁与 README 工具计数锁同批更新后仍绿。 ### 🔁 同批同步的面 `lib/index.js`(`readListReadWindow` / `listReadTail` / `readStatusTasksTail` / `ledgerRowTime` / `reverseWarningNote` / `readTaskReceiptTargets` / `surfaceMessageEvents` / `surfaceEventText` / `shortStamp` / `crossSessionBody` / `inboxOpening` / `readReceiptTargets` / `parseToolArguments` / `surfaceSendRecords` / `resolveSendTargets` / `taskMessageBlock` / `receiptRowsOf` / `renderReceiptRow` / `asText` 与两组常量;`team_link_list_sessions` 的参数面与尾巴、`team_link_status` 新工具、`team_link_team_read` 的 `readIds` 与新区块);`host-half.test.mjs`(A 批 60 条断言 + `listCall` / `statusCall` / `readCall` 三个 Y7-safe 调用器 + `mkdir` 进导入 + 工具计数 9 → 10);`README.md`(§二 读窗参数与成本不变量表 · 新增「团队状态卡(只读)」一段 · §四 收件视图与派生回执 · 工具一览表两行 + 新工具一行 · 设计文档索引 + 目录 · 三处读数);本条目;`docs/verification-log.md`(本节读数);`docs/observability-batch-design-2026-09-26.md`(§8 假设 A1–A5 的实测结论 + 状态行);`docs/team-ledger-and-mode-design-2026-09-26.md`(§7 的步 2 / 4 / 6 标已实施并链接本档)。 --- ### 团队任务台账 `tasks.md`(设计 §9 第一批:只追加台账 + 读时投影) > 设计(唯一事实源):`docs/team-ledger-and-mode-design-2026-09-26.md` **§9**(§9.3.1 行格式 / §9.3.2 写入面 / §9.3.3 读面 / §9.3.4 派生规则 / §9.4 边界与红线 / §9.5 判据 **U1–U18** / §9.6 明确不做 / §9.9 跨文档同步六条)。 ### ✨ 修了什么 **新增第三个黑板文件 `tasks.md`**(`/team//tasks.md`)——只追加的任务台账,与 `decisions.md` 同构:只追加、每行带署名、seq 由插件单调分配、单行 500 码点。 1. **行格式(§9.3.1)**:`seq | ISO时间 | author-session-id | kind | task | 正文`。`kind` 是**闭集**:`plan` / `claim` / `done` / `block` / `dispute` / `retract`;`task` 形如 `t-`。 2. **写入面(§9.3.2)**:`team_link_team_append` 的 `file` 白名单由两值扩为**三值**,新增 `kind` / `task` 两个参数。`kind=plan` 由插件**扫整个文件**分配 `t-<已登记最大号+1>`(调用方同时给 `task` 即拒绝);其余 `kind` 必填 `task`,且**该号必须已在本文件登记过**——未登记的号一律拒绝并指路 `kind=plan`;`kind` / `task` 传给 `decisions` / `discipline` **一律拒绝且零写入**;`seq` **每文件独立计数**;写入走 `appendFile`(读-算 seq-追加),只追加不重写。 3. **读面(§9.3.3)**:`team_link_team_read` 在 discipline 块之后追加 tasks 块 = **原始窗口**(末 20 条,逐字)+ **派生视图**(扫描最近 500 行,逐任务给「最后主张」与「未消解存疑」)+ `baseHash`(仅供参考/审计:tasks 只追加、不接受 baseHash 参数)+ **三类边界如实标注**(文件不存在 / 超扫描上限「更早的 N 行未参与派生」/ 不可解析行「N 行无法解析,未参与派生」)。 4. **派生规则(§9.3.4)**:最后主张 = 该任务最后一条 `claim` / `done` / `block`,一条都没有 → 「尚无主张(仅登记)」;`retract` **不抹历史**——被撤回的主张仍然显示(呈现「最后主张 done(… · 已由 #N 撤回 · 撤回者 …)」,**原主张者与撤回者两个 id 都在**);`dispute` 之后**没有**新的主张 / 撤回才标 `⚠ 未消解存疑 N 条`,否则标「存疑已由 #N 消解」;**绝不自动裁决**(不比较正文、不按时间戳定胜负),措辞一律「最后主张 / 未消解存疑」,**不出现**「状态=完成」这类把主张写成事实的词。 ### 💡 为什么 主会话的上下文会被压缩,而「我们到底在干什么」过去只活在它脑子里;但**报告必须原话进上下文**这条红线又不能动(把报告换成「写张便条贴墙上、让主管自己看」,互核就死了)。台账补的正是中间那一格:**把「账」搬出上下文,把「事」留在上下文**,并且**在结构上无法成为事实源**—— - **只追加** ⇒ 不存在「谁有资格把 doing 改成 done」这个无仲裁者的问题(不抄 CAS 看板:那张板子上写着 done 就是「现实」,一个错被全员继承); - **每行带署名** ⇒ 板上永远只有「A **声称** T3 完成」,没有「T3 完成」; - **反驳走同一条写入路径** ⇒ 存疑只是**又一行**,不需要任何新机制,也不需要新的授权类型; - **「当前状态」是读的时候算出来的** ⇒ 显式标注「这是派生读数,不是事实」;删掉文件不影响任何写入路径,派生结果随时可从原始行重算。 ### 🚫 明确不做(本批,§9.6) 不做状态机 / CAS / 自动裁决 / 自动推进(「送达即完成」禁止)· 不做写入权限门 · 不做跨会话物理队列 · 不做 12 窗口 / 形态切换 / 状态卡 / 交卷提示(§7 第 2–5 步不在本批)。**`decisions` / `discipline` 的语义与文案一字未改**,唯一例外是 `file` 白名单拒绝文案由两值改三值(§9.6⑤,已用**源码级**断言锁住)。 ### 🧪 怎么验证 - **命令**:`node host-half.test.mjs && node client-half.test.mjs`; - **新增断言 62 条**(全部落在宿主套件 `host-half.test.mjs`;U1–U18 逐条对应,读数 `1057 = 995 + 62`,基线取本批前的套件自报值 995):**51 条**随第一批(台账本体)、**11 条**随同日分歧修复轮(D4 判据缺口 5 条:U4 的 `=500 码点收` · U12 的 `block 可作最后主张` · U18 的另两组组合与四组合总锁 3 条 · D5 1 条 · D6 2 条 · D9 2 条 · D1/D8 的描述面口径 1 条)。**两套件实跑自报读数**:`node host-half.test.mjs` → `assertion total: 1057 (failed: 0)`;`node client-half.test.mjs` → `assertion total: 269 (failed: 0)`。客户端半边本批**未触碰**; - **红相(功能缺失,两次实跑)**: - **① 本批判据 + 旧实现(`3eba6c1:lib/index.js`)**:`node host-half.test.mjs` **在 `host-half.test.mjs:2145` 当场抛错、整轮中止** —— `ToolArgsError: invalid arguments: "file" must be one of ["decisions","discipline"]`(`code: 'INVALID_ARGS'`),`nodeExit=1`,**`assertion total` 一行都没打印**(红相说不清自己的规模,与本仓 Y7 教训同形)。原因正是旧实现的 `file` 枚举里没有 `tasks`、`kind` / `task` 也还不是已声明参数(新判据打旧实现会**抛**,而不是返回 FAIL); - **② 本批判据 + 本轮修复前的 `lib/index.js`**(把 D1 / D5 / D8 / D9 四处改动逐字回退):`assertion total: 1057 (failed: 8)`,8 条逐条点名 —— D9 1(`t-1e+22` 那条分配路径)· U13 三条**全行比对**(t-2 / t-4 / t-1,旧实现渲染成 `) · ⚠ …`)· D1/D8 的读面口径行 1 · D6①/② 2 · D5 1。U4 / U12 / U18 的 5 条在修复前**本来就绿**(补的是判据覆盖缺口,如实记、不算红相); - **逐条读数、夹具与还原纪律见 [`docs/verification-log.md`](docs/verification-log.md) 的「团队任务台账 `tasks.md` 第一批(设计 §9)+ 分歧修复轮」一节**; - **U14 是回归锁**:`decisions` / `discipline` 的**整段返回文本**用 `===` 逐字比对(不是 `includes`),任何一处文案漂移当场变红; - **红线回归**:导入面仍**六模块**(零新增依赖)、**不新增任何日志事件类型**、投递 `source` 仍**恰三成员**、`policy` 命名空间键集不变——本批只读写磁盘上的黑板文件,不碰投递层、不碰 settings 形状、不引入任何内存态。 ### 🔁 同批同步的面 `lib/index.js`(常量 `TASKS_FILE` / `TASKS_WINDOW` / `TASKS_PROJECTION_SCAN` / `TASK_KINDS`、`blackboardPaths` 的 `tasks` 键、公共 `lastSeq`、`tasksRow` / `parseTasksRow` / `registeredTaskIds` / `deriveTasksView` / `taskView` / `taskStamp` / `renderTaskView`、`team_link_team_read` 与 `team_link_team_append` 的两个面;分歧修复轮的 D1 分隔符 / D5 删未申报段 / D8 描述面三件套口径 / D9 安全整数分配);`host-half.test.mjs`(§9 的 **62 条**断言(51 + 修复轮 11)+ `tasksPath` / `tasksBlockOf` / `tasksHashOf` 三个夹具助手 + 那条「三个 baseHash」既有断言的同批更新);`README.md`(§四 目录树三件套 / 参数说明 / 任务台账小节 / 台账推荐用法 / 工具一览表 / **三处读数**(徽章 · 对照表 · §十));`docs/verification-log.md`(本节读数:本批做了什么 / 红相两次实测 / 绿相 / 边界证据);本条目;`docs/team-ledger-and-mode-design-2026-09-26.md`(§7 第 1 步与 §9 状态行标已实施并附实测读数;§1 / §4 的 `conflicted` 口径改成已实施的措辞;§9.3.4 规则 2 补两条子情形定义)。 --- --- ## 0.3.10 — 2026-09-22(**发布收口:0.3.9 + 0.3.10 一并落定**)(§10.2.8 `/team_session` 三条真机缺陷修订:输入文法 · 结果可见性 · 确认框边界) > 设计(唯一事实源):`docs/collab-enhancements-design-2026-09-19.md` **§10.2.8**(§10.2.8.0 现象与证据 / §10.2.8.2 修订后的文法 / §10.2.8.3 结果可见性 / §10.2.8.4 确认框边界 / §10.2.8.6 明确不做)与 **§10.4 的 U30–U34**;逐条**红相/绿相读数**见 `docs/verification-log.md` 的「§10.2.8 三条真机缺陷修订(U30–U34)」一节。 > **发布状态(2026-09-22 发布收口)**:本条目与 **0.3.9** 一并落定(当时为当前版本;其后由 **0.4.0** 接续)(`package.json` **0.3.10** · tag **v0.3.10**;0.3.9 段按本仓惯例标注「发布前收口于 0.3.10」)。①② 是宿主半边(需 DSH 重启才在真机生效);③ 含**一号宿主半边**(换槽位)与**二号浏览器半边**(§10.2.8.3 通道 1 的客户端节点,需重建客户端 bundle + 刷新页面)。 > **真机状态(取代原先那句「本条目不声称已真机验证」)**:§10.2.8 的缺陷 ①②③ 与 §10.2.8.9 / §10.2.8.10 的四项裁定**均已真机验过** —— 含确认框在真机上的四条判据、通道 4 回执(GUI 渲染为「跨会话消息」卡片)、最小唤醒、侧边栏三个会话可见、结算时序读数;逐条读数与截图证据见 `docs/verification-log.md` 的「真机复验」一节。**U33 的「渲染高度不超视口」**这一条也已被动验到(框体紧凑、底部动作行全在视野内),但它依赖窗口宽度,**仍不作单测断言**。 ### 🐞 三条真机缺陷与修法 **① 输入文法:正文被当成了参数(U30 / U31)**。真机现象:用 `/team_session team=0921-main ` 接一具 **3058 字**的交接正文,连发 5 次,界面「点了发送什么都不显示」、团队与会话一个都没建成;会话日志里是 **5 条 `command/run` + 5 条 `command/done kind=error`**,错误文案逐字相同:`N 必须是整数(会话个数,1..8)。`。**根因**:解析器在**整条输入**上做 token 扫描,**任何**「字母+等号」都被当成 `key=value` —— 正文里的「同窗 `N=3)。`」被读成 `n=3)。` ⇒ **参数校验阶段即拒绝**,连「角色名」那一步都没走到。同族的第二个雷:正文里还有 **284 个**不含等号的 token,按当时文法**全是位置角色名** ⇒ 即便躲过 `N`,也会以「会话个数 284 超过硬顶 8」失败。⇒ **当时没有任何一种拼法,能把一具含「字母+等号」的正文送进去**(`task=` 也会在第一个已知 `key=` 处断开)。 **修法(用户裁定 = 方案 A「参数可省」,§10.2.8.2 的 R1/R2/R3)**:**R1** 参数区**只在行首**且谓词闭合 —— 从行首连续取「形状合法(含 `=` 且 label 非空)、key 已知、**取值本地合法**」的 token 为参数;结束条件**恰两类**:**(a) 静默结束**(token 不含 `=`,或 `=` 在首字符 ⇒ 该 token 起全部算正文)· **(b) 报错**(形状合法而 key 未知,或 key 已知而取值不合法 ⇒ **整条命令拒绝**并**原样回显 offender**)。**R2** 正文不解析:进入正文后 `N=3` / `pid=384448` / `word=` 一律**逐字保留**。**R3** 失败必须点名:回显 offender + 给出路「想写正文就直接写…也可以用 `task=`」。**位置角色名废除**(本次的语义变更,已在设计、命令 description、`input.hint` 与报错文案里**显式宣布**):裸 token 是**正文的起点**,声明角色的唯一拼法是 `roles=`(别名 `role=`);旧的「位置参数写角色名」指引同批删除。**默认值**随之落定:`team` 省略 ⇒ 调用会话工作区目录名(不合 `[a-z0-9-]+` 回退 `default`)· `n` 省略 ⇒ 1(给了 `roles=` 时为角色数)· `roles` 省略 ⇒ 一个 `worker-1` · 既无正文也无 `task=` ⇒ 只建会话、不投启动任务。**既有语义一字不改**:N ≤ 8、每队 ≤ 24、按 role 幂等、两个别名、`model`/`preset` 规则、确认框、失败即停。 **② 结果可见性:命令结束了,界面上一行都没有(U32)**。真机现象:那 5 条错误**逐字写进了会话日志,界面上一行都没显示** —— `command/run` / `command/done` 是 **log-only** 事件(**不进模型面、也不保证在壳里渲染**),落点只在发命令的那个输入框里(§10.2.1 那条「顶层可见是免费的」被真机推翻)。**修法**:按 §10.2.8.3 的通道优先级取**可用的第一条** —— **通道 1(复用 §10.1.3 的 D 模式)经核实可用,即取通道 1,不降级**:客户端新增**独立 definition**(`kind: team-link-team-session`)与同一键控槽上的渲染面,match 既有 `command/run`(**按 `data.name` 过滤**)与 `command/done`(该事件**本身不带 name**,只能按**见过的 `commandId`** 认领 —— 有界 64、重放安全),**成功与失败走同一条通道**。**不新增任何日志事件类型**(只**读**既有事件)。 **③ 确认框无界、动作够不着(U33)**。真机现象:确认框终于弹出,但**框体高于可视区,底部的「创建 / 取消」够不着**(那次成功命令的 **27.4 秒全花在确认框上**;此前另有 **8 次** `kind=success` 的失败尝试走的是「取消则零创建」那一支)。**先修正归因(有代码依据)**:撑高框体的**不是** `task=`(那行早就是有界预览),而是**我们自己写的散文**。**修法(修法表第 0 行,首要、结构性)——换槽位**:披露正文从 `question` **整段移入 `detail`**,`question` 只留**一行话**。依据是壳的布局(逐条可复核):`question` 被渲进**无高度钳制、且在滚动容器之外**的 `

`,而 `detail` 在 `Mbwy4a_body{overflow-y:auto}` **之内**、底栏 `flex-shrink:0` 也在滚动区外 ⇒ **动作可达性与文本长度脱钩**。同批:任务预览**标注**(共 N 字 / 仅显示前 M 字 / **完整正文会原样投递**)· 正文两个硬预算(**码点 ≤ 600 且换行 ≤ 12**,两者都单测断言)· **不逐角色展开**(只列前 3 个 + 「…等 N 个」,**总数仍写全**)· **「只给 id 形状 + 计数」**(`- 会话 id:team-link---(共 N 个…)` —— 修法表第 0 行换槽位时把这行连同逐角色 id 清单一起删了,§10.2.8.4 (b) 要求的这半句一度零实现;全量清单仍只出现在完成回报里)· DEFECT-4 与模型/预设两段自述各压到**一行** · 可变字段(团队名 / cwd / 会话 id / **model= / provider=**)逐字段封顶且裁剪留下省略号 · 按段落裁剪时**标注**(「已裁剪至 600 码点 / 12 行上限,省略 N 段自述;必备披露一字未少」)。§10.2.8.7 裁定 2 的**角色指引行**(「新会话是 worker;你自己(调用会话)是 coordinator;要交棒请用 `team_link_rotate`」)在**确认框与完成回报**里各一行,并**计入**上述预算。 **明确不做**(§10.2.8.6 / §10.2.8.4 (d)):**不驱动调用会话**(`followup` 会花 token,且违背命令的 log-only 语义)· **不加**「建完即换座」(会绕开 M4 的信任快照 / 冻结广播 / 对称吊销 / 一次性令牌两阶段)· **分页杠杆已核实不适用**(壳的分页是**按问题**且每页须作答,拆散文不行)· **不改**模块级 `inject`(仍 4 项)· **不改**既有 schema key · **不新增任何日志事件类型**。 ### 🧪 怎么验证(变异验证) - **修复前必红**(隔离夹具 `.tmp-head/`:本轮的新测试 + `git show HEAD:lib/{index,client}.js` + HEAD 的 `README.md` / `package.json`):`node host-half.test.mjs` → `assertion total: 951 (failed: 30)`;`node client-half.test.mjs` → `assertion total: 269 (failed: 22)`。 - 宿主侧 30 条的**实际分布**(**逐条点名**,同夹具实跑):**U30 7/9 · U31 4/4 · U33 8/9** + 改写后的既有断言 **U16 10 条** + **DEFECT-4 槽位 1 条** = 7 + 4 + 8 + 10 + 1 = **30** ✓。**其中 3 条在旧实现上本来就绿**(原记「本批新增的 U30 / U31 / U33 全红」是**计数不实**,2026-09-22 只读审计第 6 条纠正):**U30 的 R1(b) 正相**(`n=abc` ⇒ 报错 —— 它断言的是**保留下来**的旧语义,不是本批引入的新语义)· **U30 的「既有 key 语义不变」** · **U33 的 `options` 一字未动**(换槽位只搬正文,不碰「创建 / 取消」这对授权面)。其中 U33「换槽位」那两条把**旧的槽位读数逐字留在案上**:修复前 `question` = **1068 码点 / 14 换行**(正是「长文放在 header」的形状),而 `detail` 只有 **63 码点的一行话** —— 换槽位把这两个数整个对调了。 - 客户端侧 22 条里,**U32 的 10 条中 8 条红**;另 2 条是**负相**断言(「不认别的类型」「状态不可读时不抛错」)—— 它们断言的是**缺席**,在没有该 definition 的构建上**本来就会绿**,如实记,**不算**「修复前已通过」。 - **修复后全绿**:`node host-half.test.mjs` → `assertion total: 951 (failed: 0)`;`node client-half.test.mjs` → `assertion total: 269 (failed: 0)`。新增断言 **24 条宿主 + 10 条客户端**(`951 − 927 = 24`、`269 − 259 = 10`,基线取 0.3.9 批次 4 收口时的两套件自报值,**不按「上一版 ± 本轮新增」反推**)。 - **U34 是回归守卫(§10.2.8.6)**:`/team_rotate` 的位置参数(角色名)**改前改后都应为绿** —— 隔离夹具里两条 U34 断言**当场全绿**(与 §11.2 的既有文法面同批复跑)。**红相另做单点变异**「把新文法搬到 `/team_rotate` 的解析器上」(`if (value.role !== undefined) continue;`)⇒ 套件在 `host-half.test.mjs:4646`(§11.2 的既有文法面)**当场崩**、整轮 30 条 FAIL —— **守卫是活的**。 - **红线回归**:`lib/` 内 `ctx.session`(**单数**)/ `appendEvent` / `session.log` / `.append(` **仍全部为 0**;模块级 `inject` **仍恰 4 项**(`sessionReferenceResolver / tools / sessionQuery / agents`);投递门与 `source` 三成员语义未动;**未新增任何日志事件类型**。 - **同批改写(不是删除)的既有断言**: - 宿主侧 —— `input.hint` 那条(补「不得再有位置参数写角色名」)· 三条**位置角色名**断言(§10.2.8.2 的语义变更,改为「裸 token 归正文、`roles` 只由 `roles=` 声明」)· 「U16 端到端文法」那一组(输入行在新文法下读法不同,两条改写成 U30 的判据、一条改为 `roles=` 声明)· 以及 **5 处披露文案断言只把取值槽从 `question` 换成 `detail`**(**断言文本逐字不动** —— 换槽位把正文搬进了 `detail`,断言若还读 `question` 就是在测一个已经不承载正文的槽位)。 - 客户端侧 —— 3 条 chat-node 槽位计数(2→3,新增 §10.2.8.3 通道 1 的渲染面)· 1 条「注册被拒只花顶层卡」的 warn 计数(1→2,两个 definition 各自留一行具名 console warn)· 若干 `fresh.length`(5→6)与 `names()` 清单 · `applyWith` 的 definition 计数改成**按 kind**(新增 `definitionCallsOf(kind)`,让「某一张面照常注册」的断言不再被另一张面合法地移动)。 ### 📝 如实标注(设计未逐字规定 / 与实现现状有出入的地方) - **原来那句「本命令是 §10.2.4 的批量动作:…」被**替换**,不是被删掉**:按修法表第 0 行,`detail` 现在整段承载披露正文;该句描述的批量范围由**必备披露块**「确认则:创建 → followup 投递启动任务 → 登记 roster 与 pairs;取消则零创建、零 pairs。」承接。**为什么两全不了(实测)**:正文预算在常规用例下已是 **591 / 600 码点**(另有 9 / 12 换行;**2026-09-22 修正批**把 id 形状行补回之后由 578 升到 591,见本条目末节),任何**必备**块的增加都会挤掉可选的自述块 —— 实测会先掉 DEFECT-4 的标题段,进而让「确认框说明设了什么标题」那条既有断言变红。 - **`question` 未加 `intent: plan-review`**:设计把它列为**可选**(「不必强求」),本批未启用 —— 壳的 plan-review 是一套**接管式**呈现面,为一句确认换面不划算。 - **逐字段裁剪只用省略号、不带计数**(只有**段落级**裁剪带「已裁剪至 …」标注):给每个封顶字段加计数会让每处多约 8 码点,实测足以顶破 600 并挤掉上述标题段;字段级裁剪的诚实性由「省略号 + 段落级标注」共同承担。 - **通道 1 的实机观察项**:壳可能**本来就**画出自己的 `kind:"command"` 通用行(`dsh-client-ui-chat` 也注册了一个),而本插件**无法查询「壳渲染了什么」** ⇒ 本批按设计实现通道 1 自带的顶层节点,**「是否与壳的通用行并存」留作真机观察项**。 - **未做真机验证**:本批全部读数来自两套单元套件 + 隔离夹具的红相;宿主半边要等 DSH 重启窗口,§10.2.8.3 的客户端节点要重建客户端 bundle + 刷新页面,**U33 的「渲染高度不超视口」依赖窗口宽度、刻意不作单测断言**。 - **`README.md` 的**两处**读数已由架构侧同批同步**(tests 徽章 → `951 + 269`、第 863 行「当前读数」→ `951` / `269`)✓。**原记「三处」是计数不实**(2026-09-22 只读审计第 7 条纠正):被点名的第三处「第 880 行索引里的『判据 U30–U34,未落码』」**从未进过历史** —— `git log -S 未落码 --all` 只命中 `ee7c48a` 的**提交信息**一句,`git show ee7c48a^:README.md` 里 grep「未落码」**零命中** ⇒ 不存在一处「未同步的旧读数」,那句描述已删掉。 ### 🔁 同批同步的面 `lib/index.js`(解析器 R1/R2/R3 与三个默认值、`teamSessionDialogQuestion`、`askTeamSessionBatch` 的两个槽位、`TEAM_SESSION_KEY_HINT` / `TEAM_SESSION_BODY_HINT`、命令 description 与 `input.hint`);`lib/client.js`(§10.2.8.3 通道 1 的独立 definition + 渲染面);`host-half.test.mjs` / `client-half.test.mjs`(新增 U30–U34 与上述改写);本条目;`docs/verification-log.md`。 ### 🔧 2026-09-22 修正批 —— 只读发散审计 + 代码评审的 **7 条**偏差收口 起因:一次**只读发散审计**(对 §10.2.8 的交付逐条对照)与一次**代码评审**各报出偏差,**去重后 7 条**。本批只修这 7 条,别处一字不动;其中**只有第 3 条是批次行为变更**,其余是渲染面(2 / 4)、断言(1)、注释(7)与文档读数(5 / 6)。 1. **审计 #3 / §10.2.8.4 (b) 的「只给 id 形状 + 计数」零实现**:修法表第 0 行换槽位时,旧实现那行 `- 会话 id:team-link---`(`teamSessionIdPrefix()`,`git show ee7c48a^:lib/index.js`)**连同逐角色 id 清单一起被删掉**,于是「id 形状」在框里一处不剩。修:新增 `TEAM_SESSION_ID_SHAPE` 常量,把**形状 + 总数**并进「会话」那一行(**不逐角色展开**照旧:仍只列前 3 个 +「…等 N 个」,全量清单仍只出现在完成回报里)。预算 **+35 码点**,按 §10.2.8.4 (b)「压缩只压自述段」从**可选**的 DEFECT-4 自述行里扣(删掉行内 `(DEFECT-4)` 标记与半句冗余),**必备披露一字未动**。 2. **评审 #1 / 审计 #2 的收尾:「同现 ⇒ 报错」两套件零断言**:§10.2.8.2 那句「正文与 `task=` 同现 ⇒ 报错」经**只读审计与代码评审两次独立认定可达**(反例 `team=t task=a model=m b`:`task=` 被后续已知 `key=` 截断、其后裸 token 走 R1(a) 成正文),而修法表第 0 行换槽位之后这条行为**零断言**。修:**行为一字不改**,只补两条断言 —— 反例 ⇒ 报「任务只能给一处」并点名正文起点 `b`;引号形 `task="q" 正文` ⇒ 同样被拒。 3. **审计 #4 / §10.2.8.2 默认值第 4 条:「既无正文也无 `task=`」却仍驱动会话**:`plan.task === undefined` 时实现仍然 `followup` 一具 kickoff(= 插件替人类编了一条他没写的任务)。修:该支**不投启动任务**(drive 循环只在有任务时跑),确认框与完成回报的措辞同批改准(框里「未给正文/task=,只建会话、不投启动任务」、回报里「已创建(未给正文/task=,不投启动任务)」)。 4. **评审 #2 / U33 的 600 码点被顶破**:`model=` / `provider=` 由用户给、解析器只定形状**不定长度**,而 `teamSessionModelLine` 把它们**原样内插** ⇒ 一句 300 字的 `model=` 就把 `detail` 顶到 **789 码点**(红相实测)。修:两者并入 `TEAM_SESSION_DIALOG_FIELD_CHARS`(`model: 40` / `provider: 24`),与团队名 / cwd / 会话 id 走**同一封顶**。 5. **审计 #6 / 红相逐条读数不实**:原记「U30(9 条)/ U31(4 条)/ U33(9 条)全红」,同夹具实跑的分布是 **U30 7/9 · U31 4/4 · U33 8/9**(+ 改写的既有 U16 10 条 + DEFECT-4 槽位 1 条 = **30**);**旧实现上本来就绿的有 3 条**,已在本条目上一条与 `docs/verification-log.md` 里逐条点名。**红相总数 30 / 22 不变。** 6. **审计 #7 /「README 三处未同步」实为两处**:第三处(第 880 行「判据 U30–U34,未落码」)**从未进过历史**(`git log -S 未落码 --all` 只命中 `ee7c48a` 的提交信息;`ee7c48a^:README.md` 里 grep「未落码」零命中)⇒ 计数改为**两处**,不存在的第三处描述删除。 7. **评审 #4(低)/ 注释失真**:`TEAM_SESSION_KEYS` 的注释称「**按 hint 列出的顺序**」,而数组顺序(`n, count, team, roles, role, task, preset, model`)与 `TEAM_SESSION_KEY_HINT` 的书写顺序(`n / team / roles / preset / model / task`)并不一致 ⇒ **改注释措辞**(不改数组、不改行为)。 **怎么验证(变异验证,本批)**: - **修复前必红**(隔离夹具 `.test-tmp-head/`:**本批的新测试** + `git show HEAD:lib/{index,client}.js` + 本树 `README.md` / `package.json`,在夹具目录里跑):`node host-half.test.mjs` → **`957 (failed: 4)`**,4 条逐字点名 —— **U33 id 形状在场**(`hasShape:false`)· **U33 长 model= 封顶**(`{cp:789, nl:9}`,评审 #2 那句)· **U30 默认值第 4 条**(`{creates:2, followups:[1,1]}`,审计 #4 那句)· **U30 默认值第 4 条(措辞同步)**。`node client-half.test.mjs` → **`269 (failed: 0)`**(本批未触浏览器半边)。 - **修复后全绿**:`node host-half.test.mjs` → **`957 (failed: 0)`**;`node client-half.test.mjs` → **`269 (failed: 0)`**。新增 **6** 条宿主断言(id 形状 1 + 同现 2 + 默认值第 4 条 2 + 长 model= 1),基线 `951`(`957 − 951 = 6`)。 - **两条「同现」断言是活锁(单点变异)**:撤掉「任务只能给一处」那处拒绝(`if (value.task !== undefined) {` → `if (false) {`)⇒ **`957 (failed: 1)`**,**恰是**那条新断言 —— 整轮 957 条里**没有第二条**断言发现这件事(= 修前它对这条行为确实零覆盖);撤掉引号形的「闭引号之后不许再有内容」⇒ 套件在 `host-half.test.mjs:5070`(既有断言,该处读 `.error.includes` 没有 Y7 类型护栏)**当场崩**、连 `assertion total` 都不打印 ⇒ 该分支本来就被既有断言咬住,本批那条是**第二把锁**。 - **U33 重测读数(本批,逐用例,全部 9 / 12 换行)**:常规 **591 / 600 码点**、8 会话 + 3000 字正文 **577**、长 cwd **593**、39 字团队名 **534**、团队名与 cwd 都长 **585**、DEFECT-4 槽位用例 **596**、无正文无 `task=` **563**、**300 字 `model=` 564**;`question` 一律单行(27 / 37 / 38 / 46 码点,上限 120)。 - **夹具输入的同批调整(判据文本一字未改)**:五具**原本既无正文也无 `task=`** 的夹具(DEFECT-1 降级 / DEFECT-2 降级 / DEFECT-4 服务缺席降级 / DEFECT-4 rename 抛错 / U17 保留)补上了 `task=` —— 它们断言的是「缺服务时**照建照驱动**」,而新默认下这一支**不再驱动**;补 `task=` 是**保住原有覆盖**,不是把断言改软(新默认自己有默认值第 4 条那两条断言)。 **如实标注(本批)**: - **必备披露块在最极端的组合下仍可顶破 600 码点(设计缺口,可复现)**:per-field caps 那套「保证**每一个**输入都装得下」的说法**修前就已经不成立** —— 实测(20 字团队名 + 8 个已登记角色 + 300 字 `model=` + 60 字 `preset=` + 3000 字正文 + 长 cwd + 60 字协调者 id)**修前 633 码点**,补回 id 形状行之后 **668 码点**,而 `dialogFits` 的裁剪**只丢可选自述段**(设计明令必备披露永不裁剪)⇒ 这一组合下 `detail` 必然超预算。本批**不自行发明**「裁剪必备块」的机制(那是设计面的事),如实记为**残余**;U33 的单测覆盖的是上面枚举的那批用例(最大 596)。 - **本批未改设计档**(`docs/collab-enhancements-design-2026-09-19.md` 不在本批文件清单内):§10.2.8.2 默认值第 4 条与 §10.2.8.4 (b) 本来就是本批实现的依据,**没有需要改写的设计语句**;唯一与设计现状有出入的是上一条残余(设计未规定「必备块超预算」的处置)。 **同批同步的面(本批)**:`lib/index.js`(`TEAM_SESSION_ID_SHAPE`、会话行的 id 形状、`teamSessionModelLine` 的两个封顶、`TEAM_SESSION_DIALOG_FIELD_CHARS` 的两个新键、`teamSessionTaskNote` 与「确认则」两支措辞、drive 循环的任务守卫、完成回报的第三种结果、`TEAM_SESSION_KEYS` 注释);`host-half.test.mjs`(6 条新断言 + 5 具夹具补 `task=`);本条目末节;`docs/verification-log.md`(§10.2.8 一节的逐条读数与新增的本批小节)。 ### 🔧 2026-09-22 收口批 —— 交付代码评审第 2 轮 PASS 后的 **3 条**小项(🟡#1 · 🔵#3 · 🔵#2) 起因:交付代码评审第 2 轮(窄范围:修复轮新增约 63 行)**PASS**,余下三条小项都不涉及结构。本批**只收这 3 条**,别处一字不动:改动面 = 对话框的**一处文本分支** + **一个 per-field 上限键** + **一处注释**。设计依据同批补进 `docs/collab-enhancements-design-2026-09-19.md` —— **§10.2.8.2 默认值第 4 条**下的「同支纪律」(🟡#1)· **§10.2.8.4 (b)** 的 per-field 上限清单(`preset 24`,🔵#3)· **§10.2.8.6** 的「无任务分支保留为防御」(🔵#2)。 1. **🟡#1 / 「成本口径(保守)」那行与同一张框的承诺自相矛盾**:`teamSessionDialogText` 无条件写「成本口径(保守):N 个会话 × 至少一个完整回合(**followup 驱动一次**,按各自模型计费)。」,而同一张框的「确认则」上一批已按 `plan.task === undefined` 分支化(无任务支写「不投启动任务」)⇒ 一面说**不投启动任务**、一面说 **followup 驱动一次**,后者会被读成「已经派活了」。修:成本行**按同一支**改写 —— 无任务支为「成本口径(保守):本次 0 次驱动(未给正文/task=,不投启动任务;后续派活时按各自模型计费)。」,有任务支**一字不改**。判据新增在 U30 的「措辞同步」组里(`host-half.test.mjs`),并且**两侧都咬**(有任务支必须保留原意)——见下「变异 A2」。 2. **🔵#3 / `preset=` 复用角色的 8 码点上限**:`TEAM_SESSION_DIALOG_FIELD_CHARS` 没有 `preset` 键,对话框那一行读 `cap.role` ⇒ 常见 preset id 被封成 `claude-c…` 这类认不出的形状,而「模型/预设」行是 §10.2.4 的**必备披露**。修:`preset: 24` 入表(与 `provider` 同级),那一行改读 `cap.preset`。 3. **🔵#2 / kickoff 的无任务分支是防御性死分支**:`teamSessionKickoffText` 里 `perRole === undefined` 那一支对 `/team_session` **不可达** —— 唯一的驱动路径在 drive 循环处就按 `plan.task === undefined ? [] : created` 把无任务那一支整个挡掉(as-of 本批 `lib/index.js:5122`);该函数另有导出面(`__testing.teamSessionKickoffText`,as-of `:8915`,仅供测试读数),不经那道守卫。**行为一字不改**,只在函数体首行加注释写明「不可达 + 守卫在哪 + 为什么保留」:将来若再加一条驱动路径(或有人挪走/放宽那道守卫),少了这一行就会让 kickoff **静默丢掉「任务」行**。 **怎么验证(变异验证,本批)**: - **修复前必红**(隔离夹具 `.test-tmp-r3pre/`:本批的新测试 + `git show HEAD:lib/{index,client}.js` + 本树的 `README.md` / `package.json`,在夹具目录里跑):`node host-half.test.mjs` → **`959 (failed: 2)`**,恰是本批的**两条**新断言,逐字点名 —— **U33 长 preset= 封顶**(`{"cp":595,"nl":9,"presetLine":"… · preset=ppppppp…"}`:60 字取值被封成 **7 字 + 省略号**,即复用了角色的 8 码点)· **U30 默认值第 4 条(措辞同步 · 成本行)**(`{"noTaskCostLine":"成本口径(保守):2 个会话 × 至少一个完整回合(followup 驱动一次,按各自模型计费)。",…,"cp":563,"nl":9}`)。既有 957 条在夹具里全绿 ⇒ 两条新断言是**加锁**,不是改写既有判据。 - **修复后全绿**:`node host-half.test.mjs` → **`959 (failed: 0)`**;`node client-half.test.mjs` → **`269 (failed: 0)`**(本批未触浏览器半边)。新增 **2** 条宿主断言(`959 − 957 = 2`,基线取上一批自报的 957)。 - **两条新断言都是活锁(单点变异,三具)**:**A(🟡#1 回退:成本行改回无条件那一句)** ⇒ **`959 (failed: 1)`**,**恰是**新的成本行断言(整轮 959 条里没有第二条发现这件事)· **A2(假修法:把「followup 驱动一次」半句在**两支上**都抹掉)** ⇒ **`959 (failed: 2)`**,新断言仍在列(它**两侧都咬**:有任务支必须保留原意),另带一条既有断言(U16 确认框的成本口径)⇒ 这条锁住的是「**按支写准**」,不是「把话删掉」· **B(🔵#3 回退:`cap.preset` → `cap.role`)** ⇒ **`959 (failed: 1)`**,**恰是**新的 preset 断言。三具变异逐字回退后均复跑 `959 (failed: 0)`。 - **`preset` 上限直接量到**:60 字取值 ⇒ 框里 `preset=` 后是 **23 字 + 省略号**(上限 24),不再是上一批的 **7 字 + 省略号**(上限 8)。 - **U33 重测读数(本批,逐用例,`detail` 码点 / 换行)**:常规 **591/600**(9/12)· 8 会话 + 3000 字正文 **577** · 39 字团队名 **535** · 110 字 cwd **594** · 两者都长 **585** · DEFECT-4 槽位用例 **597** · 既无正文也无 `task=` **564** · 300 字 `model=` **564** · **60 字 `preset=` 543(本轮新增用例;自述段被裁剪、标注在场)**。**无任务支那几条各 +1 码点**(成本行 49 → 50:改写后的措辞比原句更短,抵消了「0 次驱动」与「不投启动任务」两处新信息)。 - **同支纪律的代价(实测,如实)**:改成同支之后,无任务支的成本行**只许长到 53 码点** —— DEFECT-4 槽位用例(`n=2 team=title-dialog roles=worker-a,worker-b`,正是无任务支)的交付预算是 **597/600**,余量 3 码点。第一稿照父侧示例写满「后续由主会话派活时按各自模型计费」(59 码点)会让**正文(必备块 + 标题段)**达到 **606 > 600** ⇒ **裁剪丢掉可选的自述段**、让既有的「DEFECT-4 ② 确认框说明设了什么标题」断言当场变红(本批实测复现)。故取**同义更短**的措辞(50 码点,净 +1):既守住「不得自相矛盾」这条判据,**又不用**改那条既有断言、也不用给它补 `task=`。 - **红线回归(本批)**:`lib/` 内 `ctx.session`(**单数**)/ `appendEvent` / `session.log` / `.append(` 仍各 **0** 命中;模块级 `inject` 仍恰 **4** 项;既有 schema key、投递门与 `source` 三成员、`/team_rotate` 文法面、浏览器半边(`lib/client.js` 一字未改)**均未触碰**;**未新增任何日志事件类型**。 **同批同步的面(本批)**:`lib/index.js`(成本行的两支、`TEAM_SESSION_DIALOG_FIELD_CHARS` 的 `preset: 24` 与那一行的 `cap.preset`、`teamSessionKickoffText` 的防御性注释);`host-half.test.mjs`(2 条新断言);`README.md`(tests 徽章与「当前读数」);`docs/collab-enhancements-design-2026-09-19.md`(§10.2.8.6 那条的两处行号按本批 as-of 更新);`docs/verification-log.md`(本批小节);本条目末节。 **如实标注(本批)**: - **`README.md` 的两个读数此前落后一批**:0.3.10 修正批把套件自报值推到 **957**(`951 + 6`),但那一批的「同批同步的面」里**没有 README**(只列了 `lib/index.js` / `host-half.test.mjs` / 本条目末节 / `docs/verification-log.md`)⇒ README 停在**上上批**的 `951`。本批按实测把徽章与「当前读数」一并改成 **`959`**(`957 + 2`,本批新增 2 条),并把这次**跨越**写在这里 —— 不要读成「本批新增 8 条」。 - **无任务支成本行的具体措辞属实现定夺**:设计只给判据(不得自相矛盾、须与「确认则」同支),未逐字规定文案;本批取「本次 0 次驱动(未给正文/task=,不投启动任务;后续派活时按各自模型计费)。」,长度受上一条实测约束(≤ 53 码点)。 - **本批未做真机验证**(与前两批同口径):全部读数来自两套单元套件与隔离夹具;本批两处改动都只影响**确认框文本**,宿主半边仍要等 DSH 重启窗口才在真机生效。 - **本批未动**「裁剪标注不参与预算」那条已知残余(§10.2.8.4 残余行的「第二机制」):`teamSessionDialogText` 的裁剪/标注逻辑与 `TEAM_SESSION_DIALOG_MAX_*` 两个常量**一字未改**,该残余仍待裁。 ### 🔧 2026-09-22 会诊 #68 批 —— 裁剪标注不参与预算(U33 口径定档:**交付 `detail`**) 起因:上一批的**如实标注**里记下的「第二机制」残余 —— **裁剪标注行不参与 600/12 预算**(父侧独立实测,两轮评审均未发现)。会诊 **#68**(四份意见一致)已裁定修法,父侧把裁定写进设计档 **§10.2.8.4 (b) 的 (b) 行 / 残余行**与 **§10.4 的 U33 行**(三处,**本批不改设计档**,以那三处为唯一规格)。改动面 = 对话框终态的**构造**(一处)+ 标注的**文案与档位**(一处)+ 一处注释;`TEAM_SESSION_DIALOG_MAX_CODE_POINTS` / `MAX_NEWLINES` 的**数值一字未动**,**必备披露块一字未裁**。 1. **修了什么(结构:终态真过预算)**:被弃路径的终态此前是 `let text = candidate(kept, dropped)`(as-of `0f77d0a`:`lib/index.js:4754`)—— body 装进 600/12 **之后**才贴上标注,**不再过 `dialogFits`** ⇒ 交付 = 必备 body + 标注(旧文案 **41 码点 + 1 换行**)⇒ **凡 body > 558 即溢出**(与其内容无关,故不靠「某个输入的读数」立论)。修:**拟合循环与终态走同一个构造** —— `candidate(list, stillDropped)` 现在**自己**带预算判定(在档位表上 `find` 第一个装得进 `dialogFits` 的档,取不到就用最窄那一档),终态就是调它,于是「装得下」这句话说的是**交付串**(含标注),不是标价前的正文。 2. **修了什么(标注档位化)**:两档写成**具名常量** `TEAM_SESSION_DIALOG_CROP_NOTE_FULL` / `TEAM_SESSION_DIALOG_CROP_NOTE_MINIMAL`(由 `__testing.TEAM_SESSION_DIALOG_CROP_NOTE_TIERS` 导出,供单测直接量长度),模板按**实际 `dropped` 位数**算 —— 一位数时**全档 21 码点**、**最小档 11 码点**(各另加 1 换行),两位数各 +1,代码里没有写死的 21/11。选择策略 = **取能装进 `dialogFits` 的最宽档**;**两档都装不下时仍挂最小档交付** —— 那是**如实标注的受限超额**,且此时超额**只**由不可裁的必备 body 引起(机制一残余)。**任何档位都不再写「已裁剪至 600 码点 / 12 行上限」**:交付串可能正是超 600 的那一条,那句话**自证矛盾**。 3. **改掉一句假注释**:`teamSessionDialogText` 的 docstring 曾称那组 per-field caps 「is what makes the required set fit the budget for **EVERY** input」—— 实测反例(body 693)已证伪 ⇒ 改成**如实口径**(caps 让**常规与已验证的极端**装得下,**不是**对任意输入成立的不变式),并指回设计档 **§10.2.8.4 的残余行(第一机制)**。同一句话说法的**另两处**同批改准(`TEAM_SESSION_DIALOG_FIELD_CHARS` 的注释、`host-half.test.mjs` 里引用该理由的注释)—— **三处由同一条事实驱动**。 **为什么**(会诊 #68 的裁定理由,父侧核实):600 的用途是「**人读到的那一行有界**」,而标注**自己就写着 600** ⇒ **标注必须计价**;不修的话,「合法输入 ⇒ 交付超 600」与「框里写着已裁剪至 600 码点」并存 —— **输出物自述与事实相反**。不变量收敛为一条:**交付 ≤ max(600, 必备 body + 12)**(12 = 最小档 11 + 1 换行)——「**标注永不是首个破约者**」。 **怎么验证(变异验证)**: - **修复前必红**(隔离夹具 `.test-tmp-u33pre/`:**本批的新测试** + `git show HEAD:lib/{index,client}.js` + 本树 `README.md` / `package.json`;`HEAD` = `8991fc8`,夹具里的 `lib/index.js` 与 `git show HEAD:lib/index.js` **逐字节相同**已核对):`node host-half.test.mjs` → **`964 (failed: 6)`**,六条逐字点名 —— 改写后的既有 **U33 长团队名裁剪标注**(`{"cp":535,"nl":9,"note":false,"contradictory":true}`:新文案不在、旧文案还在)· 改写后的既有 **U33 两者都长**(`{"cp":585,"nl":9}`)· 新的 **U33 标注档位**(`{"tiers":0,…}`:旧树上这两个常量**根本没导出**)· 新的 **U33 (i)**(`{"bodyCp":582,"cp":624,"nl":9}`)· 新的 **U33 (ii)**(`{"bodyCp":592,"cp":634,"nl":9}`)· 新的 **U33 (iii)**(`{"bodyCp":693,"cp":735,"nl":9}`)。`node client-half.test.mjs` → **`269 (failed: 0)`**(本批未触浏览器半边)。 - **其中一条新断言在旧树上本来就绿(如实记,沿用修正批的同一口径)**:**U33 (iii) 机制一的成因可读** —— 它量的是「3 个角色已登记 ⇒ `(已登记,跳过)` 后缀 + 必备披露在场」这件**既有行为**,不是本批引入的语义。 - **修复后全绿**:`node host-half.test.mjs` → **`964 (failed: 0)`**;`node client-half.test.mjs` → **`269 (failed: 0)`**。新增 **5** 条宿主断言(`964 − 959 = 5`,基线取上一批自报的 959);**改写 2 条既有断言**(强度只增:标注在场由「逐字等于**全档常量**的产物」咬住 + 两个预算仍然都断言 + 新增「不含那句自证矛盾的话」)。 - **两条单点变异(`(i)` 必须红,逐条读数)**: - **(a) 去掉终态的 `dialogFits` 检查**(`notes.find((note) => dialogFits(note)) ?? notes[notes.length - 1]` → `notes[0]`)⇒ **`964 (failed: 3)`**,**恰是 (i)** `{"bodyCp":582,"cp":604,"minimalTier":false}` **/ (ii)** `{"bodyCp":592,"cp":614}` **/ (iii)** `{"bodyCp":693,"cp":715}` —— **(i) 当场变红** ✓,整轮没有第四条断言发现这件事。 - **(b) 只留全档(删掉最小档回退)**(档位表只剩 `…CROP_NOTE_FULL`)⇒ **`964 (failed: 4)`**,除上述三条外**多红一条**:新的 **U33 标注档位**(`{"tiers":1,…}`)。**两具变异逐字回退后均复跑 `964 (failed: 0)`。** - **U33 逐用例重测(本批,读数取自交付串 `detail`,码点 / 600,换行 / 12)**: | 用例 | 本批 `detail` | 上一批(收口批) | 变动 | | --- | --- | --- | --- | | 常规 `n=2 team=night-shift roles=worker-a,worker-b task=做接口` | **591**,9 | 591 | 0(自述段全装下 ⇒ 本来就无标注)| | 8 会话 + 3000 字正文 | **557**,9 | 577 | −20(全档 22 取代旧标注 42)| | 39 字团队名 | **515**,9 | 535 | −20 | | 110 字 cwd | **594**,9 | 594 | 0(无标注)| | 团队名与 cwd 都长 + 8 会话 + 500 字正文 | **565**,9 | 585 | −20 | | DEFECT-4 槽位 `n=2 team=title-dialog roles=worker-a,worker-b` | **597**,9 | 597 | 0(无标注)| | 既无正文也无 `task=`(`team=t n=2`) | **564**,9 | 564 | 0(无标注)| | 300 字 `model=` | **544**,9 | 564 | −20 | | 60 字 `preset=` | **523**,9 | 543 | −20 | | **(i) 装得下的最坏夹具(必备 body 582)** | **594**,9(**最小档**:全档 604 装不下)| — | 新增(改动前 **624** ✗)| | **(ii) 带内夹具(必备 body 592)** | **604**,9(= 必备 body + 12)| — | 新增(改动前 **634** ✗)| | **(iii) 机制一夹具(必备 body 693)** | **705**,9(= 必备 body + 12)| — | 新增(改动前 **735** ✗)| **超 600 的两行与为什么**:**(ii) 604** —— 必备 body 592 已落在 (588, 600] 这条带里,600 − 588 = 12 的余量装不下 21 码点的全档 ⇒ 交付**最多**超 **body + 12 − 600 = 4**,且**必带标注**(这正是会诊 #68 定下的那条不变式)✓;**(iii) 705** —— 机制一残余:**必备块本身就 693**,而它**永不裁剪** ⇒ 超额**只**由必备 body 引起(设计档 §10.2.8.4 残余行 · 第一机制),本批**不**发明「裁剪必备块」的机制。其余十具**全部 ≤ 600**。**`question` 一律单行**(本批 27–46 码点,上限 120)。 - **红线回归(本批)**:`lib/` 内 `ctx.session`(**单数**)/`appendEvent`/`session.log`/`.append(` 仍各 **0** 命中;`sessionQuery` **8**、`commands.register` **3**(与上一批同读数);模块级 `inject` 仍恰 **4** 项;**未新增任何日志事件类型**;既有 schema key、投递门与 `source` 三成员、`/team_rotate` 文法面、浏览器半边(`lib/client.js` **一字未改**)**均未触碰**。 **如实标注(本批)**: - **历史读数不改**:本条目上两节(修正批 / 收口批)的 U33 读数表是**同一棵树不同时刻**的读数,按本仓「只追加、不重写历史」的纪律**一字未动**;凡**标注在场**的用例本轮各 **−20** 码点(41 → 21),**无标注**的用例**一字未变** —— 全量新表见本节「**U33 逐用例重测(本批,读数取自交付串 `detail`…)**」那张表。 - **(b) 行的「取最宽档」与残余行的「两档都装不下仍挂最小档」并不冲突**:前者的「装得下」指 `dialogFits`,后者是**两个承诺(有界 / 必带标注)不能同时成立**时选了「标注在场」(会诊 #68 的裁定),代价是**如实标注的超额**。 - **本批未做真机验证**(与前三批同口径):全部读数来自两套单元套件与上述隔离夹具;本批只改**确认框文本的构造**,宿主半边仍要等 DSH 重启窗口才在真机生效;「渲染高度不超视口」照旧是设计显式降级的**真机项**。 - **本批未改设计档**:`docs/collab-enhancements-design-2026-09-19.md` 不在本批文件清单内 —— §10.2.8.4 (b) / 残余行与 §10.4 的 U33 行是**父侧刚写的规格**,本批逐字实现它。 **同批同步的面(本批)**:`lib/index.js`(终态构造 `candidate` 的预算判定、标注两档的具名常量与 `teamSessionDialogCropNote(dropped, tier)`、`__testing` 的档位导出、三处注释口径);`host-half.test.mjs`(5 条新断言 + 2 条既有断言的文案与强度 + 一处理由注释);`README.md`(tests 徽章与「当前读数」);`docs/verification-log.md`(本批小节:夹具、病灶 `lib/index.js:4754`(as-of `0f77d0a`)、变异表、U33 逐用例重测表、红线复测);本条目末节。 --- ### 🔧 2026-09-22 裁定 A 批 —— 确认框收紧(U33c:参照夹具 **≤6 行 / ≤350 码点**) > **判据后于同日放宽到 ≤380**(偏差修复轮把有任务支被漏删的「按各自模型计费」放回,+8 码点):本节读数按「只追加、不重写历史」原样保留,凡与修复轮读数不一致处以下一节的「偏差修复轮」为准(参照夹具复算 **353 / 6 行**、28 码点 id **369 / 6 行**、机制一 **615**)。 **设计(唯一事实源)**:`docs/collab-enhancements-design-2026-09-19.md` **§10.2.8.4 修法表 (c) 行的「已裁(2026-09-22 用户裁定 = A)」**(父侧刚写入,本批**不碰设计档**)· 判据 **§10.4 U33** · 同表 §10.2.8.7 裁定 2(角色指引行)。逐条红相/绿相读数见 `docs/verification-log.md` 的「§10.2.8 裁定 A 批」一节。 **① 改了什么**(三处,全在本插件;宿主半边 ⇒ 需 DSH 重启才在真机生效):**确认框正文从 10 行收到 6 行、参照夹具 569 → 345 码点**。 - **「- 会话标题:…」自述段整段移出框体**(它本来就是框里唯一的 `required: false` 段)。**读数没有丢**:它落在**完成回报**的 `- 标题(DEFECT-4):…` 那一行(该行**改前就在**、本批一字未动)。 - **解释性括号与 § 号压掉**(用户点名要压的那几处,逐条见下表):「(绕过发送方审批与接收方 ask 两道门,§10.2.3 预置配对)」「(保守)」「未给 model=/provider ⇒ 两半都解析并带上宿主缺省模型选择」「不设标题时它们会全都显示为工作区名;想改随时在壳里重命名」,另加两处同类的字数:`工作目录(cwd):` → `cwd:`(同义 gloss)、**没被截断时**不再把「完整正文会原样作为启动任务投递」重复一遍(它已经显示在眼前)。 - **九件必备事实落进六行**(不是删事实,是并同族):① 数量 + 角色与 id 形状 · ② 模型/预设 + cwd · ③ 启动任务 · ④ 角色指引(**单独一行**,§10.2.8.7 裁定 2 要求「各加一行」)· ⑤ 成本口径 + 信任授予 · ⑥ 确认则。**§10.2.4 必备五件(数量 / 模型 / cwd / 成本 / 信任授予)一件不少**,`共 N 字` 标注、确认则两支、id 形状与计数全部在场(逐件点名断言,见下)。 | 位置 | 改前 | 改后 | | --- | --- | --- | | 标题自述段 | `- 会话标题:worker-1(标题:…)——不设标题时它们会全都显示为工作区名;想改随时在壳里重命名。` | **整段移出**(完成回报承载同一读数)| | 数量 / id 形状 | 两行(`将创建 N 个…。` + `- 会话 id:<形状>(共 N 个):…`) | **一行** | | 模型 / cwd | 两行,模型行含教学式说明 | **一行**:`- 模型/预设:宿主缺省;cwd:…` | | 成本 / 信任授予 | 两行,含「(保守)」与「§10.2.3 预置配对」 | **一行**,虚词压掉、事实保留 | | 角色指引 / 确认则 | 两行 | **两行**(角色指引单独一行不变)| **② 为什么**:真机上确认框**动作可达**(缺陷③的原始症状未复现 ✓),但用户实测判「**有点大,文字也太多了,导致选项被积压**」✗ —— 同一输入的 `detail` 实测 **569 码点 / 10 行**(用户截图那次协调者 id 取满 28 码点 ⇒ **585**),用满 600 预算。用户裁定**方案 A**(收方向,不是只移标题段、也不是把披露全搬进回报)。 **③ 怎么验证**(两套件实测 `966 (failed: 0)` / `269 (failed: 0)`;基线 `964` / `269`): - **红相**(隔离夹具 `.test-tmp-u33a/`:本批新测试 + `git show HEAD:lib/index.js`):`966 (failed: 15)`,其中 **U33c 两条**(参照夹具 **569 码点 / 10 行** > 350/6 ⇒ 红;删除面 10 条**逐条全部还在** ⇒ 红)· **U33 必备披露逐件点名**(缺 cwd / 成本 / 信任授予 / 确认则(两支)——旧文案的字面与新判据不同)· **(i)/(ii)/(iii)** 三具夹具(旧实现 594 / 604 / 705)· 以及**改写措辞**影响的既有断言 **9** 条(2 + 1 + 1 + 1 + 1 + 1 + 1 + 1;**计数勘误**:此处原写「8 条」,而右栏与 `.test-red-a.log` 的 15 行 FAIL 都指向 **9** 条,2026-09-22 偏差修复轮同批改准,构成见 `docs/verification-log.md`)。**客户端 269 (failed: 0)**(`lib/client.js` 本批一字未改,红相不该动它)。 - **变异验证两条**(新断言是不是活锁):**(a) 把删掉的解释性文字加回一处**(信任行补回「(绕过发送方审批与接收方 ask 两道门,§10.2.3 预置配对)」)⇒ `966 (failed: 3)`:**U33c 参照夹具当场变红(379 > 350)** + 删除面点名两条 + U16 信任授予那条;**(b) 删掉一件必备披露**(整句「信任授予:…」)⇒ `966 (failed: 3)`:**U33 必备披露逐件点名当场变红,并逐件点名缺的是「信任授予」** + 机制一夹具守卫(body 551 不再 > 600)+ U16 信任授予那条。两具逐字回退后复跑 `966 (failed: 0)` ✓。 - **红线回归**:`lib/` 内 `ctx.session`(**单数**)/ `appendEvent` / `session.log` / `.append(` / `ctx.commands` 仍各 **0** 命中;`sessionQuery` / `commands.register` 仍 **8** / **3**;模块级 `inject` **仍恰 4 项**;`TEAM_SESSION_DIALOG_MAX_CODE_POINTS` / `MAX_NEWLINES` **数值一字未动**(600 / 12);**必备块一字未裁**(裁剪机制只动可选段);既有 schema key / 投递门 / `source` 三成员 / `/team_rotate` 文法面未触碰。 **④ 必须知道的一件事(如实标注,不是本批能决定的)**:移走「会话标题」段之后,框里**再没有** `required: false` 的段 ⇒ `dropped` 恒为 0,`dialogFits` 与两档裁剪标注**当前没有取用路径**(代码按用户裁定的「不许动:…两档标注与终态预算机制」**一字未改**地留着;两档文案的长度与「不含自证矛盾句」仍由一条**纯函数**断言咬住;一旦某段自述回到框里,取用路径立刻恢复)。同批把 U33 (i)/(ii)/(iii) 三具夹具改写成**现在真正成立的形状**:交付 `detail` **逐字等于必备 body**(不裁剪、不追加标注),机制一那一档(必备 body 607 > 600)**仍然必然超线**、超额只由不可裁的必备块引起。**此项已作为「需要父侧决定」的第一条写进交付报告。** **⑤ 同批同步的面(本批)**:`lib/index.js`(`teamSessionDialogText` 的六行构造 + `teamSessionModelLine` / `teamSessionTaskNote` 的措辞 + 五处注释口径);`host-half.test.mjs`(新增 U33c 两条断言 + 必备披露**逐件点名**断言 + 改写 13 条既有断言的文案/形状);`README.md`(tests 徽章与「当前读数」→ `966`);`docs/verification-log.md`(本节);本条目。 ### 🔧 2026-09-22 偏差修复轮 —— 独立发散审计 `DIVERGENCE(5)` 收口 **设计(唯一事实源)**:`docs/collab-enhancements-design-2026-09-19.md` §10.2.8.4 修法表 (c) 行的「落码读数 …… **判据定档**」与「**380 的来由**(审计 DIVERGENCE #4 后定档)」+ §10.4 U33 行末的「裁定 A 落码后的口径更正」(父侧同批写入,本批**不碰设计档**)。**任务书就是偏差清单**:独立发散审计判 `DIVERGENCE(5)` —— 实现面的九个必备事实与裁剪机制**无偏差**,5 条全在读数/计数/规格口径/未申报删字/恒真断言上。逐条红相/绿相读数见 `docs/verification-log.md` 的「§10.2.8 偏差修复轮」一节。 **① 改了什么(五条偏差逐条 + 一项附带)**: 1. **放回一条被漏删的必备事实**(偏差 4,**先修**):有任务支成本行原为「成本:N 个会话 × 至少一个完整回合(followup 驱动一次,**按各自模型计费**)」,裁定 A 落码时把「按各自模型计费」**一并删掉**了 —— 它**不在裁定清单里**,且属**成本口径**(每队各按自己的模型计费;无任务支一直保留着它)。⇒ **放回**(`+8` 码点,行数仍 6)。 2. **判据口径写齐**(偏差 3):设计档定档 **≤ 6 行(硬)且 ≤ 380 码点**,而实现注释与 U33c 断言仍写 ≤350 ⇒ 两处同批写到 **380**,并**如实留下 350 → 380 的放宽与理由**(换回一条必备事实;**不是**「本来就 380」)。 3. **删掉一条恒真断言、换成正面判定**(偏差 5,**选 (a)**):`u33RequiredBodyOf(detail) === detail` 这类「右边从 `detail` 自己算出来」的写法在裁定 A 之后**结构上恒真**(注释却称它防「假绿」)⇒ 包装删除,改对**交付串本身**正面断言「框内**没有**段落级标注」(标注形状行数 = 0),两条同类反锁一并核过。 4. **文档读数复算**(偏差 1)+ **计数勘误**(偏差 2):「全夹具逐用例读数」表有 **7 行**用了 **14 码点 cwd**(真夹具的 cwd 字段封顶 **24**)⇒ 真夹具复算(该 7 行**两列各 +10**);「既有断言 **8** 条」应为 **9**(`docs/verification-log.md` 病灶表与本节 ③ 同批改准,红相 15 的构成 = U33c 2 + 逐件点名 1 + (i)(ii)(iii) 3 + 既有 **9**)。 5. **附带**:`lib/index.js` 里说 `dialogFits`「常规路径上不再被调用」的注释改为如实口径(**任何路径都不被调用**)。 **② 为什么**:这 5 条里有 3 条是**我们自己的账没记准**(读数、计数、规格口径),1 条是**没申报就删了用户要的事实**,1 条是**把恒真式说成守卫**。用户的裁定只授权「压掉解释性文字与整段自述」,**没有**授权删成本口径;判据从 350 放宽到 380 是**有代价的放宽**(换一条事实的回归),必须写明来由而不是悄悄改数。 **③ 怎么验证**(两套件实测 `966 (failed: 0)` / `269 (failed: 0)`,与修复前**同计数**): - **红相**(隔离夹具 `.test-tmp-fix/`:本批新测试 + `git show HEAD:lib/index.js`):**`966 (failed: 3)`**,三条全咬放回的那条事实 —— U33c 参照夹具(成本口径那半句)· U33 必备披露逐件点名(`(缺:["成本"])`)· U30 成本行同支纪律。 - **变异验证两条**:**(A) 把段落级标注人为塞回** ⇒ `966 (failed: 6)`,6 条**全部**点出那行标注(U33 (i)(ii)(iii) · 长团队名 · 两者都长 · U33c 7 行)⇒ **新的正面断言可假、不是恒真** ✓;**(B) 把一处解释性文字加回**(+34 码点)⇒ `966 (failed: 3)`:**U33c `387 > 380`** ✓ + 删除面点名 + U16 信任授予。两具逐字回退后复跑 `966 (failed: 0)` ✓。 - **复算读数(真夹具,24 码点 cwd)**:参照夹具 **353 / 6 行**(12 码点 id)与 **369 / 6 行**(**28 码点 id = 真机那一次**;原 585 / 10 行);机制一残余必备 body **607 → 615**(仍 > 600,永不裁剪)。 - **红线回归**:`ctx.session`(**单数**)/ `appendEvent` / `session.log` / `.append(` 仍各 **0**;`sessionQuery` / `commands.register` 仍 **8** / **3**;模块级 `inject` **仍恰 4 项**;`TEAM_SESSION_DIALOG_MAX_CODE_POINTS` / `MAX_NEWLINES` **数值一字未动**(600 / 12);裁剪机制与必备块**未触碰**;既有 schema key / 投递门 / `source` 三成员 / `/team_rotate` 文法面未触碰;`lib/client.js` 与 `README.md` **一字未改**(断言总数未变 ⇒ 徽章与「当前读数」本就正确)。 - **同批同步的面(本批)**:`lib/index.js`(成本行放回半句 + 四处注释口径:判据定档/380 的来由/块内判据/`dialogFits` 调用面);`host-half.test.mjs`(U33c 判据 350→380 + 放宽理由注释 + 恒真包装换成正面判定 + 成本口径三处判据改写,**断言条数不变**);`docs/verification-log.md`(病灶表计数 8→9 + 复算表 + 新增「偏差修复轮」一节);本条目。 - **如实标注**:**未新增/删除任何断言**(`966` / `269` 不变 ⇒ `README.md` 的两处读数不需要动,也没有动);**未做真机验证**(宿主半边,需 DSH 重启);**未改设计档**。 ### 🆕 §10.2.8.9 / §10.2.8.10 三条新语义落码(2026-09-22 用户裁定) > 设计(唯一事实源):`docs/collab-enhancements-design-2026-09-19.md` **§10.2.8.9**(① 默认团队名带日期 · ② 现任失联则释放并认领团队名)与 **§10.2.8.10**(① 调用方回执 = 通道 4 · ② 无任务也投最小唤醒)。验收落在 `host-half.test.mjs` 的 **U35 / U36 / U37** 与「**U32 通道 4**」两组 + U30 默认值第 4 条的**反转**三条。 **① 改了什么** - **默认团队名带日期(§10.2.8.9 ①)**:`/team_session` 省略 `team=` ⇒ `-YYYYMMDD`(本地时区,**一条命令只取一次**;目录名不合 `[a-z0-9-]+` 回退 `default-YYYYMMDD`)。**孤立后果如实**:**无日期的既有团队仍完全有效**,但只能用**显式 `team=`**(或设置 UI)触达 —— **默认名永不再解析到它们,且不做任何迁移**。`team` 字段封顶 **20 → 32**(带日期的默认名 29 码点必须完整显示)。 - **现任失联 ⇒ 释放并认领(§10.2.8.9 ②)**:**只有确证**「已归档」(`workspaceRegistry.archivedSessionIds` 含它)**或**「已不存在」(`sessionQuery.filterSessions([{kind:"id",values:[…]}])` 返回空)才放行;动作是**同一道命令里**把那一段任期收口(`until=now` + 备注 `released: coordinator archived|gone`)→ `current := null` → 调用会话认领。**保留既有角色与版本史**,`policy.writer` **仍是 `coordinator`**(不放宽成 `any`)。**fail-safe 两情形互斥**:**(甲) 服务未提供** ⇒ 跳过归档信号、只按 `gone` 判;**(乙) 服务在但读不到** ⇒ `unknown` ⇒ **拒绝**。**落笔前复检一次**(现任复活 / 归档态变了 / 现任换人 ⇒ **中止且零写入,连 pairs 也不写**)。**只看现任**:换届换下的旧任归档**不**触发。 - **调用方回执(§10.2.8.10 ①,通道 4)**:命令**结算后**给**调用方会话**投一条**一行 ≤120 码点**的回执(**成功 / 失败 / 取消三态都算**),信封沿用 kickoff 那条**恰三成员**的 relay。**解析前失败**(如 `n=abc`)**不硬塞团队名**。 - **无任务也投最小唤醒(§10.2.8.10 ②,默认值第 4 条反转)**:既无正文也无 `task=` 时**不再「只建会话」**,改投一具**待命通知**(含①「本次命令未给任务」②「你已被创建为团队 X 的角色 Y」③「请等待主会话派活」;**不含任务内容、不要求它做事**)⇒ 每个新会话都有回合 ⇒ **在侧边栏可见**。 - **成本口径连带改写**:框内成本行由「N 个会话 × 至少一个完整回合(**followup 驱动一次**)」改写为**回合计数口径** `N+1 个回合(N 个新会话 + 1 条调用方回执;按各自模型计费)` —— 两支**逐字同形状**(有任务 N+1、无任务也 N+1),所以它**不再分支**,也就不可能与同一张框的「确认则」自相矛盾(🟡#1 的同支纪律由「不写任何 branch-specific 断言」满足)。**这是一次经申报的改写**:让位的是机制短语「followup 驱动一次」(机制由「确认则:创建 → 投递启动任务」承载),「**按各自模型计费**」**逐字保留**(DIVERGENCE #4 的回归)。 **② 为什么** - 默认名带日期:用户原话「默认团队名 需要加上工作区目录+日期,这样好点」;同日重复发命令仍指向**同一个团队**(§10.2.6 幂等一字未动),跨天才自然另起一队。 - 释放并认领:用户原话「如果团队主管会话被归档(现任主管,被换届的老主管会话不算),应该自动释放团队名」——**没有这条**,一个被归档的现任会把团队名永久卡死(`writerGate` 只认现任,而现任回不来)。 - 调用方回执 + 最小唤醒:真机观察 2/3/4 —— 壳只列「至少跑过一个回合」的会话 ⇒ **在空会话里发命令**、以及**不带任务建出的 worker**,都**在侧边栏隐身**。用户原话:「让插件给调用方会话投一条回执,这样才能把会话出来」+「改回『投一具最小启动任务』」。 **③ 怎么验证**(两套件实测 `995 (failed: 0)` / `269 (failed: 0)`,基线 `971 (failed: 0)` / `269 (failed: 0)`;**+24 条断言**) - **变异验证三条(逐条实跑,每条只红它该红的那条)**:**(A) 把 (乙) 的 `unknown` 改成「读不到就释放」** ⇒ `995 (failed: 1)`,恰好红「U37 (乙)」;**(B) 把「只看现任」改成「看任一历史任」** ⇒ 恰好红「U37 变异基线 2」(同跑另见一条**既有计时器夹具的偶发红**,见下);**(C) 去掉写时复检里的活性那一半** ⇒ `995 (failed: 1)`,恰好红「U37 变异基线 3(TOCTOU)」。 - **红相(新测试 + 旧实现,隔离夹具实跑)**:`995 (failed: 34)` —— 34 条逐条点名(U16 成本口径 1 · U33 2 · U33c 2 · U30 默认值 2 · U35 5 · U30 反转 4 · U32 通道 4 5 · U36 8 · U37 4),其中 3 条**负相**(`policy.writer` 边界 / 无新日志事件 / 只看现任)在旧实现上**本来就绿**,如实标注不算红相。**这次实跑还抓出我自己新测试的两个崩相**(未守卫的可测面读取 · 只在假分支求值的 `probe` 声明在后面 ⇒ `TypeError` / TDZ,`assertion total` 都不打印)—— 已按本仓 Y7 纪律修成「先取值再判类型」,教训记在 `docs/verification-log.md`。 - **逐具夹具读数**:**U35** 五条(格式 / 同日幂等纯函数 / 同日幂等命令级 / 跨日另起一队 / 无日期旧团队的孤立后果——**注入「当天」**,不靠墙钟)· **U36 (a)–(h)**(归档 / gone / 存活照旧拒 / 工具路径 / 保留既有角色 / 触发释放的框体 ≤6 行 ≤390 码点)· **U37 甲/乙两形状 + 三条变异基线** · **U32 通道 4 六条**(成功 / 无任务形态 / 取消 / 解析前失败 / 解析后失败 / 无新日志事件)· **U30 反转三条**。**U33c 重测**:12 码点 id **355** · 28 码点 id **371** · 释放变体 **348 / 382**(28 码点 id + 带日期团队名,最坏一具),判据 **≤390**(此前测试里写的 380 比设计档更严,本批与设计档对齐)。 - **真机已复验(2026-09-22 重启窗口;这一条取代原先的「未做真机验证」)**:工具面的「释放并认领」、`/team_session` 的**释放 / 回执 / 最小唤醒 / 确认框**四项、**侧边栏三会话可见**、以及**结算时序读数**(`command/done → 回执落盘 0.288 s`,回执花掉的模型回合在其后)都在真机上验到 —— 逐条读法与截图证据见 `docs/verification-log.md` 的「真机复验」一节。**这次复验还抓出一个我漏补的判据**:设计档 🟡#7 要求补一具「无任务形态」的框体读数,我漏了,而真机那一次命令恰好落在它上面 ⇒ **修前 399 码点 > 390**(重复文案所致)⇒ 压掉重复(−18)后 **381**,夹具已补(参照 F/G/H/I)。 - **如实标注**:**§10.2.8.10 的实现期兜底没被触发**(回执走的正是先试的 handler 内 `followup`);**设计档的两处计数/读数勘误**已同批改(该披露句字面量逐字数 **11**,原写「12 码点」;§10.2.8.9 ② 的 `364 / 380` 是**改写成本行之前**的读数,重测后为 `348 / 382`);**一次既有计时器夹具的偶发红**(「缺口1 超时读数自报真实后果(计时器到点)」,连续两次复跑均绿,与本批无关)如实记录、不计入本批证据。 - **真机收尾(同日)**:三个一次性探针团队按 `retire` 退役 —— `rm-probe-20260922` 与 `dsh-session-link-pro-20260922` 由本会话发起(后者**先经 §10.2.8.9 ② 把那具幽灵现任释放并认领,再退役**),`rm-release-20260922` 由用户在其测试会话里发起(`retire` 顺带**清掉 2 条 pairs** ⇒ 退役那一步的**信任清理**也在真机走过);三行现均 `vacant`。**团队行本身的删除没有工具面路径**(只有用户设置 UI),本批未碰;盘上那三个黑板目录已随退役一并删除(工作区里 `team/` 是未跟踪文件 ⇒ 删除无 git 影响)。 --- ## 0.3.9 — 2026-09-21(发布前收口于 0.3.10)(加固与恢复)— ① 导出路由信任栅栏 · ② 恢复能力加宽 · ③ 侧栏「会话工具」入口 · ④ 深链聚焦修复 > 设计(唯一事实源):`docs/hardening-and-recovery-design-2026-09-21.md` §4.1–§4.4(含 §5 红线 B1–B9 与 §6 判据 U1–U14);逐条**红相/绿相读数**见 `docs/verification-log.md`。四条工作面按 §11 分三批实施,本条目随批次追加。 > **发布状态**:①② 属宿主半边,需 DSH 重启才在真机生效;③④ 属浏览器半边(客户端 bundle 重建 + 刷新页面)。真机复验点 1 的**拒绝面已闭合**(2026-09-21 重启后复跑,原始读数见 `docs/verification-log.md`「批次 4 · H 真机复验点 1 · 绿相(重启后)」一节);**仍未闭合的真机面**(如实列出):① 点 1 的「**同源带 cookie → 200 全量**」正路径**无读数**;② **点 2**(`reappoint` 用 threat-intel 真实 roster 形演练)**从未执行** —— 现有判据都是单元夹具形;③ 点 3 的**标题栏收起态**未核对。而点 3 的**宽态**与**收起态**已由 owner 在 3080 页面上真机确认(宽态:位于消耗卡**右侧**、【设置】上方;收起态:56px 轨道下只剩图标、与使用卡同一行)。与 0.3.8 同口径,**不把未验的东西说成已验**。 ### 批次 1 —— §4.1:导出路由接入平台信任栅栏(两层 fail-closed) **修了什么**:`GET /team-link/export` 此前是一个**没有任何信任检查**的路由——任何能连上本机端口的请求(DNS rebinding / 跨站页面)都能拉走整份会话事件流。本批把它接上平台自己的栅栏 `connection.requestRejection`(Host/Origin → 403、浏览器鉴权 → 401),并**两层都 fail-closed**: 1. **挂载期**:路由只在**同时**拿到 `webServer` 与 `connection`(且后者有 `requestRejection`)时才注册;新增两个 reason code `no-connection` / `no-rejection`,与既有 `not-active` / `no-register` 一样——**不注册路由 + 一行 warn**(点名缺的是哪一个)。晚挂从 `ctx.inject(["webServer"])` 改成 `ctx.inject(["webServer", "connection"])`,两个提供方任意顺序到齐才挂载。 2. **请求期**:handler 的**第一句**实时复检栅栏;取不到(服务缺席、方法缺失、或**挂载之后被拆**)⇒ `503` + 不吐数据;取到则把它的状态码**原样写回**,响应体与官方 `client-connection` 的 RPC 通道一致(401 `unauthorized` / 403 `forbidden`)。这一层覆盖挂载期判不到的那个窗口。 3. **方法白名单**:非 `GET` ⇒ `405` + `allow: GET`(对齐官方路由写法),任何会话都不读。 **为什么**:平台契约明确「路由所有者自负其责」(`dsh-host-webserver`:不做 server-wide 认证/来源策略),栅栏由 `dsh-client-connection` 提供给**每个**路由所有者;本插件此前全库 0 处调用它——这是一处**未文档化的安全缺陷**,不是被记录下来的取舍。 **怎么验证(变异验证)**: - **修复前必红**(本批新增 14 条断言,基线 889 不动):`node host-half.test.mjs` → `assertion total: 903 (failed: 10)`。10 条 FAIL 恰是本次要修的每一面:U1 纯函数不存在 / 跨站请求真的拿到 200 + 会话正文且 `readSession` 被调用 / 路由从未把请求交给平台栅栏 / 401 分支无人写回 / 挂载后栅栏消失仍照常吐数据 / U6 非 GET 被当 GET 服务 / U2「webServer 在、connection 缺」仍然注册了路由(无门路由真实存在)/ `no-rejection` 分支不存在 / U4 只到 webServer 就挂载。 - **修复后全绿**:`node host-half.test.mjs` → `assertion total: 903 (failed: 0)`;`node client-half.test.mjs` → `170 (failed: 0)`(未触及浏览器半边)。 - **红线 B1**:`connection` **没有**进模块级 `inject`(仍恒为四项),仍走 `ctx.get` + `ctx.inject` 的可选服务缝;断言直接读导出数组。 - 新增可测面:`__testing.exportGateRejection(connection, req)` —— `connection + req → 状态码|null` 的**纯函数**判定(U1 不必依赖 HTTP 夹具;接受 `null` = 放行、`503` = 无栅栏可问)。 **如实标注(设计未规定的一处实现选择)**:`503` 这一支的响应体写的是 `unavailable`,而 401/403 严格照官方(`unauthorized` / `forbidden`)。理由写进了代码注释:403 的含义是「栅栏说了不」,503 的含义是「**没有栅栏来裁决**」——把后者写成 `forbidden` 等于报告一个从未发生过的信任判定。 **同批同步的面**:`README.md`(§二 导出段新增「下载路由走平台信任栅栏」一段、§七 契约的服务清单、依赖服务降级表新增 `connection` 行、架构图的导出路由节点);本条目。 ### 批次 2 —— §4.2:恢复能力加宽两格((a) `revive` 放开角色门 · (c) `reappoint` 常驻「自建继任者」) **修了什么**: 1. **(a) 删除 `reviveIncumbent` 的角色门**——`revive` 现在受理**任意角色**(所有权门、`resume` 可用性检查、在场确认框、两次写前复检**一律不动**)。 2. **(c) `reappoint` 的候选集改为「活成员 ∪ 插件自建继任者(常驻)」**——合成候选 `自建继任者(新建会话)` **始终在列**(不是「仅 0 活候选时才出现」:事故当场的候选长度是 **1 不是 0**)。选中它时逐条复用既有链路:能力闸门(**批次 4 收窄到这条支路**:宿主无 `agents.create` ⇒ 弹框照开、候选里**不出现**「自建继任者」;**仅当活成员候选也为 0** 时才 fail-closed 报告且零弹框——活成员改任那条路不需要该服务)→ 按 §10.2.2 铸 id(`team-link---`)→ 交接文档由**插件从 roster 事实**生成五硬节(读不到的项——前任的进行中工作与 goal、未提交的改动——**如实标未知**,不编造)→ `prepare` 逐字跑 → 审计行 `verb=reappoint`。原有的「候选集为空 ⇒ 一条死路」分支因此**不可达并被删除**。 **为什么**:(a) `revive` 是**身份不变**的操作(不写 roster、不铸令牌、不动 pairs/trustedSenders/rememberTargets、不碰 `policy`),放开角色面不向「通用 roster 编辑器」移动一寸;而重载拆掉的是**所有**插件自建会话,不只 coordinator 的——§11.9.1 只论证过「死的 coordinator 必须可救」,从未论证「死的 worker 不许救活」。(c) 候选用尽时原本**没有出口**:全队只剩协调者一个活人时,唯一可选路径是「把一个 worker 角色硬塞给协调者」。 **怎么验证(变异验证)**: - **修复前必红**:把**本批的新测试**跑在**批次 1 的 `lib/index.js`** 上(隔离夹具 `.test-tmp/s2-red/`:新测试 + 新 README + 旧实现)⇒ `assertion total: 920 (failed: 21)`,21 条 FAIL 全部落在本批断言上:U7 两条(插件自建的死 worker 被角色门拒)、U8(**连所有权门的文案都读不到**——角色门先拒,这正是「角色门挡在适用域之前」的实证)、U9 **九条**(对话框里没有合成候选、没有创建、没有交接文档、没有三处留痕;同组那条「新建的会话是根会话、meta 无血统字段」在修复前**本来就为真**——旧实现从不 `create`,`creates` 为空——所以它**不在**红相里,批次 4 重跑实测定档)、U10(无 `agents.create` 时既没有闸门也没有零弹框)、U11 与收敛性两条,以及**六条被本批语义变更推翻的既有断言**(U26 角色面 / Y1 对照 / Y1 文档面=实现面(双向锁)/ Y2 宣传面 / U27 候选由插件算 / U27 全队皆死)= 2+1+9+1+2+6 = **21**,与 `920 (failed: 21)` 逐字对上(原文记「U9 十条 / 五条」是**计数勘误**,加总 22 与实测差一条,批次 4 已按实跑读数改准——见 `docs/verification-log.md` 批次 2 的「计数勘误」小节)——设计 §9 A7 点名的「既有断言可能把非 coordinator 一律拒钉死」**实测成立**,本批按新语义改写这六条而不是删除(每条都留了改写说明)。**红相是干净的**:套件跑到底并打印了 `assertion total`(首版红跑因直接调用尚未存在的 `__testing.selfBuiltHandoffBody` 而崩在那一行、连总数都不打印,已按本仓 Y7 纪律加类型护栏——那正是 Y7 存在的理由)。 - **修复后全绿**:`node host-half.test.mjs` → `assertion total: 920 (failed: 0)`(新增 **17** 条,`920 − 903`);`node client-half.test.mjs` → `170 (failed: 0)`(未触碰)。 - **红线回归**:所有权门原样(U8)、`writerGate`/三道门本体未动、`policy.writer` 未动、`claim` 一字未改、不新增令牌类型、**不新增参数**(`handoff` 没有进 recover 的参数面——交接正文由插件生成)、模块级 `inject` 仍 4 项。 **如实标注(设计没有逐字规定、由实现定夺的两处)**: - **自建继任者的工作目录**:优先用**团队自己的 `workspace`**(该队的 roster 镜像与交接文档就在它下面),团队没记录时回落**发起会话的 cwd**;两者都取不到绝对路径 ⇒ fail-closed(零创建)。`process.cwd()` 一律不用(与 `successor:"auto"` 同一条边界)。设计 §4.2 (c) 未规定 cwd 的取法。 - **交接文档与 `prepare` 的先后**:按 **§11.9.6 的 abort-before-prepare**(文档失败 ⇒ 不铸令牌)排在 `prepare` **之前**,也就是与 `successor:"auto"` 同序;§4.2 (c) 的编号(③ prepare → ④ 文档)是**元素清单**而不是时序,本批取了与既有 auto 路径一致的时序。 **同批同步的面**:`lib/index.js` 的 `team_link_recover` description(两个动词的角色面 + 候选集 + 合成候选链路 + 代价声明)、`action`/`role` 参数文案、诊断行;`README.md` 的工具表、§11.9.4 恢复叙述(角色面 + 自建继任者段)、八条硬约束的 ③;`docs/collab-enhancements-design-2026-09-19.md` §11.9.4/§11.9.5 的两处增补。 ### 批次 3 —— §4.3 侧栏「会话工具」入口 · §4.4 深链聚焦修复 **修了什么**: 1. **§4.3 入口**:侧栏底部新增「会话工具」——注册进官方 list 槽位 `sidebar.footer.action`(`id: team-link-session-tools`、`order: 0`)⇒ 入口落在**同一 flex 行内、位于消耗卡片 `order: -10` 的右侧**(`order: 0` 升序 ⇒ 排在它之后、【设置】之前);**不是「下方」**:官方 `.footerActions` 是 `display:flex`(方向默认 row),且官方 sidebar 全档 `flex-wrap` 出现 **0** 次 ⇒ **不换行**,而插件**无法从子元素侧改变父级换行**(改父级 = 改官方文件,违反设计 §1.3 的红线 N5),2026-09-21 真机读数(owner 截图)与之一致——**原稿把「排在后面」写成上下排布,是对官方布局的错误推断,本行按设计 §4.3.1 更正**。本入口**不改任何官方文件**。宽态 = 图标 + 文字「会话工具」,收起态(56px 轨道)**只渲染图标**(文字转 `aria-label`/`title`);点击 / Enter / Space 打开官方 **`Modal`**(居中、挂 body——锚定面板会被 56px 轨道裁切)。 2. **弹窗(§4.3.2)**:标题 + 当前计数 → 搜索框 → 会话列表(可滚动)→ 有界呈现标注 → 底部(范围切换 + 关闭)。数据源 = `ctx.sessions.list` / `ctx.workspaces.list`:丢弃 `origin === 'subagent'`、丢弃已归档、blank 行只保留当前会话(官方会话浏览器同款规则),按 `updatedAt` 倒序;当前会话 = `retainedBy.mainView > 0` 的那一行;**默认范围 = 当前工作区**,可切「全部工作区」。 3. **列表行(§4.3.3)**:状态点 **恰两态**(运行中 / 空闲,唯一数据源 `SessionSummary.running`)+ 标题(超长省略)+ 相对时间(用官方 `relativeTime` 分桶、本插件字典给词;「全部工作区」时追加工作区名);「复制链接」「导出会话」**默认隐藏,悬停该行或焦点落在该行内才浮现**(CSS `opacity` + `:focus-within`);点行本身 = 打开该会话。 4. **三个动作(§4.3.4)**:复制 = 剪贴板 `dsh://session/`(与会话头部按钮**同一格式**)+ 短暂「已复制 ✓」+ `aria-live` 播报;导出 = **导航** `GET /team-link/export?session=…&format=md`(同源自动带 cookie、天然过 §4.1 的栅栏、零 CORS 面;参数必须编码);打开 = `ctx.uiWorkspace.openSession(id)`(**运行时**经 `ctx.inject(["uiWorkspace"], …)` 取用)。 5. **状态与边界(§4.3.6)**:三种空态**三句不同的话**(读取中… / 没有匹配「xx」的会话 / 暂无其他会话);超过显示上限时末尾写明「**共 N 个,仅显示前 M 个(搜索可收窄)**」;`sessions` / `workspaces` / `uiWorkspace`(以及 seed 模块的 `Modal`)缺任一项 ⇒ **入口不注册 + 一行 warn**,绝不渲染假按钮。 6. **键盘与无障碍(§4.3.7)**:Tab / Enter / Space 可用;关闭(Esc / 关闭按钮 / 成功打开)**把焦点还给入口**;动作按钮带独立无障碍名(含会话标题);尊重 `prefers-reduced-motion`。 7. **§4.4 深链聚焦**:把 `ctx.sessions.open(id)` 换成 `ctx.uiWorkspace.openSession(id)`,并**保留**「等会话出现在 `sessions.list` 再聚焦」的重试循环;服务缺席只丢「聚焦」这一步,失败只留一行痕。 8. **`package.json` 的 `dsh.client.inject`** 增三项:`@deepseek-ai/dsh-client-ui-workspace`、`@deepseek-ai/dsh-api-session-controller`、`@deepseek-ai/dsh-api-workspace-controller`。**seed 模块 `@deepseek-ai/dsh-client-ui-primitives` 刻意不声明**(与 `react` 同级由 shell 注入)。 **为什么**:官方会话行的「…」菜单**没有扩展槽位**、且硬编码在 `node_modules` 的构建产物里(设计 §2.3)——改它等于旁路官方文件且会被升级静默还原;侧栏底部那个 list 型槽位是官方**声明允许任意插件注册**的座位。而深链聚焦是一处**静默空调用**(§2.4):`ISessions` 公开面**没有** `open` 方法(同名的 `Session.open()` 是历史加载,另一回事),调用抛出的 TypeError 被 `try/catch` 吞掉 ⇒ 列表轮询到了、界面却停在原地。 **怎么验证(变异验证)**: - **修复前必红(整批)**:隔离夹具 `.test-tmp/s3-red/`(本批新测试 + `git show 8d40343:lib/client.js`)⇒ `node .test-tmp/s3-red/client-half.test.mjs` → `assertion total: 193 (failed: 27)`。27 条 FAIL 全部落在本批断言上:9 条 §4.3 红相守卫(入口未注册 / 入口与弹窗工厂不存在 / 五条纯规则不存在)+ `§4.3 可测面` + 9 条被新语义推翻的既有计数断言(`fresh.length` 4→5、`names()` 多一行)+ 7 条 U14。 - **U14 单独红相(设计 §6 点名的那条)**:`.test-tmp/s34-red/`(当前实现**只**把 §4.4 那一行还原成 `ctx.sessions.open(id)`)⇒ `257 (failed: 7)`,其中判据那条即「**深链打开后当前会话 id 不变**」——以 `retainedBy.mainView` 判定,主视图仍留在启动时那个会话上(另有 4 条 U14 与 1 条病灶锁同时红)。 - **修复后全绿**:`node client-half.test.mjs` → `assertion total: 257 (failed: 0)`(新增 **87** 条 = 257 − 170:10 条红相守卫/可测面 + 77 条 §4.3/§4.4 断言;另有 **9 条既有断言**在同一批里按新语义改写——`fresh.length` 4→5 与 `names()` 多一行,见下条变异验证);`node host-half.test.mjs` → `920 (failed: 0)`(本批未触碰宿主半边)。 - **红线**:不改官方文件;本插件**模块级** `inject` 仍恒三项(`slots` / `sessions` / `locale`)——三个新依赖只进 `dsh.client.inject`(模块图)与运行时 `ctx.inject`(服务);本入口的动作集恰为「复制链接 / 导出会话 / 打开会话」,**不做任何会话写操作**。 **同批同步的面**:`README.md`(§二 新增「侧栏「会话工具」入口」小节与三种空态表、深链段的聚焦修复指针、§九「浏览器半边的模块声明」、§十 读数与口径、设计文档索引登记本档并修正 ③b「未实施」的既有失真、版本徽章 0.3.7→0.3.8、测试徽章 889+170→920+257、架构图加侧栏入口节点与导出边);本条目;`docs/verification-log.md` 的批次 3 条目。 **如实标注(设计未逐字规定、由实现定夺的地方)**: > **后续变更(批次 4 G,2026-09-21)**:**列表已改为丢弃当前会话**(见下文「批次 4」第 7 条 / 设计 §4.3.2 的可见性规则)——下面这条「列表包含当前会话」的说明**已被取代**,**不再**是当前行为;**原文保留**(本文件不改写历史条目)。当前行为的验收点:工作区里只剩当前会话时显示「暂无其他会话」且一行都不画。(同批「弹窗」条目里那句「blank 行只保留当前会话」同属被取代的旧口径。) - **列表包含当前会话**:§4.3.2 的可见性规则里没有任何"排除当前会话"的话,且它明确要求"blank 行只保留当前会话"(以当前会话**在列表里**为前提)⇒「暂无其他会话」这句出现在**可见集为空**时(例如整个工作区都已归档);工作区里只剩当前会话时,列表如实显示那一行、不报空。 - **打开成功后关闭弹窗**:导航本身就是回答,且关闭会把焦点还给入口;只有打开**失败**(导航抛错)才保留弹窗并留一行痕。 - **行的结构**:行是 `
  • `,其可点面是一个真正 `