@mars.liu/dsh-cybernetics 0.2.0 → 0.7.8

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.
package/README.md CHANGED
@@ -7,14 +7,137 @@ task agent → verify agent → triage agent,controller 在轮间推进,用
7
7
  - 协议全文:[docs/protocol.md](docs/protocol.md)
8
8
  - 服务端:`src/index.ts`(`cybernetics` 工具 + 隐式触发提示段)+ `src/controller.ts`(轮间状态机)
9
9
  - 客户端:`lib/client.js`(「目标 / Goal」+「控制 / Control」两个会话页签,实时推送)
10
- - 「目标」读当前 session 的 goal projection;
10
+ - 「目标」优先读插件自有 session projection `cyberneticsGoals`:本会话**全部** goal
11
+ (标题 / 状态徽标 / 第 N 轮 / 当前 subgoal / depth、executor 小标签、被取代/修订来源标注),
12
+ 按服务端两段顺序渲染(未完成在前、已完成在后,组内更新时间倒序)并就地更新;
13
+ 无多 goal 数据时回退到 dsh-goal 的 `goal` projection 单 goal 渲染;
11
14
  - 「控制」读 session list store(control stream 实时推送),递归展示本会话全部
12
15
  agent(主控 + subagent catalog)的运行状态与各自 goal projection
13
- (objective / phase / rounds / 受阻原因),顶部汇总运行中数量。
16
+ (objective / phase / rounds / 受阻原因),顶部汇总运行中数量;v0.6 起顶部还列出
17
+ 每个 goal 的**执行者行**(状态 + 标题 + executor 临时/持久 + 持久 teammate 名称 +
18
+ 第 N 轮,未完成在前、已完成在后),数据源与「目标」页签同一份 `cyberneticsGoals` 投影。
14
19
  - 轮脚本:`workflows/round.js`(每次 run = 一轮;`roles` 表支持按角色覆盖 sub agent 模型)
15
20
 
16
21
  ## 状态
17
22
 
