@mars.liu/dsh-cybernetics 0.7.8 → 0.8.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,213 @@
1
+ # dsh-cybernetics
2
+
3
+ [简体中文](README.zh-CN.md) | [English](README.md)
4
+
5
+ ![version](https://img.shields.io/badge/version-0.8.2-blue) ![protocol](https://img.shields.io/badge/license-MIT-lightgrey) ![platform](https://img.shields.io/badge/platform-DSH%20plugin-9b59b6)
6
+
7
+ **dsh-cybernetics 是一个把 LLM Agent 编排明确设计成控制回路的 DSH 插件**:session goal 作为 agent 的思考产物持久存在,task agent 行动、verify agent 测量、triage agent 分诊,controller 在轮间推进,用户通过对话完成外环校准——目标、行动、测量、校准的负反馈闭环。
8
+
9
+ 这套协议得名于维纳的控制论:设定目标(setpoint)、行动、测量输出与目标的偏差、用偏差修正下一次行动。传统的一次性 prompt 是开环系统——目标只活在当前 prompt 里,测量的责任全在人;本项目把回路搬进系统本身,让人从「逐条 prompt 的作者」变成「目标的设定者与外环的校准者」。设计理念详见博客文章《[Cybernetics Agent——人工智能应用的新范式](https://marchliu.github.io/2026/11/03/cybernetics-agent-new-paradigm)》。
10
+
11
+ - 协议全文:[docs/protocol.md](docs/protocol.md)
12
+ - 演进历史:[docs/CHANGELOG.md](docs/CHANGELOG.md)
13
+
14
+ ## 核心概念
15
+
16
+ ```mermaid
17
+ flowchart TB
18
+ user["用户<br/>(外环校准者)"] -- "对话校准 / 确认完成" --> sa
19
+ sa["session agent<br/>(编排者 / 控制器)"]
20
+ subgraph loop["内环:一轮(workflows/round.js)"]
21
+ ta["task agent<br/>执行者"] --> va["verify agent<br/>独立测量"] --> ga["triage agent<br/>四分拣"]
22
+ end
23
+ sa -- "委托书 brief" --> ta
24
+ ga -- "verdict 决策" --> sa
25
+ sa -- "维护 / 推进" --> goal[("goal 板<br/>append-only")]
26
+ ```
27
+
28
+ | 角色 | 职责 |
29
+ |---|---|
30
+ | **session agent** | 编排者(控制器):识别用户输入、维护 goal 板、启动/收取/取消轮、向用户转述;从不亲自执行任务 |
31
+ | **controller** | `src/controller.ts` 的轮间状态机:`startRound`/`foldRound`/`runRound`,按轮推进 subgoal(task → verify → triage),维护审计日志 |
32
+ | **task agent** | 执行器:无对话历史的独立 sub agent,面向工作委托书(brief)实际执行,产出结构化交付报告 |
33
+ | **verify agent** | 测量仪器:独立于执行者验收,产出结构化 verdict `{met, criteria, gaps, goalItselfQuestionable}` |
34
+ | **triage agent** | 分诊:对 verdict 暴露的后续线索四分拣 auto / defer / reject / human |
35
+ | **用户** | 外环校准者:对话校准(goal 修订经 session agent 的 `revise` 执行)、裁定 human 类目、确认完成——controller 永不自宣完成 |
36
+
37
+ **goal** 是一等公民的持久状态(objective + 当前 subgoal + 完整工作委托书),构建在 dsh-goal 服务之上,v0.2 起不再依赖 goal.md 文件。内环(task → verify → triage → subgoal 修正)在目标不变时自动迭代,每轮有连续迭代上限兜底;外环(用户 ↔ session agent)只在目标本身有问题时介入——verdict 的 `goalItselfQuestionable=true` 会禁止自动迭代、强制回对话。
38
+
39
+ ## 功能特性
40
+
41
+ ### 全量 goal 模式(`allGoalsMode`,默认 false)
42
+
43
+ 开启后注入「ALL-GOALS MODE ACTIVE」路由协议:**任何用户请求一律先建 goal 再执行**,session agent 禁止用读工具就地完成任务——哪怕单文件查询或一次计算也走回路。简单只读请求 → `quick` 单轮;复杂分析/写任务 → `full` 多轮渐进(`start` 异步启动 + 逐轮 `collect` 收敛)。关闭模式时提示段与 revise 行为与 v0.6 完全一致。
44
+
45
+ ### 硬门(`hardGate`,默认 false)
46
+
47
+ 装配期从顶层 session agent 的工具清单中**物理移除**执行工具(默认 bash/edit/write,可用 `gatedTools` 覆盖)——模型看不见这些工具,自然无法调用,编排从劝告变成结构。`gateMode: 'allowlist'` 反转语义:顶层 agent 只保留读 + 对话 + 展示 + goal/编排动词(内置 DEFAULT_GATE_ALLOWLIST,可 `gateAllowlist`/`gateAllowlistExtra` 覆盖),针对变更类工具的长尾(farm_*/taskboard_*/schedule_*……)。task/verify/triage 子 agent 从不在门禁范围内,保留全部工具。
48
+
49
+ ### append-only goals 与 revise 决策树
50
+
51
+ goal 一经存在不再原地修改。`revise` 是唯一的改目标动词(必填 `reason`):创建带 `parentGoalId` 的新 goal 并立即启动其第 1 轮,旧 goal 置 `superseded` + 记录 `supersededBy`;每次修订追加审计行到 `.cybernetics/revisions-<新goalId>.jsonl`。v0.7 起 `abortPrevious` 缺省时按自动决策树裁定(纯函数 `decideAbort`,依据 goal 板 running 状态):
52
+
53
+ - (a) 旧轮在跑且修订改写目标本身(objective/depth 变化)→ **自动中止旧轮**;
54
+ - (b) 旧轮在跑但目标不变(仅 brief/title/role 修正)→ **等待旧轮自然结束**;
55
+ - (c) 旧 task agent 已结束 → **直接启动新 goal**。
56
+
57
+ 显式传 `abortPrevious: true/false` 保持人工控制;每次修订的审计行与 `revise` 返回值都携带 `abort: {abort, mode: auto|explicit, basis}`。
58
+
59
+ ### quick / full 双档轮次(depth)
60
+
61
+ - `quick`:单 task agent 一轮出结果直接反馈,跳过 verify/triage,成本约 1/3,verdict 由任务报告派生;
62
+ - `full`(默认):完整 工作 → 验证 → 分诊 循环,验收通过才交付。协议明令禁止用 quick 抄近路做变更。
63
+
64
+ ### 持久执行者(persistent executor)
65
+
66
+ `executor: 'ephemeral' | 'persistent'`,与 depth 正交。persistent 用 Agent Teams 的持久 teammate 执行 task 工作:首轮 `spawnTeammate(context:'fresh')` 投生,后续轮只投递**委托书修订**(新增约束/验收标准变化/范围收窄,禁止开放式对话);full 轮中 teammate 报告直接作为 task 报告,verify/triage 仍以临时 agent 独立运行——验证不继承任务叙述。软集成 Agent Teams(无注入反射 + 异常安全探测),取不到即回退 ephemeral。goal 板持久化到 `<workspace>/.cybernetics/board-<sessionId>.json`,宿主重启后自动 rehydrate,「目标」页签不清空。
67
+
68
+ ### 多 goal 板与「目标 / 控制」客户端页签
69
+
70
+ 同一 session 支持多个并发 goal(插件自有 goal 板;dsh-goal 的单槽降级为焦点 goal)。客户端 `lib/client.js` 提供两个会话页签,经插件自有 session 投影 `cyberneticsGoals` 实时推送、就地更新:
71
+
72
+ - **「目标 / Goal」**:本会话全部 goal(标题 / 状态徽标 / 第 N 轮 / 当前 subgoal / depth、executor 小标签 / 被取代与修订来源标注),按「未完成在前、已完成在后」两段排序;空板回退 dsh-goal 单 goal 渲染。
73
+ - **「控制 / Control」**:递归展示本会话全部 agent(主控 + subagent catalog)的运行状态与各自 goal 视图,顶部汇总运行中数量,并列出每个 goal 的执行者行(状态 + 标题 + executor 临时/持久 + 持久 teammate 名称 + 轮次),与「目标」页签同一数据源。
74
+
75
+ ### 插件页配置卡片
76
+
77
+ `src/index.ts` 导出 schemastery `Config` schema,它决定宿主设置传输层服务与接受哪些字段(仅 `.volatile()` 字段);卡片 UI 本身由 `lib/client.js` 绘制,注册 `plugins.item` / `plugins.bundle.config` / `plugins.row.config` 三个席位(bundle 详情页与组件详情页都能配置)。**这张卡片是本插件设置在 GUI 中的唯一 surface**:宿主「插件」页对插件配置完全交给 slot 卡片渲染、自身一个字段都不画,@deepseek-ai/dsh-client-ui-settings 只提供 `configForms` 服务(传输层)而非通用字段渲染器——因此 v0.8.1 起卡片渲染全部 volatile 字段的可编辑控件(v0.8.4 起共六个):全目标模式开关、task/verify/triage agent 模型文本框、子目标轮数上限与并行轮数上限数字框。`roles` 表**不在卡片上**(v0.8.4):在 GUI 里手工编辑 JSON 对象对用户不友善,roles 改为非 volatile——面板不再服务也不接受写入——改经 profile 配置 yaml 直接配置(见下文「roles 高级配置」小节)。volatile 字段保存即时生效(loader 直接提交进运行实例,不 remount);非 volatile 字段保存触发插件行 remount(需宿主重载)。写入落点是用户 patch 行(profile `cordis.patch.yml` 的 `{id, name, config}` 行)。
78
+
79
+ ### agent 模型选择
80
+
81
+ - **扁平字段**:`taskAgentModel` / `verifyAgentModel` / `triageAgentModel`(「provider/model」或裸模型名,volatile);**留空 = 与 session agent 一致**(继承语义)。保存后下一轮 goal 即生效,无需重载。triage 角色读 task/verify 结果输出本轮收敛决策(met/continue/human-input 等);quick 轮完全跳过 verify/triage,`triageAgentModel` 只在 full 轮生效。v0.8.3 起三个字段为下拉加持:`<datalist>` 由宿主实时模型目录驱动(`remote.session.modelCatalog`——与官方「子智能体」设置页同一数据源,`llm/adapters-updated` 事件时刷新),同时保留手工输入(datalist 输入框接受任意文本)。配置值已不在目录中(如该模型订阅被删除)时,卡片显示「不可用——运行时将回退到 session agent 模型」警告并附一键**清除**(显式 unset 写入——绝不静默改写已存配置);运行时回退即 v0.8.0 的 `callRoleAgent`(AGENT_START `no subagent provider registered` 覆盖 provider 被删;模型级「model not found / unknown model」报错已并入 v0.8.3 的可用性模式表)。
82
+ - **roles 表**:按角色覆盖 sub agent 模型(`default`/`verify`/`triage` 固定槽位 + 任意命名任务角色)。v0.8.4 起属高级配置,直接编辑 profile 配置 yaml 维护,不再上设置卡片——见下方「roles 高级配置」小节:
83
+
84
+ ```yaml
85
+ - id: dsh-cybernetics
86
+ config:
87
+ roles:
88
+ default: { provider: zai-coding-cn, model: glm-5.3 }
89
+ verify: { provider: zai-coding-cn, model: glm-5.3 }
90
+ writer: { provider: zai-coding-cn, model: glm-5-turbo }
91
+ ```
92
+
93
+ - **调用级回退(v0.8)**:为某角色设定的模型无法正常调用时——限额/quota 耗尽、网络不可达、provider 报错等**可用性失败**——不再让整个 goal 轮失败:`workflows/round.js` 撤销该角色的 provider/model 覆盖,以 **session agent** 的模型配置重试该角色的子 agent 一次。判定信号来自 workflow runtime(`@deepseek-ai/dsh-workflow-ptc`)`agent()` hook 的错误类型/消息:抛出的 `WorkflowError`(`AGENT_START` 恒算;`AGENT_RESULT`/无 code 错误需消息命中 quota/鉴权/5xx/网络/provider 模式),或返回 `null`(runtime 把子 run `stopReason ≠ "completed"` —— LLM 调用中途失败最常见的形态——压成 null 且不透传原因;仅当配置了覆盖时重试)。**逻辑失败永不换模型**:task agent 正常交付 `done=false` 报告、verify 返回未达成 verdict 都原样流转;非可用性错误(schema/option/cap 守卫错误)原样上抛。每次回退记录进轮结果的 `fallbacks` 数组(`{role, from, trigger, reason}`),narrate 进 `workflow/log`,随 `.cybernetics/round-*.json` 审计日志落盘,并追加进 `cybernetics` 工具结果的决策 note。配置语义向后兼容:留空 = 继承 session agent(无回退必要);已配置 = 优先用配置模型,可用性失败时回退。`workflows/round.js` 改动热生效。
94
+
95
+ ## 安装与使用
96
+
97
+ ### 安装
98
+
99
+ npm 包名 [`@mars.liu/dsh-cybernetics`](https://www.npmjs.com/package/@mars.liu/dsh-cybernetics)。在 DSH profile 中以 `link:` 方式安装本目录即可——插件对宿主包零运行时依赖(工具注册用裸 JSON Schema + 内联校验),不需要自己的 node_modules;要求 Node >= 23.6。
100
+
101
+ 从源码安装:
102
+
103
+ ```bash
104
+ git clone https://github.com/MarchLiu/dsh-cybernetics.git
105
+ cd dsh-cybernetics
106
+ ```
107
+
108
+ 在你的 DSH profile(如 `~/.dsh/profiles/web/`)中 link 安装:
109
+
110
+ ```bash
111
+ # profile/package.json 的 dependencies 添加:
112
+ # "dsh-cybernetics": "link:/path/to/dsh-cybernetics"
113
+ # 然后在 profile 目录执行:
114
+ pnpm install
115
+ ```
116
+
117
+ 安装后:
118
+
119
+ - 服务端:`src/index.ts`(`cybernetics` 工具 + 隐式触发提示段)+ `src/controller.ts`(轮间状态机)+ `src/executor.ts`(执行者适配层);
120
+ - 客户端:`lib/client.js`(「目标 / 控制」页签 + 插件页配置卡片),**改动后需重启宿主**(客户端模块图在宿主启动时构建)并刷新浏览器;`src/` 改动需宿主重载;`workflows/round.js` 改动热生效。
121
+
122
+ ### 插件页配置
123
+
124
+ Web 编辑器「插件」页的本插件卡片上直接配置——v0.8.1 起卡片自身渲染全部 volatile 字段(字段说明见 `Config` schema 的 description):
125
+
126
+ - **全目标模式**(`allGoalsMode`,volatile):开关,保存即时生效,提示段即时重挂;
127
+ - **task/verify/triage agent 模型**(`taskAgentModel` / `verifyAgentModel` / `triageAgentModel`,volatile):下拉加持的文本框——从实时系统模型列表选择(显示 `providerName · provider/model`)或手工输入「provider/model」/裸模型名,回车或移开焦点即保存,留空继承 session agent(triage = 收敛决策角色,仅 full 轮启用)。配置值不在实时模型列表中时显示不可用警告(运行时回退到 session agent 模型)并附一键清除;
128
+ - **递归深度 / 并行上限**(`roundsPerSubgoal` 整数 ≥1 / `maxConcurrentRounds` 整数 ≥0,volatile):数字框,回车或失焦保存,留空恢复默认(`unset` 写入);
129
+ - **roles 高级配置**(`roles`,非 volatile):profile 配置 yaml 直接维护——见下方小节;
130
+ - **硬门相关**(`hardGate`/`gateMode`/`gatedTools`/`gateAllowlist`/`gateAllowlistExtra`,非 volatile):不进入卡片,请在 profile 配置文档中维护(保存触发插件重载)。
131
+
132
+ ### roles 高级配置(profile yaml,agent 指导)
133
+
134
+ `roles` 字段把子 agent 角色映射到 `{provider, model}` 覆盖(`default`/`verify`/`triage` 固定槽位 + 任意命名任务角色,如 `frontend`)。**何时需要它?** 卡片上的三个扁平字段已覆盖固定三角色;只有要给**命名任务角色**(轮启动时经 `role` 参数传入的角色,如 `writer`/`frontend`)分模型、或需要面板没有覆盖的 provider+model 组合时才需要直接配 `roles`。
135
+
136
+ v0.8.4 起该字段**不在 GUI 编辑**——在设置卡片里手工编辑 JSON 对象对用户不友善。请直接编辑 profile 配置 yaml(profile 的 `cordis.patch.yml` 用户 patch 行,或 profile 自身配置文档):
137
+
138
+ ```yaml
139
+ # profile 的 cordis.patch.yml——用户 patch 行:
140
+ - id: dsh-cybernetics
141
+ name: '@mars.liu/dsh-cybernetics'
142
+ config:
143
+ roles:
144
+ default: { provider: zai-coding-cn, model: glm-5.3 }
145
+ verify: { provider: zai-coding-cn, model: glm-5.3 }
146
+ frontend: { provider: zai-coding-cn, model: glm-5-turbo }
147
+ ```
148
+
149
+ 保存后**需宿主重载**(重启宿主)才生效——roles 已不是 volatile 字段,没有面板实时写入路径。session agent 可以指导你完成:让它帮你改 yaml 行,然后重启宿主。运行时行为不变——扁平面板字段合并覆盖 `roles.default`/`roles.verify`/`roles.triage`,命名角色原样透传,每个角色保留 v0.8 可用性回退。
150
+
151
+ > **GUI 验证步骤**:(1) 重启宿主并强刷浏览器——`lib/client.js` 改动只在宿主启动时随客户端模块图加载(不重启卡片仍是旧字段集,例如只有「全目标模式」),`link:` 安装的 `src/` schema 同样只在启动时 import;(2) 打开 Web 编辑器「插件」页 → 本插件卡片/详情页,确认六个卡片字段(全目标模式 / task 模型 / verify 模型 / triage 模型 / 递归深度 / 并行上限)齐全、且无 roles JSON 文本域;(3) 改动任一字段(回车/失焦),确认控件不回弹且 profile `cordis.patch.yml` 出现/更新 `{id: dsh-cybernetics, name: '@mars.liu/dsh-cybernetics', config: …}` 用户 patch 行;(4) 会话里跑一轮 goal 验证新值生效(如 roundsPerSubgoal 改为 2 后同一子目标第 2 轮即暂停)。
152
+
153
+ > **已知宿主缺陷(均已定位)**:① *卡片字段不全* 曾有两层叠加原因:v0.8.1 之前卡片只渲染 `allGoalsMode`(宿主没有通用字段渲染器——本版本已修复,重启宿主 + 刷新浏览器后生效);早于 v0.7.7 启动的宿主其服务 schema 本身只有 `allGoalsMode` 是 volatile(运行时陈旧——`link:` 安装的 schema 在宿主启动时缓存,重启即愈,见 [docs/CHANGELOG.md](docs/CHANGELOG.md) v0.7.10 第 1 条)。② 保存被拒并报 "dsh: profile reload requires the root Include entry" = 宿主 dsh-app-boot 双模块实例——全局 CLI(npm 树)与 profile 侧 dsh-config-editor(pnpm 树)各持一份模块拷贝,`recompose` 的模块级 WeakMap 守卫(`bootstrapIncludes`)永远 miss,**任何**插件的面板保存都在落盘前被拒;插件仓库内无法修复,需把两侧收敛到同一份模块实例(如把 profile 的 `node_modules/@deepseek-ai/dsh-app-boot` 软链到 CLI 引导用的那份拷贝)或等上游修复(CHANGELOG v0.7.10 第 2 条)。workaround:手工在 profile `cordis.patch.yml` 维护用户 patch 行(如 `- id: dsh-cybernetics` + `config: { roundsPerSubgoal: 3, allGoalsMode: true }`)后重启宿主。v0.7.6 起卡片本身不再吞错:仍被拒时会显示宿主拒绝原文并回弹控件。
154
+
155
+ ### 会话内使用:`cybernetics` 工具
156
+
157
+ session agent 在会话内通过 `cybernetics` 工具(`toolName` 可配置)驱动回路,隐式触发提示段(`implicitTrigger: false` 可关)会指导它不等显式要求就按输入分类协议调用:
158
+
159
+ | action | 语义 |
160
+ |---|---|
161
+ | `start` | 新任务:`{action:'start', title, brief}` 建 goal 并(默认后台)启动第 1 轮 |
162
+ | `round` | 阻塞式驱动一轮(对既有 goal 或新建 goal) |
163
+ | `collect` | 折叠已完成的后台轮,得到 controller 决策(continue/pause) |
164
+ | `revise` | 修订目标:创建新 goal(append-only),`reason` 必填,可选 objective/title/depth/executor/abortPrevious |
165
+ | `cancel` | 中止在跑的轮(不改目标;只中止不改目标时用) |
166
+ | `list` | 查看本会话 goal 板全部 goal 的状态、轮次与运行句柄 |
167
+
168
+ 配套审计:每轮结构化结果落 `.cybernetics/round-<goalId>-<N>.json`;修订落 `.cybernetics/revisions-<新goalId>.jsonl`;goal 板快照落 `.cybernetics/board-<sessionId>.json`。
169
+
170
+ ## 设计动机与原理
171
+
172
+ (提炼自博客文章,完整叙事见原文;协议细节见 [docs/protocol.md](docs/protocol.md)。)
173
+
174
+ - **为什么「goal 是 agent 的思考产物」**:传统范式里人是控制器、模型是执行器,回路只存在于人的脑子里。让 session agent 自己把用户输入整理为持久 goal(用户通过对话校准 agent,agent 修订 goal),目标才成为系统里可跟踪的一等公民状态——反馈回路的第一块积木。
175
+ - **为什么 append-only**:目标被悄悄改写,会摧毁「经 goal 跟踪 agent」的全部价值——页签上的目标不再是你当初设定的目标。身份与历史由 goal 链表达,正如 git 用不可变 commit 表达历史;每次修订的 reason + 审计行就是校准的显式变更记录。
176
+ - **为什么 verify 独立于 task**:验证者若看执行者的叙述,会退化成「检查自述是否自洽」而非「检查事实是否满足目标」。测量仪器必须独立于被测对象——协议的核心不变量:**上下文是被配给的,不是被继承的**(task 只拿委托书、verify 只拿 goal + 报告 + 证据、triage 只拿 verdict,永不用 `context: 'fork'`)。持久的是身份和 goal 边界,不是会话历史。
177
+ - **为什么硬门**:早期的「session agent 不亲自执行」只是提示里的劝告,模型看到琐碎任务就会内联干完。硬门在装配期物理移除执行工具,把劝告变成结构——编排者的全部产出是 goal 和 brief(信息),执行者的全部产出是对世界的改变(行动),可观测性与可控性的边界划在角色上,不依赖模型自觉。
178
+ - **振荡防御与升级路径**:每轮连续迭代上限是对「反馈延迟过大导致振荡」的工程化防御;triage 的 human 分拣是控制器为「不可自动处理的扰动」预留的升级路径;`goalItselfQuestionable` 在结构上区分「方向错了」和「执行得不好」。
179
+
180
+ ## 与 DSH 现有机制的关系
181
+
182
+ - **goal**:不替代——dsh-goal 仍是 durable setpoint 与续跑驱动;本插件是它的工作回路。
183
+ - **workflow**:每轮就是一次 `ctx.workflowEngine.start()` run;不新增编排机制(构建在 `dsh-workflow` 之上)。
184
+ - **完成确认**:controller 永不自宣完成、不代改 goal phase;`met` 也只暂停等用户。
185
+
186
+ ## 已知限制
187
+
188
+ 1. **settings 写入的宿主缺陷**:如「插件页配置」一节所述,该运行时上卡片保存可能被宿主在落盘前拒绝(上游 [#12045](https://github.com/deepseek-ai/dsh/discussions/12045));workaround:重启宿主 + 强刷浏览器,或手工 patch 行 + 重启。
189
+ 2. **volatile 与非 volatile 字段的保存语义差异**:volatile 字段(`allGoalsMode`、`taskAgentModel`、`verifyAgentModel`、`triageAgentModel`、`roundsPerSubgoal`、`maxConcurrentRounds`)保存即时生效、不 remount;非 volatile 字段(v0.8.4 起的 `roles`、`hardGate`、`gateMode`、`gatedTools`、`gateAllowlist*`、`toolName`、`implicitTrigger`)保存触发插件行 remount,需宿主重载完成。且宿主设置表单只服务也只接受 volatile 路径——运行中宿主的 schema 过旧时,volatile 写也会被拒(重启即愈)。
190
+ 3. **`link:` 安装注意事项**:客户端 bundle 的注册 id 必须等于 package.json 的 `name`(`@mars.liu/dsh-cybernetics`);loader 行 id 可用短名 `dsh-cybernetics`。插件需挂在本 profile 的**顶层** loader entries(客户端扫描只枚举顶层行);`src/` 改动需宿主重载、`lib/` 改动需重启宿主 + 刷新浏览器。
191
+ 4. 其他协议边界(硬门在装配层不在执行层、dsh-goal 焦点槽不随 superseded 迁移等)见 [docs/protocol.md](docs/protocol.md) 各「已知边界」小节。
192
+
193
+ ## 演进历史
194
+
195
+ | 版本 | 关键节点 |
196
+ |---|---|
197
+ | v0.2 | session-goal 模式 + 隐式触发 + 角色化 sub agent + 客户端 goal 页签 |
198
+ | v0.3 | 多 goal 板 + 编排者协议 + 异步轮(start/collect/cancel/list)+ 委托书 brief |
199
+ | v0.4 / v0.4.1 | 硬门;allowlist 门模式(封住变更工具长尾) |
200
+ | v0.5 | 双档轮次 depth(quick / full) |
201
+ | v0.6 | append-only goals、持久执行者、多 goal 投影、板持久化与 rehydrate、控制页签执行者行 |
202
+ | v0.7–v0.7.8 | 全量 goal 模式 + revise 自动决策树;插件页配置卡片;volatile 模型字段;写入落点取证与保存失败可见化(离线测试 56 项) |
203
+ | v0.7.9–v0.7.10 | roles / roundsPerSubgoal / maxConcurrentRounds volatile 化;基于真实宿主源码定位面板渲染(陈旧运行时)与保存拒绝(dsh-app-boot 双模块实例 WeakMap miss)双根因(离线测试 58 项) |
204
+ | v0.8.0 | 调用级模型回退:角色覆盖模型可用性失败(限额/网络/provider——判定信号为 workflow runtime agent() hook 的错误类型/消息)时回退 session agent 模型重试一次;逻辑失败不换模型;回退记录进轮结果、workflow/log、审计日志与决策 note(离线测试 64 项) |
205
+ | v0.8.1 | 设置卡片覆盖全部六个 volatile 字段——卡片是 GUI 唯一 surface(宿主插件页自身不画字段,dsh-client-ui-settings 是传输服务而非渲染器);文本/数字/roles-JSON 控件带校验,留空 = 恢复默认(unset),写路径与失败兜底与开关同构(离线测试 66 项) |
206
+ | v0.8.2 | 新增 `triageAgentModel` 扁平 volatile 字段(与 task/verify 同构:解析合并进 `roles.triage`,留空 = 继承;triage = 收敛决策角色,仅 full 轮;沿用 v0.8.0 fallback 语义并锁死「无覆盖不重试」);卡片覆盖全部七个 volatile 字段(离线测试 67 项) |
207
+ | v0.8.3 | 模型下拉 + 失效模型处理:三个模型字段改为宿主实时模型目录驱动的 datalist 下拉(`remote.session.modelCatalog`,`llm/adapters-updated` 刷新),保留手工输入;配置的模型在模型设置里删除订阅后,卡片显示不可用警告 + 一键清除(显式 unset——绝不静默自动清除,取舍记录见 docs/CHANGELOG.md),运行时回退到 session agent 模型(模型级删除报错并入 v0.8.0 可用性模式表;provider 删除本就由 `no subagent provider registered` / AGENT_START 覆盖)(离线测试 68 项) |
208
+
209
+ 完整逐版记录与 dogfooding 实测修正(含 2026-11-02 启动链路四次修正、页签渲染两连修等)见 [docs/CHANGELOG.md](docs/CHANGELOG.md)。
210
+
211
+ ## 许可
212
+
213
+ MIT License,Copyright (c) 2026 MarchLiu。完整许可文本见 [LICENSE](LICENSE)。
package/cordis.patch.yml CHANGED
@@ -39,3 +39,11 @@
39
39
  gateAllowlistExtra: [spawn_teammate, send_message, interrupt_agent]
40
40
  # gateAllowlist: [...] # override the built-in survival set
41
41
  # gateMode: denylist + gatedTools: [bash, edit, write] # legacy stance
42
+ #
43
+ # GUI 配置(v0.8.4):卡片渲染六个 volatile 字段(allGoalsMode /
44
+ # taskAgentModel / verifyAgentModel / triageAgentModel / roundsPerSubgoal /
45
+ # maxConcurrentRounds);volatile 保存落为 profile cordis.patch.yml 的用户
46
+ # patch 行({id, name, config},无 insert)并即时生效。roles 已不在卡片上
47
+ # (v0.8.4 起非 volatile,GUI 不暴露)——高级配置直接编辑上方注释示例的
48
+ # config.roles 后重启宿主生效。注意:link: 安装的 src/ schema 只在宿主启动时
49
+ # import——改 schema 后必须重启宿主,面板才会出现新字段。
@@ -0,0 +1,139 @@
1
+ # 演进历史 / Changelog
2
+
3
+ > 本文件承接自项目早期的 README「状态」与「实测修正」记录,按版本倒序整理。
4
+ > 当下的功能与使用方式以 [README.md](../README.md) 为准;协议细节见 [protocol.md](protocol.md)。
5
+ > 约定:`src/` 改动需宿主重载生效;`lib/client.js` 改动需重启宿主(客户端模块图在宿主启动时构建)并刷新浏览器;`workflows/round.js` 改动热生效。
6
+
7
+ ## v0.8.4(2026-11-04):配置卡片移除 roles JSON 编辑项(roles 改经 profile yaml 高级配置)
8
+
9
+ **决策理由(记录在案)**:v0.8.1 起 roles 以 JSON 文本域 +「应用」按钮的形式上卡片,用户实测反馈:在 GUI 卡片里手工编辑一个 JSON 对象(`role → {provider, model}`)对用户不友善——JSON 语法错误校验、转义、role 名拼写全暴露给用户。roles 能力本身保留在 schema/运行时(命名角色进阶场景仍可用),但 GUI 不再暴露;确需配置时由 agent 指导用户直接编辑 profile 配置 yaml(用户 patch 行),README/README.zh-CN 各新增「roles 高级配置」小节记录字段位置示例与重载要求。
10
+
11
+ - **`src/index.ts`**:`roles` 去掉 `.volatile()` 标记(description 文案同步改为「高级配置:直接编辑 profile 配置 yaml 后需宿主重载生效」)。面板语义后果:宿主 `volatileForm` 只服务 volatile 字段,roles 从面板消失且不可经面板写入——这正是想要的行为(同 `hardGate` 等结构字段的既有处置)。**运行时语义核实(双语义均正确)**:接线处 `roleOverrides = { ...readConfigField(config.roles, {}) }` 的 `readConfigField` 同时接受 live reference(volatile,`{get}` → 解引用)与普通对象(非 volatile,原样返回)——`hardGate` 等非 volatile 字段走的就是同一条「普通值」路径,roles 与之同构,无需解 ref 的特殊处理;宿主重载后经 schemastery resolve 出的 roles 就是普通对象,合并逻辑(扁平字段覆盖 `roles.default`/`roles.verify`/`roles.triage`、命名角色透传)完全不变。兼容性:早前经面板写入的 volatile roles 配置在宿主重启后同样 resolve 为普通对象,读取路径不变。
12
+ - **`lib/client.js`**:卡片移除 roles JSON 文本域、「应用」按钮及其全部配套(`settings.roles` / `settings.rolesHint` / `settings.apply` / `settings.invalidJson` 四条 locale 词条 ×2 语言、`isRolesValue` 校验、`commitRoles` 提交逻辑、`rolesRow` JSX、`textarea`/`apply` 两条样式)。字段清单回到**六个**:allGoalsMode / taskAgentModel / verifyAgentModel / triageAgentModel / roundsPerSubgoal / maxConcurrentRounds。
13
+ - **文档**:README / README.zh-CN 新增「roles 高级配置(profile yaml,agent 指导)」小节——何时需要 roles(命名角色分模型)、`cordis.patch.yml` 用户 patch 行的字段位置 yaml 示例、保存后需宿主重载生效、agent 可指导用户完成;两份 README 的卡片字段清单、GUI 验证步骤与 volatile/非 volatile 语义清单同步为六字段版本。
14
+ - **测试**:离线测试增至 **69 项**——51(roles 非 volatile 断言、普通对象 resolve)、57(volatileForm/isVolatilePath 收敛到六字段,roles 路径断言转为非 volatile 负向控制,含宿主真实函数交叉验证)、65(卡片断言:无 roles 文本域、无「应用」按钮、六字段清单)更新;新增 69(roles 非 volatile 双语义:profile yaml 配置 resolve 为普通对象、`readConfigField` 对普通对象与遗留 live ref 双语义、命名角色在扁平字段合并后透传、README yaml 配置路径文档断言)。
15
+ - **生效手段**:`src/` 与 `lib/client.js` 均有改动——**重启宿主 + 强刷浏览器**后卡片即为六字段版本;已有 yaml roles 配置经宿主重载照常生效(含命名角色)。
16
+
17
+ ## v0.8.3(2026-11-04):模型下拉选择 + 失效模型处理(配置卡可用性闭环)
18
+
19
+ **数据源核实(宿主源码,2026-11-04)**:官方「子智能体」设置页(`@deepseek-ai/dsh-client-ui-settings-subagent`)的模型勾选列表读的是 `ctx.remote.session.modelCatalog()`(`@deepseek-ai/dsh-api-session-controller` 的 `@Remote('modelCatalog')`),返回 `{ ok, value: { groups: [{id, name, models: [{id, name}]}], failures } }` = 当前全部可路由模型按 provider 分组;provider 变更经 `ctx.remote.$on('llm/adapters-updated', …)` 推送刷新。本插件客户端照同一契约接入。
20
+
21
+ - **模型下拉**(`lib/client.js`):三个模型字段(taskAgentModel / verifyAgentModel / triageAgentModel)从纯文本框改为 **datalist 加持的文本框**——`<datalist>` 逐项列出目录模型(value = `provider/model`,label = `providerName · provider/model` 便于识别),输入框本体仍是自由文本,**手工输入兜底天然保留**(datalist 不是锁定 select,目录外的值照常提交——与运行时 `parseAgentModelSpec` 接受任意「provider/model」/裸模型名的语义一致)。目录经 `remote.session` 命名空间异步加载(新增 inject 声明 `remote.session`,全程 try/guard:宿主不服务该命名空间或 `modelCatalog` 缺失时下拉为空、不警告、行为与 v0.8.2 完全一致,绝不崩),并订阅 `llm/adapters-updated` 实时刷新(模型设置增删订阅即时反映)。
22
+ - **失效模型处理(配置页)**:卡片渲染时把**已保存的配置值**与实时目录比对(`provider/model` 精确匹配选项 value;裸模型名匹配任意 provider 的 model id)——命中失败即显示「该模型已不在当前系统模型列表中,运行时将回退到 session agent 模型」警告 + 一键「清除」按钮(走与留空保存完全相同的 `unset` op 写路径)。**取舍(记录在案)**:选择「警告 + 显式清除」而非「渲染时自动 unset」——自动改写用户配置有副作用(重新订阅后原值即恢复有效;用户应保有对已存配置的所有权),且 unset 本身就是一次面板写入、失败时还会弹保存失败提示,静默化反而更危险。目录未加载/加载失败时**永不**警告(判定必须可证,避免误报);警告只针对快照值,草稿未提交文本不触发。roles JSON 内的失效项本轮不做逐行校验(委托书明确允许,扁平三字段已覆盖主要交互面)。
23
+ - **失效模型处理(运行时,核实 + 补充)**:核实 v0.8.0 回退路径确实覆盖「模型订阅已删」——provider 被删时 dsh-subagent 在 spawn 前抛 `no subagent provider registered for "x"`(`SubagentError NO_PROVIDER`),workflow runtime 包装为 `WorkflowError code AGENT_START`,`round.js` 的 `isAvailabilityFailure` 恒判真 → 撤销覆盖重试(既有测试 59 已锁)。**补充**:provider 仍在但该 model 订阅被删时,失败以模型级报错形态出现(AGENT_RESULT 内嵌或无 code 直抛),本次在 `AVAILABILITY_PATTERNS` 加入 `model not found / no such model / unknown model / invalid model / not subscribed / model …not available/enabled/found` 模式(测试 63 矩阵新增三行触发器)。`workflows/round.js` 改动热生效。
24
+ - **测试**:离线测试增至 **68 项**——新增 68(下拉数据源:datalist/option 形状与 providerName 标注、手工输入保留;失效判定:provider/model 与裸名两形态、有效值与目录失败时零警告;清除交互:显式 unset 写入、纯渲染零写入;`llm/adapters-updated` 触发目录重取),63 矩阵扩展删除订阅触发器,67 的字段 kind 断言同步为 `model`。
25
+ - **生效手段**:`lib/client.js` 改动需**重启宿主 + 强刷浏览器**;`workflows/round.js` 热生效。验证:重启后打开插件页卡片 → 三个模型字段出现下拉(选项为 `providerName · provider/model`)→ 填入一个已删除订阅的模型回车保存 → 字段下方出现不可用警告 → 点「清除」恢复继承。
26
+
27
+ ## v0.8.2(2026-11-04):新增 triageAgentModel(第三个扁平角色模型字段,卡片覆盖全部七个 volatile 字段)
28
+
29
+ - **新增 `triageAgentModel`**(`src/index.ts` schema + Config 接口):与 `taskAgentModel` / `verifyAgentModel` 完全同构的扁平 volatile 字段——「provider/model」或裸模型名,**留空 = 继承 session agent**;经 `readConfigField` 解 live ref 后由 `parseAgentModelSpec` 解析,在轮启动接线处合并进 `roles.triage`(与 task→roles.default、verify→roles.verify 同一模式)。description 文案写明 triage 角色职责:读 task/verify 结果输出本轮收敛决策(met/continue/human-input 等),**quick 轮不启用**(quick 深度在 verify/triage 之前直接返回)。
30
+ - **fallback 语义**:沿用 v0.8.0 `callRoleAgent`——triage 调用点(普通轮与 persistent executor 的 `taskReport` 分支共两处)本就走 `callRoleAgent('triage', …)`,本次零改动;测试 67 锁死:triage 覆盖 + 可用性失败 → 撤销覆盖以 session 模型重试一次并记录 `{role:'triage', trigger:'availability-error'}`;**空配置(无覆盖)时 triage 返回 null 不触发多余重试**、无伪造回退记录;quick 轮完全不调用 triage。`src/controller.ts` 无需改动:`RolesConfig` 本就有 `triage?: RoleSpec`,扁平字段只落在 `src/index.ts` 的 Config 接口上。
31
+ - **设置卡片**(`lib/client.js`):字段清单加入 `triageAgentModel` 文本框(专属 hint 写明 triage 职责与 quick 轮语义),卡片现覆盖全部**七个** volatile 字段(allGoalsMode / taskAgentModel / verifyAgentModel / triageAgentModel / roundsPerSubgoal / maxConcurrentRounds / roles);读写路径、留空=unset、失败兜底与既有字段完全同构。
32
+ - **生效手段**:`src/index.ts`(schema + 接线)改动需**宿主重载**;`lib/client.js`(卡片)改动需**重启宿主 + 强刷浏览器**(客户端模块图在宿主启动时构建,无 watcher)。验证步骤:重启宿主后强刷「插件」页 → 打开 cybernetics 卡片 → 应看到「Triage agent 模型」文本框(位于 Verify agent 模型之下),填入「provider/model」回车保存,下一轮 goal 的 triage 角色即用该模型;留空保存 = 恢复继承。
33
+ - **测试**:离线测试增至 **67 项**——新增 67(triageAgentModel:volatile/live-ref/解析/合并接线/三处 fallback 语义/quick 轮跳过/卡片字段清单);57(volatileForm 服务清单改断言七字段,含真实宿主 volatileForm 交叉核对)与 65(卡片快照 + 字段清单 + triage 字段 set/unset 写路径)同步扩展。
34
+
35
+ ## v0.8.1(2026-11-04):设置卡片覆盖全部六个 volatile 字段(面板缺字段的真根因)
36
+
37
+ 用户实测(重启宿主 + 刷新浏览器后):插件详情页的 cybernetics 卡片仍只有「全目标模式」一个开关,roles / roundsPerSubgoal / maxConcurrentRounds / taskAgentModel / verifyAgentModel 五个 volatile 字段在 GUI 无处可见。本轮重读宿主源码后定位到**与 v0.7.10 不同的第三层根因**:
38
+
39
+ - **渲染来源核实**:插件详情页(`ItemDetail`,@deepseek-ai/dsh-client-ui-plugin-manager lib/client.js:2710)对插件配置的渲染**完全交给 slot 卡片**(`plugins.item` view "page"),页面自身一个字段都不画;@deepseek-ai/dsh-client-ui-settings 只提供 `configForms` **服务**(传输层 + `volatileForm` 决定服务/接受哪些字段),**没有按 schema 自动渲染字段的通用 ConfigForm UI**;dsh-client-ui-settings-plugins 是只读 inventory 壳(其 README 明言:"Configuring a built-in plugin happens on the sidebar's Plugins page, where each official plugin's own companion package registers its page");官方插件(agent-loop / web-search 等)各自带手绘卡片。**结论:本插件自定义卡没画的 volatile 字段在整个 GUI 中不可见也不可配**——v0.7.2/0.7.3 起的卡片只渲染 allGoalsMode 开关 + summary 文案,这就是重启后其余五字段仍缺席的原因。宿主侧「通用 ConfigForm 服务全部六字段」指的是传输层(测试 57 已证),不是一张实际存在的表单 UI;两张 UI 并不并存,唯一 surface 就是自定义卡。
40
+ - **卡片扩展**(`lib/client.js`):同一 `SettingsCard` 现在渲染全部六个 volatile 字段——allGoalsMode 开关(不变)+ taskAgentModel / verifyAgentModel 文本框 + roundsPerSubgoal / maxConcurrentRounds 数字框 + roles JSON 文本域(「应用」按钮提交,写入前校验 `{role: {provider, model}}` 形状)。单行字段回车或失焦即提交;**留空 = 恢复默认**(`unset` op);数字越界(<1 / <0)只标红不写;内容未变不写。写路径与开关完全同构:live ConfigForm `set`/`unset`(plugins.bundle.config 页)或宿主 ConfigPageForm `mutate(ops, revision)`(plugins.item / plugins.row.config 详情页),被拒时经 `remote.settings.mutate` 无 revision 兜底重试一次并原样展示宿主拒绝信息(v0.7.6 path-(b)),未就绪显示「配置不可用」行且不挂控件、15s 看守防挂起、控件值始终以宿主确认的快照为准——既有行为全部保留。
41
+ - **生效手段**(`lib/client.js` 改动的固有约定):**重启宿主 + 强刷浏览器**(客户端模块图在宿主启动时构建,无源码 watcher)。验证步骤见 README「插件页配置」。
42
+ - **测试**:离线测试增至 **66 项**——新增 65(live ConfigForm 形状:六字段清单渲染、数字解析/越界标红、留空=unset、未变不写、roles JSON 校验与解析后写入、Escape 还原)与 66(宿主 ConfigPageForm 形状:字段写经 `mutate(ops, revision)`、blank=unset op、拒绝→directMutate 无 revision 重试并展示原文、未就绪只渲染标签不挂控件)。
43
+
44
+ ## v0.8.0(2026-11-04):task/verify agent 模型覆盖的调用级回退(call-level fallback)
45
+
46
+ 为角色设定的模型无法调用时(限额/quota 耗尽、网络不可达、provider 报错等**可用性失败**),不再让整个 goal 轮失败:`workflows/round.js` 对每个角色的 `agent()` 调用包一层 `callRoleAgent`,命中可用性失败即**撤销该角色的 provider/model 覆盖**(不传覆盖 = 继承 session agent 的模型配置)重试一次。覆盖 task(含命名角色)、verify、triage 三个调用点;persistent executor 的 full 轮经 `taskReport` 分支同样走 verify/triage 的 `callRoleAgent`。
47
+
48
+ **判定信号来自 workflow runtime(`@deepseek-ai/dsh-workflow-ptc`)`agent()` hook 的错误类型/消息**(对该 runtime 源码逐行核实):
49
+
50
+ - **抛出 `WorkflowError`** —— `code: 'AGENT_START'`(消息 `agent() could not start a child: …`,provider 解析/spawn/首调失败,**恒为可用性失败**,如 `no subagent provider registered for "x"`);`code: 'AGENT_RESULT'`(`child agent run failed: …`,消息内嵌失败原因,命中可用性模式才回退)。模式表:限流(429/rate limit/too many requests)、限额(quota/insufficient/额度/余额/配额)、鉴权(401/403/unauthorized/invalid api key)、服务端不可用(502/503/529/overloaded/service unavailable)、网络(ECONNREFUSED/ETIMEDOUT/ENOTFOUND/fetch failed/network 等)。无 `code` 的错误(如离线 harness 的 mock)按消息模式判定。其余抛出(`INVALID_ARGUMENT`/`UNSUPPORTED_*`/`AGENT_CAP` 等脚本或守卫错误)**原样上抛,绝不换模型**。
51
+ - **返回 `null`** —— runtime 把子 agent 的失败压成 null 且不透传原因(子 run `stopReason ≠ 'completed'`:LLM 调用中途失败经 dsh-subagent 的 `agent/error` → `stopReason: 'error'`,是限额/网络/provider 报错**最常见的形态**;或 completed 但无结构化产出)。仅当该角色**配置了模型覆盖**时把 null 视为「覆盖模型疑似不可调用」重试一次;无覆盖时 null 维持原失败语义(本来就跑在 session 模型上,无处可退)。重试再败降级为既有「本轮无结果」路径,回退记录仍保留。
52
+ - **逻辑失败永不触发回退**:task agent 正常交付 `done=false` 报告、verify 给出未达成 verdict,都是返回值层面的判定,不换模型(离线测试 61 锁死)。
53
+
54
+ **可见性**:每次回退记录 `{role, from, trigger: 'availability-error'|'null-result', reason}` 进轮结果的 `fallbacks` 数组(所有返回路径经 `withFallbacks` 携带),随 `.cybernetics/round-<goalId>-<n>.json` 审计日志落盘;`log()` 把回退原因 narrate 进 `workflow/log` 事件流;controller 的 `decide()` 把「⚠ 模型回退 N 次:role(覆盖→session 模型)」后缀追加进决策 note(cybernetics 工具结果行可见)。配置语义向后兼容:空配置 = 继承 session agent(无回退必要);已配置 = 优先用配置模型,可用性失败时回退。离线测试增至 **64 项**(新增 59–64:AGENT_START/quota 触发、逻辑失败不触发、null-result 回退与再败降级、可用性判定矩阵、controller 层可见性)。`workflows/round.js` 改动**热生效**,无需宿主重载;`src/controller.ts` 的类型与决策 note 改动需宿主重载。
55
+
56
+ 另:用户已按 v0.7.10 第 2 条修复宿主部署(profile 侧 dsh-app-boot 收敛到 CLI 同一份模块实例),离线测试 58 的 inode 探针相应改为双态断言——两份拷贝 realpath 相同即认证「部署已修复」(面板保存恢复可用),相异则维持原有的双实例机制断言。
57
+
58
+ ## v0.7.10(2026-11-04):基于真实宿主源码的双根因定位(面板渲染 + 保存拒绝)与测试忠实化
59
+
60
+ 用户实测(2026-11-04 真实 GUI):面板只显示「全目标模式」一项,v0.7.9 新 volatile 字段(roles / roundsPerSubgoal / maxConcurrentRounds / taskAgentModel / verifyAgentModel)全部缺席;且「全目标模式」开关保存被宿主拒绝:"dsh: profile reload requires the root Include entry"。本轮重读宿主源码(@deepseek-ai/dsh-settings volatileForm/describe/write、@deepseek-ai/dsh-config-editor edit、@deepseek-ai/dsh-app-boot boot/mountRootInclude/recompose)后**修正 v0.7.9 的两个错误结论**:
61
+
62
+ 1. **渲染根因 = 运行时陈旧(stale runtime),不是 schema 缺陷。** 本插件是 `link:` 安装(profile node_modules 软链回本仓库),宿主 loader 在启动时一次性 import `src/index.ts` 并缓存模块图,没有源码 watcher——源码改动只在宿主重启后进入 `entry.fiber.runtime.Config`。实测面板恰好只有「全目标模式」一项,正对应 v0.7.0–v0.7.6 的 schema(当时只有 allGoalsMode 是 volatile;task/verify 两字段 v0.7.7 01:10 才 volatile,roles/rounds/maxConcurrent v0.7.8–0.7.9 才 volatile):运行中的宿主进程装配的是早于 v0.7.7 的运行时。v0.7.9「渲染失败是因为宿主不认新字段」的判断不成立——真实宿主的 `volatileForm`(dsh-settings lib/index.js:122-131)对**每个 volatile 字段**都会生成表单,`describe()` 按 `entry.fiber.runtime.Config` 服务;离线测试 57 证明:当前 schema 经宿主 `validateVolatilePlacement`(app-boot lib/index.js:2606-2621,volatile 必须落在无 volatile 祖先的固定对象路径上——当前六个字段全部合法,含 volatile 的 roles dict)与 `volatileForm` 后,表单恰好服务全部六个字段;而把 face 回退成 pre-0.7.7(仅 allGoalsMode volatile)即逐字段复现用户截图的「单字段面板」。**生效手段:重启宿主**(`src/` 改动需宿主重载,本文件头注约定)+ 浏览器刷新。
63
+
64
+ 2. **保存拒绝根因 = 宿主 dsh-app-boot 双模块实例(部署层),不是插件声明/写入路径问题,插件仓库内无法修复。** 真实写路径是 `settings.write/mutate`(dsh-settings)→ `ConfigEditor.edit`(dsh-config-editor lib/index.js:69-131),而 edit **无条件先跑一遍预检** `reconcileProfilePatches(root, beforePatches, "dsh")`(第 74-75 行,先于任何文档读写);该调用进入 dsh-app-boot 的 `recompose`(lib/index.js:3803),其守卫是**模块级全局 WeakMap** `bootstrapIncludes.get(ctx)`——由 `boot()`/`mountRootInclude`(lib/index.js:4151)在**引导进程自己的 dsh-app-boot 拷贝**里登记。本机部署里引导方(全局 CLI:/opt/homebrew/lib/node_modules/@deepseek-ai/dsh,npm 树)与 profile 侧的 dsh-config-editor(pnpm 树,依赖解析到 profile 自己的 @deepseek-ai/dsh-app-boot 拷贝,两份 lib/index.js inode 不同)各持一份模块实例,WeakMap 永远 miss → **任何**插件的面板保存都在落盘前被拒,报错正是 "dsh: profile reload requires the root Include entry";预检先于 `writeFileAtomic`(第 123 行)的顺序也解释了实测「profile cordis.patch.yml mtime 不变、快速 rejected」的签名。此前 v0.7.4–v0.7.8 依次排查的「非 volatile 拒绝 / revision 冲突 / 落点形状」都不是真实拒绝点。**让面板写入生效的正确装配**(宿主侧,超出本仓库边界):把引导方与 profile 收敛到**同一份** dsh-app-boot 模块实例——最直接是把 profile 的 `node_modules/@deepseek-ai/dsh-app-boot` 软链到 CLI 引导用的那份(Node ESM 按 realpath 去重,同 realpath = 同 WeakMap;注意 pnpm 重装会还原软链),或上游修复 `recompose` 的守卫(改为显式传递 root Include entry,而非模块级 WeakMap)。修复后 volatile 保存将按 v0.7.8 测试 56 已证明的形状落为 profile 用户 patch 行 `{id, name, config}` 并即时生效(volatile 语义 = 不 remount 的活提交,落盘 + loader/volatile-update)。
65
+
66
+ 插件侧本轮改动:`src/index.ts` schema **无需变更**(经真实宿主闸门逐项验证合法);离线测试增至 **58 项**——新增测试 57(validateVolatilePlacement + volatileForm 全字段断言 + roles 子路径 isVolatilePath + pre-0.7.7 陈旧运行时单字段负向对照;并在宿主 profile 在场时**直接 import 宿主自己的** `dsh-settings/lib/types/schema.js` 的 `volatileForm`/`isVolatilePath` 对当前 Config 重放——六个字段全被服务、写闸门全过、非 volatile 字段全被拒,把离线模拟钉死在宿主真实代码上,宿主将来改闸门此处先红)与测试 58(bootstrapIncludes WeakMap 按模块实例失配复现精确拒绝串 + 预检先于落盘的顺序断言 + 同实例去重对照 + 本机双拷贝 inode 探针,实测 CLI 拷贝 inode 548644475 ≠ profile 拷贝 inode 548688631)。README 的 GUI 验证步骤同步更新。
67
+
68
+
69
+ ## v0.7.9(2026-11-04):面板暴露 goal 编排核心参数
70
+
71
+ 根因:宿主 0.1.7 settings model 的插件面板**只服务 `.volatile()` 字段**(`@deepseek-ai/dsh-settings` 的 `volatileForm`:无 volatile 字段的插件条目不出现在 describe() 里,`write()` 对非 volatile 路径直接抛 "Config field ... is not volatile")。v0.7.8 只有 taskAgentModel / verifyAgentModel / allGoalsMode 是 volatile,`roles`、`roundsPerSubgoal`(递归深度上限)与并行上限完全无法在 GUI 配置。本版将 `roles`(role → {provider, model})与新增的 `roundsPerSubgoal`(同一子目标最大连续轮数,缺省 3)、`maxConcurrentRounds`(全局并行轮数上限,缺省 0 = 不限;`launchBackground` 与阻塞 round 路径启动前检查 `running.size`,超限拒绝并提示)全部标 volatile;运行时经 `readConfigField` 解 live ref(roles 不再直接 spread,避免把 ref 对象当 override)。`hardGate`/`gateMode`/`gatedTools`/`gateAllowlist`/`gateAllowlistExtra`/`toolName`/`implicitTrigger` 保持非 volatile:面板写不了它们(结构性装配,属 profile 配置文档职责)。volatile 保存即时生效、下一轮 goal 生效,无需宿主重载。客户端 `lib/client.js` 无需改动:通用 ConfigForm 从 configForms describe() 的 volatileForm 派生字段,新字段自动出现。离线测试 51 扩展锁死三个新 volatile 字段与 roles live-ref 解析(共 56 项全过)。
72
+
73
+ ## v0.7.x(2026-11-03 ~ 2026-11-04)
74
+
75
+ ### v0.7:全量 goal 模式(`allGoalsMode`,默认 false)
76
+
77
+ 开启后注入「ALL-GOALS MODE ACTIVE」路由协议:任何用户请求一律先建 goal 再执行(禁止就地分析);简单只读 → quick 单轮;复杂分析/写任务 → full 多轮渐进(start 异步 + collect 收敛);revise 缺省 `abortPrevious` 改为自动决策树(查 goal 板 running 状态:旧轮在跑且 objective/depth 被改写 → 自动中止;旧轮在跑但目标不变 → 等待自然结束;旧轮未跑 → 直启),显式传值仍是人工控制。每次修订的 `revisions-<新goalId>.jsonl` 审计行新增 `abort: {abort, mode, basis}`;`revise` 返回值同样携带 `abort`。决策树纯函数在 `src/controller.ts` 的 `decideAbort`。关闭模式时提示段与 revise 行为与 v0.6 完全一致(离线测试 48–50 锁死)。同版在 settings schema 上新增扁平 agent 模型字段 taskAgentModel / verifyAgentModel(空 = 继承 session agent)。
78
+
79
+ ### v0.7.1:插件配置面板可视化配置
80
+
81
+ `src/index.ts` 导出 schemastery `Config` schema,宿主 0.1.7 settings model 据此派生 Web 编辑器插件条目的设置页(机制同 `@linxin666/dsh-ssh` / `@linxin666/dsh-usage`:loader 经 `runtime.Config['~standard'].validate` 解析配置,面板由 dsh-client-ui-settings 的共享配置表单渲染,含每个字段的 `description` 文案)。`allGoalsMode` 标记 `.volatile()`:在面板上切换保存后 loader 直接把新值提交进运行实例(不 remount),提示段经 `loader/volatile-update` 重挂即时生效;`hardGate`/`gateMode`/`gatedTools`/`roles` 等结构性字段为普通字段,保存后触发插件行 remount(需宿主重载完成)。注意:schema 解析会把未设置的 `z.array` 字段变成 `[]` 而非 `undefined`,`apply()` 的门回退因此按「空数组 = 未设置」处理(离线测试 51 锁死)。此前版本只能手改 profile JSON 里的 `allGoalsMode: true`;现在在 Web 编辑器「插件」页的本插件卡片上直接切换即可(保存 volatile 字段无需重启宿主,刷新浏览器可见最新面板)。
82
+
83
+ ### v0.7.2 / v0.7.3:插件页设置卡片
84
+
85
+ 实测仅有服务端 Config schema 不会在「插件」页产生任何卡片(本 profile 的 @linxin666/dsh-client-ui-plugin-manager 停用了官方 ui-settings-plugins 设置分区,schema 自动面板无处落脚)。官方插件页(@deepseek-ai/dsh-client-ui-plugin-manager)只通过它声明的 slot 渲染插件配置:`plugins.item`(「内置插件配置」分组列表卡)、`plugins.bundle.config`(按 bundle 名分发,bundle 详情页配置区)、`plugins.row.config`(按 `<bundle>#<rowId>` 分发,组件详情页)。`lib/client.js` 因此注册三个席位:`plugins.item`(id = `dsh-cybernetics`)、`plugins.bundle.config`(key 同时注册 profile 依赖名 `dsh-cybernetics` 与包名 `@mars.liu/dsh-cybernetics`)、`plugins.row.config`(key `<bundle>#dsh-cybernetics`)。`plugins.item` 无条件注册(v0.7.5 加固):早先的 cut 用 `configForms.whileServed` 门控,宿主尚未服务该命名空间时整张卡消失,与「注册从未运行」不可区分;现改为 `ctx.configForms.get()` 绑定一个跟随共享 describe mirror 的活 ConfigFormController,命名空间就绪的瞬间卡片翻成开关,未就绪时渲染「配置不可用」行——可见、可诊断。卡片读写 `ctx.configForms.get('dsh-cybernetics')` 的 ConfigForm;v0.7.3 修复:详情页的 owner prop `form`(宿主 ConfigPageForm `{state, mutate}`)在渲染 seam 上覆盖 inject 的 form(renderer renderEntry 先展开 inject 后展开 ownerProps),卡片两种形态都支持(离线测试 52/53 锁死)。`dsh.client.inject` 增补 `@deepseek-ai/dsh-client-ui-settings` 保证 configForms 提供方先到。
86
+
87
+ ### v0.7.4:保存失败可见化
88
+
89
+ 实机验证(2026-11-03,浏览器实测):卡片注册、summary 文案、开关渲染均正常,但切换保存经 `configForms`→`remote.settings.mutate` 到宿主后被**静默拒绝**(写不落盘:profile `cordis.patch.yml` 未变更,RPC 错误不上控制台,卡片此前 `.catch(() => {})` 吞掉失败,第一次点击甚至把 promise 挂死在「保存中」)。离线模拟宿主 `dsh-config-editor.edit` 的文档校验(`composeEntries([patches])` 含 bundle insert 层 + 新增 override 行)是**通过**的,说明拒绝发生在更早的环节(`reconcileProfilePatches` 预检 / loader 热重载路径),宿主日志在本环境不可读,确切拒绝原因待宿主侧确认。本轮改动:卡片不再吞错——写入被拒(settles false / reject)或 15 s 未返回都显示「保存未生效(宿主拒绝了写入)」并立即把开关还原到宿主确认的快照值;成功路径语义不变(volatile 即时生效)。根治写路径需要宿主侧 settings.mutate 的拒绝日志(`harness-log` 或 `dsh plugin --profile web` 诊断),超出插件仓库边界。
90
+
91
+ ### v0.7.5:卡片注册无条件化
92
+
93
+ 见 v0.7.2/0.7.3 段的 `plugins.item` 说明;宿主日志复核(2026-11-03,`~/.dsh/logs/`):未发现 cybernetics settings mutate 的拒绝记录(最近一次启动日志是 EADDRINUSE 的失败实例,当前宿主进程无对应落盘日志),写路径的宿主侧根因仍待带日志的复现。
94
+
95
+ ### v0.7.6:路径 (b) 落点取证 + 拒绝原因透出
96
+
97
+ 代码级取证(2026-11-04,宿主 checkout 只读)确认:`settings.mutate` 的唯一宿主写路径是 `@deepseek-ai/dsh-settings SettingsForms.write` → `@deepseek-ai/dsh-config-editor ConfigEditor.edit`,后者写入的就是**用户 patch 行**(在 profile `cordis.patch.yml` 追加/更新 `{id, name, config}` 非 insert 行,再以 `reconcileProfilePatches(root, patches, "dsh", [entry.id])` 应用——见 `dsh-config-editor/lib/index.js` edit() 的 `findLastIndex` + `document.add({id, name, config})`)。即插件侧写入落点**从来就是**宿主允许的用户 patch 行,"写到了 bundle insert 行"的假设不成立;实测复现(2026-11-04 浏览器点击开关 + `cordis.patch.yml` mtime 前后比对)显示拒绝发生在宿主 `edit()` **落盘之前**(文件未被触碰、快速返回 settings/rejected),确切 throw 行仍需宿主侧 stderr(tty,本环境不可读)。本轮插件侧改动:写入被拒时经插件自有的 `remote.settings.mutate`(同一宿主写路径)做**一次无条件 revision 回退写**(expectedRevision 省略 = 宿主文档化的无条件写),陈旧 revision 冲突可就地自愈;仍被拒时卡片以「保存未生效,宿主拒绝原因:{message}」透出宿主 RemoteError 原文(ConfigFormController 原本把错误坍缩成裸 `false`)。离线测试增至 54 项,新增写入落点断言(回退写无 revision、主写成功不触回退、无 remote 降级不崩)。
98
+
99
+ ### v0.7.7:agent 模型字段 volatile 化(表单可写性根治)
100
+
101
+ 本轮实机复现(2026-11-04 浏览器点击插件详情页开关):写仍被宿主拒绝(快照回弹「全目标模式:关」,profile `cordis.patch.yml` mtime 21:58:35 未变,拒绝发生在落盘之前),且当前宿主进程 2026-11-03 22:04 启动、早于 v0.7.6 提交(00:40),运行中的仍是旧客户端模块。新增宿主侧取证:`@deepseek-ai/dsh-settings` 的 `volatileForm(schema)`(lib/index.js:122-131)只保留 `.volatile()` 字段作为整个设置表单,`write()`(同文件 502-531 行)对每个写入路径做 `isVolatilePath` 检查并抛「Config field "…" is not volatile」——即宿主表单**只服务也只接受** volatile 字段;据此把 taskAgentModel / verifyAgentModel 两个扁平字段也改为 `.volatile()`(此前非 volatile = 在插件页既不可见也不可写),读取点改为 `readConfigField(config.taskAgentModel, '')` 解包活引用('' = 与 session agent 一致),语义保持「保存后下一轮 goal 即生效,无需重载」。离线测试 55 项全过(测试 55 改为断言两字段 volatile + 活引用解析 + readConfigField 解包布线)。生效步骤:重启宿主(客户端模块图 + 服务端 schema 都在启动时装配)+ 强刷浏览器。
102
+
103
+ ### v0.7.8:写入落点证明测试(测试 56)
104
+
105
+ 把宿主 `@deepseek-ai/dsh-settings` write() 的全部预写闸门(volatileForm / isVolatilePath / projectForm / mutate op reduce / validatePaths / strip + mergeLayers,lib/index.js 122-179 与 488-534 行)逐行复刻,对**真实导出的 Config schema** 重放卡片的 toggle 写:开关写通过每一道闸门、经 schemastery 校验(= cordis `resolveConfig` 的精确检查)后产出持久化 config,最终落点形状恰为 `ConfigEditor.edit`(dsh-config-editor lib/index.js 150-155 行)追加到 profile `cordis.patch.yml` 的**用户 patch 行** `{id, name, config}`(无 `insert` 键)——即 `reconcileProfilePatches`(dsh-app-boot lib/index.js:3794)唯一接受的落点。负向对照:`hardGate` 等非 volatile 路径在写前即被同一闸门以「Config field "hardGate" is not volatile」拒绝、文件不被触碰——这正是 v0.7.4/v0.7.5 实机观测到的拒绝签名(profile patch mtime 不变、快速 rejected):**运行中宿主装配的插件 schema 若早于 volatile 标注,宿主即在写前拒绝**;生效手段不变:重启宿主装配 v0.7.7 schema + 强刷浏览器。离线测试增至 56 项。
106
+
107
+ ## v0.6(2026-11-02)
108
+
109
+ - **多 goal session 投影(`cyberneticsGoals`)**——goal 板每次变化把本 session 全部 goal 的整值快照 append 成一条 `plugin:cybernetics/goals` 事件(可跳过的插件记录,宿主持久化读取安全),服务端纯折叠、宿主整值推送,「目标」页签逐条渲染并就地更新;空板回退到 dsh-goal 单 goal 渲染。
110
+ - **执行者适配层**:`executor: 'ephemeral' | 'persistent'`(与 depth 正交)——persistent 用 Agent Teams 持久 teammate 执行 task 工作(首轮 spawn fresh,后续轮 sendMessage 投递委托书修订,`subagent/end` 捕获输出);quick 由输出契约派生 verdict,full 把报告作为 `taskReport` 交给 round.js,verify/triage 仍以临时 agent 独立运行。审计日志记录 `executor`,goal 板记录 `memberName`。
111
+ - **只新增目标模型(append-only goals)**——goal 一经存在不再原地修改。`action: 'revise'`(必填 `reason`,可选 `objective`/`title`/`depth`/`executor`/`abortPrevious`)创建一个带 `parentGoalId` 的新 goal 并立即启动其第 1 轮,旧 goal 置 `superseded` + `supersededBy` 保留最终状态;已完成的 goal 同样走这条路径。`abortPrevious` 默认 false(旧轮跑完由 session agent 决定是否打断),true 时复用 cancel 路径。每次修订追加 `.cybernetics/revisions-<新goalId>.jsonl`。页签按两段排序(未完成在前、已完成 met/cancelled/superseded 在后,组内 `updatedAt` 倒序;服务端排序、客户端不重排),并显示「已被 <新标题> 取代」/「修订自 <旧标题>」;`GoalStatus` 增加 `superseded`。
112
+ - **板持久化与 rehydrate**——controller 的 goal 板在每个写点整值快照到 `<workspace>/.cybernetics/board-<sessionId>.json`(`{version, sessionId, savedAt, goals}`,同目录临时文件 + rename 原子替换,失败不阻断);重启后首次工具调用拿到空内存板时从快照恢复(缺失/损坏/版本不符/非法记录一律空板,不抛错)并立即重推一次 `cyberneticsGoals` 投影,「目标」页签因此不再在重启后清空。「控制」页签新增每个 goal 的执行者行(状态 + 标题 + executor 临时/持久 + 持久 teammate 名称 + 第 N 轮,未完成在前、已完成在后);`GoalView` 增加可选 `memberName`,投影折叠保留。快照是当前状态而非事件日志,`updatedAt` 不持久化。
113
+ - **第 0 步实验**(2026-11-02):持久 teammate `analyst` 两轮真实知识库分析,持久记忆生效(次轮复用首轮结论,墙钟约减半)、委托书修订语义可行、自主分析质量超出委托书(fresh 上下文 + 详细委托书足以支撑领域判断)。记录见 docs/protocol.md 文末。
114
+
115
+ ## v0.5
116
+
117
+ 双档轮次(depth)——轻量分析 quick 单 agent 一轮直接反馈;复杂分析与变更走完整工作-验证循环(full)。
118
+
119
+ ## v0.4 / v0.4.1
120
+
121
+ - v0.4:可选硬门(`hardGate: true`)——装配期从顶层 session agent 物理摘除执行工具(默认 bash/edit/write),编排从劝告变成结构;task agent 不受影响。
122
+ - v0.4.1:硬门增加 allowlist 模式(只保留读+编排工具,挡住 farm_unregister 类长尾变更工具)。
123
+
124
+ ## v0.3
125
+
126
+ 多 goal 板 + 编排者协议 + 异步轮(`start`/`collect`/`cancel`/`list`)+ task agent 工作委托书(brief)。
127
+
128
+ ## v0.2
129
+
130
+ session-goal 模式 + 隐式触发 + 角色化 sub agent + 客户端 goal 页签。服务端已按 dsh 0.2.1-rc.3 真实 API 核对(tools.register / goals service / workingDirectory),客户端按 ModuleLoader 包装格式手写(参照 dsh-client-ui-goal / dsh-context)。**端到端回路尚未实跑**(此后的实测修正补上了这一课)。
131
+
132
+ ## 实测修正记录(2026-11-02,dogfooding)
133
+
134
+ - **触发链路实测**:隐式提示注入 ✅、工具注册 ✅;执行原在 `ctx.get('workflowEngine')` 处报 "cannot get property ... without inject"(宿主 cordis 对未注入服务的属性访问直接抛错,goal 护栏不可达)。已改为静态 inject `workflowEngine` / `goals` / `workingDirectory`。
135
+ - **第二次实测修正**:首次重启后插件仍 pending——`workflowEngine` 是在 `delegation` group 的 isolate 里提供的(`isolate: workflowEngine: true`),顶层 insert 的行拿不到它。`cordis.patch.yml` 改为按组 id 插入(loader 方言:patch id 指向 group 行时追加进该组 config)。
136
+ - **第三次实测修正**:按组 id 插入也被跳过(dump-config 报 `entry "delegation" not found`)——`delegation` 组不在顶层 patch 索引,而是嵌在 `preset-standard`(`@deepseek-ai/dsh-agent-preset`)的 `config.plugins` 里。最终方案:profile 层 patch 整体重写 `preset-standard` 的 config,把 `dsh-cybernetics` 加进其 delegation 组;本插件的 bundle patch 清空为 `[]`。
137
+ - **第四次实测修正**:页签仍未出现——浏览器实测发现 boot graph 的 client combo 不含本包:客户端扫描只枚举**顶层** loader entries。最终方案:插件回到顶层行,`workflowEngine` 不再静态 inject,执行时经 `ctx.agentPresets.serviceFor(agent, 'workflowEngine')` 从 Agent 的 preset mount 读取(registry 的官方 root-realm 访问器);inject 收窄为根级服务。同轮修复 goal 护栏:`goals.get()` 现严格要求 `phase === 'active'`,并按 goal id 重置轮次状态。
138
+ - **页签渲染两连修**:① inject 手动传的 `t` 遮蔽了槽位按 `locale: NS` 注入的正确翻译函数(字典注册前 bind 会解析为空串)——inject 不再传 t;② 宿主 jsx 变体忽略 `jsx(type, props, children)` 的第三参,children 必须放进 `props.children`,否则所有文本静默丢失、元素塌缩成空胶囊。修复后「目标 / 控制」两页签均完整渲染。
139
+ - **改名回归**:`@mars.liu` 改名把包名从 `dsh-cybernetics` 变成 `@mars.liu/dsh-cybernetics`,loader 行名同步了,但 `lib/client.js` 里 `__ModuleLoader__.load({ id })` 仍是短名 → boot 报 `loaded without registering`。不变量:**客户端 bundle 的注册 id 必须等于 package.json 的 `name`**(client-modules boot graph 以包名为行键,`dsh.client.inject` 里的依赖同样按包名解析);短名只用于 loader 行 id。离线测试有断言锁死这条不变量。