Files
apes-sidebar/docs/feedback-0.2.0-rc2-contract-gaps.md
T
figmar aa8f417aa3
CI / ci (pull_request) Canceled after 0s
CI / ci-windows (pull_request) Canceled after 0s
CI / plugin-mount (pull_request) Canceled after 0s
fix(settings): use the host's @deepseek-ai/schemastery so volatile prefs apply
Every side-card switch was dead: settings.update returned OK and
cordis.patch.yml was written, yet the running plugin never saw the new value
and nothing reported an error.

Root cause: the plugin declared Config with the plain npm `schemastery`
(3.18.0), which has no volatile support. The host resolves plugin config via
`runtime.Config['~standard'].validate()` — i.e. the plugin's OWN schemastery —
so `meta.volatile` nodes were never wrapped in cosmokit volatile references.
`cordis-plugin-loader`'s `_commitVolatile()` bails out as soon as
`volatileEntries(fiber.config)` is empty, so every volatile-only edit was
silently dropped: no in-place update, no plugin restart.

Fix: import z from '@deepseek-ai/schemastery' (peer provided by the host at
mount; pinned 3.18.4 as a devDependency for the dev tree), matching the
repo's existing pnpm-workspace convention for @deepseek-ai/* peers.

Verified: probe reports tabsIsVolatile=true and revision climbing 0->6 across
six writes, each read-back matching the written value (previously the value
was frozen and revision stuck at 1).

Also in this change:
- apes tab ids lose their colon (apes:tasks -> apesTasks, ...): the host's
  settings projection dropped map keys containing a colon.
- sidebar prefs ns follows the profile entry id (`better-sidebar`).
- subagent rows degrade to summary rows when the root catalog never hydrates.
- docs/feedback-0.2.0-rc2-contract-gaps.md: four host contract gaps, gap 4
  documenting the silent volatile-write drop above.

Gates: typecheck 0, lint 0, build 0, test 131 files / 1385 passed.
2026-10-13 22:27:19 +08:00

9.9 KiB
Raw Blame History

DSH 0.2.0-rc.2 插件契约缺口反馈(dsh-apes-sidebar)

反馈人:Hermes@mac13 日期:2026-10-13 对象:figmar(DSH 宿主团队) 来源:dsh-apes-sidebar feat/apes-tab-revival 分支适配 0.2.0-rc.2 时发现的四个契约缺口


背景

dsh-apes-sidebar(better-sidebar 的 fork)在适配 DSH 0.2.0-rc.2 时,遇到四个宿主侧 API/契约缺口,目前均以插件侧 workaround 绕过,但根因在宿主。其中缺口 4 是「设置开关点了不生效」的真根因,代价最大、也最隐蔽(静默失败、无任何报错)。以下逐项说明现象、根因、当前 workaround、以及期望宿主提供的正式契约。


缺口 1:settings 契约——移除 settings.register() 后,插件自定义 prefs schema 无注册通道

现象

  • 0.2.0-rc.2 起 settings.register() 被移除,改由 loader 自动注册插件导出的 Config schema,ns = profile 条目的 id(不是包名;dsh-apes-sidebar 这个 ns 注册不上,报 No configurable plugin entry "dsh-apes-sidebar")。
  • settings.update(ns, patch, expectedRevision) 只放行 schema 里标了 volatile 的字段(dsh-settings 的 write() → validatePaths → isVolatilePath,非 volatile 路径抛 Config field "<path>" is not volatile)。
  • 插件的用户偏好(tabsEnabled / autoOpenSubagent 等)原本定义在独立的 PrefsSchema,而 loader 只注册 Config,导致 PrefsSchema 成为死代码、从未被宿主认识。

当前 workaround(插件侧)

  • 把用户偏好字段合并进 Config(真正被 loader 注册的 schema),每个字段标 .extra('volatile', true);部署字段(readLimit / shell 等)保持非 volatile。
  • 条目 id 与 settings ns 对齐为 better-sidebar。