23
+ v0.7:**全量 goal 模式(`allGoalsMode`,默认 false)**——开启后注入「ALL-GOALS MODE ACTIVE」
24
+ 路由协议:任何用户请求一律先建 goal 再执行(禁止就地分析);简单只读 → quick 单轮;
25
+ 复杂分析/写任务 → full 多轮渐进(start 异步 + collect 收敛);revise 缺省 `abortPrevious`
26
+ 改为自动决策树(查 goal 板 running 状态:旧轮在跑且 objective/depth 被改写 → 自动中止;
27
+ 旧轮在跑但目标不变 → 等待自然结束;旧轮未跑 → 直启),显式传值仍是人工控制。
28
+ 每次修订的 `revisions-<新goalId>.jsonl` 审计行新增 `abort: {abort, mode, basis}`;
29
+ `revise` 返回值同样携带 `abort`。决策树纯函数在 `src/controller.ts` 的 `decideAbort`。
30
+ 关闭模式时提示段与 revise 行为与 v0.6 完全一致(离线测试 48–50 锁死)。`src/` 改动需宿主重载。
31
+ v0.7.1:**插件配置面板可视化配置**——`src/index.ts` 导出 schemastery `Config` schema,宿主
32
+ 0.1.7 settings model 据此派生 Web 编辑器插件条目的设置页(机制同 `@linxin666/dsh-ssh` /
33
+ `@linxin666/dsh-usage`:loader 经 `runtime.Config['~standard'].validate` 解析配置,面板由
34
+ dsh-client-ui-settings 的共享配置表单渲染,含每个字段的 `description` 文案)。
35
+ `allGoalsMode` 标记 `.volatile()`:在面板上切换保存后 loader 直接把新值提交进运行实例
36
+ (不 remount),提示段经 `loader/volatile-update` 重挂即时生效;`hardGate`/`gateMode`/
37
+ `gatedTools`/`roles` 等结构性字段为普通字段,保存后触发插件行 remount(需宿主重载完成)。
38
+ 注意:schema 解析会把未设置的 `z.array` 字段变成 `[]` 而非 `undefined`,`apply()` 的门
39
+ 回退因此按「空数组 = 未设置」处理(离线测试 51 锁死)。此前版本只能手改 profile JSON
40
+ 里的 `allGoalsMode: true`;现在在 Web 编辑器「插件」页的本插件卡片上直接切换
41
+ 即可(保存 volatile 字段无需重启宿主,刷新浏览器可见最新面板)。
42
+ v0.7.2/0.7.3:**插件页设置卡片**——实测仅有服务端 Config schema 不会在「插件」页产生任何
43
+ 卡片(本 profile 的 @linxin666/dsh-client-ui-plugin-manager 停用了官方 ui-settings-plugins
44
+ 设置分区,schema 自动面板无处落脚)。官方插件页(@deepseek-ai/dsh-client-ui-plugin-manager)
45
+ 只通过它声明的 slot 渲染插件配置:`plugins.item`(「内置插件配置」分组列表卡)、
46
+ `plugins.bundle.config`(按 bundle 名分发,bundle 详情页配置区)、`plugins.row.config`
47
+ (按 `<bundle>#<rowId>` 分发,组件详情页)。`lib/client.js` 因此注册三个席位:
48
+ `plugins.item`(id = `dsh-cybernetics`)、`plugins.bundle.config`(key 同时注册
49
+ profile 依赖名 `dsh-cybernetics` 与包名 `@mars.liu/dsh-cybernetics`)、`plugins.row.config`
50
+ (key `<bundle>#dsh-cybernetics`)。`plugins.item` 无条件注册(v0.7.5 加固):早先的
51
+ cut 用 `configForms.whileServed` 门控,宿主尚未服务该命名空间时整张卡消失,与「注册
52
+ 从未运行」不可区分;现改为 `ctx.configForms.get()` 绑定一个跟随共享 describe mirror
53
+ 的活 ConfigFormController,命名空间就绪的瞬间卡片翻成开关,未就绪时渲染
54
+ 「配置不可用」行——可见、可诊断。卡片读写 `ctx.configForms.get('dsh-cybernetics')`
55
+ 的 ConfigForm;v0.7.3 修复:详情页的 owner prop `form`(宿主 ConfigPageForm
56
+ `{state, mutate}`)在渲染 seam 上覆盖 inject 的 form(renderer renderEntry 先展开
57
+ inject 后展开 ownerProps),卡片两种形态都支持(离线测试 52/53 锁死)。
58
+ `dsh.client.inject` 增补 `@deepseek-ai/dsh-client-ui-settings` 保证 configForms 提供方
59
+ 先到。改动 `lib/client.js` 后需重启宿主(客户端模块图在宿主启动时构建)并刷新浏览器。
60
+ v0.7.4:**保存失败可见化**——实机验证(2026-11-03,浏览器实测):卡片注册、summary 文案、开关渲染
61
+ 均正常,但切换保存经 `configForms`→`remote.settings.mutate` 到宿主后被**静默拒绝**(写不落盘:
62
+ profile `cordis.patch.yml` 未变更,RPC 错误不上控制台,卡片此前 `.catch(() => {})` 吞掉失败,
63
+ 第一次点击甚至把 promise 挂死在「保存中」)。离线模拟宿主 `dsh-config-editor.edit` 的文档校验
64
+ (`composeEntries([patches])` 含 bundle insert 层 + 新增 override 行)是**通过**的,说明拒绝发生在
65
+ 更早的环节(`reconcileProfilePatches` 预检 / loader 热重载路径),宿主日志在本环境不可读,确切
66
+ 拒绝原因待宿主侧确认。本轮改动:卡片不再吞错——写入被拒(settles false / reject)或 15 s 未返回
67
+ 都显示「保存未生效(宿主拒绝了写入)」并立即把开关还原到宿主确认的快照值;成功路径语义不变
68
+ (volatile 即时生效)。根治写路径需要宿主侧 settings.mutate 的拒绝日志(`harness-log` 或
69
+ `dsh plugin --profile web` 诊断),超出本轮插件仓库边界。
70
+ v0.7.5:**卡片注册无条件化**——见上 v0.7.2/0.7.3 段的 `plugins.item` 说明;
71
+ 宿主日志复核(2026-11-03,`~/.dsh/logs/`):未发现 cybernetics settings mutate 的拒绝
72
+ 记录(最近一次启动日志是 EADDRINUSE 的失败实例,当前宿主进程无对应落盘日志),
73
+ 写路径的宿主侧根因仍待带日志的复现。
74
+ v0.7.6:**路径 (b) 落点取证 + 拒绝原因透出**——代码级取证(2026-11-04,宿主 checkout 只读)确认:
75
+ `settings.mutate` 的唯一宿主写路径是 `@deepseek-ai/dsh-settings SettingsForms.write` →
76
+ `@deepseek-ai/dsh-config-editor ConfigEditor.edit`,后者写入的就是**用户 patch 行**(在 profile
77
+ `cordis.patch.yml` 追加/更新 `{id, name, config}` 非 insert 行,再以
78
+ `reconcileProfilePatches(root, patches, "dsh", [entry.id])` 应用——见
79
+ `dsh-config-editor/lib/index.js` edit() 的 `findLastIndex` + `document.add({id, name, config})`)。
80
+ 即插件侧写入落点**从来就是**宿主允许的用户 patch 行,"写到了 bundle insert 行"的假设不成立;
81
+ 实测复现(2026-11-04 浏览器点击开关 + `cordis.patch.yml` mtime 前后比对)显示拒绝发生在宿主
82
+ `edit()` **落盘之前**(文件未被触碰、快速返回 settings/rejected),确切 throw 行仍需宿主侧
83
+ stderr(tty,本环境不可读)。本轮插件侧改动:写入被拒时经插件自有的 `remote.settings.mutate`
84
+ (同一宿主写路径)做**一次无条件 revision 回退写**(expectedRevision 省略 = 宿主文档化的
85
+ 无条件写),陈旧 revision 冲突可就地自愈;仍被拒时卡片以
86
+ 「保存未生效,宿主拒绝原因:{message}」透出宿主 RemoteError 原文(ConfigFormController 原本把
87
+ 错误坍缩成裸 `false`)。离线测试增至 54 项,新增写入落点断言(回退写无 revision、主写成功不触
88
+ 回退、无 remote 降级不崩)。
89
+ v0.7.7:**agent 模型字段 volatile 化(表单可写性根治)**——本轮实机复现(2026-11-04 浏览器点击
90
+ 插件详情页开关):写仍被宿主拒绝(快照回弹「全目标模式:关」,profile `cordis.patch.yml` mtime
91
+ 21:58:35 未变,拒绝发生在落盘之前),且当前宿主进程 2026-11-03 22:04 启动、早于 v0.7.6 提交
92
+ (00:40),运行中的仍是旧客户端模块。新增宿主侧取证:`@deepseek-ai/dsh-settings` 的
93
+ `volatileForm(schema)`(lib/index.js:122-131)只保留 `.volatile()` 字段作为整个设置表单,
94
+ `write()`(同文件 502-531 行)对每个写入路径做 `isVolatilePath` 检查并抛
95
+ 「Config field "…" is not volatile」——即宿主表单**只服务也只接受** volatile 字段;据此把
96
+ taskAgentModel / verifyAgentModel 两个扁平字段也改为 `.volatile()`(此前非 volatile =
97
+ 在插件页既不可见也不可写),读取点改为 `readConfigField(config.taskAgentModel, '')` 解包
98
+ 活引用('' = 与 session agent 一致),语义保持「保存后下一轮 goal 即生效,无需重载」。
99
+ 离线测试 55 项全过(测试 55 改为断言两字段 volatile + 活引用解析 + readConfigField 解包布线)。
100
+ 生效步骤:重启宿主(客户端模块图 + 服务端 schema 都在启动时装配)+ 强刷浏览器;验证步骤:
101
+ 插件详情页拨「全目标模式」→ 开关保持 + 摘要行变「全目标模式:开」+ profile patch 出现
102
+ `dsh-cybernetics` 用户行;仍失败时卡片将显示宿主拒绝原文(v0.7.6 面)。
103
+ v0.7.8:**写入落点证明测试(测试 56)**——把宿主 `@deepseek-ai/dsh-settings` write() 的全部
104
+ 预写闸门(volatileForm / isVolatilePath / projectForm / mutate op reduce / validatePaths /
105
+ strip + mergeLayers,lib/index.js 122-179 与 488-534 行)逐行复刻,对**真实导出的 Config
106
+ schema** 重放卡片的 toggle 写:开关写通过每一道闸门、经 schemastery 校验
107
+ (= cordis `resolveConfig` 的精确检查)后产出持久化 config,最终落点形状恰为
108
+ `ConfigEditor.edit`(dsh-config-editor lib/index.js 150-155 行)追加到 profile
109
+ `cordis.patch.yml` 的**用户 patch 行** `{id, name, config}`(无 `insert` 键)——即
110
+ `reconcileProfilePatches`(dsh-app-boot lib/index.js:3794)唯一接受的落点。
111
+ 负向对照:`hardGate` 等非 volatile 路径在写前即被同一闸门以
112
+ 「Config field "hardGate" is not volatile」拒绝、文件不被触碰——这正是 v0.7.4/v0.7.5
113
+ 实机观测到的拒绝签名(profile patch mtime 不变、快速 rejected):**运行中宿主装配的
114
+ 插件 schema 若早于 volatile 标注,宿主即在写前拒绝**;生效手段不变:重启宿主装配
115
+ v0.7.7 schema + 强刷浏览器。离线测试增至 56 项。
116
+ v0.6:多 goal session 投影(`cyberneticsGoals`)——goal 板每次变化把本 session 全部 goal 的
117
+ 整值快照 append 成一条 `plugin:cybernetics/goals` 事件(可跳过的插件记录,宿主持久化读取安全),
118
+ 服务端纯折叠、宿主整值推送,「目标」页签逐条渲染并就地更新;空板回退到 dsh-goal 单 goal 渲染。
119
+ **执行者适配层**:`executor: 'ephemeral' | 'persistent'`(与 depth 正交)——persistent 用 Agent Teams
120
+ 持久 teammate 执行 task 工作(首轮 spawn fresh,后续轮 sendMessage 投递委托书修订,`subagent/end`
121
+ 捕获输出);quick 由输出契约派生 verdict,full 把报告作为 `taskReport` 交给 round.js,verify/triage
122
+ 仍以临时 agent 独立运行。审计日志记录 `executor`,goal 板记录 `memberName`。
123
+ v0.6 追加:**只新增目标模型(append-only goals)**——goal 一经存在不再原地修改。`action: 'revise'`
124
+ (必填 `reason`,可选 `objective`/`title`/`depth`/`executor`/`abortPrevious`)创建一个带
125
+ `parentGoalId` 的新 goal 并立即启动其第 1 轮,旧 goal 置 `superseded` + `supersededBy` 保留最终状态;
126
+ 已完成的 goal 同样走这条路径。`abortPrevious` 默认 false(旧轮跑完由 session agent 决定是否打断),
127
+ true 时复用 cancel 路径。每次修订追加 `.cybernetics/revisions-<新goalId>.jsonl`。页签按两段排序
128
+ (未完成在前、已完成 met/cancelled/superseded 在后,组内 `updatedAt` 倒序;服务端排序、客户端不重排),
129
+ 并显示「已被 <新标题> 取代」/「修订自 <旧标题>」;`GoalStatus` 增加 `superseded`。
130
+ v0.6 追加:**板持久化与 rehydrate**——controller 的 goal 板在每个写点整值快照到
131
+ `<workspace>/.cybernetics/board-<sessionId>.json`(`{version, sessionId, savedAt, goals}`,
132
+ 同目录临时文件 + rename 原子替换,失败不阻断);重启后首次工具调用拿到空内存板时从快照恢复
133
+ (缺失/损坏/版本不符/非法记录一律空板,不抛错)并立即重推一次 `cyberneticsGoals` 投影,
134
+ 「目标」页签因此不再在重启后清空。同一版「控制」页签新增每个 goal 的执行者行
135
+ (状态 + 标题 + executor 临时/持久 + 持久 teammate 名称 + 轮次,未完成在前、已完成在后);
136
+ `GoalView` 增加可选 `memberName`,投影折叠保留。快照是当前状态而非事件日志,`updatedAt` 不持久化。
137
+ v0.5:双档轮次(depth)——轻量分析 quick 单 agent 一轮直接反馈;复杂分析与变更走完整工作-验证循环。
138
+ v0.4.1:硬门增加 allowlist 模式(只保留读+编排工具,挡住 farm_unregister 类长尾变更工具)。
139
+ v0.4:可选硬门(`hardGate: true`)——装配期从顶层 session agent 物理摘除执行工具
140
+ (默认 bash/edit/write),编排从劝告变成结构;task agent 不受影响。
18
141
  v0.3:多 goal 板 + 编排者协议 + 异步轮(`start`/`collect`/`cancel`/`list`)+ task agent 工作委托书(brief)。
