dsh-team-link
DeepSeek Harness (DSH) 的「多会话协作」插件 —— 让同一个 DSH 实例里并行干活的多个会话互相看得见、说得上话、交接得了班。
原名
dsh-session-link-pro(0.2.4 及之前),GitHub 仓库已于 2026-09-18 改名为dsh-team-link(旧地址由 GitHub 自动重定向)。历史会话日志里的旧工具名session_link_pro_*与消息 id 前缀slp-保持原样——它们是取证链,不做回写。
Fork 自 PwnKY/dsh-session-link——深链复制、/s/<id> 打开器与深链上下文注入保留自上游;本仓库在其上长出了完整的多会话协作层。
最近三批修了什么(人话版 · 2026-10-13)
三条都是真机事故逼出来的,不是设想出来的需求。每一条都经过「设计评审 → 实施 → 独立交付评审」三道关。
① 你报过:「DSH 有时候莫名其妙崩了,事后完全查不到为什么。」
- 为什么以前查不了:崩溃是非正常退出 —— 进程没有机会跑任何收尾代码,所以「死的时候写日志」这条路永远走不通。
- 现在怎么做的:改成活着的时候持续写心跳 —— 每 10 分钟把内存占用 / 磁盘余量 / 活会话数 / 看门狗数落一次盘,而且每次启动单独一个目录(崩溃后那个目录还在,下一次启动直接读)。
- 你自己怎么看:
team_link_status action=forensics一条命令,就能看到「上一次死之前那一刻」的读数与死亡窗口(从最后一拍到下次启动有多久没人知道)。 - 如实说明:它只留现场、不解释原因;而且正常退出与崩溃在这份数据上长得一样,要区分得由人结合宿主的 crash 记录判断 —— 插件一个字都不读宿主的地盘。
② 你报过:「两个协调者同时说团队名册丢了。」
- 真正发生了什么:名册数据其实没丢。是多个进程同时写同一份文件,而它们用的是同一个临时文件名 ⇒ 字节互相覆盖,文件尾部多出 73 字节垃圾,整份
JSON.parse就失败了 ⇒ 插件只好把状态放在内存里 ⇒ 会话看到「名册是空的」。 - 现在怎么做的:临时文件名改成每个写入者唯一(进程号 + 随机后缀),改名带有界重试,失败时如实报错(不静默)。
- 顺带的好处:心跳也走同一条原子写路径 ⇒ 上面第①条那个「两拍并发」的隐患一并根治。
③ 你报过:「协调者自己不派活,还去读队员的会话历史。」
- 为什么它不听:规则早就写在它的系统提示里了(「你只做管理/协调/决策/评审/设计;实施一律派给成员」),但「有活没人派」这件事只是摘要里的一行散文、还会被去抖吃掉;而它没有别的查活路,只能去读队员的上下文预算。
- 现在怎么做的:① 「有活没人派」升级成独立告警(队员全闲又没人在跑时主动提醒,同一段最多 3 次)② 它自己的上下文过 50% 时点名警告「再读别人的会话历史会把你自己的窗口读满」③ 在守则与摘要里明确写出事实源:要看队员在干什么 ⇒ 读那个团队的黑板
tasks.md,不要读它的会话历史。 - 你自己怎么看:协调者会话里会出现
[idle-unassigned](有活没人派)与[pressure-warn](自身压力过高)两条消息 —— 都只发给现任协调者、都受 ≥10 分钟去抖。 - 如实说明:插件拦不住「协调者自己动手」那一步(那一步走的是宿主工具,不经过本插件);本批做的是让事实刺眼,不是假装能阻止。
与 DSH Agent Teams 的关系:正交,不是竞争
DSH 0.1.7-rc 系列起随包提供实验性的 Agent Teams(@deepseek-ai/dsh-experimental-agent-team-profile,默认关闭、需在插件页显式开启):一个会话可以创建具名 teammate(子代理)、与它们交换持久消息、用一张共享任务板协调工作,并在 Web 面板里看成员与任务。它和本插件解决的是两件不同的事:
| 内置 Agent Teams | dsh-team-link(本插件) | |
|---|---|---|
| 团队是什么 | 一个会话内部的团队:Lead 会话 + 它生出来的子代理,单根树 | 会话之间的团队:几个平级、各自独立生死的会话 |
| 身份与家谱存在哪 | Lead 会话的日志里(TeamId = Lead 的 SessionId) |
插件 settings 的 teams 键,不挂在任何会话身上 |
| 进程边界 | 单进程 —— 官方明确「多个进程需要协调同一支团队」不在支持范围 | 跨会话、跨进程 |
| 信任模型 | 不需要:子代理的权限由 Lead 派生 | 必须有:双门批准 · 配对 · 一次性令牌换届 · 退役者对称吊销 |
| 人在环 | Lead 即授权者 | 唯一的新授权点是人类点击(fail-closed) |
| 换人 / 换届 | 没有这个概念:持久状态要有人重开 Lead 会话才回放得动 | revive / reappoint 两个封闭动词 + 令牌换届 + 信任迁移 |
| 工作跟踪 | 有共享任务板(CAS revision · 依赖 · 写作用域) | 只追加的任务台账 tasks.md(记「谁声称了什么」,读面的「最后主张 / 未消解存疑」是派生读数、绝不自动裁决;§9 第一批已实施) |
| 稳定性 | 实验原型,不承诺稳定性,schema 可自由变更 | 1783 + 310 条断言(★ 批 U 新增 12 条(看门狗运维面:update / mute / unmute 就地调参、auto 注册也能静音;wake:false 与「上一拍 tick 无 assistant 回复」两种情况下改走 inject 不唤醒 —— 判据 批 U · U1–U5,本地复跑 1783 = 1771 + 12)+ ★ 批 T 新增 13 条(黑板归档 / 回收:判据 Tr1–Tr13 —— 显式归档动词 team_link_team_archive · 只搬不删 · 归档文件只写一次 · 确定性命名(不含墙钟)· 最新形态史行钉住 · 序号与任务号取「活跃 ∪ 归档」不回卷 · 归档 id 仍认 / 伪造高号仍拒 · 崩溃幂等 · 归档索引常驻读面;本地复跑 1771 = 1758 + 13)+ ★ 批 S 新增 20 条(整队换届:现任协调者一条命令 + 一道确认框准备整队 Phase A —— 判据 Sr1–Sr20;首交付 15 条 + 分歧修复轮 5 条(批次级 freeze 恰一条 / 读面 pending 时分组 / 执行期失败回执点名失败角色自己的会话 / auto 投递与意图先增后删 / 3 分钟超时档);预检 all-or-nothing(逐角色限速 / 继任者≠现任 / 显式继任者在场;auto 角色的交接文档由插件从 roster 事实生成)· 恰一次 policy.update 落 N 条 pending + N 份撤销依据 · 一条批次级 rotation-freeze · 批量令牌 2 小时 · 协调者自己的角色排最后 · 批次互斥(单角色与二次批量都具名拒绝)· 写前重读指纹 · batchId 三面存活;本地复跑 1758 = 1738 + 20) + ★ 批 Q 新增 11 条(换届的「撤销依据」按换届各存一份:判据 Qr1–Qr11;两次 prepare 互不覆盖 · 旧档无损折叠 · claim 取对那份 · 读面列全部在飞条目 · 收尾只删自己 · 零顶层键 / 零新依赖 · 不回归 · Pi7④ 审批写面抛错分支 · session 在场守卫 · 升级窗口端到端 · 缺那份具名拒绝;宿主读 1738 = 1727 + 11)+ ★ 批 R 新增 15 条(上架合规:Rr0①–④ / Rr3①–④ / Rr5 / Rr6 + 收口单的 Rr1 / Rr2 / Rr4 / Rr7 / Rr8;单档字节 · 入口面逐字不变 · 模块图无环 · 合规档数字与实测一致 · 行为零变更;宿主机读 1727 = 1697 + 15 + 15;★ Rr4 现读绿(fdfc832 已消除 lib/shared.js ⇄ lib/rotation.js 那条 2 环 —— 2026-10-18 更正,见 §十 与合规档 §六 末行))+ 批 34 / FR-7 新增 15 条(台账写门,跨仓交付的 team-link 侧;判据 A-7:规则①/② 正反相 + 同任务谓词 + 默认 OFF 零回归 + 开关可读可写与 zod 拒非法值;宿主机读 1727 = 1697 + 15 + 15)+ 批 P 新增 14 条(换届继任者的设置继承:判据 Pi1–Pi9;三个宿主探针全通过 —— 预设/模型/审批档三项都不必降级;宿主机读 1727 = 1646 + 20 + 8 + 9 + 14 + 15 + 15)+ 批 D 新增 19 条、批 E 新增 37 条、批 F 新增 20 条、批 G 新增 25 条、批 H 新增 21 条(落码时 19 条 + 分歧修复轮 D1 的 2 条;宿主回执已回填)、批 I 新增 27 条(屏蔽的作用域,判据 U1–U20;期望值取自源码逐条计数 grep -c '^check("批 I ·',宿主回执待回填;★ 2026-10-12 追加项 D3 补第 27 条 U20「by ∉ {a,b} 的条目归一化丢弃」)、批 K 新增 20 条(崩溃现场留证 / 心跳,判据 U1–U15:首拍 / 周期 / 死亡窗口 / 保留上限 / 只读合同 / 输出面 / 失败方向 / 磁盘诚实 / 免责句 / 兜底双向 / 写面负向锁 / 中途归零重挂 / 空串 action 口径;交付评审处置轮把 18 补到 20,宿主实测 1666 = 1646 + 20)、批 L 新增 8 条(名册文件的并发写保护,判据 Aw1–Aw8;期望值取自源码逐条计数 grep -c '^check("批 L ·';两次实测读数由宿主执行 —— 交付方无 shell)、批 J 新增 9 条(协调者的派工信号加固:J-1 FR-J1 判据 U1–U3 + J-2 FR-J2 判据 U4 + J-3 FR-J3/FR-J4 判据 U5/U7/U8/U9 + 收尾单 U6(反走私负向判据:带毒注册参数 ⇒ 两条新告警正文零控制字节,且带负相自证;同批把 renderIdleAlert 补进批 E 的消毒面清单),标识符统一 Cj 中缀;期望值取自源码逐条计数 grep -c '^check("批 J ·' = 9;本单实测(本地 Node 桥,node host-half.test.mjs)ALL PASS / assertion total: 1683 (failed: 0) —— 构成 1683 = 1646 + 20 + 8 + 9),见第十节)+ 变异验证(红相必红:DEFECT-5(2026-09-30,revive 的 resume 面漏了宿主自己那条 resume 的三样)10 条新判据 + 1 条改写打在本轮修复前的实现上实测红 11 条(聚合红相 1324 (failed: 11));形态批 41 条新判据打在实施前的实现上实测红 33 条;分歧修复轮 DIVERGENCE(8) 的 16 条新判据(+ 3 条改写)打在本轮修复前的实现上实测红 13 条(首跑打印的 14 条里有一条是夹具假红,已归因;另 6 条为覆盖缺口类、本就绿,如实标负相);收尾修复轮(R1–R6)的 9 条新判据(+ 1 条改写)打在本轮修复前的实现上实测红 8 条(另 2 条为覆盖缺口类 / 反向锁,如实标 ★ 负相);真机冒烟修复轮(L1 时基 / L2 镜像)的 10 条新判据(+ 10 条改写)打在本轮修复前的实现上实测红 16 条(聚合红相 1203 (failed: 16) = 6 条新判据 + 10 条改写的期望值;另 4 条新判据为覆盖缺口类,本就绿,如实标 ★ 负相)—— 策略持久化自持化一轮(2026-09-27)新增 34 条宿主断言(1237 = 1203 + 34):文件后端档的 11 条打在实施前的实现上,其中 7 条实测红(聚合红相 1214 (failed: 7),含 1 条 import 白名单锁)+ 4 条覆盖缺口类 ★ 负相(本就绿,如实标);折叠/可核/迁移档的 23 条问题先于实现写出,其区分性改用变异验证(8 次单点变异逐条证明新判据真会变红,见该节);分歧审计修复轮(DIVERGENCE(10),2026-09-27)新增 7 条宿主断言(1244 = 1237 + 7),打在本轮修复前的实现上实测红 6 条(聚合红相 1244 (failed: 6)),另 1 条为覆盖缺口类 ★ 负相);代码评审修复轮(2026-09-27,2 🟡 + 4 🔵)新增 11 条宿主断言(1255 = 1244 + 11):U11 损坏档守卫 4 条、U12 写失败 warn 按段复位 2 条、foldedAt 盖戳门 2 条、refused 未挂载窗口 3 条,打在本轮修复前的实现上实测红 8 条(聚合红相 1255 (failed: 8)),另 3 条为控制/对照项 ★ 负相;代码评审第 2 轮(2026-09-27,1 🟡 + 2 🔵)新增 7 条宿主断言(1262 = 1255 + 7):🟡#1 catch 支盖戳门 + 🔵#2 数组 policy 也判损坏 + 🔵#3 两因并存并陈,打在本轮修复前的实现上实测红 4 条(聚合红相 1262 (failed: 4)),另 3 条为控制项 ★ 负相;代码评审第 3 轮(2026-09-27,2 🟡 + 1 条文档滞后)新增 2 条宿主断言(1264 = 1262 + 2):🟡#1 fold failed 后链尾不回写 + 🟡#2 detach() 同步置窗口判据,打在本轮修复前的实现上实测红 2 条(聚合红相 1264 (failed: 2),控制条件并入同条断言);C 批分歧审计修复轮(2026-09-27,🔵#2/#3/#4/#5)新增 1 条宿主断言(1300 = 1299 + 1):🔵#5 的 null 判据,打在本轮修复前的实现上实测红 1 条(聚合红相 1300 (failed: 1))—— 该条的红是判据面 + 「行为修正」标记缺席(null 一档的行为在修复前已成立,如实标 ★ 负相、不冒充红相);C 批代码评审修复轮(2026-09-27,2 🟡 + 4 🔵)新增 12 条断言(host 6 + client 6,1306 = 1300 + 6 / 305 = 299 + 6):面板类名↔规则 2 · 翻页不回卷 3 · 降级句随页 1 · 派生行归属 2 · wellFormed 2 · key 完备 2,打在本轮修复前的实现上实测红 9 条(聚合红相 host 1306 (failed: 5) / client 305 (failed: 4)),另 2 条为控制/对照项 ★ 负相(本就绿)、1 条为实施后补强且未取得红相读数(如实标注,不冒充)) |
我们不可被替代的四件事:跨进程 / 跨团队 root 的协调 · 平级会话之间的信任(双门、配对、对称吊销)· 身份的生死与任何一个会话解耦(换届换人、退役留痕、家谱可查)· 人在环的授权点。内置那套更擅长的则是同一间办公室里的快速分工(子代理随叫随到、任务板可改状态、状态变更零 token)。
两者结构上正交,不存在二选一:内置的成员(子代理)不在本插件的投递面上(本插件的投递面只列根代理),所以按场景选就行 —— 要跨会话、要信任、要交接 → 用本插件;要在一个会话里快速并行 → 用内置那套。
与常规 agent team 框架(CrewAI / AutoGen 一类;一般性描述,未逐版本核实)的差别同源:那些框架把多个角色放进同一个进程、同一个编排器里,靠函数调用或共享 memory 传消息 —— 没有独立会话、没有跨进程信任问题,团队随主程序生死。
14 项逐项对照、三条结构性差异与逐条出处见 docs/comparison-agent-teams-2026-09-26.md。
目录
- 与 DSH Agent Teams 的关系:正交,不是竞争
- 实战效果:两个真实项目的长跑记录
- 这是什么:三个痛点
- 架构
- 快速开始
- 工具一览
- 一、跨会话消息
- 二、会话可视:列表 / 深链 / 导出
- 三、跨会话看门狗
- 团队状态卡(只读)
- 团队形态(多会话档 ↔ agent-team 档)
- 四、团队:roster 与黑板
- 五、团队换届 rotation
- 六、策略配置
- 七、服务获取与留痕(0.3.7 的关键修复)
- 八、兼容性与字符串安全
- 九、安装
- 十、测试
- 设计文档索引
- Changelog
- Credits · License
实战效果:两个真实项目的长跑记录
下面两个案例来自连续多天、经历多次换届的真实多会话项目,全程由本插件的协调会话(团队里的「主管」)驱动,工作线是人类可见的独立会话。本项目的部分机制即来自跨会话协作时模型的自主涌现。
案例一|命令行工具项目:换届 + 夜班自主推进
| 过程指标 | 读数 |
|---|---|
| 团队规模 | 1 名协调者 + 6 条工作线并行(其中 2 条中途失联,由 roster 的活性诊断发现) |
| 角色交接 | 主管角色经历多次换届;本窗口内完成一次完整 claim(一次性令牌 + 信任快照 + rotation-freeze 广播 + 版本史留痕) |
| 跨会话记忆 | 黑板裁定从移交时的 139 条增长到 216 条以上;换届后由新会话无缝续写,磁盘文件零丢失 |
| 一夜战果(人类睡眠 + 显式授权自主推进) | 12 个批次全链闭环,全部代签合并并同步双远端——该项目的既定门是「分歧审计 → 代码评审 → 实测」三道 |
| 人类介入 | 夜间零介入;醒后只做「批准 / 否决」类裁决 |
它证明了什么:主管这个角色是可以换届的。上下文疲劳、会话被重启带走、人要去睡觉——都不会让一条长任务停下。接手靠的是五硬节交接文档 + 黑板 + 一次性令牌,而不是「谁记得」。
案例二|数据平台项目:四线并行 + 互相核对抓错
| 过程指标 | 读数 |
|---|---|
| 团队规模 | 1 名协调者 + 4 个 worker 会话同时在线 + 若干后台实施作业 |
| 单日产出 | 主干 13 波推进、黑板 20 条裁定 |
| 纪律条款 | 从移交时的 14 条增长到 19 条以上——来源全部是真实事故:占位符三犯 · 平面连错 · 冻结前没真跑 · 读数转述 · 缓存版本戳漏 bump…… |
| 角色交接 | 一次完整换届:一次性令牌在 30 分钟窗口内完成认领;迁移前快照 8 组配对关系 |
| 会诊 | 两轮:第一轮 3/3 跑题(如实留档),改用严约束简报后命中并产出采纳条件 |
互相核对抓到了什么——这一节是全文最不可替代的部分:
| 谁 → 谁 | 抓到什么 | 后果 |
|---|---|---|
| 工作线 → 协调者 | 协调者漏推了一笔提交(四重独立证据) | 补推;协调者违纪入档,该纪律被再次强调 |
| 工作线 → 协调者 | 协调者的事实性误判(某张表与某条约束是否存在) | 认错并入档,避免了按错误前提实施 |
| 工作线 → 协调者 | 协调者提的一条修复方案其实是静默空操作(两组独立证据) | 方案当场撤回,没进生产 |
| 协调者 → 工作线 | 工作线虚报了一个后台作业「已派发」 | 查作业表发现不存在,重新派发 |
| 另一条线 → 设计稿 | 设计稿断言「零 DDL」被引文推翻(约束真实存在) | 打回修订——按原稿实施会在生产写库时直接撞约束 |
| 三个不同会话 → 量具 | 哈希口径族不可比 · 行尾 CRLF/LF 假差异 · 台账断档(三次独立拦截) | 三条口径纪律入库 |
为什么这很重要——要点在两件常被误解的事上:
- 不是同一个模型自查,而是不同模型、不同供应商的会话互相抓错。 这些席位上坐过不同家族的模型,而且每个会话可以单独指定 provider / 模型 / 思考强度,也能中途换模型——身份挂在会话上,不挂在模型上。所以「互相核对」是真的互相(不同训练来源、不同盲区),而不是同一个脑子换个座位。
- 做成这些事的主力是 flash 级执行模型,不是顶级模型。 案例里多条工作线全程用 flash 级模型跑(便宜、快,才敢同时开多条),强模型只花在两处:首轮设计评审与关键决策会诊。质量由组织 + 纪律兜底,而不是「每个环节都用强模型」。
上面六例没有一件是靠「更聪明的模型」挡住的,全靠结构:独立上下文 ⇒ 天然各自核验;裁决与纪律落盘 ⇒ 记忆不靠上下文;跨线质询 ⇒ 没有「自己批自己」。这正是本插件那条设计红线(报告必须原话点对点推;共享状态只能是「主张」,不能是「事实」)的实测回报:把报告换成「写张便条贴墙上、让主管自己看」,这六例中的每一例都会静默通过。
这两个案例比「一个会话里的 agent team」多出了什么
| 维度 | 多会话编队(本插件 + 工程模式) | 会话内 agent team |
|---|---|---|
| 核验 | 每线各自核验,且跨线互相质询——A 的结论被 B 的实测打回 | 自报结果,父会话只看到摘要 |
| 上下文 | 按角色切分:worker 只拿自己那一段,便宜且少污染 | 继承父上下文,容易互相污染 |
| 续航 | 黑板 + 看门狗 + 换届 + 恢复:角色可在会话之间移交,几十小时不断线 | 随父回合成败起落 |
| 审计 | 决策与纪律落盘可导出,事后能复查整条协作过程 | 只留在对话流里 |
| 覆盖 | 跨任意会话:别的团队、别的项目、人开的会话 | 限于本队内部 |
| 人的位置 | 人可直接进任一会话旁听 / 介入;工具层可读全队活性 | 人是调度者,队友是进程内编制 |
避免过度声称:「队友要持久」不是差别——agent team 本身就是持久队友。差别在载体:会话是盘上、可被人类直接进入、可被移交的实体;teammate 是进程内的编制。所以「跨任意会话」与「换届 / 恢复」是它结构上够不到的,其余几项是程度的差别。与内置 Agent Teams 的逐项事实对照见 上一节;这里是实测多出来的东西。
更早一批被挡下的真实事故(同一种打法,是记录不是推演)
| 事故 | 被什么挡下 | 挡住的是什么 |
|---|---|---|
| 跨会话 id 凭记忆转录、差一位 | 工具**如实报「没有活动代理」**而不是静默丢消息 → 定位到是转录错误 → 立规:发送前先 list_sessions 复制 id |
消息静默丢失 / 误判对方失联 |
| 整份重写把一个 1588 行的文件截成 1240 行 | 取证证明损坏文件是 HEAD 的严格前缀 → 回滚 + 11 处定向编辑,净损失 0;立规「行数守恒 + 结构性改动只用编辑工具」 | 静默的文件损坏被当成正常产出 |
| 用错量具:按 UTF-16 数行数(1588 数成 1435) | 差点误报「多了 150 行外来代码」→ 立规:行数只认 splitlines / git numstat |
把工具假象当成代码事实 |
| 两条分支上的修复发散 | 动手前发现另一侧也有一处修复,常规合并会静默回退它 → 改按 blob 逐文件取字节 + 每文件回执 | 一次没人会发现的反向回退 |
| 一处修复引入反向回归(环境变量能把结果压得比修复前更差) | 变更前实测发现 → 冻结常量 | 修复本身变成新缺陷 |
| 一条工作线提议「把检查门全放」 | 它自己发现那会连篡改 / 伪造 / 泄露检查一起放掉(27 个完整性用例会一起翻绿)→ 撤回方案,改从根上消除 | 为了「能出结果」把安全底线一起拆掉 |
| 首次发布 4/4 全失败 | 没有急着归因到新批次:终局回放 6/6 证明是既有的「首败正常」模式 | 把既有问题误判成自己引入的,然后瞎修 |
| 发布的「成功」报告其实是空壳 | 亲读发布物发现零声明 → 纠正为「判据语义太窄」而不是「接线遗漏」 | 把「没报错」当成「合格」 |
| 一次会诊的四份回复全在答元问题 | 判 0 可用 → 换成「编号作答」的严格要求,才拿到材料级回复 | 把「模型说没问题」当成确实没问题 |
| 协调者打断了工作线的正常长回合 | 如实记「干预早了些但无害」→ 据此改对心跳与长等待协议 | 把「静默」一律当成「卡死」 |
上面这十例,没有一件是靠「更聪明的模型」挡住的,全靠结构:独立上下文 ⇒ 天然各自核验;裁决与纪律落盘 ⇒ 记忆不靠上下文;跨线质询 ⇒ 没有「自己批自己」;换届 + 恢复 ⇒ 角色能在会话之间移交。那一轮结束时两条工作线的上下文已经耗尽,而工作没丢——交接文档 + 黑板 + 主管接手,收尾项 45/45 落完。
叠加工程模式(dsh-thincoder-suite)才是完整形态
本插件负责跨会话那一半:身份、投递、信任、交接、记忆、看门狗。另一半——单会话内的审级与实施门禁——由同厂的 dsh-thincoder-suite(工程模式)提供。两个案例里的「全链」都是这两半叠出来的:
| 环节 | 提供方 |
|---|---|
设计评审(advisor type=design)→ 签发一次性设计令牌 |
工程模式 |
实施只能由 eng_coder 带令牌执行(无令牌物理拒工) |
工程模式 |
分歧审计(只读子代理)→ 代码评审(advisor type=code) |
工程模式 |
| 真跑门 / dry-run / 变异验证 / 推送后 readback | 两者共同——纪律由本插件落进黑板,工具由工程模式提供 |
| 跨会话派工 · 汇报 · 裁决回执 · 换届 · 记忆 · 失联巡检 | 本插件 |
一个可复述的判据:案例里每个批次都要走 「设计 → 评审 → 令牌 → 实施 → 审计 → 代码评审 → 合并 → 部署」八道门——前六道是工程模式的门,跨会话的派工、回执、裁决与换届是本插件的门。
如实标注:实测到的边界
| 边界 | 实测 | 已固化的应对 |
|---|---|---|
| DSH 重启会清掉本插件的团队注册表(settings 索引) | 多次实测;黑板磁盘文件本身完好,丢的是索引 | 纪律「重启后先验 roster 再写」;重建走「创建即认领」 |
| 工作线会话被重启带走后无法投递 | 多次实测(插件只标 dead,不会凭空唤醒) |
需人在侧边栏重开一次该标签页;看门狗把 dead 报给协调者 |
| 个别部署上消费方工具读不到正文 | 实战中反复出现 | 走落盘文件 + 会话状态文件两条替代读取路径 |
| 会诊(多模型并行)会跑题、也会越界写文件 | 实测:一轮 3/3 跑题;一次只读契约被违反(子进程改了仓库文件) | 严约束简报 + 只读面外部约束;违规写入按「对不对 ≠ 有没有权做」处置 |
| 跨会话投递面只覆盖根代理 | 设计如此(子代理不是可投递目标) | 需要在拒绝时说清原因并指路——见 对比页 §6 的候选改进 |
这是什么:三个痛点
在一个 DSH 实例里开多个会话干活(一个协调者 + 若干 worker)时,会话之间默认是隔离的:
| 痛点 | 表现 |
|---|---|
| 看不见 | 不知道别的会话在干嘛:跑着还是卡了?在等人还是已经静默十几分钟? |
| 说不上话 | 没法把 A 会话的结论交给 B 会话继续;想归档一个会话只能翻 UI |
| 交不了班 | 会话上下文会疲劳,但把协调者换成另一个会话,意味着信任关系、团队身份全要人工重来 |
本插件补上这三层能力:
| 能力 | 说明 | 入口 |
|---|---|---|
| 🔗 会话深链 | 复制 dsh://session/<id>,粘贴到任意会话即注入该会话只读快照(上游功能) |
会话头部按钮 / 粘贴链接 |
| 📋 会话列表 | 列出同工作区其他会话:主题、运行状态、最近消息摘要,以及活性信号行(verdict 五态 + goal 状态 + 静默时长 + 读数时效戳) | team_link_list_sessions |
| ⬇ 会话导出 | 全量事件导出为 markdown(可读)+ JSON(无损) | team_link_export / 会话头部 ⬇ 按钮 |
| 📨 跨会话消息 | 投递给另一会话,空闲目标自动唤醒为新回合;返回带 busy 预判;目标没活动代理时以 ❌ 未投递 开头并列出同工作区存活会话 |
team_link_send |
| 📣 广播 fan-out | 一次投多个目标:会话 id / team:<name>/<role> / team:<name>/*(全队,仅现任协调者);任一目标未投递则返回首行即 ❌ N 个目标未投递 |
team_link_send 的 targets |
| 🏷 信封 banner | 可选 meta(type / pri / ref)渲染进投递 banner 首行,审计一眼看清消息性质 |
team_link_send 的 meta |
| 🔁 配对通道 | 双方各批准一次后,两个会话互发免确认 | 接收确认时选「配对」 |
| 🐕 跨会话看门狗 | 盯住别的会话:它失联而我空闲时,插件向我自己的会话投一条 tick | team_link_watch |
| 🎭 团队 roster | 团队 → 角色 → 会话的身份注册表,含版本史(退役≠删除)与写入策略 | team_link_roster |
| 📋 团队黑板 | decisions.md(只追加裁决账本)+ discipline.md(整文件替换,乐观锁)+ tasks.md(只追加任务台账);旧行可显式归档到 archive/(只搬不删、号不回卷) |
team_link_team_read / team_link_team_append / team_link_team_archive |
| 🔄 团队换届 | 两阶段交接:一次性令牌 + 域限定信任迁移 + 退役者对称吊销 + 24h 可回退 | team_link_rotate |
| 🚑 团队恢复 | 现任「有席位但无活代理」时的窄恢复路径:恰两个封闭动词(revive / reappoint),人在环 fail-closed(§11.9.4) | team_link_recover |
| 🚫 屏蔽的作用域(批 I) | 屏蔽是两个会话之间的一对(双向:A↔B 之后 A→B 与 B→A 都拒,不影响它们与别的会话的正常往来),并配解除出口(unblock / clear-legacy);旧格式的全局名单原样保留、不静默放开 |
team_link_block |
一条设计红线:绝不把「没投出去」说成「已发送」
这个插件里所有对外文案都遵循同一条纪律——拒绝要刺眼、降级要留痕、不确定要如实标注。三条具体体现(都是生产事故换来的):
- 目标没有活动代理 → 首前缀是
❌ 未投递,并列出同工作区其他存活会话(有人真把「1 投递 / 2 拒绝」读成「已广播」); - 广播只要有一个目标没到 → 首行就是
❌ N 个目标未投递(M 个已投递),逐目标明细排在后面; - 服务取不到时必须留一行 warn,绝不静默降级(见第七节——0.3.7 修的就是一次「静默降级了整整一天没人发现」)。
架构
插件是标准的 DSH bundle 插件,分宿主半边与浏览器半边:
flowchart TB
subgraph Shell["DSH shell(web profile)"]
direction TB
subgraph Host["宿主半边 · lib/index.js"]
T["12 个工具 + 2 条 / 命令<br/>list / export / send / watch / block<br/>roster / team_read / team_append / team_archive / rotate / recover<br/>status(只读状态卡)<br/>/team_session · /team_rotate"]
R["HTTP 读路由<br/>GET /team-link/export<br/>GET /team-link/panel(只读面板)<br/>同一道 connection 信任栅栏"]
W["看门狗巡逻定时器<br/>+ 换届到期清扫"]
DL["深链解析(上游功能)<br/>dsh://session/<id>"]
ST["policy store<br/>settings 命名空间 team-link<br/>(settings 缺席时 → 插件自己的 policy.json)"]
end
subgraph Client["浏览器半边 · lib/client.js"]
CARD["📡 消息卡片<br/>chat.node keyed slot"]
BTN["会话头部按钮<br/>复制深链 / 导出"]
SLOT["侧栏「会话工具」入口<br/>sidebar.footer.action<br/>任意会话:复制 / 导出 / 打开<br/>+ 团队面板分区(只读)"]
end
end
S1["会话 A(协调者)"] -->|team_link_send| T
S2["会话 B(worker)"] -->|team_link_*| T
T -->|steer / followup| S2
T --> ST
ST -->|持久化| YAML[("profile/settings.yaml<br/>team-link: …")]
ST -->|settings 缺席时的落点| PFILE[("<DSH_HOME 或 ~/.dsh>/team-link/<br/>policy.json")]
T -->|best-effort 镜像| BB[("<workspace>/team/<name>/<br/>roster.md · decisions.md · discipline.md · tasks.md")]
R --> CARD
BTN --> R
SLOT -->|导航 /team-link/export| R
要点:
- 宿主半边拥有全部工具、
/team-link/export下载路由、看门狗巡逻定时器与换届到期清扫;所有模型可见输出都过wellFormed()(字符串安全,见第八节)。 - 浏览器半边只做两件事:把跨会话消息渲染成醒目卡片(影子替换 chat 包默认的灰字行,非本插件消息委托回原渲染器),以及会话头部的「复制深链 / 导出」按钮。
- 状态的事实源先 settings 后文件:settings(命名空间
team-link)在场时它是事实源;不在场时同一份状态写在<DSH_HOME 或 ~/.dsh>/team-link/policy.json。一个时刻只写一处(不双写),<workspace>/team/<name>/*仍只是人可读镜像,写入失败只告警、不回滚事实源。落点可核:team_link_roster action=get与team_link_status的首行永远说得出此刻状态存在哪。 - 团队身份与信任关系(pairs)分开存:roster 管「谁是谁」,pairs 管「谁能免确认发给谁」——换届时两者都要动,但动作路径不同(见第五节)。
快速开始
场景:一个协调者会话 + 一个 worker 会话,同一个工作区目录。
sequenceDiagram
autonumber
participant U as 用户
participant A as 会话 A(协调者)
participant P as 插件
participant B as 会话 B(worker)
U->>A: 「建团队 night,我当协调者」
A->>P: team_link_roster action=upsert-team
Note over P: 创建路径把 A 播种为<br/>coordinator 现任(创建即认领)
P-->>A: 已创建;coordinator 已由 A 认领
U->>A: 「把 B 注册成 worker」
A->>P: set-role(role=worker, session=B)
P-->>A: 已设置(写权限门通过:A 就是现任)
U->>A: 「广播全队:报到」
A->>P: team_link_send targets=["team:night/*"]
P->>B: 📨 跨会话消息(steer/followup)
B-->>U: 收到卡片并响应
四步上手:
- 开两个会话(同一个工作区目录)。协调者用强模型,worker 用
flash即可;worker 的开场白一句话:「你是 worker,等主管派活」。 - 建团队(在协调者里说):「创建团队
night,我当协调者,把 B 注册为 worker」。创建即认领,不需要手改任何配置。 - 建信任通道(可选,但推荐):让 A 给 B 发一条消息 → 接收侧确认框里选「配对:双向免确认」→ 此后 A↔B 互发免确认。
- 派活与记账:「广播全队:……」/「这条记入裁决账本」/「更新纪律条款」。
常用话术速查:
| 想做什么 | 对模型说 | 背后工具 |
|---|---|---|
| 看团队与活性 | 「现在团队状态如何 / 列一下其他会话」 | team_link_list_sessions |
| 看团队一屏现状 | 「出一张团队状态卡」(可加「只看 night」) | team_link_status(六段+⑦形态;全只读) |
| 看崩溃现场 | 「上次 DSH 是怎么死的?」/「看一下崩溃现场(死前那一刻的读数)」 | team_link_status action=forensics(只留现场:死亡窗口 + 死前四类读数 + 免责句;不下崩溃结论 —— 那是人的判断) |
| 读到第 13 行 / 点名读某人 | 「列会话,从第 13 行起读」/「点名读 <会话 id> 的活性」 | team_link_list_sessions 的 offset / readIds(单次 ≤12 次面读) |
| 广播 | 「广播全队:<内容>」(仅现任协调者) | send + targets=["team:<n>/*"] |
| 点对点 | 「发给 worker1:<内容>」 | send + targets=["team:<n>/worker1"] |
| 按优先级/性质发 | 「以 P0 裁决回复 W1,引用我上条消息」 | send 的 meta 信封 |
| 记账 | 「这条记入裁决账本」 | team_link_team_append(decisions) |
| 改纪律 | 「更新纪律条款」 | team_link_team_append(discipline,带 baseHash) |
| 派活落账 | 「派给 W1 复核 §3 的行号,记一笔」 | team_link_team_append(tasks + kind=plan,返回 t-<n>) |
| 回报状态 | 「记一笔:t-7 我接了 / 声称完成 / 卡住了 / 存疑」 | team_link_team_append(tasks + kind=claim/done/block/dispute + task=t-7) |
| 看某个任务回过什么 | 「t-7 收到过谁的回报?我发出去之后对方动了没有?」 | team_link_team_read 的收件视图 + 派生回执(默认只读自己的会话面) |
| 盯人 | 「盯住 W1/W2,静默 15 分钟叫我」 | team_link_watch |
| 交班 | 「我下岗,让 session-X 接任」 | team_link_rotate(两阶段) |
| 切形态(多会话 ↔ agent-team) | 「把团队切到 agent-team 档」/「切回多会话档」 | team_link_roster action=set-mode(双重门:writer gate + 人类确认框;缺一即零写入) |
| 删团队(清掉不再需要的队) | 「这个队不要了,从名册里删掉」/「把 u9-verify-1012 清掉」 |
team_link_roster action=delete(人类确认框;已退役 / 没有现任协调者的队也能删;不删黑板文件、不动信任数据) |
| 归档 | 「导出这个会话」 | team_link_export / 头部 ⬇ |
| 解屏蔽(谁挡的、怎么解) | 「别屏蔽我了」/「这个会话怎么一直发不出来?」 | team_link_block(list 看当前屏蔽 · unblock a=… b=… 解一对 · clear-legacy sender=… 清一条旧格式记录)—— 拒绝文案里就写着这两个命令 |
工具一览
| 工具 | 一句话 |
|---|---|
team_link_list_sessions |
同工作区其他会话 + 活性信号行(verdict 五态 / goal / 静默时长 / 读数时效戳);默认读前 12 行的活性,要读更多用 readIds(点名)或 offset(分页)——单次调用 ≤ 12 次 surface 读,末尾给「未读 Y 行 + 怎么读」 |
team_link_export |
任意会话全量导出 md + JSON |
team_link_send |
跨会话投递(单目标或 targets 广播 ≤8);meta 信封;返回带 busy 预判;发送方自己那一行渲染成卡片(§10.1 A/D) |
team_link_watch |
给自己注册跨会话看门狗(register / update / mute / unmute / list / clear;update 就地改 silentMinutes / intervalMinutes / ttlHours / wake(TTL 从现在续租、定时器按新间隔重挂);mute / unmute = 临时静音投递(默认 12h、上限 24h;auto 行也可,注册与续期不动);clear 只清你自己手工注册的那些 —— origin: auto 的全队工作台注册拿不走(拒绝并点名归属),它只由 TTL 到期或团队解散消失;arm-team = 现任协调者一条命令幂等补挂自己队的工作台注册,非现任 / 队不在名册会被拒绝并点名原因) |
team_link_block |
屏蔽的作用域管理(批 I):list(看当前屏蔽:配对屏蔽与旧格式分两组,各带可执行出口)· unblock a=… b=…(解一对,双向立刻恢复;只有发起屏蔽的那一方能解 —— 被屏蔽方与第三方调用一律拒绝且零写入)· clear-legacy sender=…(按发送方逐条清一条旧格式记录 —— 旧的全局名单范围偏大、挡的是该发送方对所有会话的投递;被它挡住的发送方本人不得自清,其余任何会话可清;不做批量清)。解除不弹确认框(可逆、一次只解一条) |
team_link_status |
只读团队状态卡:一次一屏(角色在位/空缺 · 换届 pending(token 掩码)· 看门狗 · 会话面 · 活性(有界 12 行)· 台账尾 · 形态(⑦,B 批追加))+ 反面预警注记;零写入、零额外读。可选 action=forensics(批 K)= 读崩溃现场(心跳留证:死亡窗口 + 死前四类读数 + 免责句,只读;不带该参数时输出逐字不变) |
team_link_roster |
团队身份注册表(get / upsert-team / set-role / retire / set-mode / delete):get 附形态段(形态 + Lead + 成员名册投影 + 完整能力矩阵 + 诊断行),set-mode 切团队形态(见团队形态),delete 把一行从名册里真正删掉(人类确认框;豁免写权限门;不碰黑板文件与信任数据——见团队注册表的动作表) |
team_link_team_read |
一次读齐 roster + decisions 末 20 条 + discipline 全文 + tasks 末 20 条与派生视图 + 收件视图(谁对 t-<n> 说过什么)与派生回执(我发出去之后对方动了没有:✅ / ⚠ / 未读)+ 三个 baseHash;目标面只在 readIds 点名时才读 |
team_link_team_append |
写黑板三件套:decisions 只追加 / discipline 整文件替换(乐观锁)/ tasks 只追加(kind 闭集,plan 分配 t-<n>,kind/task 误用一律拒绝);★ 批 T:plan 的号码与「已注册」校验都取「活跃 ∪ archive/」 |
team_link_team_archive |
黑板归档(批 T,file ∈ {decisions, tasks} · keepTail? 默认 200):旧行搬进 archive/<file>-g<世代>-L<起>-L<止>.md(只搬不删 · 归档文件只写一次 · 命名确定性、不含墙钟);活跃文件留尾部 + 一行归档指针;一道确认框(取消 / 超时 / 无确认服务 ⇒ 零写入);最新形态史行永不搬走;分配与「已注册」校验取「活跃 ∪ 归档」⇒ 号不回卷、归档 id 仍认、伪造高号仍拒 |
team_link_rotate |
两阶段换届(prepare / claim),一次性令牌 + 域限定迁移;successor:"auto" = 插件自建继任者 + 写交接文档 + followup 投递(§11.2)。action=prepare-batch(批 S)= 现任协调者一条命令整队准备 Phase A:显式继任者用 successors="角色=会话 id, …"、自建用 successor:"auto"(二选一,v1 不做混合批次);一道确认框 · 一次写盘 · 一条批次级 freeze,Phase B 仍逐个本人 claim |
team_link_recover |
角色恢复(恰两个封闭动词):revive(复活当前现任那个会话本身,仅插件自建会话、任意角色,身份/信任零改动)/reappoint(人改任 = 人类对话授权的 prepare,候选 = 本队活成员 ∪ 常驻的「自建继任者(新建会话)」,由插件算出)。attended-only:无确认服务即 fail-closed,刻意没有无人值守变体(§11.9.4 / §4.2) |
team_link_export走sessionQuery读会话;team_link_send走agents投递。两者都不需要目标会话正在被 UI 打开——但目标必须有活动代理(见第一节的 A4 诚实声明)。
一、跨会话消息
投递语义:steer 还是 followup
flowchart TD
START["team_link_send"] --> Q1{"目标是同一工作区的<br/>存活根代理?"}
Q1 -->|否| NOAGENT["❌ 未投递<br/>+ 列出同工作区存活会话<br/>+ 三条核对提示"]
Q1 -->|是| Q2{"目标当前状态?"}
Q2 -->|运行中| STEER["steer<br/>在步边界注入当前回合<br/>返回追加「已运行 N 分钟」"]
Q2 -->|空闲| FOLLOW["followup<br/>唤醒为新回合<br/>消息立即可见并触发响应"]
目标运行中 →
steer:在步边界注入其当前回合;目标空闲 →
followup:唤醒目标会话,作为新回合处理(立即显示并触发 LLM 响应,不会静默排队);目标没有活动代理 → 拒绝,且拒绝文案自带自愈线索:
- 首前缀
❌ 未投递(不许被读成「已发送」); - 保留原句「目标会话
<id>没有活动代理」; - 新增同工作区存活会话列表:每行
id(运行中/空闲),上限 10 个,超出时注明「共 N 个,仅列前 10 个」; - 新增核对提示:对照 id(最常见成因是转录错位)/ 刚重启 DSH 时在侧边栏打开目标会话一次使其恢复为活动代理 / 先调
team_link_list_sessions(默认只读前 12 行的活性;要读窗口外的行,用新参数readIds点名读或offset分页——两者可选、互斥,单次调用 surface 读数仍 ≤ 12,展开见 §二)。
该列表是纯 agent 注册表读取(
ctx.agents.list(),不读会话日志、零 surface 读取、无性能代价),只列根代理、同cwd、且非发起者自身——子代理不会被列为可投递目标。执行上下文没有会话身份时(插件内部通知路径)只知道「非自身」,此时跳过 cwd 过滤、列出力所能及的全部存活会话;无匹配时输出「当前工作区无其他存活会话。」- 首前缀
消息形状:为什么 source 必须恰好三个成员
投递的消息 source = { kind: "agent-message", form: "relay", senderSessionId }——恰好这三个成员。这不是风格选择:DSH 0.1.5 的会话日志迁移会逐条校验 source,白名单外的 kind 或成员多一个少一个,会让整份会话日志拒绝迁移(表现为「这个对话打不开」)。详见第八节。
发送方、时间、插件名都写在正文 banner 里:
📨 [跨会话消息 · 来自会话「X」(session-x) · 2026-09-17 23:42:05 · type=ruling pri=P0 ref=slp-a1b2]
接收方模型可直接看到,并可用同一工具回发。消息 id 固定为 slp-<uuid>,UI 卡片靠它把自己的中继与上游相邻代理消息区分开。
「三成员」有三个构造点,不是一个(差异审计修复轮 🔵-3 的措辞修正):本插件对 {kind, form, senderSessionId} 这个形状有 3 处字面构造——relayUserMessage(本插件驱动的那些中继:§10.2.3 启动任务、§11.4.5 交接投递、§11.2 命令指令共用的一处构造)、看门狗 tick(tickMessage,消息 id 前缀 slp-wd-、发送方是观察者自身)与 §3.4 的 team_link_send 投递(正文是信封 banner)。三处都恰三成员;relayUserMessage 的注释原先概括成「全模块唯一构造器」,那句只对「本插件驱动的中继」成立,对全模块不成立(已在注释里改成带范围的表述)。改这个形状要三处一起改——任何地方冒出第四处字面构造才是缺陷。
两道批准门与配对
flowchart TD
S["要投递"] --> BLK{"屏蔽检查(接收方视角,批 I)<br/>① blocks 里有同时含<br/>我和发送方的那一对吗?<br/>② 旧格式 blockedSenders 含发送方吗?"}
BLK -->|命中| REF["refused(屏蔽优先于一切;<br/>文案点名谁挡的 + 怎么解)"]
BLK -->|未命中| PAIR{"双方已配对?"}
PAIR -->|是| D["直接投递"]
PAIR -->|否| G1["门 1 · 发送方确认<br/>发送 / 记住该目标免确认 / 取消"]
G1 --> G2["门 2 · 接收方策略(receiveMode=ask)<br/>接收 / 总是接收该发送方<br/>配对:双向免确认 / 拒绝并屏蔽"]
G2 -->|配对| D
G2 -->|拒绝并屏蔽| REF
G1 -->|取消| C["取消(超时约 3 分钟同样按取消处理)"]
- 接收方选「配对」→ 写入
pairs: [{a, b, createdAt}],此后两会话双向免确认; - 选「拒绝并屏蔽」→ 写入
blocks: [{a, b, by: <点屏蔽的那一方>, since}](两个会话之间的一对,双向)并自动解除配对——屏蔽始终优先于配对;只有发起屏蔽的那一方能解(team_link_block action=unblock a=… b=…)。旧格式的blockedSenders(部署级全局名单)原样保留、不再新增,检查时并集参与(旧记录不静默放开),出口是team_link_block action=clear-legacy sender=<id>; - 超时(约 3 分钟)按取消处理;确认服务不可用时逐目标 fail-closed(宁可不发,不默认放行)。
广播 fan-out(targets)
targets 与 targetSessionId 互斥:两者都给是参数错误,都不给也是参数错误(单目标语义不变)。
targets 项 |
解析 | 谁能用 |
|---|---|---|
session-xxx |
直达该会话(最高优先级) | 任何会话 |
team:<name>/<role> |
该角色的现任会话 | 任何会话;角色空缺/不存在 → no-holder 结果(不算投递也不算失败) |
team:<name>/* |
全队:该团队全部在任且存活的角色(不含发起者) | 仅该团队现任协调者,否则整次调用拒绝 |
team:<name>/* 之所以收得这么紧:协调者的价值部分在于策展每个 worker 看到什么,而 flash worker 最稀缺的资源是上下文——全连通群播会让 worker 的上下文互相污染。
单次最多 8 个目标,超出即拒绝;团队不在 roster 或表达式形状非法 → 整次调用拒绝(不做「半发」);
fan-out 不放宽任何门:每个目标照走完整单目标路径(屏蔽检查 → 配对快路径 → 发送方确认 → 接收方策略 → steer/followup)。N 个未配对目标就是 N 次批准;
返回逐目标结果行:
- session-b(via team:night/worker1) → delivered:已投递(目标已空闲,唤醒为新回合) - session-c(via team:night/worker2) → refused:接收方拒绝 汇总:1 投递 / 1 拒绝 / 1 个重复目标已去重。失败领先:只要有一个目标不是
delivered,第一行就是❌ N 个目标未投递(M 个已投递)(N 含 refused / no-agent / no-holder,用词是「未投递」而非「失败」,故与汇总里 no-holder 的独立桶不矛盾),其后才是广播 fan-out:…头行、逐目标行与汇总。全部成功时文案形状一字不变;重复的会话 id(含不同表达式解析到同一会话)去重后只投一次,去重个数写在汇总里。
信封 banner(meta)
可选 meta: { type?: 'ruling'|'receipt'|'report'|'ask', pri?: 'P0'|'P1'|'P2', ref?: string } 渲染进 banner 首行的紧凑字段,只出现调用方给的键:
📨 [跨会话消息 · 来自会话「X」(session-x) · 2026-09-17 23:42:05 · type=ruling pri=P0 ref=slp-a1b2]
ref超过 16 字符按码点截断(不会切半 emoji),并在返回文案里注明截断前后;- 枚举外的
type/pri、未定义字段、非对象meta、空ref、含换行的ref→ 明确参数错误、整次调用拒绝(不静默丢弃、不部分采用); - fan-out 时所有目标共享同一
meta; source仍是恰好三成员:信封只走正文 banner,不扩 source、不做 sidecar 索引。
busy 预判
投递成功的返回文案附带目标忙碌状态(fan-out 逐目标独立):
- 目标运行中 → 追加
目标回合已运行 N 分钟(steer 注入当前回合);需新回合语义请等其空闲。N取自活性行的「回合始于」,读不到该时间戳时只给 steer 语义、不给分钟数; - 目标空闲 → 保持原文案(已唤醒为新回合)。
- 发送方卡片上是同一读数的徽标形态(不是上面那句机制文案):
忙碌 · 已运行 N 分钟,读不到回合起点时只说忙碌中——见「发送方卡片」一节的「A 面逐目标行的措辞」。
消息卡片(浏览器侧)
接收方 UI 把跨会话消息渲染为醒目的 📡 卡片(📡 标题行 + 高亮左边条 + 发送会话 + 时间):通过 conversation.chat.node keyed slot 以 priority: -100 影子替换 chat 包默认的折叠灰字行;非本插件消息(其他插件的 context 注入)经 slots.entries() 委托回原渲染器,显示不受影响。
判定条件不是「kind/form 命中」而是本插件自己的消息:agent-message + relay 正是上游相邻代理消息(send_message)用的形状,只按 kind/form 判断会把它们也渲染成卡片。因此卡片还要求命中本插件自己的特征之一——消息 id(在 chat node 上是 node.id,context 的 data 里没有 id)以 slp- 开头,或正文以 📨 [跨会话消息 开头;历史日志里的旧 kind: "team-link" 继续识别。卡片时间优先取旧日志的 sentAt,其次取 context node 自带的事件时间 data.time,最后才从正文 banner 里解析。
发送方卡片(team_link_send 自己那一行)
上面那张卡覆盖的是接收方;发送方过去只看到工具树里一行灰字。现在发送方也有卡,两条腿都在客户端:
每一块信息只准出现一次(§10.1.5 硬性条文,差异审计 F1 后写死):两张卡的可见文本取并集后,任一语句恰好出现一次。判据由 client-half.test.mjs 的 F1 三条断言把守——把任意一块搬回另一面,它们当场变红。
| 腿 | 位置 | 槽位 | 承载(且只承载这些) |
|---|---|---|---|
| A | 工具调用原地(审计记录不动) | tool.call.toolview,key = 线上工具名 team_link_send(逐字;typo 会静默回退通用工具行、不报错) |
极简标签(✦ 工具调用 · team_link_send · N 个目标)+ 逐目标行:每行「目标(expr 或短 id)+ outcome 的一句人话短语(从结构化字段渲染,见下「A 面逐目标行的措辞」)」,运行中的目标带 busy 徽标。不渲染标题/时间/正文/汇总,也不渲染 target.detail(那句是模型可见的报告句) |
| D | 会话流顶层 | 本插件自己的 uiConversation definition(kind team-link-send)+ 同 kind 的 conversation.chat.node(priority: -90) |
标题 + 发送方/时间 + 正文 + 汇总计数。不渲染逐目标明细行(目标身份归 A 的行)。与接收方的 key: "context" 卡片kind 不同,并存不冲突 |
A 面逐目标行的措辞(2026-09-20 用户决定;设计 §10.1.5 修订 + §12.5):A 的每一行不再原样搬运 target.detail——那句是模型可见的报告句,带「已投递到 …」「目标处于空闲」「steer 注入当前回合」这类投递机制,对人看的卡片偏机制而非结论,且一个目标就占 2–3 行。改为从结构化字段渲染一句人话:
target.outcome→ 一句固定短语(客户端OUTCOME_PHRASES映射到 zh/en 两套 locale 键,不是硬编码):delivered→ 「已送达」、refused→ 「未送达——接收方拒绝」、no-agent→ 「未送达——目标会话没有活动代理」、no-holder→ 「未送达——该角色当前空缺」;target.busy→ 徽标:「忙碌 · 已运行 N 分钟」(读不到回合起点时只说「忙碌中」,不编数字);- 目标身份 →
expr或短 id(不变)。
于是一行 = 目标 + 一句结论 +(忙碌时)徽标,长报告句(含机制、含 busy 的 steer 文案)继续留给模型可见的文本。这不违反「一处事实」:报告句与卡片短语是同一批结构化字段的两种渲染(outcome / busy / 身份),target.detail 仍留在回执里(模型可见文本的事实源 + 纯文本降级路径的兜底),只是不再是 A 的渲染输入。
实测对照(同一次投递,team:night-shift/* 广播;「改前」句取自 client-half.test.mjs 的旧期望值 fixture——那行 80 字符、卡片按 pre-wrap 折成 2–3 行,「改后」行 44 字符) |
文本 |
|---|---|
| 改前 A 面逐目标行 | session-worker-a(via team:night-shift/*) 已投递 已投递到 session-worker-a:目标空闲,已唤醒目标会话。 |
| 改后 A 面逐目标行 | session-worker-a(via team:night-shift/*) 已送达 |
跨半边行为锁(取代 B1 的字符串等式):旧的「卡内行 == 报告首行」断言已作废(它钉的是一个已被设计替换的渲染,且同源化之后由构造保证永不红,§12.3 ⑤)。现在锁的是行为:宿主半边能铸出的每个 outcome 枚举值都必须有客户端短语——host-half.test.mjs 同时读 lib/index.js 与 lib/client.js,把宿主侧的字面量 token 集合与客户端 OUTCOME_PHRASES 的键判等;新增一个枚举值而不给卡片短语 → 必红(红相实测见「测试」一节的对应条目)。测试报告里会打印两侧的实测集合。
数据来源是官方载体,不是解析返回文本:宿主半边给 team_link_send 加了 output.presentationMeta,产出的结构化回执落在 tool/result.meta 里(durable——回放同一份日志会重建同一张卡):
{ kind: "team-link-send", v: 1, at, senderSessionId,
meta?: { type?, pri?, ref? }, // 仅调用方给了信封才有
message: { text, truncated, chars }, // chars = 原始码点数
targets: [{ expr?, sessionId | null, outcome, detail, busy? }],
targetsTruncated?: { shown, total }, // 仅 targets 真被裁到 24 行才有
summary: { delivered, refused, noAgent, noHolder, deduped }, fanout }
- 体积纪律(两处上限,各自如实标注):
- 正文:
message.text上限 2000 码点,超出则取头 1500 + 省略标记 3 码点 + 尾 400(1903 码点,仍在限内)并置truncated: true;chars记原始码点数。裁剪与计数都按码点; - 逐目标行:宿主侧(权威)
targets封顶 24 行(SEND_CARD_ROW_LIMIT,= §10.2.4 的每队成员上限,全队广播仍可整份渲染)。界是行数不是表达式数:输入侧resolveTargetList裁的是表达式(≤8),而一个team:<name>/*就展开成该队全部在册存活成员,所以合法的行数可以超过 8——持久化的卡必须有界,tool/result.meta里被烙进日志的正是卡的targets。超出时卡内显式标注targetsTruncated: { shown: 24, total }(shown= 裁完实际呈现的行数——客户端sendRowsTruncated「已截断——仅显示前 {shown} 行」的口径同样是实际画出的行数,良构宿主卡上两者同值;total是这次投递真实的行数),而summary计数仍覆盖全量、文本报告仍逐目标完整——卡是有界呈现,报告是全量档案; - 客户端(防御 + 如实呈现;2026-09-19 收尾轮补齐跨轮读路径):A 面渲染期另设同值上限,因为
meta核心不透明且持久化,手改日志或异构实现可塞进任意条数。这条判据对宿主自产的卡永远不触发(宿主已先裁到 24,行数恰好落在界上),所以 A 面同时读宿主自己的targetsTruncated标注:只要卡上有该标注,或行数确实超过本地上限,A 面就渲染sendRowsTruncated「已截断——仅显示前 {shown} 行」——两类超限都在界面上看得出来。两处数字各有各的口径:{shown}是实际画出的行数(不是标注里写的数字——手改的shown不得在那个句子里安一个假数字),标签的「N 个目标」取真值——有标注时是targetsTruncated.total(30),无标注时是该卡自己的行数(外来卡的 30 行就是真值;而只画出的那 24 行不得当真值)。标注本身与其它meta字段同等不信任:形状不认(shown/total非有限数)即丢弃并退回「按行数判超限」的防御路径,卡照常渲染。无标注且 ≤24 行的卡可见文本与加这条读路径之前逐字一致(不增删任何文本);
- 正文:
- 良构(差异审计 F2 修正):卡内的每一个字符串成员——
message.text、逐目标的sessionId/expr/detail、senderSessionId、信封的ref——都在制卡时过一遍孤立代理项修复。卡不走textOutput.render,模型可见出口盖不住它,所以这条线必须逐个字段自己守住;回归锁在同一声明的三条断言上(污染sessionId/expr/meta.ref后JSON.stringify(card)无孤立代理项); - 降级:拿不到回执时(调用仍在飞、旧日志没有
meta、meta形状不认识、其他工具的 meta)先走「文本重建」,重建不出来才回退纯文本行(三种情形的判定表见下面「三种情形」);整次调用在走到逐目标投递之前就被拒(寻址互斥 / 无地址 /meta非法 / 执行上下文没有可交互的活动代理)时不产出卡,客户端回退文本——绝不为没发生的投递编造回执。注意区分:目标无活动代理(outcome: "no-agent")发生在投递阶段内,照常出卡,那一行就是那条❌ 未投递拒绝; - 零日志改动:A/D 都只读既有的
tool/call+tool/result事件,不新增任何日志事件类型(§10.3 红线);投递消息的source仍恰三成员;模型上下文无新增消息。
三种情形:卡片 / 文本重建 / 纯文本行(§10.1.5,DEFECT-5)
A 面(tool.call.toolview)逐块判定,顺序即优先级:
| # | 情形 | 判据 | A 面呈现 |
|---|---|---|---|
| ① | 有结构化回执 | tool/result.meta 读得动(kind: "team-link-send"、v: 1、字段齐) |
卡片,数据全部来自回执。文本重建在这一情形里永不参与(下面 ① 号锁) |
| ② | 无回执(或回执读不动),但文本认得 | 模型可见文本是我们自己产的两种形状之一 | 文本重建的最小卡:目标身份 + 结果短语 + 汇总计数从文本解出;at / 发送方 / 正文 / busy / detail 不造(文本里没有这些字段) |
| ③ | 两者都不成 | 在飞、无文本、认不出的句子、跨行/改写/自相矛盾的报告 | 纯文本行:原样显示模型可见文本,一行不吞、一处不改 |
② 什么时候会发生(实测定性,不是推断):presentationMeta 只在一等工具调用上投影(dsh-tools/lib/types/index.js:1191 的 exec.parent === undefined),所以在 run_code 程序里发起的 team_link_send 永远没有 tool/result.meta——桥为那次派发记的是 tool/ptc-dispatch,其字段恰为 {rootCallId, parentCallId, subCallId, name, arguments, isError, content}(没有 meta),客户端 chat 包据此建的块(childResult)也不带 meta。只读全量扫描本机 1311 个会话日志:team_link_send 的 ptc-dispatch 733 次、带 meta 的 0 次;同一批里 tool/result 带本插件回执的有 11 条(都来自一等调用)。⇒ 此前从代码里发出的那一次投递永远是灰行,而直接调用的一直有卡。
② 认的两种形状(我们自己的渲染器逐字产出,lib/index.js):
- 单目标成功:
已投递到 <sessionLabel>(<通道说明>):<细节>——sessionLabel是id或「标题」(id),通道说明是配对 / provisional 两种之一(也可以没有)。解析时按形状剥掉这两层,不硬编码那两句提示语; - 广播报告:可选的
❌ N 个目标未投递(M 个已投递)首行 +广播 fan-out:N 个目标[(重复目标已去重 N 个)]+ 逐目标行- <目标> → <结果>:<细节>+汇总:N 投递 / N 拒绝[ / N 无活动代理][ / N 空缺目标(no-holder,不计入投递与失败)][ / N 个重复目标已去重]。+ 可选的注意:…尾注。逐目标行的<细节>可以跨行(no-agent的拒绝是一整段,自带缩进列表),那些续行属于同一行,不会变成额外的目标行。
② 是「全有或全无」,并且自校验:报告里的每个结构事实都要与其它事实互相印证才成卡——行数 == 表头声明的目标数、汇总 四个桶 == 逐行自己的计数、去重数(表头与汇总两处)一致、❌ 首行有且仅当存在非 delivered 的行、且它复述的两个数字与逐行计数相同。任一处不符(含标签认不出、outcome 落在四种之外)⇒ 整条回落 ③,而不是丢掉那一行或猜一个桶。理由就是「不伪造」:单目标被拒的句子(未投递:… / 发送失败:…)根本不含目标身份,为它出卡就必须凭空造出「目标」这个字段——按设计那句话(缺的字段不造)它只能留在 ③。
重建出来的卡只在 A 面:D 的顶层节点由 tool/result.meta 的判别符经 definition match 驱动,所以文本重建永远长不出顶层卡。
已知边界(如实声明):presentationMeta 只对顶层工具调用投影(exec.parent === undefined),所以从 run_code 程序里发出的 team_link_send 拿不到结构化回执——那一行走的是上面的文本重建(② 情形),重建不出来才是纯文本行;D 的顶层节点在 chat 包把「回合过程」折叠起来时可能随之被折进去(tool-call 节点本身也是这个待遇)——tool/call 滚出历史窗口、只剩 tool/result 时按 context.matches 回退重建,卡片不会在长会话里凭空消失。客户端半边对 uiConversation 不是硬依赖(审计 F3):模块级 inject 只有 slots/sessions/locale,definition 走 ctx.inject(["uiConversation"], …) 动态注册,因此缺该服务的老壳只丢 D 的顶层卡——接收方卡片、工具行、复制/导出按钮、深链打开器全部照常;四条槽位注册(header 按钮条 + 三条 §10.1)各自加护栏(审计 B3;第四条为 round-1 🔵 #3 补齐),任一条被槽位拒绝也只丢那一行。
诚实声明(A4)
跨会话投递只能送达有活动代理的会话:目标已关闭、或刚重启 DSH 后尚未在侧边栏打开过,都没有任何机制能唤醒它。插件能做的只有如实告知 + 给出恢复动作,而不是假装发送成功。
二、会话可视:列表 / 深链 / 导出
team_link_list_sessions 的活性信号
每个会话行带一条活性信号(读一次 surface + 一次 agent 查询,纯服务调用,不解析日志)。
读取是有窗口的:只有列表前 12 个会话(PREVIEW_SESSIONS,与主题/最近摘要同一个窗口)会被读 surface,且这 12 次读取并行发出。第 13 行及以后照旧列出(id / 运行状态 / 创建时间 / 读数时效戳——全是零日志成本的面),但活性行降级为 活性:未读(超出快照窗口 12)……:verdict / 静默时长 / goal / 主题 / 最近动态一律标为未判定。
要读更多 = 点名或分页(两个可选、互斥的参数) —— 12 是预算(一行 surface = 一次冷日志解压 + 一次投影),不是能力上限;所以默认值不动,把「读哪些」的决定权交回调用方:
| 参数 | 语义 | 边界(一律拒绝,不静默截断) |
|---|---|---|
readIds?: string[] |
点名读:把指定会话强制纳入本次读窗(用于窗口外的行) | 去重后 ≤ 12 个,超出即拒绝且本次零读;只接受本次列表内的会话 id,未知 / 不属于本列表的 id → 拒绝并指出那个 id;点名优先占额,其余按原顺序补足到 12;本就在默认窗内的 id 只占一个额、不重复读;与 offset 互斥 |
offset?: number |
分页:从第 offset+1 行起读 12 行(默认 0) |
必须是整数;负数 / 越界(≥ 列表行数)→ 拒绝并给出有效区间 0..行数-1;与 readIds 互斥 |
成本不变量(按工具分账,断言锁死):team_link_list_sessions 的三条路径(默认 / offset / readIds)单次调用都 ≤ 12 次 surface 读;team_link_team_read 的收件视图 + 派生回执 ≤ min(12, 1+|readIds|)(自读占 1 个额);team_link_status ≤ 12。
列表末尾的「未读 Y 行 + 怎么读」提示段(默认调用也带,是提示不是载荷):把「未读」从终点变成下一步 —— 形如 未读 2 行(读窗 12/共 14 行):读第 13–24 行 → offset=12;或点名 → readIds=["session-x", "session-y"]。既有行结构一字未改,本次唯一的格式新增就是这一段(外加新参数)。
为什么要有界:一行 surface = 一次冷日志解压 + 一次表面投影。真实工作区实测(26 个会话)逐个串行读满
LIST_LIMIT(50)会直接超掉 60s 工具预算——0.3.5 修的就是这个(修前超时、修后约 15s)。这里选择有界 + 并行 + 如实标注,而不是编一个没读过的判定。
窗口内的字段:
| 字段 | 含义 |
|---|---|
verdict |
五态判定(见下) |
代理 |
运行中 / 空闲 / 未运行(读 agent 注册表) |
goal |
<phase>/<activation>(<已用轮次>/<上限>);blocked 另带 blocked=<code>: <message>;none = 当前无 goal;? = goals 服务缺失(降级运行,插件功能不受影响) |
静默 |
now - max(末条 assistant, 末条入站)(分钟);两侧时间戳都读不到时显示 ? |
| 回合始于 / 末条助手 / 末条入站 | 绝对时间戳(本地时区) |
verdict 五态(阈值:静默 10 分钟、回合 30 分钟;team_link_watch 的 silentMinutes 只影响巡逻判定):
flowchart TD
SIG["读信号:代理状态 · goal phase/activation · 静默时长"] --> D{"有存活代理?"}
D -->|无| DEAD["<b>dead</b><br/>会话已关闭:只有用户能处理"]
D -->|有| RUN{"运行中?"}
RUN -->|是| LONG{"当前回合 > 30 分钟?"}
LONG -->|是| LR["<b>long-running</b><br/>可能卡住,值得看一眼"]
LONG -->|否| OK1["<b>ok</b>"]
RUN -->|否,空闲| G{"goal 状态?"}
G -->|"active + armed"| OK2["<b>ok</b><br/>它有自己的续跑节拍"]
G -->|"active + disarmed"| GD["<b>goal-disarmed</b> ⚠️<br/>根因级静默:activation 不持久化<br/>重启 / max-tokens 结束 / agent error<br/>都会落到这里,且不会自愈"]
G -->|"paused / blocked / complete"| OK3["<b>ok</b><br/>已被解释的静默(在等人类决策)"]
G -->|无 goal| S{"静默 > 10 分钟?"}
S -->|是| SI["<b>silent-idle</b><br/>P1 场景:协调者在等 worker 回报"]
S -->|否| OK4["<b>ok</b>"]
goal-disarmed 是这个插件最想让你看见的状态:它看起来「空闲」,其实是根因级静默——goal 还是 durable-active,但驱动器不会再排队,除非人类(经模型转告后)显式 resume。插件绝不代调 goals.resume(那是人类授权门),只负责把状态摆出来、并给出合规的恢复回路。
读数带时间戳:每行行尾是 (读数 YYYY-MM-DD HH:mm:ss,>2min 作废)——活性是快照,超过 2 分钟须重新读。
深链(上游功能)
复制 dsh://session/<id>,粘贴到任意会话即注入该会话的只读快照作为上下文。这是上游 dsh-session-link 的能力,本仓库保留其解析行为不变(register-protocol.ps1 / dsh-open.cmd 负责协议注册与打开)。
聚焦这一步(打开之后真的切到那个会话)0.3.9 起修复:此前用的是 ctx.sessions.open(id)——会话服务上没有这个方法(同名的 Session.open() 是历史加载,另一回事),调用抛错被 try/catch 吞掉 ⇒ 静默空调用;见侧栏「会话工具」入口一节的末段。
team_link_export
任意会话全量导出:markdown(人可读,含信封首行)+ JSON(无损事件流)。同一个导出能力也挂在 GET /team-link/export?session=<id>&format=md|json(会话头部 ⬇ 按钮消费),路由与工具共用同一个文件名安全不变式(fileSafeSessionId())。
下载路由走平台的信任栅栏(0.3.9 起):路由只注册给同时拿到 webServer 与 connection(栅栏服务)的宿主,并且每个请求都先问一次 connection.requestRejection(req)——Host/Origin 栅栏(跨站/DNS rebinding → 403)与浏览器鉴权(未登录 → 401)都由平台裁决,裁决结果原样写回(响应体与官方的 RPC 通道一致:401 unauthorized、403 forbidden);被拒时不读会话。栅栏取不到(服务缺席、没有 requestRejection、或它在挂载之后消失)⇒ 503 + 不吐数据:宁可没有这条路由,也不要一条无门路由。非 GET 方法一律 405(allow: GET)。同源且已登录的浏览器下载行为不变。
侧栏「会话工具」入口(0.3.9 起)
侧栏底部(同一 flex 行内、位于消耗卡片右侧 —— order: 0 升序 ⇒ 排在 order: -10 的消耗卡之后、【设置】之前)多一个入口,点开是任意会话的列表:复制链接 / 导出会话 / 打开会话——不必先进入那个会话。入口注册进官方槽位 sidebar.footer.action(不改任何官方文件)。该位置已由 owner 在真机确认保持(同一 flex 行、消耗卡右侧;「改到下方」物理上不可达 —— 见下表「为什么不是下方」)。
| 面 | 行为 |
|---|---|
| 位置与形态 | 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 截图)与之一致:入口渲染在消耗卡右侧。宽态=图标 + 文字「会话工具」,收起态(56px 轨道)只渲染图标,文字转 aria-label/title |
| 弹窗 | 官方 Modal(居中、挂 body——收起态轨道只有 56px,锚定面板会被裁切):标题 + 当前计数 → 搜索框 → 会话列表(可滚动)→ 有界呈现标注 → 底部(范围切换 + 关闭) |
| 数据源 | ctx.sessions.list / ctx.workspaces.list:丢弃 origin === 'subagent'、丢弃已归档、丢弃 blank 行、丢弃当前会话(本面板的用途是「其他会话」——当前会话的复制/导出已在会话头部按钮上;丢弃它之后官方那条「blank 行只在它是当前会话时保留」自然退化为「blank 行一律丢」),按 updatedAt 倒序;当前会话 = retainedBy.mainView > 0 的那一行(官方同款约定,只用于定位当前工作区与把它从列表里剔除);默认范围 = 当前工作区,底部可切「全部工作区」 |
| 列表行 | 状态点(恰两态:运行中 / 空闲,唯一数据源 SessionSummary.running)+ 标题(超长省略)+ 相对时间(切「全部工作区」时追加工作区名);「复制链接」「导出会话」默认隐藏,鼠标悬停该行或该行获得键盘焦点才浮现;点行本身 = 打开该会话 |
| 三个动作 | 复制链接 = 剪贴板 dsh://session/<id>(与会话头部按钮同一格式)+ 短暂「已复制 ✓」+ aria-live 播报;导出会话 = 导航 GET /team-link/export?session=…&format=md(同源自动带 cookie、天然过栅栏、零 CORS 面;参数必须编码);打开会话 = ctx.uiWorkspace.openSession(id) |
| 三种空态 | 三句不同的话:读取中… / 没有匹配「xx」的会话 / 暂无其他会话(「还没读到」「搜不到」「真的没有」不许混) |
| 有界呈现 | 列表上限 50 行;超过时在列表末尾写明「共 N 个,仅显示前 M 个(搜索可收窄)」 |
| 服务缺失 | sessions / workspaces / uiWorkspace(以及 seed 模块的 Modal)缺任一项 ⇒ 入口不注册 + 一行 warn,绝不渲染一个点了没反应的假按钮;其余面(深链、消息卡片、头部按钮)照常 |
| 键盘与无障碍 | Tab 进入、Enter/Space 打开;关闭(Esc / 关闭按钮 / 成功打开)把焦点还给入口;动作按钮带独立无障碍名(含会话标题);尊重 prefers-reduced-motion |
| 边界 | 只做「看 / 复制 / 导出 / 打开」——不做会话写操作(改名 / 分叉 / 归档),不加右键菜单,不动会话行内菜单 |
| 团队面板分区(C 批,0.4.1 起) | 同一个弹窗里、会话列表之后的一个只读分区(不新造 Modal、不新造入口 —— 宿主 Modal 有独占交互契约,侧栏入口已经在且不与对话抢焦点)。取数走只读路由 GET /team-link/panel(与导出路由同一道栅栏:connection.requestRejection ⇒ 无 token 401 / Host-Origin 不符 403 / 仅 GET / 非 GET 405)。六段:落点行 → 团队与角色 → 换届 pending(token 掩码)→ 看门狗 → 台账尾(末 N 行 + 「最后主张 / 未消解存疑」派生读数)→ 会话面/读窗(未读行显示为未读 + 只读翻页)。一次渲染 = 一次路由调用(与工具面同一个「≤12 次面读」不变量),只在打开 / 手动刷新时拉,不轮询(无定时器)。降级:整条路由不可用 ⇒ 面板照常打开并显示「⚠ 面板数据不可用:<原因>」;单段取不到 ⇒ 该段显示「⚠ 本段不可用:<原因>」。渲染归属:落点行三态、台账派生读数、活性 verdict 都由宿主渲染好整串,客户端只显示不重算(「同源」由构造保证) |
已知限制:
- 收起侧栏 + Windows 标题栏模式(
[data-windows-titlebar])下,官方 CSS 会隐藏整个footArea(连同【设置】)⇒ 本入口一并不可见;展开侧栏即可用。 - 状态点刻意只有两态:第三态「无活动代理」是宿主侧事实(宿主半边读
ctx.agents.get(id)),而客户端公开面里SessionSummary没有 liveness 字段、SessionProjectionMap的三个键也没有 ⇒ 画第三态只能靠编造读数,因此不画(官方侧栏自己的状态点同样不含 liveness,口径一致)。⚠ 前提更正(C 批,2026-09-27):本条原文写的「本插件浏览器半边没有任何跨半边取数通道」已被取代 —— 通道一直是GET /team-link/export(下载路由)那条;C 批把它扩成一对只读路由,新增GET /team-link/panel(同一道栅栏)。但「会话列表的状态点仍是两态」这条结论不变,理由换成可核的那一条:会话列表渲染的是SessionSummary(客户端快照),而活性读数走的是面板那条路由(宿主渲染、一次一屏),二者不同源,所以列表行的点不冒充活性(面板自己那一段照旧给真 verdict / 「未读」)。 - 入口的可用性取决于三个客户端服务都在场(见 §九「浏览器半边的模块声明」);缺席时按上表「服务缺失」降级。
深链聚焦修复(§4.4,0.3.9 起):用 dsh://session/<id> 打开会话时,最后那步「切到该会话」此前从未生效——lib/client.js 调的是 ctx.sessions.open(id),而会话服务(ISessions 公开面)没有 open 方法(同名的 Session.open() 是历史加载,另一回事),调用抛出的 TypeError 被外层 try/catch 吞掉 ⇒ 静默空调用。现在改用公开导航面 ctx.uiWorkspace.openSession(id),运行时经 ctx.inject(["uiWorkspace"], …) 取用(缺席只丢「聚焦」这一步,不阻断打开),「等会话出现在 sessions.list 再聚焦」的重试循环原样保留,失败只留一行痕。
三、跨会话看门狗
看门狗解决的是「我(协调者)在等 worker,但没人叫醒我」——它反过来:盯住别人,别人失联且我空闲时叫醒我自己。
tick 策略
flowchart TD
P["巡逻"] --> W1{"观察者自己<br/>运行中 / armed-active?"}
W1 -->|是| SKIP1["不 tick<br/>绝不打断运行中的回合"]
W1 -->|否| W2{"观察者代理还在?"}
W2 -->|否| SKIP2["不 tick<br/>标「观察者=dead(等待用户)」<br/>注册随名册续期(见下)"]
W2 -->|是| T{"目标 verdict / goal 状态"}
T -->|目标 armed-active| S3["不 tick<br/>它有续跑节拍"]
T -->|silent-idle| TK["tick<br/>同一静默期最多一次"]
T -->|goal-disarmed| TK2["<b>立即</b> tick(不等静默阈)<br/>载荷带诊断 + 合规 resume 回路"]
T -->|dead| TK3["tick<br/>但唤不醒它:标 dead 等用户"]
T -->|paused / blocked / complete| S4["不 tick<br/>只在活性行展示"]
| 目标 goal 状态 | tick? | 理由 |
|---|---|---|
armed + active |
永不 | 它有自己的续跑节拍,tick 只稀释节奏 |
active + disarmed |
立即(不等静默阈) | 根因级静默态;载荷带诊断与合规恢复回路(转告用户 → 用户授权 → 模型自己 update_goal(action:"resume")) |
paused / blocked / complete |
不 tick | 在等人类决策 / 已完结 |
| 无 goal | 静默超阈才 tick | P1 场景 |
观察者侧:自己运行中或自身 armed-active 时不 tick;自己代理不存在时不 tick、不改注册(该会话在活性行里本来就是 代理=未运行,watch list 另标「观察者=dead」——代理回来了就自然恢复投递)。注册本身的寿命与观察者活不活无关(批 G 起):全队工作台那条自动注册只要名册依据还在就一直在,手工注册仍是你自己定的 TTL(见下面「注册的一生」)。
限制与实现约定
register只能给自己注册(观察者 =exec.agent.id),且targets不能含自己(自指等于变相的自 tick 定时器);- 单会话最多 3 个注册;
silentMinutes >= 10(默认 10);intervalMinutes >= 5(默认 5);ttlHours <= 24(默认 12,到点自动清理);clear幂等,且只清你自己手工注册的那些(origin: auto的全队工作台注册清不掉:点名id会得到一句「这是全队工作台的自动注册、未清除」的回执,不带id时它原样留在注册表里并如实报出还有几条未清除); - source 三成员不变:
{ kind: "agent-message", form: "relay", senderSessionId: <观察者自身> };消息 id 前缀slp-wd-; - 正文是插件常量模板,只有状态字段插值(目标 id / 读数时间 / 静默时长)——注册参数不进入正文,注册无法给观察者的下一回合夹带提示词;
- 去抖:同一目标「同一静默期最多一次 tick」,两次 tick 之间至少隔一个巡逻间隔。去抖状态是进程内 Map,不持久化——重启即忘,宁可多一次 tick,也不留会误判的持久状态;
- 巡逻定时器随插件 dispose 一起清理(
ctx.effect)。
运维面:调参 / 静音 / 不唤醒(批 U,2026-10-23)
用户反馈三件事:注册后不能调整间隔、只能等注册过期、提醒消息可能把会话拖进「一直报错」。前两件是工具面缺口,第三件实测定位到污染源不在插件:
- 注册后可以改:
team_link_watch action=update id=… silentMinutes=… intervalMinutes=… ttlHours=… wake=…—— 按 id 就地改,TTL 从现在起算续租(可延长也可缩短),定时器按新间隔重挂;不必清掉重登记(那会丢 tick 去抖状态)。边界与 register 共用同一份(silentMinutes>=10/intervalMinutes>=5/ttlHours<=24);一个字段都不给是拒绝,不是无操作。 - auto 那一档也有停止键:
mute/unmute(观察者本人即可;默认 12h、上限 24h)。静音只停投递,注册、续期、TTL、去抖状态一律不动;clear对 auto 的拒绝保持不变(它仍是全队工作台资产)。 - 提醒不许唤醒「已经跑不起来」的会话:投递有两条出口 ——
followup= 唤醒一个新回合,inject= 只进收件箱、不唤醒。两种情况走inject:① 注册里wake:false;② 本拍确实要投东西,而观察者 surface 里末条slp-wd-tick 之后没有assistant/message(上一拍没跑起来:模型 400 / 崩溃 / 卡死),并留一行 warn。★ 证据边界:surface 读不到 ⇒ 按「证据不足」处理,不降级(绝不因为读不到就少叫一次)。 - 污染源实测(不是推断):那两个会话 400 的根因是工具结果里的孤立代理项(lone surrogate) ——
72d1b89b的seq 3622、1a253727的seq 131/132各有一处(全档扫描只命中这些),由String.slice在 UTF-16 码元边界切断代理对产生;请求序列化层未消毒 ⇒ 之后每一次请求都 400。插件自己的消息全部过wellFormed()(toWellFormed),不是污染源;但followup会每 10 分钟把坏会话重新拉起一次 —— 所以修复落在「别再唤醒它」与「可静音」两处。已知边界:已写入历史的毒串不会被插件修复;那两个会话需要在侧栏 rewind 到中毒事件之前,或由宿主/上游在请求序列化层做孤立代理项消毒 —— 本批不代做。
诚实声明(A4)
看门狗只能提醒活着的观察者。观察者或目标任一方已关闭时,没有任何机制能唤醒它——插件只在信号面标 dead 等用户处理。「活会话节奏维持」是真实覆盖面,「失联恢复」不是。
注册的一生:重启后自己长回来(批 D 起)
插件的三类注册(角色段 / 资源读数 / 工作台摘要)此前各有一段「半哑窗口」,批 D 把它们收口成不变量:
- 重启后不需要人肉补挂(FR-1):角色/宪章段与资源行只对活代理注册,所以重启那一刻如果没有任何团队会话活着,attach 会注册 0 个 —— 此后你打开某个团队会话也不会自动补上(要等到下一次名册写入,实测最久差了 40 分钟)。现在插件在每一步的
agent/pre-step上做一次两道查表的检查(名册想要它、而它还没挂上 ⇒ 就地sync):打开团队会话的那一步(或最迟下一步),角色文本与资源读数自己就到位了。检查是 O(1) 的,非团队会话(绝大多数)零扫描;失败绝不打断回合(只留一行 warn)。 - 全队的工作台摘要拿不走(FR-2):
clear以前会连origin: auto的全队工作台注册一起清掉(实测踩过两轮,被清掉的那条还没到期)。现在clear只清你自己手工注册的那些;点名id要清 auto 那一条 ⇒ 拒绝,并在回执里点名它是全队工作台的自动注册。摘要脚注也同步改准(它原来在教协调者做一件会坑掉全队的事)。 - 资源读数按「回合」门控(FR-3,用户 2026-10-10 拍板):资源行落在动态运行时上下文通道上,此前每一步都变 ⇒ 每步往会话历史追加一条快照。现在同一个回合内返回上一条字符串(回合号前进才现算),快照量级降一个量级。代价如实声明:长回合里读数会变陈(压力增长看不见)——这是设计上被接受的取舍;那份缓存是进程内的、只用于门控,随注册生命周期释放。
归属与判据:设计档 docs/2026-10-10-registration-lifecycle-design.md(§5 FR-1…FR-4 / §8 U1–U9),读数见 docs/verification-log.md 的「2026-10-11 批 D」一节。
自动注册的续期:只要名册还在,工作台就一直有人看着(批 G)
问题:全队工作台那条自动注册(origin: auto)此前有 12 小时 TTL、却没有任何续期路径。连续开机满 12 小时它就静默熄灭(你原始投诉「工作台摘要没了」的复现路径),恢复只能靠重启 DSH 或手动 team_link_watch action=arm-team(而那条命令要求你是该队现任协调者 ⇒ 你没法从任意会话一键救活所有团队)。
现在:
- 续期(FR-R1):每条注册自己的巡逻里,一条 auto 行只要还在名册依据上、且距到期不足 2 小时,就把租约顺延到「现在 + 12 小时」,并在行上写一个
renewedAt(watch list与状态卡第 ③ 段都会渲染「上次续期 …」——留痕落在行字段上,不走日志)。身份字段(id/createdAt)一个都不改;targets按名册当场重导(于是中途入队的成员从此会被盯上——这是顺手修掉的同源缺口)。 - 名册门(D2):续期只对「团队仍在名册 ∧ 观察者仍是该队现任协调者 ∧ 除观察者外还有人可盯」的行生效。所以该失效的照旧失效:团队删了 / 换届了 / 退役了 ⇒ 不续 ⇒ 到期由 TTL 收尸(不会被续成永生僵尸)。刻意不看观察者会话是否开着(工作台摘要是团队资产,不是那个窗口的属性):协调者窗口关着而机器长开时,摘要依然会恢复——这正是原始投诉的另一半。
- 冷启动(FR-R2):关机超过 12 小时再开机时,盘上那些行已经过期。此前它们被当成「还在」而在几分钟内被清扫链逐条收掉(工作台当场死透,比 12h 慢死更快);现在过期的行会被同 id 原地刷新并重新挂上定时器。
- 运行期复活(FR-R3):巡逻尾部会去抖地(60 秒)补挂一次缺失的注册 ⇒ 机器睡过「2 小时窗口 + 到期点」之后行已被清扫,醒来自动重铸;换届后新协调者的行也不必等重启(旧行随名册门失效而死,新现任的行自动补上)。
- 不做的:不改 12 小时 TTL 常量;不给手工注册续期(人工 TTL 是你自己的意志,
≤24h的额度就是让你自己定);不动静默阈值 / 巡检间隔 / 投递条件。 - 边界:工作台摘要熄灭时不会有主动告警(只要求读面如实反映续期后的到期时刻);
agent-team档的团队与现状同级(不另设门)。
归属与判据:设计档 docs/2026-10-12-watchdog-renewal-design.md(§5 FR-R1…FR-R5 / §8 U1–U12),需求档 docs/2026-10-12-watchdog-renewal-requirements.md,会议裁定见 docs/consult-minutes/2026-10-12-consult-43-minutes.md;读数见 docs/verification-log.md 的「2026-10-12 批 G」一节。
工作台摘要与两条告警(批 J,2026-10-13)
全队工作台那条自动注册除摘要外,还投两条独立通道 —— 三条通道各有自己的去抖与指纹,互不抑制(一次巡逻里各投各的):
| 通道 | 什么时候来 | 说什么 |
|---|---|---|
[workbench] 工作台摘要 |
有可行动内容时(同一注册两条至少隔 10min) | 四组各一行:谁在跑 / 谁空闲待派 / 谁刚干完 / 谁需处置 |
[idle-unassigned] 有活没派 |
空闲待派非空 ∧ 无人在跑(=没有人在被派活) | 点名那些空闲成员与「没有成员在被派活」这个事实(陈述事实,不指控);同一段至多 3 次,成员集合一变即重置 —— 信号不沉没,也不刷屏 |
[pressure-warn] 自身压力告警 |
你是该队现任协调者,且你自己的上下文压力 ≥ 50% | 点名后果:再读别人的会话历史会把你自己的窗口读满;同一档(每 10% 一档)不重复,只在档位变大时再响 |
事实源指路(宪章第 3 条与摘要脚注各写一份,两处同句):要看某位成员在干什么 ⇒ 读那个团队的黑板 tasks.md(team_link_team_read),不要读它的会话历史。
归属与判据:设计档 docs/2026-10-13-coordinator-signal-design.md(§5 FR-J1…FR-J4 / §8 U1–U9),需求档 docs/2026-10-13-coordinator-signal-requirements.md。
团队状态卡(只读)
team_link_status 回答的是「人想知道团队还活着没有」——夜里三点团队卡住时,一次调用一屏讲清现状(设计档 §4.2)。全只读:不写设置、不写磁盘、不加缓存、不新增持久状态;调用前后 settings 与文件逐字节不变(连换届过期清扫都不跑——那会写)。
| 段 | 内容 | 成本 |
|---|---|---|
| ① 团队与角色 | 每个团队一行(workspace / policy.writer / 角色数)+ 每角色一行:在位(现任 id 与任期起点)或空缺(vacant),外加版本史末条 | settings 读,零 |
| ② 换届 pending | 在飞令牌:团队/角色 → 继任者、token 掩码(tok-1a2b…9f0e)、创建 / 到期、绑定三元组、已迁移条数 |
settings 读,零 |
| ③ 看门狗 | policy.watchdogs 的每一条:观察者(附代理状态)→ 目标、静默阈、巡检间隔、到期;末尾一行 attach 后置链读数(rearm 挂上几个定时器 · 段同步变化几项 · 按队补挂 created/present/skip/failed + 首因,见 FR-17;skip=没有观察者/没有可盯成员,failed=写入失败或定时器未挂上)—— 本部署没有日志出口,这一行就是那条链唯一的眼睛 |
settings 读 + 进程内记录,零写 |
| ④ 会话面 | 同工作区其他会话的 id / 代理状态 / 创建时间 / provisional 配对标记 | 注册表读,不读日志 |
| ⑤ 活性 | 与 team_link_list_sessions 同一顺序读前 12 行(PREVIEW_SESSIONS):verdict / 代理 / goal / 静默 / 时间戳;超出窗口的行如实标未读 |
一次有界并行 surface 读(≤12) |
| ⑥ 台账尾 | 每个团队的 tasks.md 末尾 tasksTail 条(默认 5、上限 20,超出拒绝)与 baseHash |
每团队一次文件读 |
| ⑧ 资源/预算(批 3 追加) | 每个会话一行:压力 / 窗口 / 累计 / 轮次(只读 sessionProjections,读数不可得则整行标「不可读」+ 原因码);达标会话行尾挂 ⚠ 轮换候选(turns / tokens) 并给出那句现成命令 team_link_rotate action=prepare successor:auto(只标记、不自动);末行是判读行(「预算耗尽」只在这里、且只以否定式出现;末尾固定接一句范围界定——本节只判上下文/预算,宿主工具闸门(如写入自动评审 429)属另一类、不在本节口径内,见 FR-14)。段体 ≤12 行(行数吃紧时先给判读行与轮换标记留位,被裁的只能是明细行并如实标注「已省略 N 行明细」,见 FR-15);policy.sections.resources=false ⇒ 整段不出现 |
投影读,零写 |
| ⑦ 形态(B 批追加) | 每个团队一行形态 + Lead;成员名册投影(读时现算;不可读时如实标注并点名原因);完整能力矩阵(事实源 = 父档 §2.3)+ 那句结论;形态诊断行(除自己外没有其他可达会话时只提示、绝不自动切档) | 宿主投影读(一次 ctx.get("agentTeams") 调用,零日志)+本卡已读到的会话面 |
| 参数 | 语义 | 边界 |
|---|---|---|
team? |
只看这一个团队 | 省略 ⇒ 聚合设置中的全部团队(每团队一段,不按工作区过滤——团队在本插件里按名全局寻址,与会话列表的工作区过滤不是一回事);给了但不存在 ⇒ 拒绝并列出已知团队名 |
tasksTail? |
台账显示最后几条 | 默认 5;上限 20,超出或非整数 → 拒绝并给出有效区间 1..20 |
action? |
forensics = 读崩溃现场(批 K 的心跳留证):本次启动 + 死亡窗口 + 死前四类读数(内存 / 磁盘 / 活会话 / 看门狗)+ 保留份数 + 免责句 |
省略 ⇒ 出上面那张团队状态卡(逐字不变);闭集外取值 ⇒ 如实拒绝并点名唯一支持的值(不静默退回默认卡) |
崩溃现场(action=forensics,批 K):DSH 反复非正常退出(non-graceful-end)时进程没有机会写任何东西,所以本插件活着时持续写心跳 —— 每次启动开一个目录 <DSH_HOME 或 ~/.dsh>/team-link/forensics/<bootId>/,里面一份定长小 JSON heartbeat.json(原子覆盖;按心跳内嵌的 lastBeatAt 保留最近 10 份启动,永不删本次)。下一次启动就能算出「死前那一刻」的现场:窗口 = [上一次最后一拍, 本次启动](今天那种 79 分钟空白就是这一格),外加内存 / 磁盘 / 活会话 / 看门狗四类读数。只读、零写入(调用前后 settings 与磁盘逐字节不变),也不读宿主自己的崩溃记录目录。★ 免责句是硬约束:心跳只证明「最后一次说话停在那一刻」,正常退出与崩溃在这份数据上同形 ⇒ 本插件只留现场、不单独下崩溃结论,区分二者要由人结合宿主的 crash.log。★ 诚实边界:不记「在飞作业数」(插件读不到宿主的作业服务,读数里没有这一格,也不用猜测的 0 冒充);statfs 读不到时磁盘两格就是 null(不编造)。归属与判据:设计档 docs/2026-10-13-crash-forensics-design.md(§5 FR-K1…FR-K5 / §8 U1–U15),需求档 docs/2026-10-13-crash-forensics-requirements.md。
反面预警注记(§4.2,会诊提出):窗口内一条消息都没有、而台账同期新增了行 ⇒ 说明大家可能已经停止说话、只写行(互核正在死亡的前导指标)。判据写死:锚 T0 = 本读窗内最旧的一条 surface 消息时间戳;N = T0 之后、本读窗内的消息条数;M = T0 之后 tasks.md 的新增行数;只有 N == 0 且 M > 0 才打注记,否则不打。两个计数都取自已经读到的东西 ⇒ 零额外读。三种情形逐条写死(设计 §4.2,三态各有断言):① 读窗为空(本工作区没有其他会话)⇒ 整段不打印(连段标题都不出现);② 读窗非空、但窗内没有一条带时间戳的 surface 消息(锚取不到)⇒ 打一句 (反面预警注记无法计算:本读窗内没有带时间戳的 surface 消息——不猜。),诚实优于沉默,台账里有多少行都不改这个结论;③ 锚取到 ⇒ 按上面那条唯一判据判 触发 / 不触发。聚合口径:省略 team 时 M = 所示各团队 tasks.md 同期新增行数的和(单团队时就是那一家)——读面上它只是一个数,不逐团队拆分。它是读数,不是裁决:只陈述计数,不下「互核已死」的结论。
诚实声明:⑤ 段只读前 12 行会话(与列表工具同一条预算、同一种标注),第 13 行起的活性未判定;调用方没有会话身份时卡照出,「当前会话」处写 (当前会话未知)——不编造身份。
(⑦ 形态由形态批追加:卡尾一段给出每个团队的形态 + Lead、宿主成员名册投影、完整能力矩阵与「单会话兜底档」那句结论,必要时附形态诊断行——见下一节。)
团队形态(多会话档 ↔ agent-team 档)
形态不是功能开关,是通道选择(设计档 §4.1,来源是《台账 + 形态设计》§2.3):
- 多会话档(
sessions,默认):平级会话之间,靠本插件的跨会话身份与信任层(投递 / 配对 / 换届 / 黑板); - agent-team 档:一个会话内部的子代理团队(宿主 Agent Teams)。本插件在这一档下只做名册与指路,不代理宿主的派活 / 任务板(那是宿主的属地)。
三句要紧的话:
- 人说了算:人在对话里说一声 → 模型发起 → 弹一次确认框 → 人点一下才落笔(并往
decisions.md记一行形态史)。没有自动切档; - 切了就换通道,不换人:信任不自动迁移(旧的配对必须人重新批准,切回来也一样);切档不改投任何在飞消息、也不建桥——要跨形态说话,由模型自己用对应形态的手段说;
- 宿主没开就报错指路,绝不悄悄退回多会话档——「悄悄降级」正是本仓最反对的那类事故。
两档各能用什么(能力矩阵,事实源 = 父档 §2.3)
| 能力 | 多会话档 | agent-team 档 |
|---|---|---|
跨会话投递(team_link_send) |
✅ 主用途 | ❌ 成员是子代理,不是可投递目标 |
| 双门批准 / 配对 | ✅ | ❌ 用不上(宿主那套没有独立信任模型) |
换届(rotate)/ 恢复(recover) |
✅ | ❌ 没有可换届的会话;Lead 会话没了团队就散了 |
看门狗(watch) |
✅ | ❌ teammate 不是根代理,盯不了 |
| 黑板(decisions / discipline / tasks) | ✅ | ⚠️ 仍可用,但只有 Lead 一方读写 |
| roster 身份 / 版本史 | ✅ | ⚠️ 只记「本团队是 agent-team 档 + Lead 是谁」 |
| 团队状态卡(只读) | ✅ | ⚠️ 成员部分读宿主投影(2026-09-27 已核:本部署已挂载该服务 ⇒ 探针 available=true) |
| 会话深链 / 导出 | ✅ | ✅ 与形态无关 |
结论:agent-team 档应叫「单会话兜底档」,不是「另一种平等的形态」。多会话不可用时才切过去,切过去就等于放弃跨会话的全部能力。
怎么切、怎么看(team_link_roster)
| 面 | 行为 |
|---|---|
| 切档 | team_link_roster action=set-mode:mode(闭集 sessions / agent-team,闭集外拒绝并列出两个值)+ leadSessionId?(省略 ⇒ 默认取调用方自己的会话 id,并在确认框正文里明示,人可见即可否决;显式空串 ⇒ 拒绝;非空须过形状 ^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$ 与在场校验(能在本次会话列表快照里找到)两道) |
| 双重门(缺一即 fail-closed、零写入) | ① writer gate 原样(policy.writer=coordinator 时只有现任协调者可写;空缺 ⇒ 会话路径拒绝,设置 UI 仍是兜底);② 人类确认框:无确认服务 / 超时(3 分钟)/ 取消 ⇒ 拒绝且零写入 |
| 宿主探测只对「切到 agent-team」这一向 | 探测不到 ⇒ 拒绝 + 指路怎么开 + 零写入(绝不把 agent-team 请求当多会话执行);切回 sessions 不探测(回默认档永远可用)。探测面是宿主服务 agentTeams(ctx.get,可选服务:缺席只影响这一项能力)。同一判据只探一次:切到 agent-team 时探过的那一次直接复用为确认框正文里的成员名册投影。只报现象、不替宿主下结论:拒绝文案说的是「服务 agentTeams 当前不可见(可能未装载该组合包,也可能尚未激活完成)」—— ctx.get 只返回已激活的 provider,一个尚未完成初始化的服务也会走到这一支,所以这里不断言「profile 没装载」 |
| 落笔 | settings 的 teams 行写入 mode / leadSessionId(只改本团队行的这两个键;teams 是整份数组回写,所以其余团队行若缺这两个键会被补默认值(sessions / 空串)、已有值一个字不被覆盖 —— 与既有 upsert-team 同形,语义无变化。信任 pairs/trustedSenders/rememberTargets 一行不写)+ decisions.md 追加恰好一行形态史(形态 → agent-team(Lead=<id>) 或 形态 → sessions)+ best-effort 重写 roster.md 镜像(与设置变更同一事务;镜像与形态史同源 —— 都取写时复检后的那一行,确认框期间设置变了也不会「账写旧路径、镜像写新路径」) |
| 幂等 | 同档同 Lead 且账上已有这一档的形态史行 ⇒ 「已是该档」,不弹框、不写 settings、零写入(首次声明也算一行:形态史记的是「声明过这一档」,不是「档位发生变化」—— 所以从未切换过、第一次收到默认档声明的团队也会补记一行,返回体如实标「补记」);账上缺那一行(上一次 settings 写成功而 decisions.md 写失败)⇒ 补记恰好一行再返回 —— 否则「补上路径后重发本次切换以补记」那条指路不可执行;账读不出来或没有 workspace ⇒ 不补记并如实说明(fail-closed,不猜) |
镜像(roster.md) |
D3 起镜像也渲染 - 形态(mode):… / - Lead 会话(leadSessionId):… 两行。渲染器永不打印 undefined:缺字段 / 空串 / 非字符串按 normalizeTeams 的同一套缺字段语义落默认值(sessions / 空 Lead),闭集外的值按读面的规则落默认档(降级留痕只在读面,镜像不带);upsert-team 新建时就把这两个默认值显式写入 settings ⇒ 「settings 行 = 读面 = 镜像」三者一致(2026-09-27 真机冒烟修复轮:新建团队的镜像此前渲染成 undefined —— 只有新建这条路会,既有团队走归一化读面看不出来) |
| 切走前的提示(提示式,不阻断) | 确认框正文列出「不会迁移的三件事」:① 信任(配对 / 白名单 / 记忆目标 —— 都要人重新批准)② 成员名册(agent-team 的成员不会变成会话)③ 任务归属(宿主的任务板不跟随;台账仍是本插件的账);并列出当前 teammate(可读则列,不可读如实说),加一句「请确认它们的结论已落到黑板或文件」。不逐成员推断有没有交卷 |
| 看形态 | team_link_roster action=get 的形态段与状态卡第 ⑦ 段:形态 + Lead + 成员名册投影(读时现算,一行不落 settings;来源文案:可读时写 来源:宿主 agentTeams 投影(本部署可读);不可读时按分支取首行 —— 只有服务缺席才说 成员名册不可读(本部署未提供该投影)—— 仍可读 Lead 与形态,其余四条分支(缺方法 / 无会话身份 / 调用方非成员 / 读取抛错)用中性首行 成员名册不可读 —— 仍可读 Lead 与形态(哪一条读不到,见下一行的原因),真正的原因由紧随的(不可读原因:…)行点名)+ 完整能力矩阵 + 那句结论 |
| 形态诊断行 | 本工作区除自己外没有其他可达会话 ⇒ 读面附一行「⚠ 多会话通道看起来不可用(列表里没有其他会话)—— 你可以让协调者切到 agent-team 档(team_link_roster action=set-mode)(只提示:本插件绝不自动切档…)」。只提示,绝不自动切;没有会话身份时不判 —— 判据是 null(不判、不猜,与「列表读不到」同一处置),而不是拿一个猜出来的工作区当结论 |
红线(形态批 §5):只读宿主(不 spawn / 不派活 / 不改宿主任务状态)· 不悄悄降级(写路径刺眼报错、读路径如实降级,两条路不许混用)· 信任不自动迁移 · 双重门 fail-closed · 指针不拷贝(只存 Lead 会话 id)· 切档永远由人触发。明确不做:不代理宿主派活 / 不读子代理日志(交卷只做提示式)/ 不自动切档 / 不做跨形态改投。
四、团队:roster 与黑板
roster(team_link_roster)
teams 键是身份层的事实源:团队 → 角色 → 会话,带版本史。
teams:
- name: night-shift # [a-z0-9-]+,工作区内唯一(也是黑板目录名)
createdAt: 1700000000000
workspace: D:/work/night # 首次创建团队时从该会话的 agentCwd 捕获,之后不再改写
policy: { writer: coordinator } # coordinator | any
mode: sessions # 形态(B 批):sessions(默认,多会话档)| agent-team(单会话兜底档);闭集外的值读时降级为 sessions 并留痕
leadSessionId: "" # 仅 agent-team 档有意义:Lead 指针(空串 ⇒ 读面标「Lead 未知」);成员名册一行都不落这里
roles:
- role: coordinator # 约定角色名;自定义角色(reviewer 等)由 set-role 按需创建
current: session-abc # 现任;null = 空缺
pending: null # M4 rotation 的继任槽位,M2 只原样保留
history: # 版本史:一段任期一条,until: null 表示仍在任
- { session: session-old, from: 1700000000000, until: 1700009999999, note: 交班 }
| action | 效果 | 写权限 |
|---|---|---|
get |
全体团队概要;指定 team 时给出详情(含 pending 与整段版本史) |
任何会话可读,无门 |
upsert-team |
创建(默认 policy.writer=coordinator、workspace 取调用会话的 agentCwd,并把调用会话播种为该团队 coordinator 现任——创建即认领)或幂等更新 |
已存在的团队过写权限门;重复调用不重置 roles / 版本史 / createdAt,也不会再播种一次 |
set-role |
current 替换 + 版本史追加:旧任那条记 until=now(带 note),新任那条以 until: null 打开;角色不存在则本次指定即创建。指定的会话正是某个在飞换届 pending 的继任者时,该令牌当场作废(身份已由本次显式变更,claim 会报「没有 pending」) |
过写权限门;不迁移 pairs——信任迁移是 rotation 的专属动作 |
retire |
current 置空(vacant)+ 版本史记退役(until=now,带 note)。退役本身不动信任数据,随后弹一个确认对话框列出所有仍指向该会话的 pairs / trustedSenders / rememberTargets,选「清理」才删除 |
仅现任协调者会话或用户发起(与 policy.writer 无关) |
set-mode |
切团队形态(sessions / agent-team):mode(闭集,闭集外拒绝并列出两个值)+ leadSessionId?(省略 ⇒ 默认取调用方自己的会话 id 并在确认框正文里明示;显式空串拒绝;非空过形状 + 在场两道校验)。宿主探测只对「切到 agent-team」这一向(探测不到 ⇒ 拒绝 + 指路 + 零写入 + 补一行设置 UI 的兜底路);双重门:writer gate 原样 + 人类确认框(无服务 / 超时 / 取消 ⇒ 拒绝且零写入);落笔后 decisions.md 追加恰好一行形态史 + best-effort 重写 roster.md 镜像;同档同 Lead 且账上已有该行 ⇒ 「已是该档」(不弹框、不写 settings、零写入),账上缺该行 ⇒ 补记一行再返回(任一档都可执行的补记路径)。信任一行不写、成员名册一行不落 settings |
过写权限门(与 set-role 同一道) |
delete(批 H,2026-10-12) |
把团队从名册里真正删掉:teams 里那一行连同它自己的版本史一起消失(不是退役留痕——那是 retire)。顺序是判据的一半:先校验在册(不在册 ⇒ 如实拒绝 + 列出已知团队名,零弹框零写入)⇒ 人类确认框(逐项写清:删哪个队 · 会删什么(含版本史 N 条)· 不会删磁盘上的黑板目录 · 不会改信任数据 · 不可逆;无确认服务 / 取消 / 超时 ⇒ 拒绝且零写入)⇒ 写前重读名册(确认窗口是分钟级,期间别的会话的 set-role / upsert-team / 删别的队不会被静默回退;若这一行已不在 ⇒ 幂等拒绝、零写入)⇒ 单笔 policy.update({teams});写盘抛错 ⇒ 行仍在 + 如实报错(绝不报成功)。报告面点出黑板目录路径(存在 / 不存在都如实说) |
豁免写权限门(US-H2:已退役的队没有现任协调者,不豁免就谁都删不掉)—— 把关改由人类确认框承担;其余动作的写权限逐字不变 |
写权限与「创建即认领」
coordinator(默认):只有coordinator角色的现任会话可写(比对exec.agent.id);any:任何会话可写;- 唯一的例外是
delete(批 H):它豁免这道门(US-H2 的核心场景就是「已退役、没有现任协调者」的队——不豁免则功能对它的目标场景完全不可用),把关改由人类确认框承担;其余动作一个字都没放宽(既有断言仍钉住它们); - 读永远开放;
policy本身只有用户能改(工具参数里没有 policy 槽位)。
0.3.7 修掉的一个死锁:upsert-team 的创建路径本来就不该过写权限门(否则没人能建第一个团队),但 set-role 必然过门——而门在「writer=coordinator 且现任空缺」时拒绝一切会话路径。于是旧版本会出现:模型能建出团队,却永远写不进首任协调者,团队到手即只读。现在创建路径把调用会话直接播种为 coordinator 现任,团队建完即可派活/写黑板,不需要手改任何配置。手写出来的空缺行仍然全拒(那是用户显式表达的状态)。
自助引导(手写设置)
正常流程不需要这一步。手改 settings.yaml 只在两种场合需要:① 把某个已存在团队的现任换成别的会话,或把被手写成空缺的团队救回来(此时写权限门会拦下一切会话路径,工具走不通);② 用户不在模型回路里时预先铺设团队。
直接编辑 profile 的 settings.yaml,在 team-link: 段下写入(键名与第六节一致):
team-link:
teams:
- name: night-shift
createdAt: 1700000000000
workspace: D:/work/night
policy: { writer: coordinator }
mode: sessions # 形态:sessions(默认)/ agent-team;闭集外的值读时降级为 sessions 并留痕
leadSessionId: "" # 仅 agent-team 档有意义(Lead 指针;空串 = 读面标「Lead 未知」)
roles:
- role: coordinator
current: session-abc # 现任会话 id;null = 空缺(会话路径会全拒)
pending: null
history:
- { session: session-abc, from: 1700000000000, until: null }
- 保存即生效:
dsh-settings-file的 watcher 会热加载(2026-09-18 实测:外部删除team-link:段后,team_link_roster action=get立刻回到 0 团队);若个别环境不触发,重启 DSH 即可确定性地重新加载; - 只写
current不写history也能生效(任期起点退回createdAt显示);补一条until: null的任期记录才让版本史自洽; - 名字不合
[a-z0-9-]+的团队行、没有role的角色行会被归一化丢弃,不会让整个命名空间失效(其余行照旧生效); - 该命名空间只有在插件成功注册后才存在;
settings.yaml里的team-link:段在第一次真正写入后落盘——注册失败或迟迟未挂载会在日志里留痕,见第七节。
黑板(team_link_team_read / team_link_team_append / team_link_team_archive)
以 team.workspace 为根:
<workspace>/team/<name>/roster.md # roster 镜像(插件同一事务内 best-effort 写;失败只告警,settings 是事实源)
<workspace>/team/<name>/decisions.md # 裁决账本:只追加,每行 `seq | ISO 时间 | author-session-id | 正文`
<workspace>/team/<name>/discipline.md # 纪律条款:整文件替换,必须携带 team_read 返回的当前 baseHash
<workspace>/team/<name>/tasks.md # 任务台账:只追加,每行 `seq | ISO 时间 | author-session-id | kind | task | 正文`
黑板是三件套:decisions(裁决)、discipline(纪律,唯一带乐观锁的一份)、tasks(任务台账)。三者都只受同一条单行上限约束,seq 各自独立计数(tasks 的 seq 不受 decisions 影响)。
team_link_team_read(team)一次读齐:roster 概要 +decisions末 20 条 +discipline全文 +tasks末 20 条与它的派生视图 + 三个文件的baseHash(sha256 前 16 位十六进制)。文件不存在按空处理并如实标注(含「空内容哈希」);team_link_team_append(team, file, line, baseHash?, kind?, task?):decisions只追加(无需 baseHash,正文必须单行);discipline整文件替换(baseHash 缺失或不匹配即拒绝并要求重新team_read);tasks只追加一条主张(下一小节)。三者都受单行 2000 字符上限(按码点计);file是白名单枚举(decisions/discipline/tasks),任何路径形状都会被拒;kind/task只对file=tasks生效,把它们传给另两个文件一律拒绝(参数误用不静默生效);- 黑板没有写权限门(任何会话可写):写入者身份记在行的 author 字段里,透明可审计;
discipline的整文件替换靠 baseHash 串行化。
任务台账 tasks.md(§9 第一批 · 只追加)
它记的永远是「谁在什么时候声称了什么」,不是「事实是什么」——这条是设计红线(见 设计档 §9):
seq | ISO时间 | author-session-id | kind | task | 正文
41 | 2026-09-26T21:10:02.123Z | session-lead | plan | t-7 | 让 worker-b 复核 §3 的行号
42 | 2026-09-26T21:12:44.001Z | session-wb | claim | t-7 | 接了,预计 10 分钟
43 | 2026-09-26T21:19:31.552Z | session-wb | done | t-7 | 核出 3 处错(依据 lib/index.js:3548)
44 | 2026-09-26T21:21:08.900Z | session-lead | dispute | t-7 | 对 43 存疑:第 2 处的行号我读到的不一样
45 | 2026-09-26T21:24:55.310Z | session-wb | retract | t-7 | 撤回 43 的第 2 处:确认是我读数窗口旧了
kind是闭集:plan(派活)/claim(接活)/done(声称完成)/block(卡住、需要授权)/dispute(存疑)/retract(撤回自己此前的某条主张)。闭集之外的kind、以及缺失的kind,一律拒绝并列出这六个值;- 任务号由插件分配:
kind=plan扫整个文件取已登记的最大号 ⇒ 写t-<max+1>并在返回体里给出这个号(此时不要再传task,传了就拒);分配只接受安全整数——文件里若有手写的、超出安全整数范围的任务号(22 位以上,Number会读成1e22),plan拒绝并点名那个号,不钳制、也不猜(t-1e+22这种连它自己都解析不了的行永远不会被写出来);其余kind必填task,且该号必须已在本文件登记过——没登记过的号一律拒绝并指路kind=plan(这条正对着「凭记忆转录 id」那类事故); - 只追加(
appendFile):写后的文件 = 旧内容 + 新行,既有行逐字节不变。撤回也是一条新行,历史行永远不被改写; - 正文单行、非空、≤ 2000 码点(与另两个文件同一上限,astral 字符记 1);含 CR / LF 一律拒绝;
seq每文件独立计数:tasks.md的#1与decisions.md的#26互不相干;- 长行只影响显示,不影响账本(批 F · FR-4):两处显示面 ——
team_link_status第 ⑥ 段(台账尾)与客户端团队面板的tasksTail—— 共用同一个显示截断:超过 300 码点的行显示为「前 300 码点 +…(已截断,全文见 team_link_team_read)」(按码点切,不切半代理对)。盘上的文件与team_link_team_read的返回一字不动 —— 要看全文用team_read; - 并发最坏情况如实声明:黑板无锁,两个
plan同时扫到同一个最大号时会撞号——该号下多出一行plan,读面照显。这与「两条同 seq」同族,不引入锁。
台账写门(批 34 / FR-7;默认 OFF)——开关 = 本插件自己配置面的 antiDrift.ledgerTransitionGate(见第六节的键集表)。门只对 kind=done 与 kind=retract 两条规则生效,位置在全部既有校验之后、seq 分配与追加之前 ⇒ 拒绝 = 零写入(文件逐字节不变,也没有半条行落盘):
- 规则①:
done(t-N)要求该t-N在本文件里已有至少一条claim行;没有 ⇒ 拒,回执点名任务号并指路先写kind=claim。「声称完成一件没人接过的事」正是它要挡的那类漂移; - 规则②:
retract的正文必须含一个指向本文件里真实存在、且属于同一任务的行的指针(t-<n>或#<seq>);抽不出这样的指针 ⇒ 拒,回执点名抽到的引用并列出本任务可用的行。指向别的任务的行不算凭据(那等于拿别人的任务行当装饰,不是证据); - 失败朝向 = fail-closed:台账读不出、行解析不了 ⇒ 拒写(放行一条凭空销账的行,比拒绝一条合法行不可逆得多);
- 门刻意不做的三件事:不判作者是否同一人(资格仍归人判断,见上面那条)、不判撤回是否真的驳倒了它(那需要自然语言理解,撞本仓铁律)、不加
kind白名单(retract在既有设计里正是用来消解dispute的,加白名单会把既有合法撤回打红); - ★ 开门 = 两侧各设一次:本键住在本插件自己的配置面(设置项的
team-link命名空间 /policy.json的antiDrift.ledgerTransitionGate,缺省false);另一侧dsh-thincoder-suite的anti_drift_config写的是它自己配置面上的同名键,对这里的门完全无效(两个插件零运行时耦合)。门默认 OFF ⇒ 上面两条规则一条都不生效,写入行为与从前逐字相同; - ★ 已知的既有语料不一致(如实登记,不改示例语义):本节上面那份示例台账的第 45 行写作「撤回 43 的第 2 处」(裸数字、无
#),而规则②只认#<seq>/t-<n>两种形态 ⇒ 门打开时这种写法会被拒。示例保留原样(它记录的是历史写法);要开门的队把正文写成撤回 #43 的第 2 处即可。门默认 OFF ⇒ 既有部署不受影响。
黑板归档 archive/(批 T · 只搬不删,显式动词)
decisions.md / tasks.md 只增不减,长期团队会越攒越沉。批 T 给了一个显式动词 —— 阈值只提示、绝不自动搬:
team_link_team_archive(team, file ∈ {decisions, tasks}, keepTail? = 200)—— 过一道确认框(文件 / 将搬行数 / 保留行数;无确认服务 / 取消 / 超时 ⇒ 零写入),把旧行搬进archive/<file>-g<世代>-L<起>-L<止>.md;- 只搬不删:被搬的行在归档文件里逐字存在(归档头之后原样一段),活跃文件保留尾部
keepTail行 + 一行归档指针(> 归档指针:…—— 指针不算一条账:读面的「共 N 条」与序号扫描都不把它算进去); - 归档文件只写一次:文件名是确定性的(如
archive/decisions-g1-L1-L4.md,L是活跃文件里的 1-based 物理行号,不含墙钟)且用wx创建 ⇒ 同一份活跃文件再跑一次会命中「同名且正文逐行相等」⇒ 跳过归档写、原归档字节一律不改(「归档已写、活跃未重写」之间崩溃 ⇒ 重跑幂等);同名但正文不同 ⇒ 拒绝、不覆盖(人工处置);下一次归档开新文件; - 最新形态史行永不搬走:切点 =
min(位置切点, 最新形态史行位置)——set-mode的幂等检查只读decisions.md,把那一行切走会让它漏判、重复追加形态史行; - 号不回卷:
seq与任务号t-<n>的分配与**「已注册」校验**都取「活跃 ∪archive/」的并集 ⇒ 归档之后新号一定大于历史最大号;归档过的旧任务号仍被认(claim 合法),伪造的高号仍被拒; - 归档头记:时间 / 执行者 / 搬走行数与范围 / 各角色届数快照(
generationOf= 各角色history条数最大值)—— 世代只进归档头,不落 policy(顶层键仍十一键); - 读面:行数 > 500(具名常量
BLACKBOARD_ARCHIVE_NOTICE_LINES)给一行提示(只提示、绝不自动搬);archive/非空时始终给一行「另有 M 行已归档至archive/(最近<ts>)」; discipline.md是可锁定的文档、handoff-<role>-是换届取证的活源头 ⇒ 都不在归档面;归档落在archive/子目录、文件名不以handoff-开头,所以交接文档的扫描谓词在结构上撞不上它。- 代价如实:归档是两次写(先归档、再原子重写活跃文件),中间崩溃会留下「归档已写、活跃未重写」的现场 —— 重跑按确定性文件名识别并幂等完成,不丢行、不重复搬;极罕见的「归档后前缀被改」⇒ 中止并如实报孤儿归档(行同时存在于两处,不丢数据)。
收件视图与派生回执(同一块,只读调用方自己的会话面)
team_link_team_read 在 tasks 块之后还追加一块(既有四块一字未动,唯一新增就是它):
- 收件视图(谁对我说了什么):读调用方自己的会话面,抽出正文里带
ref=t-<n>的消息(meta.ref的映射约定;归组面 = 任何消息文本里第一个ref=t-<n>——不要求 banner 形状,也不排除本会话自己发的消息,取不到发送方会话 id 时如实回落标(本会话)),按任务号分组给出「谁 · 什么时候 · 开头几个字符」。两个尺寸常量:读最近 20 条消息(TASK_INBOX_MESSAGES)、每条预览 80 码点(TASK_INBOX_PREVIEW,越界截断并标注省略)。没有ref的消息不进视图但如实计数;ref指向从未登记过的任务号时照样分组显示,只在该组标注(该号在台账里未登记 —— 可能是笔误,也可能是先发后记)——不静默丢弃。 - 派生回执(我发出去之后,对方动了没有):纯派生、零新增持久状态。投递事实取自调用方自己会话面里的
team_link_send调用(既有的tool/call,或run_code里发出的tool/ptc-dispatch——只读既有事件类型,不新增)。结构化回执按callId配对:tool/result的meta挂回那一次调用,用回执里投递当时解析出的目标与结果(全队表达式在那一刻已展开,换届之后仍指向当时那个会话);没有回执(run_code那一类)才按实参兜底解析寻址表达式,其中team:<name>/<role>/team:<name>/*是按读取时的名册解析的 ⇒ 该行如实标注「按当前名册解析(该表达式按读取时的名册解析,未必是投递当时那一个)」(裸 id 是字面量,不带此标注)。窗口的单位是「最近 20 次team_link_send」(TASK_INBOX_MESSAGES),不是 20 条消息;渲染行再截同一个常量,超出时给一行(本次还有 M 行未显示:一行 = 一次投递里的一个目标;同一屏界常量 TASK_INBOX_MESSAGES)(M = 真实差额,不静默截断)。反应判据写死四态:
| 情形 | 读数 |
|---|---|
未点名读目标 T |
未读(未点名读该会话;要读 → readIds=["<id>"]) —— 不猜 |
点名读了 T,投递时刻 t 之后存在 assistant 事件 |
✅ 有反应(HH:mm 起有 assistant 事件) |
点名读了 T,t 之后没有 assistant 事件 |
⚠ 无后续反应(该时刻之后无 assistant 事件) |
回执里 outcome != delivered 的那一笔(第 4 态) |
未投递(outcome=<回执里的原值>) —— 只陈述回执自己的 outcome,不下结论(不写「失败」「被拒」这类裁决词,也不冒充上面三态里的任何一个:那一笔根本没送到,谈不上「有没有后续反应」) |
成本不变量:≤ min(12, 1+|readIds|) —— 自读占 1 个额,所以目标面最多 11 个;未点名时目标面零读(断言锁死,防它悄悄变贵);超出读额的点名逐个点名标「未读」,不静默丢。读不到的面一律标「未读」,绝不用空数据算读数。
它是读数,不是裁决:「无后续反应」既可能是没接住,也可能是还没想完——卡里只陈述事实(有没有后续事件)。台账 = 谁声称了什么;收件视图 = 谁对我说了什么。
读面给的是「派生读数」,不是裁决(§9.3.4):team_link_team_read 在原始窗口(末 20 条)之前给一段派生视图,扫描最近 500 行,逐任务一行:
--- tasks.md(只追加台账;显示末 20 条)---
共 5 行,显示 5 条 · baseHash=<16hex>(仅供参考/审计:tasks 只追加、不接受 baseHash 参数)
(派生读数:扫描最近 500 行;逐任务给「最后主张」与「未消解存疑」——是读数,不是裁决)
- t-7 · 5 行 · 最后主张 done(2026-09-26 21:19 · session-wb · 已由 #45 撤回 · 撤回者 session-wb)· 存疑已由 #45 消解
- t-8 · 1 行 · 尚无主张(仅登记)
(原始行)
41 | 2026-09-26T21:10:02.123Z | session-lead | plan | t-7 | 让 worker-b 复核 §3 的行号
- 最后主张 = 该任务最后一条
claim/done/block;一条都没有 → 「尚无主张(仅登记)」; retract不抹历史:被撤回的主张仍然显示,只是标上「已由 #N 撤回 · 撤回者 …」——原主张者与撤回者两个 id 都出现(谁有资格撤回由人判断:黑板没有写权限门);多条撤回时#N取最后一条,撤回条数不另印(原始行里逐条可见);- 未消解存疑 = 有
dispute、且其后没有该任务的claim/done/block/retract→ 标⚠ 未消解存疑 N 条;否则标「存疑已由 #N 消解」; - 绝不自动裁决:派生只做「取最后一条 + 看其后有没有新主张」,不比较正文、不按时间戳定胜负。措辞一律「最后主张 / 未消解存疑」,不输出「状态=完成」这类把主张写成事实的词;
- 三类边界如实标注:文件不存在 → 「(文件不存在,按空处理:0 行)」;超过扫描上限 → 「(派生只扫描最近 500 行;更早的 N 行未参与派生)」;不可解析行 → 「(N 行无法解析,未参与派生)」;
- 时间基(渲染 = 本地时基,原始行 = UTC ISO):派生视图那个时间戳(上面样例里的
2026-09-26 21:19)是本地时基YYYY-MM-DD HH:mm,与同一屏末尾那句(读数 …)同一套本地格式化 —— 两处是给人对比用的(「这条主张是刚发生的吗」);而(原始行)之下的原始行第 2 列是toISOString()的 UTC 值,逐字保留(事实源,可排序、可外部消费)。两者相差一个时区偏移,不是同一条串的截断;时间列解析不出日期的手写行单行化后回显该列(preview()会把内部连续空白折叠成单个空格并 trim;不编造时间、也不打印NaN)。(切 ISO 串前 16 位是 2026-09-27 真机冒烟修复轮废弃的旧做法——真机同屏差 8 小时。)
明确不做(本批):不做状态机 / CAS / 自动裁决 / 自动推进(「送达即完成」禁止)、不做写入权限门、不做跨会话物理队列;decisions 与 discipline 的语义与文案一字未改(唯一例外是白名单拒绝文案由两值改三值)。
实现约定(设计未明说、按既有约定裁决的细节):
workspace只在团队首次由会话创建时捕获,过后永不改写。用户手工建的行若workspace为空,第一次会话侧upsert-team会补记它(唯一一次例外,否则该团队的黑板永远没有根);- 版本史按任期记:一段任期一条,
until: null标在任;set-role/retire关掉旧条目并写 note。首次指派(无旧任)把 note 记在它打开的那条上,避免 note 丢失; - 镜像里时间戳统一
YYYY-MM-DD HH:mm:ss(本地);decisions/tasks行里的时间是toISOString()(UTC 带毫秒),便于排序与外部消费;而tasks的派生视图渲染出来的时间戳是本地时基YYYY-MM-DD HH:mm(与同屏「读数」同一套格式化)—— 渲染 = 本地时基,原始行 = UTC ISO,见「任务台账」一节; - 没有会话身份的写入仍被接受(黑板无门),author 记
unknown——不编造身份; decisions写入是读-算 seq-追加(appendFile,不重写正文):并发追加最坏只是两条同 seq,不会丢行。
推荐起手的 discipline.md(把协作纪律写成条款,而不是靠每个会话自觉 —— 四条都只用今天已有的能力,零新增机制):
1. 派活即落账:派活的人当场把这件事写进任务台账(tasks.md,kind=plan),并拿到插件分配的任务号(t-<n>)。
2. 回报必带任务号:回报消息的 meta.ref 填对应任务号 —— 主会话不必记得全部计划。
3. 记不清就读台账,不靠回忆。台账是路标,不是事实。
4. 验货查文件与证据,不查台账:关于「做完没有」的结论,永远以各会话的原话与证据为准。
台账推荐用法(tasks.md 已实施,§9 第一批 —— 派活落账 / 回报带 ref):
- 派活即落账:派活的人当场调
team_link_team_append(file="tasks", kind="plan", line="让 worker-b 复核 §3 的行号"),把它返回的t-<n>写进派发消息的meta.ref。计划从此住在文件里,压缩够不着;主会话失忆后一次team_link_team_read就能读回「每个任务的最新主张」; - 回报带
ref:回报消息的meta.ref = t-<n>(字段今天就有,≤16 字符、已校验、已渲染在消息 banner 首行)——主会话不必记得全部计划,看ref就知道这是哪件事的更新; - 其余动作各追加一行:
claim(接活)/done(声称完成)/block(卡住、需要授权)/dispute(存疑)—— 都用task="t-<n>"。反驳也是追加一行,不是改旧行(台账只能追加,谁也覆盖不了谁); - 边界:台账是路标——验货仍然查文件与证据。读面给的「最后主张 / 未消解存疑」是派生读数,而工具绝不替你裁决:主张照原话走消息,存疑靠会话之间说清楚。
这四条的由来与完整论证(含只追加台账的原始层/投影层之分)见
docs/team-ledger-and-mode-design-2026-09-26.md§1 与 §2.5。
五、团队换届 rotation
团队换人不是「改个 current」:新任会自动继承一条绕过两道批准门的免确认通道(pairs,连接收方的显式 reject 策略都覆盖),所以交接被拆成两个阶段 + 一次性令牌 + 域限定迁移 + 可回退的临时信任。
sequenceDiagram
autonumber
participant R as 现任(旧任)
participant P as 插件
participant S as 继任者
participant W as 团队其他成员
participant U as 用户
R->>P: team_link_rotate action=prepare(successor)
Note over R,P: 整队:action=prepare-batch + successors="角色=会话 id, …"(或 successor:"auto")
P->>W: [rotation-freeze] 固定冻结清单(停哨兵/后台 job)
P-->>R: 令牌 T(30 分钟有效,明文只此一次)
R->>S: 交接 prompt(含 T,内容由模型起草)
S->>P: team_link_rotate action=claim(token=T)
P->>U: 单个对话框:逐项勾选要迁移的 pairs
alt 在场并提交(ratified)
P->>W: pairs 域内迁移(正式)+ [rotation-done 已批准]
else 无人值守 / 超时 / 无确认服务
P->>W: pairs 域内迁移(provisional, 24h)+ [rotation-done 待批准(24h)]
U-->>P: 24h 内在设置里把该 pair 的 provisional 置 false → 转正式
P->>W: 24h 未批准 → 删除迁移出的 pairs + [rotation-expired]
end
P->>W: 30 分钟无人认领 → [rotation-cancelled](旧任仍为 current,解除冻结)
prepare(Phase A,只能由该角色的现任会话发起)
- 前置:
exec.agent.id === roles[role].current;用户路径是设置 UI,不是工具调用; - 速率限制:同一 team+role 在 10 分钟内已有 pending 或刚完成过一次换届 → 拒绝(防换届风暴);
- 令牌:
randomUUID(),绑定三元组(team, role, successor),30 分钟有效、成功认领即作废; - 快照:把 prepare 时刻的
pairs/trustedSenders/rememberTargets/ roster 全量写进rotationBackups里本次换届的那一条(按换届各存一份,键 =rid;旧单槽rotationBackup只作迁移源)—— 这才是对称撤销的还原依据; - 广播:向团队全部在任成员投递
[rotation-freeze]冻结清单(停哨兵/后台 job → 确认无在飞动作 → 状态冻结回报 → 等待交接结果通知); - 返回:令牌明文(唯一一次)+ 掩码形式 + 交接指引(30 分钟有效、「交接内容由模型起草,机制与判断分离」、上任首动作建议
/goal resume或建新 goal、错峰默认、以及successor:"auto"的自建路径——§3.6.4 的手工兜底始终保留,见下一节)。
claim(Phase B,只能由 pending 指定的继任者会话凭令牌发起)
- 前置:
exec.agent.id === pending.session(且 pending 未过期);令牌精确匹配且三元组一致; - 一个对话框(多选):把全部「退役者↔同 team 成员」的候选 pairs 列成一个多选问题——勾选 = 迁移,不勾选 = 随退役清理(今后该对端走正常首问门)。对端在团队外的 pairs 不进候选但会在 detail 里被点名(透明)。候选为空时不弹框:没有可批准的东西,落定后状态词记
无待迁移对而不是「已批准」; - 在场确认 = ratified;超时(3 分钟)/ 无确认服务 / 对话框失败 = 无人值守路径:全部域内候选以
provisional迁移并开 24h 回退窗口; - 迁移与撤销:
- 迁移 = 删除退役者的旧 pair,新建
{a: 新任, b: 对端, provisional, expiresAt}; - 对称撤销 = 退役者持有的 pairs(全部)、
trustedSenders中指向它的项、rememberTargets中指向它的项,一并清除——退役会话可能还活着,不吊销就是永久保送;
- 迁移 = 删除退役者的旧 pair,新建
- 落盘顺序:迁移 + 落定(
current/ 版本史 / 迁移标记)在同一笔写入里完成,随后才清除pending。因此「pending 还在」且「current已是继任者」只可能意味着上次 claim 没收尾;幂等判据是current === pending.session,而不是「迁移清单是否非空」(域内没有候选、或每个候选对端都与继任者本已配对时,本次迁移本就不新建任何记录); - 幂等:同一令牌重放 → 返回既有迁移清单、不重复迁移、不重复改信任数据,只补做收尾(清 pending、补写
roster.md镜像、重发一次rotation-done避免 worker 因崩溃卡在冻结里);成功之后令牌作废; - 广播
[rotation-done]:已批准/待批准(24h)/无待迁移对。第三个词用于域内没有任何候选 pair 的换届:既然没弹过确认框,就不能谎称「已批准」。落定时该状态词一并记进roles[].rotationStatus,重放直接读记录值——它不由provisional/ 迁移清单重推(「对话框答了但一条都没勾」是 ratified 且无迁移、无回退窗口,正是重推会失真的窄边缘)。
未批准路径与到期清扫
| 对象 | 期限 | 到点行为 |
|---|---|---|
pending(令牌) |
30 分钟 | 清除 pending + 广播 [rotation-cancelled]:旧任仍为 current、令牌过期未认领、解除冻结。若 current 已是继任者(上次 claim 只差最后一步收尾),则静默清除 pending、不广播——换届其实已经发生,「旧任仍为 current」与事实相反 |
provisional pairs |
24 小时 | 删除迁移出的 pairs + 版本史追加 provisional 未批准过期 + 广播 [rotation-expired]:新任保持 current(换届事实已成立,降格要用户显式操作),信任回退为正常过门。若该窗口迁移出的 pair 都已在设置 UI 补批准为正式通道(或本次迁移本就没新建通道),则窗口静默关闭:不记版本史、不广播——没有回退发生,「迁移的 pairs 已删除」会是假话 |
清扫有两个触发面:看门狗的同一个巡逻定时器(定时器随注册存在——一个看门狗都没注册时它不跑),以及惰性检查(每次 roster 触碰 / rotate 调用 / team_read 读取都会先扫一遍)。换言之:只要团队里还有人读黑板/动 roster/走换届,过期 pending 与过期窗口就不会漏;三者都没人碰时,冻结状态要等下一次触碰才解除。
投递侧另有独立守门:expiresAt 一到,该 pair 立刻不再算配对(pairRecordBetween 视过期 provisional 记录为无 pair),投递当场退回正常双门——不必等清扫真的删掉那一行。过期 pairs 的删除也不依赖角色记账:doomed pairs 的计算与删除排在角色记账判据之前,所以即使用户在设置 UI 手删了 roles[].provisional 窗口(甚至整行角色/团队),过期通道照样会被清除。
补批准入口是设置 UI:24h 内把该 pair 的 provisional 置为 false 即转正式(唯一口径——不要用删 expiresAt 的方式:那会留下 provisional: true 且永不过期的记录,可见面文案与通道事实不一致);已回退后不再复得。补批准只改 pair、不改 roles[].provisional 窗口——窗口到点时若域内已无待回退的 pair,它就静默关闭。
自动换届(③):successor:"auto"、交接文档与 /team_rotate
换届机制(令牌 / 域限定迁移 / 对称撤销 / 24h 回退 / 到期清扫)早已发布;③ 消掉的是它唯一的人工前提——继任者会话原本必须先在壳里建好。两条入口:
| 入口 | 谁发起 | 做什么 |
|---|---|---|
team_link_rotate action=prepare, successor:"auto"[, handoff] |
现任模型(工具调用) | 自建继任者 + 写交接文档 + 铸令牌 + 广播 freeze + followup 把令牌与交接正文投给继任者 |
/team_rotate <role> [team=<name>] |
人类(命令) | 校验发起者确为该角色现任 → 向该会话 followup 一条指令(起草交接并调用上面的工具)→ 返回摘要给 UI |
/team_rotate 只做机制:CommandResult 的文本由派发它的 UI 渲染、进不了模型上下文,所以命令无法自己产出交接正文——那必须由模型起草。命令也不建任何会话(零创建、零 pending、不改信任);真正的新建 + 交班在工具侧,且必过一次确认框。命令与工具入口并存,工具入口是兜底。
确认(§11.4.1):新建会话 + 交班是爆炸半径大的动作,工具路径在建会话之前必过一次 userQuestions.ask,框内写明新建几个会话(successor:"auto" 每次恰 1 个,§11.2 逐角色)、新会话 id、cwd、模型情形(本路径没有 model/provider 参数 ⇒ 两半都由插件解析并带上宿主缺省模型选择**(agentDefaultModel.currentSelection(),§10.2.2 模板第 ④ 步——不是「新会话继承默认选择」,那条判断已被真机推翻,见 DEFECT-3);preset 取宿主缺省,缺省也解析并挂载,见 §10.2 的 DEFECT-1 修复)、保守成本口径、将触碰的信任面、取消 = 零副作用**。无确认服务 → fail-closed(不建、不铸令牌、不广播 freeze)。
自建继任者(§11.4.2):与 /team_session 完全同源——根会话(meta 只放 cwd / agentPreset(缺省也解析出的那一个),origin / parentSession / delegationDepth / parentAgent 一律不写)、id 形如 team-link-<team>-<role>-<uuid8>、从插件根 ctx 创建并由插件持有 handle(所以 §10.2.5 的生命周期代价同样适用);cwd 取发起会话的工作目录,取不到就拒绝——绝不用宿主进程的 process.cwd() 顶替。这一条是 DEFECT-1 影响面最大的地方:继任者与 worker 走的是同一个 createRootAgent(原先只叫 buildTeamSessionCreateOptions),所以「缺省不挂 preset」在换届路径上意味着「令牌投给了一个跑不起来的持钥者、而旧任已冻结」。DEFECT-2 的影响面同源:同一个函数也负责把会话挂进工作区(workspaceRegistry.create → meta.cwd = workspace.path → attachSession),所以不修则换届后的新协调者在侧边栏里同样找不到。
交接文档(§11.4.3 / §11.9.6):插件写 <workspace>/team/<name>/handoff-<role>-<YYYYMMDD-HHmmss>.md,三层结构——
| 层 | 谁写 | 内容 |
|---|---|---|
| 头 | 插件 | schema / team / role / 前后任 id / preparedAt / claimedAt / 令牌掩码 / rotationStatus / 完整性判定 |
| 事实段 | 插件 | 与 claim 返回文案同一事实源(同一个构造器渲染迁移/未迁移/对称撤销行,同一常量给 freeze 正文,同一函数给 provisional 回退窗口)——「不可两处口径」 |
| 正文 | 旧任模型 | 五个硬节 + 软节 |
正文的五个硬节逐字为 mission / in-flight / commitments / unknowns / task-and-goal(软节 first-actions / team-map / conventions)。缺项阶梯(验证点在任何副作用之前):
| 情形 | 处置 |
|---|---|
successor:"auto" + 正文缺失/空 |
拒绝于工具入口:零建会话、零令牌、零 freeze,错误文案给出五硬节脚手架 |
| 正文在场但硬节缺 / 空 | 拒绝并点名缺哪些节 |
| 软节缺 | 放行 + 警告(头部记录) |
显式 successor + 无正文 |
放行 + 警告(那个会话有自己的生命与上下文,M4 现语义不收紧) |
| 文档写入失败(auto 路径) | abort-before-prepare:不铸令牌、不广播 freeze,已建会话如实报为孤儿 |
| claim 时 | 不复验文档(它是审计件,不是执行件) |
诚实原则(写在契约里):插件只把关结构(节在场 / 非空 / 有界),不把关内容质量——五个节全写
TODO也能过。presence 门防遗忘,不防敷衍。
投递(§11.4.5):令牌明文(只此一次,此后一律掩码)+ 交接正文 + 「立即 claim」用 followup 发给继任者(不是 inject——任务需要驱动),消息 source 仍恰三成员 {kind, form, senderSessionId}。
claim 一步未省(§11.6 红线):自动化不得跳过 claim / 令牌 / 域限定迁移 / 对称吊销——信任迁移仍要继任者在 claim 时逐项勾选(无人值守则 provisional + 24h 回退),本次确认框不迁移任何 pairs。
失败与孤儿(§11.5):① 令牌 30 分钟未认领 → 既有 rotation-cancelled(旧任仍为 current、解除冻结),并额外点名本次插件自建的那个继任者(「仍存活但未认领,可收编或关闭」)——否则它会变成一个没人知道的孤儿;② 崩溃在 create 与 prepare 之间 → 复用 §10.2.6 的 pending-create 意图(TTL + 启动清扫 + 可收编清单);③ 部分成功不回滚:会话已在盘上、可能已被打开,一律如实报告。
写文档的时机(一处需要读者知道的设计张力):§11.9.6 要求「写文档失败 → 不铸令牌、不广播 freeze」,而头部又要带令牌掩码、事实段要带 freeze 投递摘要——后两者在写文档那一刻还不存在。本实现取前者严格成立:令牌先在内存里铸出(只为拿到掩码)→ 写文档 → 走 prepare(令牌落盘、freeze 广播、投出);写文档失败时那次铸出的令牌从不落盘、从不投出、prepare 不进入,可观测意义上仍是「零令牌、零 freeze」。事实段里 prepare 之后才产生的类目(迁移清单、对称吊销明细、freeze 逐目标结果)如实标为「待 claim 落定 / 本文件先于广播写入」,落定后以 claim 返回为准——文档不追写。
现任无人时的恢复(§11.9.3–§11.9.5):诊断面 + team_link_recover
卡住的是身份与信任面,不是全瘫(§11.9.1):黑板写(team_link_team_append)与跨会话投递(team_link_send)都不过 writerGate,团队照样能说话;写不进去的是 roster 变更、换届与建队登记。硬死锁只有一格:policy.writer=coordinator(默认)且死的是 coordinator——死 worker 时活协调者可以 retire + set-role 重建(丢信任拓扑但不死锁),writer=any 的队任何会话都能 set-role 补位。批 H(2026-10-12)加了另一格出口:team_link_roster action=delete 豁免这道门(把关交给人类确认框),于是已退役 / 现任空缺的队也能从名册里真正删掉——但它只删不修(要恢复一个在职协调者仍走设置 UI / team_link_recover),所以下面那张「第一动作」表三格的语义一格未改(vacant 那一格多了一个「这个队不要了」的出口)。
诊断面不新增任何持久状态(§11.9.3):liveness 是进程内、瞬态、观察者相对的事实,落盘即陈旧(一次插件重载会把全体插件自建会话同时写成 dead),而且刻意空缺与死亡空缺必须可分。于是两个词只在读取时派生、只在活着的读面出现:
| 词 | 含义 | 第一动作 |
|---|---|---|
vacant |
current=null(retire 或用户经设置 UI 造出的显式表达) |
设置 UI 指定现任 / team_link_recover action=reappoint / 这个队不再需要了 ⇒ team_link_roster action=delete 把这一行清掉(批 H;豁免写权限门、需人类确认) |
seated-dead |
current 非空但 agents.get(current) === undefined(悬空指针) |
在侧边栏重新打开那个会话(同 id 复活,信任零手术) |
出现的地方:三道门(writerGate / rotateGate / retireGate)的拒绝文案(gate 本体保持纯函数,由有 ctx 的工具层富化——今天这道文案对死现任是误导性的)、team_link_roster get 的现任行、以及启动清扫新增的一行「各团队 current 无活代理的角色」。roster.md 镜像刻意不加:它是落盘文件,把读数瞬间烙进持久物会立刻陈旧。
恢复工具 team_link_recover(§11.9.4):恰两个封闭动词。
| 级 | 动词 | 机制 | 何时用 |
|---|---|---|---|
| L1 | revive |
ctx.agents.resume 复活同一个会话(身份不变、roster 不动、信任零改动),并按宿主自己那条 resume 路径的时序把这具代理组装齐:模型选择(agentOptions)+ 这个会话自己存下来的 preset 组装源(工具面与人设)——组装不出来就拒绝恢复(零 resume、零写入),绝不留下一个第一回合就死在 prompt variable "{{model}}" has no value 的代理(DEFECT-5) |
死亡绝大多数是重载/重启假象 |
| L2 | reappoint |
人类对话授权的 prepare:候选 = 插件从本队活成员算出的成员 ∪ 常驻的「自建继任者(新建会话)」 → 人类勾选 → 铸令牌绑定 (team, role, successor) → 逐字走既有 prepare(rotationBackups 里本次那条快照 + rotation-freeze 广播)→ 继任者凭令牌 claim(claim 一步不改,不新增令牌类型) |
现任不会/不应再回来 |
L1 的适用域(revive 只对插件自建会话开放):resume 的 ownerCtx 是插件根 ctx,复活后该代理的运行时所有权归插件、插件卸载即拆;对人类自建会话做 revive 会把它的生命周期从 UI 转给插件,比现状更差——所以人类自建的会话只输出深链指引(「请在侧边栏打开」)。插件自建的判定是两半:本进程仍持有它的 AgentHandle(teamSession.hasHandle),或它的 id 合 §10.2.2 的文法 team-link-<team>-<role>-<uuid8>——后半是跨重载的那半,而重载恰恰是 L1 最要紧的时刻(重载后 handle 表按构造就是空的)。resume 不可用(无 factory / 无 sessionPersistence)或抛错 → fail-closed 报告,零改动。
八条硬约束(§11.9.5,每条都落成会红的断言):① 动词封闭(只 revive/reappoint),不接受任意 roster 字段写入、不改 policy.writer;② attended-only——必须有人在对话框里点一下,刻意不设 provisional / 无人值守变体,无确认服务即 fail-closed(与 claim 的不对称是刻意的:pair 迁移可被清扫自动回退,incumbency 不可);③ 候选由插件计算(活成员 ∪ 常驻合成候选「自建继任者(新建会话)」),模型只传 team(+可选 role),不得指定继任者 id(对话框选项就是候选,答案按 label 回读——合成候选连 id 都还没有,它由插件在人类勾选之后才铸);④ revive 只绑当前 current(不存在「复活任意历史会话」的动词);⑤ writerGate 原样不动;⑥ 绝不把 policy.writer 降级为 any 当作「修复」;⑦ 限速(与换届同一 10 分钟窗口)+ 三处留痕(role 行的 recoveries 备注 recovery(<verb>, vacant-due-to-death, requester=…) / roster.md 镜像 / decisions.md 追加——黑板无门,死锁下也能落账);⑧ 进入即先跑既有过期清扫。另有一条不编号的发起域(§11.9.5,见下「发起域」段):发起者只能是本队现任成员或该角色最近一任前任——它不属于这八条,也不改任何一条。
写时复检(TOCTOU):对话框横跨任意长的人工等待,所以条件由宿主观测、不由调用方主张——本轮跑两次 agents.get(current) === undefined(对话框弹出时、落笔前各一次),现任已复活则中止「现任已复活,无需恢复」;候选在确认期间死亡同样中止(否则原地再造一个死结)。
角色面(两个动词都受理任意角色,差别在动词语义上):revive 受理任意角色(0.3.9 批次 2 §4.2 (a) 放开了原来的 coordinator 窄域)——重载拆掉的是所有插件自建会话,不只 coordinator 的;而 §11.9.1 只论证过「死的 coordinator 必须可救」,从未论证「死的 worker 不许救活」,那条窄域是范围最小化选择、不是安全属性。放开它不移动红线一寸:revive 是身份不变的操作——不写 roster、不铸令牌、不动 pairs/trustedSenders/rememberTargets、也不碰 policy。所有权门原样不动(只对插件自建会话开放)。reappoint 也受理任意角色——授权从不源自 coordinator 身份(唯一来源是对话框里人类那一下点击)。两者的差别在动词上:revive = 同一个会话复活(零写),reappoint = 换人(令牌 + 快照 + 冻结,走完整 M4)。不带 role 时只输出诊断(每角色一行:现任 + 活性 + 在飞令牌),零副作用。
选「自建继任者」时发生什么(§4.2 (c):链路的每一步都与既有机制同源,收尾的投递与 successor:"auto" 同一条):候选列表常驻一项「自建继任者(新建会话)」——宿主可编程创建会话时它始终在列,不是「只在没有活候选时才出现」(事故当场的候选长度是 1 不是 0,条件式触发根本不会激活)。能力闸门只约束这条支路,不在动词入口(0.3.9 批次 4 收窄):宿主没有 agents.create ⇒ 弹框照开,只是候选里不出现「自建继任者」(只列活成员——活成员改任不需要该服务),仅当连一个活成员候选都没有时才 fail-closed 报告且零弹框。选中后的链路一步不新造:按 §10.2.2 铸 id(team-link-<team>-<role>-<uuid8>,与 /team_session、successor:"auto" 同源)→ 交接文档由插件从 roster 事实生成五硬节(mission / in-flight / commitments / unknowns / task-and-goal;读不到的项如实标未知,不编造——task-and-goal 一节填的是 goals 服务的读数,读不到才标未知)→ prepare 逐字跑 → 审计行 verb=reappoint → 用 followup 把令牌与交接正文投给刚建出的继任者(与 successor:"auto" 的投递同一条:不是 inject,任务需要驱动;消息 source 仍恰三成员)。代价如实声明:继任者是空上下文的新会话,历史不迁移;信任靠它本人 claim 时逐项勾选迁移。收敛性红利:此后每次恢复的终态都是插件自建 id,而插件自建 id 正是 revive 的适用域。
发起域(§11.9.5 的「发起 ≠ 授权 ≠ 复权」):发起者只能是该团队现任成员或该角色最近一任前任(发起权不依赖信任、只依赖身份资格;旧任上下文最完整,「回聘旧任」本就是最自然的恢复)。域外的活会话被拒绝,拒绝文案点名现任成员集合、该角色的前任与调用会话,并指出设置 UI(R2 级,用户在那里是超级写者)仍是永远可用的出口——挡住的只是会话路径,不是人。代价如实声明:不属于本队的活会话(人类随手开的、没登记进 roster 的会话)不能发起恢复,这是有意的收窄;带 role 的诊断读态不受此限(它零写入)。
内部通知
四种通知(rotation-freeze / rotation-done / rotation-cancelled / rotation-expired)的正文是插件常量:只有团队名、角色名、会话 id、读数时间、状态词被插值,且每个插值都先过单行清洗——模型的 note、消息正文一律进不去(与看门狗 tick 同一条红线)。
- 免发送方审批(正文是插件常量,不是模型可注入的载荷),但接收方的 inbound 策略与
blockedSenders照旧生效; - 发送方身份:
prepare/claim用发起该动作的会话;清扫类通知用「该角色现任」(取消时是旧任,回退时是继任者);现任空缺且无继任者时通知不发并如实报告,绝不编造发送方; - 收件人并集:
rotation-done/rotation-cancelled的收件人 = 团队在任成员 ∪ 本次换届快照(rotationBackups里该rid那条;旧槽经读时折叠)的roster里该角色当时的旧任(仍存活且不是继任者本人时)——退役者在落定之后就不再是任何角色的current,不并进来就永远收不到「换届完成」; - 内部通知一律不弹确认框:通知在工具调用或巡逻里逐个串行投递,接收方策略为
ask时该目标记一行「不弹确认框」并跳过——几个 ask 成员各弹 3 分钟就会吃掉整个工具预算,弹框也会把巡逻阻塞 3 分钟; - provisional 可见面(不下放给
source、也不加meta字段):send经 provisional 通道投递的返回文案后缀、rotation-done的状态词、roster get的 pending/provisional 行、list_sessions会话行上的「provisional 配对 N 条」标记。
换届记账的结构(teams 键内)
roles:
- role: coordinator
current: session-new # 换届后 = 新任
pending: # 仅在 prepare 与 claim 之间非空
session: session-new # 继任者;只有该会话能 claim
token: 1a2b3c4d-... # 一次性令牌(一切渲染都是掩码 tok-1a2b…9f0e)
team: night-shift
role: coordinator
createdAt: 1700000000000
expiresAt: 1700001800000 # = createdAt + 30min
migratedPairs: [] # 上次 claim 已迁移的通道清单(重放时回显);幂等判据不是它是否非空
note: 交班 # 可选:随 pending 带到 claim 的版本史备注
rotationId: rot-1a2b3c4d-… # 批 Q 起:本次换届的稳定标识;旧 pending 无此键 ⇒ 读时确定性推导 legacy:<角色名>
rotationAt: 1700000000000 # 最近一次「完成」的换届(速率限制的另一半)
rotationStatus: 已批准 # 最近一次落定换届的状态词;重放直接读它
provisional: # 未批准的 24h 回退窗口;ratified / 已回退时为 null
at: 1700000000000
expiresAt: 1700086400000
session: session-new
rotationBackups: # 按换届各存一份(撤销依据;每条的 rid 为键)
- rid: rot-1a2b3c4d-…
at: 1700000000000
backup:
at: 1700000000000
pairs: [...] # prepare 时刻的全部 pairs(副本)
trustedSenders: [...]
rememberTargets: [...]
roster: {...} # prepare 时刻的团队记录快照(含旧任)
rotationBackup: null # 旧单槽:仅迁移源;下一次换届类写吸收为 null
# ★ 不再「永不自动清理」:claim 成功 / 过期取消 / 清扫会按 rid 只删自己那条
错峰默认:先换协调者 → 稳定 → 再换 worker,任何时刻保留一个活记忆;一次全换之前必须先跑冻结清单并把交接文档落盘。
六、策略配置
设置命名空间 team-link(设置 UI 可直接编辑)。
落点二选一,一个时刻只写一处:
- settings 服务在场 ⇒ 状态只在 settings 里,本插件不写那个文件(读不读见下一段:命名空间为空时会读进来折叠并入,已有数据时连读都不读);
- settings 不在场 ⇒ 同一份状态写在
<DSH_HOME 或 ~/.dsh>/team-link/policy.json(路径优先取DSH_HOME,缺失才退回~/.dsh;落全局目录而非工作区,团队名是全局寻址的);文档形状{schema, savedAt, foldedAt, policy},policy与下表的键集一一对应(无损)。写入是原子的(policy.json.tmp→fsync→ 覆盖),写失败保留内存态并告警、下次写入重试; - 两个落点都没有 ⇒ 进程内记忆 + 一行告警(最后兜底)。
policy.json读不出来 / 不是合法 JSON / 没有 policy 对象时:插件不覆盖那份文件(写入每次都直接跳过并说明「未落盘、该文件未被覆盖」),状态暂存进程内存 —— 修复或移走该文件后重启 DSH 即可恢复(每个进程只读一次文件,改好了不重启不会自动生效)。
settings 之后再挂上时,文件里的内容会折叠并入 settings(服务成为落点),并立刻把合并结果写回文件、盖上 foldedAt;此时文件与服务都有数据,以 settings 为准,文件那一份原样归档成 policy.superseded-<时间戳>.json 并留痕(不做静默合并)。回写挂在 attach 后置链的末尾(旧命名空间迁移之后)—— 写早了,文件会缺迁移进来的那一份信任数据。
settings 从一开始就已在场时同理:命名空间若还是空的,文件里那一份照样折叠并入(不会因为「挂得早」被跳过);命名空间里已有团队则一律 settings 为准,文件那一份连读都不读。
怎么确认它存在哪:team_link_roster action=get 与 team_link_status 的首行给出三态之一 —— 存储:设置服务(team-link 命名空间) / 存储:文件 <绝对路径> / ⚠ 存储:仅进程内存(重启即失)—— 原因:…(含 文件损坏(不覆盖;请人工处理) 这一项,见上一段的处置办法);状态卡里团队清单那一段还会再说一遍来源(来源:设置 team-link 的 teams 键 / 来源:文件 <绝对路径> 的 teams 键 / 来源:进程内存的 teams 键(重启即失)),跟着实际落点走。
命名空间的键集十一项(FR-9 起,PolicyConfig 声明面同批更新;★ 批 I 追加第十项 blocks;★ 批 34 / FR-7 追加第十一项 antiDrift):
| 键 | 类型 | 说明 |
|---|---|---|
receiveMode |
ask / accept / reject |
默认 ask:逐条确认 |
trustedSenders |
string[] |
免确认接收的发送方会话 |
blockedSenders |
string[] |
旧格式屏蔽名单(部署级全局:挡的是该发送方对所有会话的投递)。原样保留、不再新增,检查时与 blocks 并集参与(旧记录不静默放开);出口 = team_link_block action=clear-legacy sender=<id>(被它挡住的发送方本人不能自清,其余任何会话可清;不做批量清) |
blocks |
{a, b, by, since}[] |
配对屏蔽(批 I,屏蔽的真正的作用域):a / b 是两个会话 id(写入时按字典序规范化 ⇒ A↔B 与 B↔A 是同一条),by 是点屏蔽的那一方(只有它能解),since 只作留痕。双向:命中即两个方向都拒;手改时缺 a/b/by 的条目会被丢弃(一行坏的不会带走整张表),缺整个键读成 [] |
rememberTargets |
string[] |
发送方免确认的目标会话 |
pairs |
{a, b, createdAt, provisional, expiresAt}[] |
双向免确认配对通道。provisional: true = 由换届在无人值守路径上临时授予,expiresAt 到期未获批准即自动删除并回退为正常过门;正常配对 provisional: false、expiresAt: 0 |
watchdogs |
{id, team, watcherSession, targets, silentMinutes, intervalMinutes, expiresAt, createdAt, mutedUntil?, wake?}[] |
跨会话看门狗注册(到点自动清理;手改时缺字段的条目会被丢弃,不会让整个命名空间失效)。批 U 的两个加性字段:mutedUntil(静音截止时刻;仅 >0 时写键)与 wake(仅 false 时写键 —— 投递不唤醒);两者与 renewedAt 同一纪律:进归一化白名单、不新增顶层键 |
pendingCreates |
{team, role, sessionId, createdAt, expiresAt, by}[] |
§10.2.6 的孤儿守卫:建会话之前先落的「待创建」意图,创建落定即移除 ⇒ 留到最后的就是唯一证据「有人试过创建」,启动清扫把它作为可收养的会话如实报出来(缺 sessionId / 期限不可用的行会被丢弃) |
sections |
{role: boolean, resources: boolean} |
总开关(FR-9,缺省都开):role=false ⇒ 不注册角色/宪章段(工具返回面的角色重钉同关);resources=false ⇒ 两面都不挂资源行(动态运行时上下文快照 / 状态卡第 ⑧ 段 / 工作台摘要脚注)。★ 批 C 起资源行在模型可见面的落点是动态运行时上下文通道(agent.ctx.systemPrompt.context()),不再是系统提示段;★ 批 D 起同一回合内不重算(按 sessionStats.turns 门控,回合号前进才现算 —— 状态卡第 ⑧ 段与摘要脚注那两处不受门控、照旧每次现读) |
antiDrift |
{ledgerTransitionGate: boolean} |
台账写门开关(批 34 / FR-7,跨仓交付的 team-link 侧;缺省 false = 门关着)。true ⇒ tasks.md 的 done 需该任务先有 claim、retract 需同任务真实存在的指针(两条规则见「任务台账 tasks.md」那一节;拒绝一律零写入、朝向 fail-closed)。嵌套形态是刻意的:将来加兄弟键(如别的 anti-drift 开关)不再动这一节的键集锁。★ 开门 = 两侧各设一次 —— 本键只由本插件的配置面改;dsh-thincoder-suite 的 anti_drift_config 改的是它自己配置面上的同名键,对这里的门完全无效 |
teams |
{name, createdAt, workspace, policy:{writer}, roles:[{role, current, pending, rotationAt, provisional, rotationStatus, history}], rotationBackups:[{rid, at, backup}], rotationBackup(旧槽·仅迁移源), mode, leadSessionId}[] |
团队 roster 与换届记账。mode 是形态(sessions 默认 / agent-team,闭集外的值读时降级为 sessions 并在读面留痕);leadSessionId 是 agent-team 档的 Lead 指针(空串 = 未指定,读面标「Lead 未知」;成员名册一行都不落 settings)。name 必须 [a-z0-9-]+(它是黑板目录的路径段);workspace 是团队首次创建时捕获的会话工作目录、也是黑板根;手改时非法团队名/无名角色会被丢弃 |
七、服务获取与留痕(0.3.7 的关键修复)
这一节只留使用者要用的契约;那次故障的完整调试历程(病灶代码、逐条修法、三个配套细节、红线与回归锁)已移到 docs/verification-log.md——README 是说明书,不是实验记录本。
契约:本插件对 settings / webServer / connection / commands / agentPresets / workspaceRegistry / agentDefaultModel / sessionTitle 等运行期服务一律走可选获取(模块级 inject 恒为 4 项,见 §八)——服务缺席或迟到时,受影响的只是一项能力,插件本体与其余能力照常工作;每一次降级都留一行具名 warn(点名缺了哪个服务、后果是什么)。分界:在决定**「会话能不能跑」的地方(preset 源、模型选择)改为 fail-fast——宁可不建,也不建一个跑不起来的会话;只决定呈现面**的地方(标题、卡片)则降级不阻断。
八、兼容性与字符串安全
DSH 0.1.5 会话格式迁移
0.1.5 的会话日志迁移(V0→V3)逐条校验每条 message 的 source:白名单外的 kind、或成员多一个少一个,都会让整份会话日志拒绝迁移(SessionFormatUnsupportedMigrationError,表现为「这个对话打不开」),且拒绝时不会破坏原始日志。
旧版本插件写的是 kind: "team-link",正是这种会被拒的形状——所以 0.2.3 起改用上游自己的中继形状 { kind: "agent-message", form: "relay", senderSessionId }(白名单见 @deepseek-ai/dsh-session-format-v2-to-v3 的 SOURCE_KINDS;agent-message 的成员集合被锁死为恰好三项,多余信息只能进正文)。
已经在旧日志里的 kind: "team-link" 事件改不回来(kind 已烙在已发布的事件流里),需要外部工具改写原始日志或在导出前修复——修复方向即把 source 替换成上述三项形状。
字符串安全:孤立代理项
半截 emoji(孤立代理项,例如单独的 U+D83D)不是显示瑕疵。DSH 把工具结果逐字放进下一次模型请求的 JSON 体,非法 UTF-16 会让那次请求直接以 400 INVALID_REQUEST 失败;而这段文本已经写进调用方会话历史,于是该会话此后每一条消息都以同一个 400 失败(连 test 四个字母也一样),INVALID_REQUEST 又不在重试白名单里——会话永久不可恢复。
本地扫描 882 份会话日志,只有 5 份含真正的孤立代理项:走 deepseek-official 的 4 份全部在下一次请求当场死亡(0 条成功回复),走 qax 的 1 份存活。诚实交代:没有做重放实验(没有构造含孤立代理项的请求去打 API),这是观测相关性;但机制无争议,且「按码点截断」本来就是这两个函数应有的写法。
- 根因:
preview()/truncate()用slice()按 UTF-16 code unit 截断,切割点落在代理对中间时只留下一半。凶器是列表工具的主题预览preview(topic, 90):emoji 恰好压在第 89 个 code unit 上; - 修法一(不再制造):两个 helper 改为按码点截断(
[...str]迭代),限额与纯 BMP 文本的渲染结果完全不变;truncate()的「已截断 N 字符」计数口径随之从 code unit 变成码点(更正确:原来一个 astral 字符被算作 2 个「字符」,却只输出一半); - 修法二(纵深防御):新增
wellFormed()消毒所有对外字符串——列表工具正文、export 的 md 与 JSON、send的两处批准提问正文、投递到目标会话的 banner(否则毒的是接收方会话)、拒绝文本里回显的目标 id、深链注入的会话快照、team_link_send的结构化回执(tool/result.meta,见 §10.1——它不走output.render,是唯一一条绕过模型可见出口的持久化路径),以及三个工具output.render这最后一道模型可见出口; - 客户端同理:
shortSessionId()的slice改为按码点切,卡片正文、发送方 id 与委托回退文本渲染前都过一遍wellFormed()。这条路径只影响显示(浏览器 DOM 的 USVString 转换本就会把孤立代理项变成U+FFFD,且不会再进入模型请求),属显示层加固; - 验收不变量:本插件返回的字符串里永远不出现孤立代理项——把 emoji 摆在任意切割位置上,输出要么完整包含它、要么完整丢弃它。回执卡另有一条同样口径的不变量(差异审计 F2):卡内每一个字符串成员都过
wellFormed(),判据是JSON.stringify(card)上无孤立代理项。
字符串安全:框定 token 不搬运(批 E,2026-10-11)
它是什么:DeepSeek 的框定 token(全角竖线 U+FF5C 包夹一段短词的那种包裹结构 —— 全角竖线,不是键盘上那个 ASCII 竖线 |)是一个特殊 token。它会偶尔漏进工具调用的参数内容里,而流式解析器把它当成「又一次工具调用的边界」,那一次的参数串就此截断 ⇒ 整个回合以 tool input is invalid JSON 死掉。
为什么本插件要管:它碰到一次就留在对话里(那段文本已进上下文),模型看得见自己上一轮的产物 ⇒ 接着复现。本插件在这条回路里的角色是黑板(全队共享、反复被读)与跨会话投递——一旦它落进黑板,全队每次读都会被再喂一次。
本版怎么处理(2026-10-11 起):
- 一个消毒函数(
stripFramingTokens),挂在既有的三个wellFormed*收口点(覆盖全部 12 个工具)+ 其余文本出口(跨会话投递正文、六个渲染面、deep-link 快照); - 替换而不拒绝:命中的包裹结构换成
[framing-token-removed]。处数写在文本自己里 —— 命中 1 处就是这一串,命中 N>1 处是[framing-token-removed 1/N]、[… 2/N]…(于是「改了就看得见」不需要任何额外状态);没命中 ⇒ 输出逐字节与改前相同; - 入口也管:
team_link_team_append写 decisions / tasks / discipline 落盘前过同一道消毒 ⇒ 黑板文件里不会出现它(这条回路因此断在源头); - 失败方向是 fail-open:消毒自身出错时原样返回,绝不让任何工具调用失败;
- 范围如实:入口消毒只覆盖黑板(
team_link_team_append的三个文件)。其它模型可写的持久化字段(roster 的角色备注、recoveries的 note、团队名)本版不拦 —— 它们的读回路已被收口点覆盖,所以传播环仍然断,但「盘上永不出现」这句话只对黑板成立; - 不追溯:历史已落盘的内容不改写(只保证新写入与读出干净);
- 不修触发层:流式解析器那一侧在宿主 / 适配器里,不在本仓(已出观测报告给平台侧)。
为什么这份文档里不写出它的原串:逐字写出它本身就会提高「写这一动作」被判 MALFORMED_RESPONSE 的概率(2026-10-11 实测:父侧因此自伤三次)。本仓的纪律因此是「构造它必须用码点」(String.fromCharCode(0xFF5C)),源码、测试、文档、脚本一并适用;体检工具的输出也必须遮蔽成 [BARS]。
判据与读数:用码点拼出的匹配式扫全仓(lib/ · 两个测试套件 · docs/*.md · 根 README.md / CHANGELOG.md / AGENTS.md · .investigations/**/*.{mjs,js,md})= 0 命中。
- 扫描命令(由宿主执行):
node .investigations/token-sanitize-2026-10-11/scripts/scrub-sources.mjs . - 读数出处:
docs/verification-log.md的「批 E 分歧修复轮」一节登记了这条命令与该脚本判据口径的对齐;正式读数(全仓 0 命中)以宿主的回执节为准 —— 在宿主回执落账之前,0 命中的背书方是审计侧的独立自检(见该节)。★ 评审 🔵#7 更正:原措辞把「审计侧自检」说成了正式读数。
九、安装
宿主版本支持(先读这一节)
| 版本面 | 状态 | 建议 |
|---|---|---|
| Node.js | ✅ 本版实测通过:>=22(真机只验过宿主自带的 Node v24.14.1) |
用 Node 22 及以上(更低的下限我们没验过,见下面那句) |
| DSH ≥ 0.2.0-rc.1 | ✅ 本版实测通过(2026-09-29 真机:0.2.0-rc.1 桌面版加载成功 · 10 个 team_link_* 工具在场 · team_link_status 实调返回完整状态卡) |
用 0.4.2 及以后 |
| 0.1.0-rc.6 – 0.1.7-rc.x | ⚠️ peer 区间仍声明支持(^0.1.x 两个历史分支未撤,装得上),但本版含 0.4.0 之后累积的 10 个条目、实现相对 0.4.0 有实质改动(lib/index.js + lib/client.js 合计 1188 增 / 70 删;lib/index.js 的 sha256 071f3269… → 860d51d7…),而本轮只在 0.2.0-rc.1 上实测 |
保守起见留在 0.4.0 |
低于 v0.2.0-rc.1 的使用者:请使用 0.4.0 版(见 CHANGELOG.md 的 0.4.0 — 2026-09-27 一节)。本版(0.4.2)的 peer 区间虽然保留着 ^0.1.x 分支、在 0.1.x 上仍然装得上,但 0.4.2 的实现相对 0.4.0 有实质改动,而本轮只在 0.2.0-rc.1 上实测过 ⇒ 老宿主上我们不背书。
为什么这张表是硬约束、不是建议:DSH 0.2.0-rc.1 起,加载器在挂载任何插件之前先跑一道兼容性闸门——对 bundle 的 peerDependencies 里每一个 @deepseek-ai/dsh / @deepseek-ai/dsh-* 条目做 semver.satisfies(运行时版本, 区间, { includePrerelease: true }),任一条不满足就把整个 bundle 静默丢弃(只往宿主 stderr 打一行,GUI 里毫无提示,症状就是「插件的工具凭空消失」)。机制细节见下面「依赖声明:宿主包一律走 peerDependencies」一节。
Node.js 那一行为什么是 >=22(诚实口径):这张表只写我们真跑过的范围——本插件真机只验过宿主自带的 Node v24.14.1(桌面 DSH 的运行时);18 / 20 上一次都没跑过,所以不写没验过的更低下限(承诺宽了就是假话)。要降下限:先在 Node 20 / 22 上实跑两套件全绿,再同批改这里与 package.json 的 engines(与合规档 docs/2026-10-15-store-compliance.md 的 §一 同一口径)。
方式一:本地目录 + 热装配(开发常用)
依赖通过 junction 复用 DSH 检出目录的 node_modules(免下载):
$dir = "<克隆目标目录>" # 换成你自己的路径,例如 D:\dsh-plugins\dsh-team-link
git clone https://github.com/shenhuanageshei/dsh-team-link.git $dir
cd $dir
$nm = "$(Get-Location)\node_modules"
$dsh = "<DSH checkout 路径>\node_modules" # DSH 检出目录下的 node_modules
New-Item -ItemType Directory -Force "$nm\@deepseek-ai" | Out-Null
foreach ($p in @('schemastery', '@deepseek-ai\cordis', '@deepseek-ai\dsh-session-reference', '@deepseek-ai\dsh-tools')) {
New-Item -ItemType Junction -Path "$nm\$p" -Target "$dsh\$p" | Out-Null
}
然后用 dsh-super-injector 热装配进运行中的 shell:
dev_install_package { dir: "<你的目录>/dsh-team-link", profile: "web" }
或手动装配:profile package.json 的 dependencies 写 "dsh-team-link": "link:<本目录>",dsh.profile.bundles 数组加入 "dsh-team-link",重启 shell 生效(bundle 层不会被 HMR 重载——这一点在 0.3.7 的验证里被实测确认过)。
改完代码怎么让它生效(link: 装配)
推荐接线 = link:(junction 直指工作目录):DSH 加载的就是你正在改的那份代码,改完只需重启,没有任何拷贝步骤:
// profile 的 package.json
"dependencies": { "dsh-team-link": "link:<本目录的绝对路径>" },
"dsh.profile.bundles": [ /* … */ "dsh-team-link" ]
并让 <profile>\node_modules\dsh-team-link 是指向 <本目录> 的 junction(用方式一装出来的 profile 本来就是这样)。
为什么不要用
file::pnpm 会把整包拷贝进<profile>\node_modules\.pnpm\dsh-team-link@file+...\node_modules\dsh-team-link,DSH 加载的是那份拷贝 —— 于是「改代码 → 重启」看着一切正常,跑的却是旧版(真机踩过:连重启两次都没生效,最后靠比对lib/index.js的 sha256 才揪出来)。若因故必须用file:,改完代码要先刷新那份拷贝(pnpm install,或手动覆盖)再重启。
两种接线对宿主包的要求相同:
@deepseek-ai/dsh-tools/@deepseek-ai/dsh-session-reference由宿主提供,工作目录里只要能解析schemastery即可。
方式二:npm / bundle 安装
package.json 声明了 dsh.bundle.patch(cordis.patch.yml 只插入本包自身一行),可按 DSH bundle 插件的标准流程从包管理器安装。
注意:
cordis.patch.yml有意不插入session-reference行——DSH 已自带该服务(loader id 已存在),重复插入会导致 duplicate loader entry 启动崩溃。
依赖声明:宿主包一律走 peerDependencies
@deepseek-ai/dsh-session-reference、@deepseek-ai/dsh-tools、@deepseek-ai/dsh-client-locale、@deepseek-ai/dsh-client-ui-conversation、@deepseek-ai/dsh-client-ui-workspace、@deepseek-ai/dsh-api-session-controller、@deepseek-ai/dsh-api-workspace-controller、@deepseek-ai/cordis 都由 shell 提供,因此声明为 peerDependencies(八项)——纪律一句话:凡进 dsh.client.inject 的宿主包,一律同时进 peerDependencies(dsh.client.inject 的五项逐个都在上面这份名单里;shell 的 seed 模块 dsh-client-ui-primitives 两边都不进,理由见本节末)。只有与 shell 无身份耦合的纯库 schemastery 留在 dependencies。写成 dependencies 会在全新安装时拉进第二份同一个包(版本还可能落后于 shell)。
版本区间写成 ^0.1.0-rc.6 || ^0.1.5-rc.1 || ^0.2.0-rc.1 而不是单个 ^0.1.0-rc.6:npm 的 semver 规定「预发布版本只有在区间里存在同一 major.minor.patch 的预发布比较符时才算满足」,所以 ^0.1.0-rc.6(乃至 *)都匹配不到 0.1.5-rc.1——区间写窄了会在 0.1.5 上误报 unmet peer,甚至触发自动安装第二份。
这份区间同时是「能不能装上」的开关(DSH 0.2.0-rc.1 起,2026-09-29 实测):加载器在挂载任何插件之前先跑一道兼容性闸门,对 bundle 里每一个 @deepseek-ai/dsh / @deepseek-ai/dsh-* peer 做 semver.satisfies(运行时版本, 区间, { includePrerelease: true })——任一条不满足就把整个 bundle 静默丢弃:只往宿主 stderr 打一行,GUI 里毫无提示,症状就是「插件的工具凭空消失」。运行时版本取 @deepseek-ai/dsh-app-boot/package.json 的 version(不是桌面壳自己的版本号)。桌面版自动升到 0.2.0-rc.1 当天,本插件就因为区间停在 ^0.1.x 被整个跳过(^0.1.5-rc.1 展开是 >=0.1.5-rc.1 <0.2.0,不含 0.2.0-rc.1)。⇒ 每次宿主小版本换代(0.1 → 0.2 → …)都必须复核这份区间,否则升级即静默下线。
依赖的宿主服务
| 服务 | 用途 | 缺失时 |
|---|---|---|
session-reference |
深链解析 | 必需(在 inject 数组里) |
user-questions |
批准门 / 确认对话框 | 逐目标 fail-closed |
session-query |
列表 / 导出 | 必需(在 inject 数组里) |
agents |
投递 / 活性 | 必需(在 inject 数组里) |
goals |
活性行的 goal 状态 | 降级:显示 goal=? |
sessionProjections |
资源读数(stateOf(session, key)):动态运行时上下文快照(模型可见那条)里的资源行、状态卡第 ⑧ 段与判读行、工作台摘要脚注 |
晚挂取用(ctx.get);缺席 ⇒ 三处如实标「不可读(sessionProjections 缺席)」,判读行改说「读数不可得,不作判读」 |
agent.ctx.systemPrompt(agent 作用域) |
两条并列的注册面(批 C):① section({name, order, text}) = 角色/宪章段(order 取 getSectionOrder("TEAM_POLICY")),文本在会话期内逐字节恒定;② context({name, order, text}) = 资源行的动态运行时上下文通道(name=team-link:resources、order=130,落点是尾部追加的 user 消息、不在系统提示里;批 D 起同一回合内不重算,按 sessionStats.turns 门控)。可达性(批 D):注册只对活代理、只对名册现任,所以重启后若该会话当时没活着就挂不上 —— 插件在每一步的 agent/pre-step 上做一次 O(1) 检查(名册想要它、而它还没挂上 ⇒ 就地对齐一次),把它自己补上(不新增动词、不改 attach 链) |
特征探测 + 两面独立降级:缺 section/getSectionOrder ⇒ 段零注册 + 恰一行 warn;缺 context ⇒ 上下文零注册 + 恰一行 warn;互不牵连;连服务都没有时只留一行。任一面缺失都回退到「工具返回重钉」(不抛错、不影响任何会话的提示装配)。★ 自愈链失败只留一行 warn,绝不改变该步 next() 的决定 |
settings |
策略落点(与插件自己的文件二选一,见第六节) | 晚挂取用;挂上之前状态写在 <DSH_HOME 或 ~/.dsh>/team-link/policy.json,两个落点都没有才降级为进程内记忆 + 一行 warn |
webServer |
导出下载路由 | 晚挂取用,降级为「无路由,工具照常」+ 一行 warn |
connection |
导出下载路由的平台信任栅栏(Host/Origin + 浏览器鉴权;requestRejection) |
晚挂取用,且与 webServer 成对齐备才注册路由:缺任一 ⇒ 不注册路由(绝不注册一条无门路由)+ 一行 warn(点名缺的是哪一个);导出工具照常 |
agentPresets |
§10.2.2 创建会话时的 preset 解析与挂载(每个新建会话都必须有 persona-prefix 组装源) | 创建时 ctx.get 取用;缺席 ⇒ 每个新建会话一行 warn、会话照建(缺了组成源它可能跑不起来,DEFECT-1) |
workspaceRegistry |
§10.2.2 模板的另一半:建/取工作区 + 把新建会话挂进它(侧边栏按工作区分组,没有这份归属就列不出该会话) | 创建时 ctx.get 取用;缺席 ⇒ 每个新建会话一行 warn(点名「未挂进工作区,可能不会出现在侧边栏」)、会话照建照驱动,只是不出现工作区归属(DEFECT-2) |
agentTeams |
§4.2 / §4.3 的团队形态:成员名册投影(读面)与「切到 agent-team」的能力探测(写路径)—— 只读宿主:只调 tryMembership(agent) / listMembers(agent),任何宿主写方法都不调用 |
ctx.get 取用(不在 inject 数组里,模块级 inject 恒 4 项);服务缺席 ⇒ 读面如实标注「成员名册不可读(本部署未提供该投影)—— 仍可读 Lead 与形态」并照给能力矩阵(服务在场而读不到的四条分支走中性首行 + 原因行,见团队形态一节);写路径切到 agent-team 拒绝 + 指路怎么开(通用表述:按当前活动 profile** 的 dsh.profile.bundles 自查)+ 设置 UI 兜底路 + 零写入**(切回 sessions 不探测) |
DSH 默认装配均有。
资源行为什么走
context()而不是系统提示段(批 C,2026-10-10):那行读数(窗口 / 压力 / 累计 / 轮次)每一步都在变。系统提示是请求前缀的一部分 —— 前缀里有一个字变了,后面几万到几十万 token 的历史就全部按全价重算(真机实测:glm-5.3 上命中率 0.5%–8.7%,把这一行从提示里去掉是 96%–99%)。context()的落点是尾部追加的 user 消息,变化只影响尾部、可复用前缀不受影响。⇒ 系统提示段从此只留会话期内逐字节恒定的宪章 / 义务行,资源读数一条不减,只是换了位置。诊断与设计见docs/2026-10-10-prompt-cache-design.md。
浏览器半边的模块声明(dsh.client.inject):除既有的 @deepseek-ai/dsh-client-locale 与 @deepseek-ai/dsh-client-ui-conversation,0.3.9 起另声明三项——@deepseek-ai/dsh-client-ui-workspace(会话导航 uiWorkspace.openSession)、@deepseek-ai/dsh-api-session-controller(ctx.sessions.list)、@deepseek-ai/dsh-api-workspace-controller(ctx.workspaces.list)。三者都随已发布的 web 组合恒在,且都是宿主包 ⇒ 与既有两个客户端模块同一条纪律:同时写进 dsh.client.inject(模块图)与 peerDependencies(沿用同款版本区间),防止全新安装时拉进第二份;服务本身是否可用仍逐项运行时判定:ctx.inject(["sessions", "workspaces", "uiWorkspace"], …) 齐备才注册侧栏入口,缺任一项 ⇒ 入口不注册 + 一行 warn(见 §二)。
纯 React 原子模块 @deepseek-ai/dsh-client-ui-primitives(弹窗用的官方 Modal、相对时间分桶、剪贴板写入)不写进 dsh.client.inject:它是 shell 的 seed 模块(与 react 同级由 shell 注入),官方插件如 dsh-better-sidebar 引用它时同样不声明。
十、测试
当前读数(实跑时点,一律取套件自报的那两行):node host-half.test.mjs → 1783 条(★ 批 U 新增 12 条:grep -c '^check("批 U ·' = 12,构成 1783 = 1771 + 12;同一跑的 failed: 0 / EXIT=0 读数见 docs/verification-log.md 的「2026-10-23 批 U」一节)(★ 当前无红项:Rr4(模块图无环)于 fdfc832 之后现读绿(环 = [],lib/shared.js 已无 from "./rotation.js"、lib/rotation.js 只剩单向 import ./shared.js)—— 2026-10-18 更正:旧文本的「真实 2 环 / 当前唯一红项」已作废;其余条目(同一次运行的全部 1771 条)全绿。现行值 = 1646(批 I 收口的宿主实测值)+ 20(批 K 判据)+ 8(批 L 判据 Aw1–Aw8)+ 9(批 J 判据:J-1 FR-J1 ⇒ U1–U3 · J-2 FR-J2 ⇒ U4 · J-3 FR-J3/FR-J4 ⇒ U5/U7/U8/U9 + 全量计数与文档收口 · 收尾单 ⇒ U6(反走私负向判据 + renderIdleAlert 补进批 E 消毒面清单)+ 14(批 P 判据 Pi1–Pi9:换届继任者的设置继承 —— 三个宿主探针全通过(模型对 / 审批档可写且「创建返回后设」/ 已死前任可读预设)⇒ 三项都不必降级;grep -c 'check(\批 P ·' = **14**);★ **批 34 / FR-7 的 15 条**(grep -c 'check("A-7'= **15**;台账写门落在本仓的 **A-7** 腿,构成1727 = 1697 + 15 + 15,**跨仓交付**;权威规格 = 套件仓 dsh-thincoder-suite的docs/2026-10-14-anti-drift-design.md §5 FR-7 / §8 腿 A-7 / §14 L-9));★ **批 R 的 15 条**(grep -c '^check(批 R · Rr' = 15**:Rr0①–④(FR-R0 测试档就绪)+ Rr3①–④ / Rr5 / Rr6(R-1 首批判据)+ Rr1 / Rr2 / Rr4 / Rr7 / Rr8(收口单);判据面 = 单档字节 · 入口与对外面逐字不变 · 模块图无环 · 合规档数字与实测一致 · 行为零变更;构成 1727 = 1697 + 15(FR-7)+ 15(批 R));★ 批 Q 的 11 条(grep -c '^check("批 Q · Qr' = 11:Qr1–Qr11,换届的「撤销依据」按换届各存一份 —— 两次 prepare 互不覆盖 / 旧档无损折叠 / claim 取对那份 / 镜像与 roster get 列全部在飞条目 / 收尾只删自己 / 零顶层键与零新依赖 / 不回归 / Pi7④ 审批写面抛错分支 / session 在场守卫 / 升级窗口端到端 / 缺那份具名拒绝;构成 1738 = 1727 + 11);★ 批 S 的 20 条(grep -c '^check("批 S · Sr' = 20:Sr1–Sr20(首交付 15 + 分歧修复轮 5),整队换届(批量准备 + 各自认领)—— 一次写盘 · 一条 freeze · 2 小时令牌 · 协调者排最后 · 批次互斥 · 写前重读 · 限速按角色 · batchId 三面存活 · 显式继任者在场 · 执行期失败回执点名失败角色自己的已建会话 · pending 时读面分组 · auto 投递与意图先增后删 · 3 分钟超时档;构成 1758 = 1738 + 20);★ 批 T 的 13 条(grep -c '^check("批 T · Tr' = 13:Tr1–Tr13,黑板归档 / 回收 —— 阈值只提示(零写盘)/ 显式归档逐字搬、留尾部与指针 / 归档文件只写一次 / 序号与任务号不回卷 / 归档头与零新增顶层键 / 对账与交接扫描不相交 / 既有判据族不回归 / 边界与无确认服务 fail-closed / 最新形态史行钉住 / 归档 id 仍认 / 伪造高号仍拒 / 崩溃幂等 / 归档索引常驻读面;构成 1771 = 1758 + 13);grep -c '^check("批 J ·' = 9;本单实测(本地 Node 桥)ALL PASS / assertion total: 1683 (failed: 0);收尾单的红相实测 1683 (failed: 1)(只红 U6));批 K 的宿主回执已实测回填(2026-10-13,父侧实跑 + 交付评审处置轮实跑):ALL PASS / assertion total: 1666 (failed: 0)。★ 沿革:本行原写 1598(批 G 收口实测)→ 1619(批 H 收口实测)→ 1646(批 I,宿主实测),逐次构成见下面各批段落);node client-half.test.mjs → 310(failed: 0)。2026-10-13 更新(批 K:崩溃现场留证 / 心跳):新增 20 条宿主判据(host-half.test.mjs 的「批 K」块,标识符统一 Fk 中缀),判据 U1–U15 逐条落地(U1 首拍在场 · U2 周期心跳(注入时钟跑两轮巡逻 ⇒ seq 递增、不新开目录)· U3 原子写(零 .tmp 残渣、随时可解析)· U4 死亡窗口双向(预置旧目录 ⇒ 窗口 = [上次 lastBeatAt, 本次 startedAt];无旧目录 ⇒ 如实说「无(首次运行)」)· U5 保留上限(12 份旧目录 ⇒ 恰剩 10、删最旧、本次永不删)· U6 只读合同(settings 与磁盘递归快照逐字节不变)· U7 输出面(不带 action 逐字不变 + 闭集外取值如实拒绝;带 action=forensics 含窗口与四类读数、不含「在飞作业」)· U8 失败方向(目标不可写 ⇒ 巡逻不抛、只留一行 warn、其余读面照常 + 负相自证)· U9 零新依赖(import 仍 8 条)与 policy.json 仍十键 · U10 磁盘诚实路径(statfs 拒 ⇒ 两格恰为 null)· U11 免责句(「非正常退出不单独归因」+「正常退出与崩溃同形」+ 指路由人结合宿主 crash.log)· U12①② 零注册兜底双向(watchdogs === [] ⇒ 兜底在场且仍周期更新;出现注册 ⇒ 兜底退场、不双写)· U13 写面只落在自家根下(变更路径全在 forensics/ 之下 + 源码对宿主那个目录名零命中)· U14 兜底双向②「中途归零必须重挂」(最后一条注册被删掉 / 被巡逻过期清扫收掉 ⇒ 兜底重新挂上、心跳仍周期更新、再触发一次也不产生第二个定时器;过期那一档钉住巡逻末尾那处重挂)· U15 空串 action 的口径(参数描述逐字含「空串视为省略」∧ "" 走省略支 ∧ 闭集外的 "nope" 仍如实拒绝 ⇒ 描述与实现逐字一致才绿)),另改写 2 处既有判据(都是「同一份清单两处写、只改一处」那类同改锁:U6 只写一处 与 U9 迁移可核 都把「<home>/team-link 下条目数」改成点名条目集 —— 批 K 的心跳根按设计(D1)与 policy.json 同级,那个目录从此必然存在)⇒ 宿主套件 1666 = 1646 + 20(那是批 K 收口时的读数;批 L 之后的现行值见本行开头)。2026-10-13 更新(批 L:名册文件的并发写保护):本批新增 8 条宿主判据(host-half.test.mjs 的「批 L」块,标识符统一 Aw 中缀),判据 Aw1–Aw8 逐条落地(Aw1 唯一临时名(正向:含 process.pid + 一次哈希调用,且函数体内不含那个固定名字面量)· Aw2 固定名不在场(全文件作用域的反向锁 —— 与 Aw1 第三子句互为冗余锁、范围不同)· Aw3 重复写不撕裂(连续两次 policy.update ⇒ 文件恰等于第二次那一份;Promise.all 双写者并发 ⇒ 仍是一份完整文档、零 .tmp-* 残留、且两次写都算落盘成功(落点行不翻 ⚠);等价新进程读回 ⇒ 十键齐、不判损坏)· Aw4 rename 失败有重试(经接缝注入「前两次抛、第 3 次放行」⇒ 文件最终恰为本次写入、恰 2 次注入失败、留痕恰一行且三要素齐)· Aw5 重试耗尽后的失败方向(落点行降级 ⚠ … 文件不可写 + 内存态照常可读 + 目标文件逐字节未动 + 零 .tmp-* 残留 + 留痕里读得到原始 errno 与注入时的原句 + 两道门各恰一行 + 接缝已还原自证)· Aw6 零 schema / 零形状变更(policy 仍十键、schema 仍 1)· Aw7 零新依赖(顶层 import 仍恰 8 条、说明符逐字是白名单;★ 本批一个新名字都没加:随机后缀用既有的 createHash、残留清理用既有的 rm)· Aw8 负向锁(writePolicyFileAtomic 必经 renameThroughSeam、不含直接 rename / 直接写目标,且接缝体内恰一句)),另改写 2 处既有判据(都是跟着新名字口径走、语义一字未动:U2 原子写 的遗留临时文件改成 .tmp-* 形态且只认「别人的那一份残留」,🟡#1 夹具的「回写在飞」守卫改成按 .tmp- 前缀扫目录)⇒ 宿主套件 1674 = 1666 + 8。★ 两次实测读数(修复前必红 / 修复后全绿)由宿主执行 —— 交付方无 shell(本批未执行任何命令);红相配方逐条写在 host-half.test.mjs 的「批 L」块头注里(就地变异 → 跑 → 逐字节还原 + sha256 自证),宿主回执待回填。★ (以下这一句属批 K 那一轮)交付评审处置轮(同日,只修偏离、不新增设计):判据 18 → 20(补 U14/U15),并把兜底心跳从单向改成双向(最后一条注册消失 ⇒ 重新挂上 —— 否则只是把「静默冻结」从启动时换个入口)、把空串 action 的口径定案为「空串视为省略」并逐字写进参数描述(U15:判据同时咬住描述与实现两侧)、把 bootId 目录形态正则收紧到与生成端严格互为镜像;红相读数见 CHANGELOG.md 的同批条目。2026-10-12 更新(批 I:屏蔽的作用域):本批新增 27 条宿主判据(host-half.test.mjs 的「批 I」块,标识符统一 Bi 中缀),判据 U1–U20 逐条落地(★ 2026-10-12 追加项 D3:在原 26 条之外补 U20「by ∉ {a,b} 的条目归一化丢弃」1 条 ⇒ 26 → 27 条),另改写 9 处既有判据(都是「同一份清单两处写、只改一处」那类同改锁,逐条见下)⇒ 宿主套件 1646 = 1619 + 27。判据逐条:U1 blocks 三点齐(声明面 / 写入面 / 旧档归一化补 [],3 条)· U2 写入配对化(规范化形状 + 旧名单一字未变)· U13 写入必记 by · U3 幂等 + 规范化(正向 + 反向各写一遍仍只一条、零新增写)· U4 ★ 只挡那一对(B 拒 A / A 拒 B / C→B 与 B→C 照常)· U5 旧格式仍挡 + 标注「范围偏大」+ 不挡住别的发送方 · U6①③ 解除真的解开(判定两向都放行 + 端到端重新投得出去)与再屏蔽一次又能挡上 · U7 解不存在的 ⇒ 拒绝 + 零写入 + 列出当前全部屏蔽 · U11/U18② 逐条清只清那一条(其余旧记录与其它九个键逐字节不变、恰一笔单键补丁)· U12/U18③ 旧名单里没有 ⇒ 拒绝 + 零写入 + 列出旧名单 · U14①②③ 解除权限三态(by 那方能解 / 被屏蔽方拒 / 第三方拒)· U15 list 分组呈现(配对组在前、旧格式组标注范围偏大,各带指引)+空集负相 · U16 单条畸形条目丢弃、其余原样保留、不带进判定 · U17 import 面仍 8 条 · U18① 受益方不能自清 · U19 三个写路径各恰一笔 policy.update、补丁单键、无直接文件写 · U8 两处拒绝文案点名「谁挡的 + 怎么解」(对话框那一处 + 收件检查的配对/旧格式两分支)· U9 文档同步(工具 description / 本文件 / CHANGELOG.md / docs/backlog.md 的 B-2 ✅ 且 B-3 仍在册)· U10 不回归(屏蔽仍优先于配对快路径、两道门的提问顺序与 id 逐字未动)。夹具纪律:一律用既有工厂 setup(与 teamEnv 同一个),不依赖墙钟。既有断言同批改写 9 处(改写不是删除,逐条留证):① §11.9 文档面=实现面(工具计数同改锁) 的硬常量 registeredTools.length === 10 → 11(新增第 11 个工具,漏它当场红);② U5(fan-out 的屏蔽行)的期望串只钉到「…已屏蔽来自当前会话的消息」为止(FR-I5 在它后面接了「旧格式 / 范围偏大 / 清除指引」,旧期望串带句号 ⇒ 改写的对象是判据的期望值,行为语义不变);③–⑧ 六处键集同改锁(U29 schema / U9 对照 (pendingCreates) 的折叠键集 / U19 schema 的 U19_POLICY_KEYS 与声明面计数 / U1 文件后端 的文件 schema 键集 / U11 schema / U17 键集与 README 同批)各加第十项 blocks,并把「八/九项」的计数与 README 的「十项」同批改准;⑨ 批 E · U2 ①(工具全覆盖)的 BE_TOOLS 清单与它的两组计数 10 → 11 —— 这处不改不会红,但不改就是「全部 10 个工具都过消毒收口点」这句覆盖面假声明(第 11 个工具在册却不在覆盖面内)。★ 设计档 FR-I6 只点名了 ① 与 README 架构图行,③–⑧ 与 ⑨ 是实施时发现的同类锁,一并改准,见账本本批一节。客户端半边一字未改(仍 310)。2026-10-12 更新(批 H:删团队 team_link_roster action=delete):本批新增 19 条宿主判据(host-half.test.mjs 的「批 H」块,标识符统一 Dh 中缀)+ 改写 1 条既有判据(B/宣传面=实现面 那条锁的是 roster 的共享 action 闭集,期望串末位加第六个动词 delete —— 闭集锁必须跟着枚举走,不是 set-mode 的语义变更)⇒ 宿主套件 1617 = 1598 + 19(期望值取自源码逐条计数 grep -c '^check("批 H ·';按纪律本批交付方未执行任何命令,宿主实测回执待回填)★ 同一 grep 在分歧修复轮后的现读数 = 21 ⇒ 现行期望值 1619 = 1617 + 2(D1 的 U12①②,见本段末)。判据逐条:U1 枚举 + 描述面 · U2 删除生效 + 恰一笔写 · U3 逐字段不误伤 + 版本史条数如实 · U4 幂等拒绝 + 列出已知团队名 · U5 确认门三态(无服务 / 取消 / 超时,各零写入)· U6 黑板逐字节 + mtime 不变 · U7 文档同步 · U8 其它动作的 gate 逐字不动(现任在册 + 空缺两半)· U9 ★ US-H2(非现任 / vacant 也删得掉)· U10 写盘抛错 ⇒ 行仍在 + 如实报错 · U11 黑板目录不存在也如实说;另 NF-H5 零新依赖。客户端半边一字未改(仍 310)。2026-10-12 分歧修复轮(批 H,实现零改动):只读审计给出 1 条恒假红 + 4 条分歧,逐条修判据 / 夹具 / 文档 —— D0 🔴:U3 的 ④ 恒假(夹具工厂对每一行都种同一个 legacy 标记 dh-old-1,而 U3 的「保留队」也走同一工厂 ⇒ 断言要求「保留下来的行里不许出现夹具刚种进去的标记」,不可能绿)⇒ 工厂形参加 legacy 默认值(默认值逐字不动)+ 保留队传自己的标记;D1 🟡:「写前重读」此前零判据 ⇒ 补 2 条 TOCTOU 夹具 U12①②(确认期间目标行被并发删 ⇒ 幂等拒绝+零写入;确认期间别的行被并发改 ⇒ 那笔改动不被陈旧快照回写抹掉);D2 🔵:证据账本 §六 的判据行号 5 处漂移 ⇒ 按符号名复核重取;D3 🔵:U3 逐字节比对纳入 blockedSenders(+ receiveMode / pendingCreates / sections,且种成非空避免空真);D4 🔵:本节首行读数 1598 → 1617(1598 保留为上一批 as-of 说明)。⇒ 批 H 判据 19 → 21、宿主套件 1617 → 1619。★ 评审 🟡#1 更正:本行原写 1594(那是批 G 分歧修复前的基线,已由宿主回执取代)。2026-10-12 更新(批 G:自动工作台注册的续期):本批新增 24 条宿主判据(host-half.test.mjs 的「批 G」块,标识符统一 Rg 中缀)⇒ 宿主套件 1594 = 1570 + 24;分歧修复轮只改这 24 条自身(既有断言一字未动),修掉 9 条分歧:D1 生产缺陷(FR-R3 引用了作用域外的 watchdog 绑定 ⇒ 每轮巡逻都抛 ReferenceError 被空操作 logger 吞掉,功能等于没做)· D2 8 个夹具用错工厂(setup 不挂 env.watchdog)· D3 夹具时钟面(注入时钟锚真墙钟)· D4 去抖窗口未跨过 · D5 三条判据与本批有意语义相抵 · D6 第 2 趟 /team_session 幂等早退 ⇒ refresh 分支无人验证。★ 同一轮更正一处与事实相反的措辞:批 G 的夹具不是「不依赖墙钟」,而是注入时钟锚真墙钟(const RG_NOW = Date.now())—— 因为这些行的第一个写者是 attach 后置链的 ensureTeamAutoWatchdogs(它不注入 now),而 rearm() 的过期判定也走真实 Date.now();把行种在假时钟上会被 attach 先刷新一遍,判据当场失真。★ 真机验证(2026-10-12 09:43,重启后):昨夜 00:28 到期的 10 条 auto 注册全部被自动救回 —— 其中 9 条重建(新 id,到期 = 本次 attach + 12h)、1 条还活着的原样未动(id 未变 ⇒「缺失才写」幂等成立);team_link_watch action=list 与状态卡第 ③ 段都出现了「上次续期(从未续期)」那一格 ⇒ FR-R2(冷启动复活)与 FR-R4(留痕可核)在真机上证实。★ 口径如实:批 F 的 1570 = 1550 + 20 先由源码逐条计数(check( 调用点)给出期望值(本批交付方按纪律未执行任何命令),随后由宿主实测回填 —— ALL PASS / assertion total: 1570 (failed: 0)(退出码 0),期望值与实测一致;输出原文见 docs/verification-log.md 的批 F 第五节。2026-10-11 更新(批 F:team_link_team_append 的长文本压力):本批新增 20 条宿主判据(12 条 FR-1/FR-2/FR-3/§8-U7:上限常量=2000 · 恰 2000 收 / 2001 拒且盘上零写 · 639 码点历史失败样本被接受且逐字读回 · 描述面与 README 不含「500」含「2000」 · 原名锚仍可 grep 到 · 零污染对照;8 条 FR-4/U5:显示截断两处共用、按码点、只影响显示),并改写 7 处既有判据(7 处钉上限的断言 500 → 2000:M2 块的 decisions / discipline 三条 + §9 U4 两条 + §9 U14 两条字节相同的期望串;另加 1 处是批 E · U1 ⑦ 提到 500 的措辞——它的夹具本来就按上限现造,自动改瞄 2000),条数不变,原名锚逐字留在注释里。⇒ 宿主套件 1570 = 1550 + 20。2026-10-11 更新(批 E:框定 token 消毒面):本批新增 37 条宿主判据(33 条交付面 + 交付评审修复轮的 4 条:上界复检两档 + scrub 拒写两条),⇒ 宿主套件 1550 = 1513 + 37(宿主实测 ALL PASS / assertion total: 1550 (failed: 0)、退出码 0)。2026-10-11 更新(批 D:注册生命周期与自动提示):本批新增 16 条宿主判据(15 条批 D + 1 条改写的 批 C · U4 资源行四项 ← 三个 key(批 D §5.2 第 1 行改写 · 回合门),原名 批 C · U4 改写;同时改写 4 组既有判据、条数不变),⇒ 宿主套件 1513 = 1494 + 19(交付轮 +16;分歧修复轮 +3)。★ 口径如实(宿主回填):交付轮源码逐条计数给出的期望值是 1494 + 16 = 1510,宿主实测为 1510 (failed: 2) —— 2 条红(clear 的拒绝回执缺「未清除」三个字)经分歧修复轮修掉、并新增 3 条判据(U5 扩为三条)后,回填实测读数=assertion total: 1513 (failed: 0)(退出码 0);client 半边 310 (failed: 0)。命令与输出原文见 docs/verification-log.md 的「2026-10-11 批 D」一节。上一条 2026-10-10 更新(批 C 分歧修复轮):本轮修掉批 C 交付的两处判据缺口(U5 夹具静默丢参 ⇒ 那条恒红;§6.1 段面渲染体抛错的留痕判据被批 C 删掉)并补上 U6 第 ⑤ 支(两面都缺 ⇒ 两面各恰一行),宿主套件从 1491(failed: 1) 变 1494(failed: 0) = 1491 + 3(新增 3 条判据:U5 夹具前置对照 + §6.1 段面 onError 留痕 + U6 ⑤ 两面都缺;两个新正相 + 1 条对照 —— 详见本档的「2026-10-10 批 C 分歧审计修复轮」一节)。上一版的 1442 是 0.5.0 发版时点的旧读数(0.5.1 → 1466、0.5.2 → 1477、批 C 交付 1491),三个数现在统一为这一个。下面那段 1324 = 1314 + 10 是 2026-09-30 当时的构成说明,保留为历史:1324 = 1314 + 10:2026-09-30 DEFECT-5(revive 复活出来的 worker 第一回合必死 —— prompt variable "{{model}}" has no value;resume 面漏了宿主自己那条 resume 的三样:resumeSessionId + agentOptions + setup)新增 10 条宿主断言(含同批两轮代码评审修复轮补的 DEFECT-5 ⑤b、DEFECT-5 ⑦ 口径 与 组成面(mount 缺失·形状断层))+ 改写 1 条(U26 revive 原来的 Object.keys(resumeCalls[0]).length === 1 就是缺陷本身,改钉那三个键);红相=git checkout -- lib/index.js(夹具不动)⇒ host 1324 (failed: 11);绿相 host 1324 (failed: 0) / client 310 (failed: 0);原始读数见 docs/verification-log.md 的「2026-09-30 DEFECT-5」一节。)(1314 = 1310 + 4:2026-09-29 preset 租约补丁(同批代码评审发现:acquireScope 返回的是要释放的租约,原修复把返回值丢掉 ⇒ 每建一个会话泄一个)新增 4 条宿主断言(租约必须释放 / 失败路径也还 / 旧代不伪造释放 / 句柄幂等 ★ 控制项);红相=摘掉调用方的释放 ⇒ host 1314 (failed: 2)(另 2 条在两种状态下都该绿,如实记、不冒充 4/4);绿相 host 1314 (failed: 0);原始读数见 docs/verification-log.md 的「2026-09-29 preset 租约释放」一节。)(310 = 305 + 5:同日 客户端团队面板可读性重设计(owner 截图:「超长文字弹窗超长,而且文字太密集太多根本不知道应该看哪里」)新增 5 条客户端断言(高度上限 / 段标题字号严格大于正文 / 默认全折叠 / 点标题即展开且展开前后 panelLines 逐字相同 / 缺数段不折叠);红相 client 310 (failed: 5) —— 五条全红且套件跑完不崩,绿相 client 310 (failed: 0);本地视觉夹具读数 941px → 406px;原始读数见 docs/verification-log.md 的「2026-09-29 客户端团队面板可读性重设计」一节。)(1310 = 1306 + 4:2026-09-29 DSH 0.2.0-rc.1 preset pin 换代修复轮(standingKeyFor → acquireScope,真机 /team_session 两次失败的同日第二处换代断层)新增 4 条宿主断言(现名成立 / 两名都在只走现名 / 旧名回退成立 / 双缺显式点名断层);红相=夹具先行、实现未动时套件以真机同款文案崩(可见逐字相同的 创建失败(agentPresets.standingKeyFor is not a function),多条 FAIL 后中止于 U16 端到端,套件中止故无完整 failed 计数、如实登记);绿相 host 1310 (failed: 0) / client 305 (failed: 0);原始读数见 docs/verification-log.md 的「2026-09-29 DSH 0.2.0-rc.1 preset pin 换代」一节。)(1300 = 1264 + 35 + 1 / 299 = 269 + 30:2026-09-27 C 批(客户端团队面板 + 形态可视化 + 🔵 登记 4 条 / 本批执行 3 条,① 已在上一批修完)新增 65 条断言,另改写既有期望值 4 条(host 3 条路由计数断言、client 1 条段落类序断言 —— 新增一条路由 / 一个分区逼出来的,断言照旧严格);红相读数分三段:C1 host 1290 (failed: 23) / client 275 (failed: 4,四条命名守卫=覆盖缺口类,未执行正相断言)、C2 host 1295 (failed: 4) / client 299 (failed: 1)、C3 host 1299 (failed: 3);原始读数见 docs/verification-log.md 末节。)(1203 = 1193 + 10:2026-09-27 真机冒烟修复轮 L1/L2 新增 10 条宿主断言;该轮聚合红相 1203 (failed: 17) = 7 条新断言真红 + 10 条改写期望值;另 3 条覆盖缺口类如实标 ★负相 —— 原始读数见 docs/verification-log.md 的「真机冒烟修复轮」一节。)(1262 = 1255 + 7:2026-09-27 代码评审第 2 轮新增 7 条宿主断言,聚合红相 1262 (failed: 4) = 4 条新断言真红 + 3 条控制项 ★ 负相本就绿;原始读数见同档的「代码评审第 2 轮」一节。)(1264 = 1262 + 2:2026-09-27 代码评审第 3 轮新增 2 条宿主断言,聚合红相 1264 (failed: 2) = 2 条新断言真红(控制条件并入同条断言,无单独负相项);原始读数见同档的「代码评审第 3 轮」一节。)(1300 = 1299 + 1:2026-09-27 C 批分歧审计修复轮(🔵#2/#3/#4/#5)新增 1 条宿主断言 —— 🔵#5 的 null 判据:红相 1300 (failed: 1)(行为半在本轮修复前已成立 ⇒ ★ 负相不冒充;真红的是判据面与「行为修正」标记两半)、绿相 host 1300 (failed: 0) / client 299 (failed: 0);原始读数见同档的「C 批分歧审计修复轮」一节。)(1306 = 1300 + 6 / 305 = 299 + 6:2026-09-27 C 批代码评审修复轮(2 🟡 + 4 🔵)新增 12 条断言(host 6 / client 6)—— 🟡#1 面板类名↔样式规则 2 条 · 🟡#2 翻页不回卷 3 条 · 🔵#3 降级句随页 1 条 · 🔵#4 派生行团队归属 2 条 · 🔵#5 wellFormed 2 条 · 🔵#6 React key 完备 2 条;红相 host 1306 (failed: 5) / client 305 (failed: 4)(实测红 9 条;另 2 条为控制/对照项 ★ 负相本就绿、1 条为实施后补强未取得红相读数,均如实标注),绿相 host 1306 (failed: 0) / client 305 (failed: 0);原始读数见同档的「C 批代码评审修复轮」一节。)(2026-10-13 更新(批 J:协调者的派工信号加固):本批分三个小单、共新增 8 条宿主判据(grep -c '^check("批 J ·' = 8;标识符统一 Cj 中缀)—— J-1 FR-J1 ⇒ U1/U2/U3(「有活没派」触发条件 + 角色门 · 自己的去抖与窗口外复投与每段限量 3 次 · 与既有摘要互不抑制)· J-2 FR-J2 ⇒ U4(协调者自身压力告警:具名阈值 50 · 同一档不重复 · 上升沿 · 读不到投影就不说)· J-3 FR-J3/FR-J4 ⇒ U5(事实源指路在场:宪章第 3 条 + 摘要脚注)· U7(不回归:失联三态仍恰三态 · 摘要四组正文逐行 === 未动 · 行数恰 6 = 表头 + 四组 + 脚注一行)· U8(零新依赖:顶层 import 仍 8 条 · 注册出去的命名空间仍十键)· U9(工具 description ↔ README 摘要段的指路句逐字一致)。★ 两次实测读数(本单,本地 Node 桥执行 node host-half.test.mjs):修复前必红 —— 把 FR-J3 两处指路句就地去掉 ⇒ assertion total: 1682 (failed: 1)(U5 红)· 只改 README 那一份的一个词 ⇒ 1682 (failed: 1)(U9 红)· 顶层加第 9 条 import ⇒ 1682 (failed: 6)(U8 红 + 同源的 U19 / 批 H NF-H5 / 批 I U17 / 批 K U9 / 批 L Aw7 一并红)· TICKABLE_VERDICTS 多塞一态 ⇒ 1682 (failed: 1)(U7 红);四次变异后都逐字节还原并以 sha256 自证。修复后全绿:ALL PASS / assertion total: 1682 (failed: 0);客户端半边一字未改**(仍 310 (failed: 0))。)
§10.2.8.10 之后的一条用法变化(值得先知道):
/team_session结算后会给调用方会话投一条短回执(成功 / 失败 / 取消三态都算,一行、≤120 码点)。收益是在空会话里发命令也能把那个会话从侧边栏「显出来」——此前它没有回合 ⇒ 隐身;无任务建出的 worker 也不再隐身(改投一具「最小唤醒」待命通知)。代价如实:每条命令多一个调用方模型回合(连取消也算一个),回执是真实消息,调用方上下文会永久增长一条 user + 一条 assistant。
node host-half.test.mjs # 宿主半边:工具面 / 策略 / 换届 / 恢复 / 红线锁
node client-half.test.mjs # 浏览器半边:卡片渲染 / 降级路径 / 槽位注册
两条都是自报计数的脚本(结尾打印 assertion total: N (failed: 0) 并以退出码表态);不要用 node --test(沙箱下 spawn EPERM)。
逐轮的 红相/绿相 证据账本已移到
docs/verification-log.md——0.3.1 → 0.4.0 每一轮的审计变异、修复前必红读数,以及本仓库的「变异验证」验收文化。本节的定位是面向读者:怎么跑、现在多少条、红线是什么。
红线由哪些断言把守(宿主套件内的源码级与运行时锁):模块级 inject 恒 4 项;投递 source 恰三成员;不引入任何新的会话日志事件类型;PolicyConfig 的键集;writerGate 函数体逐字节不变。
设计文档索引
| 文档 | 内容 |
|---|---|
docs/team-upgrade-design-2026-09-17.md |
实施级设计(v1.4):M1–M5 机制、伪代码与 schema、安全边界与红线、验收标准(U1–U11 + 集成演练)、§9 收尾修复设计、会诊 #27 与清单闭合台账 |
docs/collab-enhancements-design-2026-09-19.md |
协作增强设计:§10 ① 发送方可见性 A+D(已实施,U13–U15 见上)/ ② /team_session 自动建队(已实现,U16–U19 见上;2026-09-21 真机缺陷修订见其 §10.2.8 —— 输入文法 + 失败可见性 + 确认框边界三条,判据 U30–U34,已落码(2026-09-22,commit ee7c48a))/ §11 ③a 自动换届主路径(已实现,U20–U24 / U28 见上)/ §11.9 ③b 团队恢复工具 team_link_recover(已实现,0.3.8 随 ③ 一批落地;0.3.9 批次 2 又把 revive 的角色面放开到任意角色、给 reappoint 加了常驻的「自建继任者」候选)。发布与验证状态:①(发送方可见性)0.3.8 已发布并经真机验证(§12.1);② ③ 的宿主半边要等 DSH 重启窗口;H1/H3/H4 的结论见该档 §12。会诊 #37 纪要见 docs/consult-minutes/2026-09-19-consult-37-minutes.md,会诊 #43(§11.9 的裁定)见 docs/consult-minutes/2026-09-20-consult-43-minutes.md |
docs/hardening-and-recovery-design-2026-09-21.md |
加固与恢复设计(v1,0.3.9):① 导出路由接入平台信任栅栏 · ② 恢复能力加宽(revive 角色面 / reappoint 自建继任者)· ③ 侧栏「会话工具」入口(§4.3 是本入口 UI/交互的唯一事实源)· ④ 深链聚焦修复(§4.4);含 §5 红线 B1–B9、§6 判据 U1–U14、§9 假设与待验项、§11 分批。会诊 #62 纪要见 docs/consult-minutes/2026-09-21-consult-62-minutes.md |
docs/client-panel-batch-design-2026-09-27.md |
C 批设计:客户端团队面板(六段只读 + 落点行)· 形态可视化(Lead/成员/多会话徽标)· 🔵 收尾(登记 4 条 / 本批执行 3 条,① 已在上一批修完);判据 W1–W9(§3.0 的跨半边取数通道落地为只读路由 GET /team-link/panel,与导出路由同一道栅栏 —— 新增判据 W9:面板读路径零写) |
docs/policy-persistence-design-2026-09-27.md |
策略持久化自持化(设计):三档落点 → 四档、原子写、晚挂折叠、可核落点行;判据 U1–U8 |
docs/2026-10-10-prompt-cache-design.md |
批 C 设计(提示缓存保真):把「每步都在变的资源读数」从系统提示段搬到宿主的动态运行时上下文通道(systemPrompt.context(),team-link:resources / order=130)—— 落点搬家、功能一条不减;含 §6.2 的 as-of 改写注记(批 D 把该处的「不缓存」改写为「允许一份只用于门控的进程内缓存」);判据 U1–U11 |
docs/2026-10-10-registration-lifecycle-design.md |
批 D 设计(注册生命周期与自动提示):FR-1 自愈可达(agent/pre-step 上的 O(1) 检查,重启后打开团队会话即自动补挂)· FR-2 clear 只清手工(auto 工作台注册拿不走 + 脚注口径同批改准)· FR-3 回合门(资源读数按 sessionStats.turns 门控,用户拍板 A)· §4.3 的进程内状态表 · §5.2 既有断言改写清单;判据 U1/U1b/U2–U9(U9 为真机项,属父侧) |
docs/2026-10-11-token-sanitize-design.md |
批 E 设计(框定 token 消毒面,v1.1):一个消毒函数 + 挂既有收口点(FR-1…FR-7);§6.5 v1.1 的处数由文本自带(占位符带编号 —— 因导入面白名单这条硬约束,不新增 import、不加累加器);判据 U1–U11(U9 为真机项,属父侧)。配对需求档 docs/2026-10-11-token-sanitize-requirements.md |
docs/2026-10-12-watchdog-renewal-design.md |
批 G 设计(自动工作台注册的续期):把 planAutoWatchdog 里已写好却跑不到的 refresh 语义接到每条注册自己的巡逻定时器上(FR-R1 续期 + 名册门 + rearm())· FR-R2 冷启动(ensure 的 existing 查找加「未过期」谓词)· FR-R3 运行期复活(巡逻尾部去抖周期 ensure,时钟可注入)· FR-R4 renewedAt 留痕与归一化白名单 · FR-R5 文档与既有断言迁移;判据 U1–U12。配对需求档 docs/2026-10-12-watchdog-renewal-requirements.md,决策来源 docs/consult-minutes/2026-10-12-consult-43-minutes.md |
docs/2026-10-12-team-delete-design.md |
批 H 设计(删团队):team_link_roster action=delete —— 把一行从名册里真正删掉(连同它自己的版本史);豁免写权限门(已退役的队没有现任协调者,不豁免就谁都删不掉)并把关交给人类确认框(无服务 / 取消 / 超时 ⇒ 零写入);写前重读名册(确认窗口内别人的 set-role / upsert-team / 删别的队不会被静默回退);不碰黑板文件、不动信任数据;判据 U1–U11。配对需求档 docs/2026-10-12-team-delete-requirements.md |
docs/2026-10-12-block-scope-design.md |
批 I 设计(屏蔽的作用域):把屏蔽从「部署级全局名单」改成「两个会话之间的一对」(双向,条目 {a, b, by, since}、字典序规范化、写入必记 by)· 旧格式 blockedSenders 原样保留、不再新增,检查时并集(不静默放开)· 新工具 team_link_block(list 分组 / unblock 只认 by / clear-legacy 按发送方逐条、受益方不得自清)· 两处拒绝文案点名「谁挡的 + 怎么解」;决策 D1–D8、判据 U1–U20(U20 = 用户 2026-10-12 追加项 D3)。配对需求档 docs/2026-10-12-block-scope-requirements.md |
docs/2026-10-13-crash-forensics-design.md |
批 K 设计(崩溃现场留证 / 心跳):DSH 非正常退出时进程没机会写任何东西 ⇒ 改成「活着时持续写」—— 每次启动开一个目录 <DSH_HOME>/team-link/forensics/<bootId>/,原子覆盖一份定长小 JSON heartbeat.json(内存 / 磁盘 / 活会话 / 看门狗,在飞作业数不做);按心跳内嵌 lastBeatAt 保留最近 10 份、永不删本次;零看门狗注册时另起 unref() 兜底定时器(FR-K5 双向:零注册时另起 · 中途归零时重挂 —— 否则心跳静默冻结在最后一次心跳那一拍);下次启动读上一份 ⇒ 死亡窗口;读面 = team_link_status action=forensics(只读,不带该参数时输出逐字不变);只写自家、宿主那半个仪表一概不读;判据 U1–U15。配对需求档 docs/2026-10-13-crash-forensics-requirements.md |
docs/2026-10-13-policy-atomic-write-design.md |
批 L 设计(名册文件的并发写保护):多个进程同时写同一份 policy.json 时,旧的固定临时名互相覆盖 ⇒ 尾部垃圾、整份解析失败(真机事故:会话看到「名册丢了」,数据其实没丢)。修法 = 唯一临时名(.tmp-<pid>-<8hex>)+ 可注入的改名接缝 + 有界重试 + 失败时留痕三要素(路径 / errno / 写入者);判据 Aw1–Aw8。配对需求档 docs/2026-10-13-policy-atomic-write-requirements.md |
docs/2026-10-13-coordinator-signal-design.md |
批 J 设计(协调者的派工信号加固):把「有活没人派」做成独立告警(只发现任协调者 · 窗口去抖 · 每段最多 3 次 · 条件转假即重置)+ 协调者自身上下文 ≥50% 时的告警(上升沿才再打扰)+ 在守则与摘要脚注里写死事实源(要看队员在干什么 ⇒ 读黑板 tasks.md,不要读它的会话历史);判据 U1–U9(含 U6 反走私负向判据)。配对需求档 docs/2026-10-13-coordinator-signal-requirements.md |
docs/2026-10-14-rotation-inheritance-design.md |
批 P 设计(换届继任者的设置继承):换届建出来的新会话自带前任的预设与模型(可写时含审批档)—— 新增继承读取器 + 两处调用点接线(只有三项进 plan)+ 创建返回之后设审批档(共用函数一字未改)+ 三处逐项如实可见(确认框 / 回执 / 交接文档:值是什么、没继承的原因);三个宿主探针(只读翻 app.asar)证实三项都不必降级;判据 Pi1–Pi9(14 条 check())。配对需求档 docs/2026-10-14-rotation-inheritance-requirements.md |
docs/2026-10-14-rotation-backup-per-rotation-design.md |
批 Q 设计(换届的撤销依据按换届各存一份):撤销依据原是团队级单槽、prepare 整队覆写 ⇒ 换第二个角色就抹掉第一个的回滚依据(今天已在发生,整队换届的前置)。修法 = 团队行内数组形 rotationBackups(每条 {rid, at, backup})+ 旧槽读取时无损折叠(legacy:<角色名> 确定性推导)+ 写回吸收(吃掉的不复活) + 四个读面按 rid 精确取 + 收尾只删自己那份;零顶层新键;判据 Qr1–Qr11(含 Qr10 升级窗口端到端)。配对需求档 docs/2026-10-14-rotation-backup-per-rotation-requirements.md |
docs/2026-10-15-store-compliance-design.md |
批 R 设计(上架合规 · DSH STORE 契约):卡点 = 单文件 1,093,629 B 超过商城 262,144 B 的自动审查上限 ⇒ 纯机械的领域拆分(入口保持 lib/index.js,已落地 23 个域档,最大单档 254,802 B)+ 补 engines.node >=22 + 合规档写清依赖/权限/外部服务/失败边界;红线 = 对外行为零变更;决策 D1–D7、判据 Rr0①–④ / Rr1–Rr8(§6.1 落地模块图 + 模块图实测状态)。配对需求档 docs/2026-10-15-store-compliance-requirements.md,契约逐条回答 docs/2026-10-15-store-compliance.md |
docs/2026-10-15-store-compliance-requirements.md |
批 R 需求档:总体目标(通过商城自动审查、对外行为零变更)· 问题表(单文件越限 / 未声明 Node 范围 / 依赖声明面缺失)· US-R1…US-R5(★ US-R5 是红线:现有用户体验零影响)· 非功能标准 NF-R1…NF-R7 · 七条明确的非目标 · 验收面(本地可核 + 商城外部复检) |
docs/2026-10-15-store-compliance.md |
上架合规档(商城契约的逐条回答):§一 兼容范围(Node >=22 的诚实口径 + DSH peer 区间 + 换代复核纪律)· §二 依赖(运行时恰 1 个 schemastery;零 devDependencies、零构建)· §三 权限与外部服务(零网络 / 零子进程 / 写面三处自家地盘 / 无凭证)· §四 失败边界(观测面 fail-open、写面与信任面 fail-closed)· §五 装/启/卸证据 · §六 达标状态(逐档字节 + 最大单档 254,802 + 仍红的 Rr4 如实登记) |
docs/poster/plugin-poster.html |
海报(零截图 · 自制风格 · 已脱敏):三层结构 / 与宿主 Agent Teams 正交 / 实战数据 / 纪律 |
docs/comparison-agent-teams-2026-09-26.md |
对比(事实核对,不是设计):本插件 vs DSH 内置 Agent Teams 的 14 项逐项对照、三条结构性差异、双方优势、两套共存时的边界;出处分 [实测] / [README] / [会诊] 三级,§9 是未复核的源码级线索 |
docs/observability-batch-design-2026-09-26.md |
可观测批(A 批,2026-09-26):读窗扩展(readIds 点名读 / offset 分页 + 按工具分账的成本不变量 + 末尾「未读 Y 行 + 怎么读」提示)· 只读团队状态卡 team_link_status(六段 + 反面预警注记;第 ⑦ 段「形态」由形态批追加)· 台账收件视图 + 派生回执;判据 U1–U14,§3 的成本模型与 §8 的假设 A1–A5 逐条实测,读数见 docs/verification-log.md |
docs/team-ledger-and-mode-design-2026-09-26.md |
「台账 + 形态」设计(2026-09-26 已拍板 · 第 1 步「只追加台账 tasks.md」已实施,判据 U1–U18;第 2 / 4 / 6 步已随「可观测批(A 批)」一并实施;第 3 步(形态开关)与第 5 步(teammate 交卷提示)已随「形态批(B 批)」实施):只追加台账 tasks.md + meta.ref 映射(把「账」搬出上下文、把「事」留在上下文)+ 团队形态开关(多会话 ↔ agent team 单会话兜底档)+ 只读团队状态卡 + 四条纪律约定;含 §6 拍板结论与 §7 实施顺序 |
docs/team-mode-batch-design-2026-09-26.md |
形态批(B 批,2026-09-26):团队形态字段(mode / leadSessionId,只落在 settings)· team_link_roster action=set-mode(第五个封闭动词:宿主探测 + 双重门 + Lead 校验与默认 + decisions 恰好一行 + 幂等)· 读面形态段(形态 + Lead + 成员名册投影 + 完整能力矩阵 + 形态诊断行;roster get 与状态卡 ⑦ 段同源)· teammate 交卷提示(提示式、不阻断);判据 U1–U14 + U11b,§8 的假设 A1–A4 逐条实测,读数见 docs/verification-log.md |
docs/team-upgrade-research-2026-09-17.md |
调研:一次 16+ 小时真实多会话联调的复盘,与升级提案(其 §5 已被设计取代,以设计文档为准) |
docs/consult-minutes/ |
多模型会诊纪要(含裁定层:逐条采纳/不采纳与理由、分歧父侧裁定、教训、不可验清单) |
docs/verification-log.md |
验证账本(证据,不是说明书):0.3.1 → 0.5.2 逐轮的红相/绿相读数、审计变异矩阵、以及每次真机验证的原始取证(含 0.3.7 那次「静默失效一整天」的完整调试历程) |
Changelog
完整变更史见 CHANGELOG.md——0.3.1 → 0.5.2 逐版条目,每版按「修了什么 → 为什么 → 怎么验证」组织,行为修复都附变异验证证据(修复前必红 / 修复后全绿)。
最近一次发布:0.5.2(2026-10-10,热修:重启后真机复验抓到的三处)——(上一版 0.5.1(2026-10-10,发布收口:让既有团队也吃到自治那套 + 判读行自证口径);再上一版 0.5.0(2026-10-10,发布收口:团队自治三批 + 一次真机修复);0.4.2(2026-09-29,发布时一次性收口 3 个「未发布」条目:DSH 0.2.0-rc.1 preset pin 换代 standingKeyFor → acquireScope · preset 租约释放 · 客户端团队面板可读性重设计)——(上一版 0.4.1(2026-09-29,发布时一次性收口 10 个「未发布」条目:DSH 0.2.0-rc.1 兼容性闸门 · 部署接线改 link: · 策略持久化自持化 · C 批客户端团队面板与形态可视化 · 六轮代码评审/分歧审计修复);再上一版 0.4.0(2026-09-27,发布时一次性收口「团队任务台账」+「可观测批:读窗扩展 · 团队状态卡 · 台账收件视图/派生回执」+「形态批:团队形态 · agent-team 档 · teammate 交卷提示」+三轮分歧/收尾修复轮) 见下)——加固与恢复(导出路由接入平台信任栅栏 · 恢复能力加宽 · 侧栏「会话工具」入口 · 深链聚焦修复)· §10.2.8 /team_session 三条真机缺陷修订(输入文法 · 结果可见性 · 确认框边界)· §10.2.8.9 / §10.2.8.10 四项裁定(默认团队名带日期 · 现任失联则释放并认领 · 调用方回执通道 4 · 无任务也投最小唤醒)。逐轮红相/绿相读数与真机取证见 docs/verification-log.md。
Credits
Fork 自 PwnKY/dsh-session-link——深链复制、/s/<id> 打开器、深链上下文注入均保留自上游,感谢上游工作。
License
MIT
No comments yet. Be the first to write one.