期望宿主提供的契约

  1. 明确文档:0.2.0-rc.2 起插件如何注册「用户偏好 schema」+ 如何声明字段 volatile(目前只能读 dsh-settings / cosmokit / schemastery 源码逆向)。
  2. ns 解析规则文档化:settings ns 取的是 profile 条目 id 而非包名,这一条没有任何文档,是 No configurable plugin entry 的唯一原因。
  3. volatile 语义边界:volatile 字段的约束("fixed object path without an enclosing volatile field")需文档化——尤其 z.dict 节点标 volatile 后,dict 内任意 key 是否都可写(实测可以,但无文档背书)。

缺口 2:SidebarSessionsService 缺 openSubagent(address) 方法

现象

  • 子代理(subagent)拓扑图里,点击一个子代理行应切到子会话。
  • better-sidebar 原版的 openChild 调用 sessions.openSubagent(address)(address 含 childSessionId + parentSessionId)。
  • DSH 0.2.0-rc.2 的 SidebarSessionsService 没有 openSubagent 方法(context-types.ts 声明为可选 openSubagent?,但宿主 runtime 未实现/未暴露)。
  • 结果:点击子代理行无反应("点不进去")。

当前 workaround(插件侧)

  • openChild 降级:sessions.openSubagent(address) 优先;宿主未暴露时回退 sessions.open(address.childSessionId)。
  • sessions.open(id) 语义是「选中一个已列会话为当前会话」,子会话在 sessions.list 里,选中即落地到子会话布局。

期望宿主提供的契约

  1. 实现并暴露 openSubagent(address):比 sessions.open(childSessionId) 更精确(带 parent 上下文,可区分同 id 不同 parent 的子会话)。
  2. 或明确文档:若 sessions.open(childSessionId) 就是推荐路径,请在 SidebarSessionsService 的 JSDoc 注明,避免插件按 openSubagent 契约写代码后静默失效。

缺口 3:subagentsByParent 对 root 会话不 hydrate

现象

  • 子代理 catalog(subagentsByParent)对 root 会话不 hydrate,rootCatalog 恒为 undefined。
  • 宿主日志有 warning:[agent-registry] agent/disposed listener threw: TypeError Cannot read properties of undefined (reading 'catch'),疑打断 hydrate 流程。
  • 结果:任务管理 / 子 Agent tab 的子代理全部显示「加载中…」,永不 hydrate。

当前 workaround(插件侧)

  • 用 sessions.list(byId 镜像)+ synthesizeCatalog(byId, parentId) 按原版 SidebarSubagentChildEntry 契约合成 catalog(label 回退 summary.displayTitle,mode/activity/hasChildren 推断),递归补缺后再喂原版 CatalogRows。
  • 已生效(行可显示),但合成数据缺少宿主实时 activity/hasChildren,不如宿主 hydrate 精确。

期望宿主提供的契约

  1. 修复 agent/disposed listener 的 catch TypeError:该异常疑打断 subagentsByParent 的 hydrate。
  2. 或提供降级数据源:提供 subagents.listByParent(rootId) 返回 summary 级数据,让插件能显示「已知子代理 + 基础元数据」而非恒「加载中」。

缺口 4(真根因):插件 Config 必须用 @deepseek-ai/schemastery,否则 volatile 写入被 loader 静默丢弃

现象

  • 设置页的每个开关点击后:不报错、无 toast、但状态立刻弹回,即「开关都不能开启或关闭」,且开关对运行时毫无作用。
  • 服务端 settings.update(ns, patch, revision) 返回成功,profile 文档 cordis.patch.yml 也确实被写入(例如 tabsEnabled.git: false 落盘成功)。
  • 但读回(settings.describe().value)与插件自身 config 永远不变,且插件既不被重启、也不被就地更新。

