shenhuanageshei/dsh-team-link

dsh-session-link-pro

Session deep links + full session export (markdown/JSON) + approved cross-session messaging with pairing for DeepSeek Harness (dsh).

未分類deepseekdeepseek-harnessdsh

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/ 打开器与深链上下文注入保留自上游;本仓库在其上长出了完整的多会话协作层。

最近三批修了什么(人话版 · 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 假差异 · 台账断档(三次独立拦截) · 三条口径纪律入库

为什么这很重要——要点在两件常被误解的事上:

  1. 不是同一个模型自查,而是不同模型、不同供应商的会话互相抓错。 这些席位上坐过不同家族的模型,而且每个会话可以单独指定 provider / 模型 / 思考强度,也能中途换模型——身份挂在会话上,不挂在模型上。所以「互相核对」是真的互相(不同训练来源、不同盲区),而不是同一个脑子换个座位。
  2. 做成这些事的主力是 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/,粘贴到任意会话即注入该会话只读快照(上游功能) · 会话头部按钮 / 粘贴链接 📋 会话列表 · 列出同工作区其他会话:主题、运行状态、最近消息摘要,以及活性信号行(verdict 五态 + goal 状态 + 静默时长 + 读数时效戳) · team_link_list_sessions ⬇ 会话导出 · 全量事件导出为 markdown(可读)+ JSON(无损) · team_link_export / 会话头部 ⬇ 按钮 📨 跨会话消息 · 投递给另一会话,空闲目标自动唤醒为新回合;返回带 busy 预判;目标没活动代理时以 `❌ 未投递` 开头并列出同工作区存活会话 · team_link_send 📣 广播 fan-out · 一次投多个目标:会话 id / team:/ / team:/*(全队,仅现任协调者);任一目标未投递则返回首行即 `❌ 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/&lt;id&gt;"]
            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[("&lt;DSH_HOME 或 ~/.dsh&gt;/team-link/<br/>policy.json")]
    T -->|best-effort 镜像| BB[("&lt;workspace&gt;/team/&lt;name&gt;/<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)在场时它是事实源;不在场时同一份状态写在 /team-link/policy.json。一个时刻只写一处(不双写),/team//* 仍只是人可读镜像,写入失败只告警、不回滚事实源。落点可核: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: 收到卡片并响应

四步上手:

  1. 开两个会话(同一个工作区目录)。协调者用强模型,worker 用 flash 即可;worker 的开场白一句话:「你是 worker,等主管派活」。
  2. 建团队(在协调者里说):「创建团队 night,我当协调者,把 B 注册为 worker」。创建即认领,不需要手改任何配置。
  3. 建信任通道(可选,但推荐):让 A 给 B 发一条消息 → 接收侧确认框里选「配对:双向免确认」→ 此后 A↔B 互发免确认。
  4. 派活与记账:「广播全队:……」/「这条记入裁决账本」/「更新纪律条款」。

常用话术速查:

想做什么 · 对模型说 · 背后工具 看团队与活性 · 「现在团队状态如何 / 列一下其他会话」 · team_link_list_sessions 看团队一屏现状 · 「出一张团队状态卡」(可加「只看 night」) · team_link_status(六段+⑦形态;全只读) 看崩溃现场 · 「上次 DSH 是怎么死的?」/「看一下崩溃现场(死前那一刻的读数)」 · team_link_status action=forensics(只留现场:死亡窗口 + 死前四类读数 + 免责句;不下崩溃结论 —— 那是人的判断) 读到第 13 行 / 点名读某人 · 「列会话,从第 13 行起读」/「点名读 的活性」 · team_link_list_sessions 的 offset / readIds(单次 ≤12 次面读) 广播 · 「广播全队:」(仅现任协调者) · send + targets=["team:/*"] 点对点 · 「发给 worker1:」 · send + targets=["team:/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-) 回报状态 · 「记一笔: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- 说过什么)与派生回执(我发出去之后对方动了没有:✅ / ⚠ / 未读)+ 三个 baseHash;目标面只在 `readIds` 点名时才读 team_link_team_append · 写黑板三件套:decisions 只追加 / discipline 整文件替换(乐观锁)/ tasks 只追加(kind 闭集,plan 分配 t-,kind/task 误用一律拒绝);★ 批 T:plan 的号码与「已注册」校验都取「活跃 ∪ archive/」 team_link_team_archive · 黑板归档(批 T,file ∈ {decisions, tasks} · keepTail? 默认 200):旧行搬进 archive/-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(运行中/空闲),上限 10 个,超出时注明「共 N 个,仅列前 10 个」;
  • 新增核对提示:对照 id(最常见成因是转录错位)/ 刚重启 DSH 时在侧边栏打开目标会话一次使其恢复为活动代理 / 先调 team_link_list_sessions(默认只读前 12 行的活性;要读窗口外的行,用新参数 readIds 点名读或 offset 分页——两者可选、互斥,单次调用 surface 读数仍 ≤ 12,展开见 §二)。

该列表是纯 agent 注册表读取(ctx.agents.list(),不读会话日志、零 surface 读取、无性能代价),只列根代理、同 cwd、且非发起者自身——子代理不会被列为可投递目标。执行上下文没有会话