Merge remote-tracking branch 'origin/master' into persistence-schema-digest-keys

# Conflicts:
#	docs/persistence-schema.json
This commit is contained in:
turtle1999 committed 2026-10-20 12:24:48 +08:00
commit 23a3ba3039
377 files changed
+11839 -1259

No files matched your search

@@ -0,0 +1,2 @@
2026-10-14-built-in-plugin-feature-previews.md: ae9b32340d9b1e77a99465445a05ccee05df0e79
2026-10-14-built-in-plugin-feature-previews.zh.md: 9229b0835a23a0eefffa1bd157efece333aebfa9
@@ -1,6 +1,7 @@
# Agent Note: Built-in plugin feature previews
Status: implemented
Archived: 2026-10-19
English | [中文](2026-10-14-built-in-plugin-feature-previews.zh.md)
@@ -1,6 +1,7 @@
# Agent Note: 内置插件功能配图
Status: implemented
Archived: 2026-10-19
[English](2026-10-14-built-in-plugin-feature-previews.md) | 中文
+3
View File
@@ -2020,6 +2020,9 @@
"feature/2026-09-28-desktop-login-shell-environment.i18n.yaml": "sha256:6129edeb0a7ecf087911193ca5a15223a730ad01e30d2ead7739d01f589fafa7",
"feature/2026-09-28-desktop-login-shell-environment.md": "sha256:cd133b1ef1a178828d30b9db176a91597eedf9e67802551356f34d3e0f80206d",
"feature/2026-09-28-desktop-login-shell-environment.zh.md": "sha256:1422667a44aacc51736f31b328a8e171a39cb6b5168d6993e980e4324b23b98f",
"feature/2026-10-14-built-in-plugin-feature-previews.i18n.yaml": "sha256:4769fd7fa4881a38fa8e2682b2ea62ed5a5ec6588e87f225c2b106e1bd308200",
"feature/2026-10-14-built-in-plugin-feature-previews.md": "sha256:093e00da4cebf732861c1e5c47465fb00359eb89a86dc212f3f90b43f9b80cec",
"feature/2026-10-14-built-in-plugin-feature-previews.zh.md": "sha256:56adb70945f68b073357c437bbec9e69f110587cf302291f2f135d9fcb5537e7",
"process/2026-06-11-doc-sync-enforcement.i18n.yaml": "sha256:33b6d5874427bd7a2bd82e7e2f4f482b12448b2464aef15a9c57975edb48554d",
"process/2026-06-11-doc-sync-enforcement.md": "sha256:aa2fe83d519fc30d48dff19e596e83c8922aacc9e063e14fe2cc35b769b9100e",
"process/2026-06-11-doc-sync-enforcement.zh.md": "sha256:698017bd35f030fdea3eac51df9e43138c48140f504739d687b7251d13fced2b",
@@ -21,8 +21,8 @@
en: 0a4240d88cca240a
zh: 8f5f1527caf571cd
/agent-note-client-plugin-loading-lazy-factories-cordis-lifecycle-and-hot-reload/decision/the-loading-flow-end-to-end:
en: 7dac236d6add3c3c
zh: 2a43a923a0646bb0
en: 15fb56befd328339
zh: 0ffb10ada7e52805
/agent-note-client-plugin-loading-lazy-factories-cordis-lifecycle-and-hot-reload/decision/live-graph-reconciliation:
en: 37e2fa1fe5cb2b95
zh: 80e513db87709cb4
@@ -33,8 +33,8 @@
en: 94ca0d09e6fd1e3a
zh: d5949a2bdec795ba
/agent-note-client-plugin-loading-lazy-factories-cordis-lifecycle-and-hot-reload/consequences:
en: 6a96d9ac06c222e5
zh: b2814ab58348e831
en: 16e1f2467c559315
zh: a714ec6ff2722cf1
/agent-note-client-plugin-loading-lazy-factories-cordis-lifecycle-and-hot-reload/alternatives-considered:
en: b8f036f50cff7cd8
zh: 6d623c4a1f4314b1
@@ -56,7 +56,7 @@ What happens between `dsh web` starting and the UI appearing? Three stages: the
1. The composing app (`apps/cli`) ships the roster as ordinary rows in its `cordis.yml` config tree — client plugin packages are entry rows like every host plugin, including the always-mounted `client-hmr` row. `auditStartupEntries` reports failed imports, activation errors with their original stacks, and pending dependencies; optional entries warn and required entries reject startup ([startup policy](2026-09-09-consumer-owned-startup-strictness.md)).
2. The `dsh-client-modules` node half (the package is dual-face: its browser half is the module table) resolves each live Loader entry through the same `name` and owning-tree `baseUrl` inputs that imported its Host face, then reads the nearest owning package.json `dsh.client` declaration and composes `window.__DSH_BOOT__`: `{ rev, entries: [{ id, url, rev, inject?, immediately?, external? }], batches: [{ phase, url, rev, entries }] }`. The manifest package name is the browser module identity even when an overlay names a relative source or built entry file. Distinct active Loader sources resolving to one package name fail composition; after one source unloads, the surviving source supplies the row without a fiber restart. The row's three optional fields come from manifests, never hand-copied. Composition orders requested dynamic rows before their consumers, rejects synchronous request cycles, and assigns every row to exactly one initial batch. It refuses declared plugins without built `./client` bundles and groups their package/path rows under one required source-build instruction; malformed declaration fields also fail activation, and the Host audit reports either error from the FAILED fiber.
3. Scanning is incremental per package — there is no full-rescan code path. Each cordis `internal/plugin` emission marks the fiber's entry name dirty (entry-less fibers drop O(1)); a microtask flush reconciles each dirty name against live loader entries, with package metadata (including the negative "not a client package" verdict) cached per entry name and owning-tree base URL for the process lifetime and changed bundle publication reachable only through `rebuilt(id)`. The activation pass seeds the same dirty set from current entries and flushes synchronously, so first scan and steady state share one implementation. Initial rows hash their pre-read filesystem metadata; startup combo revisions hash the ordered row identities and revisions, and the rows plus batch descriptors hash into `graph.rev`. The graph types are single-sourced in the modules package's `./client` export — the webserver knows nothing about the graph, while modules registers the combo route and contributes structured index-injection rows.
3. Scanning is incremental per package — there is no full-rescan code path. Each cordis `internal/plugin` emission marks the fiber's entry name dirty (entry-less fibers drop O(1)); a microtask flush reconciles each dirty name against live loader entries, rereading the package manifest and bundle metadata on each reconciliation, so an entry restart publishes a changed declaration or bundle and an unchanged one publishes nothing; a bundle rebuilt without an entry restart is published through `rebuilt(id)` ([package asset decision](2026-10-19-client-plugin-package-assets.md)). The activation pass seeds the same dirty set from current entries and flushes synchronously, so first scan and steady state share one implementation. Initial rows hash their pre-read filesystem metadata; startup combo revisions hash the ordered row identities and revisions, and the rows plus batch descriptors hash into `graph.rev`. The graph types are single-sourced in the modules package's `./client` export — the webserver knows nothing about the graph, while modules registers the combo route and contributes structured index-injection rows.
Why is the roster yml rows and not a scan? Because which plugins compose into a deployment is a composition decision, not a package property — a package declaring `dsh.client` in the repo does not mean this deployment mounts it, so discovery-by-scan cannot make that call; the node half scans only what the tree actually mounted.
@@ -102,7 +102,7 @@ The current package inventory and build forms live in the [client shell layering
## Consequences
One governance implementation runs on both sides of the wire; the browser-specific layer is one module system plus one reload plugin. Dynamic packages have one artifact form, so the purity check covers them all. Cordis dependencies, module requests, and the boot tier live with their owners — the manifests — while the composing app holds only the roster. Host graph validation and recursive request arrival keep synchronous factory dependencies explicit. Browser-native script loading preserves the standard mapping among plugin network resources, generated bundles, and TypeScript/TSX sources, while the module system keeps only one replaceable `loadBundle` hook.
One governance implementation runs on both sides of the wire; the browser-specific layer is one module system plus one reload plugin. Dynamic packages have one artifact form, so the purity check covers them all. Cordis dependencies, module requests, and the boot tier live with their owners — the manifests — while the composing app holds only the roster. Host graph validation and recursive request arrival keep synchronous factory dependencies explicit. Browser-native script loading preserves the standard mapping among plugin network resources, generated bundles, and TypeScript/TSX sources, while the module system keeps two replaceable carrier hooks: `loadBundle` for bundle bytes and `resolveAsset` for package-asset references.
Costs accepted: the vendored Loader carries idle machinery in the browser (EntryTree persistence is a no-op, groups/isolation unused); every plugin edit in dev pays a bundle rebuild plus fiber remount; graph `inject` rows guide factory arrival but service availability remains the activation authority, so a mismatch appears at the settled sweep; the static UI libraries keep direct value exports; every bundle gains a source-map artifact; and external-script failures provide only coarse URL diagnostics instead of the HTTP status available to an explicit fetch. The Host retains bundle snapshots and lazy response plans for the current graph and one prior startup generation. Script and map bodies are cached after their first `GET`, so memory grows with bundle snapshots and requested response bodies. Each materialized response stays fixed at its URL; a prior-generation map first requested after a rebuild reads the current map file.
@@ -56,7 +56,7 @@ Host 会快照每个已构建插件入口 bundle,并把每个调度阶段的
1. 负责组合的 app(`apps/cli`)把名册作为普通行放进它的 `cordis.yml` 配置树——client 插件包与每个 host 插件一样是 entry 行,包括无条件挂载的 `client-hmr` 行。`auditStartupEntries` 报告 import 失败、带原始 stack 的激活错误,以及待满足的依赖;optional entry 输出 warning,required entry 则使启动失败([启动策略](2026-09-09-consumer-owned-startup-strictness.zh.md))。
2. `dsh-client-modules` 的 node 半(该包是双面的:浏览器半就是模块表)使用 Host face import 时相同的 `name` 与所属 tree `baseUrl` 解析每个 live Loader entry,再读取最近归属 package.json 的 `dsh.client` 声明并组合出 `window.__DSH_BOOT__`:`{ rev, entries: [{ id, url, rev, inject?, immediately?, external? }], batches: [{ phase, url, rev, entries }] }`。即使 overlay 指向相对的 source 或 built entry 文件,manifest 包名仍是浏览器模块身份。若不同的 active Loader source 解析到同一包名,组合会失败;一个来源卸载后,仍存活的来源无需重启 fiber 即可提供该 row。Row 的三个可选字段都来自 manifest,永不人肉抄写。组合会把被请求的动态图 row 排到消费者之前、拒绝同步请求环,并把每个 row 恰好分配给一个初始批次。它会拒绝没有已构建 `./client` bundle 的已声明插件,并把它们的 package/path 行归到一条源码构建要求下;畸形声明字段同样会让激活失败,Host 检查会从 FAILED fiber 报告这两类错误。
3. 扫描是单包增量——不存在全量重扫代码路径。每次 cordis `internal/plugin` 发射把该 fiber 的 entry 名标脏(无 entry 的 fiber O(1) 丢弃);微任务 flush 把每个脏名对账 live loader entries,包元数据(含「非 client 包」的否定结论)按 entry 名与所属 tree base URL 缓存至进程结束,变化后 bundle 的发布只经 `rebuilt(id)` 可达。激活趟从当前 entries 灌同一脏集合并同步 flush,初扫与稳态共享一条实现。初始 row 对读取前的文件系统元数据求哈希;启动 combo revision 对有序 row 的身份与 revision 求哈希,row 与批次描述再共同哈希进 `graph.rev`。图类型单源在 modules 包的 `./client` 出口——webserver 对图一无所知;modules 会注册 combo 路由并贡献结构化 index 注入行。
3. 扫描是单包增量——不存在全量重扫代码路径。每次 cordis `internal/plugin` 发射把该 fiber 的 entry 名标脏(无 entry 的 fiber O(1) 丢弃);微任务 flush 把每个脏名对账 live loader entries,每次对账都重新读取包 manifest 与 bundle 元数据,因此条目重启会发布变化后的声明或 bundle,未变化时不发布;没有伴随条目重启的 bundle 重建经 `rebuilt(id)` 发布([包资源决策](2026-10-19-client-plugin-package-assets.zh.md))。激活趟从当前 entries 灌同一脏集合并同步 flush,初扫与稳态共享一条实现。初始 row 对读取前的文件系统元数据求哈希;启动 combo revision 对有序 row 的身份与 revision 求哈希,row 与批次描述再共同哈希进 `graph.rev`。图类型单源在 modules 包的 `./client` 出口——webserver 对图一无所知;modules 会注册 combo 路由并贡献结构化 index 注入行。
为什么名册是 yml 行而不是扫描?因为哪些插件组合进一次部署是组合决策,不是包属性——一个在仓库中声明了 dsh.client 的包,不代表这次部署要挂载它,扫描发现无从替人做这个决定;node 半只扫描配置树实际挂载了的东西。
@@ -102,7 +102,7 @@ Bootstrap 替换会在失效或卸载前被拒绝:模块系统保留其初始
## Consequences
Wire 两侧运行同一份治理实现;浏览器特有层只包含一套模块系统和一个重载插件。动态包只有一种产物形态,因此纯度检查覆盖全部动态包。Cordis 依赖、模块请求与启动档位都与其所有者——manifest——同住,负责组合的 app 只握名册。Host graph 校验与递归请求到达使同步 factory 依赖保持显式。浏览器原生 script 装载保留插件网络资源、生成 bundle 与 TypeScript/TSX 源码之间的标准映射,模块系统也只保留一个可替换的 `loadBundle` 钩子。
Wire 两侧运行同一份治理实现;浏览器特有层只包含一套模块系统和一个重载插件。动态包只有一种产物形态,因此纯度检查覆盖全部动态包。Cordis 依赖、模块请求与启动档位都与其所有者——manifest——同住,负责组合的 app 只握名册。Host graph 校验与递归请求到达使同步 factory 依赖保持显式。浏览器原生 script 装载保留插件网络资源、生成 bundle 与 TypeScript/TSX 源码之间的标准映射,模块系统只保留两个可替换的载体钩子:负责 bundle 字节的 `loadBundle` 和负责包资源引用的 `resolveAsset`。
接受的代价:vendored Loader 在浏览器里背着闲置机件(EntryTree 持久化是 no-op,分组/隔离未用);开发期每次修改插件都要付一次 bundle 重建加 fiber 重挂;graph `inject` row 指导 factory 到达,但服务可用性仍是激活权威,因此不匹配会在 settled 扫描时浮出;静态 UI 库保留直接实体导出;每个 bundle 多出一份 sourcemap 产物,外部 script 失败也只能给出粗粒度 URL 诊断,不能像显式 fetch 那样报告 HTTP 状态。Host 为当前图及上一代启动图保留 bundle 快照与惰性响应计划。脚本和 map body 在首次 `GET` 后缓存,因此内存随 bundle 快照和已请求的响应 body 增长。每份已物化响应在其 URL 下保持固定;若上一代 map 在重建后才首次被请求,则会读取当前 map 文件。
@@ -9,11 +9,11 @@
en: 47ba548b869be190
zh: c2e5038a56fe741c
/agent-note-asynchronous-projection-registration-and-retained-readers/decision:
en: 9e56d4bcbdc3a291
zh: bebaaab338a15e40
en: be0d17bfbd1b59d4
zh: 7a23bccdf95c21ff
/agent-note-asynchronous-projection-registration-and-retained-readers/alternatives-considered:
en: ee5b0cd715ed1fef
zh: 6b1478dcde21916d
en: e7c66403fab991d2
zh: 1580ec29116c7d42
/agent-note-asynchronous-projection-registration-and-retained-readers/consequences:
en: 6f0e04ef7924e2c6
zh: e3730273e323440d
en: 31ce6f02dd3bf39d
zh: bcd29b2146f689b5
@@ -10,12 +10,14 @@ Host readers coupled to registry key lookup lose access when a plugin unloads, a
## Decision
`SessionProjectionRegistry.register` returns a promise for a typed handle. Domain owners await it during activation before exposing dependent operations. Function plugins retain their namespace exports and use asynchronous `apply`; services use their initialization hook. Each domain service keeps its definitions and handles internal and exposes methods returning business data. Consumers depend on that service rather than registering its definitions or receiving projection readers. This supersedes the optional first-access acquisition and disposer-return details in [the mandatory-reader decision](2026-08-19-session-projection-mandatory-seam.md), while retaining its mandatory-state and cropped-client-view rules.
`SessionProjectionRegistry.register` returns an awaitable registration for a typed handle. Domain owners await it during activation before exposing dependent operations. Function plugins retain their namespace exports and use asynchronous `apply`; services use their initialization hook. Each domain service keeps its definitions and handles internal and exposes methods returning business data. Consumers depend on that service rather than registering its definitions or receiving projection readers. This supersedes the optional first-access acquisition and disposer-return details in [the mandatory-reader decision](2026-08-19-session-projection-mandatory-seam.md), while retaining its mandatory-state and cropped-client-view rules.
A handle owns one reference to a shared definition and its per-Session cells. Its disposer releases global participation exactly once; the final release removes the unit from live drives, snapshots, and checkpoints. Its reader remains usable after release, folding unseen events with the original definition. A later registration at a different state version does not replace an older handle's definition. Registration checks owner liveness after its asynchronous handoff and rejects if the owner unloaded before receiving the handle.
AgentRegistry owns turn and step state and exposes `turnBoundaryOf(session)`. AgentLoop acquires its inbox handle once before publishing its factory, reads the last turn before creating an Agent scope, and passes an inbox callback plus that turn number into synchronous Agent construction. The callback retains its handle through cleanup after the loop fiber removes global registrations. This preserves the existing creation/setup sequence without per-Agent registration or an extra initialization state.
Plugins compiled against the synchronous API keep running during migration. The awaitable registration is also the original Cordis effect disposer, so calling it releases the registration and a composing effect adopts its ownership; that call is deprecated. Registry-level `stateOf(session, key)` remains a deprecated read returning `unknown`; it loses access after the final release and logs one `ctx.logger.warn` per key per registry. Registration itself does not warn, because an ignored result is indistinguishable from one awaited later. A legacy plugin may never observe the registration, so the registry attaches its own rejection handler: once the current macrotask and its microtasks finish, a rejection nobody observed through `then`, `catch`, or `finally` is logged as an error while the plugin stays active, and one whose owner is no longer loading or active is dropped. TypeScript consumers migrate their declarations; the host state map is not restored.
## Alternatives considered
**Keep registry-level lookup and delay unregistering.** This couples unrelated plugin disposal order to every reader's final access. Retained readers make cleanup independent of that order.
@@ -26,6 +28,12 @@ AgentRegistry owns turn and step state and exposes `turnBoundaryOf(session)`. Ag
**Convert function plugins to classes.** Cordis already awaits promises returned by `apply`. Preserving namespace exports avoids a published entry-point and diagnostic-name break.
**Require every external plugin to migrate immediately.** Compiled plugins fail at runtime on the missing disposer and keyed read, although the projection keys and state fields they read are unchanged.
**Return a new wrapper around the disposer.** Cordis composition adopts effects by their returned identity. A wrapper leaves the original registration owned by the fiber, so owner disposal can release it before the composed cleanup finishes.
**Dispose the plugin after an ignored rejection.** This approximates the old failed activation, but it needs a startup observer, runs only after the plugin's services became visible, and ends in `DISPOSED` rather than a retryable `FAILED`. Logging keeps the failure visible without a second lifecycle path.
## Consequences
Projection owners must migrate acquisition and retain handles; other consumers use the owning services. Released handles retain their definitions and weakly keyed cells for as long as the caller keeps them; they do not restore global wire participation. Registration-reader tests cover post-release folding, version replacement, unrelated-unit isolation, repeated disposal, concurrent owner disposal, and owner unload during acquisition. Agent lifecycle tests cover cleanup failures, scope-minting races, and background child shutdown.
Projection owners must migrate acquisition and retain handles; other consumers use the owning services. Released handles retain their definitions and weakly keyed cells for as long as the caller keeps them; they do not restore global wire participation. Registration-reader tests cover post-release folding, version replacement, unrelated-unit isolation, repeated disposal, concurrent owner disposal, and owner unload during acquisition. Agent lifecycle tests cover cleanup failures, scope-minting races, and background child shutdown. Compatibility tests and a real Loader subprocess verify the deprecated calls.
@@ -10,12 +10,14 @@ Status: implemented
## Decision
`SessionProjectionRegistry.register` 返回类型化句柄的 Promise。领域所有者在激活期间等待它,再提供依赖操作。函数插件保留命名空间导出并使用异步 `apply`;服务使用初始化钩子。领域服务在内部持有定义和句柄,通过方法返回业务数据。消费方依赖该服务,无需注册其定义或接收投影读取句柄。这取代了[必需读取方决策](2026-08-19-session-projection-mandatory-seam.zh.md)中的可选首次访问获取方式与返回释放函数的细节,同时保留必需状态和裁剪 Client 视图的规则。
`SessionProjectionRegistry.register` 返回可等待的注册结果,用于获取类型化句柄。领域所有者在激活期间等待它,再提供依赖操作。函数插件保留命名空间导出并使用异步 `apply`;服务使用初始化钩子。领域服务在内部持有定义和句柄,通过方法返回业务数据。消费方依赖该服务,无需注册其定义或接收投影读取句柄。这取代了[必需读取方决策](2026-08-19-session-projection-mandatory-seam.zh.md)中的可选首次访问获取方式与返回释放函数的细节,同时保留必需状态和裁剪 Client 视图的规则。
句柄拥有共享定义及其逐 Session 单元的一次引用。其释放函数恰好一次地解除全局参与;最后一次释放会从实时推进、快照和检查点中移除单元。读取句柄在释放后仍可使用原定义折叠未读取事件。后续注册的其他状态版本不会替换旧句柄的定义。注册在异步交接后检查 owner 是否存活;如果 owner 在收到句柄前卸载,注册拒绝。
AgentRegistry 持有轮次和步骤状态,并提供 `turnBoundaryOf(session)`。AgentLoop 在发布工厂前只获取一次 inbox 句柄,在创建 Agent 作用域前读取最后轮次,并将 inbox 回调与该轮次编号传入同步 Agent 构造过程。loop fiber 移除全局注册后,回调仍在清理期间保留其句柄。这保留既有创建与 setup 顺序,无需逐 Agent 注册或增加初始化状态。
按同步 API 编译的插件在迁移期间继续运行。可等待的注册结果同时就是原有的 Cordis effect 释放函数:调用它会释放注册,组合 effect 也会接管其所有权;该调用已弃用。注册表级 `stateOf(session, key)` 作为已弃用的读取保留,返回 `unknown`;最后一次释放后它失去访问,并且每个 registry 对每个键输出一次 `ctx.logger.warn`。注册本身不产生警告,因为被忽略的结果与稍后才等待的结果无法区分。旧插件可能从不观察注册结果,因此 registry 挂上自己的拒绝处理函数:当前宏任务及其微任务结束时,没有任何调用方通过 `then`、`catch` 或 `finally` 观察的拒绝会被记录为错误,插件保持激活;owner 已不在加载或激活状态的拒绝会被丢弃。TypeScript 消费方仍需迁移声明;宿主状态映射不再恢复。
## Alternatives considered
**保留注册表级查询并延迟注销。** 这使无关插件的释放顺序依赖每个读取方的最后一次访问。保留读取句柄使清理不再依赖该顺序。
@@ -26,6 +28,12 @@ AgentRegistry 持有轮次和步骤状态,并提供 `turnBoundaryOf(session)`
**将函数插件改为类。** Cordis 已等待 `apply` 返回的 Promise。保留命名空间导出可避免发布入口与诊断名称的破坏性变更。
**要求所有外部插件立即迁移。** 已编译的插件会因缺失的释放函数和按键读取在运行时失败,尽管它们读取的投影键与状态字段都没有变化。
**返回包装释放函数的新函数。** Cordis 组合按返回值身份接管 effect。包装函数会让原注册继续归 fiber 所有,因此 owner 卸载可能在组合清理结束前释放它。
**被忽略的拒绝出现后卸载插件。** 这近似于旧 API 的激活失败,但需要启动观察器,只能在插件服务已经可见后执行,并以 `DISPOSED` 而非可重试的 `FAILED` 结束。记录日志能让失败可见,而无需第二条生命周期路径。
## Consequences
投影所有者必须迁移获取方式并保留句柄;其他消费方使用所属服务。只要调用方持有已释放句柄,其定义和弱键单元就仍存在;句柄不会恢复全局 wire 参与。注册读取测试覆盖释放后折叠、版本替换、无关单元隔离、重复释放、并发 owner 释放以及获取期间 owner 卸载。Agent 生命周期测试覆盖清理失败、作用域创建竞态与后台子 Agent 停止。
投影所有者必须迁移获取方式并保留句柄;其他消费方使用所属服务。只要调用方持有已释放句柄,其定义和弱键单元就仍存在;句柄不会恢复全局 wire 参与。注册读取测试覆盖释放后折叠、版本替换、无关单元隔离、重复释放、并发 owner 释放以及获取期间 owner 卸载。Agent 生命周期测试覆盖清理失败、作用域创建竞态与后台子 Agent 停止。兼容测试与真实 Loader 子进程验证这些已弃用调用。
@@ -12,8 +12,8 @@
en: 972bbe45fe8ac28e
zh: 593b851c142894e1
/agent-note-one-fold-motion-for-collapsing-on-the-next-message/decision/batches:
en: f44b784545c6d9a2
zh: 3eb6fcb9f348dec4
en: 913d1dceab36d4a1
zh: 049521c5127f3478
/agent-note-one-fold-motion-for-collapsing-on-the-next-message/decision/landings:
en: b8278678170f1507
zh: 113a51ba5cf27de1
@@ -38,7 +38,7 @@ Subagent children showed no local echo because, when echoes were introduced, `su
- `finish`: install the final room.
Content added during the batch updates the remaining movement by starting a new segment.
- A row that starts closing or revealing while a batch runs keeps its current size, without a transition of its own, until that batch settles; the next flush drives it in a new batch, or starts a held revealing row's own transition when nothing closes. Rows therefore never change outside a batch while following continues, which would follow the bottom room without the growth that should have consumed it. A batch takes over every member at its current size: a closing row shrinks from the height it has now, and a revealing row grows from the height its own transition already reached, so only the remaining growth enters the landing; a closing row with no size left finishes without animation.
- A row that starts closing or revealing while a batch runs keeps its current size, without a transition of its own, until that batch settles; the next flush drives it in a new batch, or starts a held revealing row's own transition when nothing closes. Rows therefore never change outside a batch while following continues, which would follow the bottom room without the growth that should have consumed it. A batch takes over every member at its current size: a closing row shrinks from the height it has now, and a revealing row grows from the height its own transition already reached, so only the remaining growth enters the landing; a closing row with no size left finishes without animation, and so does a row whose ancestor finished closing, because `hidden="until-found"` keeps that row's own size while removing it from the flow.
- A batch with only revealing rows needs no room and keeps CSS transitions. When the viewport has no reading policy attached, closing rows also keep CSS transitions, and held rows finish immediately; under reduced motion closing rows finish immediately.
- Closing rows commit their hidden state first, the driver then installs the final geometry, and only then are fold-idle listeners notified (for example, clearing room after a display-mode change).
- The generic path is removed: the `reserve` callback, the `pendingReserve` and `pendingGrowth` reservation arithmetic, `reserveBelow`, and the post-fold `followAfterFold`. `reclaimBelow` (returning room after new content or reader scrolling) and `resetBelow` (clearing room after a display-mode change) remain.
@@ -38,7 +38,7 @@ Status: implemented
- `finish`:安装最终补白。
批次中途新增的内容按分段规则更新剩余运动。
- 批次运行期间开始收起或展开的行保持当前尺寸、不走自己的过渡,等当前批次落定后由下一次刷新处理:有收起行时进入新批次,只剩展开行时启动它自己的过渡。因此不会出现「批次之外有行在变化、同时跟随照常进行」的情况;否则跟随会把底部空间整块跟进去,而本该抵扣它的新内容没有被计入。批次按每个成员的当前尺寸接管:收起行从现有高度收缩,展开行从它自己的过渡已达到的高度继续增长,只有剩余的增长量计入落点;已无尺寸的收起行直接完成、不做动画。
- 批次运行期间开始收起或展开的行保持当前尺寸、不走自己的过渡,等当前批次落定后由下一次刷新处理:有收起行时进入新批次,只剩展开行时启动它自己的过渡。因此不会出现「批次之外有行在变化、同时跟随照常进行」的情况;否则跟随会把底部空间整块跟进去,而本该抵扣它的新内容没有被计入。批次按每个成员的当前尺寸接管:收起行从现有高度收缩,展开行从它自己的过渡已达到的高度继续增长,只有剩余的增长量计入落点;已无尺寸的收起行直接完成、不做动画,祖先已收起完成的行同样如此,因为 `hidden="until-found"` 让这类行保留自身尺寸,却已不占文档流。
- 只含展开行的批次不需要补白,仍用 CSS 过渡。viewport 未挂载阅读策略时,收起行也用 CSS 过渡、被挂起的行立即完成;减少动态效果时收起行立即完成。
- 收起行先提交 hidden,驱动器再安装最终几何,之后才通知 fold-idle 监听者(例如切换展示模式后清空补白)。
- 通用路径已删除:`reserve` 回调、`pendingReserve` 与 `pendingGrowth` 的预留计算、`reserveBelow`、折叠结束后再跟随的 `followAfterFold`。保留 `reclaimBelow`(新内容到来或读者滚动后归还补白)与 `resetBelow`(切换展示模式后清空补白)。
@@ -0,0 +1,19 @@
# Bilingual-pair consistency record for 2026-10-17-agent-controller-message-delivery.md (docs/i18n/README.md): per heading
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
# After editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-10-17-agent-controller-message-delivery.md
/agent-note-delivering-messages-from-outside-an-agent-through-the-agent-controller:
en: 9105f8148e107f43
zh: a160e87bb0605da4
/agent-note-delivering-messages-from-outside-an-agent-through-the-agent-controller/problem:
en: 5071232d9743096c
zh: b553a626538b3466
/agent-note-delivering-messages-from-outside-an-agent-through-the-agent-controller/decision:
en: 1b61d351c4d2bdf1
zh: d6f8180b30d97b34
/agent-note-delivering-messages-from-outside-an-agent-through-the-agent-controller/alternatives-considered:
en: acc465cfe02b624c
zh: db1d06574f647130
/agent-note-delivering-messages-from-outside-an-agent-through-the-agent-controller/consequences:
en: 9d8486586d044709
zh: 24c7249421fd2541
@@ -0,0 +1,51 @@
# Agent Note: Delivering messages from outside an Agent through the Agent controller
Status: implemented
English | [中文](2026-10-17-agent-controller-message-delivery.zh.md)
## Problem
Plugins need to decide, with Host context, whether a message someone sends may enter a Session now. The experimental forge panel must create a requested worktree before the first message reaches the Agent, and must stop a message after the Session's worktree was removed. The Host had no point for that decision. `session/prompt` appended a prompt to the Agent inbox synchronously, and the only seam every sender passes, `agent/pre-step`, runs after the inbox accepted the message and the client already showed it as sent. A pre-step rejection ends the turn without returning the message to its sender, and a queued message reaches pre-step only when its turn starts. A gate in the Web composer, tried first, had no Host context and was bypassed by every other client.
## Decision
The core package `dsh-agent-controller` owns how a message from outside an Agent enters its inbox. The base bundle mounts it as `ctx.agentController`, so every base-backed profile can deliver through it; `dsh-agent` and `agent-loop` keep their inbox methods unchanged.
| Part | Contract |
|---|---|
| Entry | `ctx.agentController.deliver(agent, { mode, source, signal, prepare, beforeInsert })` prepares the content, builds the message, asks the `agent-controller/admission` waterfall for an `AdmissionDecision`, checks the sender's cancellation and that the Agent is still registered, calls `beforeInsert`, then `followup()` or `steer()`, and resolves with the message id once the message is in the inbox |
| Sender work | `prepare` turns the sender's content into durable message content inside the Agent's delivery order; the optional `beforeInsert` runs synchronously immediately before the append, for bookkeeping that must precede the Agent's wake |
| Listeners | Run in registration order with Agent scope filtering and receive the Agent, the message, and a signal that aborts when the sender cancels or the controller stops; return the decision of `next()` to admit, await before `next()` to hold, and return `{ kind: 'reject', reason, message, presented }` to refuse without asking later listeners; throw only when the check itself fails |
| Delivery mode | Stays with the sender; listeners do not see it, because no admission policy depends on it |
| Order | Messages to one Agent are prepared, decided, and appended one at a time in arrival order; a sender that cancels before the append is not admitted and stops waiting at once, even behind a held message; disposing the controller stops every message that has not entered an inbox and finishes once those deliveries settled |
| Failures | A rejection raises `AgentAdmissionRejected`, a `HarnessError` with code `AGENT_ADMISSION_REJECTED`, with its reason and `presented`; senders recognize it by that code rather than its class and import only types from this package; a thrown listener error raises it with `agent-controller/admission-failed`, and an Agent that left the registry with `agent-controller/agent-gone`; errors from `prepare`, `beforeInsert`, `followup()`, and `steer()` pass through; no listener runs after the message entered the inbox |
Only senders that deliver work on someone's behalf call `deliver()`. Messages the Agent's own machinery produces, such as job results, goal rounds, plan transitions, and question answers, keep `followup()`, `steer()`, and `inject()`: they belong to work that was already admitted. A listener must not wait for a person when the message source is unattended, such as a schedule or a webhook; it passes or refuses such a message.
`presented` tells an interactive client that the refusing plugin explains the refusal in its own interface, so the client adds no generic notice.
The Web Session Remote adopts the controller first. `session/prompt` resolves or resumes the Agent, acknowledges a repeated `requestId` itself, and delivers with a `prepare` that checks image input against the Session model, resolves upload receipts, and stores attachments, and a `beforeInsert` that binds the receipts to the request. It maps a refusal to `session/prompt-rejected` with `reason` and `presented`. The call stays open while a listener holds the message, and the client's cancellation stops it. The SDK server, ACP, headless runs, schedules, webhooks, and subagent prompts move to `deliver()` in later changes, each mapping a refusal to its own protocol. Until they do, a plugin that must cover them keeps an `agent/pre-step` check.
Request deduplication stays in the Session Remote. It reads the Session history synchronously, and the [synchronous-read decision](2026-09-09-deprecate-synchronous-session-event-reads.md) prohibits copying such a read into new production code; the controller can take over deduplication once a projection records accepted request ids.
The Web client keeps its existing echo: Enter clears the composer and shows the message at once, and a refusal reaches the client as an ordinary prompt failure, which retires the echo and restores the draft unless new text was typed meanwhile.
## Alternatives considered
- **An admission stage in `dsh-agent`.** `ctx.agents.admit()` with an `agent/admission` event reaches every sender as well, but it adds a delivery concern and a new extension point to the Agent core, whose contract the loop implements. A package composed in the base bundle reaches the same senders without changing that contract.
- **A Session Remote event, or a layer inside the Session Controller.** It fits the Web path, but the Session Controller is composed only in the Web profile, so ACP, SDK, and headless senders could never reach it.
- **`agent/pre-step`.** It covers every sender, but it runs after the inbox accepted the message and the client showed it as sent, a refusal drops the message instead of returning it, and a queued message is checked only when its turn starts.
- **Gated, asynchronous `followup()` and `steer()`.** Every internal sender would pass listeners meant for outside input, and their callers rely on synchronous enqueueing.
- **A composer gate in the Web client.** It has no Host context, and other clients and `ctx.conversation.send` bypass it.
- **Appending in the innermost step, with refusals thrown.** Listeners could act once `next()` returned with the message already in the inbox, but that capability, which no listener uses, cost three special cases: a listener failure after the append could only be logged, the inbox's own refusal had to be told apart from a listener failure, and a refusal thrown from another module copy of the error class needed a structural check. A returned decision keeps the stage a pure decision, as `agent/pre-step` is; a plugin that reacts to admitted messages listens to `agent/inbox/inserted`.
The liveshare proposal in PR #3854 also introduced an admission hook, for caller identity. [Peer principals](2026-10-12-peer-principal-and-remote-access.md) replaced it; this stage decides whether the Agent can take a message now, independent of who sent it.
## Consequences
- A listener that holds one message keeps later messages to the same Agent waiting; other Agents proceed.
- While a listener holds a Web prompt, the client already shows its echo; a refusal then retires the echo and returns the draft.
- `prepare` stores attachments before the decision, so a refused prompt can leave a stored attachment unreferenced.
- Image prompts deliver inside the Session Remote's image admission chain, so a model switch waits while a listener holds an image prompt.
- Until every outside sender adopts `deliver()`, the stage is not a complete enforcement point.
@@ -0,0 +1,51 @@
# Agent Note:经 Agent 控制器投递来自 Agent 外部的消息
Status: implemented
[English](2026-10-17-agent-controller-message-delivery.md) | 中文
## 问题
插件需要借助 Host 侧的上下文,判断别人发来的一条消息现在能否进入 Session。实验性的 forge 面板必须在第一条消息到达 Agent 之前建好所请求的工作树,并在 Session 的工作树被删除后拦下消息。Host 此前没有做这个判断的位置:`session/prompt` 同步地把提示词追加进 Agent 收件箱,而所有发送方都会经过的唯一扩展点 `agent/pre-step` 运行在收件箱接受消息、客户端已显示为已发送之后。pre-step 拒绝会结束本轮,却不会把消息退回给发送方;排队的消息也要等到它的轮次开始才会到达 pre-step。最初尝试的 Web 输入框闸门拿不到 Host 上下文,其他客户端也都会绕过它。
## 决策
core 包 `dsh-agent-controller` 负责来自 Agent 外部的消息如何进入其收件箱。base 组合包把它挂载为 `ctx.agentController`,因此每个基于 base 的 profile 都能经它投递;`dsh-agent` 与 `agent-loop` 的收件箱方法保持不变。
| 部分 | 约定 |
|---|---|
| 入口 | `ctx.agentController.deliver(agent, { mode, source, signal, prepare, beforeInsert })` 准备内容、构造消息、向 `agent-controller/admission` waterfall 要一个 `AdmissionDecision`,检查发送方是否取消、Agent 是否仍在注册表中,调用 `beforeInsert`,再调用 `followup()` 或 `steer()`,消息进入收件箱后返回消息 id |
| 发送方的工作 | `prepare` 在该 Agent 的投递顺序内把发送方的内容转成持久的消息内容;可选的 `beforeInsert` 紧挨在放入之前同步运行,用于必须先于 Agent 唤醒完成的登记 |
| 监听器 | 按注册顺序运行,按 Agent 作用域过滤,拿到 Agent、消息和一个在发送方取消或控制器停止时中止的信号;返回 `next()` 的决定即放行,在 `next()` 前 await 即暂留,返回 `{ kind: 'reject', reason, message, presented }` 即拒绝且不再询问后面的监听器;只在检查本身执行失败时抛出异常 |
| 投递方式 | 留在发送方,监听器看不到,因为目前没有按投递方式区分的准入策略 |
| 顺序 | 同一个 Agent 的消息按到达顺序逐条准备、决定并放入;在放入之前取消的发送方不会被接纳,并立即结束等待,即使排在被暂留的消息之后;销毁控制器会停止所有尚未进入收件箱的消息,并在这些投递结束后才完成 |
| 失败 | 拒绝时抛出带其 reason 与 `presented` 的 `AgentAdmissionRejected`,它是 code 为 `AGENT_ADMISSION_REJECTED` 的 `HarnessError`;发送方按这个 code 而不是按类识别拒绝,对本包只导入类型;监听器抛出异常时 reason 为 `agent-controller/admission-failed`,Agent 离开注册表时为 `agent-controller/agent-gone`;`prepare`、`beforeInsert`、`followup()` 与 `steer()` 的错误原样抛出;消息进入收件箱之后不再有监听器运行 |
只有代表他人送来工作的发送方调用 `deliver()`。Agent 自身机制产生的消息,例如后台任务结果、goal 轮次、plan 切换和问题答复,继续使用 `followup()`、`steer()` 与 `inject()`:它们属于已经接纳的工作。消息来源无人值守时(例如定时任务或 webhook),监听器不得等待人来处理,只能放行或拒绝。
`presented` 告诉交互式客户端,拒绝它的插件会在自己的界面里给出说明,客户端因此不再追加通用提示。
Web Session Remote 首先接入控制器。`session/prompt` 自己解析或恢复 Agent、确认重复的 `requestId`,再调用投递:`prepare` 按 Session 模型检查图片输入、解析上传凭证并存储附件,`beforeInsert` 把凭证绑定到该请求。它把拒绝映射为带 `reason` 与 `presented` 的 `session/prompt-rejected`。监听器暂留消息期间调用保持未完成,客户端取消会让它停止。SDK 服务端、ACP、headless 运行、定时任务、webhook 与子代理提示词在后续改动中改用 `deliver()`,各自把拒绝映射到自己的协议。在此之前,必须覆盖它们的插件仍保留一个 `agent/pre-step` 检查。
请求去重留在 Session Remote。它同步读取 Session 历史,而[同步读取决策](2026-09-09-deprecate-synchronous-session-event-reads.zh.md)禁止把这类读取复制到新的生产代码;等有投影记录已接收的请求 id 后,控制器才能接管去重。
Web 客户端保留现有的回显方式:Enter 立即清空 composer 并显示消息;拒绝在客户端表现为一次普通的提示词失败,回显撤下,草稿回到 composer,除非期间已经输入了新内容。
## 备选方案
- **在 `dsh-agent` 里加接纳阶段。** `ctx.agents.admit()` 加 `agent/admission` 事件同样能覆盖所有发送方,但它把投递职责和一个新扩展点加进了 Agent 核心,而 Agent 的约定由循环实现。组合在 base 组合包里的独立包能覆盖同样的发送方,又不改变这项约定。
- **Session Remote 自己的事件,或 Session Controller 内部的一层。** 它适合 Web 路径,但 Session Controller 只在 Web profile 中组合,ACP、SDK 与 headless 发送方永远无法使用它。
- **`agent/pre-step`。** 它覆盖所有发送方,但运行在收件箱接受消息、客户端显示为已发送之后;拒绝会丢掉消息而不是退回;排队的消息要到轮次开始才会被检查。
- **让 `followup()` 与 `steer()` 异步并经过闸门。** 每个内部发送方都会经过为外部输入准备的监听器,而它们的调用方依赖同步入队。
- **Web 客户端的输入框闸门。** 它拿不到 Host 上下文,其他客户端和 `ctx.conversation.send` 都会绕过它。
- **在最内层步骤放入消息,并以抛出异常表示拒绝。** 监听器能在 `next()` 返回、消息已进入收件箱之后继续执行,但这项没有监听器使用的能力带来三处特殊处理:放入之后的监听器失败只能记录日志;要把收件箱自身的拒绝与监听器失败区分开;另一个模块副本抛出的错误类需要按结构识别。返回决定让这个阶段和 `agent/pre-step` 一样只做决定;需要对已接纳消息作出反应的插件监听 `agent/inbox/inserted`。
PR #3854 中的 liveshare 方案也引入过一个接纳钩子,用于调用方身份。[Peer principals](2026-10-12-peer-principal-and-remote-access.zh.md) 取代了它;本阶段判断 Agent 现在能否接收一条消息,与发送者是谁无关。
## 影响
- 监听器暂留一条消息时,同一 Agent 之后的消息都要等待;其他 Agent 不受影响。
- 监听器暂留一条 Web 提示词期间,客户端已经显示其回显;随后被拒绝时,回显撤下,草稿回到 composer。
- `prepare` 在决定之前存储附件,因此被拒绝的提示词可能留下一个无人引用的已存附件。
- 图片提示词在 Session Remote 的图片接纳链内投递,因此监听器暂留图片提示词期间,切换模型要等待。
- 在所有外部发送方都改用 `deliver()` 之前,这个阶段还不是完整的强制执行点。
@@ -0,0 +1,40 @@
# Bilingual-pair consistency record for 2026-10-19-client-plugin-package-assets.md (docs/i18n/README.md): per heading
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
# After editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-10-19-client-plugin-package-assets.md
/agent-note-client-plugin-package-assets-served-by-url:
en: 5505b948511a2d52
zh: dae985d67e5c5935
/agent-note-client-plugin-package-assets-served-by-url/problem:
en: 159de908b09780eb
zh: 8d15ca2f15b8846a
/agent-note-client-plugin-package-assets-served-by-url/decision:
en: 8ffcd355ec5e8271
zh: 2932530bf783e3f7
/agent-note-client-plugin-package-assets-served-by-url/decision/references-and-file-names:
en: 704267953c123629
zh: 860997024ad7825e
/agent-note-client-plugin-package-assets-served-by-url/decision/build:
en: 485013b2b6778fa2
zh: 7e5cb238bb2b8585
/agent-note-client-plugin-package-assets-served-by-url/decision/host-route:
en: 151b7300f5b27a9f
zh: fae9adff942c2ce8
/agent-note-client-plugin-package-assets-served-by-url/decision/webworker-preview:
en: db1c7b2a0b916804
zh: 0edf4123667d8e2e
/agent-note-client-plugin-package-assets-served-by-url/decision/changes-and-package-manifests:
en: eedff52d04763c50
zh: 203bfcf81242b8fe
/agent-note-client-plugin-package-assets-served-by-url/decision/gates-and-migration:
en: 1e4d83d0de2d47c9
zh: dc2c2876c73d3b1c
/agent-note-client-plugin-package-assets-served-by-url/alternatives-considered:
en: 98960e10634ee536
zh: f0fdb0b0cdf9511c
/agent-note-client-plugin-package-assets-served-by-url/consequences:
en: 898262f39c0243ae
zh: e30f2161f5b669fe
/agent-note-client-plugin-package-assets-served-by-url/testing:
en: 8b6ce91df7c4c3a5
zh: b149b71cef5be761
@@ -0,0 +1,92 @@
# Agent Note: Client plugin package assets served by URL
Status: implemented
English | [中文](2026-10-19-client-plugin-package-assets.zh.md)
## Problem
The browser half of a dynamic Client plugin is a lazy-CJS factory built by the [client build preset](../../../../packages/client/tsdown.client.ts) and loaded through the `/plugins` route of the [Client module system](../../../../packages/client/modules/README.md). That route served only `client.js` and package-local chunks, so an image that plugin code referenced had to be embedded in the bundle as a base64 data URL, and an image used by a stylesheet needed a package-level build plugin that rewrote it into the inlined style.
The embedded bytes were downloaded and parsed with the JavaScript whether or not the image was shown, and base64 adds a third to their size: the onboarding illustrations made the ui-settings-account `client.js` about 3.4 MB. ui-chat carried its own [tsdown plugin](../../../../packages/client/ui-chat/tsdown.config.ts) for one mask image.
## Decision
A Client plugin package keeps raw images under `src/client/assets/`. The shared preset emits each referenced image under `lib/assets/` and turns the reference into a synchronous `require.asset(<document-relative URL>)` call on the bundle's module loader; the package publishes the emitted files with `"exports": { "./assets/*": "./lib/assets/*" }` and `lib/assets` in `files`; the client-modules Host serves them at `/plugins/<package>/assets/<emitted path>`.
### References and file names
Client code imports an image (`import art from './assets/a.webp'`), and the package's own stylesheets reference one with a relative `url()`. Both compile to a `require.asset(reference)` call whose argument is the same reference; a stylesheet becomes string literals joined with those calls, which run when its injected text is built. The module system answers each call through its `resolveAsset` option, which returns the reference unchanged unless a carrier supplies one. The emitted file name inserts the content revision, the first 12 hexadecimal digits of the SHA-1 digest of the bytes, before the extension, so one URL always names one content:
```text
src/client/assets/[email protected]
lib/assets/[email protected]
plugins/@deepseek-ai/dsh-client-ui-chat/assets/running-whale%402x-06738fa29b2f.png
```
Each path segment is percent-encoded with `encodeURIComponent` and then `!`, `'`, `(`, `)`, and `*`, so the reference is also a valid unquoted CSS `url()`, and the package name is emitted verbatim. The URL is relative to the shell document, like every app-owned route, so it resolves under any mount and from injected `<style>` text. Components need no hook, injected value, or service: the URL is a module constant computed when the module first runs.
The supported extensions are `.svg`, `.png`, `.webp`, `.jpg`, and `.jpeg`. [`src/assets.ts`](../../../../packages/client/modules/src/assets.ts) in client-modules owns the media-type table, the export key and target, the revision, the emitted name, and the URL, and the preset, the route, and the gates read them from there. The shared Client type root [`scripts/types/client-assets`](../../../../scripts/types/client-assets/index.d.ts), registered in [`tsconfig.base.client.json`](../../../../tsconfig.base.client.json), declares each extension as a module with a string default export; a test keeps it equal to the table.
### Build
The `dsh-package-asset` plugin of the dynamic browser face resolves an import against its source location and fails the build when the file's real path is outside the package's `src/client/assets/` (which also rejects another package's or a dependency's image), when `exports["./assets/*"]` is not exactly `./lib/assets/*`, when the extension is unsupported, when a path segment is `node_modules` or contains `%`, `#`, `?`, `\`, or NUL, which the Host route cannot serve, or when the extension is not lowercase, as the import types are. It emits each file once per build and registers it as a watch input. The three CSS pipelines (CSS Modules, global, and `?inline`) rewrite relative `url()` values through a lightningcss visitor in stylesheets inside the package's own `src/`; `data:`, fragment, scheme, `/`-prefixed, and empty URLs stay unchanged, a relative file reference with a fragment or query fails the build, and dependency stylesheets are not rewritten. lightningcss does not call its `Url` visitor inside `image-set()`, so an `Image` visitor rewrites those options with the same rule. The rewrite writes an identifier placeholder that lightningcss never escapes, so a file name with parentheses or quotes still becomes its resolver call. The Node Client face of a dual-face package rejects asset imports. The browser face is the only writer of `lib/assets/`: a one-shot build deletes files it did not emit, and a watch rebuild keeps them, because the Host still serves the previous bundle generation and pages running it request those names.
### Host route
`bundleResource` matches `/plugins/<package>/assets/<path>` before the bundle tables, and such a path is answered by the asset route alone. For an active client row, the path is percent-decoded once and `createRequire(<package manifest>).resolve('<package>/assets/<path>')` applies the package's own exports, so unexported paths, `.` and `..` segments, missing files, and directories do not resolve. The media type comes from the resolved file's extension, because Node reads `#` and `?` in the subpath as URL syntax. Any resolution failure, an inactive package, and an unsupported type answer 404; the query selects nothing.
A served asset streams from disk on every request; the Host keeps neither its bytes nor a hash, and a request the browser cancels mid-stream ends without a warning. It carries the media type, the bundle `cache-control: public, max-age=31536000, immutable`, `etag` equal to the requested path, `x-content-type-options: nosniff`, and, for SVG, `content-security-policy: script-src 'none'`. An `If-None-Match` equal to the tag answers 304 without opening the file; `HEAD` answers without a body. `fetchBundle()` returns the same responses with a streamed body that closes the file when cancelled.
### WebWorker preview
The preview page runs its Host in a Worker, and a page request for a reference never reaches it. Before any Client bundle runs, the page asks the Worker for `GET /__assets__`, answered by tunnel glue like `GET /__boot__`: an uncompressed tar of every file under `lib/assets/` of each image package that declares `"./assets/*": "./lib/assets/*"`, named by its unencoded `plugins/<package>/assets/<path>`, which fits the ustar name limits the base image already met. The page encodes each entry name into its reference and turns the entry into a Blob URL with the media type of its extension, and its `resolveAsset` returns that URL, or the reference when the image holds no such file. The base image already carries these files, so no network request is added; each boot still walks the image's packages, archives every asset, and copies each entry into a Blob, enabled or not.
### Changes and package manifests
A rebuilt image produces a new file name in a rebuilt `client.js`, which reaches the browser through the existing bundle HMR; assets have no channel of their own. Client-modules reads a package manifest and the bundle metadata on every reconciliation, that is, on each Loader entry construction and disposal, instead of caching the manifest until restart, so an entry that restarts after a reinstall, upgrade, or move publishes its current declaration, client export, and manifest path; a reconciliation that finds the same manifest and bundle metadata leaves the published graph unchanged and reads no bundle bytes.
### Gates and migration
[`check-workspace-constraints`](../../../../scripts/check-workspace-constraints.ts) requires `lib/assets` in `files` when the export is declared. [`verify-client-packages`](../../../../scripts/verify-client-packages.ts) requires the exact export target, allows the export only on dynamic Web Client packages, and keeps the export and the package's references (imports, and `url()` in the stylesheets its Client sources import) consistent in both directions. [`verify-package-dependencies`](../../../../scripts/verify-package-dependencies.ts) treats `./assets/*` as a non-code export. ui-settings-account and ui-chat moved to package assets, and ui-chat's tsdown plugin is gone.
Images that third-party libraries load themselves (PDF.js fonts, cmaps, wasm, and workers), plugin metadata icons, detail-document images, and static-linked packages keep their own delivery.
## Alternatives considered
**Keep data URLs.** No route is needed, but the bytes travel and parse with the JavaScript, and every package that styles with an image writes its own build plugin.
**A plain URL string with no runtime helper.** The served Web app needs nothing more, but the WebWorker preview page cannot request the URL, and changing it afterwards means copying the files to a static deployment or rewriting strings found in bundle text. A static copy is a build-time snapshot that bypasses the Host's rules and misses packages installed at runtime; finding strings in bundle text guesses at code. A synchronous call keeps the build output explicit and lets each carrier decide the URL.
**A Client Cordis service such as `ctx.assets.url(path)`.** Current consumers reference fixed files; a runtime lookup also cannot know the content revision without publishing a per-package asset list.
**Raw files in a package-root `assets/` without the build.** Authors could not preprocess them, so naming and format conversion would stay manual.
**The revision in a `?rev=` query, verified by the Host.** The Host must read a whole file to decide between 200 and 404, so a first request reads it twice and the Host must cache hashes by file metadata; a revision in the file name lets the Host only resolve and stream.
**The `client.js` revision as the asset revision.** That revision comes from the written bundle's metadata after the build, too late to appear in the bundle.
**Images in CSS only through JavaScript style objects.** Masks, backgrounds, and `@supports` fallbacks would move into components, while the URL is known at build time and the same rules rewrite `url()`.
**One plugin file protocol shared with detail documents.** Detail images read any package version from local, npm, or Git sources, need authentication because they trigger Host network access, and cache by source. Joining them would move authentication, source providers, and cache policy into client-modules; the Plugin Manager and Plugin Market detail routes stay separate until another consumer needs files of other packages.
**Manifests from Plugin Manager, or a manifest cache with change checks.** Plugin Manager lists bundles, not every client row, and resolves from the profile installation rather than the Loader resolution of the running entry. The failure was timing: a cache that outlived entry restarts. Rereading on each reconciliation and skipping unchanged results removes the cache instead of adding validation to it.
## Consequences
- The ui-settings-account `client.js` is 214 KB, and the account and ui-chat bundles contain no image data URLs; an image costs one same-origin request when shown and may appear after layout.
- `require.asset` belongs to the bundle factory contract beside `require.async`; a package built outside this repository must emit the call. The WebWorker preview creates a Blob URL for every package asset of its image at boot, enabled or not, and keeps them for the page's lifetime.
- The Desktop forwarder rewrites `/plugins/` cache headers to `no-store`, so Desktop reads an asset again on each display.
- The Host does not compare file contents with the revision in the name: a file under `lib/assets/` edited in place keeps its immutable cache entries. Only the build writes that directory.
- `require.resolve` runs synchronously on each request; a local measurement took about 43 µs for an existing file and 170 µs for a missing one. It reads exports through Node's package configuration cache, so an `./assets/*` export edited in place takes effect after boot HMR expires that package or the Host restarts. The route does not check the resolved file's real path, so a symbolic link under `lib/assets/` that leads outside the package is served.
- A package outside this repository must reproduce the emitted-name rule and the `require.asset` call; the Client module system README records them.
- A development session leaves earlier asset files under `lib/assets/` until the next one-shot build removes them; that pruning assumes the browser face is the directory's only writer.
- An entry restart reads its manifest and stats its bundle; only a changed result reads the bundle and broadcasts a graph frame (about 30 KB for 69 rows). Editing only `package.json`, without an entry restart, still takes effect at the next entry or Host restart.
## Testing
- [package-assets.client.spec.ts](../../../../packages/client/modules/tests/package-assets.client.spec.ts) composes the real Web server and compares every answer with `fetchBundle()`: headers including the SVG policy, 304 without opening the file, `HEAD`, 405, multi-chunk streaming, stream closing on cancel, a browser cancellation that logs nothing, and 404 for excluded subpaths, missing exports and files, `..`, `%2e%2e`, encoded `..`, an empty path, malformed escapes, decoded `%` and `#`, unsupported types, directories, and inactive packages.
- [assets.client.spec.ts](../../../../packages/client/modules/tests/assets.client.spec.ts) covers media types, the revision, emitted names, and references; [client-asset-types.spec.ts](../../../../scripts/client-asset-types.spec.ts) keeps the type root equal to the table.
- [client-bundle-css.spec.ts](../../../../scripts/client-bundle-css.spec.ts) covers emitted names, resolver calls in scripts and stylesheet text, their CommonJS rendering in a complete build, preserved `url()` forms, each build rejection, the Node face guard, one-shot pruning, and watch rebuilds that keep earlier files; [loader.client.spec.ts](../../../../packages/client/modules/tests/loader.client.spec.ts) covers the default and carrier resolvers.
- The gate specs reject each invalid manifest or reference form, and [node-half.client.spec.ts](../../../../packages/client/modules/tests/node-half.client.spec.ts) covers declarations, moved exports, and added or removed client declarations after an entry restart, an unchanged restart that publishes nothing and reads no bundle, and a chunk of a package named `@scope/assets`.
- [desktop-onboarding.e2e.ts](../../../../apps/web/tests/desktop-onboarding.e2e.ts) and [reasoning-preview.e2e.ts](../../../../apps/web/tests/reasoning-preview.e2e.ts) assert the package-asset URLs and decode the images in a browser; [preview-boot.e2e.ts](../../../../apps/web/tests/preview-boot.e2e.ts) decodes the ui-chat mask the WebWorker preview resolves to a Blob URL, and the runtime's [package-assets.spec.ts](../../../../packages/experimental/webworker-runtime/tests/package-assets.spec.ts) covers the archive and resolver.
@@ -0,0 +1,92 @@
# Agent Note: Client 插件包资源经 URL 提供
Status: implemented
[English](2026-10-19-client-plugin-package-assets.md) | 中文
## Problem
动态 Client 插件的浏览器半侧由 [client 打包预设](../../../../packages/client/tsdown.client.ts) 打成 lazy-CJS factory,经 [Client 模块系统](../../../../packages/client/modules/README.zh.md) 的 `/plugins` 路由加载。这条路由过去只提供 `client.js` 和包内分片,所以插件代码引用的图片只能以 base64 data URL 打进 bundle,样式里用到的图片则要靠包级构建插件替换成内联样式。
不管图片显不显示,这些字节都随 JavaScript 一起下载和解析,base64 还会让体积大三分之一:引导插图使 ui-settings-account 的 `client.js` 达到约 3.4 MB。ui-chat 还为一张遮罩图维护了自己的 [tsdown 插件](../../../../packages/client/ui-chat/tsdown.config.ts)。
## Decision
Client 插件包把原始图片放在 `src/client/assets/`。共享预设把每个被引用的图片产出到 `lib/assets/`,并把引用改成对 bundle 模块加载器的同步调用 `require.asset(<文档相对 URL>)`;包通过 `"exports": { "./assets/*": "./lib/assets/*" }` 和 `files` 里的 `lib/assets` 发布这些产物;client-modules 的 Host 在 `/plugins/<包名>/assets/<产出路径>` 提供它们。
### 引用与文件名
Client 代码用 import 引用图片(`import art from './assets/a.webp'`),包自己的样式表用相对 `url()` 引用,两者都编译成参数为同一个引用的 `require.asset(reference)` 调用;样式表变成字符串字面量与这些调用的拼接,在构建注入文本时执行。模块系统通过 `resolveAsset` 选项回答每次调用;除非载体提供了自己的实现,否则原样返回引用。产出文件名在扩展名前插入内容 revision,即文件字节 SHA-1 摘要的前 12 位十六进制,因此一个 URL 永远只对应一份内容:
```text
src/client/assets/[email protected]
lib/assets/[email protected]
plugins/@deepseek-ai/dsh-client-ui-chat/assets/running-whale%402x-06738fa29b2f.png
```
路径逐段先用 `encodeURIComponent` 编码,再编码 `!`、`'`、`(`、`)` 和 `*`,因此引用本身也是合法的无引号 CSS `url()`;包名原样输出。URL 和其他应用自有路由一样相对外壳文档,在任意挂载下、以及在注入的 `<style>` 文本中都能正确解析。组件不需要 hook、注入值或 service:URL 就是模块首次运行时算出的常量。
支持的扩展名是 `.svg`、`.png`、`.webp`、`.jpg` 和 `.jpeg`。client-modules 的 [`src/assets.ts`](../../../../packages/client/modules/src/assets.ts) 统一定义类型表、导出键与目标、revision、产出文件名和 URL,预设、路由和门禁都从这里读取。共享的 Client 类型根 [`scripts/types/client-assets`](../../../../scripts/types/client-assets/index.d.ts) 在 [`tsconfig.base.client.json`](../../../../tsconfig.base.client.json) 中登记,把每个扩展名声明为默认导出字符串的模块;有测试保证它与类型表一致。
### 构建
动态浏览器 face 的 `dsh-package-asset` 插件按源码位置解析 import。以下情况构建失败:文件真实路径不在本包 `src/client/assets/` 内(他包或依赖里的图片也因此被拒绝);`exports["./assets/*"]` 不是恰好 `./lib/assets/*`;扩展名不受支持;路径段为 `node_modules` 或含 Host 路由无法提供的 `%`、`#`、`?`、`\` 或 NUL;扩展名不是小写(导入类型只声明小写扩展名)。每个文件每次构建只产出一次,并登记为监听输入。三条 CSS 管线(CSS Modules、全局样式、`?inline`)对本包 `src/` 内的样式表,经 lightningcss visitor 改写相对 `url()`;`data:`、片段、带协议、以 `/` 开头和空的 URL 保持不变,带片段或查询串的相对文件引用会让构建失败,依赖包的样式表不改写。lightningcss 不会在 `image-set()` 内部调用 `Url` visitor,因此由 `Image` visitor 按同样的规则改写这些选项。改写时先写入 lightningcss 从不转义的标识符占位,因此含括号或引号的文件名仍会变成对应的解析调用。双 face 包的 Node Client face 拒绝资源 import。浏览器 face 是 `lib/assets/` 的唯一写入方:一次性构建会删除本次没有产出的文件,watch 重建则保留它们,因为 Host 仍在提供上一代 bundle,运行它的页面会请求那些文件名。
### Host 路由
`bundleResource` 先于 bundle 表匹配 `/plugins/<包名>/assets/<路径>`,这类路径只由资源路由回答。对启用中的 client 行,路径解码一次后,`createRequire(<包的 manifest>).resolve('<包名>/assets/<路径>')` 套用包自己的 exports,未导出路径、`.` 和 `..` 段、不存在的文件和目录都解析不出来。媒体类型按解析出的文件扩展名判断,因为 Node 会把子路径中的 `#` 和 `?` 当作 URL 语法。任何解析失败、未启用的包和不支持的类型都返回 404;查询串不参与选择。
被提供的资源每次请求都从磁盘流式读取,Host 既不保存字节也不保存 hash;浏览器中途取消的请求直接结束,不记 warning。响应带媒体类型、与 bundle 相同的 `cache-control: public, max-age=31536000, immutable`、等于请求路径的 `etag`、`x-content-type-options: nosniff`;SVG 另带 `content-security-policy: script-src 'none'`。`If-None-Match` 等于该 tag 时返回 304,且不打开文件;`HEAD` 不带 body。`fetchBundle()` 返回同样的响应,body 为流,取消时关闭文件。
### WebWorker 预览
预览页的 Host 运行在 Worker 中,页面对引用发出的请求到达不了它。在任何 Client bundle 运行之前,页面向 Worker 请求 `GET /__assets__`,它和 `GET /__boot__` 一样由隧道胶水层回答:镜像中每个声明了 `"./assets/*": "./lib/assets/*"` 的包,其 `lib/assets/` 下的所有文件打成一个未压缩的 tar,条目以未编码的 `plugins/<包名>/assets/<路径>` 命名,基础镜像已满足同样的 ustar 名称限制。页面把每个条目名编码为引用,并按扩展名的媒体类型转成 Blob URL,它的 `resolveAsset` 返回这个 URL;镜像里没有对应文件时返回引用本身。基础镜像本来就带着这些文件,所以不增加网络请求;但每次启动仍会遍历镜像中的包、打包全部资源,并把每个条目复制成 Blob,不论包是否启用。
### 变化与包 manifest
图片重建后,重建的 `client.js` 带着新文件名,经现有的 bundle HMR 到达浏览器;资源没有单独的通道。client-modules 每次对账(即每次 Loader 条目构造和销毁)都读取包 manifest 和 bundle 元数据,不再把 manifest 缓存到重启,所以重装、升级或移动后重启的条目会发布当前的声明、client 导出和 manifest 路径;读到相同 manifest 和 bundle 元数据的对账不改动已发布的启动图,也不读取 bundle 字节。
### 门禁与迁移
[`check-workspace-constraints`](../../../../scripts/check-workspace-constraints.ts) 要求声明导出时 `files` 含 `lib/assets`。[`verify-client-packages`](../../../../scripts/verify-client-packages.ts) 要求导出目标精确匹配,只允许动态 Web Client 包声明该导出,并要求导出与包内引用(import,以及其 Client 源码所 import 的样式表中的 `url()`)双向一致。[`verify-package-dependencies`](../../../../scripts/verify-package-dependencies.ts) 把 `./assets/*` 视为非代码导出。ui-settings-account 和 ui-chat 已迁移到包资源,ui-chat 的 tsdown 插件已删除。
三方库自己加载的资源(PDF.js 的字体、cmap、wasm 和 worker)、插件元数据图标、详情文档图片,以及 static-linked 包,保留各自的交付方式。
## Alternatives considered
**保留 data URL。** 不需要路由,但字节随 JavaScript 一起传输和解析,每个在样式里用图片的包都要自写构建插件。
**不带运行时 helper 的纯 URL 字符串。** 普通 Web 应用不需要更多,但 WebWorker 预览页无法请求这个 URL,事后改写只能把文件复制到静态部署,或在 bundle 文本里查找字符串替换。静态复制是构建时的快照,绕过了 Host 的规则,也覆盖不到运行时安装的包;在 bundle 文本里找字符串是在猜代码。同步调用让构建产物保持显式,并由各个载体决定 URL。
**Client Cordis service,例如 `ctx.assets.url(path)`。** 当前使用方都引用固定文件;运行时查找也拿不到内容 revision,除非另外发布每个包的资源清单。
**原始文件直接放在包根 `assets/`,不经构建。** 作者无法做预处理,命名和格式转换只能手工维护。
**revision 放在 `?rev=` 查询串,由 Host 校验。** Host 必须读完整个文件才能决定回 200 还是 404,首次请求要读两遍,还要按文件元数据缓存 hash;revision 写进文件名后,Host 只需解析路径并流式发送。
**用 `client.js` 的 revision 作为资源 revision。** 这个 revision 在构建写出 bundle 后才由文件元数据得出,来不及写进 bundle。
**CSS 只能经 JavaScript style 对象引用图片。** 遮罩、背景和 `@supports` 降级会被挪进组件,而 URL 在构建时就能确定,同一套规则也能改写 `url()`。
**与详情文档共用一套插件文件协议。** 详情图片要从本地、npm 或 Git 读取任意包版本,会触发 Host 网络访问因而需要鉴权,缓存方式也随来源而定。合并会把鉴权、来源提供方和缓存策略搬进 client-modules;在出现需要读取其他包文件的新使用方之前,Plugin Manager 和 Plugin Market 的详情路由保持独立。
**从 Plugin Manager 获取 manifest,或给 manifest 缓存加变化检查。** Plugin Manager 只列出 bundle,不覆盖所有 client 行,而且从 profile 安装目录解析,而不是用运行中条目的 Loader 解析。问题出在时机:缓存活得比条目重启更久。每次对账重新读取并跳过未变化的结果,是删掉缓存,而不是给缓存加校验。
## Consequences
- ui-settings-account 的 `client.js` 为 214 KB,账户和 ui-chat 的 bundle 都不再含图片 data URL;图片显示时多一次同源请求,可能晚于布局出现。
- `require.asset` 和 `require.async` 一样属于 bundle factory 契约;仓库外构建的包必须输出这个调用。WebWorker 预览在启动时为镜像里所有包资源创建 Blob URL,不论包是否启用,并在页面生命周期内保留。
- Desktop 转发层把 `/plugins/` 的缓存头改写为 `no-store`,Desktop 每次显示都会重新读取资源。
- Host 不比对文件内容与文件名中的 revision:原地修改 `lib/assets/` 下的文件会保留它的 immutable 缓存。这个目录只由构建写入。
- `require.resolve` 在每次请求时同步执行;本机实测文件存在约 43 µs,不存在约 170 µs。它经 Node 的包配置缓存读取 exports,因此原地修改的 `./assets/*` 导出要等 boot HMR 让该包失效或 Host 重启后才生效。路由不检查解析结果的真实路径,所以 `lib/assets/` 下指向包外的符号链接会被提供。
- 仓库外的包必须复刻产出文件名规则和 `require.asset` 调用;Client 模块系统 README 记录了它们。
- 开发期间 `lib/assets/` 会留下旧的资源文件,直到下一次一次性构建把它们清掉;这种清理的前提是浏览器 face 是该目录的唯一写入方。
- 条目重启时会读取它的 manifest 并 stat 它的 bundle;只有结果变化时才读取 bundle 并广播启动图帧(69 行时约 30 KB)。只改 `package.json` 而条目没有重启时,仍要到下次条目重启或 Host 重启才生效。
## Testing
- [package-assets.client.spec.ts](../../../../packages/client/modules/tests/package-assets.client.spec.ts) 组合真实 Web server,并将每个响应与 `fetchBundle()` 比对:包括 SVG 策略在内的响应头、不打开文件的 304、`HEAD`、405、多分片流式发送、取消时关闭流、浏览器取消请求时不记日志,以及以下情况的 404:被排除的子路径、缺少导出或文件、`..`、`%2e%2e`、编码后的 `..`、空路径、错误的转义、解码后的 `%` 和 `#`、不支持的类型、目录和未启用的包。
- [assets.client.spec.ts](../../../../packages/client/modules/tests/assets.client.spec.ts) 覆盖媒体类型、revision、产出文件名和引用;[client-asset-types.spec.ts](../../../../scripts/client-asset-types.spec.ts) 保证类型根与类型表一致。
- [client-bundle-css.spec.ts](../../../../scripts/client-bundle-css.spec.ts) 覆盖产出文件名、脚本和样式表文本中的解析调用、完整构建中它们的 CommonJS 渲染、保留的 `url()` 形式、每种构建拒绝、Node face 守卫、一次性构建的清理,以及 watch 重建保留旧文件;[loader.client.spec.ts](../../../../packages/client/modules/tests/loader.client.spec.ts) 覆盖默认解析和载体解析。
- 各门禁的 spec 拒绝每种无效的 manifest 或引用形式;[node-half.client.spec.ts](../../../../packages/client/modules/tests/node-half.client.spec.ts) 覆盖条目重启后的声明更新、导出移动、client 声明的增减、未变化的重启既不发布也不读取 bundle,以及名为 `@scope/assets` 的包的分片。
- [desktop-onboarding.e2e.ts](../../../../apps/web/tests/desktop-onboarding.e2e.ts) 和 [reasoning-preview.e2e.ts](../../../../apps/web/tests/reasoning-preview.e2e.ts) 断言包资源 URL,并在浏览器中解码这些图片;[preview-boot.e2e.ts](../../../../apps/web/tests/preview-boot.e2e.ts) 解码 WebWorker 预览解析为 Blob URL 的 ui-chat 遮罩图,运行时的 [package-assets.spec.ts](../../../../packages/experimental/webworker-runtime/tests/package-assets.spec.ts) 覆盖归档和解析。
@@ -1,19 +0,0 @@
# Bilingual-pair consistency record for 2026-10-14-built-in-plugin-feature-previews.md (docs/i18n/README.md): per heading
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
# After editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-10-14-built-in-plugin-feature-previews.md
/agent-note-built-in-plugin-feature-previews:
en: f6abb7d00b13d362
zh: b89d5da18677f31a
/agent-note-built-in-plugin-feature-previews/problem:
en: e38fa1e5b3a62b5a
zh: c612ea229705d695
/agent-note-built-in-plugin-feature-previews/decision:
en: aa12e3f72f7c2498
zh: 7dda9f061009aaa0
/agent-note-built-in-plugin-feature-previews/alternatives-considered:
en: 4a4416610894d7c6
zh: 4256e81e7ab70db5
/agent-note-built-in-plugin-feature-previews/consequences:
en: a3dbd70080416314
zh: 4d802da45085dfb0
@@ -0,0 +1,22 @@
# Bilingual-pair consistency record for 2026-10-19-sdk-native-user-questions.md (docs/i18n/README.md): per heading
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
# After editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-10-19-sdk-native-user-questions.md
/agent-note-sdk-native-user-questions:
en: b506991bd07d87dc
zh: 9f3e1ecb42163d72
/agent-note-sdk-native-user-questions/problem:
en: b3dbcdb4b5bce2d1
zh: 60df882dc253fcf5
/agent-note-sdk-native-user-questions/decision:
en: 0f7b71f0ef58f5f8
zh: 07bb12acad27d2fc
/agent-note-sdk-native-user-questions/consequences:
en: 46e3a450c6dce54e
zh: 9f99efc6f8d2ab8d
/agent-note-sdk-native-user-questions/alternatives-considered:
en: 9861066ae00e7bde
zh: 0dec00f4e3c692e0
/agent-note-sdk-native-user-questions/verification:
en: c16fb364fe8ec13b
zh: fa23c162348ba682
@@ -0,0 +1,29 @@
# Agent Note: SDK native user questions
Status: implemented
English | [中文](2026-10-19-sdk-native-user-questions.zh.md)
## Problem
The SDK can expose the shared `ask_user_question` tool without a Web GUI, but an out-of-process host cannot answer the native user-questions event through the SDK. Publishing the tool alone leaves calls without an answerer. The SDK must carry the interaction while the application chooses its user interface or simulated-user policy.
## Decision
The TypeScript and Python clients accept a question callback and advertise it during initialization. The SDK server adapts the existing user-questions seam to a bidirectional RPC only for its exact owned root Agents. Clients without the capability keep native in-process answerers. Tool definitions and the service's live-root and delegation checks remain shared with the desktop composition.
Question identity is independent of the JSON-RPC envelope so cancellation can identify outstanding host work. The runtime validates a complete answer batch, aborts pending requests when the tool or server stops, and ignores late responses. The host owns answer collection; the SDK neither chooses answers nor treats missing input as approval. The [protocol reference](../../../../packages/sdk/protocol/README.md#native-user-questions) owns wire details.
## Consequences
A GUI and runtime patch module are unnecessary. A host can use the same interface for a terminal, browser, or simulated user. The Python SDK keeps its synchronous API by running callbacks outside the reader thread; callbacks must observe cancellation and release their own resources. Late callbacks cannot publish into a later runtime process. Question waits are process-local; completed tool results retain ordinary Session persistence.
A Web Session controller is not an interaction provider for the SDK. Reminder restoration uses the SDK's own Session owner and initialized model options; it does not claim a second write handle through a GUI controller.
## Alternatives considered
Requiring a Web GUI or mounting its controller couples headless execution to another lifecycle owner. Asking each host to inject a runtime patch repeats the transport bridge outside the maintained SDK. Removing the shared question tool would reduce the model's available capabilities rather than supply its missing answer path.
## Verification
Server tests cover ownership, native fallback, answer validation, cancellation, and transport failure. Client tests cover concurrent bidirectional transport and cancellation, including callback advertisement and answer delivery with either explicit startup or a direct `initialize()` call. Initialization starts the runtime before constructing the handshake capabilities so both entry paths advertise the installed callback. Python tests also cover reentrant requests and a late callback after process replacement. Real shipped-profile tests drive the built-in question in standard and PTC modes and close a runtime while the host is answering. The [server tests](../../../../packages/sdk/server/tests/user-questions.spec.ts), [client tests](../../../../packages/sdk/client/tests/user-questions.spec.ts), [Python tests](../../../../python/sdk/tests/test_user_questions.py), and [profile tests](../../../../apps/cli/tests/profiles/sdk/user-questions.e2e.ts) retain this evidence.
@@ -0,0 +1,29 @@
# Agent Note: SDK 原生用户问答
Status: implemented
[English](2026-10-19-sdk-native-user-questions.md) | 中文
## 问题
SDK 可以在没有 Web GUI 时暴露共享的 `ask_user_question` 工具,但进程外宿主无法通过 SDK 回答原生用户问答事件。仅发布工具会使调用缺少回答方。SDK 必须承载交互,应用则自行选择用户界面或模拟用户策略。
## 决策
TypeScript 与 Python 客户端接收问答回调,并在初始化时声明。SDK server 仅为其确切拥有的根 Agent,把现有用户问答 seam 适配为双向 RPC。未声明能力的客户端保留原生进程内回答方。工具定义以及服务对存活根 Agent 和委派关系的检查仍与桌面端组合共享。
问题标识独立于 JSON-RPC 帧,使取消能够定位待决的宿主工作。运行时校验完整答案批次,在工具或服务器停止时中止待决请求,并忽略晚到响应。宿主拥有答案收集过程;SDK 不选择答案,也不把缺失输入当作批准。[协议参考](../../../../packages/sdk/protocol/README.zh.md#native-user-questions)拥有具体协议说明。
## 影响
此流程不要求 GUI 或运行时补丁模块。宿主可以用同一接口接入终端、浏览器或模拟用户。Python SDK 在读取线程之外执行回调,以保留同步 API;回调必须响应取消并释放自己的资源。晚到回调无法向后续新运行时进程发送结果。问题等待仅属于当前进程;已完成工具结果仍通过普通 Session 路径持久化。
Web Session controller 不能充当 SDK 的交互提供方。提醒恢复使用 SDK 自身的 Session 所有者和初始化模型选项,不通过 GUI 控制器争用第二个写入 handle。
## 已考虑的替代方案
要求 Web GUI 或挂载其控制器,会将 headless 执行与另一个生命周期所有者耦合。要求各宿主注入运行时补丁,会在受维护的 SDK 外重复实现传输适配。移除共享问答工具只会减少模型可用能力,无法提供缺失的回答通道。
## 验证
服务器测试覆盖所有权、原生回退、答案校验、取消和传输失败。客户端测试覆盖并发双向传输和取消,包括显式启动和直接调用 `initialize()` 两条路径的回调声明与答案投递。初始化在构造握手能力之前启动运行时,使两条入口均声明已安装的回调。Python 测试还覆盖回调内再次请求,以及进程替换后的晚到回调。真实发行 profile 测试在标准模式和 PTC 模式调用内建问答,并在宿主回答期间关闭运行时。[服务器测试](../../../../packages/sdk/server/tests/user-questions.spec.ts)、[客户端测试](../../../../packages/sdk/client/tests/user-questions.spec.ts)、[Python 测试](../../../../python/sdk/tests/test_user_questions.py)和 [profile 测试](../../../../apps/cli/tests/profiles/sdk/user-questions.e2e.ts)保留这些依据。
@@ -26,6 +26,9 @@
/agent-note-frontend-large-session-performance-budgets/calibration/trajectory-readiness-polling:
en: bdcfefd7358e335b
zh: f718f66ca5944203
/agent-note-frontend-large-session-performance-budgets/calibration/history-selector-invalidation:
en: 2d2f78f4cf6aad8a
zh: aec6af5f7c66de16
/agent-note-frontend-large-session-performance-budgets/alternatives-considered:
en: 4d2a4a64ccd14507
zh: 8a4aaf4d4abea13d
@@ -86,6 +86,22 @@ A controlled macOS arm64 diagnostic on Node 24.19.0, Chromium 149.0.7827.55 and
Complete local 240-turn runs produce Trajectory samples of 157.795/146.737/148.571 ms before the observer change and 130.184/191.247/157.543 ms afterward, with medians 148.571 and 157.543 ms. Both runs pass every fixed endpoint budget; no local speedup or hosted calibration is claimed. The readiness regression rejects a hidden searchbox or record despite a visible unrelated table row; restoring the original selector waits makes that test fail.
### History selector invalidation
The older-history control is the immediate sibling before the `display: contents` flow anchor. Leading spacing therefore uses `.column > .older + [data-slot="conversation.chat.flow"]`, preserving the earlier selector's specificity and visible-row filtering. An ancestor `.column:has(> .older)` combined with the descendant empty-row `:has()` makes unrelated draft mutations invalidate the history subtree. A temporary Chromium invalidation trace identified this exact spacing selector and style recalculations covering 13,591–13,641 elements. The trace is causal evidence, not a latency measurement: tracing itself adds substantial overhead.
On Linux x64, Node 24.19.0 and Chromium 149.0.7827.55, separate three-sample runs of the unchanged browser benchmark measured these medians using the original and sibling selectors. Both retained all 25,409 DOM nodes and every trusted input overlapped the streaming reply.
| Endpoint | Original selector | Sibling selector |
|---|---:|---:|
| Draft typing | 517.39 ms | 378.50 ms |
| First reply | 1556.89 ms | 801.58 ms |
| Stream main-thread task | 2540.59 ms | 1774.75 ms |
These local runs both failed the independent open budget: their open medians were 1192.85 and 1666.99 ms. A separate six-sample diagnostic alternated the original and sibling selectors in the browser CSSOM before timing, using the same built artifacts. Its original/sibling open wall medians were 2598.99/1349.22 ms, while Chromium task medians were 325.67/305.90 ms. This finds no browser-computation regression in those samples, but does not explain the remaining wait or certify the whole benchmark. The alternating diagnostic measured typing medians of 350.18/362.82 ms, so the earlier typing improvement is not uniform across runs; first-reply medians were 1498.48/815.85 ms and stream-task medians 2449.69/1915.73 ms. The diagnostic changes are not shipped; workload, overlap assertions, budgets and benchmark source remain unchanged.
Post-GC browser heap was 80.70/80.71 MiB. GNU time reported maximum RSS of 535.78/561.80 MiB for each benchmark command and its children; this is not input-phase memory or an aggregate browser-memory measurement. Moving the composer's selection reveal into an animation frame was rejected: profiling showed the style flush moved to another geometry read, without improving typing. The product fix removes the invalidation source instead.
## Alternatives considered
**Use the Node fold as paint evidence.** Rejected because it never performs DOM mutation, layout, or browser scheduling. The focused reconnect case likewise makes no GUI speed claim.
@@ -86,6 +86,22 @@ Trajectory 激活在动画帧上采样可见搜索框与 `[data-trajectory-scrol
完整本地 240 轮运行在修改观察器前得到 Trajectory 样本 157.795/146.737/148.571 ms,修改后为 130.184/191.247/157.543 ms,中位数分别为 148.571 和 157.543 ms。两次运行均通过所有固定终点预算;不声称本地提速或托管环境校准。即使存在可见的无关表格行,就绪回归测试仍拒绝隐藏的搜索框或记录;恢复原选择器等待会使该测试失败。
### 历史区域选择器失效
更早历史控件紧邻 `display: contents` 流锚点之前。因此首行间距使用 `.column > .older + [data-slot="conversation.chat.flow"]`,保持原选择器的优先级与可见行过滤。祖先 `.column:has(> .older)` 与后代空行 `:has()` 组合,会让无关的草稿修改使历史子树样式失效。临时 Chromium 失效跟踪定位到这条具体的间距选择器,样式重算覆盖 13,591–13,641 个元素。跟踪用于定位原因,不用于测量延迟:跟踪本身会增加大量开销。
在 Linux x64、Node 24.19.0 与 Chromium 149.0.7827.55 上,使用原选择器与兄弟选择器分别执行未修改的浏览器基准,每组取三个样本的中位数。两组都保留全部 25,409 个 DOM 节点,且每个受信任的输入都与流式回复重叠。
| 终点 | 原选择器 | 兄弟选择器 |
|---|---:|---:|
| 草稿键入 | 517.39 ms | 378.50 ms |
| 首段回复 | 1556.89 ms | 801.58 ms |
| 流式主线程任务 | 2540.59 ms | 1774.75 ms |
这两次本地运行都未通过独立的打开预算:打开中位数分别为 1192.85 与 1666.99 ms。另一项六样本诊断使用同一份构建产物,在计时前通过浏览器 CSSOM 交替使用原选择器与兄弟选择器。原版/修改版的打开壁钟中位数为 2598.99/1349.22 ms,Chromium 任务中位数为 325.67/305.90 ms。这些样本未观察到浏览器计算回归,但没有解释剩余等待,也不代表完整基准已通过。交替诊断的键入中位数为 350.18/362.82 ms,因此前述键入改善并非每次运行都能观察到;首段回复中位数为 1498.48/815.85 ms,流式任务中位数为 2449.69/1915.73 ms。诊断改动不随产品提交;负载、重叠断言、预算与基准源码保持不变。
强制 GC 后的浏览器 heap 为 80.70/80.71 MiB。GNU time 为两次基准命令及其子进程报告的最大 RSS 为 535.78/561.80 MiB;这不是输入阶段内存,也不是浏览器总内存测量。曾尝试将输入框的选区显示推迟到动画帧,但分析显示样式刷新转移到另一处几何读取,没有改善键入,因此未采用。产品修复直接移除样式失效的来源。
## 考虑过的替代方案
**用 Node 折叠作为绘制证据。** 拒绝,因为它不执行 DOM 修改、布局或浏览器调度。聚焦重连用例同样不声称 GUI 提速。
@@ -9,7 +9,7 @@ Draw recognizable artwork that belongs beside the existing official icons. Offic
## Optional feature previews
Before drawing a larger image below a built-in bundle's description, read and follow [adding a built-in plugin feature preview](../../../docs/cookbook/adding-a-plugin-preview.md). It owns optionality, image count, design canvas, display size and position, language variants, asset limits, and acceptance. The guide also owns the embedded-raster exception for SVG previews; small icons remain geometry-only. The icon dimensions below do not apply to previews. Inspect the real feature, preserve its produced assets, and keep built-in previews in the Plugin Manager client assets directory and its internal bilingual map.
Before drawing a larger image for the top of an official bundle's detail document, read and follow [adding a plugin feature preview](../../../docs/cookbook/adding-a-plugin-preview.md). It owns optionality, image count, design canvas, position, language variants, file location, and acceptance. The guide also owns the embedded-raster exception for SVG previews; small icons remain geometry-only. The icon dimensions below do not apply to previews. Inspect the real feature and preserve its produced assets.
## Establish the subject
+3
View File
@@ -40,6 +40,8 @@ flowchart LR
cfg --> plugin_dsh_base_user_questions
plugin_dsh_base_agent["agent<br/>@deepseek-ai/dsh-agent"]
cfg --> plugin_dsh_base_agent
plugin_dsh_base_agent_controller["agent-controller<br/>@deepseek-ai/dsh-agent-controller"]
cfg --> plugin_dsh_base_agent_controller
plugin_dsh_base_plugin_package_inventory_deepseek["plugin-package-inventory-deepseek<br/>@deepseek-ai/dsh-plugin-package-inventory-deepseek"]
cfg --> plugin_dsh_base_plugin_package_inventory_deepseek
plugin_dsh_base_agent_default_model["agent-default-model<br/>@deepseek-ai/dsh-agent-default-model"]
@@ -218,6 +220,7 @@ flowchart LR
| `session-title-llm` | `@deepseek-ai/dsh-session-title-first-prompt-llm` |
| `user-questions` | `@deepseek-ai/dsh-user-questions` |
| `agent` | `@deepseek-ai/dsh-agent` |
| `agent-controller` | `@deepseek-ai/dsh-agent-controller` |
| `plugin-package-inventory-deepseek` | `@deepseek-ai/dsh-plugin-package-inventory-deepseek` |
| `agent-default-model` | `@deepseek-ai/dsh-agent-default-model` |
| `jobs` | `@deepseek-ai/dsh-jobs-local` |
@@ -0,0 +1,10 @@
/** Frame one text or tool response from the local Messages endpoint. */
export function messagesResponse(content: Record<string, unknown>, stopReason: 'end_turn' | 'max_tokens' | 'tool_use'): string {
return [
{ type: 'message_start', message: { id: 'sdk-smoke-response', model: 'deepseek-v4-pro', usage: { input_tokens: 3, output_tokens: 0 } } },
{ type: 'content_block_start', index: 0, content_block: content },
{ type: 'content_block_stop', index: 0 },
{ type: 'message_delta', delta: { stop_reason: stopReason }, usage: { output_tokens: 1 } },
{ type: 'message_stop' },
].map(event => `event: ${event.type}\ndata: ${JSON.stringify(event)}\n\n`).join('')
}
@@ -0,0 +1,10 @@
/** Freeze only the isolated runtime's wall clock before reminder activation starts. */
export function apply(ctx) {
const original = Date.now
const now = Number(process.env.DSH_TEST_SCHEDULE_NOW)
ctx.effect(() => {
Date.now = () => now
return () => { Date.now = original }
})
ctx.provide('sdkTestClock', { ready: true })
}
@@ -10,6 +10,8 @@ import { execa } from 'execa'
import { describe, expect, it, onTestFinished } from 'vitest'
import { workspaceDependencyPaths, type PrimaryRuntimeManifest } from '@deepseek-ai/dsh-tool-workspace-dependencies'
import { messagesResponse } from './fixtures/messages-response.ts'
const repoRoot = fileURLToPath(new URL('../../../../../', import.meta.url))
const launch = resolveExampleLaunch({
srcBin: fileURLToPath(new URL('../../../src/bin.ts', import.meta.url)),
@@ -17,16 +19,6 @@ const launch = resolveExampleLaunch({
})
const decompress = promisify(zstdDecompress)
/** Frame one text or tool response from the local Messages endpoint. */
function messagesResponse(content: Record<string, unknown>, stopReason: 'end_turn' | 'max_tokens' | 'tool_use'): string {
return [
{ type: 'message_start', message: { id: 'sdk-smoke-response', model: 'deepseek-v4-pro', usage: { input_tokens: 3, output_tokens: 0 } } },
{ type: 'content_block_start', index: 0, content_block: content },
{ type: 'content_block_stop', index: 0 },
{ type: 'message_delta', delta: { stop_reason: stopReason }, usage: { output_tokens: 1 } },
{ type: 'message_stop' },
].map(event => `event: ${event.type}\ndata: ${JSON.stringify(event)}\n\n`).join('')
}
function waitForLine(
lines: string[],
@@ -66,7 +58,11 @@ describe('Python SDK dsh profile keyless smoke', () => {
{ label: 'reports max-token turns with mapping enabled through env', envValue: 'true', editorEnabled: false },
{ label: 'reports max-token turns with mapping disabled through env', envValue: 'false', editorEnabled: false },
{ label: 'allows an explicit patch to enable str_replace_editor', envValue: undefined, editorEnabled: true },
])('$label', async ({ envValue, editorEnabled }) => {
{ label: 'uses desktop tools through the PTC environment switch', envValue: undefined, editorEnabled: false, ptc: true },
])('$label', async (scenario) => {
const { envValue, editorEnabled } = scenario
const ptc = 'ptc' in scenario && scenario.ptc
const root = await mkdtemp(join(tmpdir(), 'dsh-python-sdk-runtime-smoke-'))
const editorPatch = join(root, 'editor.patch.yml')
if (editorEnabled) await writeFile(editorPatch, [
@@ -103,6 +99,7 @@ describe('Python SDK dsh profile keyless smoke', () => {
DSH_HOME: join(root, '.dsh'),
DSH_PERMISSION_MODE: 'danger-full-access',
DSH_TELEMETRY_DISABLED: '1',
DSH_TOOLS_MODE: ptc ? 'ptc' : 'native',
DEEPSEEK_API_KEY: 'keyless-smoke-no-call',
DEEPSEEK_BASE_URL: `http://127.0.0.1:${address.port}`,
...(envValue === undefined ? {} : { DSH_MAX_TOKENS_AS_SUCCESS: envValue }),
@@ -176,9 +173,23 @@ describe('Python SDK dsh profile keyless smoke', () => {
const toolNames = tools.map(tool => tool.name)
expect(modelRequests[0]?.output_config).toEqual({ effort: 'max' })
expect(modelRequests[0]?.max_tokens).toBe(1234)
expect(toolNames).toEqual(expect.arrayContaining(['read', 'write', 'edit', 'web_fetch', 'web_search']))
expect(toolNames.includes('str_replace_editor')).toBe(editorEnabled)
expect(toolNames).not.toContain('list_subagent_models')
const expectedTools = ['read', 'write', 'edit', 'web_fetch', 'web_search', 'subagent', 'subagent_fork',
'present', 'ask_user_question', 'schedule_create', 'schedule_delete', 'schedule_list', 'schedule_update']
const requestText = JSON.stringify(modelRequests[0])
if (ptc) {
expect(toolNames).toEqual(['run_code'])
for (const tool of expectedTools) expect(requestText.includes(`${tool}:`), tool).toBe(true)
} else {
expect(toolNames).toEqual(expect.arrayContaining(expectedTools))
expect(toolNames.includes('str_replace_editor')).toBe(editorEnabled)
expect(toolNames).not.toContain('list_subagent_models')
}
for (const text of ['Tokens prefixed with @', 'Prefer showing the primary results',
'Time sampled while preparing']) {
expect(requestText.includes(text), text).toBe(true)
}
expect(requestText).not.toContain('Current DeepSeek Harness implementation checkout:')
expect(requestText).not.toContain('Current DeepSeek Harness Web GUI URL:')
child.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', id: 3, method: 'shutdown' })}\n`)
const shutdown = await waitForLine(lines, value => value.id === 3, () => stderr)
@@ -0,0 +1,72 @@
/** Reminder activation must share SDK ownership and initialized model limits after process restart. */
import { createServer } from 'node:http'
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { DeepSeekHarness } from '@deepseek-ai/dsh-sdk-client'
import { clearAmbientProxyEnv } from '../../../../../scripts/test-proxy-environment.ts'
import { expect, it, onTestFinished, vi } from 'vitest'
import { messagesResponse } from './fixtures/messages-response.ts'
it('delivers an overdue reminder with the SDK route and lets the client resume that same Session', async () => {
const root = await mkdtemp(join(tmpdir(), 'sdk-schedule-resume-'))
const patch = join(root, 'clock.patch.yml')
const fixture = fileURLToPath(new URL('./fixtures/schedule-clock.mjs', import.meta.url))
await writeFile(patch, [
'- insert:', ' - id: sdk-test-clock', ` name: ${JSON.stringify(fixture)}`,
'- id: sdk-jsonrpc-server', ' inject: [sdkAppStartup, loader, sdkTestClock]', '',
].join('\n'))
const requests: Record<string, unknown>[] = []
const restored: Record<string, unknown>[] = []
let restarting = false
const server = createServer((incoming, response) => {
let body = ''
incoming.setEncoding('utf8')
incoming.on('data', (chunk: string) => { body += chunk })
incoming.on('end', () => {
const request = JSON.parse(body) as Record<string, unknown>
requests.push(request)
if (restarting) restored.push(request)
const content = requests.length === 1 ? {
type: 'tool_use', id: 'create-reminder', name: 'schedule_create',
input: { title: 'Check SDK', prompt: 'Report the SDK reminder', after_seconds: 86400 },
} : { type: 'text', text: 'ready' }
response.writeHead(200, { 'content-type': 'text/event-stream' })
response.end(messagesResponse(content, requests.length === 1 ? 'tool_use' : 'end_turn'))
})
})
const clients: DeepSeekHarness[] = []
onTestFinished(async () => {
try { await Promise.all(clients.map(client => client.close())) } finally {
try {
await new Promise<void>((resolve, reject) => {
server.close((error) => { if (error === undefined) resolve(); else reject(error) })
})
} finally { await rm(root, { recursive: true, force: true }) }
}
})
await new Promise<void>(resolve => server.listen(0, '127.0.0.1', resolve))
const address = server.address()
if (address === null || typeof address === 'string') throw new Error('model fixture has no port')
const env = { ...process.env, DSH_TOOLS_MODE: 'native', DSH_PERMISSION_MODE: 'danger-full-access', DSH_TELEMETRY_DISABLED: '1',
DEEPSEEK_API_KEY: 'keyless-schedule-fixture', DEEPSEEK_BASE_URL: `http://127.0.0.1:${String(address.port)}` }
clearAmbientProxyEnv(env)
const first = new DeepSeekHarness({ cwd: root, dshHome: join(root, '.dsh'), patches: [patch],
model: 'deepseek-v4-pro', maxTokens: 5678, initializeTimeoutMs: 30_000,
env: { ...env, DSH_TEST_SCHEDULE_NOW: String(Date.parse('2030-01-01T00:00:00Z')) } })
clients.push(first)
expect((await first.session('saved').run('Set a reminder')).finalResponse).toBe('ready')
expect(requests).toHaveLength(2)
await first.close()
restarting = true
const second = new DeepSeekHarness({ cwd: root, dshHome: join(root, '.dsh'), patches: [patch],
model: 'deepseek-v4-flash', maxTokens: 1234, initializeTimeoutMs: 30_000,
env: { ...env, DSH_TEST_SCHEDULE_NOW: String(Date.parse('2030-01-03T00:00:00Z')) } })
clients.push(second)
await second.start()
await vi.waitFor(() => { expect(restored).toHaveLength(1) }, { timeout: 30_000 })
expect(restored[0]).toMatchObject({ model: 'deepseek-v4-flash', max_tokens: 1234 })
const session = await second.resumeSession('saved')
expect((await session.run('Continue after the reminder')).finalResponse).toBe('ready')
})
@@ -0,0 +1,84 @@
/** Keyless SDK questions through the shipped profile, native tools, and both tool-call modes. */
import { createServer } from 'node:http'
import { mkdtemp, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { DeepSeekHarness, type SdkUserQuestionRequest } from '@deepseek-ai/dsh-sdk-client'
import { clearAmbientProxyEnv } from '../../../../../scripts/test-proxy-environment.ts'
import { expect, it, onTestFinished } from 'vitest'
import { messagesResponse } from './fixtures/messages-response.ts'
it.each([
{ mode: 'native', cancel: false }, { mode: 'ptc', cancel: false }, { mode: 'native', cancel: true },
])('handles a built-in question through $mode (cancel: $cancel)', async ({ mode, cancel }) => {
const root = await mkdtemp(join(tmpdir(), 'sdk-question-profile-'))
const requests: Record<string, unknown>[] = []
const questions = [{ id: 'format', question: 'Which format?', options: [{ label: 'CSV' }, { label: 'JSON' }] }]
const server = createServer((incoming, response) => {
let body = ''
incoming.setEncoding('utf8')
incoming.on('data', (chunk: string) => { body += chunk })
incoming.on('end', () => {
requests.push(JSON.parse(body) as Record<string, unknown>)
const tool = mode === 'ptc' ? {
type: 'tool_use', id: 'question-call', name: 'run_code', input: {
code: `return await tools.ask_user_question(${JSON.stringify({ questions })});`, description: 'Ask which output format to use',
},
} : { type: 'tool_use', id: 'question-call', name: 'ask_user_question', input: { questions } }
response.writeHead(200, { 'content-type': 'text/event-stream' })
response.end(messagesResponse(requests.length === 1 ? tool : { type: 'text', text: 'answer received' },
requests.length === 1 ? 'tool_use' : 'end_turn'))
})
})
const state: { harness?: DeepSeekHarness } = {}
onTestFinished(async () => {
try { await state.harness?.close() } finally {
try {
await new Promise<void>((resolve, reject) => {
server.close((error) => {
if (error === undefined) resolve()
else reject(error)
})
})
}
finally { await rm(root, { recursive: true, force: true }) }
}
})
await new Promise<void>(resolve => server.listen(0, '127.0.0.1', resolve))
const address = server.address()
if (address === null || typeof address === 'string') throw new Error('model fixture has no port')
const env = { ...process.env, DSH_TOOLS_MODE: mode, DSH_PERMISSION_MODE: 'danger-full-access', DSH_TELEMETRY_DISABLED: '1',
DEEPSEEK_API_KEY: 'keyless-question-fixture', DEEPSEEK_BASE_URL: `http://127.0.0.1:${String(address.port)}` }
clearAmbientProxyEnv(env)
const seen: SdkUserQuestionRequest[] = []
const entered = Promise.withResolvers<AbortSignal>()
const harness = state.harness = new DeepSeekHarness({
cwd: root, dshHome: join(root, '.dsh'), env, model: 'deepseek-v4-pro', maxTokens: 1234, initializeTimeoutMs: 30_000,
onUserQuestion: async (request, { signal }) => {
expect(signal.aborted).toBe(false)
seen.push(request)
entered.resolve(signal)
if (cancel) return new Promise((_resolve, reject) => {
signal.addEventListener('abort', () => { reject(new Error('question cancelled')) }, { once: true })
})
return { answers: [{ id: 'format', selected: ['CSV'] }] }
},
})
const pending = harness.session('question-session').run('Ask which output format to use')
if (cancel) {
const settled = pending.catch(() => undefined)
const signal = await entered.promise
await harness.close()
await settled
expect(signal.aborted).toBe(true)
expect(requests).toHaveLength(1)
return
}
const result = await pending
expect(seen).toEqual([{ requestId: expect.any(String) as string, sessionId: 'question-session', questions }])
expect(result.finalResponse).toBe('answer received')
expect(requests).toHaveLength(2)
expect(requests[1]?.max_tokens).toBe(1234)
expect(JSON.stringify(requests[1]?.messages)).toContain('CSV')
expect(result.events.some(event => event.type === 'tool/result' && event.data.error !== undefined)).toBe(false)
})
@@ -0,0 +1,367 @@
/**
* Scroll continuity when "On next message" folds a process group the reader opened. The latest Turn has
* one Step; the reader opened its group and expanded the reasoning inside, so the group body is capped and
* scrollable, then sends the next message from the floor. A held prompt keeps the new Turn from starting
* until the fold settles, so only the fold moves the view while the scrollport is sampled on every
* animation frame.
*
* - A long answer leaves the folded group above the viewport. The scroll position changes in one direction
* only: the fold never scrolls the view away and then snaps it back.
* - A short answer leaves fold room below the tail. Growth fills that room without scrolling, so the running
* status keeps its entrance slide when the new Step's first row appears instead of moving in one frame.
*/
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import type { Browser, Page, Route } from 'playwright'
import { chromium } from 'playwright'
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
import type { StreamChunk } from '@deepseek-ai/dsh-llm'
import { createAssistantMessage, createUserMessage } from '@deepseek-ai/dsh-llm'
import type { ReplayEntry } from '@deepseek-ai/dsh-llm-replay'
import { SessionId, type SessionEvent } from '@deepseek-ai/dsh-session'
import { createChatScrollFixture } from './chat-scroll-fixture.ts'
import { launchWebScaffold, parseSeedFixture, seedSession, watchConsole, webSnapshotMode, type WebScaffold } from './scaffold.ts'
import { newEnglishPage, openSettings } from './support.ts'
declare global {
interface Window {
openFoldTrace?: { readonly frames: TraceFrame[]; readonly slides: number[]; stop(): void }
}
}
const MODE = webSnapshotMode()
const ROOT = '[data-conversation-scroll]:not([data-sidebar-chat] [data-conversation-scroll])'
/** Settled Turns before the latest one, so the transcript overflows the scrollport. */
const TURNS = 7
const NEXT_INPUT = 'OPENFOLD_NEXT_INPUT'
const REPLY = 'OPENFOLD_REPLY'
/** One painted frame of the main scrollport. */
interface TraceFrame {
readonly top: number
readonly floor: number
/** Height of the fold room below the transcript. */
readonly spacer: number
/** The column marks the tail as pinned, which runs the status slide at zero duration. */
readonly pinned: boolean
/** The running status's top relative to the scrollport, when it is mounted. */
readonly status: number | null
readonly folding: boolean
}
function paragraphs(marker: string, count: number, text: string): string {
return `${marker}\n\n${Array.from({ length: count }, (_, line) => `${text} ${line}.`).join('\n\n')}`
}
/** Settled history whose latest Turn is one Step: long reasoning, then an answer of `answerParagraphs`. */
function seededLog(id: string, answerParagraphs: number): { log: string; search: string } {
const base = createChatScrollFixture({ markerPrefix: `OPENFOLD${id.toUpperCase()}`, title: `OPENFOLD ${id}`, turns: TURNS })
const parsed = parseSeedFixture(base.log)
const boundary = parsed.events.findIndex(event => event.type === 'turn/start' && event.data.turn === TURNS)
const drafts: (SessionEvent | { type: string; data: object; surfaceOp?: 'append' })[] = parsed.events.slice(0, boundary)
drafts.push(
{ type: 'turn/start', data: { turn: TURNS } },
{ type: 'step/start', data: { turn: TURNS, step: 1 } },
{ type: 'user/message', data: createUserMessage({
content: [{ type: 'text', text: 'OPENFOLD_LAST_USER' }], source: { kind: 'user' },
}), surfaceOp: 'append' },
{ type: 'assistant/message', surfaceOp: 'append', data: {
turn: TURNS, step: 1, stream: [], message: createAssistantMessage({
content: [
{ type: 'reasoning', text: paragraphs('OPENFOLD_REASONING', 30, 'Reasoning weighs one more consideration, paragraph') },
{ type: 'text', text: paragraphs('OPENFOLD_ANSWER', answerParagraphs, 'Final answer paragraph of the single Step') },
],
source: { provider: 'fixture', model: 'fixture' },
}),
} },
{ type: 'step/end', data: { turn: TURNS, step: 1 } },
{ type: 'turn/end', data: { turn: TURNS, reason: { kind: 'completed' } } },
)
return {
log: [parsed.headerLine, ...drafts.map((event, seq) => JSON.stringify({ ...event, seq, time: seq + 1 })), ''].join('\n'),
search: base.markers.user(1),
}
}
function reply(): ReplayEntry {
const text = `${REPLY} short answer.`
const finish: StreamChunk[] = [
{ type: 'usage', usage: { inputTokens: 512, outputTokens: 64 } },
{ type: 'finish', reason: { kind: 'stop' } },
]
return { kind: 'chunks', chunks: [
{ type: 'block-start', index: 0, blockType: 'text' },
{ type: 'text-delta', index: 0, text },
{ type: 'block-end', index: 0, block: { type: 'text', text } },
...finish,
] }
}
async function stableScroll(page: Page): Promise<void> {
await page.evaluate(async (selector) => {
const host = document.querySelector<HTMLElement>(selector)!
let previous = host.scrollTop
const started = performance.now()
let stableSince = started
await new Promise<void>((resolveStable, reject) => {
const check = (): void => {
const now = performance.now()
const moving = host.querySelector('[data-chat-motion="collapse"], [data-chat-motion="reveal"]') !== null
if (Math.abs(host.scrollTop - previous) > 0.25 || moving) stableSince = now
previous = host.scrollTop
if (now - stableSince >= 300) resolveStable()
else if (now - started >= 20_000) reject(new Error(`scroll did not settle: top ${String(host.scrollTop)}`))
else requestAnimationFrame(check)
}
requestAnimationFrame(check)
})
}, ROOT)
}
async function toBottom(page: Page): Promise<void> {
const button = page.locator(ROOT).getByRole('button', { name: 'Back to bottom', exact: true })
if (await button.count() > 0) await button.click()
await stableScroll(page)
await expect.poll(() => page.locator(`${ROOT} [data-chat-following-tail]`).count()).toBe(1)
}
async function openSessionBySearch(page: Page, query: string): Promise<void> {
await page.getByText('Ungrouped', { exact: true }).waitFor({ timeout: 30_000 })
const searchButton = page.getByRole('button', { name: 'Search sessions' })
if (await searchButton.getAttribute('aria-expanded') !== 'true') await searchButton.click()
await page.getByRole('textbox', { name: 'Search session names', exact: true }).fill(query)
const results = page.getByRole('tree', { name: 'Search results' }).getByRole('treeitem')
await expect.poll(() => results.count(), { timeout: 60_000 }).toBe(1)
await results.click()
}
async function choosePreferences(page: Page): Promise<void> {
await openSettings(page, 'en')
const dialog = page.getByRole('dialog', { name: 'Settings', exact: true })
const timing = dialog.getByText('When to Collapse Work Details', { exact: true }).locator('../..')
await timing.getByRole('button').click()
await page.getByRole('menuitem', { name: 'On next message', exact: true }).click()
const details = dialog.getByText('Work details', { exact: true }).locator('../..')
await details.getByRole('button').click()
await page.getByRole('menuitem', { name: 'Standard', exact: true }).click()
await dialog.getByRole('button', { name: 'Close', exact: true }).click()
}
/** Open the latest Turn's process group and expand its reasoning until the group body caps its height. */
async function openLatestGroup(page: Page): Promise<void> {
const group = page.locator(`${ROOT} [data-chat-group-key][data-chat-turn="${String(TURNS)}"]`).last()
const header = group.locator('button[aria-expanded]').first()
if (await header.getAttribute('aria-expanded') !== 'true') await header.click()
const thought = group.locator('[data-variant="think"]').getByRole('button').first()
if (await thought.getAttribute('aria-expanded') !== 'true') await thought.click()
const body = group.locator('[data-step-process-body]')
await expect.poll(() => body.evaluate(element => element.scrollHeight > element.clientHeight + 100)).toBe(true)
// Focus left inside a folding row keeps that row open; a reader who moved on has focused nothing there.
await page.evaluate(() => { if (document.activeElement instanceof HTMLElement) document.activeElement.blur() })
await page.mouse.move(0, 0)
}
/**
* Sample the scrollport on every animation frame, and record the duration of every status entrance slide
* that a flow change starts.
*/
async function startTrace(page: Page): Promise<void> {
await page.evaluate((selector) => {
const host = document.querySelector<HTMLElement>(selector)!
const column = host.querySelector<HTMLElement>('[data-chat-flow]')!
const spacer = host.querySelector<HTMLElement>('[data-chat-turn-spacer]')!
const frames: TraceFrame[] = []
const slides: number[] = []
let frame = 0
const tick = (): void => {
const status = host.querySelector<HTMLElement>('[data-chat-running]')
frames.push({
top: host.scrollTop,
floor: Math.max(0, host.scrollHeight - host.clientHeight),
spacer: spacer.getBoundingClientRect().height,
pinned: column.hasAttribute('data-chat-tail-pinned'),
status: status === null ? null : status.getBoundingClientRect().top - host.getBoundingClientRect().top,
folding: column.hasAttribute('data-input-fold') || host.querySelector('[data-chat-motion="collapse"]') !== null,
})
frame = requestAnimationFrame(tick)
}
// The slide starts at the style update after the flow change; reading animations there sees its duration.
const observer = new MutationObserver(() => {
const status = host.querySelector<HTMLElement>('[data-chat-running]')
for (const animation of status?.getAnimations() ?? []) {
if (animation instanceof CSSAnimation && animation.animationName.includes('running-follows-output')) {
slides.push(Number(animation.effect?.getComputedTiming().duration ?? 0))
}
}
})
observer.observe(column, { childList: true, subtree: true })
window.openFoldTrace = { frames, slides, stop: () => { cancelAnimationFrame(frame); observer.disconnect() } }
frame = requestAnimationFrame(tick)
}, ROOT)
}
async function readTrace(page: Page): Promise<{ frames: TraceFrame[]; slides: number[] }> {
return page.evaluate(() => {
const trace = window.openFoldTrace!
return { frames: [...trace.frames], slides: [...trace.slides] }
})
}
/** Per-frame scroll movements larger than subpixel rounding. */
function movements(frames: readonly TraceFrame[]): number[] {
const steps: number[] = []
for (let index = 1; index < frames.length; index++) {
const step = frames[index]!.top - frames[index - 1]!.top
if (Math.abs(step) > 1) steps.push(Math.round(step))
}
return steps
}
interface World {
readonly scaffold: WebScaffold
readonly page: Page
readonly session: SessionId
readonly tripwire: ReturnType<typeof watchConsole>
/** Releases the held prompt; the new Turn starts after this. */
readonly releasePrompt: () => void
/** Releases the held model stream; the new Step's output arrives after this. */
readonly releaseModel: () => void
readonly ended: Promise<void>
readonly close: () => Promise<void>
}
/**
* Seed and open the case, open the latest group, and send the next message from the floor with the prompt
* and the model stream held.
*/
async function sendFromOpenedGroup(browser: Browser, id: string, answerParagraphs: number): Promise<World> {
const replayDir = await mkdtemp(join(tmpdir(), 'dsh-open-fold-'))
await writeFile(join(replayDir, 'replay.override.json'), JSON.stringify([reply()]))
const scaffold = await launchWebScaffold({
replayFixture: join(replayDir, 'override-only.jsonl'),
replayOverride: join(replayDir, 'replay.override.json'),
paceMs: 16,
replayContextWindow: 10_000_000,
})
const model = Promise.withResolvers<undefined>()
const disposeHold = scaffold.ctx.on('llm/stream', async function* (_options, next) {
await model.promise
for await (const chunk of next()) yield chunk
})
const prompt = Promise.withResolvers<undefined>()
const holdPrompt = async (route: Route): Promise<void> => {
await prompt.promise
await route.continue()
}
const page = await newEnglishPage(browser, 826)
const close = async (): Promise<void> => {
prompt.resolve(undefined)
model.resolve(undefined)
disposeHold()
const failures: unknown[] = []
await page.context().close().catch((error: unknown) => failures.push(error))
await scaffold.close().catch((error: unknown) => failures.push(error))
await rm(replayDir, { recursive: true, force: true }).catch((error: unknown) => failures.push(error))
if (failures.length === 1) throw failures[0]
if (failures.length > 1) throw new AggregateError(failures, `${id}: cleanup failed`)
}
try {
const tripwire = watchConsole(page)
await page.setViewportSize({ width: 1170, height: 826 })
await page.emulateMedia({ reducedMotion: 'no-preference' })
const session = SessionId(`open-fold-${id}`)
const { log, search } = seededLog(id, answerParagraphs)
await seedSession(scaffold, log, session)
await page.goto(scaffold.authenticatedUrl, { waitUntil: 'load' })
await openSessionBySearch(page, search)
await page.locator(ROOT).getByText('OPENFOLD_ANSWER', { exact: false }).last().waitFor({ timeout: 30_000 })
await choosePreferences(page)
await toBottom(page)
await openLatestGroup(page)
await toBottom(page)
await page.route('**/api/session/prompt', holdPrompt)
const ended = new Promise<void>((resolveEnded, reject) => {
const timer = setTimeout(() => { off(); reject(new Error(`${id}: no turn/end within 60000ms`)) }, 60_000)
const off = scaffold.ctx.on('session/event', (owner: { id: SessionId }, event: SessionEvent) => {
if (owner.id !== session || event.type !== 'turn/end') return
clearTimeout(timer)
off()
resolveEnded()
})
})
// A failure before the release awaits nothing; the success path awaits and rethrows this rejection.
ended.catch(() => undefined)
await startTrace(page)
await page.locator(`${ROOT} [data-composer-input][contenteditable="true"]`).last().fill(NEXT_INPUT)
await page.locator(ROOT).getByRole('button', { name: 'Send message', exact: true }).click()
await page.locator(`${ROOT} [data-submission-echo]`).waitFor()
await stableScroll(page)
return {
scaffold, page, session, tripwire, ended, close,
releasePrompt: () => { prompt.resolve(undefined) },
releaseModel: () => { model.resolve(undefined) },
}
} catch (error) {
await close().catch(() => undefined)
throw error
}
}
describe.skipIf(MODE === 'record')('web e2e: next-message fold of an opened process group', () => {
let browser: Browser
beforeAll(async () => {
browser = await chromium.launch()
})
afterAll(async () => {
await browser?.close()
})
it('folds the opened group above a long answer without scrolling away and back', async () => {
const world = await sendFromOpenedGroup(browser, 'long', 30)
try {
const { frames } = await readTrace(world.page)
const steps = movements(frames)
const reversals = steps.filter((step, index) => index > 0 && Math.sign(step) !== Math.sign(steps[index - 1]!)).length
// Soft assertions let the held Turn finish, so a failure reports the curve instead of replay leftovers.
expect.soft(frames.some(frame => frame.folding), 'the message folds the opened group').toBe(true)
expect.soft(reversals, `scroll direction changes; per-frame movements ${JSON.stringify(steps)}`).toBe(0)
const settled = frames.at(-1)!
expect.soft(settled.floor - settled.top, 'distance from the floor after the fold').toBeLessThanOrEqual(1)
world.releasePrompt()
world.releaseModel()
await world.ended
expect(world.tripwire.pageErrors).toEqual([])
} finally {
await world.close()
}
})
it('keeps the running status slide while fold room below a short answer takes the new Step', async () => {
const world = await sendFromOpenedGroup(browser, 'short', 2)
try {
const folded = (await readTrace(world.page)).frames.at(-1)!
expect.soft(folded.spacer, 'fold room below the short answer').toBeGreaterThan(0)
expect.soft(folded.pinned, 'tail pin while fold room remains').toBe(false)
world.releasePrompt()
// The running Step's group row is the first row after the user message; output stays held until then.
await world.page.locator(`${ROOT} [data-chat-group-key][data-chat-turn="${String(TURNS + 1)}"]`).waitFor({ timeout: 30_000 })
await world.page.locator(`${ROOT} [data-chat-running]`).waitFor()
await world.page.waitForTimeout(300)
const { frames, slides } = await readTrace(world.page)
const statusSteps = movements(frames.map(frame => ({ ...frame, top: frame.status ?? Number.NaN })))
.filter(step => Number.isFinite(step))
// The fold runs status moves at zero duration on its own, so only settled frames show the pin's effect.
const pinnedOverRoom = frames.filter(frame => !frame.folding && frame.pinned && frame.spacer > 0)
expect.soft(pinnedOverRoom, 'settled frames pinned over fold room').toEqual([])
expect.soft(slides.some(duration => duration > 0), `status slide durations ${JSON.stringify(slides)}; status movements ${JSON.stringify(statusSteps)}`).toBe(true)
world.releaseModel()
await world.ended
expect(world.tripwire.pageErrors).toEqual([])
} finally {
await world.close()
}
})
})
+125 -3
View File
@@ -44,9 +44,54 @@ it('places dynamic Session menu rows by order among the shipped ones and removes
await row.waitFor({ timeout: 20_000 })
const trigger = row.getByRole('button', { name: `Session actions for ${SESSION_TITLE}` })
const trailing = row.locator('[data-session-trailing]')
const observations: { stage: string; icons: string[]; time: boolean; pinned: boolean; actions: boolean }[] = []
const observe = async (stage: string, iconsVisible: boolean, actionsVisible: boolean, target = row): Promise<void> => {
const icons = target.locator('[data-session-trailing]')
const actions = target.getByRole('button', { name: `Session actions for ${SESSION_TITLE}` })
const time = target.locator('[class*="time"]')
await expect.poll(() => icons.first().isVisible()).toBe(iconsVisible)
await expect.poll(() => time.isVisible()).toBe(iconsVisible)
await expect.poll(() => actions.isVisible()).toBe(actionsVisible)
observations.push({
stage,
icons: await icons.filter({ visible: true }).evaluateAll(elements => elements.map(element => element.getAttribute('aria-label')!)),
time: await time.isVisible(),
pinned: await target.getByRole('img', { name: 'Pinned', exact: true }).isVisible(),
actions: await actions.isVisible(),
})
}
await expect.poll(() => trailing.count()).toBe(2)
expect(await trailing.evaluateAll(elements => elements.map(element => element.getAttribute('data-session-trailing'))))
.toEqual([sessionId, sessionId])
await page.mouse.move(0, 0)
await observe('resting', true, false)
await page.emulateMedia({ colorScheme: 'dark' })
await expect.poll(() => page.locator('body').evaluate(element => element.hasAttribute('data-ds-dark-theme'))).toBe(true)
await observe('dark', true, false)
await page.emulateMedia({ colorScheme: 'light' })
await expect.poll(() => page.locator('body').evaluate(element => element.hasAttribute('data-ds-dark-theme'))).toBe(false)
const titleWidth = await row.locator('[class*="title"]').evaluate(element => element.getBoundingClientRect().width)
await row.hover()
await trigger.focus()
await observe('hover', false, true)
await page.mouse.move(0, 0)
await observe('pointer-left', true, false)
await row.click()
await expect.poll(() => page.locator('[data-composer-input][contenteditable="true"]')
.evaluate(element => element === document.activeElement)).toBe(true)
await row.click()
await page.mouse.move(0, 0)
expect(await row.evaluate(element => element === document.activeElement)).toBe(true)
await observe('clicked-current-row', true, false)
expect(await row.locator('[data-slot="sidebar.session.row.leading"]').evaluate(element => element.tagName)).toBe('SPAN')
await page.keyboard.press('Shift+Tab')
await page.keyboard.press('Tab')
expect(await row.evaluate(element => element === document.activeElement)).toBe(true)
await observe('focus', false, true)
await page.keyboard.press('Tab')
expect(await row.evaluate(element => element.querySelector('button') === document.activeElement)).toBe(true)
await trigger.click()
await observe('menu-open', false, true)
const menu = page.getByRole('menu')
await menu.waitFor()
expect(await menu.getByRole('separator').count()).toBe(1)
@@ -56,7 +101,7 @@ it('places dynamic Session menu rows by order among the shipped ones and removes
webSnapshotMode(),
)
await menu.getByRole('menuitem', { name: 'Export session' }).click()
await menu.getByRole('menuitem', { name: 'Export session' }).press('Enter')
await expect.poll(() => menu.count()).toBe(0)
expect(await page.evaluate(() => ({
action: document.documentElement.dataset.sessionAction,
@@ -64,10 +109,87 @@ it('places dynamic Session menu rows by order among the shipped ones and removes
title: document.documentElement.dataset.sessionActionTitle,
}))).toEqual({ action: 'fixture.export-session', id: sessionId, title: SESSION_TITLE })
await page.getByRole('button', { name: 'Account menu', exact: true }).focus()
await page.mouse.move(0, 0)
await observe('blur', true, false)
await page.setViewportSize({ width: 390, height: 844 })
await page.getByRole('button', { name: 'Open sidebar', exact: true }).first().click()
await page.mouse.move(0, 0)
await observe('narrow', true, false)
const bounds = await row.evaluate((element) => {
const title = element.querySelector('[class*="title"]')!.getBoundingClientRect()
const icons = [...element.querySelectorAll('[data-session-trailing]')].map(icon => icon.getBoundingClientRect())
return {
titleRight: title.right, firstIconLeft: icons[0]!.left, lastIconRight: icons.at(-1)!.right,
rowRight: element.getBoundingClientRect().right,
}
})
expect(bounds.titleRight).toBeLessThanOrEqual(bounds.firstIconLeft)
expect(bounds.lastIconRight).toBeLessThanOrEqual(bounds.rowRight)
await page.setViewportSize({ width: 1680, height: 1000 })
const touchPage = await browser.newPage({
viewport: { width: 390, height: 844 }, locale: 'en-US', isMobile: true, hasTouch: true,
})
try {
const touchConsole = watchConsole(touchPage)
await touchPage.goto(scaffold.authenticatedUrl, { waitUntil: 'load' })
await touchPage.getByRole('button', { name: 'Open sidebar', exact: true }).first().click()
const touchRow = touchPage.getByRole('treeitem').filter({ has: touchPage.getByText(SESSION_TITLE, { exact: true }) })
const touchTrigger = touchRow.getByRole('button', { name: `Session actions for ${SESSION_TITLE}` })
await touchTrigger.waitFor()
expect(await touchPage.evaluate(() => matchMedia('(hover: hover)').matches)).toBe(false)
await observe('touch', true, true, touchRow)
expect(await touchRow.getByRole('button', { name: 'Archive session', exact: true }).isVisible()).toBe(false)
expect(await touchRow.getByRole('button', { name: 'Pin session', exact: true }).isVisible()).toBe(false)
await touchTrigger.tap()
await touchPage.getByRole('menu').waitFor()
await observe('touch-menu-open', true, true, touchRow)
await touchPage.keyboard.press('Escape')
await observe('touch-menu-closed', true, true, touchRow)
await touchTrigger.tap()
await touchPage.getByRole('menuitem', { name: 'Pin session', exact: true }).tap()
await touchRow.getByRole('img', { name: 'Pinned', exact: true }).waitFor()
await observe('touch-pinned', true, true, touchRow)
const touchBounds = await touchRow.evaluate((element) => {
const title = element.querySelector('[class*="title"]')!.getBoundingClientRect()
const resting = element.querySelector('[class*="rowResting"]')!.getBoundingClientRect()
const actions = element.querySelector('[class*="rowActions"]')!.getBoundingClientRect()
return { titleRight: title.right, restingLeft: resting.left, restingRight: resting.right,
actionsLeft: actions.left, actionsRight: actions.right, rowRight: element.getBoundingClientRect().right }
})
expect(touchBounds.titleRight).toBeLessThanOrEqual(touchBounds.restingLeft)
expect(touchBounds.restingRight).toBeLessThanOrEqual(touchBounds.actionsLeft)
expect(touchBounds.actionsRight).toBeLessThanOrEqual(touchBounds.rowRight)
await touchTrigger.tap()
await touchPage.getByRole('menuitem', { name: 'Archive session', exact: true }).tap()
await expect.poll(() => touchRow.count()).toBe(0)
expect(scaffold.ctx.workspaceRegistry.archivedSessionIds.includes(sessionId)).toBe(true)
await page.getByRole('button', { name: 'View options', exact: true }).click()
await page.getByRole('menuitem', { name: 'All conversations (show archived)', exact: true }).click()
await row.click()
await page.getByRole('alert').filter({ hasText: 'Archived sessions cannot be opened' }).waitFor()
await page.mouse.move(0, 0)
expect(await row.evaluate(element => element === document.activeElement)).toBe(true)
await observe('clicked-archived-row', true, false)
await scaffold.ctx.workspaceRegistry.unarchiveSession(sessionId)
await observe('touch-unarchived', true, true, touchRow)
expect(touchConsole.pageErrors).toEqual([])
} finally {
await touchPage.close()
}
scaffold.ctx.loader.remove(entryId)
await expect.poll(() => page.evaluate(
() => document.documentElement.dataset.liveDisposals,
)).toBe('1')
await expect.poll(() => trailing.count()).toBe(0)
await expect.poll(() => row.locator('[class*="title"]').evaluate(element => element.getBoundingClientRect().width))
.toBeGreaterThan(titleWidth)
await compareOrRefreshGolden(
join(EXPECTED, 'session-trailing.expected.json'),
JSON.stringify(observations, null, 2),
webSnapshotMode(),
)
await row.hover()
await trigger.click()
await menu.waitFor()
@@ -86,7 +208,7 @@ it('places dynamic Session menu rows by order among the shipped ones and removes
expect(console.pageErrors).toEqual([])
await assertFixtureInventory(EXPECTED, [
'bootstrap-rebuild.expected.md', 'enabled.expected.md', 'recovered.expected.md',
'session-actions.expected.md',
'session-actions.expected.md', 'session-trailing.expected.json',
])
} finally {
await browser.close()
+15 -10
View File
@@ -19,6 +19,7 @@ const MODE = webSnapshotMode()
const EXPECTED = fileURLToPath(new URL('./expected/desktop-onboarding', import.meta.url))
const NS = 'ui-settings-account'
const INITIAL = { version: 1, step: 'welcome', purpose: null, process: null, completion: null, usage: 'compact', developerTools: false }
const ACCOUNT_ASSET = /\/plugins\/@deepseek-ai\/dsh-client-ui-settings-account\/assets\/onboarding-[a-z-]+-[0-9a-f]{12}\.(?:svg|webp)$/
describe.skipIf(MODE === 'record')('web e2e: App-only desktop onboarding', () => {
let root: string
@@ -124,15 +125,18 @@ describe.skipIf(MODE === 'record')('web e2e: App-only desktop onboarding', () =>
async function snapshot(name: string): Promise<void> {
await expect.poll(() => page.locator('[data-desktop-onboarding]').getAttribute('aria-busy')).toBe('false')
const hasIllustration = name.startsWith('welcome') || name.startsWith('credit')
if (hasIllustration) {
const decoded = await page.locator('[class*="illustration"] > img').evaluateAll(async elements =>
Promise.all(elements.map(async (element) => {
const image = element as HTMLImageElement
await image.decode()
return image.naturalWidth > 0 && image.naturalHeight > 0
})))
expect(decoded).toEqual([true, true])
}
// Illustrations and card icons are package assets fetched after the step renders.
const images = await page.locator('[data-desktop-onboarding] img').evaluateAll(async elements =>
Promise.all(elements.map(async (element) => {
const image = element as HTMLImageElement
await image.decode()
return {
source: image.currentSrc, illustration: image.matches('[class*="illustration"] > img'),
loaded: image.naturalWidth > 0 && image.naturalHeight > 0,
}
})))
expect(images.filter(image => !image.loaded || !ACCOUNT_ASSET.test(image.source)), `${name}: images outside loaded package assets`).toEqual([])
expect(images.filter(image => image.illustration), `${name}: illustration images`).toHaveLength(hasIllustration ? 2 : 0)
const aria = await captureStableAria(page, '[data-desktop-onboarding]', scaffold.workspaceCwd)
await mkdir(EXPECTED, { recursive: true })
await compareOrRefreshGolden(join(EXPECTED, `${name}.expected.md`), aria, MODE)
@@ -424,7 +428,8 @@ describe.skipIf(MODE === 'record')('web e2e: App-only desktop onboarding', () =>
expect(await page.getByRole('button', { name: 'Next', exact: true })
.evaluate(element => getComputedStyle(element).backgroundColor)).toBe('rgb(72, 73, 76)')
await snapshot('credit-en-dark')
expect(await page.locator('[class*="backIcon"]').evaluate(element => getComputedStyle(element).maskImage)).toContain('data:image/svg+xml')
expect(await page.locator('[class*="backIcon"]').evaluate(element => getComputedStyle(element).maskImage))
.toMatch(/^url\("[^"]*\/plugins\/@deepseek-ai\/dsh-client-ui-settings-account\/assets\/onboarding-back-[0-9a-f]{12}\.svg"\)$/)
await page.getByRole('button', { name: 'Next', exact: true }).click()
await page.getByRole('button', { name: 'Got it', exact: true }).click()
await page.getByRole('checkbox', { name: 'Coding & development', exact: true }).locator('..').click()
@@ -0,0 +1,143 @@
[
{
"stage": "resting",
"icons": [
"Fixture badge",
"Fixture section"
],
"time": true,
"pinned": false,
"actions": false
},
{
"stage": "dark",
"icons": [
"Fixture badge",
"Fixture section"
],
"time": true,
"pinned": false,
"actions": false
},
{
"stage": "hover",
"icons": [],
"time": false,
"pinned": false,
"actions": true
},
{
"stage": "pointer-left",
"icons": [
"Fixture badge",
"Fixture section"
],
"time": true,
"pinned": false,
"actions": false
},
{
"stage": "clicked-current-row",
"icons": [
"Fixture badge",
"Fixture section"
],
"time": true,
"pinned": false,
"actions": false
},
{
"stage": "focus",
"icons": [],
"time": false,
"pinned": false,
"actions": true
},
{
"stage": "menu-open",
"icons": [],
"time": false,
"pinned": false,
"actions": true
},
{
"stage": "blur",
"icons": [
"Fixture badge",
"Fixture section"
],
"time": true,
"pinned": false,
"actions": false
},
{
"stage": "narrow",
"icons": [
"Fixture badge",
"Fixture section"
],
"time": true,
"pinned": false,
"actions": false
},
{
"stage": "touch",
"icons": [
"Fixture badge",
"Fixture section"
],
"time": true,
"pinned": false,
"actions": true
},
{
"stage": "touch-menu-open",
"icons": [
"Fixture badge",
"Fixture section"
],
"time": true,
"pinned": false,
"actions": true
},
{
"stage": "touch-menu-closed",
"icons": [
"Fixture badge",
"Fixture section"
],
"time": true,
"pinned": false,
"actions": true
},
{
"stage": "touch-pinned",
"icons": [
"Fixture badge",
"Fixture section"
],
"time": true,
"pinned": true,
"actions": true
},
{
"stage": "clicked-archived-row",
"icons": [
"Fixture badge",
"Fixture section"
],
"time": true,
"pinned": false,
"actions": false
},
{
"stage": "touch-unarchived",
"icons": [
"Fixture badge",
"Fixture section"
],
"time": true,
"pinned": false,
"actions": true
}
]
@@ -1,7 +1,7 @@
- dialog "选择要添加的模型":
- heading "选择要添加的模型" [level=2]
- dialog "选择模型":
- heading "选择模型" [level=2]
- button "关闭"
- paragraph: 以下是模型提供商的可用模型,勾选要添加的模型。
- paragraph: 以下是模型提供商的可用模型,勾选的模型即为模型列表。
- searchbox "搜索模型"
- button "取消全选"
- list:
@@ -15,4 +15,4 @@
- checkbox "MiniMax-M3" [checked]
- text: MiniMax-M3
- button "取消"
- button "添加所选"
- button "更新列表"
@@ -5,54 +5,9 @@
- paragraph:
- code: "@deepseek-ai/dsh-experimental-badge-skill-bundle"
- paragraph: 让 Agent 为文档、PR 等产出添加「powered by dsh」徽章。
- img "DSH 徽章技能功能预览"
- region "详细介绍":
- heading "@deepseek-ai/dsh-experimental-badge-skill-bundle" [level=1]
- paragraph: English | 中文
- heading "Summary" [level=2]
- paragraph: Let Agents whose presets load skills add a “powered by dsh” badge to documents, pull requests, and other outputs. The shared provider adds no model tool. It ships switched off in Web and Desktop. Select it in Plugins to enable it.
- heading "Table of Contents" [level=2]
- list:
- listitem: Use this package
- listitem: Understand the implementation
- listitem: Further Exploration
- listitem: Model Experience
- listitem: Known Limitations and Deferred Work
- listitem: Dev Note
- separator
- paragraph
- heading "Use this package" [level=2]
- paragraph:
- text: In Web or Desktop, open Plugins and enable
- strong: DSH badge skill
- text: in Official. Switch it off to remove its profile layer.
- paragraph: The switch adds or removes a Host-wide skill provider for existing and new Agents. Presets that expose skill loading can use the badge; minimal keeps its tool catalog unchanged.
- separator
- paragraph
- heading "Understand the implementation" [level=2]
- paragraph:
- code: cordis.patch.yml
- text: contributes the
- code: skill-badge
- text: provider row on the Host. Later root patches can disable that row; an override alone does not select the bundle. The profile composer owns patch ordering and errors.
- separator
- paragraph
- heading "Further Exploration" [level=2]
- paragraph: Capability implementation
- separator
- paragraph
- heading "Model Experience" [level=2]
- paragraph: Indirectly, through the capability implementation, which owns model context and results.
- heading "KV Cache effect" [level=4]
- paragraph: The layer adds no request content directly; the capability implementation owns cache effects from tools, prompts, and results.
- heading "Known Limitations and Deferred Work" [level=2]
- paragraph
- list:
- listitem: A preset must include skill discovery and loading to use the badge. The minimal preset does not gain skill tools from this bundle.
- separator
- paragraph
- heading "Dev Note" [level=3]
- paragraph: None.
- img "DSH 徽章技能功能预览"
- heading "包含的组件" [level=4]
- text: 共 1 个 · 1 已停用
- list:
@@ -1 +1 @@
Prefer showing the primary results within your final response alongside a brief explanation. Use ![Description](<path/to/image.png>) when an image supports an explanation or comparison. Use [Description](<path/to/image.png>) when referring to an image or listing files. Enclose Markdown file destinations in angle brackets, especially paths containing spaces. Do not call present just to list edited source files, or run commands to check whether a diff view will appear. Use present when a separate file card helps the user open the complete deliverable, including images, Office documents, spreadsheets, and slide decks. Each presented file adds a card below the reply, with preview and native-open actions. Avoid repeating results already shown inline unless the separate card adds useful access. Outside commands, configuration expressions, and code blocks, link every mention of an existing file, including repeats and tables, to its full path relative to the working directory or absolute; append #L24 or #L24-L30 to the target for known lines. Use the filename or a clear alias as the label, adding only enough parent directories to distinguish files; keep full paths out of labels. Default to the name alone; when precise locations matter, append :24 or :24–30, with no # or L in the line suffix.
Prefer showing the primary results within your final response alongside a brief explanation. Use ![Description](<path/to/image.png>) when an image supports an explanation or comparison. Use [Description](<path/to/image.png>) when referring to an image or listing files. Enclose Markdown file destinations in angle brackets, especially paths containing spaces. Do not call present just to list edited source files, or run commands to check whether a diff view will appear. Use present to publish complete deliverables, including images, Office documents, spreadsheets, and slide decks, when separate access helps the user. Hosts that support file cards can show the presented file with preview and open actions. Avoid repeating results already shown inline unless the separate card adds useful access. Outside commands, configuration expressions, and code blocks, link every mention of an existing file, including repeats and tables, to its full path relative to the working directory or absolute; append #L24 or #L24-L30 to the target for known lines. Use the filename or a clear alias as the label, adding only enough parent directories to distinguish files; keep full paths out of labels. Default to the name alone; when precise locations matter, append :24 or :24–30, with no # or L in the line suffix.
@@ -3,7 +3,7 @@ window.__ModuleLoader__.load({
id: '@fixture/live-client',
factory(require) {
const React = require('react')
const { MenuItemButton } = require('@deepseek-ai/dsh-client-ui-primitives')
const { IconFolderCloseRegular, IconNewChatOutlineRegular, MenuItemButton } = require('@deepseek-ai/dsh-client-ui-primitives')
const style = document.createElement('style')
style.dataset.plugin = '@fixture/live-client'
style.textContent = '[data-live-client] { color: rgb(12, 34, 56); position: absolute; bottom: 20px; right: 20px; }'
@@ -88,6 +88,18 @@ window.__ModuleLoader__.load({
t(label),
)
})
ctx.slots.inject('sidebar.session.row.trailing', function* () {
for (const [id, order, Icon, label] of [
['fixture.trailing-new-chat', 200, IconNewChatOutlineRegular, 'section'],
['fixture.trailing-folder', 100, IconFolderCloseRegular, 'badge'],
]) {
yield ctx.slots.register({
name: 'sidebar.session.row.trailing', id, order, locale: 'fixtureLive',
}, ({ sessionId, t }) => React.createElement('span', {
role: 'img', 'aria-label': t(label), 'data-session-trailing': sessionId,
}, React.createElement(Icon)))
}
})
ctx.slots.inject('sidebar.workspaces.session.menu.item', function* () {
// The shipped rows end at Session ID (450); these follow as one group,
// opened by the export row's hairline.
+8 -7
View File
@@ -241,14 +241,14 @@ describe('web e2e: Models settings page configures a dormant provider', () => {
expect(tripwire.pageErrors).toEqual([])
}, 60_000)
it('filters the discovered model catalog and clears hidden selections', async () => {
it('filters the discovered model catalog and deselects only visible candidates', async () => {
onTestFailed(() => saveFailureShot(page, 'web-e2e-models-picker'))
const settingsDialog = page.getByRole('dialog', { name: '设置' })
await settingsDialog.getByRole('button', { name: '编辑 minimax-cn' }).click()
await settingsDialog.getByText('自定义设置').click()
await settingsDialog.getByRole('button', { name: '获取可用模型' }).click()
const picker = page.getByRole('dialog', { name: '选择要添加的模型' })
const picker = page.getByRole('dialog', { name: '选择模型' })
await picker.waitFor({ timeout: 10_000 })
const boxes = picker.getByRole('checkbox')
const count = await boxes.count()
@@ -268,12 +268,13 @@ describe('web e2e: Models settings page configures a dormant provider', () => {
await search.fill('')
await expect.poll(async () => boxes.count()).toBe(count)
const restored = await boxes.evaluateAll(nodes => nodes.map(node => (node as HTMLInputElement).checked))
expect(restored).toEqual(Array.from({ length: count }, () => false))
expect(restored.filter(checked => !checked)).toHaveLength(1)
expect(await picker.getByRole('checkbox', { name: 'MiniMax-M2.7-highspeed', exact: true }).isChecked()).toBe(false)
await picker.getByRole('button', { name: '全选' }).waitFor()
await picker.getByRole('button', { name: '全选' }).click()
const snapshot = await captureStableAria(
page,
'[role="dialog"][aria-label="选择要添加的模型"]',
'[role="dialog"][aria-label="选择模型"]',
scaffold.workspaceCwd,
)
await compareOrRefreshGolden(MODEL_PICKER_EXPECTED, snapshot, MODE)
@@ -517,11 +518,11 @@ describe('web e2e: Models settings page configures a dormant provider', () => {
await dialog.getByRole('button', { name: '删除模型 1' }).click()
await dialog.getByRole('button', { name: '获取可用模型' }).click()
const picker = page.getByRole('dialog', { name: '选择要添加的模型' })
const picker = page.getByRole('dialog', { name: '选择模型' })
await picker.getByRole('button', { name: '取消全选' }).click()
await picker.getByRole('searchbox', { name: '搜索模型' }).fill('gpt-6-astra')
await picker.getByRole('checkbox', { name: 'gpt-6-astra', exact: true }).check()
await picker.getByRole('button', { name: '添加所选' }).click()
await picker.getByRole('button', { name: '更新列表' }).click()
await dialog.getByRole('button', { name: '模型选项 1' }).click()
expect(await image.isChecked()).toBe(true)
await dialog.getByRole('button', { name: '保存', exact: true }).click()
@@ -530,7 +531,7 @@ describe('web e2e: Models settings page configures a dormant provider', () => {
} finally {
await scaffold.ctx.settings.mutate('llm-pi-ai', [{ op: 'unset', path: ['providers', 'openai'] }])
await edit.waitFor({ state: 'detached' })
await page.getByRole('dialog', { name: '选择要添加的模型' }).waitFor({ state: 'detached' })
await page.getByRole('dialog', { name: '选择模型' }).waitFor({ state: 'detached' })
}
expect(tripwire.pageErrors).toEqual([])
}, 60_000)
+6 -6
View File
@@ -690,13 +690,13 @@ describe('web e2e: plugin manager', () => {
expect(tripwire.pageErrors).toEqual([])
})
it('shows an optional feature preview before activation and leaves other plugin details unchanged', async () => {
it('shows a bundle detail document image before activation and leaves other plugin details unchanged', async () => {
onTestFailed(() => saveFailureShot(page, 'web-e2e-plugin-manager-preview'))
const panel = await openPluginsPanel()
await panel.getByRole('button', { name: '更多', exact: true }).click()
await panel.getByRole('button', { name: '查看 DSH 徽章技能', exact: true }).click()
const detail = panel.locator('[data-plugin-detail]')
const preview = detail.getByRole('img', { name: 'DSH 徽章技能功能预览', exact: true })
const preview = detail.locator('[data-detail-document]').getByRole('img', { name: 'DSH 徽章技能功能预览', exact: true })
await preview.waitFor()
await preview.evaluate(async (image: HTMLImageElement) => { await image.decode() })
expect(await preview.evaluate((image: HTMLImageElement) => image.naturalWidth)).toBe(640)
@@ -709,8 +709,6 @@ describe('web e2e: plugin manager', () => {
expect(fits).toBe(true)
await detail.getByText(`v${badgeVersion}`, { exact: true }).waitFor()
await detail.getByText(badgeVersion, { exact: true }).waitFor()
// The bundle's README is its detail document, shown between its components and its source.
await detail.locator('[data-detail-document] h1').waitFor()
await compareOrRefreshGolden(join(SNAPSHOT_DIR, 'preview.expected.md'),
await captureStableAria(page, '[data-plugin-panel]', scaffold.workspaceCwd, {
replacements: [[badgeVersion, '{{version}}']],
@@ -718,7 +716,8 @@ describe('web e2e: plugin manager', () => {
const chineseSource = await preview.getAttribute('src')
try {
await setLanguage('en')
const englishPreview = detail.getByRole('img', { name: 'DSH badge skill feature preview', exact: true })
const englishPreview = detail.locator('[data-detail-document]')
.getByRole('img', { name: 'DSH badge skill feature preview', exact: true })
await englishPreview.waitFor()
await englishPreview.evaluate(async (image: HTMLImageElement) => { await image.decode() })
expect(await englishPreview.evaluate((image: HTMLImageElement) => image.naturalWidth)).toBe(640)
@@ -729,7 +728,8 @@ describe('web e2e: plugin manager', () => {
expect(await preview.getAttribute('src')).toBe(chineseSource)
await panel.getByRole('button', { name: '返回 实验性插件', exact: true }).click()
await panel.getByRole('button', { name: '查看 智能体团队', exact: true }).click()
// Detail icons have alt=""; only feature previews expose an image role.
// Detail icons have alt=""; this bundle's README detail document has no images.
await panel.locator('[data-detail-document] h1').waitFor()
expect(await panel.getByRole('img').count()).toBe(0)
await panel.getByRole('button', { name: '返回 实验性插件', exact: true }).click()
expect(tripwire.pageErrors).toEqual([])
+15
View File
@@ -611,6 +611,21 @@ async function toggleOptionalBundles(page: Page): Promise<void> {
await cards.first().waitFor({ timeout: 30_000 })
const listed = await cards.evaluateAll(nodes => nodes.map(node => node.getAttribute('data-plugin-package')))
expect(listed.filter(name => name !== null && OPTIONAL_BUNDLES.includes(name)).length).toBeGreaterThan(0)
// Package assets resolve from the Worker image: ui-chat's injected mask is a Blob URL that decodes.
const mask = await page.evaluate(async () => {
const styles = [...document.querySelectorAll('style[data-plugin="@deepseek-ai/dsh-client-ui-chat"]')]
.map(style => style.textContent).join('\n')
const url = /url\((blob:[^)]+)\)/u.exec(styles)?.[1]
const unresolved = styles.includes('plugins/@deepseek-ai/dsh-client-ui-chat/assets/')
if (url === undefined) return { url: null, width: 0, unresolved }
const image = new Image()
image.src = url
await image.decode()
return { url, width: image.naturalWidth, unresolved }
})
expect(mask.url).toMatch(/^blob:/u)
expect(mask.unresolved).toBe(false)
expect(mask.width).toBeGreaterThan(0)
const result = await page.evaluate(async () => {
type Result<T> = { result: { ok: true; value: T } | { ok: false; error: { message: string } } }
const transport = (globalThis as typeof globalThis & {
+6 -4
View File
@@ -12,6 +12,8 @@ import { connectFreshWorkspace, expandOwningTurnProcess, newEnglishPage, writeCo
const SUMMARY = `Next paragraph: ${'inspect the loaded context and pending tools '.repeat(8).trim()}`
const DELTAS = ['First paragraph', `\nDetails\n\n\n${SUMMARY}`, '\nMore detail']
const UI_EXPECTED = fileURLToPath(new URL('./expected/reasoning-preview/running.expected.md', import.meta.url))
/** Computed mask of the animated whale: the ui-chat package asset URL, whose `@` is percent-encoded. */
const WHALE_MASK = /^url\("([^"]*\/plugins\/@deepseek-ai\/dsh-client-ui-chat\/assets\/running-whale%402x-[0-9a-f]{12}\.png)"\)$/
class PausedReasoningAdapter extends LlmAdapter {
override async listModels(provider: string) { return [{ provider, id: 'paused', name: `${provider}/paused` }] }
@@ -96,14 +98,14 @@ it('shows completed paragraph first lines across blank lines with a right-edge f
await page.emulateMedia({ reducedMotion: 'no-preference', forcedColors: 'none' })
await expect.poll(() => animatedWhale.isVisible()).toBe(true)
expect(await animatedWhale.evaluate(element => getComputedStyle(element).maskMode)).toBe('alpha')
const imageSize = await animatedWhale.evaluate(async (element) => {
const source = getComputedStyle(element).maskImage.match(/^url\("?(data:image\/png;base64,[A-Za-z0-9+/=]+)"?\)$/)?.[1]
if (source === undefined) throw new Error('Running whale mask must use the bundled PNG data URL')
const imageSize = await animatedWhale.evaluate(async (element, mask) => {
const source = getComputedStyle(element).maskImage.match(new RegExp(mask))?.[1]
if (source === undefined) throw new Error('Running whale mask must use the ui-chat package asset URL')
const image = new Image()
image.src = source
await image.decode()
return [image.naturalWidth, image.naturalHeight]
})
}, WHALE_MASK.source)
expect(imageSize).toEqual([28, 28])
expect(await restingWhale.isVisible()).toBe(false)
const firstWhaleFrame = await whale.screenshot({ animations: 'allow' })
+6 -4
View File
@@ -12,6 +12,8 @@ import { newEnglishPage } from './support.ts'
const bundle = fileURLToPath(new URL('../../../packages/experimental/voice-input-bundle', import.meta.url))
const expected = fileURLToPath(new URL('./expected/voice-download.expected.md', import.meta.url))
// The input-device picker on the same page shows its own alert when headless Chromium offers no microphone.
const DOWNLOAD_ALERT = '[role="alert"]:has-text("Download source")'
it('shows the failed asset, actual download source and recovery advice, then retries on request', async () => {
const resources: { scaffold?: WebScaffold; browser?: Browser } = {}
@@ -45,14 +47,14 @@ it('shows the failed asset, actual download source and recovery advice, then ret
await page.getByRole('button', { name: 'Plugins', exact: true }).click()
await page.locator('[data-plugin-package="@deepseek-ai/dsh-experimental-voice-input-bundle"]').getByRole('button').click()
await page.getByRole('button', { name: 'Download and prepare', exact: true }).click()
const alert = page.getByRole('alert')
const alert = page.getByRole('alert').filter({ hasText: 'Download source' })
await alert.waitFor()
expect(await alert.textContent()).toContain('HTTP 503')
expect(await alert.textContent()).toContain(`Download source: ${origin}`)
expect(requests).toHaveLength(1)
expect(requests[0]).toContain('/resolve/2365baeacb507f821a0c8120fcee3d484dba7a07/model.int8.onnx')
await compareOrRefreshGolden(expected,
(await captureStableAria(page, '[role="alert"]', scaffold.workspaceCwd)).replaceAll(origin, 'https://model-mirror.example'), webSnapshotMode())
(await captureStableAria(page, DOWNLOAD_ALERT, scaffold.workspaceCwd)).replaceAll(origin, 'https://model-mirror.example'), webSnapshotMode())
status = 404
await page.getByRole('button', { name: 'Retry preparation', exact: true }).click()
await expect.poll(() => alert.textContent()).toContain('HTTP 404')
@@ -108,7 +110,7 @@ it.each([{ winner: 0, manual: false }, { winner: 1, manual: false }, { winner: 0
await picker.waitFor()
if (manual) await picker.selectOption(origins[winner]!)
await page.getByRole('button', { name: 'Download and prepare', exact: true }).click()
const alert = page.getByRole('alert')
const alert = page.getByRole('alert').filter({ hasText: 'Download source' })
await alert.waitFor()
expect(downloads).toEqual(manual ? [winner] : [winner, 1 - winner])
expect(requestedPaths).toHaveLength(manual ? 1 : 4)
@@ -118,7 +120,7 @@ it.each([{ winner: 0, manual: false }, { winner: 1, manual: false }, { winner: 0
expect(await alert.textContent()).toContain(manual ? 'HTTP 503' : 'HTTP 502')
expect(await alert.textContent()).toContain(`Download source: ${failedOrigin}`)
await compareOrRefreshGolden(manual ? expected : fileURLToPath(new URL('./expected/voice-download-fallback.expected.md', import.meta.url)),
(await captureStableAria(page, '[role="alert"]', scaffold.workspaceCwd)).replaceAll(failedOrigin, manual ? 'https://model-mirror.example' : 'https://fallback-model-source.example'),
(await captureStableAria(page, DOWNLOAD_ALERT, scaffold.workspaceCwd)).replaceAll(failedOrigin, manual ? 'https://model-mirror.example' : 'https://fallback-model-source.example'),
webSnapshotMode())
expect(tripwire.pageErrors).toEqual([])
})
+1
View File
@@ -25,6 +25,7 @@
"tests/collapse-timing.e2e.ts",
"tests/notice-wake-tail.e2e.ts",
"tests/fold-blank-matrix.e2e.ts",
"tests/chat-fold-opened-group.e2e.ts",
"tests/chat-flow-layout.e2e.ts",
"tests/layout-bottom.e2e.ts",
"tests/shortcuts-desktop.e2e.ts",
+4 -4
View File
@@ -18,11 +18,11 @@
en: b9eeb18645121a8b
zh: 70d87a12c43e042a
/deepseek-harness-architecture/core-packages:
en: 20ef882e44b06a10
zh: c10c3934ed5570e9
en: f4dde8809a713bad
zh: ccbee814b4e8b4b7
/deepseek-harness-architecture/events:
en: e89802f066e4955d
zh: ea2fb4bef94e086d
en: c00467ff3a210ef5
zh: 7619d7cca723fa6b
/deepseek-harness-architecture/turn-flow:
en: e6402b28c0ed15fa
zh: 6c07be9047b42056
+2
View File
@@ -61,6 +61,7 @@ Electron starts the private Desktop Host in Electron Node mode. The Host invokes
| [`core/tools`](subsystems/tools.md) | The scoped tool registry and guarded execution pipeline | `ctx.tools` |
| [`core/agent`](subsystems/core.md) | The `Agent` interface, live registry, and `agent/*` events | `ctx.agents` |
| [`core/agent-loop`](subsystems/core.md) | The default driver implementing that interface | `ctx.agentLoop` |
| [`core/agent-controller`](subsystems/core.md#messages-from-outside-an-agent) | Admission and delivery of outside messages | `ctx.agentController` |
| [`core/scope`](subsystems/scope.md) | The per-agent scoped-registration primitive | library, no key |
| [`llm/llm`](subsystems/llm-streaming.md) | Message and stream vocabulary plus the adapter seam | `ctx.llm` |
@@ -70,6 +71,7 @@ Events are the extension points, and picking the right domain is the first decis
- **Session events** are durable facts appended to the log and broadcast through `session/event`. Use one when the fact must survive a reload.
- **Agent events** (`agent/*`) carry a live `Agent`: inbox, step, status, request, validation, continuation. Use one to observe or intercept work in flight.
- **Delivery admission** (`agent-controller/admission`) holds or refuses outside input before it enters the inbox.
- **Capability events** attach policy and adapters to a seam (`fs/*`, `tools/*`, `telemetry/*`) without importing the loop.
AgentLoop awaits serial `agent/created` before queued work and rolls back failed initialization. Unload retains resources through Agent teardown; [agent-loop](../packages/core/agent-loop/README.md#understand-the-implementation) defines ordering.
+2
View File
@@ -61,6 +61,7 @@ Electron 使用 Electron Node 模式启动私有 Desktop Host。Host 调用共
| [`core/tools`](subsystems/tools.zh.md) | 作用域化的工具注册表和带把关的执行流水线 | `ctx.tools` |
| [`core/agent`](subsystems/core.zh.md) | `Agent` 接口、活跃 agent 注册表和 `agent/*` 事件 | `ctx.agents` |
| [`core/agent-loop`](subsystems/core.zh.md) | 实现该接口的默认驱动器 | `ctx.agentLoop` |
| [`core/agent-controller`](subsystems/core.zh.md#messages-from-outside-an-agent) | 外部消息的准入与投递 | `ctx.agentController` |
| [`core/scope`](subsystems/scope.zh.md) | 按 agent 划分作用域的注册原语 | 库,无 ctx 键 |
| [`llm/llm`](subsystems/llm-streaming.zh.md) | 消息与流式词汇表,以及适配器 seam | `ctx.llm` |
@@ -72,6 +73,7 @@ Electron 使用 Electron Node 模式启动私有 Desktop Host。Host 调用共
- **会话事件**是追加到日志并通过 `session/event` 广播的持久事实。当某个事实必须在重新加载后仍然存在时,使用它。
- **Agent 事件**(`agent/*`)携带活跃 `Agent`:inbox、步骤、状态、请求、验证、续跑。要观察或拦截进行中的工作时,使用它。
- **投递准入**(`agent-controller/admission`)在外部输入进入收件箱之前暂留或拒绝它。
- **能力事件**无需导入循环即可向某个 seam(`fs/*`、`tools/*`、`telemetry/*`)附加策略和适配器。
AgentLoop 在启动已排队工作前等待串行 `agent/created`,初始化失败则回滚。卸载在 Agent teardown 期间保留所需资源;[agent-loop](../packages/core/agent-loop/README.zh.md#understand-the-implementation)定义顺序。
+2 -2
View File
@@ -6,5 +6,5 @@
en: 41d772192075e133
zh: 5ef3da69b4de1d8a
/capability-seams-and-core-services:
en: 753b3da3abc6aeea
zh: aab02ec16c3f744b
en: 3e87315977a89b5a
zh: 626a75f3bebaa39f
+5
View File
@@ -191,6 +191,8 @@ flowchart LR
pkg_skill_office["skill-office"]
svc_agents["ctx.agents<br/>Agent service"]
pkg_acp["acp"]
pkg_agent_controller["agent-controller"]
svc_agentController["ctx.agentController<br/>Outside-message delivery"]
svc_agentDefaultModel["ctx.agentDefaultModel<br/>Default Agent model selection"]
svc_agentLoop["ctx.agentLoop<br/>Concrete loop driver"]
pkg_base["base"]
@@ -298,6 +300,7 @@ flowchart LR
svc_dynamicCordisRunner["ctx.dynamicCordisRunner<br/>Dynamic Cordis package host runner"]
svc_cordisInspect["ctx.cordisInspect<br/>Dynamic Cordis inspect registry"]
pkg_agent --> svc_agents
pkg_agent_controller --> svc_agentController
pkg_agent_default_model --> svc_agentDefaultModel
pkg_agent_loop --> svc_agentLoop
pkg_agent_preset_registry --> svc_agentPresets
@@ -456,6 +459,7 @@ flowchart LR
pkg_working_directory --> svc_workingDirectory
pkg_workspace --> svc_workspaceRegistry
pkg_workspace_changes --> svc_workspaceChanges
svc_agentController --> pkg_api_session_controller
svc_agentDefaultModel --> pkg_api_session_controller
svc_agentDefaultModel --> pkg_headless
svc_agentLoop --> pkg_base
@@ -686,6 +690,7 @@ flowchart LR
| `ctx.sessionProjectionCache` | `core` | [`session-projection-cache`](../packages/session/session-projection-cache) | - | [`api-session-controller`](../packages/api/session-controller), [`session-query`](../packages/session-query/session-query), [`session-reference`](../packages/context/session-reference) | - | Durably checkpoints projection unit states per session (throttled + turn/end/detach mandatory points), serves cached projection views, and accelerates prepared-Session projection hydration. |
| `ctx.skills` | `seam` | [`skill`](../packages/skill/skill) | [`sandbox-windows-acl`](../packages/sandbox/sandbox-windows-acl), [`skill-badge`](../packages/experimental/skill-badge), [`skill-filesystem`](../packages/skill/skill-filesystem), [`skill-office`](../packages/skill/skill-office) | [`tool-skill`](../packages/skill/tool-skill) | - | Merges provider skill catalogs; tool-skill renders the session-prefix catalog and loads complete skill bodies. |
| `ctx.agents` | `core` | [`agent`](../packages/core/agent) | - | [`agent-loop`](../packages/core/agent-loop), [`acp`](../packages/acp/acp), [`subagent`](../packages/subagent/subagent) | - | Owns live Agent handles, the create/resume factory seam, and process-local initiator propagation. |
| `ctx.agentController` | `core` | [`agent-controller`](../packages/core/agent-controller) | - | [`api-session-controller`](../packages/api/session-controller) | - | Prepares, admits through the agent-controller/admission stage, and appends messages that senders bring on someone's behalf, one at a time per Agent. |
| `ctx.agentDefaultModel` | `core` | [`agent-default-model`](../packages/core/agent-default-model) | - | [`api-session-controller`](../packages/api/session-controller), [`headless`](../packages/bundle/headless) | - | Reads the default ModelSelection from volatile Config and saves selections through the profile editor. |
| `ctx.agentLoop` | `bundle` | [`agent-loop`](../packages/core/agent-loop) | - | [`base`](../packages/bundle/base), [`sdk-minimal`](../packages/bundle/sdk-minimal) | - | The one concrete loop plugin; extension packages depend on dsh-agent events and services, not on this package. |
| `ctx.schedule` | `core` | [`schedule`](../packages/schedule/schedule) | - | - | - | Stores tasks independently of Session activation and queues due messages in the original Session. |
+5
View File
@@ -193,6 +193,8 @@ flowchart LR
pkg_skill_office["skill-office"]
svc_agents["ctx.agents<br/>Agent service"]
pkg_acp["acp"]
pkg_agent_controller["agent-controller"]
svc_agentController["ctx.agentController<br/>Outside-message delivery"]
svc_agentDefaultModel["ctx.agentDefaultModel<br/>Default Agent model selection"]
svc_agentLoop["ctx.agentLoop<br/>Concrete loop driver"]
pkg_base["base"]
@@ -300,6 +302,7 @@ flowchart LR
svc_dynamicCordisRunner["ctx.dynamicCordisRunner<br/>Dynamic Cordis package host runner"]
svc_cordisInspect["ctx.cordisInspect<br/>Dynamic Cordis inspect registry"]
pkg_agent --> svc_agents
pkg_agent_controller --> svc_agentController
pkg_agent_default_model --> svc_agentDefaultModel
pkg_agent_loop --> svc_agentLoop
pkg_agent_preset_registry --> svc_agentPresets
@@ -458,6 +461,7 @@ flowchart LR
pkg_working_directory --> svc_workingDirectory
pkg_workspace --> svc_workspaceRegistry
pkg_workspace_changes --> svc_workspaceChanges
svc_agentController --> pkg_api_session_controller
svc_agentDefaultModel --> pkg_api_session_controller
svc_agentDefaultModel --> pkg_headless
svc_agentLoop --> pkg_base
@@ -688,6 +692,7 @@ flowchart LR
| `ctx.sessionProjectionCache` | `core` | [`session-projection-cache`](../packages/session/session-projection-cache) | - | [`api-session-controller`](../packages/api/session-controller), [`session-query`](../packages/session-query/session-query), [`session-reference`](../packages/context/session-reference) | - | 按会话持久保存投影单元状态的检查点(节流检查点,以及轮次/结束/分离时的必选检查点),提供缓存投影视图,并加速 prepared Session 的投影恢复。 |
| `ctx.skills` | `seam` | [`skill`](../packages/skill/skill) | [`sandbox-windows-acl`](../packages/sandbox/sandbox-windows-acl), [`skill-badge`](../packages/experimental/skill-badge), [`skill-filesystem`](../packages/skill/skill-filesystem), [`skill-office`](../packages/skill/skill-office) | [`tool-skill`](../packages/skill/tool-skill) | - | 合并提供方的 skill(技能)目录;tool-skill 渲染会话前缀目录,并加载完整的 skill 正文。 |
| `ctx.agents` | `core` | [`agent`](../packages/core/agent) | - | [`agent-loop`](../packages/core/agent-loop), [`acp`](../packages/acp/acp), [`subagent`](../packages/subagent/subagent) | - | 拥有实时 Agent 句柄、创建/恢复工厂 seam,以及进程本地的发起方传播。 |
| `ctx.agentController` | `core` | [`agent-controller`](../packages/core/agent-controller) | - | [`api-session-controller`](../packages/api/session-controller) | - | 准备发送方代表他人送来的消息,经 agent-controller/admission 阶段接纳后放入收件箱,每个 Agent 逐条处理。 |
| `ctx.agentDefaultModel` | `core` | [`agent-default-model`](../packages/core/agent-default-model) | - | [`api-session-controller`](../packages/api/session-controller), [`headless`](../packages/bundle/headless) | - | Reads the default ModelSelection from volatile Config and saves selections through the profile editor. |
| `ctx.agentLoop` | `bundle` | [`agent-loop`](../packages/core/agent-loop) | - | [`base`](../packages/bundle/base), [`sdk-minimal`](../packages/bundle/sdk-minimal) | - | 唯一的具体循环插件;扩展包依赖 dsh-agent 的事件和服务,而不依赖此包。 |
| `ctx.schedule` | `core` | [`schedule`](../packages/schedule/schedule) | - | - | - | 独立于 Session 的加载状态存储任务,并将到期消息排入原 Session。 |
+6 -4
View File
@@ -225,7 +225,7 @@ export interface Config {
## `@deepseek-ai/dsh-api-session-controller`
- `inject`: `agentDefaultModel` · `agents` · `attachments` · `fileUploads` · `fs` · `llm` · `remoteAccess` · `sessions` · `sessionProjections` · `sessionQuery` · `typert` · `workspaceRegistry`
- `inject`: `agentController` · `agentDefaultModel` · `agents` · `attachments` · `fileUploads` · `fs` · `llm` · `remoteAccess` · `sessions` · `sessionProjections` · `sessionQuery` · `typert` · `workspaceRegistry`
- `source`: [`packages/api/session-controller/src/index.ts:93`](../packages/api/session-controller/src/index.ts)
```ts config-catalog
@@ -2799,8 +2799,8 @@ export interface Config {
## `@deepseek-ai/dsh-schedule`
- `inject`: `agents` · `sessions` · `storageDomain` · `sessionController` · `sessionPersistence`
- `source`: [`packages/schedule/schedule/src/index.ts:72`](../packages/schedule/schedule/src/index.ts)
- `inject`: `agents` · `sessions` · `storageDomain` · `sessionPersistence`
- `source`: [`packages/schedule/schedule/src/index.ts:83`](../packages/schedule/schedule/src/index.ts)
```ts config-catalog
/** Configuration for the Host Schedule domain. */
@@ -4696,6 +4696,7 @@ These load from a `cordis.yml` entry with no `config:` block; they declare no co
| --- | --- | --- |
| `@deepseek-ai/dsh-acp-app` | `cmdlineArgs` | [`packages/bundle/acp-app/src/index.ts`](../packages/bundle/acp-app/src/index.ts) |
| `@deepseek-ai/dsh-agent` | `sessionProjections` | [`packages/core/agent/src/index.ts`](../packages/core/agent/src/index.ts) |
| `@deepseek-ai/dsh-agent-controller` | `agents` | [`packages/core/agent-controller/src/index.ts`](../packages/core/agent-controller/src/index.ts) |
| `@deepseek-ai/dsh-api-account-controller` | `deepseekAccount` · `agents` | [`packages/api/account-controller/src/index.ts`](../packages/api/account-controller/src/index.ts) |
| `@deepseek-ai/dsh-api-remote-access` | — | [`packages/api/remote-access/src/index.ts`](../packages/api/remote-access/src/index.ts) |
| `@deepseek-ai/dsh-api-remotes` | `typertGateway` | [`packages/api/remotes/src/index.ts`](../packages/api/remotes/src/index.ts) |
@@ -4714,7 +4715,7 @@ These load from a `cordis.yml` entry with no `config:` block; they declare no co
| `@deepseek-ai/dsh-client-ui-conversation` | — | [`packages/client/ui-conversation/src/index.ts`](../packages/client/ui-conversation/src/index.ts) |
| `@deepseek-ai/dsh-client-ui-cordis` | — | [`packages/extensions/ui-cordis/src/index.ts`](../packages/extensions/ui-cordis/src/index.ts) |
| `@deepseek-ai/dsh-client-ui-deepseek-model-label-1017` | — | [`packages/client/ui-deepseek-model-label-1017/src/index.ts`](../packages/client/ui-deepseek-model-label-1017/src/index.ts) |
| `@deepseek-ai/dsh-client-ui-deliverables` | `systemPrompt` · `connection` · `sessionQuery` · `sessionController` · `workspaceFiles` · `fs` · `sandboxPolicy` · `workspaceChanges` | [`packages/client/ui-deliverables/src/index.ts`](../packages/client/ui-deliverables/src/index.ts) |
| `@deepseek-ai/dsh-client-ui-deliverables` | `connection` · `sessionQuery` · `sessionController` · `workspaceFiles` · `fs` · `sandboxPolicy` · `workspaceChanges` | [`packages/client/ui-deliverables/src/index.ts`](../packages/client/ui-deliverables/src/index.ts) |
| `@deepseek-ai/dsh-client-ui-directory-picker-browse` | — | [`packages/client/ui-directory-picker-browse/src/index.ts`](../packages/client/ui-directory-picker-browse/src/index.ts) |
| `@deepseek-ai/dsh-client-ui-directory-picker-native` | — | [`packages/client/ui-directory-picker-native/src/index.ts`](../packages/client/ui-directory-picker-native/src/index.ts) |
| `@deepseek-ai/dsh-client-ui-goal` | — | [`packages/client/ui-goal/src/index.ts`](../packages/client/ui-goal/src/index.ts) |
@@ -4764,6 +4765,7 @@ These load from a `cordis.yml` entry with no `config:` block; they declare no co
| `@deepseek-ai/dsh-cordis-client-runner` | — | [`packages/extensions/cordis-client-runner/src/index.ts`](../packages/extensions/cordis-client-runner/src/index.ts) |
| `@deepseek-ai/dsh-cua` | — | [`packages/experimental/cua/src/index.ts`](../packages/experimental/cua/src/index.ts) |
| `@deepseek-ai/dsh-deepseek-llm-api-extensions` | — | [`packages/llm/deepseek-llm-api-extensions/src/index.ts`](../packages/llm/deepseek-llm-api-extensions/src/index.ts) |
| `@deepseek-ai/dsh-deliverable-file-references` | `systemPrompt` | [`packages/deliverables/deliverable-file-references/src/index.ts`](../packages/deliverables/deliverable-file-references/src/index.ts) |
| `@deepseek-ai/dsh-experimental-auto-review` | `approval` · `llm` · `permissionPresets` · `sessions` · `tools` · `workingDirectory` | [`packages/experimental/auto-review/src/index.ts`](../packages/experimental/auto-review/src/index.ts) |
| `@deepseek-ai/dsh-experimental-client-ui-agent-team` | — | [`packages/experimental/client-ui-agent-team/src/index.ts`](../packages/experimental/client-ui-agent-team/src/index.ts) |
| `@deepseek-ai/dsh-experimental-client-ui-claude-code-mods` | — | [`packages/experimental/client-ui-claude-code-mods/src/index.ts`](../packages/experimental/client-ui-claude-code-mods/src/index.ts) |
+6 -4
View File
@@ -227,7 +227,7 @@ export interface Config {
## `@deepseek-ai/dsh-api-session-controller`
- `inject`: `agentDefaultModel` · `agents` · `attachments` · `fileUploads` · `fs` · `llm` · `remoteAccess` · `sessions` · `sessionProjections` · `sessionQuery` · `typert` · `workspaceRegistry`
- `inject`: `agentController` · `agentDefaultModel` · `agents` · `attachments` · `fileUploads` · `fs` · `llm` · `remoteAccess` · `sessions` · `sessionProjections` · `sessionQuery` · `typert` · `workspaceRegistry`
- `source`: [`packages/api/session-controller/src/index.ts:93`](../packages/api/session-controller/src/index.ts)
```ts config-catalog
@@ -2801,8 +2801,8 @@ export interface Config {
## `@deepseek-ai/dsh-schedule`
- `inject`: `agents` · `sessions` · `storageDomain` · `sessionController` · `sessionPersistence`
- `source`: [`packages/schedule/schedule/src/index.ts:72`](../packages/schedule/schedule/src/index.ts)
- `inject`: `agents` · `sessions` · `storageDomain` · `sessionPersistence`
- `source`: [`packages/schedule/schedule/src/index.ts:83`](../packages/schedule/schedule/src/index.ts)
```ts config-catalog
/** Configuration for the Host Schedule domain. */
@@ -4698,6 +4698,7 @@ export interface Config {
| --- | --- | --- |
| `@deepseek-ai/dsh-acp-app` | `cmdlineArgs` | [`packages/bundle/acp-app/src/index.ts`](../packages/bundle/acp-app/src/index.ts) |
| `@deepseek-ai/dsh-agent` | `sessionProjections` | [`packages/core/agent/src/index.ts`](../packages/core/agent/src/index.ts) |
| `@deepseek-ai/dsh-agent-controller` | `agents` | [`packages/core/agent-controller/src/index.ts`](../packages/core/agent-controller/src/index.ts) |
| `@deepseek-ai/dsh-api-account-controller` | `deepseekAccount` · `agents` | [`packages/api/account-controller/src/index.ts`](../packages/api/account-controller/src/index.ts) |
| `@deepseek-ai/dsh-api-remote-access` | — | [`packages/api/remote-access/src/index.ts`](../packages/api/remote-access/src/index.ts) |
| `@deepseek-ai/dsh-api-remotes` | `typertGateway` | [`packages/api/remotes/src/index.ts`](../packages/api/remotes/src/index.ts) |
@@ -4716,7 +4717,7 @@ export interface Config {
| `@deepseek-ai/dsh-client-ui-conversation` | — | [`packages/client/ui-conversation/src/index.ts`](../packages/client/ui-conversation/src/index.ts) |
| `@deepseek-ai/dsh-client-ui-cordis` | — | [`packages/extensions/ui-cordis/src/index.ts`](../packages/extensions/ui-cordis/src/index.ts) |
| `@deepseek-ai/dsh-client-ui-deepseek-model-label-1017` | — | [`packages/client/ui-deepseek-model-label-1017/src/index.ts`](../packages/client/ui-deepseek-model-label-1017/src/index.ts) |
| `@deepseek-ai/dsh-client-ui-deliverables` | `systemPrompt` · `connection` · `sessionQuery` · `sessionController` · `workspaceFiles` · `fs` · `sandboxPolicy` · `workspaceChanges` | [`packages/client/ui-deliverables/src/index.ts`](../packages/client/ui-deliverables/src/index.ts) |
| `@deepseek-ai/dsh-client-ui-deliverables` | `connection` · `sessionQuery` · `sessionController` · `workspaceFiles` · `fs` · `sandboxPolicy` · `workspaceChanges` | [`packages/client/ui-deliverables/src/index.ts`](../packages/client/ui-deliverables/src/index.ts) |
| `@deepseek-ai/dsh-client-ui-directory-picker-browse` | — | [`packages/client/ui-directory-picker-browse/src/index.ts`](../packages/client/ui-directory-picker-browse/src/index.ts) |
| `@deepseek-ai/dsh-client-ui-directory-picker-native` | — | [`packages/client/ui-directory-picker-native/src/index.ts`](../packages/client/ui-directory-picker-native/src/index.ts) |
| `@deepseek-ai/dsh-client-ui-goal` | — | [`packages/client/ui-goal/src/index.ts`](../packages/client/ui-goal/src/index.ts) |
@@ -4766,6 +4767,7 @@ export interface Config {
| `@deepseek-ai/dsh-cordis-client-runner` | — | [`packages/extensions/cordis-client-runner/src/index.ts`](../packages/extensions/cordis-client-runner/src/index.ts) |
| `@deepseek-ai/dsh-cua` | — | [`packages/experimental/cua/src/index.ts`](../packages/experimental/cua/src/index.ts) |
| `@deepseek-ai/dsh-deepseek-llm-api-extensions` | — | [`packages/llm/deepseek-llm-api-extensions/src/index.ts`](../packages/llm/deepseek-llm-api-extensions/src/index.ts) |
| `@deepseek-ai/dsh-deliverable-file-references` | `systemPrompt` | [`packages/deliverables/deliverable-file-references/src/index.ts`](../packages/deliverables/deliverable-file-references/src/index.ts) |
| `@deepseek-ai/dsh-experimental-auto-review` | `approval` · `llm` · `permissionPresets` · `sessions` · `tools` · `workingDirectory` | [`packages/experimental/auto-review/src/index.ts`](../packages/experimental/auto-review/src/index.ts) |
| `@deepseek-ai/dsh-experimental-client-ui-agent-team` | — | [`packages/experimental/client-ui-agent-team/src/index.ts`](../packages/experimental/client-ui-agent-team/src/index.ts) |
| `@deepseek-ai/dsh-experimental-client-ui-claude-code-mods` | — | [`packages/experimental/client-ui-claude-code-mods/src/index.ts`](../packages/experimental/client-ui-claude-code-mods/src/index.ts) |
+2 -2
View File
@@ -21,8 +21,8 @@
en: 0894bd9f35430753
zh: 2d28090e71134edd
/cookbook-adding-a-workspace-package/5-add-optional-plugin-display-metadata:
en: 68b463cd3d2e5fb0
zh: 80ea797c5770c573
en: ed501a72ab8ba6e7
zh: 8ad775b735887a10
/cookbook-adding-a-workspace-package/6-verify:
en: 80c7fb90b1175aae
zh: 2ba37962dbc57299
+1 -1
View File
@@ -147,7 +147,7 @@ Export `<package name>/locale/en.json` for locale lookup; expose `<package name>
For an image on bundle cards, details, and component rows, a package-root plugin sets top-level `"icon": "./icon.svg"` in its exported manifest, or exports `./icon` when that field is omitted. A subpath plugin exports its own icon, such as `"./search/icon": "./search.svg"`. Include each image in `files`. A manifest `icon` is relative to the manifest's directory; absolute paths, URLs, paths outside that directory, and symlinks resolving outside it are rejected. An exported icon resolves through Node exports like locale resources and must remain inside its package. SVG, PNG, JPEG (`.jpg`/`.jpeg`), and WebP files are supported up to 256 KiB. Images must be self-contained; SVG is rendered as an image, not inline HTML. The Host returns a data URL without activating the plugin. Invalid declarations or unreadable files produce a metadata diagnostic while retaining valid text, without trying the export; missing or undecodable images use the panel's default artwork.
A bundle can also set `meta.detail` to a Markdown file path relative to the package root, such as `"docs/DETAIL.md"` in `locale/en.json` and `"docs/DETAIL.zh.md"` in `locale/zh.json`; it is read from these files at the package root. The path uses `/`, stays inside the package, ends in `.md` or `.markdown`, and has no package-field fallback; include the file and its images in `files`. A bundle's page in **Plugins**, and its market entry page when the [custom plugin markets bundle](../../packages/experimental/plugin-market-bundle/README.md) is on, show the file for the active UI language as the plugin's detail document. Without `meta.detail` they show the package README, and a market entry page then falls back to the declared repository's README; the [document order](../../packages/util/plugin-detail/README.md#document-order) defines the rules. Write it for users, and reference images by paths relative to the file.
A bundle can also set `meta.detail` to a Markdown file path relative to the package root, such as `"docs/DETAIL.md"` in `locale/en.json` and `"docs/DETAIL.zh.md"` in `locale/zh.json`; it is read from these files at the package root. The path uses `/`, stays inside the package, ends in `.md` or `.markdown`, and has no package-field fallback; keep its images beside it, and list its top-level directory, such as `docs`, in `files` right after the locale resources, as `check-workspace-constraints` requires. The named file must exist in a subdirectory of the package; a root-level document is rejected because its neighboring images would not be published. A bundle's page in **Plugins**, and its market entry page when the [custom plugin markets bundle](../../packages/experimental/plugin-market-bundle/README.md) is on, show the file for the active UI language as the plugin's detail document. Without `meta.detail` they show the package README, and a market entry page then falls back to the declared repository's README; the [document order](../../packages/util/plugin-detail/README.md#document-order) defines the rules. Write it for users, and reference images by paths relative to the file.
Installed bundle cards and details, component lists and configuration details, and Settings' plugin inventory display this metadata, including disabled and preset plugins. Reads do not activate plugins.
+1 -1
View File
@@ -147,7 +147,7 @@ Append-only, prefix-stable, replacing, or independent behavior, including the ex
要在组合包卡片、详情和组件行显示图片,包根插件在导出清单顶层设置 `"icon": "./icon.svg"`,或在省略该字段时导出 `./icon`。子路径插件导出自己的图标,例如 `"./search/icon": "./search.svg"`。将每张图片加入 `files`。清单 `icon` 相对于清单所在目录;绝对路径、URL、目录外路径,以及解析到目录外的符号链接均被拒绝。导出的图标像 locale 资源一样经 Node exports 解析,且必须位于所属包内。支持不超过 256 KiB 的 SVG、PNG、JPEG(`.jpg`/`.jpeg`)和 WebP 文件。图片必须自包含;SVG 作为图片渲染,不作为内联 HTML。Host 返回 data URL,不激活插件。声明无效或文件不可读时,显示元信息诊断并保留有效文本,不再尝试导出的图标;图片缺失或无法解码时使用面板的默认插画。
组合包还可以把 `meta.detail` 设为相对于包根目录的 Markdown 文件路径,例如在 `locale/en.json` 中设为 `"docs/DETAIL.md"`,在 `locale/zh.json` 中设为 `"docs/DETAIL.zh.md"`;读取时使用包根目录下的这些文件。路径使用 `/`,必须位于包内,以 `.md` 或 `.markdown` 结尾,且没有包字段回退;将该文件及其图片加入 `files`。组合包在**插件**中的页面,以及启用[自定义插件市场组合包](../../packages/experimental/plugin-market-bundle/README.zh.md)时其市场条目页面,把当前界面语言对应的该文件作为插件的详细介绍文档显示。没有 `meta.detail` 时,它们显示包 README;市场条目页面在此之后还会回退到所声明仓库的 README;规则由[文档顺序](../../packages/util/plugin-detail/README.zh.md#document-order)定义。该文档面向用户编写,图片使用相对于该文件的路径引用。
组合包还可以把 `meta.detail` 设为相对于包根目录的 Markdown 文件路径,例如在 `locale/en.json` 中设为 `"docs/DETAIL.md"`,在 `locale/zh.json` 中设为 `"docs/DETAIL.zh.md"`;读取时使用包根目录下的这些文件。路径使用 `/`,必须位于包内,以 `.md` 或 `.markdown` 结尾,且没有包字段回退;图片放在该文件旁边,并按 `check-workspace-constraints` 的要求,在 `files` 中紧接语言资源之后列出该文件的顶层目录,例如 `docs`。该文件必须位于包的子目录中且已存在;包根目录下的文档会被拒绝,因为其旁边的图片不会被发布。组合包在**插件**中的页面,以及启用[自定义插件市场组合包](../../packages/experimental/plugin-market-bundle/README.zh.md)时其市场条目页面,把当前界面语言对应的该文件作为插件的详细介绍文档显示。没有 `meta.detail` 时,它们显示包 README;市场条目页面在此之后还会回退到所声明仓库的 README;规则由[文档顺序](../../packages/util/plugin-detail/README.zh.md#document-order)定义。该文档面向用户编写,图片使用相对于该文件的路径引用。
已安装 bundle 的卡片和详情、组件列表与配置详情、设置中的插件清单都展示这些元信息,包括禁用插件和预设内插件。读取时不激活插件。
+18 -18
View File
@@ -2,21 +2,21 @@
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
# After editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/cookbook/adding-a-plugin-preview.md
/cookbook-add-a-built-in-plugin-feature-preview:
en: 1038debc95283b43
zh: a41eae3d9203c400
/cookbook-add-a-built-in-plugin-feature-preview/1-decide-whether-an-image-helps:
en: d2b84c3a4d590f43
zh: b064b42f148bd323
/cookbook-add-a-built-in-plugin-feature-preview/2-draw-one-fixed-size-svg:
en: 1abb5284f499ae7e
zh: b4dacd16d8f04728
/cookbook-add-a-built-in-plugin-feature-preview/3-register-the-internal-pair:
en: e79355a7184d82d3
zh: 92a0e188dee2717c
/cookbook-add-a-built-in-plugin-feature-preview/4-keep-the-page-contract:
en: 095cb97e16c2232e
zh: 2ed9b60710ed522c
/cookbook-add-a-built-in-plugin-feature-preview/5-validate-and-review:
en: 053b2105cc00b8d0
zh: d06467ded2ccac42
/cookbook-add-a-plugin-feature-preview:
en: c37096cd3cdb886c
zh: fc6850d68c638898
/cookbook-add-a-plugin-feature-preview/1-decide-whether-an-image-helps:
en: ee84f52d834cbccf
zh: 7bf3a425a231b5e6
/cookbook-add-a-plugin-feature-preview/2-draw-one-fixed-size-svg:
en: 3a28640aa605dce8
zh: c5336958f92b5a70
/cookbook-add-a-plugin-feature-preview/3-put-it-at-the-top-of-the-detail-document:
en: 17e2ace32a89c419
zh: c635a890e86a41ec
/cookbook-add-a-plugin-feature-preview/4-page-behavior:
en: 20ce358cb110104c
zh: abb8d72fed81f3c8
/cookbook-add-a-plugin-feature-preview/5-validate-and-review:
en: 2888c02bac267662
zh: 5b0e95611c8d5d94
+18 -25
View File
@@ -1,52 +1,45 @@
# Cookbook: Add a built-in plugin feature preview
# Cookbook: Add a plugin feature preview
English | [中文](adding-a-plugin-preview.zh.md)
Plugin Manager owns optional feature illustrations for built-in bundles. Follow this guide when editing its internal `BUNDLE_PREVIEWS` map. Packages cannot declare illustrations through their manifest or exports; third-party plugins have no preview interface.
A feature preview is an optional illustration at the top of a bundle's detail document, the long description that its page in **Plugins** shows first. Follow this guide when drawing one for an official bundle. The detail document itself is declared through [`meta.detail`](adding-a-package.md#plugin-display-metadata).
## 1. Decide whether an image helps
Show one concrete result or recognizable capability. Omit the map entry when the description is enough: no image is required, and omission reserves no space. Essential instructions stay in localized text or configuration outside the illustration.
Show one concrete result or recognizable capability. Omit the image when the description is enough: no image is required. Essential instructions stay in the document's text or in configuration outside the illustration.
## 2. Draw one fixed-size SVG
| Item | Requirement |
|---|---|
| Count | Zero or one logical feature image per bundle. English and Chinese files are alternatives for that image, not a gallery or carousel. |
| Format | SVG only: `width="640" height="300" viewBox="0 0 640 300"` (32:15). Raster preview files and other dimensions are unsupported. |
| Languages | Supply both `en` and `zh`. Translate headings, annotations, and example content; preserve official brand assets and names. No single strings, arrays, or extra languages. |
| Count | Zero or one feature image per bundle. English and Chinese files are alternatives for that image, not a gallery or carousel. |
| Format | SVG: `width="640" height="300" viewBox="0 0 640 300"` (32:15). |
| Languages | Supply both an English and a Chinese file. Translate headings, annotations, and example content; preserve official brand assets and names. |
| Composition | One main example, optionally with one close-up. Keep important content about 32 canvas units from the edges. Review text at the page's actual size. |
| Typography | Outline lettering into paths using a suitable font. Match the feature and neighboring UI; do not rely on browser font substitution. |
| Theme | Compare light and dark themes. One illustration serves both; avoid transparent dark text that disappears on dark backgrounds. |
The SVG is self-contained: no fonts, remote resources, scripts, handlers, animation, stylesheets, `text`, or `foreignObject`. An `image` element may embed an unchanged official PNG/JPEG/WebP brand asset as a base64 data URL. Small plugin icons remain geometry-only. This exception preserves brand artwork and does not permit raster preview files. Validation checks the data-URL envelope, not decoded pixels; the page renders the SVG through `<img>`. Both raw language files together must stay within 1 MiB. They are embedded as base64 data URLs in the Plugin Manager client bundle, so keep them small.
The SVG is self-contained: no fonts, remote resources, scripts, handlers, animation, stylesheets, `text`, or `foreignObject`. An `image` element may embed an unchanged official PNG/JPEG/WebP brand asset as a base64 data URL. Small plugin icons remain geometry-only. The page renders the SVG through `<img>`. Keep each file small; the Host refuses images above plugin-manager's `detailImageMaxBytes`, 5 MiB by default.
## 3. Register the internal pair
## 3. Put it at the top of the detail document
Put `<feature>-preview.svg` and `<feature>-preview.zh.svg` under [Plugin Manager assets](../../packages/client/ui-plugin-manager/src/client/assets/). Import both into [PluginManagerPage.tsx](../../packages/client/ui-plugin-manager/src/client/PluginManagerPage.tsx), then add one exact built-in package name to `BUNDLE_PREVIEWS`:
Save the pair as `docs/preview.svg` and `docs/preview.zh.svg` in the bundle package, beside `docs/DETAIL.md` and `docs/DETAIL.zh.md`, and make the image the first line of each document:
```ts ignore-check
import examplePreview from './assets/example-preview.svg'
import examplePreviewZh from './assets/example-preview.zh.svg'
// One entry in the existing Map<string, Readonly<Record<'en' | 'zh', string>>>.
['@deepseek-ai/dsh-example-bundle', { en: examplePreview, zh: examplePreviewZh }]
```markdown
![Example feature preview](preview.svg)
```
Keep the map's required `en` and `zh` type. Do not add package metadata, a Host read method, exported preview resources, or a third-party declaration path. Use the [badge skill](../../packages/client/ui-plugin-manager/src/client/assets/badge-skill-preview.svg) and [voice input](../../packages/client/ui-plugin-manager/src/client/assets/voice-input-preview.svg) illustrations as examples. The voice transcript is explanatory sample content, not a recording result.
The Chinese document references `preview.zh.svg` with Chinese alternative text. Use the [badge skill](../../packages/experimental/badge-skill-bundle/docs/DETAIL.md) and [voice input](../../packages/experimental/voice-input-bundle/docs/DETAIL.md) documents as examples. The voice transcript is explanatory sample content, not a recording result.
## 4. Keep the page contract
## 4. Page behavior
| Item | Current behavior |
|---|---|
| Position | Below the description and status notices, above components and source information; aligned with the description. |
| Spacing | 24 px above the image; 32 px before the following details section. |
| Display | One `<img>`, natural 640×300 canvas, at most the available width and 320 px high; proportional scaling. The page supplies border and 16 px corners. |
| Language | The current UI language selects one of the required files. Alternative text uses the localized plugin name. |
| Availability | Visible before activation. Decode failure shows a localized notice without blocking controls. Lists and component rows retain their small icons. |
| Position | First in the detail document, which is the first section of the bundle page, below the description and status notices. |
| Display | The detail document renders the image at most its own width, inside its height-bounded scroll area. |
| Language | The document for the UI language references its own file. |
| Availability | Visible before activation. Lists and component rows keep their small icons. |
## 5. Validate and review
Run `pnpm exec vitest run scripts/builtin-plugin-preview.spec.ts packages/client/ui-plugin-manager/tests/components.client.spec.tsx scripts/client-bundle-css.spec.ts`, then `pnpm run build`. The asset test checks every registered pair, fixed SVG dimensions, self-contained content, and combined size. Type checking requires both languages. These checks enforce format; visual quality and equivalent meaning in both languages still require review.
Open the real plugin details in English and Chinese before activation. Decode the image and confirm 640×300 natural dimensions, placement, light/dark readability, and no horizontal overflow at 390 px. Also check a built-in without artwork and a third-party plugin. Follow [record-browser-gif](../../.agents/skills/record-browser-gif/SKILL.md) for PR evidence; record from the final clean commit and label the demonstrated flow accurately.
`scripts/plugin-preview-assets.spec.ts` checks every preview pair in a bundle's `docs` directory: the fixed canvas, absent scripts, stylesheets, text elements, and external references, the 1 MiB pair limit, and English and Chinese files that differ. Then open the real plugin details in English and Chinese before activation and confirm the image loads, its placement, light/dark readability, and no horizontal overflow at 390 px; review owns visual quality and equivalent meaning in both languages. Follow [record-browser-gif](../../.agents/skills/record-browser-gif/SKILL.md) for PR evidence; record from the final clean commit and label the demonstrated flow accurately.
+18 -25
View File
@@ -1,52 +1,45 @@
# Cookbook: 添加内置插件功能配图
# Cookbook: 添加插件功能配图
[English](adding-a-plugin-preview.md) | 中文
插件管理拥有内置组合包的可选功能配图。编辑其内部 `BUNDLE_PREVIEWS` 映射时遵循本指引。包不能通过 manifest 或 exports 声明配图;第三方插件没有配图接口。
功能配图是位于组合包详细介绍文档顶部的可选插图;详细介绍文档是该组合包在**插件**中的页面最先显示的长描述。为官方组合包绘制配图时遵循本指引。详细介绍文档本身通过 [`meta.detail`](adding-a-package.zh.md#plugin-display-metadata) 声明。
## 1. 判断是否需要配图
展示一个具体结果或容易辨认的能力。文字说明足够时不添加映射项:配图不是必需的,省略时不占空间。必要操作说明仍放在图外的本地化文字或配置中。
展示一个具体结果或容易辨认的能力。文字说明足够时省略配图:配图不是必需的。必要操作说明仍放在文档文字或图外的配置中。
## 2. 绘制一张固定尺寸 SVG
| 项目 | 要求 |
|---|---|
| 数量 | 每个组合包零张或一张逻辑配图。英文和中文文件是同一张图的替代版本,不是图库或轮播。 |
| 格式 | 仅 SVG:`width="640" height="300" viewBox="0 0 640 300"`(32:15)。不支持位图配图文件或其他尺寸。 |
| 语言 | 同时提供 `en` 和 `zh`。翻译标题、标注和示例内容,保留官方品牌素材和名称。不使用单个字符串、数组或额外语言。 |
| 数量 | 每个组合包零张或一张配图。英文和中文文件是同一张图的替代版本,不是图库或轮播。 |
| 格式 | SVG:`width="640" height="300" viewBox="0 0 640 300"`(32:15)。 |
| 语言 | 同时提供英文和中文文件。翻译标题、标注和示例内容,保留官方品牌素材和名称。 |
| 构图 | 一个主体示例,可选一个局部特写。重要内容距边缘约 32 个画布单位;按页面实际显示尺寸检查文字。 |
| 字体 | 选择合适字体并把文字转成路径,匹配功能及相邻界面,不依赖浏览器字体替换。 |
| 主题 | 对比浅色和深色主题。同一张图服务两种主题,避免透明底深色文字在深色背景中消失。 |
SVG 必须自包含:不使用字体、远程资源、脚本、事件处理器、动画、样式表、`text` 或 `foreignObject`。`image` 元素可以通过 base64 data URL 嵌入未改动的官方 PNG/JPEG/WebP 品牌素材。小插件图标仍仅使用几何图形。此例外用于保留品牌素材,不允许直接使用位图配图文件。校验检查 data URL 结构,不解码像素;页面通过 `<img>` 渲染 SVG。两种语言的原始文件合计不超过 1 MiB。图片会以 base64 data URL 嵌入插件管理 client bundle,因此应控制大小。
SVG 必须自包含:不使用字体、远程资源、脚本、事件处理器、动画、样式表、`text` 或 `foreignObject`。`image` 元素可以通过 base64 data URL 嵌入未改动的官方 PNG/JPEG/WebP 品牌素材。小插件图标仍仅使用几何图形。页面通过 `<img>` 渲染 SVG。每个文件都应控制大小;Host 会拒绝超过 plugin-manager 的 `detailImageMaxBytes`(默认 5 MiB)的图片。
## 3. 注册内部双语配图
## 3. 放在详细介绍文档顶部
将 `<feature>-preview.svg` 和 `<feature>-preview.zh.svg` 放在[插件管理素材目录](../../packages/client/ui-plugin-manager/src/client/assets/)下。在 [PluginManagerPage.tsx](../../packages/client/ui-plugin-manager/src/client/PluginManagerPage.tsx) 中导入两者,再向 `BUNDLE_PREVIEWS` 添加一个精确的内置包名:
将这对文件保存为组合包中的 `docs/preview.svg` 和 `docs/preview.zh.svg`,与 `docs/DETAIL.md` 和 `docs/DETAIL.zh.md` 放在一起,并把图片作为每份文档的第一行:
```ts ignore-check
import examplePreview from './assets/example-preview.svg'
import examplePreviewZh from './assets/example-preview.zh.svg'
// One entry in the existing Map<string, Readonly<Record<'en' | 'zh', string>>>.
['@deepseek-ai/dsh-example-bundle', { en: examplePreview, zh: examplePreviewZh }]
```markdown
![Example feature preview](preview.svg)
```
保留映射中必需的 `en` 和 `zh` 类型。不添加包元数据、Host 读取方法、导出的配图资源或第三方声明入口。可参考[徽章技能](../../packages/client/ui-plugin-manager/src/client/assets/badge-skill-preview.svg)和[语音输入](../../packages/client/ui-plugin-manager/src/client/assets/voice-input-preview.svg)配图。语音图中的转写文字是说明性示例,不是录音结果。
中文文档引用 `preview.zh.svg`,并使用中文替代文字。可参考[徽章技能](../../packages/experimental/badge-skill-bundle/docs/DETAIL.md)和[语音输入](../../packages/experimental/voice-input-bundle/docs/DETAIL.md)文档。语音图中的转写文字是说明性示例,不是录音结果。
## 4. 保持页面契约
## 4. 页面行为
| 项目 | 当前行为 |
|---|---|
| 位置 | 在描述和状态提示下方、组件与来源信息上方,与说明左边缘对齐。 |
| 间距 | 图片上方 24 px,后续详情区前 32 px。 |
| 显示 | 一个 `<img>`,原始画布 640×300,最大宽度为可用宽度,最大高度 320 px,等比缩放;页面提供边框及 16 px 圆角。 |
| 语言 | 当前 UI 语言选择必需的两份文件之一;替代文字使用本地化插件名。 |
| 可用性 | 启用前可见;解码失败显示本地化提示,不阻塞操作;列表和组件行保留小图标。 |
| 位置 | 位于详细介绍文档开头;该文档是组合包页面的第一个区域,在描述和状态提示下方。 |
| 显示 | 详细介绍文档以不超过文档自身宽度的尺寸渲染图片,图片位于有高度上限的滚动区域内。 |
| 语言 | 当前 UI 语言对应的文档引用其自己的文件。 |
| 可用性 | 启用前可见;列表和组件行保留小图标。 |
## 5. 校验与验收
运行 `pnpm exec vitest run scripts/builtin-plugin-preview.spec.ts packages/client/ui-plugin-manager/tests/components.client.spec.tsx scripts/client-bundle-css.spec.ts`,再运行 `pnpm run build`。素材测试检查每个注册条目的双语文件、固定 SVG 尺寸、自包含内容和合计大小;类型检查要求同时提供两种语言。这些检查约束格式;视觉质量及双语含义一致仍需人工审阅。
在真实插件详情页分别使用英文和中文,启用前解码图片,确认原始尺寸 640×300、插入位置、浅色及深色主题的可读性,以及 390 px 宽度下没有横向溢出。同时检查一个无配图的内置插件和一个第三方插件。按 [record-browser-gif](../../.agents/skills/record-browser-gif/SKILL.md) 制作 PR 证据,从最终干净提交录制,并准确说明演示范围。
`scripts/plugin-preview-assets.spec.ts` 检查组合包 `docs` 目录中的每一对配图:固定画布;不使用脚本、样式表、`text` 元素或外部引用;每对文件合计不超过 1 MiB;英文和中文文件不相同。然后在真实插件详情页分别使用英文和中文,启用前确认图片能加载,并检查插入位置、浅色及深色主题的可读性,以及 390 px 宽度下没有横向溢出;审阅负责检查视觉质量及双语含义是否一致。按 [record-browser-gif](../../.agents/skills/record-browser-gif/SKILL.md) 制作 PR 证据,从最终干净提交录制,并准确说明演示范围。
+8 -6
View File
@@ -8,6 +8,7 @@ This matrix shows which packages dispatch each harness-owned event and which pac
<!-- BEGIN GENERATED event-producer-consumer:events -->
| Event | Mode | Declared in | Dispatchers | Listeners |
| --- | --- | --- | --- | --- |
| `agent-controller/admission` | `waterfall` | [`packages/core/agent-controller/src/types.ts:79`](../packages/core/agent-controller/src/types.ts) | [`agent-controller`](../packages/core/agent-controller) (`waterfall`) | - |
| `agent-loop/config-start-failed` | `emit` | [`packages/core/agent-loop/src/index.ts:196`](../packages/core/agent-loop/src/index.ts) | [`agent-loop`](../packages/core/agent-loop) (`events.dispatch`) | - |
| `agent-preset/selected` | `emit` | [`packages/preset/agent-preset-registry/src/types.ts:66`](../packages/preset/agent-preset-registry/src/types.ts) | [`agent-preset-registry`](../packages/preset/agent-preset-registry) (`emit`) | [`cua`](../packages/experimental/cua), `remotes` |
| `agent/assistant-stream` | `emit` | [`packages/core/agent/src/runtime-types.ts:363`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`headless`](../packages/bundle/headless), `session-controller` |
@@ -22,11 +23,11 @@ This matrix shows which packages dispatch each harness-owned event and which pac
| `agent/request-error` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:353`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compaction-basic`](../packages/compaction/compaction-basic), [`compaction-image-offload`](../packages/compaction/compaction-image-offload), [`llm-retry`](../packages/llm/llm-retry) |
| `agent/status` | `emit` | [`packages/core/agent/src/runtime-types.ts:280`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), `agent-team`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), `server`, `session-controller` |
| `agent/turn-stopping` | `serial` | [`packages/core/agent/src/runtime-types.ts:381`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`workspace-changes`](../packages/deliverables/workspace-changes) |
| `api-session/activity` | `emit` | [`packages/api/session-controller/src/types.ts:626`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
| `api-session/added` | `emit` | [`packages/api/session-controller/src/types.ts:606`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
| `api-session/error` | `emit` | [`packages/api/session-controller/src/types.ts:633`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
| `api-session/removed` | `emit` | [`packages/api/session-controller/src/types.ts:612`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
| `api-session/status` | `emit` | [`packages/api/session-controller/src/types.ts:619`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
| `api-session/activity` | `emit` | [`packages/api/session-controller/src/types.ts:632`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
| `api-session/added` | `emit` | [`packages/api/session-controller/src/types.ts:612`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
| `api-session/error` | `emit` | [`packages/api/session-controller/src/types.ts:639`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
| `api-session/removed` | `emit` | [`packages/api/session-controller/src/types.ts:618`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
| `api-session/status` | `emit` | [`packages/api/session-controller/src/types.ts:625`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
| `app-boot/config-reload` | `emit` | [`packages/boot/app-boot/src/index.ts:54`](../packages/boot/app-boot/src/index.ts) | [`app-boot`](../packages/boot/app-boot) (`emit`) | [`settings`](../packages/settings/settings) |
| `approval/request` | `waterfall` | [`packages/interaction/user-approval/src/types.ts:87`](../packages/interaction/user-approval/src/types.ts) | [`user-approval`](../packages/interaction/user-approval) (`waterfall`) | [`acp`](../packages/acp/acp), `remotes` |
| `authorization/settled` | `emit` | [`packages/credentials/authorization/src/index.ts:57`](../packages/credentials/authorization/src/index.ts) | [`authorization`](../packages/credentials/authorization) (`events.dispatch`) | - |
@@ -62,6 +63,7 @@ This matrix shows which packages dispatch each harness-owned event and which pac
| `plugin-manager/update-state` | `emit` | [`packages/boot/plugin-manager/src/types.ts:412`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` |
| `plugin-market/changed` | `emit` | [`packages/boot/plugin-market/src/types.ts:168`](../packages/boot/plugin-market/src/types.ts) | [`plugin-market`](../packages/boot/plugin-market) (`emit`) | `remotes` |
| `schedule/changed` | `emit` | [`packages/schedule/schedule/src/types.ts:478`](../packages/schedule/schedule/src/types.ts) | [`schedule`](../packages/schedule/schedule) (`emit`) | `remotes` |
| `schedule/resolve-agent` | `waterfall` | [`packages/schedule/schedule/src/index.ts:74`](../packages/schedule/schedule/src/index.ts) | [`schedule`](../packages/schedule/schedule) (`waterfall`) | `server` |
| `session-telemetry/record` | `waterfall` | [`packages/session/session-telemetry/src/index.ts:44`](../packages/session/session-telemetry/src/index.ts) | [`session-telemetry`](../packages/session/session-telemetry) (`waterfall`) | - |
| `session/created` | `emit` | [`packages/core/session/src/index.ts:54`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`permission-presets`](../packages/interaction/permission-presets), [`schedule`](../packages/schedule/schedule), `server`, `session-controller`, [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry) |
| `session/disposed` | `emit` | [`packages/core/session/src/index.ts:63`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `agent-team`, `file-upload`, `session-controller`, [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title), [`workspace-changes`](../packages/deliverables/workspace-changes) |
@@ -81,7 +83,7 @@ This matrix shows which packages dispatch each harness-owned event and which pac
| `tools/pre-execute` | `waterfall` | [`packages/core/tools/src/index.ts:154`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | `auto-review`, [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`tool-jobs`](../packages/jobs/tool-jobs), [`workspace-changes`](../packages/deliverables/workspace-changes) |
| `tools/ptc-dispatch-log` | `waterfall` | [`packages/core/tools/src/index.ts:191`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | [`spill-policy`](../packages/spill/spill-policy) |
| `tools/result` | `emit` | [`packages/core/tools/src/index.ts:199`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`events.dispatch`) | [`agent-instructions`](../packages/context/agent-instructions), `claude-code-mods`, [`subagent`](../packages/subagent/subagent), [`tool-present`](../packages/deliverables/tool-present) |
| `user-questions/request` | `waterfall` | [`packages/interaction/user-questions/src/types.ts:157`](../packages/interaction/user-questions/src/types.ts) | [`user-questions`](../packages/interaction/user-questions) (`waterfall`) | `remotes` |
| `user-questions/request` | `waterfall` | [`packages/interaction/user-questions/src/types.ts:157`](../packages/interaction/user-questions/src/types.ts) | [`user-questions`](../packages/interaction/user-questions) (`waterfall`) | `remotes`, `server` |
| `webserver/index-inject` | `emit` | [`packages/host/webserver/src/index.ts:39`](../packages/host/webserver/src/index.ts) | `webserver` (`emit`) | `connection`, `inspector`, `modules`, `remote-access`, `shortcuts`, `ui-sidebar-documentpreview`, `ui-theme` |
| `workflow/agent-end` | `emit` | [`packages/workflow/workflow/src/index.ts:79`](../packages/workflow/workflow/src/index.ts) | [`workflow`](../packages/workflow/workflow) (`events.dispatch`) | [`tool-workflow`](../packages/workflow/tool-workflow) |
| `workflow/agent-start` | `emit` | [`packages/workflow/workflow/src/index.ts:68`](../packages/workflow/workflow/src/index.ts) | [`workflow`](../packages/workflow/workflow) (`events.dispatch`) | [`tool-workflow`](../packages/workflow/tool-workflow) |
+8 -6
View File
@@ -10,6 +10,7 @@
<!-- BEGIN GENERATED event-producer-consumer:events -->
| Event | Mode | Declared in | Dispatchers | Listeners |
| --- | --- | --- | --- | --- |
| `agent-controller/admission` | `waterfall` | [`packages/core/agent-controller/src/types.ts:79`](../packages/core/agent-controller/src/types.ts) | [`agent-controller`](../packages/core/agent-controller) (`waterfall`) | - |
| `agent-loop/config-start-failed` | `emit` | [`packages/core/agent-loop/src/index.ts:196`](../packages/core/agent-loop/src/index.ts) | [`agent-loop`](../packages/core/agent-loop) (`events.dispatch`) | - |
| `agent-preset/selected` | `emit` | [`packages/preset/agent-preset-registry/src/types.ts:66`](../packages/preset/agent-preset-registry/src/types.ts) | [`agent-preset-registry`](../packages/preset/agent-preset-registry) (`emit`) | [`cua`](../packages/experimental/cua), `remotes` |
| `agent/assistant-stream` | `emit` | [`packages/core/agent/src/runtime-types.ts:363`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`headless`](../packages/bundle/headless), `session-controller` |
@@ -24,11 +25,11 @@
| `agent/request-error` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:353`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compaction-basic`](../packages/compaction/compaction-basic), [`compaction-image-offload`](../packages/compaction/compaction-image-offload), [`llm-retry`](../packages/llm/llm-retry) |
| `agent/status` | `emit` | [`packages/core/agent/src/runtime-types.ts:280`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), `agent-team`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), `server`, `session-controller` |
| `agent/turn-stopping` | `serial` | [`packages/core/agent/src/runtime-types.ts:381`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`workspace-changes`](../packages/deliverables/workspace-changes) |
| `api-session/activity` | `emit` | [`packages/api/session-controller/src/types.ts:626`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
| `api-session/added` | `emit` | [`packages/api/session-controller/src/types.ts:606`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
| `api-session/error` | `emit` | [`packages/api/session-controller/src/types.ts:633`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
| `api-session/removed` | `emit` | [`packages/api/session-controller/src/types.ts:612`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
| `api-session/status` | `emit` | [`packages/api/session-controller/src/types.ts:619`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
| `api-session/activity` | `emit` | [`packages/api/session-controller/src/types.ts:632`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
| `api-session/added` | `emit` | [`packages/api/session-controller/src/types.ts:612`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
| `api-session/error` | `emit` | [`packages/api/session-controller/src/types.ts:639`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
| `api-session/removed` | `emit` | [`packages/api/session-controller/src/types.ts:618`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
| `api-session/status` | `emit` | [`packages/api/session-controller/src/types.ts:625`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
| `app-boot/config-reload` | `emit` | [`packages/boot/app-boot/src/index.ts:54`](../packages/boot/app-boot/src/index.ts) | [`app-boot`](../packages/boot/app-boot) (`emit`) | [`settings`](../packages/settings/settings) |
| `approval/request` | `waterfall` | [`packages/interaction/user-approval/src/types.ts:87`](../packages/interaction/user-approval/src/types.ts) | [`user-approval`](../packages/interaction/user-approval) (`waterfall`) | [`acp`](../packages/acp/acp), `remotes` |
| `authorization/settled` | `emit` | [`packages/credentials/authorization/src/index.ts:57`](../packages/credentials/authorization/src/index.ts) | [`authorization`](../packages/credentials/authorization) (`events.dispatch`) | - |
@@ -64,6 +65,7 @@
| `plugin-manager/update-state` | `emit` | [`packages/boot/plugin-manager/src/types.ts:412`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` |
| `plugin-market/changed` | `emit` | [`packages/boot/plugin-market/src/types.ts:168`](../packages/boot/plugin-market/src/types.ts) | [`plugin-market`](../packages/boot/plugin-market) (`emit`) | `remotes` |
| `schedule/changed` | `emit` | [`packages/schedule/schedule/src/types.ts:478`](../packages/schedule/schedule/src/types.ts) | [`schedule`](../packages/schedule/schedule) (`emit`) | `remotes` |
| `schedule/resolve-agent` | `waterfall` | [`packages/schedule/schedule/src/index.ts:74`](../packages/schedule/schedule/src/index.ts) | [`schedule`](../packages/schedule/schedule) (`waterfall`) | `server` |
| `session-telemetry/record` | `waterfall` | [`packages/session/session-telemetry/src/index.ts:44`](../packages/session/session-telemetry/src/index.ts) | [`session-telemetry`](../packages/session/session-telemetry) (`waterfall`) | - |
| `session/created` | `emit` | [`packages/core/session/src/index.ts:54`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`permission-presets`](../packages/interaction/permission-presets), [`schedule`](../packages/schedule/schedule), `server`, `session-controller`, [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry) |
| `session/disposed` | `emit` | [`packages/core/session/src/index.ts:63`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `agent-team`, `file-upload`, `session-controller`, [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title), [`workspace-changes`](../packages/deliverables/workspace-changes) |
@@ -83,7 +85,7 @@
| `tools/pre-execute` | `waterfall` | [`packages/core/tools/src/index.ts:154`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | `auto-review`, [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`tool-jobs`](../packages/jobs/tool-jobs), [`workspace-changes`](../packages/deliverables/workspace-changes) |
| `tools/ptc-dispatch-log` | `waterfall` | [`packages/core/tools/src/index.ts:191`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | [`spill-policy`](../packages/spill/spill-policy) |
| `tools/result` | `emit` | [`packages/core/tools/src/index.ts:199`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`events.dispatch`) | [`agent-instructions`](../packages/context/agent-instructions), `claude-code-mods`, [`subagent`](../packages/subagent/subagent), [`tool-present`](../packages/deliverables/tool-present) |
| `user-questions/request` | `waterfall` | [`packages/interaction/user-questions/src/types.ts:157`](../packages/interaction/user-questions/src/types.ts) | [`user-questions`](../packages/interaction/user-questions) (`waterfall`) | `remotes` |
| `user-questions/request` | `waterfall` | [`packages/interaction/user-questions/src/types.ts:157`](../packages/interaction/user-questions/src/types.ts) | [`user-questions`](../packages/interaction/user-questions) (`waterfall`) | `remotes`, `server` |
| `webserver/index-inject` | `emit` | [`packages/host/webserver/src/index.ts:39`](../packages/host/webserver/src/index.ts) | `webserver` (`emit`) | `connection`, `inspector`, `modules`, `remote-access`, `shortcuts`, `ui-sidebar-documentpreview`, `ui-theme` |
| `workflow/agent-end` | `emit` | [`packages/workflow/workflow/src/index.ts:79`](../packages/workflow/workflow/src/index.ts) | [`workflow`](../packages/workflow/workflow) (`events.dispatch`) | [`tool-workflow`](../packages/workflow/tool-workflow) |
| `workflow/agent-start` | `emit` | [`packages/workflow/workflow/src/index.ts:68`](../packages/workflow/workflow/src/index.ts) | [`workflow`](../packages/workflow/workflow) (`events.dispatch`) | [`tool-workflow`](../packages/workflow/tool-workflow) |
+2 -2
View File
@@ -12,8 +12,8 @@
en: 08386e96eebf06d9
zh: c6d2b41199b0c466
/bilingual-documentation/scope-and-exclusions:
en: db70e758813d339b
zh: 3dcea283a08c47cb
en: eeda71b7ce01c121
zh: a6361361c1e3d578
/bilingual-documentation/division-of-labor:
en: 88097c97cb72f3ba
zh: 78a18000626cc818
+2
View File
@@ -47,6 +47,8 @@ The gate's limit, stated plainly: **a green gate means the pair was confirmed co
**Scope**: the root `CONTRIBUTING.md`, `BRAND_GUIDELINES.md`, and `SAFETY.md` documents, every non-vendor README, and every active document under `.agents/notes/**`, `docs/**`, and `python/**`. README matching is case-insensitive on the basename and covers future directories without another manifest edit. Dependency and ignored build-output trees and the frozen `.agents/notes/archived/` tree are discovery exclusions, not evolving translation source.
**Outside scope**: a bundle's detail documents under `packages/<group>/<package>/docs/`, which locale `meta.detail` names for the plugin page. The page shows the file for the active UI language, so these documents carry no in-document language switcher and no consistency record; [`verify-md-links`](../../scripts/verify-md-links.ts) and [`verify-md-wrap`](../../scripts/verify-md-wrap.ts) still check both languages, and review keeps them in step.
Generated English references and graphs participate in pairing when a reviewed Chinese counterpart is available. A generator puts its generated data in generated regions and writes each region into both pages, so regeneration never requires a manual Chinese update or a new record; only the prose outside the regions is translated and recorded. Freshness and pairing gates enforce their respective invariants independently.
Shared generated regions, such as the Cordis subsystem regions and the per-package regions of the [config catalog](../config-catalog.md), keep every generated byte identical across languages except localized paired-document paths; labels inside a region are code identifiers rather than translated words. A full-page generator can also render source-owned paired prose, such as [persistence-catalog-text.ts](../../scripts/persistence-catalog-text.ts), from the same structural data with matching Markdown structure and identical code blocks. It regenerates both pages and their consistency record together; review still verifies the paired prose's meaning and terminology.
+2
View File
@@ -49,6 +49,8 @@
**范围**:根目录 `CONTRIBUTING.md`、`BRAND_GUIDELINES.md` 与 `SAFETY.md` 文档、除 vendor 源码外的全部 README,以及 `.agents/notes/**`、`docs/**` 与 `python/**` 下的全部活跃文档。匹配 README 时只看文件名且不区分大小写,因此今后新增的目录无需再修改 manifest。依赖目录、被忽略的构建产物目录以及冻结的 `.agents/notes/archived/` 目录树只在发现阶段排除,不属于持续演进的翻译源文档。
**范围外**:组合包 `packages/<group>/<package>/docs/` 下由 locale `meta.detail` 指定给插件页的详情文档。插件页只显示当前界面语言对应的文件,因此这些文档不带文档内的语言切换行,也没有一致性记录;[`verify-md-links`](../../scripts/verify-md-links.ts) 与 [`verify-md-wrap`](../../scripts/verify-md-wrap.ts) 仍会检查两种语言,两侧是否同步由评审把关。
有经评审的中文对侧的生成英文参考文档和图文档遵循配对规则。生成器把生成数据放在生成区域中,并把每个区域写入两种语言的页面,因此重新生成不需要手工更新中文,也不需要重新记录;只有区域外的说明文字需要翻译并计入记录。新鲜度门禁与配对门禁各自独立强制其约束。
共享生成区域(例如 Cordis 子系统区域和[配置目录](../config-catalog.zh.md)中每个包的区域)在两种语言间保持每个生成字节相同,仅配对文档路径按语言本地化;区域内的标签使用代码标识符而非译文。整页生成器也可以从相同的结构数据渲染源码维护的双语说明,例如 [persistence-catalog-text.ts](../../scripts/persistence-catalog-text.ts),并保持 Markdown 结构对应、代码块完全相同。它同时重新生成两页及其一致性记录;评审仍须验证双语说明的含义与术语。
+24 -12
View File
@@ -41,6 +41,7 @@ flowchart TD
end
subgraph group_core["packages/core"]
pkg_agent["agent"]
pkg_agent_controller["agent-controller"]
pkg_agent_default_model["agent-default-model"]
pkg_agent_loop["agent-loop"]
pkg_agent_tool_presentation["agent-tool-presentation"]
@@ -240,6 +241,7 @@ flowchart TD
pkg_deepseek_account_platform["deepseek-account-platform"]
end
subgraph group_deliverables["packages/deliverables"]
pkg_deliverable_file_references["deliverable-file-references"]
pkg_tool_present["tool-present"]
pkg_workspace_changes["workspace-changes"]
end
@@ -502,6 +504,7 @@ flowchart TD
pkg_attachment_local --> pkg_home_paths
pkg_hmr --> pkg_app_boot
pkg_hmr --> pkg_cmdline
pkg_deliverable_file_references --> pkg_system_prompt
pkg_experimental_computer_use_cua_driver_mcp --> pkg_computer_use
pkg_experimental_webworker_runtime --> pkg_client_connection
pkg_experimental_webworker_runtime --> pkg_client_modules
@@ -638,6 +641,9 @@ flowchart TD
pkg_llm_retry --> pkg_session
pkg_llm_retry --> pkg_session_projection
pkg_llm_retry --> pkg_timeout
pkg_agent_controller --> pkg_agent
pkg_agent_controller --> pkg_llm
pkg_agent_controller --> pkg_scope
pkg_agent_default_model --> pkg_agent
pkg_agent_default_model --> pkg_llm
pkg_goal --> pkg_agent
@@ -1269,6 +1275,7 @@ flowchart TD
pkg_tool_subagent_control --> pkg_subagent
pkg_tool_subagent_control --> pkg_tools
pkg_api_session_controller --> pkg_agent
pkg_api_session_controller --> pkg_agent_controller
pkg_api_session_controller --> pkg_agent_default_model
pkg_api_session_controller --> pkg_agent_preset_registry
pkg_api_session_controller --> pkg_api_gateway
@@ -1361,6 +1368,7 @@ flowchart TD
pkg_sdk_protocol --> pkg_llm
pkg_sdk_protocol --> pkg_session
pkg_sdk_protocol --> pkg_subagent
pkg_sdk_protocol --> pkg_user_questions
pkg_workflow_ptc --> pkg_agent
pkg_workflow_ptc --> pkg_llm
pkg_workflow_ptc --> pkg_ptc_runtime
@@ -1392,15 +1400,6 @@ flowchart TD
pkg_sdk_client --> pkg_llm
pkg_sdk_client --> pkg_sdk_protocol
pkg_sdk_client --> pkg_session
pkg_sdk_jsonrpc_server --> pkg_agent
pkg_sdk_jsonrpc_server --> pkg_attachment
pkg_sdk_jsonrpc_server --> pkg_llm
pkg_sdk_jsonrpc_server --> pkg_llm_deepseek_api_key
pkg_sdk_jsonrpc_server --> pkg_scope
pkg_sdk_jsonrpc_server --> pkg_sdk_protocol
pkg_sdk_jsonrpc_server --> pkg_session
pkg_sdk_jsonrpc_server --> pkg_subagent
pkg_sdk_jsonrpc_server --> pkg_working_directory
pkg_client_test_runtime --> pkg_api_gateway
pkg_client_test_runtime --> pkg_api_session_controller
pkg_client_test_runtime --> pkg_api_workspace_controller
@@ -1430,6 +1429,17 @@ flowchart TD
pkg_tool_schedule --> pkg_schedule
pkg_tool_schedule --> pkg_subagent
pkg_tool_schedule --> pkg_tools
pkg_sdk_jsonrpc_server --> pkg_agent
pkg_sdk_jsonrpc_server --> pkg_attachment
pkg_sdk_jsonrpc_server --> pkg_llm
pkg_sdk_jsonrpc_server --> pkg_llm_deepseek_api_key
pkg_sdk_jsonrpc_server --> pkg_schedule
pkg_sdk_jsonrpc_server --> pkg_scope
pkg_sdk_jsonrpc_server --> pkg_sdk_protocol
pkg_sdk_jsonrpc_server --> pkg_session
pkg_sdk_jsonrpc_server --> pkg_subagent
pkg_sdk_jsonrpc_server --> pkg_user_questions
pkg_sdk_jsonrpc_server --> pkg_working_directory
```
<!-- BEGIN GENERATED module-graph:packages -->
@@ -1597,6 +1607,7 @@ flowchart TD
| [`api-terminal-controller`](../packages/api/terminal-controller) | `api` | [`subprocess`](../packages/subprocess/subprocess) |
| [`attachment-local`](../packages/attachment/attachment-local) | `attachment` | [`attachment`](../packages/attachment/attachment), [`home-paths`](../packages/util/home-paths) |
| [`hmr`](../packages/boot/hmr) | `boot` | [`app-boot`](../packages/boot/app-boot), [`cmdline`](../packages/boot/cmdline) |
| [`deliverable-file-references`](../packages/deliverables/deliverable-file-references) | `deliverables` | [`system-prompt`](../packages/core/system-prompt) |
| [`experimental-computer-use-cua-driver-mcp`](../packages/experimental/computer-use-cua-driver-mcp) | `experimental` | [`computer-use`](../packages/computer-use/computer-use) |
| [`experimental-webworker-runtime`](../packages/experimental/webworker-runtime) | `experimental` | [`client-connection`](../packages/client/connection), [`client-modules`](../packages/client/modules), [`host-webserver`](../packages/host/webserver) |
| [`skill-badge`](../packages/experimental/skill-badge) | `experimental` | [`skill`](../packages/skill/skill) |
@@ -1644,6 +1655,7 @@ flowchart TD
| [`pwsh-local`](../packages/shell/pwsh-local) | `shell` | [`shell`](../packages/shell/shell), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
| [`llm-deepseek-api-key`](../packages/llm/llm-deepseek-api-key) | `llm` | [`credentials`](../packages/credentials/credentials), [`launch-environment`](../packages/util/launch-environment), [`llm`](../packages/llm/llm), [`llm-deepseek`](../packages/llm/llm-deepseek) |
| [`llm-retry`](../packages/llm/llm-retry) | `llm` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`timeout`](../packages/util/timeout) |
| [`agent-controller`](../packages/core/agent-controller) | `core` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope) |
| [`agent-default-model`](../packages/core/agent-default-model) | `core` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm) |
| [`goal`](../packages/goal/goal) | `goal` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`typert-protocol`](../packages/typert/protocol) |
| [`file-reference`](../packages/context/file-reference) | `context` | [`agent`](../packages/core/agent) |
@@ -1762,7 +1774,7 @@ flowchart TD
| [`subagent-spawn-in-process`](../packages/subagent/subagent-spawn-in-process) | `subagent` | [`subagent`](../packages/subagent/subagent) |
| [`tool-subagent`](../packages/subagent/tool-subagent) | `subagent` | [`agent`](../packages/core/agent), [`agent-preset-registry`](../packages/preset/agent-preset-registry), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
| [`tool-subagent-control`](../packages/subagent/tool-subagent-control) | `subagent` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) |
| [`api-session-controller`](../packages/api/session-controller) | `api` | [`agent`](../packages/core/agent), [`agent-default-model`](../packages/core/agent-default-model), [`agent-preset-registry`](../packages/preset/agent-preset-registry), [`api-gateway`](../packages/api/gateway), [`attachment`](../packages/attachment/attachment), [`client-connection`](../packages/client/connection), [`client-file-upload`](../packages/client/file-upload), [`commands`](../packages/interaction/commands), [`credentials`](../packages/credentials/credentials), [`file-reference`](../packages/context/file-reference), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`native-command`](../packages/util/native-command), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-query`](../packages/session-query/session-query), [`session-title`](../packages/session/session-title), [`settings`](../packages/settings/settings), [`skill`](../packages/skill/skill), [`subagent`](../packages/subagent/subagent), [`typert-protocol`](../packages/typert/protocol), [`typert-registry`](../packages/typert/registry), [`util-time`](../packages/util/time), [`util-values`](../packages/util/values), [`util-workspace-path`](../packages/util/workspace-path), [`working-directory`](../packages/session/working-directory), [`workspace`](../packages/workspace/workspace) |
| [`api-session-controller`](../packages/api/session-controller) | `api` | [`agent`](../packages/core/agent), [`agent-controller`](../packages/core/agent-controller), [`agent-default-model`](../packages/core/agent-default-model), [`agent-preset-registry`](../packages/preset/agent-preset-registry), [`api-gateway`](../packages/api/gateway), [`attachment`](../packages/attachment/attachment), [`client-connection`](../packages/client/connection), [`client-file-upload`](../packages/client/file-upload), [`commands`](../packages/interaction/commands), [`credentials`](../packages/credentials/credentials), [`file-reference`](../packages/context/file-reference), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`native-command`](../packages/util/native-command), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-query`](../packages/session-query/session-query), [`session-title`](../packages/session/session-title), [`settings`](../packages/settings/settings), [`skill`](../packages/skill/skill), [`subagent`](../packages/subagent/subagent), [`typert-protocol`](../packages/typert/protocol), [`typert-registry`](../packages/typert/registry), [`util-time`](../packages/util/time), [`util-values`](../packages/util/values), [`util-workspace-path`](../packages/util/workspace-path), [`working-directory`](../packages/session/working-directory), [`workspace`](../packages/workspace/workspace) |
| [`plugin-market`](../packages/boot/plugin-market) | `boot` | [`plugin-manager`](../packages/boot/plugin-manager), [`settings`](../packages/settings/settings), [`subprocess`](../packages/subprocess/subprocess), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol) |
| [`headless`](../packages/bundle/headless) | `bundle` | [`agent`](../packages/core/agent), [`agent-default-model`](../packages/core/agent-default-model), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`permission-presets`](../packages/interaction/permission-presets), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`session-query`](../packages/session-query/session-query), [`subagent`](../packages/subagent/subagent), [`user-approval`](../packages/interaction/user-approval) |
| [`compaction-basic`](../packages/compaction/compaction-basic) | `compaction` | [`agent`](../packages/core/agent), [`commands`](../packages/interaction/commands), [`compaction`](../packages/compaction/compaction), [`compaction-tool-result-pruner`](../packages/compaction/compaction-tool-result-pruner), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`token-meter`](../packages/llm/token-meter) |
@@ -1772,13 +1784,13 @@ flowchart TD
| [`experimental-tool-subagent-oar`](../packages/experimental/tool-subagent-oar) | `experimental` | [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) |
| [`hooks-claude-code`](../packages/experimental/hooks-claude-code) | `experimental` | [`agent`](../packages/core/agent), [`hook-protocol`](../packages/experimental/hook-protocol), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) |
| [`tool-ralph`](../packages/experimental/tool-ralph) | `experimental` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) |
| [`sdk-protocol`](../packages/sdk/protocol) | `sdk` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) |
| [`sdk-protocol`](../packages/sdk/protocol) | `sdk` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`user-questions`](../packages/interaction/user-questions) |
| [`workflow-ptc`](../packages/workflow/workflow-ptc) | `workflow` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`ptc-runtime`](../packages/ptc-runtime/ptc-runtime), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow), [`working-directory`](../packages/session/working-directory) |
| [`experimental-tool-agent-team`](../packages/experimental/tool-agent-team) | `experimental` | [`agent`](../packages/core/agent), [`experimental-agent-team`](../packages/experimental/agent-team), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
| [`schedule`](../packages/schedule/schedule) | `schedule` | [`agent`](../packages/core/agent), [`api-session-controller`](../packages/api/session-controller), [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`storage-domain`](../packages/storage/storage-domain), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol), [`util-time`](../packages/util/time), [`workspace`](../packages/workspace/workspace) |
| [`sdk-client`](../packages/sdk/client) | `sdk` | [`llm`](../packages/llm/llm), [`sdk-protocol`](../packages/sdk/protocol), [`session`](../packages/core/session) |
| [`sdk-jsonrpc-server`](../packages/sdk/server) | `sdk` | [`agent`](../packages/core/agent), [`attachment`](../packages/attachment/attachment), [`llm`](../packages/llm/llm), [`llm-deepseek-api-key`](../packages/llm/llm-deepseek-api-key), [`scope`](../packages/core/scope), [`sdk-protocol`](../packages/sdk/protocol), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`working-directory`](../packages/session/working-directory) |
| [`client-test-runtime`](../packages/test-support/client-runtime) | `test-support` | [`api-gateway`](../packages/api/gateway), [`api-session-controller`](../packages/api/session-controller), [`api-workspace-controller`](../packages/api/workspace-controller), [`attachment`](../packages/attachment/attachment), [`client-connection`](../packages/client/connection), [`client-hmr`](../packages/client/hmr), [`client-modules`](../packages/client/modules), [`client-store`](../packages/client/store), [`client-ui-chat`](../packages/client/ui-chat), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-settings`](../packages/client/ui-settings), [`client-ui-slots`](../packages/client/ui-slots), [`client-web`](../packages/client/web), [`remote-mock`](../packages/test-support/remote-mock), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`typert-protocol`](../packages/typert/protocol) |
| [`subagent-dsh-sdk`](../packages/subagent/subagent-dsh-sdk) | `subagent` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`sdk-client`](../packages/sdk/client), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess) |
| [`tool-schedule`](../packages/schedule/tool-schedule) | `schedule` | [`llm`](../packages/llm/llm), [`schedule`](../packages/schedule/schedule), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) |
| [`sdk-jsonrpc-server`](../packages/sdk/server) | `sdk` | [`agent`](../packages/core/agent), [`attachment`](../packages/attachment/attachment), [`llm`](../packages/llm/llm), [`llm-deepseek-api-key`](../packages/llm/llm-deepseek-api-key), [`schedule`](../packages/schedule/schedule), [`scope`](../packages/core/scope), [`sdk-protocol`](../packages/sdk/protocol), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`user-questions`](../packages/interaction/user-questions), [`working-directory`](../packages/session/working-directory) |
<!-- END GENERATED module-graph:packages -->
+24 -12
View File
@@ -43,6 +43,7 @@ flowchart TD
end
subgraph group_core["packages/core"]
pkg_agent["agent"]
pkg_agent_controller["agent-controller"]
pkg_agent_default_model["agent-default-model"]
pkg_agent_loop["agent-loop"]
pkg_agent_tool_presentation["agent-tool-presentation"]
@@ -242,6 +243,7 @@ flowchart TD
pkg_deepseek_account_platform["deepseek-account-platform"]
end
subgraph group_deliverables["packages/deliverables"]
pkg_deliverable_file_references["deliverable-file-references"]
pkg_tool_present["tool-present"]
pkg_workspace_changes["workspace-changes"]
end
@@ -504,6 +506,7 @@ flowchart TD
pkg_attachment_local --> pkg_home_paths
pkg_hmr --> pkg_app_boot
pkg_hmr --> pkg_cmdline
pkg_deliverable_file_references --> pkg_system_prompt
pkg_experimental_computer_use_cua_driver_mcp --> pkg_computer_use
pkg_experimental_webworker_runtime --> pkg_client_connection
pkg_experimental_webworker_runtime --> pkg_client_modules
@@ -640,6 +643,9 @@ flowchart TD
pkg_llm_retry --> pkg_session
pkg_llm_retry --> pkg_session_projection
pkg_llm_retry --> pkg_timeout
pkg_agent_controller --> pkg_agent
pkg_agent_controller --> pkg_llm
pkg_agent_controller --> pkg_scope
pkg_agent_default_model --> pkg_agent
pkg_agent_default_model --> pkg_llm
pkg_goal --> pkg_agent
@@ -1271,6 +1277,7 @@ flowchart TD
pkg_tool_subagent_control --> pkg_subagent
pkg_tool_subagent_control --> pkg_tools
pkg_api_session_controller --> pkg_agent
pkg_api_session_controller --> pkg_agent_controller
pkg_api_session_controller --> pkg_agent_default_model
pkg_api_session_controller --> pkg_agent_preset_registry
pkg_api_session_controller --> pkg_api_gateway
@@ -1363,6 +1370,7 @@ flowchart TD
pkg_sdk_protocol --> pkg_llm
pkg_sdk_protocol --> pkg_session
pkg_sdk_protocol --> pkg_subagent
pkg_sdk_protocol --> pkg_user_questions
pkg_workflow_ptc --> pkg_agent
pkg_workflow_ptc --> pkg_llm
pkg_workflow_ptc --> pkg_ptc_runtime
@@ -1394,15 +1402,6 @@ flowchart TD
pkg_sdk_client --> pkg_llm
pkg_sdk_client --> pkg_sdk_protocol
pkg_sdk_client --> pkg_session
pkg_sdk_jsonrpc_server --> pkg_agent
pkg_sdk_jsonrpc_server --> pkg_attachment
pkg_sdk_jsonrpc_server --> pkg_llm
pkg_sdk_jsonrpc_server --> pkg_llm_deepseek_api_key
pkg_sdk_jsonrpc_server --> pkg_scope
pkg_sdk_jsonrpc_server --> pkg_sdk_protocol
pkg_sdk_jsonrpc_server --> pkg_session
pkg_sdk_jsonrpc_server --> pkg_subagent
pkg_sdk_jsonrpc_server --> pkg_working_directory
pkg_client_test_runtime --> pkg_api_gateway
pkg_client_test_runtime --> pkg_api_session_controller
pkg_client_test_runtime --> pkg_api_workspace_controller
@@ -1432,6 +1431,17 @@ flowchart TD
pkg_tool_schedule --> pkg_schedule
pkg_tool_schedule --> pkg_subagent
pkg_tool_schedule --> pkg_tools
pkg_sdk_jsonrpc_server --> pkg_agent
pkg_sdk_jsonrpc_server --> pkg_attachment
pkg_sdk_jsonrpc_server --> pkg_llm
pkg_sdk_jsonrpc_server --> pkg_llm_deepseek_api_key
pkg_sdk_jsonrpc_server --> pkg_schedule
pkg_sdk_jsonrpc_server --> pkg_scope
pkg_sdk_jsonrpc_server --> pkg_sdk_protocol
pkg_sdk_jsonrpc_server --> pkg_session
pkg_sdk_jsonrpc_server --> pkg_subagent
pkg_sdk_jsonrpc_server --> pkg_user_questions
pkg_sdk_jsonrpc_server --> pkg_working_directory
```
<!-- BEGIN GENERATED module-graph:packages -->
@@ -1599,6 +1609,7 @@ flowchart TD
| [`api-terminal-controller`](../packages/api/terminal-controller) | `api` | [`subprocess`](../packages/subprocess/subprocess) |
| [`attachment-local`](../packages/attachment/attachment-local) | `attachment` | [`attachment`](../packages/attachment/attachment), [`home-paths`](../packages/util/home-paths) |
| [`hmr`](../packages/boot/hmr) | `boot` | [`app-boot`](../packages/boot/app-boot), [`cmdline`](../packages/boot/cmdline) |
| [`deliverable-file-references`](../packages/deliverables/deliverable-file-references) | `deliverables` | [`system-prompt`](../packages/core/system-prompt) |
| [`experimental-computer-use-cua-driver-mcp`](../packages/experimental/computer-use-cua-driver-mcp) | `experimental` | [`computer-use`](../packages/computer-use/computer-use) |
| [`experimental-webworker-runtime`](../packages/experimental/webworker-runtime) | `experimental` | [`client-connection`](../packages/client/connection), [`client-modules`](../packages/client/modules), [`host-webserver`](../packages/host/webserver) |
| [`skill-badge`](../packages/experimental/skill-badge) | `experimental` | [`skill`](../packages/skill/skill) |
@@ -1646,6 +1657,7 @@ flowchart TD
| [`pwsh-local`](../packages/shell/pwsh-local) | `shell` | [`shell`](../packages/shell/shell), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
| [`llm-deepseek-api-key`](../packages/llm/llm-deepseek-api-key) | `llm` | [`credentials`](../packages/credentials/credentials), [`launch-environment`](../packages/util/launch-environment), [`llm`](../packages/llm/llm), [`llm-deepseek`](../packages/llm/llm-deepseek) |
| [`llm-retry`](../packages/llm/llm-retry) | `llm` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`timeout`](../packages/util/timeout) |
| [`agent-controller`](../packages/core/agent-controller) | `core` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope) |
| [`agent-default-model`](../packages/core/agent-default-model) | `core` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm) |
| [`goal`](../packages/goal/goal) | `goal` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`typert-protocol`](../packages/typert/protocol) |
| [`file-reference`](../packages/context/file-reference) | `context` | [`agent`](../packages/core/agent) |
@@ -1764,7 +1776,7 @@ flowchart TD
| [`subagent-spawn-in-process`](../packages/subagent/subagent-spawn-in-process) | `subagent` | [`subagent`](../packages/subagent/subagent) |
| [`tool-subagent`](../packages/subagent/tool-subagent) | `subagent` | [`agent`](../packages/core/agent), [`agent-preset-registry`](../packages/preset/agent-preset-registry), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
| [`tool-subagent-control`](../packages/subagent/tool-subagent-control) | `subagent` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) |
| [`api-session-controller`](../packages/api/session-controller) | `api` | [`agent`](../packages/core/agent), [`agent-default-model`](../packages/core/agent-default-model), [`agent-preset-registry`](../packages/preset/agent-preset-registry), [`api-gateway`](../packages/api/gateway), [`attachment`](../packages/attachment/attachment), [`client-connection`](../packages/client/connection), [`client-file-upload`](../packages/client/file-upload), [`commands`](../packages/interaction/commands), [`credentials`](../packages/credentials/credentials), [`file-reference`](../packages/context/file-reference), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`native-command`](../packages/util/native-command), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-query`](../packages/session-query/session-query), [`session-title`](../packages/session/session-title), [`settings`](../packages/settings/settings), [`skill`](../packages/skill/skill), [`subagent`](../packages/subagent/subagent), [`typert-protocol`](../packages/typert/protocol), [`typert-registry`](../packages/typert/registry), [`util-time`](../packages/util/time), [`util-values`](../packages/util/values), [`util-workspace-path`](../packages/util/workspace-path), [`working-directory`](../packages/session/working-directory), [`workspace`](../packages/workspace/workspace) |
| [`api-session-controller`](../packages/api/session-controller) | `api` | [`agent`](../packages/core/agent), [`agent-controller`](../packages/core/agent-controller), [`agent-default-model`](../packages/core/agent-default-model), [`agent-preset-registry`](../packages/preset/agent-preset-registry), [`api-gateway`](../packages/api/gateway), [`attachment`](../packages/attachment/attachment), [`client-connection`](../packages/client/connection), [`client-file-upload`](../packages/client/file-upload), [`commands`](../packages/interaction/commands), [`credentials`](../packages/credentials/credentials), [`file-reference`](../packages/context/file-reference), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`native-command`](../packages/util/native-command), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-query`](../packages/session-query/session-query), [`session-title`](../packages/session/session-title), [`settings`](../packages/settings/settings), [`skill`](../packages/skill/skill), [`subagent`](../packages/subagent/subagent), [`typert-protocol`](../packages/typert/protocol), [`typert-registry`](../packages/typert/registry), [`util-time`](../packages/util/time), [`util-values`](../packages/util/values), [`util-workspace-path`](../packages/util/workspace-path), [`working-directory`](../packages/session/working-directory), [`workspace`](../packages/workspace/workspace) |
| [`plugin-market`](../packages/boot/plugin-market) | `boot` | [`plugin-manager`](../packages/boot/plugin-manager), [`settings`](../packages/settings/settings), [`subprocess`](../packages/subprocess/subprocess), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol) |
| [`headless`](../packages/bundle/headless) | `bundle` | [`agent`](../packages/core/agent), [`agent-default-model`](../packages/core/agent-default-model), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`permission-presets`](../packages/interaction/permission-presets), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`session-query`](../packages/session-query/session-query), [`subagent`](../packages/subagent/subagent), [`user-approval`](../packages/interaction/user-approval) |
| [`compaction-basic`](../packages/compaction/compaction-basic) | `compaction` | [`agent`](../packages/core/agent), [`commands`](../packages/interaction/commands), [`compaction`](../packages/compaction/compaction), [`compaction-tool-result-pruner`](../packages/compaction/compaction-tool-result-pruner), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`token-meter`](../packages/llm/token-meter) |
@@ -1774,13 +1786,13 @@ flowchart TD
| [`experimental-tool-subagent-oar`](../packages/experimental/tool-subagent-oar) | `experimental` | [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) |
| [`hooks-claude-code`](../packages/experimental/hooks-claude-code) | `experimental` | [`agent`](../packages/core/agent), [`hook-protocol`](../packages/experimental/hook-protocol), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) |
| [`tool-ralph`](../packages/experimental/tool-ralph) | `experimental` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) |
| [`sdk-protocol`](../packages/sdk/protocol) | `sdk` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) |
| [`sdk-protocol`](../packages/sdk/protocol) | `sdk` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`user-questions`](../packages/interaction/user-questions) |
| [`workflow-ptc`](../packages/workflow/workflow-ptc) | `workflow` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`ptc-runtime`](../packages/ptc-runtime/ptc-runtime), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow), [`working-directory`](../packages/session/working-directory) |
| [`experimental-tool-agent-team`](../packages/experimental/tool-agent-team) | `experimental` | [`agent`](../packages/core/agent), [`experimental-agent-team`](../packages/experimental/agent-team), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
| [`schedule`](../packages/schedule/schedule) | `schedule` | [`agent`](../packages/core/agent), [`api-session-controller`](../packages/api/session-controller), [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`storage-domain`](../packages/storage/storage-domain), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol), [`util-time`](../packages/util/time), [`workspace`](../packages/workspace/workspace) |
| [`sdk-client`](../packages/sdk/client) | `sdk` | [`llm`](../packages/llm/llm), [`sdk-protocol`](../packages/sdk/protocol), [`session`](../packages/core/session) |
| [`sdk-jsonrpc-server`](../packages/sdk/server) | `sdk` | [`agent`](../packages/core/agent), [`attachment`](../packages/attachment/attachment), [`llm`](../packages/llm/llm), [`llm-deepseek-api-key`](../packages/llm/llm-deepseek-api-key), [`scope`](../packages/core/scope), [`sdk-protocol`](../packages/sdk/protocol), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`working-directory`](../packages/session/working-directory) |
| [`client-test-runtime`](../packages/test-support/client-runtime) | `test-support` | [`api-gateway`](../packages/api/gateway), [`api-session-controller`](../packages/api/session-controller), [`api-workspace-controller`](../packages/api/workspace-controller), [`attachment`](../packages/attachment/attachment), [`client-connection`](../packages/client/connection), [`client-hmr`](../packages/client/hmr), [`client-modules`](../packages/client/modules), [`client-store`](../packages/client/store), [`client-ui-chat`](../packages/client/ui-chat), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-settings`](../packages/client/ui-settings), [`client-ui-slots`](../packages/client/ui-slots), [`client-web`](../packages/client/web), [`remote-mock`](../packages/test-support/remote-mock), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`typert-protocol`](../packages/typert/protocol) |
| [`subagent-dsh-sdk`](../packages/subagent/subagent-dsh-sdk) | `subagent` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`sdk-client`](../packages/sdk/client), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess) |
| [`tool-schedule`](../packages/schedule/tool-schedule) | `schedule` | [`llm`](../packages/llm/llm), [`schedule`](../packages/schedule/schedule), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) |
| [`sdk-jsonrpc-server`](../packages/sdk/server) | `sdk` | [`agent`](../packages/core/agent), [`attachment`](../packages/attachment/attachment), [`llm`](../packages/llm/llm), [`llm-deepseek-api-key`](../packages/llm/llm-deepseek-api-key), [`schedule`](../packages/schedule/schedule), [`scope`](../packages/core/scope), [`sdk-protocol`](../packages/sdk/protocol), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`user-questions`](../packages/interaction/user-questions), [`working-directory`](../packages/session/working-directory) |
<!-- END GENERATED module-graph:packages -->
+4 -4
View File
@@ -1347,8 +1347,8 @@
en: 2786fef560851bb4
zh: 5d787475356b2606
/session-persistence-event-catalog/resolved-persistence-types/string:
en: 41c03b03d1bd5a44
zh: 1b0e430af6c140b3
en: 324d7ebffee10aad
zh: 31f7b8a18571665a
/session-persistence-event-catalog/resolved-persistence-types/string~2:
en: 0089620bf4638263
zh: 1922bef1713094bb
@@ -1833,8 +1833,8 @@
en: 809a23c25ded8497
zh: e9cd23e281f8d31f
/session-persistence-event-catalog/resolved-persistence-types/kind-user:
en: 53b7abac679ee252
zh: 63eb6a015986ef6d
en: db30e659a1cc4c06
zh: 8585ea4c8178ab37
/session-persistence-event-catalog/resolved-persistence-types/kind-user-approval:
en: a52d2dfbaf844127
zh: 4c5306ca40f20873
+2 -2
View File
@@ -5583,7 +5583,7 @@ Array of `number`.
SHA-256: `4cd48f3e5108bd6ebedf7301c3638839b8b2b9db8dbd493974177fbdeb91a01b`
Sources: [`packages/api/session-controller/src/types.ts:406`](../packages/api/session-controller/src/types.ts) · [`packages/attachment/attachment/src/brand.ts:6`](../packages/attachment/attachment/src/brand.ts) · [`packages/compaction/compaction/src/brand.ts:4`](../packages/compaction/compaction/src/brand.ts) · [`packages/core/session/src/types.ts:21`](../packages/core/session/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:21`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:33`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:9`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/webhook/src/brand.ts:12`](../packages/experimental/webhook/src/brand.ts) · [`packages/experimental/webhook/src/brand.ts:6`](../packages/experimental/webhook/src/brand.ts) · [`packages/experimental/webhook/src/brand.ts:9`](../packages/experimental/webhook/src/brand.ts) · [`packages/feedback/message-feedback/src/types.ts:14`](../packages/feedback/message-feedback/src/types.ts) · [`packages/goal/goal/src/types.ts:17`](../packages/goal/goal/src/types.ts) · [`packages/interaction/commands/src/brand.ts:31`](../packages/interaction/commands/src/brand.ts) · [`packages/interaction/user-approval/src/types.ts:17`](../packages/interaction/user-approval/src/types.ts) · [`packages/jobs/jobs/src/brand.ts:19`](../packages/jobs/jobs/src/brand.ts) · [`packages/llm/llm-retry/src/brand.ts:4`](../packages/llm/llm-retry/src/brand.ts) · [`packages/llm/llm/src/brand.ts:16`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:31`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:43`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:67`](../packages/llm/llm/src/brand.ts) · [`packages/schedule/schedule/src/types.ts:14`](../packages/schedule/schedule/src/types.ts) · [`packages/session/session-title/src/index.ts:42`](../packages/session/session-title/src/index.ts) · [`packages/session/session-title/src/types.ts:17`](../packages/session/session-title/src/types.ts) · [`packages/workflow/workflow/src/types.ts:13`](../packages/workflow/workflow/src/types.ts)
Sources: [`packages/api/session-controller/src/types.ts:412`](../packages/api/session-controller/src/types.ts) · [`packages/attachment/attachment/src/brand.ts:6`](../packages/attachment/attachment/src/brand.ts) · [`packages/compaction/compaction/src/brand.ts:4`](../packages/compaction/compaction/src/brand.ts) · [`packages/core/session/src/types.ts:21`](../packages/core/session/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:21`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:33`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:9`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/webhook/src/brand.ts:12`](../packages/experimental/webhook/src/brand.ts) · [`packages/experimental/webhook/src/brand.ts:6`](../packages/experimental/webhook/src/brand.ts) · [`packages/experimental/webhook/src/brand.ts:9`](../packages/experimental/webhook/src/brand.ts) · [`packages/feedback/message-feedback/src/types.ts:14`](../packages/feedback/message-feedback/src/types.ts) · [`packages/goal/goal/src/types.ts:17`](../packages/goal/goal/src/types.ts) · [`packages/interaction/commands/src/brand.ts:31`](../packages/interaction/commands/src/brand.ts) · [`packages/interaction/user-approval/src/types.ts:17`](../packages/interaction/user-approval/src/types.ts) · [`packages/jobs/jobs/src/brand.ts:19`](../packages/jobs/jobs/src/brand.ts) · [`packages/llm/llm-retry/src/brand.ts:4`](../packages/llm/llm-retry/src/brand.ts) · [`packages/llm/llm/src/brand.ts:16`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:31`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:43`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:67`](../packages/llm/llm/src/brand.ts) · [`packages/schedule/schedule/src/types.ts:14`](../packages/schedule/schedule/src/types.ts) · [`packages/session/session-title/src/index.ts:42`](../packages/session/session-title/src/index.ts) · [`packages/session/session-title/src/types.ts:17`](../packages/session/session-title/src/types.ts) · [`packages/workflow/workflow/src/types.ts:13`](../packages/workflow/workflow/src/types.ts)
`string`
@@ -7554,7 +7554,7 @@ Sources: [`packages/core/tools/src/index.ts:35`](../packages/core/tools/src/inde
SHA-256: `6dacb1584f2bfbe209dc6b6f578d51da6d55595f64d0e63c727c98f359278def`
Sources: [`packages/api/session-controller/src/types.ts:415`](../packages/api/session-controller/src/types.ts)
Sources: [`packages/api/session-controller/src/types.ts:421`](../packages/api/session-controller/src/types.ts)
| Property | Presence | Type |
|---|---|---|
+2 -2
View File
@@ -5585,7 +5585,7 @@ SHA-256: `5d03ba38734809bcbd2a55221bd938b3b3fc34b49bb686c6113e9e4931e3aa78`
SHA-256: `4cd48f3e5108bd6ebedf7301c3638839b8b2b9db8dbd493974177fbdeb91a01b`
来源:[`packages/api/session-controller/src/types.ts:406`](../packages/api/session-controller/src/types.ts) · [`packages/attachment/attachment/src/brand.ts:6`](../packages/attachment/attachment/src/brand.ts) · [`packages/compaction/compaction/src/brand.ts:4`](../packages/compaction/compaction/src/brand.ts) · [`packages/core/session/src/types.ts:21`](../packages/core/session/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:21`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:33`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:9`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/webhook/src/brand.ts:12`](../packages/experimental/webhook/src/brand.ts) · [`packages/experimental/webhook/src/brand.ts:6`](../packages/experimental/webhook/src/brand.ts) · [`packages/experimental/webhook/src/brand.ts:9`](../packages/experimental/webhook/src/brand.ts) · [`packages/feedback/message-feedback/src/types.ts:14`](../packages/feedback/message-feedback/src/types.ts) · [`packages/goal/goal/src/types.ts:17`](../packages/goal/goal/src/types.ts) · [`packages/interaction/commands/src/brand.ts:31`](../packages/interaction/commands/src/brand.ts) · [`packages/interaction/user-approval/src/types.ts:17`](../packages/interaction/user-approval/src/types.ts) · [`packages/jobs/jobs/src/brand.ts:19`](../packages/jobs/jobs/src/brand.ts) · [`packages/llm/llm-retry/src/brand.ts:4`](../packages/llm/llm-retry/src/brand.ts) · [`packages/llm/llm/src/brand.ts:16`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:31`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:43`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:67`](../packages/llm/llm/src/brand.ts) · [`packages/schedule/schedule/src/types.ts:14`](../packages/schedule/schedule/src/types.ts) · [`packages/session/session-title/src/index.ts:42`](../packages/session/session-title/src/index.ts) · [`packages/session/session-title/src/types.ts:17`](../packages/session/session-title/src/types.ts) · [`packages/workflow/workflow/src/types.ts:13`](../packages/workflow/workflow/src/types.ts)
来源:[`packages/api/session-controller/src/types.ts:412`](../packages/api/session-controller/src/types.ts) · [`packages/attachment/attachment/src/brand.ts:6`](../packages/attachment/attachment/src/brand.ts) · [`packages/compaction/compaction/src/brand.ts:4`](../packages/compaction/compaction/src/brand.ts) · [`packages/core/session/src/types.ts:21`](../packages/core/session/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:21`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:33`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:9`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/webhook/src/brand.ts:12`](../packages/experimental/webhook/src/brand.ts) · [`packages/experimental/webhook/src/brand.ts:6`](../packages/experimental/webhook/src/brand.ts) · [`packages/experimental/webhook/src/brand.ts:9`](../packages/experimental/webhook/src/brand.ts) · [`packages/feedback/message-feedback/src/types.ts:14`](../packages/feedback/message-feedback/src/types.ts) · [`packages/goal/goal/src/types.ts:17`](../packages/goal/goal/src/types.ts) · [`packages/interaction/commands/src/brand.ts:31`](../packages/interaction/commands/src/brand.ts) · [`packages/interaction/user-approval/src/types.ts:17`](../packages/interaction/user-approval/src/types.ts) · [`packages/jobs/jobs/src/brand.ts:19`](../packages/jobs/jobs/src/brand.ts) · [`packages/llm/llm-retry/src/brand.ts:4`](../packages/llm/llm-retry/src/brand.ts) · [`packages/llm/llm/src/brand.ts:16`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:31`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:43`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:67`](../packages/llm/llm/src/brand.ts) · [`packages/schedule/schedule/src/types.ts:14`](../packages/schedule/schedule/src/types.ts) · [`packages/session/session-title/src/index.ts:42`](../packages/session/session-title/src/index.ts) · [`packages/session/session-title/src/types.ts:17`](../packages/session/session-title/src/types.ts) · [`packages/workflow/workflow/src/types.ts:13`](../packages/workflow/workflow/src/types.ts)
`string`
@@ -7556,7 +7556,7 @@ SHA-256: `8fcb0b44a575e998665c053643d1ab04c82f39253ebd190ac0c975b78e40b543`
SHA-256: `6dacb1584f2bfbe209dc6b6f578d51da6d55595f64d0e63c727c98f359278def`
来源:[`packages/api/session-controller/src/types.ts:415`](../packages/api/session-controller/src/types.ts)
来源:[`packages/api/session-controller/src/types.ts:421`](../packages/api/session-controller/src/types.ts)
| 属性 | 存在性 | 类型 |
|---|---|---|
+2 -2
View File
@@ -253,7 +253,7 @@
{"digest":"4b259f7f8f0a44ad5e90b70a73553ced1df27012d126921d397ff403b6c86731","names":["packages/llm/llm/src/message.ts#ToolResultMessage"],"sources":["packages/llm/llm/src/message.ts:173"]},
{"digest":"4b7ddaf3710b88ff9b8df66304f5f97bf03688fd19730eedc2e9535203c820ea","names":[],"sources":[]},
{"digest":"4bb451330ff699f36b45539ceb0eb2606ead72084e2c5bb637c36cc80e2252db","names":[],"sources":["packages/core/session/src/types.ts:194"]},
{"digest":"4cd48f3e5108bd6ebedf7301c3638839b8b2b9db8dbd493974177fbdeb91a01b","names":["packages/api/session-controller/src/types.ts#SessionRequestId","packages/attachment/attachment/src/brand.ts#AttachmentId","packages/compaction/compaction/src/brand.ts#CompactionId","packages/core/session/src/types.ts#SessionId","packages/experimental/agent-team/src/types.ts#TeamId","packages/experimental/agent-team/src/types.ts#TeamMessageId","packages/experimental/agent-team/src/types.ts#TeamTaskId","packages/experimental/webhook/src/brand.ts#WebhookDeliveryId","packages/experimental/webhook/src/brand.ts#WebhookRuleId","packages/experimental/webhook/src/brand.ts#WebhookSourceId","packages/feedback/message-feedback/src/types.ts#MessageFeedbackVersion","packages/goal/goal/src/types.ts#GoalId","packages/interaction/commands/src/brand.ts#CommandId","packages/interaction/user-approval/src/types.ts#ApprovalRequestId","packages/jobs/jobs/src/brand.ts#JobId","packages/llm/llm-retry/src/brand.ts#RetryId","packages/llm/llm/src/brand.ts#MessageId","packages/llm/llm/src/brand.ts#ProviderRequestId","packages/llm/llm/src/brand.ts#ReasoningEffortId","packages/llm/llm/src/brand.ts#ToolCallId","packages/schedule/schedule/src/types.ts#ScheduleId","packages/session/session-title/src/index.ts#SessionTitleProviderId","packages/session/session-title/src/types.ts#SessionTitleProviderId","packages/workflow/workflow/src/types.ts#WorkflowRunId"],"sources":["packages/api/session-controller/src/types.ts:406","packages/attachment/attachment/src/brand.ts:6","packages/compaction/compaction/src/brand.ts:4","packages/core/session/src/types.ts:21","packages/experimental/agent-team/src/types.ts:21","packages/experimental/agent-team/src/types.ts:33","packages/experimental/agent-team/src/types.ts:9","packages/experimental/webhook/src/brand.ts:12","packages/experimental/webhook/src/brand.ts:6","packages/experimental/webhook/src/brand.ts:9","packages/feedback/message-feedback/src/types.ts:14","packages/goal/goal/src/types.ts:17","packages/interaction/commands/src/brand.ts:31","packages/interaction/user-approval/src/types.ts:17","packages/jobs/jobs/src/brand.ts:19","packages/llm/llm-retry/src/brand.ts:4","packages/llm/llm/src/brand.ts:16","packages/llm/llm/src/brand.ts:31","packages/llm/llm/src/brand.ts:43","packages/llm/llm/src/brand.ts:67","packages/schedule/schedule/src/types.ts:14","packages/session/session-title/src/index.ts:42","packages/session/session-title/src/types.ts:17","packages/workflow/workflow/src/types.ts:13"]},
{"digest":"4cd48f3e5108bd6ebedf7301c3638839b8b2b9db8dbd493974177fbdeb91a01b","names":["packages/api/session-controller/src/types.ts#SessionRequestId","packages/attachment/attachment/src/brand.ts#AttachmentId","packages/compaction/compaction/src/brand.ts#CompactionId","packages/core/session/src/types.ts#SessionId","packages/experimental/agent-team/src/types.ts#TeamId","packages/experimental/agent-team/src/types.ts#TeamMessageId","packages/experimental/agent-team/src/types.ts#TeamTaskId","packages/experimental/webhook/src/brand.ts#WebhookDeliveryId","packages/experimental/webhook/src/brand.ts#WebhookRuleId","packages/experimental/webhook/src/brand.ts#WebhookSourceId","packages/feedback/message-feedback/src/types.ts#MessageFeedbackVersion","packages/goal/goal/src/types.ts#GoalId","packages/interaction/commands/src/brand.ts#CommandId","packages/interaction/user-approval/src/types.ts#ApprovalRequestId","packages/jobs/jobs/src/brand.ts#JobId","packages/llm/llm-retry/src/brand.ts#RetryId","packages/llm/llm/src/brand.ts#MessageId","packages/llm/llm/src/brand.ts#ProviderRequestId","packages/llm/llm/src/brand.ts#ReasoningEffortId","packages/llm/llm/src/brand.ts#ToolCallId","packages/schedule/schedule/src/types.ts#ScheduleId","packages/session/session-title/src/index.ts#SessionTitleProviderId","packages/session/session-title/src/types.ts#SessionTitleProviderId","packages/workflow/workflow/src/types.ts#WorkflowRunId"],"sources":["packages/api/session-controller/src/types.ts:412","packages/attachment/attachment/src/brand.ts:6","packages/compaction/compaction/src/brand.ts:4","packages/core/session/src/types.ts:21","packages/experimental/agent-team/src/types.ts:21","packages/experimental/agent-team/src/types.ts:33","packages/experimental/agent-team/src/types.ts:9","packages/experimental/webhook/src/brand.ts:12","packages/experimental/webhook/src/brand.ts:6","packages/experimental/webhook/src/brand.ts:9","packages/feedback/message-feedback/src/types.ts:14","packages/goal/goal/src/types.ts:17","packages/interaction/commands/src/brand.ts:31","packages/interaction/user-approval/src/types.ts:17","packages/jobs/jobs/src/brand.ts:19","packages/llm/llm-retry/src/brand.ts:4","packages/llm/llm/src/brand.ts:16","packages/llm/llm/src/brand.ts:31","packages/llm/llm/src/brand.ts:43","packages/llm/llm/src/brand.ts:67","packages/schedule/schedule/src/types.ts:14","packages/session/session-title/src/index.ts:42","packages/session/session-title/src/types.ts:17","packages/workflow/workflow/src/types.ts:13"]},
{"digest":"4e011753f8263f436efaabfbb3b4db53a98fc55342c5ded2daf7208b217be159","names":[],"sources":[]},
{"digest":"4e44c9118152f6829634bfd508e47669ab0497caffa380c94b4be91f9390d35d","names":[],"sources":[]},
{"digest":"4e887768586528565381dadbddee6cef555874148089f4579e0b4b1ad096fc9c","names":["packages/llm/llm/src/types.ts#TextBlock"],"sources":["packages/llm/llm/src/types.ts:62","packages/web/web-search-deepseek/src/provider.ts:66"]},
@@ -335,7 +335,7 @@
{"digest":"6ccb6bc423fb3952458abf9c1b1c3284208b7e660344b612cc55da773d61afe4","names":[],"sources":[]},
{"digest":"6cda0d6d3b4336d83998fe580628426df81c2afbab5d768b8813b56d569120e8","names":[],"sources":[]},
{"digest":"6cf082d0fb6034dea61636787ff0ddf43b6e59aad71e644435a479bc9b83ad65","names":[],"sources":[]},
{"digest":"6dacb1584f2bfbe209dc6b6f578d51da6d55595f64d0e63c727c98f359278def","names":[],"sources":["packages/api/session-controller/src/types.ts:415"]},
{"digest":"6dacb1584f2bfbe209dc6b6f578d51da6d55595f64d0e63c727c98f359278def","names":[],"sources":["packages/api/session-controller/src/types.ts:421"]},
{"digest":"6e52dd55a1d48954ed5464ec46fe34d9eb65f709baabc9561e7a279910b0c406","names":[],"sources":["packages/preset/agent-preset-registry/src/session.ts:31"]},
{"digest":"6ea608bdc8a7a63d3d23b84c49721325094db88d136482e43a820945295eee3e","names":[],"sources":[]},
{"digest":"6ebcc2dbec9f2dabca9f668ae6d39e877899b180740570f453409db339221df3","names":[],"sources":[]},
+6 -6
View File
@@ -9,11 +9,11 @@
en: 96b95cdb015c1af6
zh: 01e4144d91551c13
/client-modules/the-scan:
en: 91ce66fab388a8a2
zh: 77428943582ae3f6
en: 811c0e8a3a370399
zh: 4a49a3999d35503a
/client-modules/the-bundle-route-and-index-injection:
en: d653be500d3a0d67
zh: fffb1901ce8ba7d9
en: 82c089fbd9ffd535
zh: 30ac52d01b4fc074
/client-modules/the-service:
en: 000882ed7f09ad3f
zh: c17c05f087d9574d
en: b1df1bf2b8ee3ad1
zh: 6373d1b8cae22d02
+14 -10
View File
@@ -95,11 +95,11 @@ A package joins the table by declaring `dsh.client` (`platform: 'web'`, optional
Scanning is incremental per package; there is no full-rescan code path. Every cordis `internal/plugin` emission (fiber construction or disposal) marks the fiber's entry name dirty, and a microtask flush reconciles each dirty name against the live loader entries. The activation pass seeds the same dirty set with all current entries and flushes synchronously, so first scan and steady state share one implementation — with opposite failure postures. At activation, a malformed declaration or missing bundle among the already-loaded entries aggregates into one loud `AggregateError` listing every broken package: the fiber FAILS and the boot's fail-loud sweep reports it. In steady state, a broken package logs a warning and must not poison the others.
Package metadata — including the negative "not a client package" verdict — is cached per Loader specifier and owning-tree base URL until restart. A fiber restart from the same source reuses its row and rev untouched; bundle content changes reach the graph only through `rebuilt()`.
Each reconciliation reads the package manifest and the bundle metadata again. A fiber restart from the same source that finds the same declaration and bundle metadata keeps its row and rev and reads no bundle bytes; a changed declaration, client export, or bundle is published by that reconciliation. A bundle rebuilt without an entry restart reaches the graph through `rebuilt()`.
## The bundle route and index injection
`GET`/`HEAD /plugins/??<package-a>/client.js,<package-b>/client.js&rev=<rev>` addresses one generated combo script; a one-resource request uses the same form and is the HMR path. The script is concatenated once on its first `GET` and ends with a `sourceMappingURL` that carries the combo query alone — `??<package-a>/client.js.map,<package-b>/client.js.map&rev=<rev>`, resolved against the script's own directory rather than the document. The map files are not read by startup, index rendering, script `GET`, or `HEAD`; the first map `GET` reads and validates them, composes one Indexed Source Map v3, and caches that body. An authored component map supplies its section; a component without one receives an identity section whose `sourcesContent` is the captured bundle and whose source name is its packaged `sourceURL` or plugin route. Every startup request URL is at most 3 KiB measured as UTF-8 bytes; partitioning uses the longer map form. All application URLs are preloaded, and all bootstrap URLs execute before the graph global and Vite entry. Materialized responses use long-lived immutable caching. Unknown or altered resource lists, missing revisions, and stale revisions answer 404 rather than serving different bytes or letting the SPA fallback return HTML as JavaScript; other methods are 405. The injection rows carry the current graph on every index render, so a reload always boots against the live composition.
`GET`/`HEAD /plugins/??<package-a>/client.js,<package-b>/client.js&rev=<rev>` addresses one generated combo script; a one-resource request uses the same form and is the HMR path. `/plugins/<package>/assets/<path>` serves a package's published images; the [package README](../../packages/client/modules/README.md#package-assets) owns that route. The script is concatenated once on its first `GET` and ends with a `sourceMappingURL` that carries the combo query alone — `??<package-a>/client.js.map,<package-b>/client.js.map&rev=<rev>`, resolved against the script's own directory rather than the document. The map files are not read by startup, index rendering, script `GET`, or `HEAD`; the first map `GET` reads and validates them, composes one Indexed Source Map v3, and caches that body. An authored component map supplies its section; a component without one receives an identity section whose `sourcesContent` is the captured bundle and whose source name is its packaged `sourceURL` or plugin route. Every startup request URL is at most 3 KiB measured as UTF-8 bytes; partitioning uses the longer map form. All application URLs are preloaded, and all bootstrap URLs execute before the graph global and Vite entry. Materialized responses use long-lived immutable caching. Unknown or altered resource lists, missing revisions, and stale revisions answer 404 rather than serving different bytes or letting the SPA fallback return HTML as JavaScript; other methods are 405. The injection rows carry the current graph on every index render, so a reload always boots against the live composition.
## The service
@@ -117,7 +117,7 @@ interface ClientArtifactBaseline {
}
```
`ClientModuleRegistry` (`ctx.clientModules`, defined in [`packages/client/modules/src/index.ts`](../../packages/client/modules/src/index.ts)) exposes reads and the rebuild face; signatures are in the generated [service catalog](#ctxclientmodules--clientmoduleregistry). `graph()` returns the current composed graph (a stable object between changes), `clientPath(id)` returns the bundle's absolute path, and `artifactBaseline(id)` returns the bundle stat values captured before the current snapshot was read. `fetchBundle()` resolves the same lazy response used by the HTTP route. `rebuilt(id)` is the only entry point through which changed bundle content reaches the graph: it derives the revision from filesystem metadata, and only a revision change reads the new bytes, recomposes the graph, and notifies. `onRebuilt` fires per changed bundle with the new revision; `onGraphChanged` fires after any flush that recomposed the graph (row added or removed, or a rebuilt revision change) and is pull-model — listeners re-read `graph()`. Both notification paths contain listener exceptions so one throwing subscriber cannot skip later subscribers or kill whatever triggered the flush.
`ClientModuleRegistry` (`ctx.clientModules`, defined in [`packages/client/modules/src/index.ts`](../../packages/client/modules/src/index.ts)) exposes reads and the rebuild face; signatures are in the generated [service catalog](#ctxclientmodules--clientmoduleregistry). `graph()` returns the current composed graph (a stable object between changes), `clientPath(id)` returns the bundle's absolute path, and `artifactBaseline(id)` returns the bundle stat values captured before the current snapshot was read. `fetchBundle()` returns the same responses as the HTTP route: the lazy bundle responses, and package assets read on each request. `rebuilt(id)` publishes a bundle rebuilt without an entry restart: it derives the revision from filesystem metadata, and only a revision change reads the new bytes, recomposes the graph, and notifies; an entry restart publishes through reconciliation instead. `onRebuilt` fires per changed bundle with the new revision; `onGraphChanged` fires after any flush that recomposed the graph (row added or removed, or a rebuilt revision change) and is pull-model — listeners re-read `graph()`. Both notification paths contain listener exceptions so one throwing subscriber cannot skip later subscribers or kill whatever triggered the flush.
[`dsh-client-hmr`](../../packages/client/hmr/README.md) delivers live graph snapshots in the shipped Web composition. The Host forwards existing graph-change notifications immediately, and reconnect sends the current full graph. A graph describes desired browser entries without asserting that Host cleanup has completed. Its artifact poll separately reports rebuilt revisions. Source-map changes alone do not trigger a reload; a new combo-map URL appears only after a bundle revision changes, and each map body is fixed by its first `GET`. Client Modules validates snapshots and serializes reconciliation with those rebuilds; it owns the boot-created entry map, single-resource arrivals, asynchronous removal, unused-module/style cleanup and page-local retry status. Static platform modules and the bootstrap retain their page lifetime; Electron installation is a separate flow.
@@ -133,7 +133,7 @@ Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnp
### `ctx.clientModules` — `ClientModuleRegistry`
The web plugin table service: incremental `dsh.client` scan + wire composition + bundle route + index injection rows. Construction runs the activation scan synchronously — a malformed declaration or missing bundle among the already-loaded entries aggregates into one loud throw (FAILED fiber; the boot activation audit reports it).
The web plugin table service: incremental `dsh.client` scan + wire composition + bundle and package-asset route + index injection rows. Construction runs the activation scan synchronously — a malformed declaration or missing bundle among the already-loaded entries aggregates into one loud throw (FAILED fiber; the boot activation audit reports it).
```ts cordis-catalog
/**
@@ -150,10 +150,13 @@ graph(): WebBootGraph
clientPath(id: string): string | undefined
/**
* Serve an advertised revisioned bundle or source map without a Web server.
* Unknown URLs return 404, unsupported methods return 405, and `HEAD`
* returns the same immutable headers without materializing a body. Each body
* is built once on its first `GET`; script construction never reads maps.
* Serve an advertised revisioned bundle, source map, chunk, or package asset
* without a Web server. Unknown URLs return 404, unsupported methods return
* 405, and `HEAD` returns the same immutable headers without materializing a
* body. Each bundle body is built once on its first `GET`; script
* construction never reads maps. A package asset is read on every request
* and answers 304 without a body when `If-None-Match` equals its entity tag;
* its body holds the file open until the caller consumes or cancels it.
* @param request - shell-carrier request for a `/plugins` resource.
* @returns the exact response also exposed by the optional Web route.
*/
@@ -171,7 +174,7 @@ artifactBaseline(id: string): ClientArtifactBaseline | undefined
/**
* Publish one completed bundle generation (the HMR watch's registration
* hook — the only entry point through which build changes reach the graph).
* hook; an entry restart publishes through reconciliation instead).
* Unchanged mtime, ctime and size preserve the graph without reading the bundle.
* @param id - entry id (package name).
* @returns the current artifact rev, or undefined for an unknown id.
@@ -179,7 +182,8 @@ artifactBaseline(id: string): ClientArtifactBaseline | undefined
rebuilt(id: string): string | undefined
/**
* Subscribe to bundle rebuilds; fires only when artifact metadata changes the rev.
* Subscribe to bundle rebuilds; fires when {@link rebuilt} changes a rev. A rev an
* entry restart publishes reaches only {@link onGraphChanged} listeners.
* @param listener - receives the entry id and its new bundle rev.
* @returns the unsubscriber.
*/
+14 -10
View File
@@ -95,11 +95,11 @@ interface WebBootGraph {
扫描是单包增量的;不存在全量重扫代码路径。fiber 构造或 dispose(资源释放)时的每次 cordis `internal/plugin` 发射都把该 fiber 的 entry 名标脏,一次微任务 flush 把每个脏名与实时 loader entry 对账。激活趟以全部当前 entry 灌入同一个脏集合并同步 flush,因此初扫与稳态共享一条实现——但失败姿态相反。激活时,已加载 entry 中的畸形声明或缺失 bundle 会聚合为一个大声的 `AggregateError`,列出每个损坏的包:该 fiber 进入 FAILED,由启动的大声失败 sweep 上报。稳态下,损坏的包只记录一条警告,且不得殃及其他包。
包元数据——包括「非 client 包」这一否定结论——按 Loader specifier 与所属 tree base URL 缓存至重启。同一来源的 fiber 重启会原样复用其 row 与 rev;bundle 内容变更只经 `rebuilt()` 到达图。
每次对账都会重新读取包 manifest 与 bundle 元数据。同一来源的 fiber 重启若读到相同的声明与 bundle 元数据,就保留其 row 与 rev,且不读取 bundle 字节;声明、client 导出或 bundle 发生变化时,由这次对账发布。没有伴随条目重启的 bundle 重建经 `rebuilt()` 到达图。
## bundle 路由与 index 注入
`GET`/`HEAD /plugins/??<package-a>/client.js,<package-b>/client.js&rev=<rev>` 寻址一份生成的 combo 脚本;单资源请求采用同一形式,也是 HMR 路径。脚本在首次 `GET` 时只拼接一次,并以 `sourceMappingURL` 结尾,该引用只携带 combo 查询串——`??<package-a>/client.js.map,<package-b>/client.js.map&rev=<rev>`,按脚本自身目录而非文档解析。启动、index 渲染、脚本 `GET` 和 `HEAD` 都不会读取 map 文件;首次 map `GET` 才会读取并校验这些文件、组合一份 Indexed Source Map v3,并缓存该 body。组件有自带 map 时直接用于对应 section;没有时则获得 identity section,其 `sourcesContent` 是捕获的 bundle,source 名取打包后的 `sourceURL` 或插件路由。每条启动请求 URL 按 UTF-8 字节计算都不超过 3 KiB;切分按更长的 map 形式计算。所有 application URL 都会预加载,所有 bootstrap URL 都会在图全局量与 Vite entry 之前执行。已物化响应使用长期 immutable 缓存。未知或被修改的资源列表、缺少 revision 及陈旧 revision 都返回 404,绝不提供其他字节,也不会让 SPA fallback 把 HTML 当作 JavaScript 返回;其他方法返回 405。注入行在每次 index 渲染时携带当前图,因此重新加载总是基于实时组合启动。
`GET`/`HEAD /plugins/??<package-a>/client.js,<package-b>/client.js&rev=<rev>` 寻址一份生成的 combo 脚本;单资源请求采用同一形式,也是 HMR 路径。`/plugins/<package>/assets/<path>` 提供包发布的图片,该路由由[包 README](../../packages/client/modules/README.zh.md#package-assets) 说明。脚本在首次 `GET` 时只拼接一次,并以 `sourceMappingURL` 结尾,该引用只携带 combo 查询串——`??<package-a>/client.js.map,<package-b>/client.js.map&rev=<rev>`,按脚本自身目录而非文档解析。启动、index 渲染、脚本 `GET` 和 `HEAD` 都不会读取 map 文件;首次 map `GET` 才会读取并校验这些文件、组合一份 Indexed Source Map v3,并缓存该 body。组件有自带 map 时直接用于对应 section;没有时则获得 identity section,其 `sourcesContent` 是捕获的 bundle,source 名取打包后的 `sourceURL` 或插件路由。每条启动请求 URL 按 UTF-8 字节计算都不超过 3 KiB;切分按更长的 map 形式计算。所有 application URL 都会预加载,所有 bootstrap URL 都会在图全局量与 Vite entry 之前执行。已物化响应使用长期 immutable 缓存。未知或被修改的资源列表、缺少 revision 及陈旧 revision 都返回 404,绝不提供其他字节,也不会让 SPA fallback 把 HTML 当作 JavaScript 返回;其他方法返回 405。注入行在每次 index 渲染时携带当前图,因此重新加载总是基于实时组合启动。
## 服务
@@ -117,7 +117,7 @@ interface ClientArtifactBaseline {
}
```
`ClientModuleRegistry`(`ctx.clientModules`,定义于 [`packages/client/modules/src/index.ts`](../../packages/client/modules/src/index.ts))暴露读取面与重建面;签名见生成的[服务目录](#ctxclientmodules--clientmoduleregistry)。`graph()` 返回当前组合出的图(两次变更之间是同一个稳定对象),`clientPath(id)` 返回 bundle 的绝对路径,`artifactBaseline(id)` 返回读取当前快照前捕获的 bundle stat 值。`fetchBundle()` 解析 HTTP 路由所使用的同一份惰性响应。`rebuilt(id)` 是变化后的 bundle 内容到达图的唯一入口:它从文件系统元数据派生 revision,只有 revision 变化才会读取新字节、重新组合图并发出通知。`onRebuilt` 按发生变化的 bundle 逐个触发并携带新 revision;`onGraphChanged` 在任何一次重新组合了图的 flush 之后触发(行的增删,或 rebuilt 带来的 revision 变化),并采用拉取模型——监听器自行重读 `graph()`。两条通知路径都会兜住监听器异常,因此一个抛错的订阅者既不能让后续订阅者被跳过,也不能杀死触发这次 flush 的一方。
`ClientModuleRegistry`(`ctx.clientModules`,定义于 [`packages/client/modules/src/index.ts`](../../packages/client/modules/src/index.ts))暴露读取面与重建面;签名见生成的[服务目录](#ctxclientmodules--clientmoduleregistry)。`graph()` 返回当前组合出的图(两次变更之间是同一个稳定对象),`clientPath(id)` 返回 bundle 的绝对路径,`artifactBaseline(id)` 返回读取当前快照前捕获的 bundle stat 值。`fetchBundle()` 返回与 HTTP 路由相同的响应:惰性的 bundle 响应,以及每次请求都读取的包资源。`rebuilt(id)` 发布没有伴随条目重启的 bundle 重建:它从文件系统元数据派生 revision,只有 revision 变化才会读取新字节、重新组合图并发出通知;条目重启则经对账发布。`onRebuilt` 按发生变化的 bundle 逐个触发并携带新 revision;`onGraphChanged` 在任何一次重新组合了图的 flush 之后触发(行的增删,或 rebuilt 带来的 revision 变化),并采用拉取模型——监听器自行重读 `graph()`。两条通知路径都会兜住监听器异常,因此一个抛错的订阅者既不能让后续订阅者被跳过,也不能杀死触发这次 flush 的一方。
随包提供的 Web 组合通过 [`dsh-client-hmr`](../../packages/client/hmr/README.zh.md) 交付动态图快照。Host 立即转发现有图变化通知,重连会发送当前完整图。图描述浏览器的目标条目,不声明 Host 清理已经完成。产物轮询另外报告重建 revision。仅 source map 变化不会触发重载;新 combo-map URL 只会在 bundle revision 变化后出现,每份 map body 由其首次 `GET` 固定。Client Modules 校验快照,并将对账与重建串行协调;它持有启动创建的条目映射,负责单资源到达、异步移除、未使用模块与样式清理,以及页面本地重试状态。静态平台模块与 bootstrap 保持页面生命周期;Electron 安装属于独立流程。
@@ -133,7 +133,7 @@ Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnp
### `ctx.clientModules` — `ClientModuleRegistry`
The web plugin table service: incremental `dsh.client` scan + wire composition + bundle route + index injection rows. Construction runs the activation scan synchronously — a malformed declaration or missing bundle among the already-loaded entries aggregates into one loud throw (FAILED fiber; the boot activation audit reports it).
The web plugin table service: incremental `dsh.client` scan + wire composition + bundle and package-asset route + index injection rows. Construction runs the activation scan synchronously — a malformed declaration or missing bundle among the already-loaded entries aggregates into one loud throw (FAILED fiber; the boot activation audit reports it).
```ts cordis-catalog
/**
@@ -150,10 +150,13 @@ graph(): WebBootGraph
clientPath(id: string): string | undefined
/**
* Serve an advertised revisioned bundle or source map without a Web server.
* Unknown URLs return 404, unsupported methods return 405, and `HEAD`
* returns the same immutable headers without materializing a body. Each body
* is built once on its first `GET`; script construction never reads maps.
* Serve an advertised revisioned bundle, source map, chunk, or package asset
* without a Web server. Unknown URLs return 404, unsupported methods return
* 405, and `HEAD` returns the same immutable headers without materializing a
* body. Each bundle body is built once on its first `GET`; script
* construction never reads maps. A package asset is read on every request
* and answers 304 without a body when `If-None-Match` equals its entity tag;
* its body holds the file open until the caller consumes or cancels it.
* @param request - shell-carrier request for a `/plugins` resource.
* @returns the exact response also exposed by the optional Web route.
*/
@@ -171,7 +174,7 @@ artifactBaseline(id: string): ClientArtifactBaseline | undefined
/**
* Publish one completed bundle generation (the HMR watch's registration
* hook — the only entry point through which build changes reach the graph).
* hook; an entry restart publishes through reconciliation instead).
* Unchanged mtime, ctime and size preserve the graph without reading the bundle.
* @param id - entry id (package name).
* @returns the current artifact rev, or undefined for an unknown id.
@@ -179,7 +182,8 @@ artifactBaseline(id: string): ClientArtifactBaseline | undefined
rebuilt(id: string): string | undefined
/**
* Subscribe to bundle rebuilds; fires only when artifact metadata changes the rev.
* Subscribe to bundle rebuilds; fires when {@link rebuilt} changes a rev. A rev an
* entry restart publishes reaches only {@link onGraphChanged} listeners.
* @param listener - receives the entry id and its new bundle rev.
* @returns the unsubscriber.
*/
+6 -3
View File
@@ -6,8 +6,8 @@
en: f6ae07d8902e8edf
zh: c92fef531c6da331
/core/the-spine-package-by-package:
en: c90cb23528ddb6c8
zh: 62724b7255b586b6
en: 60926dc99f41024d
zh: f40370b034f91463
/core/creation-and-ownership:
en: ab09a38b4e1f0883
zh: 81e2d14c4036ab6c
@@ -16,7 +16,10 @@
zh: 25bc9cb60ee1520a
/core/initiating-agent:
en: 6c8c47ac78de2dde
zh: c9d527a25a85d1d9
zh: acde99c8808ea891
/core/messages-from-outside-an-agent:
en: cf087fd9ff5edb87
zh: c3bb56b17cd76c52
/core/interception-decisions:
en: 6fccceb0a5f1cad9
zh: f43c30c2386d8cc8
+125 -1
View File
@@ -15,9 +15,10 @@ A turn flows through the six packages in one loop: the driver in [`agent-loop`](
| `tools/` | The scoped tool registry and guarded execution pipeline (`ctx.tools`) | [tools.md](tools.md) |
| `agent/` | The `Agent` interface, live registry, initiator scope, and `agent/*` event vocabulary (`ctx.agents`) | this page |
| `agent-loop/` | The concrete driver implementing the public `Agent` contract (`ctx.agentLoop`) | this page |
| `agent-controller/` | Delivery of messages from outside an Agent through the `agent-controller/admission` stage (`ctx.agentController`) | this page |
| `scope/` | The scoped-registration primitive the registries and loop build per-agent scoping on | [scope.md](scope.md) |
`scope/` is the one non-service package: a dependency-free library (`createScope`/`scopeOf`/`scopeTarget`) that sits below `session/` and `system-prompt/` in the module graph precisely so they can consume it without a cycle. `agent-loop` is the one concrete implementation of the public `Agent` contract and lives here because it is the harness's default product loop; it runs each driver inside `ctx.agents.withInitiator()`. Extension plugins depend on `agent` — including when they need the initiating Agent — and never on `agent-loop` directly, so the loop stays swappable. [`dsh-base`](../../packages/bundle/base/README.md) is the default product composition, while [`dsh-sdk-minimal`](../../packages/bundle/sdk-minimal/README.md) declares a smaller standalone tree.
`scope/` is the one non-service package: a dependency-free library (`createScope`/`scopeOf`/`scopeTarget`) that sits below `session/` and `system-prompt/` in the module graph precisely so they can consume it without a cycle. `agent-loop` is the one concrete implementation of the public `Agent` contract and lives here because it is the harness's default product loop; it runs each driver inside `ctx.agents.withInitiator()`. Extension plugins depend on `agent` — including when they need the initiating Agent — and never on `agent-loop` directly, so the loop stays swappable. `agent-controller` sits in front of the loop: a sender that brings a message on someone's behalf delivers it through `ctx.agentController` instead of appending to the inbox itself. [`dsh-base`](../../packages/bundle/base/README.md) is the default product composition, while [`dsh-sdk-minimal`](../../packages/bundle/sdk-minimal/README.md) declares a smaller standalone tree.
## Creation and ownership
@@ -305,6 +306,65 @@ The [event taxonomy](../architecture.md#events) owns the `agent/*` lifecycle, ch
The process-local initiator carried by `ctx.agents` is the exact `Agent` above, not a separate frame or copied identity. Ambient presence is neither liveness proof nor authorization; the [initiator-scope decision](../../.agents/notes/implemented/architecture/2026-07-15-agent-initiator-scope.md) defines its lifetime and scope rules.
## Messages from outside an Agent
A sender that brings a message on someone's behalf, such as the Web Session Remote, calls `ctx.agentController.deliver` from [`agent-controller`](../../packages/core/agent-controller/README.md) instead of `followup()` or `steer()`. The controller prepares the content in the Agent's delivery order, asks the `agent-controller/admission` waterfall for a decision, and appends an admitted message; messages the Agent's own machinery produces enter without the stage. The [delivery decision](../../.agents/notes/implemented/architecture/2026-10-17-agent-controller-message-delivery.md) explains why the stage lives outside `agent`.
Source: [`packages/core/agent-controller/src/types.ts`](../../packages/core/agent-controller/src/types.ts)
```ts type-equiv
/** How a delivered message enters the inbox: `followup` for its own turn, `steer` for the nearest step. */
type DeliveryMode = 'followup' | 'steer'
```
```ts type-equiv
/** One message a sender delivers on someone's behalf. */
interface DeliveryRequest {
/** Where the message enters once it is admitted. */
readonly mode: DeliveryMode
/** Durable attribution recorded on the message. */
readonly source: MessageSource
/** The sender's cancellation; it ends the sender's wait at once and stops a message that has not entered the inbox. */
readonly signal: AbortSignal
/**
* Turn the sender's content into durable message content, such as by
* storing attachments. It runs in the Agent's delivery order, after the
* earlier messages to the same Agent settled; its failure reaches the
* sender unchanged.
* @param signal - aborted when the sender cancels or the controller stops; controller disposal waits for the preparation to settle.
* @returns the content of the message.
*/
readonly prepare: (signal: AbortSignal) => Promise<UserMessage['content']>
/**
* Record what the sender needs before the Agent wakes. It runs
* synchronously after admission, immediately before the message enters the
* inbox. A throw keeps the message out of the inbox and reaches the sender
* unchanged; the sender reverts its own effects when `deliver` rejects.
* @param message - the admitted message.
*/
readonly beforeInsert?: (message: UserMessage) => void
}
```
A listener admits by returning the decision of `next()`, holds the message by awaiting before `next()`, and refuses it by returning a `reject` decision:
```ts type-equiv
/** The outcome of the admission stage for one message. */
type AdmissionDecision =
/** The message enters the inbox. */
| { readonly kind: 'admit' }
/** The message stays out of the inbox. */
| {
readonly kind: 'reject'
/** Code of the refusal, owned by its source, such as `forge/worktree-missing`. */
readonly reason: string
/** Explanation for a sender that shows nothing of the refusing plugin. */
readonly message: string
/** Whether the refusing plugin explains the refusal in its own interface. */
readonly presented: boolean
}
```
## Interception decisions
Pre-step decisions use the same identified `UserMessage` type as durable user-role input. The entered batch is authoritative and preserves every message's `id` and `source`. Hook bridges map their native decision fields onto this typed result.
@@ -437,6 +497,33 @@ interface TurnBoundaryProjection {
Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — the language sides differ only in locale-specific paired document paths. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
<a id="ctxagentcontroller--agentcontroller"></a>
### `ctx.agentController` — `AgentController`
Delivers the messages that senders, such as the Web Session Remote, bring on someone's behalf. Messages to one Agent are prepared, decided, and appended one at a time in arrival order; messages to different Agents do not wait for each other. Disposal stops every message that has not entered an inbox and finishes once each delivery it stopped has settled.
```ts cordis-catalog
/**
* Deliver one message from outside the Agent: prepare its content, ask the
* `agent-controller/admission` stage for a decision, then append an
* admitted message with `followup` or `steer`.
* @param agent - the receiving Agent.
* @param request - delivery mode, source, cancellation, and the sender's content preparation.
* @returns the id of the message once it is in the inbox.
* @throws {@link AgentAdmissionRejected} when a listener rejects the message
* or throws, or when the Agent left the registry first; the error of
* `prepare`, `beforeInsert`, `followup`, or `steer` unchanged; the sender's
* cancellation reason as soon as it cancels, also while the message waits
* behind another one; the stop reason once the controller stops.
*/
async deliver(agent: Agent, request: DeliveryRequest): Promise<MessageId>
```
Types: [MessageId](llm-streaming.md)
Source: [`packages/core/agent-controller/src/index.ts`](../../packages/core/agent-controller/src/index.ts)
<a id="ctxagentdefaultmodel--agentdefaultmodelconfig"></a>
### `ctx.agentDefaultModel` — `AgentDefaultModelConfig`
@@ -1099,6 +1186,43 @@ Types: [Scoped](scope.md)
Source: [`packages/core/agent/src/runtime-types.ts`](../../packages/core/agent/src/runtime-types.ts)
<a id="agent-controller-events"></a>
### `agent-controller/*` events
<a id="agent-controlleradmission--waterfall"></a>
#### `agent-controller/admission` — waterfall
Decide whether one message from outside an Agent enters its inbox. `ctx.agentController.deliver` runs this stage and appends the message only after the whole chain admitted it; messages the Agent's own machinery produces enter through `followup`, `steer`, or `inject` without it. Listeners run in registration order, and messages to one Agent are decided one at a time in arrival order. A listener admits by returning the decision of `next()`, whose last step admits; awaiting before `next()` holds the message, and returning a `reject` decision refuses it without asking later listeners. A thrown error means the listener's check itself failed. A listener must not wait for a person when the message source is unattended, such as a schedule or a webhook.
```ts cordis-catalog
/**
* Decide whether one message from outside an Agent enters its inbox.
* `ctx.agentController.deliver` runs this stage and appends the message
* only after the whole chain admitted it; messages the Agent's own
* machinery produces enter through `followup`, `steer`, or `inject`
* without it. Listeners run in registration order, and messages to one
* Agent are decided one at a time in arrival order. A listener admits by
* returning the decision of `next()`, whose last step admits; awaiting
* before `next()` holds the message, and returning a `reject` decision
* refuses it without asking later listeners. A thrown error means the
* listener's check itself failed. A listener must not wait for a person
* when the message source is unattended, such as a schedule or a webhook.
* @param payload.agent - the receiving Agent.
* @param payload.message - the message as it will enter the inbox.
* @param payload.signal - aborted when the sender cancels or the controller stops; a holding
* listener must settle once it aborts, because controller disposal waits for it.
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): Agent-scoped listeners receive only that Agent.
* @mode waterfall
*/
'agent-controller/admission'(this: Scoped<Agent>, payload: { agent: Agent; message: UserMessage; signal: AbortSignal }, next: () => Promise<AdmissionDecision>): Promise<AdmissionDecision>
```
Types: [Scoped](scope.md) · [UserMessage](session.md)
Source: [`packages/core/agent-controller/src/types.ts`](../../packages/core/agent-controller/src/types.ts)
<a id="agent-loop-events"></a>
### `agent-loop/*` events
+127 -1
View File
@@ -15,9 +15,10 @@
| `tools/` | 带作用域的工具注册表与受保护的执行流水线(`ctx.tools`) | [tools.md](tools.zh.md) |
| `agent/` | `Agent` 接口、实时注册表、发起者作用域与 `agent/*` 事件词汇(`ctx.agents`) | 本页 |
| `agent-loop/` | 实现公开 `Agent` 约定的具体 driver(`ctx.agentLoop`) | 本页 |
| `agent-controller/` | 外部消息经 `agent-controller/admission` 阶段投递进 Agent(`ctx.agentController`) | 本页 |
| `scope/` | 注册表与循环用于构建按 agent 作用域的注册原语 | [scope.md](scope.zh.md) |
`scope/` 是这里唯一的非服务包:一个零依赖库(`createScope`/`scopeOf`/`scopeTarget`),在模块图中位于 `session/` 与 `system-prompt/` 之下,正是为了让它们消费它而不形成环。`agent-loop` 是公开 `Agent` 约定的唯一具体实现,放在这里因为它是 harness 的默认产品循环;它在 `ctx.agents.withInitiator()` 内运行每个 driver。扩展插件依赖 `agent`——包括需要发起 Agent 时——而绝不直接依赖 `agent-loop`,因此循环保持可替换。[`dsh-base`](../../packages/bundle/base/README.zh.md) 是默认产品组合,[`dsh-sdk-minimal`](../../packages/bundle/sdk-minimal/README.zh.md) 则声明一棵更小的独立配置树。
`scope/` 是这里唯一的非服务包:一个零依赖库(`createScope`/`scopeOf`/`scopeTarget`),在模块图中位于 `session/` 与 `system-prompt/` 之下,正是为了让它们消费它而不形成环。`agent-loop` 是公开 `Agent` 约定的唯一具体实现,放在这里因为它是 harness 的默认产品循环;它在 `ctx.agents.withInitiator()` 内运行每个 driver。扩展插件依赖 `agent`——包括需要发起 Agent 时——而绝不直接依赖 `agent-loop`,因此循环保持可替换。`agent-controller` 位于循环之前:代表他人送来消息的发送方经 `ctx.agentController` 投递,而不是自己写入收件箱。[`dsh-base`](../../packages/bundle/base/README.zh.md) 是默认产品组合,[`dsh-sdk-minimal`](../../packages/bundle/sdk-minimal/README.zh.md) 则声明一棵更小的独立配置树。
<a id="creation-and-ownership"></a>
@@ -311,6 +312,67 @@ cause 是由 TypeScript 强制约束的同进程输入。活跃的取消持有
`ctx.agents` 携带的进程本地 initiator 就是上面的确切 `Agent`,不是单独的 frame 或复制的标识。环境中存在该值既不能证明存活,也不代表授权;[initiator 作用域决策](../../.agents/notes/implemented/architecture/2026-07-15-agent-initiator-scope.zh.md)定义其生命周期和作用域规则。
<a id="messages-from-outside-an-agent"></a>
## 来自 Agent 外部的消息
代表他人送来消息的发送方(例如 Web Session Remote)调用 [`agent-controller`](../../packages/core/agent-controller/README.zh.md) 的 `ctx.agentController.deliver`,而不是 `followup()` 或 `steer()`。控制器按该 Agent 的投递顺序准备内容,向 `agent-controller/admission` waterfall 请求决定,再放入被接纳的消息;Agent 自身机制产生的消息不经过这个阶段。[投递决策](../../.agents/notes/implemented/architecture/2026-10-17-agent-controller-message-delivery.zh.md)说明了这个阶段为什么不放在 `agent` 里。
源码:[`packages/core/agent-controller/src/types.ts`](../../packages/core/agent-controller/src/types.ts)
```ts type-equiv
/** How a delivered message enters the inbox: `followup` for its own turn, `steer` for the nearest step. */
type DeliveryMode = 'followup' | 'steer'
```
```ts type-equiv
/** One message a sender delivers on someone's behalf. */
interface DeliveryRequest {
/** Where the message enters once it is admitted. */
readonly mode: DeliveryMode
/** Durable attribution recorded on the message. */
readonly source: MessageSource
/** The sender's cancellation; it ends the sender's wait at once and stops a message that has not entered the inbox. */
readonly signal: AbortSignal
/**
* Turn the sender's content into durable message content, such as by
* storing attachments. It runs in the Agent's delivery order, after the
* earlier messages to the same Agent settled; its failure reaches the
* sender unchanged.
* @param signal - aborted when the sender cancels or the controller stops; controller disposal waits for the preparation to settle.
* @returns the content of the message.
*/
readonly prepare: (signal: AbortSignal) => Promise<UserMessage['content']>
/**
* Record what the sender needs before the Agent wakes. It runs
* synchronously after admission, immediately before the message enters the
* inbox. A throw keeps the message out of the inbox and reaches the sender
* unchanged; the sender reverts its own effects when `deliver` rejects.
* @param message - the admitted message.
*/
readonly beforeInsert?: (message: UserMessage) => void
}
```
监听器返回 `next()` 的决定即放行,在 `next()` 之前 await 即暂留消息,返回 `reject` 决定即拒绝:
```ts type-equiv
/** The outcome of the admission stage for one message. */
type AdmissionDecision =
/** The message enters the inbox. */
| { readonly kind: 'admit' }
/** The message stays out of the inbox. */
| {
readonly kind: 'reject'
/** Code of the refusal, owned by its source, such as `forge/worktree-missing`. */
readonly reason: string
/** Explanation for a sender that shows nothing of the refusing plugin. */
readonly message: string
/** Whether the refusing plugin explains the refusal in its own interface. */
readonly presented: boolean
}
```
<a id="interception-decisions"></a>
## 拦截决策
@@ -449,6 +511,33 @@ interface TurnBoundaryProjection {
Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — the language sides differ only in locale-specific paired document paths. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.zh.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
<a id="ctxagentcontroller--agentcontroller"></a>
### `ctx.agentController` — `AgentController`
Delivers the messages that senders, such as the Web Session Remote, bring on someone's behalf. Messages to one Agent are prepared, decided, and appended one at a time in arrival order; messages to different Agents do not wait for each other. Disposal stops every message that has not entered an inbox and finishes once each delivery it stopped has settled.
```ts cordis-catalog
/**
* Deliver one message from outside the Agent: prepare its content, ask the
* `agent-controller/admission` stage for a decision, then append an
* admitted message with `followup` or `steer`.
* @param agent - the receiving Agent.
* @param request - delivery mode, source, cancellation, and the sender's content preparation.
* @returns the id of the message once it is in the inbox.
* @throws {@link AgentAdmissionRejected} when a listener rejects the message
* or throws, or when the Agent left the registry first; the error of
* `prepare`, `beforeInsert`, `followup`, or `steer` unchanged; the sender's
* cancellation reason as soon as it cancels, also while the message waits
* behind another one; the stop reason once the controller stops.
*/
async deliver(agent: Agent, request: DeliveryRequest): Promise<MessageId>
```
Types: [MessageId](llm-streaming.zh.md)
Source: [`packages/core/agent-controller/src/index.ts`](../../packages/core/agent-controller/src/index.ts)
<a id="ctxagentdefaultmodel--agentdefaultmodelconfig"></a>
### `ctx.agentDefaultModel` — `AgentDefaultModelConfig`
@@ -1111,6 +1200,43 @@ Types: [Scoped](scope.zh.md)
Source: [`packages/core/agent/src/runtime-types.ts`](../../packages/core/agent/src/runtime-types.ts)
<a id="agent-controller-events"></a>
### `agent-controller/*` events
<a id="agent-controlleradmission--waterfall"></a>
#### `agent-controller/admission` — waterfall
Decide whether one message from outside an Agent enters its inbox. `ctx.agentController.deliver` runs this stage and appends the message only after the whole chain admitted it; messages the Agent's own machinery produces enter through `followup`, `steer`, or `inject` without it. Listeners run in registration order, and messages to one Agent are decided one at a time in arrival order. A listener admits by returning the decision of `next()`, whose last step admits; awaiting before `next()` holds the message, and returning a `reject` decision refuses it without asking later listeners. A thrown error means the listener's check itself failed. A listener must not wait for a person when the message source is unattended, such as a schedule or a webhook.
```ts cordis-catalog
/**
* Decide whether one message from outside an Agent enters its inbox.
* `ctx.agentController.deliver` runs this stage and appends the message
* only after the whole chain admitted it; messages the Agent's own
* machinery produces enter through `followup`, `steer`, or `inject`
* without it. Listeners run in registration order, and messages to one
* Agent are decided one at a time in arrival order. A listener admits by
* returning the decision of `next()`, whose last step admits; awaiting
* before `next()` holds the message, and returning a `reject` decision
* refuses it without asking later listeners. A thrown error means the
* listener's check itself failed. A listener must not wait for a person
* when the message source is unattended, such as a schedule or a webhook.
* @param payload.agent - the receiving Agent.
* @param payload.message - the message as it will enter the inbox.
* @param payload.signal - aborted when the sender cancels or the controller stops; a holding
* listener must settle once it aborts, because controller disposal waits for it.
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): Agent-scoped listeners receive only that Agent.
* @mode waterfall
*/
'agent-controller/admission'(this: Scoped<Agent>, payload: { agent: Agent; message: UserMessage; signal: AbortSignal }, next: () => Promise<AdmissionDecision>): Promise<AdmissionDecision>
```
Types: [Scoped](scope.zh.md) · [UserMessage](session.zh.md)
Source: [`packages/core/agent-controller/src/types.ts`](../../packages/core/agent-controller/src/types.ts)
<a id="agent-loop-events"></a>
### `agent-loop/*` events
+2 -2
View File
@@ -36,5 +36,5 @@
en: 077aa6573d9ede1a
zh: 0acea592a6e3630a
/host-wide-schedule/host-delivery:
en: b6d522d24b9b97ba
zh: 84df8ea09bedf51c
en: d5fc5b853529b139
zh: 4cc5eba14f9660e6
+21 -1
View File
@@ -431,7 +431,7 @@ New tasks initialize an empty history. Older task rows without history expose on
## Host delivery
The Host reads persisted tasks at startup and sets its timer to the earliest active target. Inactive tasks remain queryable but never schedule another delivery. At delivery it resolves the original Session through the Session controller, including cold Sessions. After restoration it samples the wall clock again and excludes members that are no longer due after a rollback; those tasks retain their timer obligation. Neither a Goal nor a separate Run completion record participates in dispatch.
The Host reads persisted tasks at startup and sets its timer to the earliest active target. Inactive tasks remain queryable but never schedule another delivery. At delivery it resolves the original Session, including cold Sessions, through `schedule/resolve-agent`. The embedding owns activation: the SDK handler waits for initialization and retains its Session handle and model options; without a handler, the Web Session controller restores the Session. The composition waits for its activation provider before starting Schedule. After restoration it samples the wall clock again and excludes members that are no longer due after a rollback; those tasks retain their timer obligation. Neither a Goal nor a separate Run completion record participates in dispatch.
Plugin-sourced `followup()` synchronously appends the message to the Session inbox. After a successful Session flush, one task-record put appends a saved delivery with its instruction snapshot and stores the latest `ScheduleDeliveryReceipt` together with the one-shot's `inactive` status or the recurring task's next target and status. Recurring tasks in the same batch share the receipt's `messageId`; each receipt retains that task's delivered occurrence time. Delivery never steers or cancels the current turn and does not wait for model completion.
@@ -547,4 +547,24 @@ Durable task set changed; clients refetch global task and Session-active catalog
```
Source: [`packages/schedule/schedule/src/types.ts`](../../packages/schedule/schedule/src/types.ts)
<a id="scheduleresolve-agent--waterfall"></a>
#### `schedule/resolve-agent` — waterfall
Restore a reminder's Session through its embedding's owning controller. Call next() to use the Web Session controller.
```ts cordis-catalog
/**
* Restore a reminder's Session through its embedding's owning controller.
* Call next() to use the Web Session controller.
* @param sessionId - Original Session receiving the reminder.
* @mode waterfall
*/
'schedule/resolve-agent'(sessionId: SessionId, next: () => Promise<Agent>): Promise<Agent>
```
Types: [Agent](core.md) · [SessionId](core.md)
Source: [`packages/schedule/schedule/src/index.ts`](../../packages/schedule/schedule/src/index.ts)
<!-- END GENERATED cordis-surface -->
+21 -1
View File
@@ -431,7 +431,7 @@ type ScheduleDeliveryHistoryResult =
## 宿主投递
宿主启动时读取持久任务,并将定时器设为最早的活动目标。未运行任务仍可查询,但不会再次安排投递。投递时通过 Session controller 获取原 Session,包括尚未加载的 Session。恢复后重新采样实际时间,并排除时钟回拨后尚未到期的成员;这些任务仍保留定时义务。投递不涉及 Goal 或独立的 Run 完成记录。
宿主启动时读取持久任务,并将定时器设为最早的活动目标。未运行任务仍可查询,但不会再次安排投递。投递通过 `schedule/resolve-agent` 解析原 Session,包括尚未激活的会话。嵌入方拥有激活过程:SDK 处理方等待初始化,并保留其 Session handle 和模型选项;没有处理方时由 Web Session controller 恢复会话。组合等待其激活提供方就绪后再启动 Schedule。恢复后重新采样实际时间,并排除时钟回拨后尚未到期的成员;这些任务仍保留定时义务。投递不涉及 Goal 或独立的 Run 完成记录。
插件来源的 `followup()` 同步将消息追加到 Session 收件箱。Session flush 成功后,一次任务记录 put 追加带有指令快照的发送记录,并同时存储最新的 `ScheduleDeliveryReceipt`,以及一次性任务的 `inactive` 状态或重复任务的下一目标和状态。同一批次的重复任务共享回执的 `messageId`;每条回执保留该任务已投递的发生时点。投递不会中途引导或取消当前轮次,也不等待模型完成。
@@ -547,4 +547,24 @@ Durable task set changed; clients refetch global task and Session-active catalog
```
Source: [`packages/schedule/schedule/src/types.ts`](../../packages/schedule/schedule/src/types.ts)
<a id="scheduleresolve-agent--waterfall"></a>
#### `schedule/resolve-agent` — waterfall
Restore a reminder's Session through its embedding's owning controller. Call next() to use the Web Session controller.
```ts cordis-catalog
/**
* Restore a reminder's Session through its embedding's owning controller.
* Call next() to use the Web Session controller.
* @param sessionId - Original Session receiving the reminder.
* @mode waterfall
*/
'schedule/resolve-agent'(sessionId: SessionId, next: () => Promise<Agent>): Promise<Agent>
```
Types: [Agent](core.zh.md) · [SessionId](core.zh.md)
Source: [`packages/schedule/schedule/src/index.ts`](../../packages/schedule/schedule/src/index.ts)
<!-- END GENERATED cordis-surface -->
+2 -2
View File
@@ -3,8 +3,8 @@
# After editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/subsystems/session-projection.md
/session-projections:
en: e9330151d3eb579a
zh: 53e47acd1d810da9
en: 45e5f8cb8ca18d6b
zh: 4ecfad40ec9e221c
/session-projections/the-unit:
en: d48814d3ab0e997f
zh: d0337d8eba097a63
+4 -2
View File
@@ -26,6 +26,8 @@ interface ProjectionHandle<S> {
}
```
The [compatibility rules](../../packages/session/session-projection/README.md#deprecated-plugin-compatibility) define legacy disposal, keyed reads, and their once-per-key deprecation warnings.
## The unit
`ProjectionDefinition<K, S>` carries each unit's host state type locally; registration infers `S` from `stateSchema` and returns `ProjectionHandle<S>`. Only client-visible keys merge into `SessionProjectionMap`, which types their whole values. A `wire` block makes a unit client-visible, and rendering belongs to the slot system, never this layer:
@@ -314,7 +316,7 @@ Source: [`packages/session/session-projection-cache/src/index.ts`](../../package
* Retained handles remain readable after release.
* Callers must await registration before publishing consumers; registration may perform asynchronous work.
* @param definition - key, state schema, pure unit functions, and stateVersion.
* @returns a ready handle with synchronous state reads and the effect disposer.
* @returns an awaitable yielding a ready handle; the callable runtime value is deprecated.
*/
register<K extends keyof SessionProjectionMap, S>( definition: Omit<ProjectionDefinition<K, S>, 'wire'> & { wire: NonNullable<ProjectionDefinition<K, S>['wire']> }, ): Promise<ProjectionHandle<S>>
@@ -323,7 +325,7 @@ register<K extends keyof SessionProjectionMap, S>( definition: Omit<ProjectionDe
* and always checkpointed like every other unit.
* Callers must await registration before publishing consumers; registration may perform asynchronous work.
* @param definition - key, state schema, pure unit functions, and stateVersion.
* @returns a ready handle with synchronous state reads and the effect disposer.
* @returns an awaitable yielding a ready handle; the callable runtime value is deprecated.
*/
register<K extends string, S>( definition: ProjectionDefinition<K, S> & { key: Exclude<K, keyof SessionProjectionMap> }, ): Promise<ProjectionHandle<S>>
+4 -2
View File
@@ -26,6 +26,8 @@ interface ProjectionHandle<S> {
}
```
[兼容规则](../../packages/session/session-projection/README.zh.md#deprecated-plugin-compatibility)规定旧释放操作、按键读取及其每个键一次的弃用警告。
## 投影单元
`ProjectionDefinition<K, S>` 在各单元内部携带宿主状态类型;注册从 `stateSchema` 推导 `S`,并返回 `ProjectionHandle<S>`。只有客户端可见的 key 合并到 `SessionProjectionMap`,由它约束对应的全量值。`wire` 块使单元对客户端可见,渲染归 slot 体系管,永远不归本层:
@@ -314,7 +316,7 @@ Source: [`packages/session/session-projection-cache/src/index.ts`](../../package
* Retained handles remain readable after release.
* Callers must await registration before publishing consumers; registration may perform asynchronous work.
* @param definition - key, state schema, pure unit functions, and stateVersion.
* @returns a ready handle with synchronous state reads and the effect disposer.
* @returns an awaitable yielding a ready handle; the callable runtime value is deprecated.
*/
register<K extends keyof SessionProjectionMap, S>( definition: Omit<ProjectionDefinition<K, S>, 'wire'> & { wire: NonNullable<ProjectionDefinition<K, S>['wire']> }, ): Promise<ProjectionHandle<S>>
@@ -323,7 +325,7 @@ register<K extends keyof SessionProjectionMap, S>( definition: Omit<ProjectionDe
* and always checkpointed like every other unit.
* Callers must await registration before publishing consumers; registration may perform asynchronous work.
* @param definition - key, state schema, pure unit functions, and stateVersion.
* @returns a ready handle with synchronous state reads and the effect disposer.
* @returns an awaitable yielding a ready handle; the callable runtime value is deprecated.
*/
register<K extends string, S>( definition: ProjectionDefinition<K, S> & { key: Exclude<K, keyof SessionProjectionMap> }, ): Promise<ProjectionHandle<S>>
+6 -2
View File
@@ -930,10 +930,14 @@ workspaceDesktop(): { name: string; available: boolean; fileManager: 'finder' |
@Remote('fork') async fork(request: SessionForkRequest): Promise<SessionForkValue>
/**
* Admit one prompt after explicitly resuming its Session.
* Admit one prompt after explicitly resuming its Session. The prompt is
* delivered through `ctx.agentController`, whose `agent-controller/admission`
* stage runs before it enters the inbox, so the call can wait for a listener
* that holds it.
* @param request - Session identity, prompt content, source metadata, and delivery mode.
* @param signal - caller cancellation before prompt admission begins.
* @param signal - caller cancellation; it also stops a prompt waiting for or held at the admission stage.
* @returns acknowledgement that the Agent accepted the prompt.
* @throws `session/prompt-rejected` when an admission listener refuses the prompt or fails.
*/
@Remote('prompt') async prompt(request: SessionPromptRequest, signal: AbortSignal): Promise<SessionPromptValue>
+6 -2
View File
@@ -934,10 +934,14 @@ workspaceDesktop(): { name: string; available: boolean; fileManager: 'finder' |
@Remote('fork') async fork(request: SessionForkRequest): Promise<SessionForkValue>
/**
* Admit one prompt after explicitly resuming its Session.
* Admit one prompt after explicitly resuming its Session. The prompt is
* delivered through `ctx.agentController`, whose `agent-controller/admission`
* stage runs before it enters the inbox, so the call can wait for a listener
* that holds it.
* @param request - Session identity, prompt content, source metadata, and delivery mode.
* @param signal - caller cancellation before prompt admission begins.
* @param signal - caller cancellation; it also stops a prompt waiting for or held at the admission stage.
* @returns acknowledgement that the Agent accepted the prompt.
* @throws `session/prompt-rejected` when an admission listener refuses the prompt or fails.
*/
@Remote('prompt') async prompt(request: SessionPromptRequest, signal: AbortSignal): Promise<SessionPromptValue>
+2 -2
View File
@@ -3,8 +3,8 @@
# After editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/subsystems/slots.md
/web-client-slots:
en: 8d33b164230344fa
zh: 13a930ab96280744
en: 5f160332e4980284
zh: b1ae1da451a6209e
/web-client-slots/declaration-and-lifecycle:
en: f2d2c0d3b8f14ec7
zh: 441ed13ff3f81d2a
+2
View File
@@ -10,6 +10,8 @@ This page documents slot ownership, component inputs, extension APIs, and the sh
The plugin catalog declares `plugins.add.actions`, a root-scoped list for `MenuItemButton` rows after installation in the Add plugin menu. Its owner supplies `onDismiss()` to close the menu before starting an action. The slot follows the owning Plugins page declaration lifetime.
[ui-workspace](../../packages/client/ui-workspace/README.md#understand-the-implementation) documents Session-row extension positions, visibility, and keyboard behavior.
## Declaration and lifecycle
`SlotMap` is the compile-time registry. A package declaration-merges the key, cardinality, scope, owner props, keyed props, and optional slot-level inject face. The runtime declaration is the matching `children` entry on the component that owns the render location.
+2
View File
@@ -10,6 +10,8 @@ Slots 是 Web Client 的类型化 React 组合系统。[`dsh-client-ui-slots`](.
插件目录声明 `plugins.add.actions`,它是 root 作用域的 list,在添加插件菜单的安装项之后提供 `MenuItemButton` 行。owner 提供 `onDismiss()`,用于开始动作前关闭菜单。该 slot 遵循所属插件页面声明的生命周期。
[ui-workspace](../../packages/client/ui-workspace/README.zh.md#understand-the-implementation) 记录 Session 行的扩展位置、显示条件和键盘行为。
## 声明与生命周期
`SlotMap` 是编译期注册表。包通过声明合并写入 key、cardinality(基数)、scope、owner props、keyed props 与可选的 slot 级 inject face。运行时声明则是拥有该渲染位置的组件在 `children` 中给出的对应条目。
@@ -1,21 +0,0 @@
---
kind: upgrade-guide
description: "Session projection registration is asynchronous and returns a retained reader instead of a disposer."
---
# Session projection registration handles
English | [中文](guide.zh.md)
## Change
`@deepseek-ai/dsh-session-projection` changes `SessionProjectionRegistry.register(definition)` from a synchronous disposer to `Promise<ProjectionHandle<S>>`. Registry-level `stateOf(session, key)` and `SessionProjectionStateMap` are removed. Host state types stay local to `ProjectionDefinition<K, S>` and are inferred from `stateSchema` during registration. Registration errors now reject the returned promise. After the last registration is disposed, the unit leaves snapshots, checkpoints, and live notifications, but retained handles can still fold Session events on demand.
`@deepseek-ai/dsh-agent` and `@deepseek-ai/dsh-subagent` require `sessionProjections`. AgentRegistry owns turn and step state; consumers use `ctx.agents.turnBoundaryOf(session)` instead of importing `turnBoundaryProjectionDefinition`. Turn-state consumers, including the Claude Code mods bridge, require `agents`. Preset and context-pressure reads use `ctx.agentPresets.presetForSession(session)` and `ctx.tokenMeter.contextPressure(session)`. The API controller and mods bridge no longer register those definitions independently; their optional-service behavior applies when the owning service is absent. `@deepseek-ai/dsh-api-session-controller` also removes `installModelSelectionProjection`.
## Migration
1. Mount `SessionProjectionRegistry` before `AgentRegistry`, `SubagentRuntime`, and other projection owners in custom compositions. Include `@deepseek-ai/dsh-agent` wherever a plugin requires `agents`. Consumers of domain data inject the owning service.
2. Remove host-state map augmentations and imports; pass the local state type as the second `ProjectionDefinition<K, S>` argument. Keep client-value declarations in `SessionProjectionMap`. For a plugin's own definitions, await `ctx.sessionProjections.register(definition)` in asynchronous `apply()` or `[Service.init]()` and retain the handle privately before publishing dependent operations. Keep existing function-plugin namespace exports.
3. Replace `registry.stateOf(session, key)` with a business service method for another domain, or `handle.stateOf(session)` inside the owner. Replace stored unregister callbacks with `handle.dispose`, which always returns `Promise<void>`, and await explicit disposal; fiber disposal still releases registrations automatically. Await registration when handling initialization failures.
4. Replace `installModelSelectionProjection(ctx)` with `await ctx.sessionProjections.register(modelSelectionProjection)`, importing the definition from `@deepseek-ai/dsh-api-session-controller/src/model-selection-projection.ts`.
5. Typecheck custom plugins, then verify activation, a Session state read, and owner disposal. A released reader remains usable, while the released unit disappears from new snapshots once its final registration is gone.
@@ -1,21 +0,0 @@
---
kind: upgrade-guide
description: "Session 投影注册改为异步,并返回保留读取能力的句柄而不是释放函数。"
---
# Session 投影注册句柄
[English](guide.md) | 中文
## 变更
`@deepseek-ai/dsh-session-projection` 的 `SessionProjectionRegistry.register(definition)` 从同步释放函数改为返回 `Promise<ProjectionHandle<S>>`。注册表级 `stateOf(session, key)` 与 `SessionProjectionStateMap` 已移除。宿主状态类型保留在各自的 `ProjectionDefinition<K, S>` 中,注册时从 `stateSchema` 推导。注册错误现在使返回的 Promise 拒绝。最后一次注册释放后,该投影单元不再参与快照、检查点和实时通知,但保留的句柄仍可按需折叠 Session 事件。
`@deepseek-ai/dsh-agent` 与 `@deepseek-ai/dsh-subagent` 依赖 `sessionProjections`。AgentRegistry 持有轮次和步骤状态;消费方使用 `ctx.agents.turnBoundaryOf(session)`,不再导入 `turnBoundaryProjectionDefinition`。包括 Claude Code mods 桥接在内的轮次状态消费方依赖 `agents`。预设与上下文压力分别通过 `ctx.agentPresets.presetForSession(session)` 和 `ctx.tokenMeter.contextPressure(session)` 读取。API 控制器与 mods 桥接不再独立注册这些定义;所属服务缺席时采用各自的可选服务行为。`@deepseek-ai/dsh-api-session-controller` 同时移除了 `installModelSelectionProjection`。
## 迁移
1. 在自定义组合中,先挂载 `SessionProjectionRegistry`,再挂载 `AgentRegistry`、`SubagentRuntime` 及其他投影所有者。组合中的插件依赖 `agents` 时,加入 `@deepseek-ai/dsh-agent`。领域数据的消费方注入所属服务。
2. 删除宿主状态映射的声明合并与导入,将局部状态类型作为 `ProjectionDefinition<K, S>` 的第二个类型参数。保留 `SessionProjectionMap` 中的客户端值声明。对插件自己的定义,在异步 `apply()` 或 `[Service.init]()` 中等待 `ctx.sessionProjections.register(definition)`,并在提供依赖操作前于内部保存句柄。保留现有函数插件命名空间导出。
3. 将 `registry.stateOf(session, key)` 替换为其他领域的业务服务方法,或所有者内部的 `handle.stateOf(session)`。将保存的取消注册回调替换为始终返回 `Promise<void>` 的 `handle.dispose`,并等待显式释放完成;fiber 释放仍会自动释放注册。处理初始化失败时须等待注册。
4. 将 `installModelSelectionProjection(ctx)` 替换为 `await ctx.sessionProjections.register(modelSelectionProjection)`,从 `@deepseek-ai/dsh-api-session-controller/src/model-selection-projection.ts` 导入该定义。
5. 对自定义插件执行类型检查,再验证激活、Session 状态读取与 owner 释放。已释放的读取句柄仍可使用,而投影单元在最后一次注册解除后会从新快照中消失。
@@ -0,0 +1,16 @@
# Bilingual-pair consistency record for guide.md (docs/i18n/README.md): per heading
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
# After editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/upgrade-guide/v0.2.1-rc.2/sdk-desktop-context/guide.md
/:
en: d3cffa64b9c50498
zh: 515c03ce9ae6d031
/sdk-desktop-tools-and-context:
en: d202616bf6e03fa4
zh: cc276adda4b952be
/sdk-desktop-tools-and-context/change:
en: 85ac2825c52e2503
zh: fd6fdbdca131c778
/sdk-desktop-tools-and-context/migration:
en: 9caef4f8b153622a
zh: 1b4901f5b18b72fd
@@ -0,0 +1,18 @@
---
kind: upgrade-guide
description: The existing SDK profile includes native desktop tools and model context.
---
# SDK desktop tools and context
English | [中文](guide.zh.md)
## Change
The existing `sdk` profile now includes `present`, `ask_user_question`, schedule tools, file-reference guidance and time context. It does not start a Web server. Standard tool calling remains the default; `DSH_TOOLS_MODE=ptc` selects programmatic tool calling in the same profile. `sdk-minimal` is unchanged.
## Migration
1. Keep selecting `sdk`. Provide `onUserQuestion` (TypeScript) or `on_user_question` (Python) to answer built-in questions over the native protocol, or retain an in-process `user-questions/request` answerer; without one, the tool reports `NO_PROVIDER`.
2. Working directory and model still come from SDK initialization. The SDK profile does not mount the Web-specific checkout and GUI development context.
3. Custom compositions that need the previous model-visible tool/context roster can disable the `present`, `tool-ask-user`, `tool-schedule`, `file-reference-local`, `deliverable-file-references`, and `time-context` rows. Review any pinned prompts and tool schemas before upgrading.
@@ -0,0 +1,18 @@
---
kind: upgrade-guide
description: 现有 SDK profile 包含原生桌面工具和模型上下文。
---
# SDK 桌面工具与上下文
[English](guide.md) | 中文
## 变更
现有 `sdk` profile 现在包含 `present`、`ask_user_question`、定时任务工具、文件引用说明和时间上下文。它不启动 Web server。默认仍使用标准工具调用;`DSH_TOOLS_MODE=ptc` 在同一 profile 中选择程序化工具调用。`sdk-minimal` 保持不变。
## 迁移
1. 继续选择 `sdk`。通过 `onUserQuestion`(TypeScript)或 `on_user_question`(Python)使用原生协议回答内建问题,或保留进程内的 `user-questions/request` 回答方;没有回答方时,工具报告 `NO_PROVIDER`。
2. 工作目录和模型仍来自 SDK 初始化。SDK profile 不挂载 Web 专用的 checkout 和 GUI 开发上下文。
3. 需要保留之前模型可见工具/上下文组合的自定义配置,可以禁用 `present`、`tool-ask-user`、`tool-schedule`、`file-reference-local`、`deliverable-file-references` 和 `time-context` 配置项。升级前请检查固定的提示词和工具 schema。
@@ -1,16 +1,16 @@
# Bilingual-pair consistency record for guide.md (docs/i18n/README.md): per heading
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
# After editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/upgrade-guide/v0.2.1-alpha.3/session-projection-handles/guide.md
# pnpm run verify-translation-pairing --write docs/upgrade-guide/v0.2.1-rc.2/session-projection-handles/guide.md
/:
en: b3f54a7eadb3398a
zh: 6815b996b2cebc6f
en: a7c158942ce2a575
zh: 7103da2fa7ca494a
/session-projection-registration-handles:
en: 5cf8cb0051860df5
zh: 2578c6c3e77bc1c7
en: 08af78abac6d3bcd
zh: 85ede5596cac9d5e
/session-projection-registration-handles/change:
en: 64c2492c43c7a263
zh: ba5a15b959df496c
en: d5e6ea606de638a0
zh: a041eee1ffd48ac3
/session-projection-registration-handles/migration:
en: bd4310bb946b6f08
zh: 2086e6e9c11bc4ed
en: 9c7d73f9f50c4bdd
zh: b1c597c63d52b444
@@ -0,0 +1,24 @@
---
kind: upgrade-guide
description: "Session projection registration resolves asynchronously to a retained reader; the callable result and registry-level state reads are deprecated."
---
# Session projection registration handles
English | [中文](guide.zh.md)
## Change
`@deepseek-ai/dsh-session-projection` changes `SessionProjectionRegistry.register(definition)` from a synchronous disposer to `Promise<ProjectionHandle<S>>`. The definition installs synchronously; awaiting the result yields a handle with `stateOf(session)` and `dispose()`, and registration errors reject it. The runtime value is a callable thenable, not a native Promise: calling it still releases the registration, but that call is deprecated. `typeof registration` is `"function"` and `registration instanceof Promise` is false, so code that branches on those checks or treats functions as callbacks is affected; Vitest's `.rejects` calls it instead of observing the rejection.
Registry-level `stateOf(session, key): unknown` is deprecated. It returns `undefined` whenever no plugin currently registers the key: its owner is not composed, has not reached registration, or has released its last registration. `SessionProjectionStateMap` is removed; host state types stay local to `ProjectionDefinition<K, S>` and are inferred from `stateSchema`. After the last registration is disposed, the unit leaves snapshots, checkpoints, and live notifications, but retained handles still fold Session events on demand. [Runtime compatibility](../../../../packages/session/session-projection/README.md#deprecated-plugin-compatibility) details the deprecated calls.
`@deepseek-ai/dsh-agent` and `@deepseek-ai/dsh-subagent` require `sessionProjections`. AgentRegistry owns turn and step state and provides `ctx.agents.turnBoundaryOf(session)`; `@deepseek-ai/dsh-agent-loop` no longer exports `turnBoundaryProjectionDefinition`. Turn-state consumers, including the Claude Code mods bridge, require `agents`. Preset and context-pressure reads use `ctx.agentPresets.presetForSession(session)` and `ctx.tokenMeter.contextPressure(session)`. The API controller and mods bridge no longer register those definitions independently; their optional-service behavior applies when the owning service is absent. `@deepseek-ai/dsh-api-session-controller` removes `installModelSelectionProjection`.
## Migration
1. Mount `SessionProjectionRegistry` before `AgentRegistry`, `SubagentRuntime`, and other projection owners in custom compositions. Include `@deepseek-ai/dsh-agent` wherever a plugin requires `agents`.
2. For a plugin's own definitions, await `ctx.sessionProjections.register(definition)` in asynchronous `apply()` or `[Service.init]()` and retain the handle before publishing dependent operations. Remove `SessionProjectionStateMap` augmentations and pass the state type as the second `ProjectionDefinition<K, S>` argument. Keep client-value declarations in `SessionProjectionMap`.
3. Replace `registry.stateOf(session, key)` with the owning service's method, such as `ctx.agents.turnBoundaryOf(session)`, or `handle.stateOf(session)` inside the owner; until then, handle its `undefined` result. Replace calls to the registration result with an awaited `handle.dispose()`; fiber disposal still releases registrations automatically.
4. Where a native Promise is required, pass `Promise.resolve(registration)`, including `await expect(Promise.resolve(registration)).rejects.toThrow(...)` in Vitest. Replace `typeof` or `instanceof Promise` checks on the result with awaiting.
5. Replace `installModelSelectionProjection(ctx)` with `await ctx.sessionProjections.register(modelSelectionProjection)`, importing the definition from `@deepseek-ai/dsh-api-session-controller/src/model-selection-projection.ts`.
6. Typecheck custom plugins, then verify activation, a Session state read, a rejected registration, and owner disposal.
@@ -0,0 +1,24 @@
---
kind: upgrade-guide
description: "Session 投影注册异步解析为保留读取能力的句柄;可调用的注册结果与注册表级状态读取已弃用。"
---
# Session 投影注册句柄
[English](guide.md) | 中文
## 变更
`@deepseek-ai/dsh-session-projection` 的 `SessionProjectionRegistry.register(definition)` 从同步释放函数改为 `Promise<ProjectionHandle<S>>`。定义同步安装;等待结果得到带 `stateOf(session)` 与 `dispose()` 的句柄,注册错误使结果拒绝。运行时返回值是可调用的 thenable,而非原生 Promise:调用它仍会释放注册,但该调用已弃用。`typeof registration` 为 `"function"`,`registration instanceof Promise` 为 false,因此按这些检查分支或把函数视为回调的代码会受影响;Vitest 的 `.rejects` 会调用它,而不是观察拒绝。
注册表级 `stateOf(session, key): unknown` 已弃用。当前没有任何插件注册该 key 时它返回 `undefined`:所属插件未组合、尚未执行到注册,或已释放最后一次注册。`SessionProjectionStateMap` 已移除;宿主状态类型保留在各自的 `ProjectionDefinition<K, S>` 中,并从 `stateSchema` 推导。最后一次注册释放后,该投影单元不再参与快照、检查点和实时通知,但保留的句柄仍可按需折叠 Session 事件。[运行时兼容说明](../../../../packages/session/session-projection/README.zh.md#deprecated-plugin-compatibility)详述这些已弃用调用。
`@deepseek-ai/dsh-agent` 与 `@deepseek-ai/dsh-subagent` 依赖 `sessionProjections`。AgentRegistry 持有轮次和步骤状态,并提供 `ctx.agents.turnBoundaryOf(session)`;`@deepseek-ai/dsh-agent-loop` 不再导出 `turnBoundaryProjectionDefinition`。包括 Claude Code mods 桥接在内的轮次状态消费方依赖 `agents`。预设与上下文压力分别通过 `ctx.agentPresets.presetForSession(session)` 和 `ctx.tokenMeter.contextPressure(session)` 读取。API 控制器与 mods 桥接不再独立注册这些定义;所属服务缺席时采用各自的可选服务行为。`@deepseek-ai/dsh-api-session-controller` 移除了 `installModelSelectionProjection`。
## 迁移
1. 在自定义组合中,先挂载 `SessionProjectionRegistry`,再挂载 `AgentRegistry`、`SubagentRuntime` 及其他投影所有者。组合中的插件依赖 `agents` 时,加入 `@deepseek-ai/dsh-agent`。
2. 对插件自己的定义,在异步 `apply()` 或 `[Service.init]()` 中等待 `ctx.sessionProjections.register(definition)`,并在提供依赖操作前保存句柄。删除 `SessionProjectionStateMap` 的声明合并,将状态类型作为 `ProjectionDefinition<K, S>` 的第二个类型参数。保留 `SessionProjectionMap` 中的客户端值声明。
3. 将 `registry.stateOf(session, key)` 替换为所属服务的方法(例如 `ctx.agents.turnBoundaryOf(session)`),或所有者内部的 `handle.stateOf(session)`;替换之前须处理其 `undefined` 结果。将对注册结果的调用替换为等待 `handle.dispose()`;fiber 释放仍会自动释放注册。
4. 需要原生 Promise 时传入 `Promise.resolve(registration)`,包括在 Vitest 中写 `await expect(Promise.resolve(registration)).rejects.toThrow(...)`。将对结果的 `typeof` 或 `instanceof Promise` 检查改为直接等待。
5. 将 `installModelSelectionProjection(ctx)` 替换为 `await ctx.sessionProjections.register(modelSelectionProjection)`,从 `@deepseek-ai/dsh-api-session-controller/src/model-selection-projection.ts` 导入该定义。
6. 对自定义插件执行类型检查,再验证激活、Session 状态读取、注册被拒绝以及 owner 释放。
+2 -2
View File
@@ -15,8 +15,8 @@
en: 17867305a3e1b52a
zh: 876fedf1f6f563e4
/configure-models/add-a-custom-model-api/discover-models:
en: 97318c5a115199ff
zh: 0c455e4232cf0265
en: ec10ba53a00eddc6
zh: 857373a4e0fefee4
/configure-models/select-a-model:
en: 6a94264c99535caf
zh: f97bb0dc6a40c0d3
+1 -1
View File
@@ -32,7 +32,7 @@ The Provider ID is permanent because requests, saved sessions, model defaults, a
### Discover models
Under **Model catalog**, choose **Fetch available models** to ask the endpoint which models it serves. The request uses the base URL, protocol, and key currently in the form, or a saved provider's stored key, and the reply opens a searchable picker: search, tick the models you want, and choose **Add selected**. Nothing is stored until you save or create the provider.
Under **Model catalog**, choose **Fetch available models** to ask the endpoint which models it serves. The request uses the base URL, protocol, and key currently in the form, or a saved provider's stored key, and the reply opens a searchable picker: models already in the list start ticked, so search, tick the models to add, untick the ones to remove, and choose **Update list**. Configured models the endpoint does not list stay. Nothing is stored until you save or create the provider.
Discovery reads the listing formats common gateways publish, but not every endpoint answers in one of them, so treat it as a convenience rather than a guarantee: when it fails or lists nothing, add the model ids by hand and they work just the same. A built-in provider is always answered from the installed catalog, even when its base URL points at a gateway, so fetch through a custom provider to see what the gateway really serves.
+1 -1
View File
@@ -32,7 +32,7 @@ Provider ID 是永久的,因为请求、已保存会话、模型默认值和
### 探测模型
在**模型目录**中选择**获取可用模型**,即可询问端点它提供哪些模型。请求使用表单当前的 API 地址、协议和密钥,已保存的提供商则用已存储的密钥;响应会打开一个可搜索的选择框,搜索、勾选想要的模型,再点**添加所选**。保存或创建提供商之前不会存储任何内容。
在**模型目录**中选择**获取可用模型**,即可询问端点它提供哪些模型。请求使用表单当前的 API 地址、协议和密钥,已保存的提供商则用已存储的密钥;响应会打开一个可搜索的选择框,列表中已有的模型默认勾选,搜索后勾选要添加的模型、取消勾选要移除的模型,再点**更新列表**。端点未列出的已配置模型保持不变。保存或创建提供商之前不会存储任何内容。
探测读取的是常见网关公开的列表格式,但并非每个端点都用这些格式作答,所以它只是便利手段而非保证:探测失败或列表为空时,手动添加模型 ID 即可,效果完全一样。内置提供商一律由已安装目录作答,即使其 API 地址指向网关也是如此,要查看网关实际提供的模型,请通过自定义提供商探测。
Loaded 100 of 377 files, more files were not shown because too many files have changed in this diff. Show more