根因链(逐层定位,全部有宿主源码/运行时证据)

  1. 宿主 dsh-settings 的 write() 落盘后,靠 configEditor.edit() → reconcileProfilePatches() → 最终回到 cordis-plugin-loader 的条目 update()。
  2. loader 判定 volatileOnly(本次改动只涉及 volatile 字段)后走 _commitVolatile(),其第一行是: const refs = volatileEntries(fiber.config); if (!refs.length) return true;
  3. fiber.config 里的 volatile 字段应当是 createVolatile() 产生的引用对象(形如 Object.freeze({ get(), [Symbol.for('cosmokit.volatile.write')]() })),而 volatileEntries() 只认这种引用。
  4. 这些引用由 schemastery 的 Schema.resolve() 在遇到 meta.volatile 节点时创建(@deepseek-ai/schemastery 的 resolve() 有该分支;公版 schemastery 没有)。
  5. 宿主 cordis 的 resolveConfig() 走的是 runtime.Config["~standard"].validate() —— 也就是插件自己那份 schemastery 的解析器。
  6. 若插件声明的是 npm 公版 schemastery(3.18.0,volatile 相关代码 0 处),则解析结果里没有任何 volatile 引用 → refs.length === 0 → _commitVolatile() 立即 return true → loader 视作「已处理」→ 既不就地更新也不重启 → 写入被彻底丢弃。
  7. 插件侧实测确认:修复前 config.tabsEnabled 的 write 符号为 undefined(tabsIsVolatile=false);改用宿主同源包后变为 function(tabsIsVolatile=true),同一套探针下 revision 由恒为 1变为单调递增 0→6,每次写入立即读回正确值。

当前 workaround(插件侧)

  • src/config.ts:import z from 'schemastery' → import z from '@deepseek-ai/schemastery'。
  • package.json:dependencies.schemastery 移除;peerDependencies 加 "@deepseek-ai/schemastery": "^3.18.1"(宿主在 mount 时提供,与本 repo pnpm-workspace.yaml 的既有约定一致);devDependencies 钉 3.18.4 供开发树解析。
  • 已验证:tabsIsVolatile=true、rev 单调递增、写入即时读回、门禁全绿(typecheck/lint/build/test 131 files 1385 passed)。

期望宿主提供的契约

  1. 文档化:「声明 volatile 字段的插件必须从 @deepseek-ai/schemastery 导入 schemastery」——这是强制要求(宿主的 volatile 能力是 schemastery 的 fork 特性),但当前无任何文档或校验。
  2. 启动期校验(强烈建议):loader 加载插件时,若插件 Config 含 meta.volatile 节点,但解析后 fiber.config 里不存在任何 volatile 引用,应报错或 warn。现状是「静默丢弃每一次用户设置写入」,排查成本极高(需读 4 个包的源码才能定位)。
  3. 或消除版本耦合:让 resolveConfig 使用宿主自己的 schemastery 解析器,而不是插件携带的那一份,从根上避免此类版本错配。

汇总

# 缺口 严重度 当前 workaround 期望宿主动作
1 settings 契约(ns = 条目 id、volatile 无文档) 中(已绕过) 偏好合并进 Config + .extra('volatile',true) + ns 对齐条目 id 文档化 ns 解析规则与 volatile 声明
2 缺 openSubagent(address) 高(影响核心交互) 降级 sessions.open(childSessionId) 实现 openSubagent 或文档化推荐路径
3 subagentsByParent root 不 hydrate + catch TypeError 高(恒「加载中」) 合成 catalog 绕过 修 TypeError 或提供降级数据源
4 插件 Config 未用 @deepseek-ai/schemastery → volatile 写入被静默丢弃 最高(设置全部失效且无报错) 改用宿主同源 schemastery 包 文档化强制要求 + 启动期校验 + 解耦解析器

复现环境

  • DSH Desktop 2.0.17(Electron),宿主内置 @deepseek-ai/dsh = 0.2.0-rc.2、@deepseek-ai/schemastery = 3.18.4
  • dsh-apes-sidebar feat/apes-tab-revival(+ 本反馈对应的改动)
  • macOS 13.7.8,Node v22.22.0
  • 插件加载点:~/.dsh/profiles/desktop/node_modules/dsh-apes-sidebar/lib/
  • 关键源码锚点:@deepseek-ai/cordis/lib/index.js: resolveConfig()(~standard.validate)、@deepseek-ai/cordis-plugin-loader/lib/index.js: _commitVolatile()、@deepseek-ai/schemastery/lib/index.cjs: Schema.resolve()、@deepseek-ai/dsh-settings/lib/index.js: describe()/write()