# dsh-team-conductor 的六个上游问题 本文件记录插件针对性解决的六个问题:现象、根因坐标、本插件能力、需要上游做的事。 **坐标来源:** 全部坐标读自本机 build 的只读副本 `work/ref/dsh/node_modules/@deepseek-ai/`(内核版本统一为 `0.2.0-rc.2`;未提取的包经 `work/asar-tool.mjs` / `work/asar-grep.mjs` 从 `app.asar` 内读取)。行号对应上述副本;`lib/index.js` 指的是包内该路径,不是插件自己的文件。凡是**没有**在本机源码里读到的结论,都在表内标注「需上游确认」并写出依据。 **总览表** | # | 现象 | 根因坐标 | 本插件能力 | 需要上游做的事 | |---|---|---|---|---| | 1 | teammate 上下文饱和后无法完成最小闭环;饱和状态对 Lead 不可见;每次续用都要重放整段历史,开销随轮次线性增长 | **token 维度没有接进 roster 视图:** `@deepseek-ai/dsh-experimental-tool-agent-team/lib/index.js` `MEMBER_VIEW_SCHEMA` (:35-71) 与 `modelMember` (:73-79) 只暴露 target/role/status/description/provider/context/model/diagnostics;`list_agents` (:323-330) 直接 `ctx.agentTeams.listMembers(...).map(modelMember)`。`@deepseek-ai/dsh-experimental-agent-team/lib/types/roster.js` `list()` (:104-135) 同样不含占用字段。
**能力其实都在:** `@deepseek-ai/dsh-token-meter/lib/index.js` `measure(session)` (:641-695,`totalTokens` 在 :680);`@deepseek-ai/dsh-compaction-basic/lib/index.js` `compactNow(agent, signal, sourceCommandId)` (:985);窗口来自 `@deepseek-ai/dsh-llm/lib/index.js` `resolveModelInfo(provider, model, signal)` (:2098-2100),返回体 `context.contextWindow` 在 `:2124` | `team_overview` 给每条 roster 行附 `contextPressure`(tokens/contextWindow/tokenBudget/ratio/remaining/level/known/source);未加载成员用持久化日志造分离副本测量,可在唤醒前看到它已经多满;`team_member_edit action=compact` 调 `compactNow` 并在前后各测一次回报变化;`watch`/`near_saturated`/`over` 成员在 `diagnostics` 里被点名并建议「compact 或换新 subagent」 | 把 token 占用与饱和等级接进内核 roster 视图(`roster.js` 的 `list()` 与 `MEMBER_VIEW_SCHEMA`),让任何 Team 工具消费者都能看到;或者由内核在 `spawn_teammate`/`send_message` 前对目标做一次压力检查并给出类型化信号。压缩与测量服务已具备,缺的只是把两者接到 roster 视图上(本插件只是把这个接缝做在了自己的工具里) | | 2 | `spawn_teammate` 上限 8,且没有任何退役/删除工具 → 满了即死锁,`spawn_teammate` 永远只回 `TEAM_MEMBER_LIMIT` | **8 来自 profile 而非内核默认:** `@deepseek-ai/dsh-experimental-agent-team-profile/cordis.patch.yml:20` `maxMembers: 8`;内核默认 16 —— `@deepseek-ai/dsh-experimental-agent-team/lib/types/index.js:15` `DEFAULT_MAX_MEMBERS = 16`(同文件 `:48` 用它兜底)。
**判定点:** `@deepseek-ai/dsh-experimental-agent-team/lib/types/roster.js:241-250` `spawnAdmitted` 的 `journal.transact` 内 `if (state.members.length >= this.maxMembers) throw new TeamError(\`Team member limit ${this.maxMembers} reached\`, 'TEAM_MEMBER_LIMIT')`;名字占用在同段 `:243-245`(`TEAM_MEMBER_NAME_TAKEN`,失败的名字也保留)。`this.maxMembers` 是普通可写字段(`:41-45` constructor 直接赋值),因此运行时可改,判定读的是字段而非 config。
**不可删除是设计:** `@deepseek-ai/dsh-experimental-agent-team/README.zh.md:208`:「扁平且不可变的 roster——只有 Lead 可以创建直接 teammate;不支持嵌套 Team、重命名、删除或名字复用」。
**唯一释放路径:** `roster.js:218-220` `stopTeammates(root, childIds)` → `this.ctx.subagents.drainContinuableChildren(root, childIds)`。
**成员只能经历一次终态迁移:** `@deepseek-ai/dsh-experimental-agent-team/lib/index.js:1292-1308` 的投影只允许 `provisioning → active` 或 `provisioning → failed`;`prior.phase !== 'provisioning'` 时一律抛 `teammate "" has an invalid ${prior.phase} -> ${member.phase} transition`(同规则见源码副本 `lib/types/projection.js:187-206`)。名字复用检查在同一段。 | `team_capacity_set` 在运行时直接写 `teamService.roster.maxMembers`,拒绝降到已用名额以下,并把变更记入侧车 `capacity`;`team_member_add` 满员时自己 `raiseCeiling(+1)`,因此不再撞 `TEAM_MEMBER_LIMIT`;`team_member_delete` 用「编排层墓碑(`deleted/deletedAt/deleteReason`)+ 消息拦截 + `raiseCeiling(+1)`」表达删除(对仍在 `provisioning` 的成员才会追加唯一合法的 `provisioning → failed`),让「删掉再新建」可行;`team_member_edit action=reassign` 还能让已完成任务的成员改名接手下一段共享任务,连新名额都不用花;`team_member_edit action=retire` 是可逆软下线:先 `interrupt`(若在跑)再 `stopTeammates(root,[id])`(15s 超时)释放运行激活,最后写 `retired: true` + `retiredAt`;`action=restore` 翻转标记,下一条消息 cold-resume | 提供受支持的运行时上限接口(例如 roster 上的显式 `setMaxMembers`)与成员退役/删除语义,让「满了」有一个名字稳定、可审计的出口;或者明确文档化 `roster.maxMembers` 可写这一事实与 `stopTeammates` 的释放契约。若要真正删除 roster 行,需要内核重新定义「名字永不复用」与持久日志回放的关系;在此之前,任何「把已结算成员密封成终态」的写法都会被 `lib/index.js:1292-1308` 的迁移规则拒绝,因此还需要一条受支持的「active 成员如何进入终态」的路径 | | 3 | subagent 与 `spawn_teammate` 的定位差异没有文档说明;模型创建的 subagent 无法被 `send_message` 寻址(`active teammate "..." not found`),也不出现在 `list_agents` 里 | **全局 child 控制工具被整排替换:** `@deepseek-ai/dsh-experimental-agent-team-profile/cordis.patch.yml:4-14` 把 `tool-subagent-control`、`tool-subagent-list-agents`、`tool-subagent`、`tool-subagent-fork` 全部 `disabled: true`。
**替代物只认 roster:** `@deepseek-ai/dsh-experimental-tool-agent-team/lib/index.js` `send_message` (:295-320) → `ctx.agentTeams.sendMessage` (agent-team `lib/index.js:1773-1775`) → `TeamMailbox.send` (`.../lib/types/mailbox.js:97-107`) → `resolveActiveMember` (`.../lib/types/roster.js:18-27`),对非 active 成员抛 `active teammate "" not found` / `TEAM_MEMBER_NOT_FOUND`;`list_agents` (tool-agent-team `:323-330`) 同样只映射 roster。
**真正可达的通道:** `@deepseek-ai/dsh-subagent/lib/index.js` `sendMessage(sender, targetId, content, options)` (:1762-1775) → `deliverToChild` (:1808-1818) → `deliverFollowup` (:1820-1824) → 无 live Activation 时 `coldResume` (:1898) | `team_message` 的 `target` 接受 roster durable name、侧车 alias(含 `formerNames` 历史名),**也接受 child session id**,后者直接走双作用域解析出的 subagent 服务(先插件 ctx、取不到再退回 `agent.ctx`)的 `sendMessage`,因此可中途 steer 非 roster 的 continuable subagent;`team_overview.children` 只列出**本团队**(`parentSession === team.root`)的这类子代理,带 id/父会话/是否同 Team/模型,让 Lead 有东西可寻址(别的 Lead 的子代理不会被混入);注入的 policy 段明确写出「一次性有界收尾任务用 subagent,需反复 steer 的长期角色才用 durable teammate」 | 在面向模型的文档/提示里写清 subagent 与 durable teammate 的定位差异与各自的生命周期;给非 roster continuable child 一个稳定、可由 `send_message` 接受的名字或别名(现在只有 session id,且形状匹配是纯字符串约定) | | 4 | 同一份 teammate 完成通知(closing message)重复投递,模型把它当作「又完成了一轮」 | **通知不含 activation 标识:** `@deepseek-ai/dsh-subagent/lib/types/continuation-messages.js` `createSettlementMessage(childId, terminal)` (:85-105),source 只有 `{ kind: 'subagent-settled', form: 'notice', summary, senderSessionId }` (:98-103)。
**投递点:** `@deepseek-ai/dsh-subagent/lib/index.js` `finishDisposal` (:1208-1252,`:1248` 调 `this.notifySettlement(activation, activation.observer.terminal(failure))`) → `notifySettlement(activation, terminal)` (:1254-1268,走 `parent.inject(message)` 或 `sendWaking`)。
**推论:** 「同一 Activation 被通知两次」与「两个不同 Activation 产出相同结尾」在持久内容上不可区分——source 里没有 activation/settlement id,唯一的无歧义信号是 Activation 对象本身 | 结算通知守卫把**激活注册表**上的 `notifySettlement`(真实路径 `subagents.continuations.activations`,不是 `subagents` 服务)就地包一层 `WeakSet`:同一 Activation 对象第二次被通知时抑制并计数(`team_overview.guard.suppressed` / 状态路由 `suppressedNotices`),两个不同 Activation 各产一次时即使正文一字不差也全部放行;配置 `dedupeSettlementNotices: false` 可关闭。`team_overview.settlements` 另按 `(child, summary, 正文摘要)` 分组报告重复次数,供人工与守卫计数对照 | 给 notice source 增加 activation/settlement 标识(例如 settlement id),或让 `notifySettlement` 自身按 activation 幂等——两种做法都能把去重从「包装服务方法」变回内核不变量;否则每个消费者都得自己包一层,且跨进程重复永远无法覆盖 | | 5 | `edit` 报「read 状态过期」时缺少可执行的恢复指引,队友整轮零产出 | **拒绝侧:** `@deepseek-ai/dsh-fs-observation-policy/lib/index.js`(`editIntent` 对未观测目标抛 `FS_NOT_OBSERVED`,presence 提供 observed version 作为 CAS 基准);`@deepseek-ai/dsh-fs-observation-policy/README.zh.md` 说明观察状态不持久化、恢复的会话必须重新读取目标。
**模型可见文案止于「重读」:** `@deepseek-ai/dsh-tool-fs/lib/index.js` 的 `remediateFsError()`:`FS_NOT_OBSERVED` 规范化为 ``cannot modify "": file has not been read — read the file, then retry``,`FS_STALE_VERSION` 保留提供方原因并追加 ``— re-read the file, then retry``(`@deepseek-ai/dsh-tool-fs/README.zh.md`「对模型而言的变化」「失败与恢复」同述)。
**缺口:** 文案没有「怎么把这次编辑重新施加」的可执行步骤(唯一字符串替换、不要按行号重放、不要因此丢掉整轮) | 注入的 policy 段(section `team:conductor:policy`,order 640,紧跟内核 `TEAM_POLICY = 600`)给出完整恢复序列:先用 `read` 刷新观察状态,再用**唯一字符串**替换、不要用行号,一次一处、改完立刻自检,并且**不要放弃本轮** | 把恢复序列并入 `FS_STALE_VERSION` / `FS_NOT_OBSERVED` 的模型可见文案(`remediateFsError`),让「重读」之后那一步也可执行;或者在系统提示的通用文件工具段里给出统一的「过期后如何继续」指引 | | 6 | 队友倾向写外部补丁脚本(`fix3.py`/`fix4.py`、shell heredoc)打补丁;脚本因引号/语法问题失败时静默无产出,文件其实没变 | **没有单点内核缺陷:** `@deepseek-ai/dsh-tool-fs/lib/index.js` 的 `write`/`edit` 只通过 `fs/*` 意图槽位约束「读后写」,`remediateFsError()` 只处理观测/版本错误——**不约束用哪种方式写**;heredoc 与外部脚本经 `@deepseek-ai/dsh-tool-bash`(包目录 `@deepseek-ai/dsh-tool-bash`)执行,脚本自身语法错误与「产物是否真的改变」没有任何校验。在 `app.asar` 内 `dsh/node_modules/@deepseek-ai/**` 的 prompt/policy/README 文本中检索 `heredoc|patch script|补丁脚本` **无任何命中**,即不存在这条纪律文本。
**该在哪一层补属于上游选择:需上游确认**(候选位置:`@deepseek-ai/dsh-tool-bash` 的模型可见文案,或内核 `TEAM_POLICY` / 通用工具纪律段)。依据:上述检索无命中,且 bash 工具不以「产物是否变化」为成败条件 | 注入的 policy 段明确要求:用编辑工具就地改,先 `read` 刷新观察状态,再用唯一字符串替换、一次一处、改完立刻自检;**禁止**生成外部补丁脚本,**禁止**用 heredoc 打补丁,并直接点明失败模式(引号/语法问题 → 静默失败 → 看起来改过其实没变) | 增加模型可见的成文纪律(禁止用 heredoc/外部脚本打补丁,要求就地编辑并自检);更彻底的做法是让补丁类操作具备可验证的产物保证(例如执行后比对目标文件的哈希是否真的变化),把「静默失败」变成明确错误 | ## 两条需要上游确认的判断 **问题 4 的「重复投递」是内核缺陷还是宿主行为。** 本插件能证明的是:notice source 不含 activation 标识,同一 Activation 二次通知在内容上不可区分(`continuation-messages.js:98-103`)。至于二次通知的确切触发条件(例如某个 disposal 路径重复进入、或 wake 重放),本机源码中没有一条可复现的调用链能单点归因,因此**不在本文件里断言触发路径**。需要上游确认的是:修在 source 标识层还是 `notifySettlement` 幂等层。 **问题 6 的修复位置。** 依据只有「检索无命中」与「bash 工具的成败不校验产物」,属于「缺少纪律文本」而非「某个函数写错」。因此标注需上游确认:这条纪律该由 `@deepseek-ai/dsh-tool-bash` 的文案、内核 `TEAM_POLICY` 段,还是通用系统提示承担。 ## 本插件不做的两件事(边界) 1. **不改内核调度。** `team_overview` 的测量与等级只用于显示与建议;`compact` 是显式动作,只在 `team_member_edit action=compact` 时发生。 2. **不替换内核工具。** 六个工具都是新增名字(内核 team bundle 的 `spawn_teammate`/`send_message`/`list_agents`/`wait_agent`/`interrupt_agent`/`team_task_*` 与本插件的 `team_overview`/`team_member_edit`/`team_capacity_set`/`team_message`/`team_member_add`/`team_member_delete` 无重名),bundle patch 只做 `insert`,不含任何 `disabled: true`。 ## 已知实现边界(坐标在本插件内) **`team_member_delete` 的 `seal_kernel` 只在 `provisioning` 成员上可能成功,这是刻意的。** 内核投影只允许一次迁移(`provisioning → active|failed`),对任何其它起点抛 `teammate "" has an invalid ${prior.phase} -> ${member.phase} transition`(`@deepseek-ai/dsh-experimental-agent-team/lib/types/projection.js:196-203`,`:128-133` 还会让 `TeamJournal.state()` 在 `projection.failure` 存在时抛错)。所以对已 `active` 的记录**追加 `phase:"failed"` 是非法的**:那会污染投影、让之后每次 `journal.state()` 读取都失败、把整支 Team 的编排面弄坏。实现因此从不尝试:`sealMemberInKernel()` 仅在 `phase === "provisioning"` 时追加,其余情况返回 `immutable-active` / `immutable-failed` 并**不写任何日志**。删除的实际效果来自三件事:编排层墓碑(`deleted`/`deletedAt`/`deleteReason`)、`agentTeams.sendMessage` 上的消息拦截、以及 `raiseCeiling(+1)`。 **`rename` / `reassign` 改的是编排层名字,不是内核名字。** 内核把 `name`/`provider`/`context` 当作不可变身份字段(`projection.js:196-201`,`changed immutable identity fields`),所以改名只写侧车 `alias` 并把旧名压进 `formerNames`;新旧名字都能被本插件寻址,但内核 `list_agents` 仍显示 durable name。这是设计约束,不是缺口。 **两个守卫都是进程内包装。** 结算守卫装在 `subagents.continuations.activations.notifySettlement`(激活注册表,不是 `subagents` 服务),按 Activation 对象身份去重;删除守卫装在 `agentTeams.sendMessage`(只覆盖走 Team 服务的投递——按 child session id 直达 `subagents.sendMessage` 的通道不在其覆盖面内),按侧车墓碑判定。都不跨进程,重启即恢复内核原样;分别可用 `dedupeSettlementNotices: false` / `blockDeletedTargets: false` 关闭。