19
142
  轮脚本现读(改 `workflows/round.js` 热生效);`src/` 改动需宿主重载。
20
143
  v0.2:session-goal 模式 + 隐式触发 + 角色化 sub agent + 客户端 goal 页签。
@@ -55,6 +178,14 @@ children 必须放进 `props.children`(对照 dsh-client-ui-trajectory 编译
55
178
  否则所有文本静默丢失、元素塌缩成空胶囊。修复后「目标 / 控制」两页签均完整
56
179
  渲染(2026-11-02:goal 徽章/objective/轮次 + 1/11 agent 树与各自 goal 状态)。
57
180
 
181
+ 第五次实测修正(改名后的回归):`@mars.liu` 改名把包名从 `dsh-cybernetics` 变成
182
+ `@mars.liu/dsh-cybernetics`,loader 行名同步了,但 `lib/client.js` 里
183
+ `__ModuleLoader__.load({ id })` 仍是短名 → boot 报
184
+ `could not load "@mars.liu/dsh-cybernetics": … loaded without registering`。
185
+ 规则:客户端 bundle 的注册 id 必须等于 package.json 的 `name`(client-modules
186
+ boot graph 以包名为行键,`dsh.client.inject` 里的依赖同样按包名解析);短名只用于
187
+ loader 行 id(`- id: dsh-cybernetics`)。离线测试已加断言锁死这条不变量。
188
+
58
189
  ## 与 dsh 现有机制的关系
59
190
 
60
191
  - goal:不替代——dsh-goal 仍是 durable setpoint 与续跑驱动;本插件是它的工作回路。
package/cordis.patch.yml CHANGED
@@ -21,4 +21,21 @@
21
21
  # mounting the whole plugin inside preset-standard silenced the client scan.
22
22
  - insert:
23
23
  - id: dsh-cybernetics
24
- name: dsh-cybernetics
24
+ # The client-modules scan matches the loader entry name against the
25
+ # package manifest name; after the @mars.liu rename the two must agree
26
+ # or the browser half is silently skipped (tabs disappear).
27
+ name: '@mars.liu/dsh-cybernetics'
28
+ config:
29
+ # v0.4 hard gate: restrict top-level session agents to read/orchestration
30
+ # tools at prompt-assembly time — every CHANGE must go through a
31
+ # cybernetics goal + task agent. Task/verify/triage subagents unaffected.
32
+ hardGate: true
33
+ # allowlist mode: keep ONLY reads + conversation + orchestration verbs
34
+ # (built-in DEFAULT_GATE_ALLOWLIST); robust against the long tail of
35
+ # mutation tools (farm_*, taskboard_*, knowledge_save, ...).
36
+ gateMode: allowlist
37
+ # v0.6 step 0: keep the team verbs so the session agent can breed a
38
+ # persistent analyst teammate (context:'fresh') for multi-turn analysis.
39
+ gateAllowlistExtra: [spawn_teammate, send_message, interrupt_agent]
40
+ # gateAllowlist: [...] # override the built-in survival set
41
+ # gateMode: denylist + gatedTools: [bash, edit, write] # legacy stance
@@ -0,0 +1,79 @@
1
+ # Issue 草稿 / Issue Draft
2
+
3
+ > 本文件为可直接提交至上游仓库 DeepSeek-AI/deepseek-harness 的 issue 草稿。
4
+ > Title(英文)见下;正文以中文为主,附英文摘要。
5
+
6
+ ---
7
+
8
+ ## Title / 标题
9
+
10
+ **[EN]** Bug: Persistent SSE/long-polling event streams from client plugins exhaust the browser's per-host HTTP/1.1 connection pool (~6 connections), leaving new API requests (e.g. `/api/pluginManager/listBundles`) queued indefinitely and the page unresponsive
11
+
12
+ **[ZH]** 各插件常驻 SSE/长轮询事件流占满浏览器对同一 host 的 HTTP/1.1 连接池(约 6 条),导致新请求(如 `/api/pluginManager/listBundles`)无限排队、页面假死
13
+
14
+ ---
15
+
16
+ ## Summary / 摘要(EN)
17
+
18
+ When multiple DSH web tabs are open against the same host, the browser's per-origin HTTP/1.1 connection limit (~6 sockets in Chrome) is saturated by resident SSE / long-polling event streams opened by client plugins (plus dsh-farm's 5s polling). New requests — e.g. `POST /api/pluginManager/listBundles`, `listPlugins`, `listVersionExemptions`, `schedule/list`, `GET /farm/services` — stay *pending* for 15s+ and end with `net::ERR_ABORTED`, while requests from already-established tabs still return 200. The page appears frozen. Server-side investigation rules out a slow host: `listBundles` is fully synchronous and millisecond-scale, and the host main thread is idle (94% in `kevent`).
19
+
20
+ ---
21
+
22
+ ## 正文 / Body(ZH)
23
+
24
+ ### Environment / 环境
25
+
26
+ - dsh `0.2.1-rc.3`
27
+ - OS: macOS
28
+ - Browser: Chrome(HTTP/1.1;宿主 server 当前仅支持 HTTP/1.1,未启用 h2)
29
+ - profile 挂载了大量客户端插件(含多个带常驻 SSE/长轮询事件流的插件,以及 dsh-farm 服务)
30
+ - 同一 host 下多标签页使用
31
+
32
+ ### Steps to Reproduce / 复现步骤
33
+
34
+ 1. 启动 DSH web profile,浏览器打开 DSH web 页面(标签页 A),登录。
35
+ 2. 再开 1–2 个同 host 的 DSH web 标签页(标签页 B、C),保持打开(各标签页的插件事件流与轮询即开始占用连接)。
36
+ 3. 在新标签页中打开插件页(如 plugin manager UI)。
37
+ 4. 打开 DevTools → Network 面板,观察:
38
+ - `POST /api/pluginManager/listBundles`
39
+ - `POST /api/pluginManager/listPlugins`
40
+ - `POST /api/pluginManager/listVersionExemptions`
41
+ - `POST /api/schedule/list`
42
+ - `GET /farm/services`
43
+
44
+ ### Expected vs Actual / 预期与实际
45
+
46
+ - **Expected**:上述 API 在毫秒~秒级返回 200,页面正常渲染、可交互。
47
+ - **Actual**:新标签页中上述请求全部 **pending 15s+** 后以 `net::ERR_ABORTED` 失败;与此同时老标签页对同一批 API 的请求仍正常返回 **200**。页面交互假死(点击、渲染均无响应)。独立复现 3 次现象一致。
48
+
49
+ ### Analysis / 根因分析(附证据)
50
+
51
+ **已排除服务端问题:**
52
+
53
+ 1. 宿主 `listBundles` 实现为全同步逻辑,单次调用仅毫秒级 —— 证据:`dsh-plugin-manager/lib/index.js:1681-1799`。
54
+ 2. macOS `sample` 抓栈显示宿主进程主线程空闲,94% 时间停留在 `kevent`(事件循环等待),不存在主线程阻塞。
55
+ 3. 未登录状态下直接 curl 该接口,**1ms 返回 401** —— 请求根本没被服务端排队,排除网关/路由层阻塞。
56
+
57
+ **真实机制:**
58
+
59
+ - 宿主 server 仅支持 HTTP/1.1(无 h2 多路复用),Chrome 对同一 host 默认仅约 6 条并发 TCP 连接。
60
+ - 每个标签页除 1 条 `/api/remote.mux` WebSocket(WS 不占用 HTTP 连接池)外,各客户端插件还各自打开常驻 **SSE / 长轮询事件流**;dsh-farm 另有**每 5 秒一次**的轮询请求。这些常驻流把 6 条连接持续占满。
61
+ - 于是新标签页发起的普通 API 请求只能在浏览器 socket 池中**无限排队**,直到超时被浏览器 abort(15s+ 后 `net::ERR_ABORTED`),表现为页面假死。这也一并解释了「多标签页导致浏览器失响应」的现象。
62
+
63
+ ### Suggested Fixes / 修复建议(分层)
64
+
65
+ **宿主 / 上游侧:**
66
+
67
+ 1. **事件通道合并**:将各插件独立的 SSE/长轮询事件流迁移到已存在的 `/api/remote.mux` WebSocket 上复用单一条连接(每标签页一条 WS,多路复用所有插件事件),从根本上消除连接池占用。
68
+ 2. **client RPC 加固**:client RPC 增加请求超时与失败重试,避免排队超时后页面无限等待、无恢复路径。
69
+ 3. **dsh-farm 轮询降频**:dsh-farm 的 5 秒轮询改为指数退避,或迁移为 WS 推送。
70
+ 4. **评估启用 HTTP/2**:宿主 server 评估支持 h2,使浏览器对同一 host 多路复用,绕开 HTTP/1.1 的 6 连接限制。
71
+
72
+ **用户侧临时缓解:**
73
+
74
+ - 减少同一 host 下打开的 DSH web 标签页数量;
75
+ - 停用带常驻 SSE/长轮询事件流的插件。
76
+
77
+ ### Additional Context / 补充
78
+
79
+ 以上结论来自对 `0.2.1-rc.3` 的排查:服务端同步实现(`dsh-plugin-manager/lib/index.js:1681-1799`)+ `sample` 主线程空闲证据 + 401 快速返回,三点共同将问题定位在浏览器连接池耗尽,而非宿主处理能力。如需完整排查记录可进一步提供。
@@ -0,0 +1,93 @@
1
+ # /api/pluginManager/listBundles 挂起问题排查报告(cybernetics 本轮,只读调研)
2
+
3
+ 日期:2026-11-03(本地 23:39–00:02)
4
+ 执行者:task agent(dsh-cybernetics 会话,宿主 checkout 只读)
5
+
6
+ ## 结论(一句话)
7
+
8
+ `/api/pluginManager/listBundles` 挂起**不是** listBundles 实现慢或死锁,也**不是**插件页重复调用导致宿主扫描过重;
9
+ 它是**浏览器对 HTTP/1.1 同一 host 的连接池(约 6 条)被大量常驻长连接(WS 之外的 SSE/事件流 + 轮询)占满后,
10
+ 新请求在浏览器侧排队、永远等不到 socket** 的现象。宿主进程主线程空闲、从未收到/处理这些请求。
11
+ 多标签页打开 GUI 时每个标签都会新增一条 `/api/remote.mux` WebSocket + 多条事件流,迅速耗尽 6 条池连接——
12
+ 这正是"多标签页导致浏览器失去响应"和"插件页卡片链路(listBundles/listPlugins)被阻塞"的共同根源。
13
+
14
+ ## 复现(真实环境,证据)
15
+
16
+ - 三次独立复现(三个全新浏览器会话,23:41 / 23:48 / 23:55):带登录态 `POST /api/pluginManager/listBundles`
17
+ 7.6s/15.6s/8.3s 超时,`net::ERR_ABORTED`,**从未收到响应头**(CDP 网络捕获:request state=failed/pending,
18
+ response_body unavailable)。同法 `pluginManager/listPlugins`、`listVersionExemptions`、`schedule/list` 同样挂起。
19
+ - 未登录 `curl -X POST http://127.0.0.1:3080/api/...` → **401,1ms 内返回**(说明 HTTP 服务、路由、admit 全部正常)。
20
+ - 宿主进程 `sample 39225`(端口 3080 监听者):主线程 2260 个采样中 2121 个停在 `kevent`(事件循环空闲等待),
21
+ **没有阻塞**;常驻内存 1.1GB(另一个值得关注的独立问题)。
22
+ - 用户实际打开插件页(/plugins/??…)时,整页请求全部 pending:`/farm/services`(×6,5 秒重试堆积)、
23
+ `/api/pluginManager/listPlugins`、`/api/pluginManager/listBundles`、`/api/schedule/list`、`/api/commands/list`、
24
+ `/api/skills/list`、`/api/credentials/describe`(×3)、`/api/pet/*`、`/api/skin-center/*`、
25
+ `/api/dsh-usage/overview`(20 秒后被页面自身超时中止)——见 CDP capture `listBundles-hang`(116 条请求,~25 条 pending)。
26
+ - `lsof -nP -iTCP:3080`:Chrome 持有 **17 条** ESTABLISHED 连接;`curl -I` 确认服务器为 **HTTP/1.1**(无 h2)。
27
+ Chromium 对 HTTP/1.1 同一 host 的并发连接上限为 6;WS 不占该池,SSE/普通 fetch 占用。
28
+
29
+ ## 代码证据(文件+行,均为只读引用)
30
+
31
+ 1. listBundles 本身是**全同步**方法,不可能自行挂起:
32
+ - `@deepseek-ai/dsh-plugin-manager/lib/index.js:1681`(`listBundles()`,profile 与全局安装 diff 为 SAME)
33
+ - 实现内只有 `readFileSync`/YAML 解析(`lib/index.js:1766-1799` 收尾 `return Promise.resolve(bundles)`);
34
+ 实测量级:80 个依赖的 package.json 共 223KB + 57 个 patch 文件 15KB,单次调用远小于 100ms。
35
+ 2. 分发链路也没有异步等待点(profile 与全局 SAME):
36
+ - `@deepseek-ai/dsh-client-connection/lib/index.js:1045-1062`:`/api` 路由 admit(401 快路径)→ `bridge` →
37
+ `rpcFetchHandler.fetch`(:871-913)→ gateway `dispatchRpc`。
38
+ - `@deepseek-ai/dsh-api-gateway/lib/index.js:961-978`(dispatchRpc/invokeRpc)、`:1190-1230`(prepareInvocation,
39
+ strict 描述符 `invocation:{kind:'direct'}`,见 `dsh-plugin-manager/lib/typert.host.js:580-604`)→ 同步调用业务方法。
40
+ 3. 浏览器侧传输(挂起真正发生的地方):
41
+ - `@deepseek-ai/dsh-client-connection/lib/client.browser.js:1209-1237`(`createWebConnectionRpc.call`):
42
+ 普通 `fetch(POST /api/<endpoint>)`,**无超时**;请求发不出去(池排队)时 promise 永远 pending。
43
+ - WS 通道 `/api/remote.mux` 定义于 `dsh-api-gateway/lib/index.js:133`;浏览器每页一条
44
+ (`dsh-api-gateway/lib/client.browser.js:1630`),WS 不占 HTTP 池,但每个标签的 SSE/事件流占用。
45
+ 4. 放大器(插件侧):
46
+ - `dsh-farm/client.js:209`:`setInterval(refresh, 5000)` —— 每个挂载了 farm 面板的标签每 5 秒打一次
47
+ `/farm/services`;请求排队后,每个 tick 再堆一条 pending(捕获中同一 tab 堆了 6 条)。
48
+ - 插件页自身的读组逻辑 `@linxin666/dsh-client-ui-plugin-manager/lib/client.js:635`(readGroup 并发调
49
+ listPlugins + listBundles)本身频率合理,不是问题源头。
50
+
51
+ ## 与委托书预期的偏差
52
+
53
+ - 委托书假设"listBundles 读多少文件/扫多少目录/等待锁导致挂起"——已排除:实现同步、量级小、
54
+ 宿主主线程空闲、且同时段 `/farm/services` 等非 RPC 路由同样挂起。
55
+ - 委托书假设"SSE 资源耗尽"——**方向正确**:耗尽的是浏览器每 host 6 条的 HTTP/1.1 连接池,
56
+ 被 GUI 各标签的常驻事件流(以及每标签 1 条 WS 之外的 SSE 类流)占满。
57
+ - 与并行 goal cyb-30cba954 的关系:其结论"listBundles 挂起阻塞插件页"成立,但归因(宿主实现)
58
+ 需修正为浏览器连接池饥饿;其改动(cordis.patch.yml / lib/client.js,dsh-cybernetics 仓库)不受影响。
59
+
60
+ ## 修复建议分层
61
+
62
+ ### A. 用户侧(立即生效,无需改码)
63
+ - 关闭多余的 GUI 标签页(尤其是停留在插件页/看板页的后台标签),挂起的 listBundles 立即恢复返回。
64
+
65
+ ### B. 插件侧(可落地,但不在本轮可写仓库内)
66
+ 本轮可写仓库为 `/Users/mars/jobs/dsh-cybernetics`(dsh-cybernetics),其 client 不发起任何常驻 HTTP 流
67
+ (`lib/client.js` 只使用宿主提供的 faces),**无该问题的可落地修复点** —— 按验收项 2 如实报告"本仓库无可行插件侧方案"。
68
+ 真正值得改的插件(均在只读 profile 内,属下一轮或对应仓库任务):
69
+ - `dsh-farm/client.js:209`:把固定 5s 轮询改为事件驱动/指数退避 + `document.visibilityState==='hidden'` 时暂停;
70
+ - 各自维护 SSE/长轮询的插件(taskboard events、convmap、pet 等)合并为共享一条流或迁移到 `/api/remote.mux` WS 复用;
71
+ - `@linxin666/dsh-client-ui-plugin-manager`:对 readGroup 结果做 30-60s 内存缓存,失败重试加退避,
72
+ 避免排队期间反复堆叠请求。
73
+
74
+ ### C. 宿主/上游侧(建议补丁位置,未修改)
75
+ - 核心:减少长连接对浏览器 6-socket 池的占用。`@deepseek-ai/dsh-api-gateway/lib/index.js:133`
76
+ 已有 WS mux(`/api/remote.mux`)承载 Typert 流;建议上游把插件自定义事件通道统一迁移到该 mux
77
+ 的逻辑流(open/item/end/cancel 帧协议已在 `stream-protocol` 实现),或提供官方 SSE→WS 复用桥。
78
+ - 次要:`@deepseek-ai/dsh-client-connection/lib/client.browser.js:1209` 的 `createWebConnectionRpc.call`
79
+ 增加可配置超时(如 AbortSignal.timeout(30s)),把"永远 pending"变成可见错误;
80
+ `dsh-host-webserver` 可考虑响应头提示或文档说明 HTTP/1.1 单 host 限制。
81
+ - 观察项:宿主进程 RSS 1.1GB(sample 输出 Physical footprint),建议上游加内存基线告警(独立问题)。
82
+
83
+ ## 验证方式(可复现命令)
84
+ 1. `curl -o /dev/null -w '%{http_code} %{time_total}\n' -X POST -H 'content-type: application/json' -d '{}' http://127.0.0.1:3080/api/pluginManager/listBundles` → `401 0.001`(服务正常)。
85
+ 2. `sample <node-pid> 3` → 主线程 kevent 空闲(服务未阻塞)。
86
+ 3. 登录态浏览器 `fetch('/api/pluginManager/listBundles', {method:'POST', ...})` + AbortController(8s) → 挂起;
87
+ 同时 `lsof -nP -iTCP:3080` 数 Chrome ESTABLISHED ≈17(HTTP/1.1,池 6)。
88
+ 4. CDP 网络捕获:新开插件页后 `/api/pluginManager/listBundles`、`/api/schedule/list`、`/farm/services` 全部 pending。
89
+
90
+ ## 风险与残余不确定
91
+ - "池排队"是从浏览器外部可观测行为(请求未送达 + 服务空闲 + 旧连接正常 + HTTP/1.1 + 连接数)反推的最优解释;
92
+ 如需 100% 确证,可用 chrome://net-export 抓一次 net-log(需要用户配合,本轮未做)。
93
+ - 未修改任何文件;未运行测试(纯只读调研轮,无代码变更故无测试项)。