mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-11-09 20:07:43 +08:00
Merge remote-tracking branch 'origin/master' into feat/mcp-manager-core
This commit is contained in:
+4
-4
@@ -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: 15fb56befd328339
|
||||
zh: 0ffb10ada7e52805
|
||||
en: dd05aa1ee608c414
|
||||
zh: 2863ced0086e3122
|
||||
/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: 16e1f2467c559315
|
||||
zh: a714ec6ff2722cf1
|
||||
en: 624efdd136029771
|
||||
zh: 7d60207147e631fe
|
||||
/agent-note-client-plugin-loading-lazy-factories-cordis-lifecycle-and-hot-reload/alternatives-considered:
|
||||
en: b8f036f50cff7cd8
|
||||
zh: 6d623c4a1f4314b1
|
||||
|
||||
@@ -67,8 +67,8 @@ Why is the roster yml rows and not a scan? Because which plugins compose into a
|
||||
1. The kernel mounts the vendored Loader and injects the module system as `internal` before any entry exists. Ordering matters: `tree.import`'s bare-import fallback must never run in a browser.
|
||||
2. It creates every graph row uniformly. Importing the modules row returns the memoized bootstrap exports, whose `apply()` reads that tree's `Loader.internal` and provides the same instance as `ctx.modules`; rows that require that service remain PENDING until then, so the modules row needs no special creation position. Render assembly is an ordinary host-graph row provided by `dsh-client-ui-renderer`; the kernel appends no assembly pseudo-entry.
|
||||
3. Graph order governs synchronous factory availability; Cordis activation remains independent and proceeds through service waiting.
|
||||
4. `settled` = every entry created + `loader.await()` quiescent + an all-ACTIVE sweep. The sweep lists each import-failed, FAILED, or PENDING fiber with its missing services. It exists because cordis inject waits have no timeout — the sweep is the fail-loud floor.
|
||||
5. The framework-free loading page projects real fiber states via `internal/status`. After the sweep, the kernel calls `ctx.uiRenderer.mount(container)` and replaces the page with the real UI in one pass.
|
||||
4. `settled` = every entry created + `loader.await()` quiescent + the boot audit. The audit lists each import-failed, FAILED, or PENDING fiber with its activation error or missing services. It exists because cordis inject waits have no timeout. An inactive entry on the browser required list rejects startup; other inactive entries produce one console warning ([startup policy](2026-09-09-consumer-owned-startup-strictness.md)).
|
||||
5. The framework-free loading page projects real fiber states via `internal/status`. After the audit accepts the tree, the kernel calls `ctx.uiRenderer.mount(container)` and replaces the page with the real UI in one pass.
|
||||
|
||||
### Live graph reconciliation
|
||||
|
||||
@@ -104,7 +104,7 @@ The current package inventory and build forms live in the [client shell layering
|
||||
|
||||
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.
|
||||
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 boot audit; 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.
|
||||
|
||||
Roster: it lives in the web bundle's config tree (`packages/bundle/web-app/cordis.patch.yml`); `mountWebPlugins` and the `CLIENT_PACKAGES` constant are gone, and recomposing a deployment means swapping the yml/overlay. The graph composer lives in the `dsh-client-modules` node half, while the parser-preloaded client face bootstraps the browser module table. The webserver remains a plain route-registration plugin; `/api/*` binding, browser authentication, RPC envelopes, and exact Fetch routes belong to the Connection node half, while Remote dispatch belongs to API Gateway and the dev bundle watch plus SSE channel belongs to the hmr node half.
|
||||
|
||||
|
||||
@@ -67,8 +67,8 @@ Host 会快照每个已构建插件入口 bundle,并把每个调度阶段的
|
||||
1. 内核挂载 vendored Loader,在任何 entry 存在之前就把模块系统注入为 `internal`。顺序有讲究:`tree.import` 的裸 import 兜底分支在浏览器里绝不能跑到。
|
||||
2. 它统一创建每个 graph row。Import modules row 会返回记忆化的 bootstrap exports,其 `apply()` 读取该树的 `Loader.internal`,把同一个实例提供为 `ctx.modules`;需要该 service 的 row 会保持 PENDING 直至此时,因此 modules row 无需特殊创建位置。渲染组装是由 `dsh-client-ui-renderer` 提供的普通 host graph row;内核不追加组装伪 entry。
|
||||
3. Graph 顺序治理同步 factory 可用性;Cordis 激活与之独立,仍经服务等待推进。
|
||||
4. `settled` = 每个 entry 已创建 + `loader.await()` 完全停稳 + 一次全 ACTIVE 扫描。扫描列出每个 import 失败、FAILED 或 PENDING 的 fiber 及其缺失的服务。它存在的理由:cordis 的 inject 等待没有超时——这次扫描就是大声失败的兜底线。
|
||||
5. 不依赖框架的 loading 页经 `internal/status` 投影真实 fiber 状态。检查完成后,内核调用 `ctx.uiRenderer.mount(container)`,一次切换到真实 UI。
|
||||
4. `settled` = 每个 entry 已创建 + `loader.await()` 完全停稳 + 启动审计。审计列出每个 import 失败、FAILED 或 PENDING 的 fiber 及其激活错误或缺失的服务。它存在的理由:cordis 的 inject 等待没有超时。浏览器必需名单中的 entry 未激活会拒绝启动;其他未激活 entry 输出一条控制台警告([启动策略](2026-09-09-consumer-owned-startup-strictness.zh.md))。
|
||||
5. 不依赖框架的 loading 页经 `internal/status` 投影真实 fiber 状态。审计接受插件树后,内核调用 `ctx.uiRenderer.mount(container)`,一次切换到真实 UI。
|
||||
|
||||
### 动态图对账
|
||||
|
||||
@@ -104,7 +104,7 @@ Bootstrap 替换会在失效或卸载前被拒绝:模块系统保留其初始
|
||||
|
||||
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 文件。
|
||||
接受的代价:vendored Loader 在浏览器里背着闲置机件(EntryTree 持久化是 no-op,分组/隔离未用);开发期每次修改插件都要付一次 bundle 重建加 fiber 重挂;graph `inject` row 指导 factory 到达,但服务可用性仍是激活权威,因此不匹配会在启动审计时浮出;静态 UI 库保留直接实体导出;每个 bundle 多出一份 sourcemap 产物,外部 script 失败也只能给出粗粒度 URL 诊断,不能像显式 fetch 那样报告 HTTP 状态。Host 为当前图及上一代启动图保留 bundle 快照与惰性响应计划。脚本和 map body 在首次 `GET` 后缓存,因此内存随 bundle 快照和已请求的响应 body 增长。每份已物化响应在其 URL 下保持固定;若上一代 map 在重建后才首次被请求,则会读取当前 map 文件。
|
||||
|
||||
名册位于 web 组合包的配置树(`packages/bundle/web-app/cordis.patch.yml`);`mountWebPlugins` 与 `CLIENT_PACKAGES` 常量已消失,重组一次部署等于替换 yml/overlay。Graph 组合器位于 `dsh-client-modules` node 半,由 parser 预载的 Client face 则自举浏览器模块表。Webserver 继续作为朴素路由注册插件;`/api/*` 绑定、浏览器认证、RPC envelope 与精确 Fetch 路由属于 Connection node 半,Remote 分发属于 API Gateway,开发期 bundle 监视与 SSE 通道属于 HMR node 半。
|
||||
|
||||
|
||||
+4
-4
@@ -9,8 +9,8 @@
|
||||
en: 4548e7fbdbee5b5a
|
||||
zh: 0078762ea92fa180
|
||||
/agent-note-consumer-owned-startup-strictness/decision:
|
||||
en: 1f11e04162e19ad1
|
||||
zh: d102e300eba9a2ea
|
||||
en: 62e93742af5bed66
|
||||
zh: 3944d685b6503a2c
|
||||
/agent-note-consumer-owned-startup-strictness/alternatives-considered:
|
||||
en: 1b67e062213184cd
|
||||
zh: 91cc8a015de2bfe4
|
||||
@@ -18,5 +18,5 @@
|
||||
en: 283dec1b336c540d
|
||||
zh: 2ed358079072cfff
|
||||
/agent-note-consumer-owned-startup-strictness/testing:
|
||||
en: c38221069ce98fd3
|
||||
zh: 9a2f91f539eba4d4
|
||||
en: cee14c22297e5471
|
||||
zh: a68748cc36f4a574
|
||||
|
||||
@@ -20,6 +20,8 @@ The audit runs only during initial application boot. Later config HMR remains be
|
||||
|
||||
This policy governs [Web host boot](../../../../packages/boot/app-boot/README.md), including its [client plugin roster](2026-07-23-client-plugin-loading-model.md). [Per-session presets](2026-09-18-declarative-agent-presets.md) own a separate strict subtree audit.
|
||||
|
||||
The browser boot audit applies the same policy to the client plugin tree with its own global list in [`boot-client.ts`](../../../../packages/client/web/src/boot-client.ts): the module system, Session control, the renderer, the application frame, the Session view, and its sidebar. Browser entries are named by package, so the list holds package names; a name absent from the boot graph has no effect, and neither list is derived from the other. The list is the minimum fatal set: an entry that a listed entry waits on blocks startup too. Feature plugins, including credential masking, stay optional. A failed entry reports its activation error. Optional failures appear in one console warning, and in Settings → Plugins → Plugin list only after a later graph synchronization reconciles them. Renaming a listed package requires updating the list and its tests.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
- **Add transactional and best-effort modes to vendored Loader.** Rejected because strictness belongs to the application or resource owner, while a Loader group contains unrelated plugins. A mode would also expand the vendor patch and leave callers to select a policy at every group.
|
||||
@@ -36,6 +38,8 @@ The compact terminal report keeps the failing plugins visible; a separate file r
|
||||
|
||||
App-boot unit tests cover absent and disabled required ids, optional import failure, config evaluation failure, synchronous and asynchronous `apply()` failure, pending dependencies, and required failure teardown. Unit expectations pin diagnostic grouping, preservation of original error objects and import logs, exporter cleanup, complete diagnostic values, private file creation, concurrent report names, and failed-write fallback. The built Web-profile acceptance asserts a single port-conflict stack without Node wrapper output, verifies the saved diagnostic file and its stderr fallback, serves the full UI with optional failures and exits nonzero without readiness when the required HTTP port is occupied or `modules` or `connection` cannot activate.
|
||||
|
||||
Browser audit unit tests (`boot-client.client.spec.ts`, `boot.client.spec.ts`) cover required and optional import, creation, activation, and pending failures, carrier presentation, and required names absent from the boot graph. Test carriers are stricter than the product: `TestClient` requires every roster entry to activate unless a case passes `allowInactiveEntries`, and the browser e2e lanes treat a `web boot: warning` as a failure with no opt-out.
|
||||
|
||||
The [Web process matrix](../../../../apps/cli/tests/profiles/web/tests/web-failure-matrix.expected.e2e.ts) independently exercises optional and required failures at startup and after native patch-file edits. Authenticated HTTP requests and plugin lifecycle files distinguish a usable application from a surviving process. These keyless process checks complement the [controlled-delivery unit tests](../../../../packages/boot/app-boot/tests/user-patches.spec.ts): unit tests isolate reconciliation failures, while the process tests also require the shipped launcher, native watcher, and bounded shutdown to work together.
|
||||
|
||||
The matrix enables Chokidar's `awaitWriteFinish` to acknowledge stable file contents before each reload; otherwise its short change-event suppression window can discard the next test edit. Native events remain required, and assertions wait for observed activation or failure rather than a fixed settling sleep. This is explicit test configuration, not evidence for the default watcher timing.
|
||||
|
||||
+4
@@ -20,6 +20,8 @@ Required id 为 `agent-loop`、`webserver`、`modules`、`connection`、`headles
|
||||
|
||||
该策略适用于 [Web host 启动](../../../../packages/boot/app-boot/README.zh.md),包括其 [client 插件名册](2026-07-23-client-plugin-loading-model.zh.md)。[按会话的 preset](2026-09-18-declarative-agent-presets.zh.md)持有独立的严格子树审计。
|
||||
|
||||
浏览器启动审计以同一策略审计客户端插件树,使用 [`boot-client.ts`](../../../../packages/client/web/src/boot-client.ts) 中独立的全局名单:模块系统、Session 控制、渲染器、应用框架、Session 视图及其侧栏。浏览器 entry 以包名命名,因此名单写包名;启动图中不存在的名字不产生影响,两份名单互不推导。名单是最小致命集合:已列出 entry 所等待的 entry 同样会阻止启动。功能插件(包括凭据遮挡)保持可选。失败 entry 报告其激活错误。可选失败出现在一条控制台警告中;只有后续图同步调和之后,才会出现在「设置 → 插件 → 插件列表」。重命名名单中的包需要同步更新名单及其测试。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
- **给 vendored Loader 增加 transactional 与 best-effort mode。** 拒绝,因为严格语义属于应用或资源 owner,而一个 Loader group 包含互不相关的 plugin。Mode 还会扩大 vendor patch,并要求 caller 为每个 group 选择 policy。
|
||||
@@ -36,6 +38,8 @@ Required id 为 `agent-loop`、`webserver`、`modules`、`connection`、`headles
|
||||
|
||||
App-boot 单元测试覆盖缺失和禁用的 required id、optional import failure、config evaluation failure、同步和异步 `apply()` failure、pending dependency,以及 required failure teardown。单元预期输出固定诊断分组、原始错误对象与导入日志的保留、exporter 清理、完整诊断值、私有文件创建、并发报告命名以及写入失败回退行为。构建后的 Web-profile acceptance 断言端口冲突堆栈只输出一次且不包含 Node 包装输出,验证已保存的诊断文件及其 stderr 回退,并会在 optional failure 存在时继续提供完整 UI,并在 required HTTP port 被占用或 `modules`、`connection` 无法激活时以非零码退出,且不报告就绪。
|
||||
|
||||
浏览器审计单元测试(`boot-client.client.spec.ts`、`boot.client.spec.ts`)覆盖必需与可选 entry 的导入、创建、激活与 pending 失败、carrier 呈现,以及启动图中不存在的必需名称。测试载体比产品更严格:除非用例传入 `allowInactiveEntries`,`TestClient` 要求每个 roster entry 都激活;浏览器 e2e 把 `web boot: warning` 视为失败,且不提供放宽。
|
||||
|
||||
[Web 进程矩阵](../../../../apps/cli/tests/profiles/web/tests/web-failure-matrix.expected.e2e.ts)分别验证启动时和原生补丁文件修改后的 optional 与 required 失败。经过认证的 HTTP 请求和插件生命周期文件区分可用应用与仅存活的进程。这些无需密钥的进程检查与[受控事件投递单元测试](../../../../packages/boot/app-boot/tests/user-patches.spec.ts)互补:单元测试隔离配置协调失败,进程测试还要求随附启动器、原生监听器和有界关闭流程协同工作。
|
||||
|
||||
矩阵启用 Chokidar 的 `awaitWriteFinish`,在每次重载前确认文件内容已稳定;否则它的短暂 change 事件抑制窗口可能丢弃下一次测试编辑。测试仍然依赖原生事件,并等待观察到激活或失败,而不是固定时长的休眠。这是显式测试配置,不能证明默认监听器的时序行为。
|
||||
|
||||
@@ -9,11 +9,11 @@
|
||||
en: 97fe58a13135b4a1
|
||||
zh: 7490373a8d941da5
|
||||
/agent-note-active-tool-headers-and-stateful-request-projection/decision:
|
||||
en: 0c40a9283650c643
|
||||
zh: 91a18d752d09a9ce
|
||||
en: 24138152d511bf12
|
||||
zh: 95516d7566b6db7c
|
||||
/agent-note-active-tool-headers-and-stateful-request-projection/alternatives-considered:
|
||||
en: b78785f2a603eba8
|
||||
zh: b8c268228da7e2e7
|
||||
en: 311db1f781e386e7
|
||||
zh: 7e3312544154a158
|
||||
/agent-note-active-tool-headers-and-stateful-request-projection/consequences:
|
||||
en: 60d7bf28fa5a8fd1
|
||||
zh: a17b222e7aca01e2
|
||||
en: 8a64dba42c01f647
|
||||
zh: 9e7ba7a5bd855031
|
||||
|
||||
@@ -14,9 +14,9 @@ Plugin and MCP tools change during conversations. Providers differ in whether th
|
||||
|
||||
`Session.toolHistory()` caches a capability-independent fold of committed headers and developer messages. First access restores inherited events; later reads consume only unseen events. Immutable snapshots carry initial declarations and ordered update identities with additions resolved through their historical headers. Explicit series starts and changed retained definitions rebuild declarations. Restoring a retained name with an unchanged definition continues the series: the projection keeps the declaration unchanged and lets the recorded addition re-offer the tool after its earlier removal, so a removal never disables a declared tool and a loading mode never rewrites an earlier declaration.
|
||||
|
||||
Loop and compaction requests carry this snapshot as `GenerateOptions.toolHistory`. At adapter dispatch, `projectToolUpdates` uses the prepared route's `toolUpdate`. Unsupported routes receive active definitions without `deferLoading` or developer messages. Supported routes defer historical additions; `in-history` retains removed definitions and removal blocks, while `addition-only` omits both. Only current-series update identities survive. Missing history or a request prefix omitting recorded updates uses current declarations without developer updates. Explicit deferred loading remains available on capable routes.
|
||||
Loop and compaction requests carry this snapshot as `GenerateOptions.toolHistory`. At adapter dispatch, `projectToolUpdates` uses the prepared route's `toolUpdate`. Unsupported routes receive active definitions without `deferLoading` or developer messages. Supported routes defer historical additions; `in-history` retains removed definitions and removal blocks, while `addition-only` omits both. On addition-only routes, removing any tool rebuilds the route's baseline after that update: the request declarations change even when the removed tool was activated later, so retaining superseded activation blocks cannot preserve the full conversation prefix. Adding tools when no declarations exist also rebuilds the baseline. Deferred declarations count as existing tools. The first native tool changes the empty declaration prefix; deferring it provides no cache advantage over declaring it immediately. Previously activated tools become immediate; explicitly deferred declarations with no activation remain deferred. Subsequent additions remain deferred until their recorded activation. Superseded tool blocks are omitted without discarding other content in those messages. The Session fold and durable events remain capability-independent. Missing history or a request prefix omitting recorded updates uses current declarations without developer updates.
|
||||
|
||||
DeepSeek translates projected updates into system-role `tool_addition` and `tool_removal` blocks and `defer_loading` declarations, sending the tool-changes beta header when update blocks occur. The default `deepseek-flash` entry declares `addition-only`; pi-ai declares no mode. Chat and Trajectory display developer messages as context nodes, using localized tool-change notices: single changes show the tool name; multiple changes show counts and expandable name lists.
|
||||
DeepSeek translates projected updates into system-role `tool_addition` and `tool_removal` blocks and `defer_loading` declarations, sending the tool-changes beta header when update blocks occur. The default `deepseek-flash` entry declares `addition-only`; pi-ai selects `in-history` for compatible Anthropic routes and `addition-only` for compatible Chat Completions and Responses routes. Chat and Trajectory display developer messages as context nodes, using localized tool-change notices: single changes show the tool name; multiple changes show counts and expandable name lists.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
@@ -26,6 +26,8 @@ DeepSeek translates projected updates into system-role `tool_addition` and `tool
|
||||
|
||||
**Scan the complete log on every request.** The cached fold consumes new events and supplies stable snapshots to ordinary and auxiliary calls without adding arbitrary synchronous history readers.
|
||||
|
||||
**Rebuild the baseline on every removal.** Deferred tools also have request declarations; deleting a tool changes those declarations regardless of its activation position. Addition-only routes cannot express that deletion in history. The rebuild therefore also covers removal-only updates and removals of later-activated tools. A past reset cannot force every future addition to become immediate: each reset establishes a new baseline from which deferred additions resume.
|
||||
|
||||
**Reject same-name schema changes during tool registration.** This restriction is not enforced. Immediate failure for a plugin registering an incompatible tool requires comparing the new registration with historical definitions of tools called on each affected current surface, while excluding calls removed by compaction. That registration-time historical and scope-aware check is deferred because of its implementation complexity. Same-name schema changes remain allowed; they rebuild declarations and invalidate prefix reuse even on `in-history` routes.
|
||||
|
||||
## Consequences
|
||||
@@ -34,4 +36,6 @@ Tool lists and logical update events are capability-independent; only outgoing d
|
||||
|
||||
The authored [headless prompt/tool scenario](../../../../snapshots/session/dynamic-tool-prompt-updates/snapshot.yml) pins an appended prompt beside a deferred tool addition and verifies that the first request's messages stay unchanged. It shares the stable-prompt scenario's tool lifecycle and schema expectations.
|
||||
|
||||
The [DeepSeek replacement scenario](../../../../snapshots/session/deepseek-deferred-tools/snapshot.yml) checks the first additions to an empty set, a rebuilt baseline, a subsequent deferred addition, and removal of that later-activated tool through Messages serialization. Explicit deferred-only requests remain unchanged under the [DeepSeek failure policy](../simplification/2026-11-02-deepseek-deferred-tool-failures.md). These checks establish request construction, not measured cache savings from rebuilding.
|
||||
|
||||
The [real DeepSeek SDK test](../../../../apps/cli/tests/profiles/sdk/dynamic-tool-cache.e2e.ts) measures the first request after a native tool addition with both unchanged and simultaneously updated instructions. Unique user history prevents a shared system-prompt cache hit from satisfying the preceding-input assertion. Removal checks tool availability without requiring cache retention; each case disables retries.
|
||||
|
||||
@@ -14,9 +14,9 @@ Status: implemented
|
||||
|
||||
`Session.toolHistory()` 缓存对已提交请求头和 developer 消息的归约,不依赖模型能力。首次访问恢复继承事件;后续读取仅处理新增事件。不可变快照携带初始声明和有序更新标识,添加定义通过历史请求头解析。显式新序列和保留定义变化会重建声明。以未变化的定义恢复保留名称时序列继续:投影保持声明不变,由记录的添加在先前移除之后重新提供该工具,因此移除不会禁用已声明的工具,加载模式也不会改写先前声明。
|
||||
|
||||
循环和压缩请求通过 `GenerateOptions.toolHistory` 携带此快照。适配器分发时,`projectToolUpdates` 使用已准备路由的 `toolUpdate`。不支持的路由接收有效定义,不携带 `deferLoading` 或 developer 消息。支持的路由延迟历史添加;`in-history` 保留已移除定义和移除块,`addition-only` 则省略两者。仅保留当前序列的更新标识。缺少历史或请求前缀遗漏已记录更新时,使用当前声明且不发送 developer 更新。显式延迟加载在支持的路由上仍然可用。
|
||||
循环和压缩请求通过 `GenerateOptions.toolHistory` 携带此快照。适配器分发时,`projectToolUpdates` 使用已准备路由的 `toolUpdate`。不支持的路由接收有效定义,不携带 `deferLoading` 或 developer 消息。支持的路由延迟历史添加;`in-history` 保留已移除定义和移除块,`addition-only` 则省略两者。在 addition-only 路由移除任何工具时,使用该次更新后的状态重建路由基线:即使被移除工具是后来激活的,请求声明也已发生变化,保留已被取代的激活块无法保留完整对话前缀。不存在任何声明时添加工具,也会重建基线。延迟声明也计入已有工具。首个原生工具会改变空声明前缀;将其延迟加载并不比直接声明具有缓存优势。此前已激活工具变为立即加载;没有激活记录的显式延迟声明仍保持延迟。后续添加在记录的激活位置前保持延迟加载。已被取代的工具块会被省略,但不会丢弃这些消息中的其他内容。Session 归约和持久事件仍不依赖模型能力。缺少历史或请求前缀遗漏已记录更新时,使用当前声明且不发送 developer 更新。
|
||||
|
||||
DeepSeek 把投影后的更新转换为 system 角色的 `tool_addition` 和 `tool_removal` 块以及 `defer_loading` 声明,在存在更新块时发送工具变更 beta 请求头。默认 `deepseek-flash` 条目声明 `addition-only`;pi-ai 不声明模式。Chat 和 Trajectory 将 developer 消息显示为上下文节点,工具变更使用本地化通知:单项变更显示工具名,多项变更显示数量及可展开的名称列表。
|
||||
DeepSeek 把投影后的更新转换为 system 角色的 `tool_addition` 和 `tool_removal` 块以及 `defer_loading` 声明,在存在更新块时发送工具变更 beta 请求头。默认 `deepseek-flash` 条目声明 `addition-only`;pi-ai 为兼容的 Anthropic 路由选择 `in-history`,为兼容的 Chat Completions 和 Responses 路由选择 `addition-only`。Chat 和 Trajectory 将 developer 消息显示为上下文节点,工具变更使用本地化通知:单项变更显示工具名,多项变更显示数量及可展开的名称列表。
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
@@ -26,6 +26,8 @@ DeepSeek 把投影后的更新转换为 system 角色的 `tool_addition` 和 `to
|
||||
|
||||
**每次请求扫描完整日志。** 缓存归约处理新事件,为普通和辅助调用提供稳定快照,不增加任意同步历史读取接口。
|
||||
|
||||
**每次移除都重建基线。** 延迟工具也具有请求声明;删除工具会改变这些声明,与其激活位置无关。addition-only 路由无法通过历史表达删除。因此,仅移除的更新和移除后来激活的工具也会重建基线。一次历史重置不能让所有后续添加都变为立即加载:每次重置确立新基线,后续添加继续采用延迟加载。
|
||||
|
||||
**在工具注册时拒绝同名 schema 变化。** 当前不执行此限制。要让注册不兼容工具的插件立即收到异常,需要将新注册与各受影响当前 surface 中已调用工具的历史定义比较,同时排除已被压缩移除的调用。由于实现复杂,注册时结合历史和作用域的检查留待后续实现。同名 schema 变化仍被允许;它们会重建声明,即使在 `in-history` 路由上也会使前缀复用失效。
|
||||
|
||||
## Consequences
|
||||
@@ -34,4 +36,6 @@ DeepSeek 把投影后的更新转换为 system 角色的 `tool_addition` 和 `to
|
||||
|
||||
手写的[无界面提示词/工具场景](../../../../snapshots/session/dynamic-tool-prompt-updates/snapshot.yml)钉住追加提示词与延迟工具添加同时发生的行为,并验证首次请求的消息保持不变。它复用稳定提示词场景的工具生命周期和 schema 期望值。
|
||||
|
||||
[DeepSeek 替换场景](../../../../snapshots/session/deepseek-deferred-tools/snapshot.yml)通过 Messages 序列化验证向空工具集首次添加工具、重建后的基线、后续延迟添加,以及移除该后来激活的工具。根据 [DeepSeek 失败策略](../simplification/2026-11-02-deepseek-deferred-tool-failures.zh.md),显式全延迟请求保持原样。这些检查验证请求构造,不代表已经测量重建带来的缓存收益。
|
||||
|
||||
[真实 DeepSeek SDK 测试](../../../../apps/cli/tests/profiles/sdk/dynamic-tool-cache.e2e.ts) 分别在指令不变和指令同时更新时测量原生工具新增后的首次请求。独有的用户历史确保仅命中共享系统提示词缓存无法满足前序输入断言。移除场景检查工具可用性,不要求保留缓存;每个用例均禁用重试。
|
||||
|
||||
+6
-6
@@ -9,11 +9,11 @@
|
||||
en: 47ba548b869be190
|
||||
zh: c2e5038a56fe741c
|
||||
/agent-note-asynchronous-projection-registration-and-retained-readers/decision:
|
||||
en: be0d17bfbd1b59d4
|
||||
zh: 7a23bccdf95c21ff
|
||||
en: a6f9ae8a53af5e4b
|
||||
zh: 0aece791574ef060
|
||||
/agent-note-asynchronous-projection-registration-and-retained-readers/alternatives-considered:
|
||||
en: e7c66403fab991d2
|
||||
zh: 1580ec29116c7d42
|
||||
en: 12e0f5fc9638acc6
|
||||
zh: 24213550da27b70b
|
||||
/agent-note-asynchronous-projection-registration-and-retained-readers/consequences:
|
||||
en: 31ce6f02dd3bf39d
|
||||
zh: bcd29b2146f689b5
|
||||
en: f5c294608d87844f
|
||||
zh: 24488e439a04d81a
|
||||
|
||||
+6
-2
@@ -12,7 +12,7 @@ Host readers coupled to registry key lookup lose access when a plugin unloads, a
|
||||
|
||||
`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.
|
||||
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 and snapshots. Checkpoints keep each Session's last cell of the released unit until its key registers again: the registry no longer advances that cell, while a retained handle read still advances it forward, so a writer that runs after the owner unloads still stores the state already folded. Under the Cordis rc.11 teardown order, Web-layer plugins release their units before the Sessions they describe detach, and the final projection-cache write would otherwise drop those rows. Stored rows can therefore lag the log; the cache promises only possibly stale rows. 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.
|
||||
|
||||
@@ -20,6 +20,10 @@ Plugins compiled against the synchronous API keep running during migration. The
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Detach owned Sessions before unregistering.** Other Web-layer plugins unload in earlier fibers under the rc.11 teardown order, and plugin reload or disable reaches the same timing, so ordering one owner cannot keep every row.
|
||||
|
||||
**Checkpoint a released unit only while its cell is at the Session cursor.** A Session with a running turn at shutdown appends teardown events after its units are released, so its rows would be dropped again.
|
||||
|
||||
**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.
|
||||
|
||||
**Return a synchronous handle.** This preserves callers' immediate reads but commits the acquisition API to synchronous initialization. Awaited acquisition leaves that timing explicit while state reads remain synchronous after readiness.
|
||||
@@ -36,4 +40,4 @@ Plugins compiled against the synchronous API keep running during migration. The
|
||||
|
||||
## 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. Compatibility tests and a real Loader subprocess verify the deprecated calls.
|
||||
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. The registry also retains a released predecessor, with its definition closures and weakly keyed cells, until the same key registers again, so a key that a reloaded plugin renames or stops registering keeps the old definition referenced for the process lifetime. 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.
|
||||
|
||||
+6
-2
@@ -12,7 +12,7 @@ Status: implemented
|
||||
|
||||
`SessionProjectionRegistry.register` 返回可等待的注册结果,用于获取类型化句柄。领域所有者在激活期间等待它,再提供依赖操作。函数插件保留命名空间导出并使用异步 `apply`;服务使用初始化钩子。领域服务在内部持有定义和句柄,通过方法返回业务数据。消费方依赖该服务,无需注册其定义或接收投影读取句柄。这取代了[必需读取方决策](2026-08-19-session-projection-mandatory-seam.zh.md)中的可选首次访问获取方式与返回释放函数的细节,同时保留必需状态和裁剪 Client 视图的规则。
|
||||
|
||||
句柄拥有共享定义及其逐 Session 单元的一次引用。其释放函数恰好一次地解除全局参与;最后一次释放会从实时推进、快照和检查点中移除单元。读取句柄在释放后仍可使用原定义折叠未读取事件。后续注册的其他状态版本不会替换旧句柄的定义。注册在异步交接后检查 owner 是否存活;如果 owner 在收到句柄前卸载,注册拒绝。
|
||||
句柄拥有共享定义及其逐 Session 单元的一次引用。其释放函数恰好一次地解除全局参与;最后一次释放会从实时推进和快照中移除单元。检查点仍保留每个 Session 中已释放单元的最后一个 cell,直到它的 key 再次注册:注册表不再推进这个 cell,但已保留的 handle 读取时仍会将其向前推进,因此 owner 卸载后才运行的写入方仍会存下已经折叠出的状态。按 Cordis rc.11 的卸载顺序,Web 层插件会在它们所描述的 Session detach 之前释放单元,否则 projection cache 的最终写入会丢掉这些行。因此存储的行可能落后于日志;cache 只承诺行可能过时。读取句柄在释放后仍可使用原定义折叠未读取事件。后续注册的其他状态版本不会替换旧句柄的定义。注册在异步交接后检查 owner 是否存活;如果 owner 在收到句柄前卸载,注册拒绝。
|
||||
|
||||
AgentRegistry 持有轮次和步骤状态,并提供 `turnBoundaryOf(session)`。AgentLoop 在发布工厂前只获取一次 inbox 句柄,在创建 Agent 作用域前读取最后轮次,并将 inbox 回调与该轮次编号传入同步 Agent 构造过程。loop fiber 移除全局注册后,回调仍在清理期间保留其句柄。这保留既有创建与 setup 顺序,无需逐 Agent 注册或增加初始化状态。
|
||||
|
||||
@@ -20,6 +20,10 @@ AgentRegistry 持有轮次和步骤状态,并提供 `turnBoundaryOf(session)`
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**卸载前先 detach 自己拥有的 Session。** 按 rc.11 的卸载顺序,其他 Web 层插件在更早卸载的 fiber 里,插件重载或禁用也会碰上同样的时机,调整单个 owner 的顺序保不住所有行。
|
||||
|
||||
**只在已释放单元的 cell 位于 Session 游标时才写入检查点。** 关停时正在运行一轮的会话会在单元释放后追加 teardown 事件,因此它的这些行会再次被丢掉。
|
||||
|
||||
**保留注册表级查询并延迟注销。** 这使无关插件的释放顺序依赖每个读取方的最后一次访问。保留读取句柄使清理不再依赖该顺序。
|
||||
|
||||
**返回同步句柄。** 这保留消费者的立即读取,却将获取 API 固定为同步初始化。等待获取过程使时序显式,同时就绪后的状态读取仍同步。
|
||||
@@ -36,4 +40,4 @@ AgentRegistry 持有轮次和步骤状态,并提供 `turnBoundaryOf(session)`
|
||||
|
||||
## Consequences
|
||||
|
||||
投影所有者必须迁移获取方式并保留句柄;其他消费方使用所属服务。只要调用方持有已释放句柄,其定义和弱键单元就仍存在;句柄不会恢复全局 wire 参与。注册读取测试覆盖释放后折叠、版本替换、无关单元隔离、重复释放、并发 owner 释放以及获取期间 owner 卸载。Agent 生命周期测试覆盖清理失败、作用域创建竞态与后台子 Agent 停止。兼容测试与真实 Loader 子进程验证这些已弃用调用。
|
||||
投影所有者必须迁移获取方式并保留句柄;其他消费方使用所属服务。只要调用方持有已释放句柄,其定义和弱键单元就仍存在;句柄不会恢复全局 wire 参与。注册表自身也会保留已释放的前任及其定义闭包和弱键单元,直到同一 key 再次注册;因此重载后改名或不再注册的 key 会在进程生命周期内一直引用旧定义。注册读取测试覆盖释放后折叠、版本替换、无关单元隔离、重复释放、并发 owner 释放以及获取期间 owner 卸载。Agent 生命周期测试覆盖清理失败、作用域创建竞态与后台子 Agent 停止。兼容测试与真实 Loader 子进程验证这些已弃用调用。
|
||||
|
||||
+2
-2
@@ -9,8 +9,8 @@
|
||||
en: 9f84c9bd85b6aace
|
||||
zh: 8ebb10962c219624
|
||||
/agent-note-serve-session-list-rows-from-a-summary-key-set/decision:
|
||||
en: 6e8f43d7a53c35fd
|
||||
zh: a6e2bccc20474515
|
||||
en: aada3ca481120bff
|
||||
zh: 1fa45a276a47e921
|
||||
/agent-note-serve-session-list-rows-from-a-summary-key-set/alternatives-considered:
|
||||
en: ce8320f1032018ed
|
||||
zh: 4c5fad5e605bf524
|
||||
|
||||
+2
-2
@@ -13,10 +13,10 @@ A Session-list row carries every registered client view. Four of those views are
|
||||
The Session summary is every registered client view except the keys whose `SessionProjectionMap` entry carries the summary-exclusion mark.
|
||||
|
||||
- [`packages/session/session-projection/src/types.ts`](../../../../packages/session/session-projection/src/types.ts) declares the type-only mark `SessionProjectionSummaryExcluded<T>` over a unique symbol that is never exported and never present at runtime. `SessionProjectionViewValue<T>` strips it distributively, so a nullable entry keeps its `null`; `SessionProjectionSummaryExcludedKey` tests the non-nullish part; `SessionProjectionSummaryValues` is every view value minus those keys.
|
||||
- Each owning package wraps only its own map entry: `turnOutline` (session-turn-outline), `sessionStats` (session-stats), `todos` (tool-todo), and `imageLimits` (session-controller's boot-constant attachment limits, which every Session shares). The todo entry is `SessionProjectionSummaryExcluded<TodoItem[]> | null`, because an intersection with `null` would absorb the null member.
|
||||
- Each owning package wraps only its own map entry: `turnOutline` (session-turn-outline), `sessionStats` (session-stats), `todos` (tool-todo), `imageLimits` (session-controller's boot-constant attachment limits, which every Session shares), and `contextPressure` and `contextBreakdown` (token-meter). The todo entry is `SessionProjectionSummaryExcluded<TodoItem[]> | null`, because an intersection with `null` would absorb the null member.
|
||||
- Registration carries the same decision as a plain boolean: `wire.summary` says whether a Session summary includes the unit, and omitting it means `true`. A key whose map entry carries the mark declares `false` so the runtime key set and the client view type agree. The mark itself has no runtime representation — `wire.view` and `viewSchema` are typed with `SessionProjectionViewValue`, which strips it, so the erase in `register` only widens the state type.
|
||||
- [`packages/session/session-projection/src/index.ts`](../../../../packages/session/session-projection/src/index.ts) exposes `summaryKeys(): ReadonlySet<key>`, derived from the live registrations and rebuilt only when a new key registers or a key's last registrant disposes. `cachedSnapshot`, `viewCheckpoint`, and `snapshot` accept `ProjectionKeySelection` (`readonly Key[] | ReadonlySet<Key>`) and use a passed set as-is.
|
||||
- [`packages/session/session-projection-cache/src/index.ts`](../../../../packages/session/session-projection-cache/src/index.ts) forwards that selection unchanged, and [`packages/api/session-controller/src/list.ts`](../../../../packages/api/session-controller/src/list.ts) passes `summaryKeys()` to the live and cold cached faces. `cachedPredecessorTitle` keeps its title-only read.
|
||||
- [`packages/session/session-projection-cache/src/index.ts`](../../../../packages/session/session-projection-cache/src/index.ts) forwards that selection unchanged, and [`packages/api/session-controller/src/list.ts`](../../../../packages/api/session-controller/src/list.ts) passes `summaryKeys()` to the live registry's `snapshot` and to the cold cache read. `cachedPredecessorTitle` keeps its title-only read.
|
||||
- Client types only: `SessionSummary.projectionValues` narrows to `Partial<SessionProjectionSummaryValues>`, while `SessionProjectionSnapshot.values` keeps every view value. No wire field, function, or UI name changes, and an opened Session still receives every key through `session/follow`, the control baseline, and `session.projections`.
|
||||
|
||||
Estimated effect: with logs whose outlines dominate the projection payload, the four keys are estimated at roughly two thirds of a Session-list response for a few hundred Sessions — the outline alone near 57%, the figures and todo list near 5% together, and the shared image limits near 5% — and at roughly 85% of the same response after transport compression, where the outline's unique text barely compresses while the remaining keys do.
|
||||
|
||||
+2
-2
@@ -13,10 +13,10 @@ Session 列表行携带每个已注册的客户端视图。其中四个视图从
|
||||
Session 摘要就是每个已注册客户端视图,减去 `SessionProjectionMap` 条目带摘要排除标记的键。
|
||||
|
||||
- [`packages/session/session-projection/src/types.ts`](../../../../packages/session/session-projection/src/types.ts) 声明类型层标记 `SessionProjectionSummaryExcluded<T>`,它基于一个从不导出、运行时从不存在的 unique symbol。`SessionProjectionViewValue<T>` 以分发方式去掉标记,可空条目因此保留 `null`;`SessionProjectionSummaryExcludedKey` 取非空部分判断;`SessionProjectionSummaryValues` 是全部视图值减去这些键。
|
||||
- 每个 owning package 只包装自己的 map 条目:`turnOutline`(session-turn-outline)、`sessionStats`(session-stats)、`todos`(tool-todo),以及 `imageLimits`(session-controller 的启动常量附件上限,每个 Session 共享)。todo 条目写作 `SessionProjectionSummaryExcluded<TodoItem[]> | null`,因为与 `null` 求交会吞掉 null 成员。
|
||||
- 每个 owning package 只包装自己的 map 条目:`turnOutline`(session-turn-outline)、`sessionStats`(session-stats)、`todos`(tool-todo)、`imageLimits`(session-controller 的启动常量附件上限,每个 Session 共享),以及 `contextPressure` 和 `contextBreakdown`(token-meter)。todo 条目写作 `SessionProjectionSummaryExcluded<TodoItem[]> | null`,因为与 `null` 求交会吞掉 null 成员。
|
||||
- 注册用普通布尔值承载同一决定:`wire.summary` 表示 Session 摘要是否包含该单元,省略即 `true`。map 条目带标记的 key 声明 `false`,让运行时键集合与客户端视图类型一致。标记本身没有运行时表示 —— `wire.view` 与 `viewSchema` 的签名已用 `SessionProjectionViewValue` 去标记,因此 `register` 里的擦除只是放宽状态类型。
|
||||
- [`packages/session/session-projection/src/index.ts`](../../../../packages/session/session-projection/src/index.ts) 提供 `summaryKeys(): ReadonlySet<key>`,从实时注册派生,只有新键注册或某键最后一个注册者注销时才重建。`cachedSnapshot`、`viewCheckpoint`、`snapshot` 接受 `ProjectionKeySelection`(`readonly Key[] | ReadonlySet<Key>`),传入 Set 时直接使用。
|
||||
- [`packages/session/session-projection-cache/src/index.ts`](../../../../packages/session/session-projection-cache/src/index.ts) 原样转发该选择,[`packages/api/session-controller/src/list.ts`](../../../../packages/api/session-controller/src/list.ts) 把 `summaryKeys()` 传给 live 与 cold 两个缓存读取面。`cachedPredecessorTitle` 保持只读 title。
|
||||
- [`packages/session/session-projection-cache/src/index.ts`](../../../../packages/session/session-projection-cache/src/index.ts) 原样转发该选择,[`packages/api/session-controller/src/list.ts`](../../../../packages/api/session-controller/src/list.ts) 把 `summaryKeys()` 传给实时注册表的 `snapshot` 与冷会话的缓存读取。`cachedPredecessorTitle` 保持只读 title。
|
||||
- 客户端只有类型变化:`SessionSummary.projectionValues` 收窄为 `Partial<SessionProjectionSummaryValues>`,而 `SessionProjectionSnapshot.values` 保留全部视图值。wire 字段、函数名与 UI 名称均不变;打开的 Session 仍通过 `session/follow`、控制基线与 `session.projections` 收到每个键。
|
||||
|
||||
估算效果:当 outline 在投影负载中占主导时,四个键估计占几百个 Session 时列表响应的约三分之二 —— outline 单项约 57%,统计与 todo 合计约 5%,共享的图片上限约 5% —— 占传输压缩后同一响应的约 85%,因为 outline 的唯一文本几乎无法压缩,而其余键可以。
|
||||
|
||||
+1
-1
@@ -19,7 +19,7 @@
|
||||
zh: 993378f78f5a4120
|
||||
/agent-note-liveshare-sharing-a-dsh-session-with-a-colleague/decision/two-driving-tiers:
|
||||
en: 0b9303333bf482ae
|
||||
zh: 6dfc68ad9e2924c3
|
||||
zh: 8a2ffe660bbe8730
|
||||
/agent-note-liveshare-sharing-a-dsh-session-with-a-colleague/decision/default-visibility:
|
||||
en: 754705367c350b31
|
||||
zh: e10b6541458c4a45
|
||||
|
||||
+1
-1
@@ -32,7 +32,7 @@ owner 在 DSH 里为某一个会话创建分享。分享发布到 Liveshare 服
|
||||
|
||||
每个分享带一个 `ViewerSessionMode`,只有两个值,且两档都会驱动共享会话:
|
||||
|
||||
- **view** —— 读者通过普通 prompt 路径驱动共享会话的 loop,在授予工作区的只读沙箱里运行,因此会话的工具绝不写文件。读取与检索面(`read`、`read_image`、`glob`、`grep`、`present`)、无本地文件副作用的工具(`todo_write`、`skill`、`web_search`、`web_fetch`)都限制在授权范围内:工具运行前检查其声明的每个路径,无法检查的调用一律拒绝。
|
||||
- **view** —— 读者通过普通 prompt 路径驱动共享会话的 loop,在授予工作区的只读沙箱里运行,因此会话的工具绝不写文件。读取与检索面(`read`、`read_image`、`glob`、`grep`、`present`)、无本地文件副作用的工具(`todo_write`、`skill`、`web_search`、`web_fetch`)都限制在授权范围内:工具运行前检查其声明的每个路径,无法检查的调用一律拒绝。两档也都准入 PTC 传输工具 `run_code`:程序运行在所有者自己的常驻文件策略下而不是分享档位下,因此可以触碰授权范围之外的文件,而其嵌套调用仍保留同样的守卫与路径检查;分享对话框会就此向所有者提示。
|
||||
- **write** —— 读者在受宿主写入上限约束的可写沙箱里驱动同一个 loop,沙箱也会修改授予工作区内的文件。
|
||||
|
||||
沙箱安装在会话上,但只约束分享查看者发起的轮次:`agent/pre-step` 记录一个已准入轮次的进入输入是否带有 share-viewer principal,守卫与替代定义对该轮次的每次执行读取这一结论。所有者自己发起的轮次保留会话自己的工具;所有者在查看者轮次中追加的输入仍留在该受限轮次内。该标记只存在于 Host 内存,按轮次重算,不写入会话日志。所有分享停止或全部过期后执行层被释放,会话恢复自己的工具。
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
en: 344bcc7e9825bbc8
|
||||
zh: c1d0f4b54f5b3786
|
||||
/agent-note-change-request-panel-and-worktree-sessions/decision:
|
||||
en: 6a0dc836205863ba
|
||||
zh: 3d4feb0385953dda
|
||||
en: 66bde24e7c601eb6
|
||||
zh: 5091591f3dc1ac85
|
||||
/agent-note-change-request-panel-and-worktree-sessions/deferred:
|
||||
en: 32ef8af85f4d2f66
|
||||
zh: b82b67f94346399e
|
||||
@@ -18,5 +18,5 @@
|
||||
en: e471e16b6b6450ca
|
||||
zh: 5fbc9242edc8d112
|
||||
/agent-note-change-request-panel-and-worktree-sessions/consequences:
|
||||
en: 81790c448f0a7d3b
|
||||
zh: ec63ccb2ad315ff8
|
||||
en: 0b785a2204963c4f
|
||||
zh: be4bd8db40b801a1
|
||||
|
||||
@@ -10,7 +10,9 @@ English | [中文](2026-10-09-forge-panel.zh.md)
|
||||
|
||||
## Decision
|
||||
|
||||
[`dsh-experimental-client-ui-forge`](../../../../packages/experimental/client-ui-forge/README.md) is a two-face package. Its Host half, the `forgePanel` Remote, assembles everything the panel shows for one Session into one streamed state; the forge registry and the binding service keep only their domain operations. Its Client half is a right-sidebar tab, a Session header button that shows or hides it, a New Session option, a creation card above the composer, a toast, and the [Session list icons and hover card section](2026-10-20-forge-session-list.md). Every surface calls a bound item a change request, the forge registry's term, whatever its code host calls it. The [forge bundle](../../../../packages/experimental/forge-bundle/README.md) ships the panel, the forge registry, the GitHub provider, the bindings, and an isolated worktree service as one default-disabled Plugins page entry.
|
||||
[`dsh-experimental-client-ui-forge`](../../../../packages/experimental/client-ui-forge/README.md) is a two-face package. Its Host half, the `forgePanel` Remote, assembles everything the panel shows for one Session into one streamed state; the forge registry and the binding service keep only their domain operations. Its Client half is a right-sidebar tab, a Session header button that shows or hides it, a New Session option, a creation card above the composer, a toast, and the [Session list icons and hover card section](2026-10-20-forge-session-list.md). Every surface calls a bound item a change request, the forge registry's term, whatever its code host calls it. The [forge bundle](../../../../packages/experimental/forge-bundle/README.md) ships the panel, the forge registry, the GitHub provider, the bindings, the worktree model tool, and an isolated worktree service for the panel as one default-disabled Plugins page entry.
|
||||
|
||||
Review and worktree controls serve one code-change workflow. One Plugins entry lets users enable that workflow without having to distinguish the underlying package split. Packages and the two worktree services remain independent so each owns its before-Session or in-conversation creation flow.
|
||||
|
||||
The Remote inspects the checkout through the Session's subprocess provider under its [resolved permissions and query restrictions](../bug-fix/2026-10-28-forge-git-session-permissions.md), so it reads what the Session's commands see. Inspection reads the top-level, Git, and common Git directories, the branch or the abbreviated HEAD commit, the upstream, the remotes, and the size of the branch's changes against its fork point. It runs when a Session is followed, after each `working-directory/change`, `tool/result`, and `sandbox/mode` event, and on refresh, one at a time with one trailing rerun.
|
||||
|
||||
@@ -89,4 +91,4 @@ The changes view offers no scope for one commit or for the last turn yet; both w
|
||||
|
||||
## Consequences
|
||||
|
||||
The panel shows current status only while it is open; checks that finish while nothing is mounted produce no notification. Changes made outside the Session appear after Refresh or the next tool result. Every inspection spawns up to twelve short Git processes in the Session's execution world with one remote, and two more for each further remote it tries for a fork point. A worktree exists only for a Session that requested it before its first step, and a failed or cancelled attempt can leave its branch or checkout behind. A message that enters while the Agent runs a turn or other maintenance gets no hold, so its worktree is created within the first turn and a removed worktree is not checked. While a message waits, the Agent stays idle in maintenance: the composer offers no stop, and callers waiting for the Agent to become idle wait as well. Restoring loses changes that were never committed. A base branch that requires a merge queue cannot merge from the panel. With Git Worktrees also enabled, two independent worktree services exist; the model's tool never sees the panel's service. A local or change request diff over `maxDiffBytes`, or one the host refuses, shows no files. Expanding unchanged lines of a change request reads each file from the code host once.
|
||||
The panel shows current status only while it is open; checks that finish while nothing is mounted produce no notification. Changes made outside the Session appear after Refresh or the next tool result. Every inspection spawns up to twelve short Git processes in the Session's execution world with one remote, and two more for each further remote it tries for a fork point. A worktree exists only for a Session that requested it before its first step, and a failed or cancelled attempt can leave its branch or checkout behind. A message that enters while the Agent runs a turn or other maintenance gets no hold, so its worktree is created within the first turn and a removed worktree is not checked. While a message waits, the Agent stays idle in maintenance: the composer offers no stop, and callers waiting for the Agent to become idle wait as well. Restoring loses changes that were never committed. A base branch that requires a merge queue cannot merge from the panel. The bundle mounts two independent worktree services; the model's tool never sees the panel's service. A local or change request diff over `maxDiffBytes`, or one the host refuses, shows no files. Expanding unchanged lines of a change request reads each file from the code host once.
|
||||
|
||||
@@ -10,7 +10,9 @@ Status: implemented
|
||||
|
||||
## Decision
|
||||
|
||||
[`dsh-experimental-client-ui-forge`](../../../../packages/experimental/client-ui-forge/README.zh.md) 是一个双端包。宿主端的 `forgePanel` Remote 把面板为一个 Session 展示的全部内容组装成一条流式状态;forge 注册表与绑定服务只保留各自的领域操作。客户端包括一个右侧边栏标签页、一个显示或隐藏它的 Session 标题栏按钮、一个新会话选项、一张位于输入框上方的创建卡片、一个 toast,以及[会话列表中的图标与悬浮卡片部分](2026-10-20-forge-session-list.zh.md)。所有界面都把已绑定的条目称为变更请求(forge 注册表的术语),不随代码托管平台的叫法变化。[forge 套件](../../../../packages/experimental/forge-bundle/README.zh.md)把面板、forge 注册表、GitHub 提供方、绑定服务与一个隔离的 worktree 服务作为插件页中默认禁用的一个条目发布。
|
||||
[`dsh-experimental-client-ui-forge`](../../../../packages/experimental/client-ui-forge/README.zh.md) 是一个双端包。宿主端的 `forgePanel` Remote 把面板为一个 Session 展示的全部内容组装成一条流式状态;forge 注册表与绑定服务只保留各自的领域操作。客户端包括一个右侧边栏标签页、一个显示或隐藏它的 Session 标题栏按钮、一个新会话选项、一张位于输入框上方的创建卡片、一个 toast,以及[会话列表中的图标与悬浮卡片部分](2026-10-20-forge-session-list.zh.md)。所有界面都把已绑定的条目称为变更请求(forge 注册表的术语),不随代码托管平台的叫法变化。[forge 套件](../../../../packages/experimental/forge-bundle/README.zh.md)把面板、forge 注册表、GitHub 提供方、绑定服务、工作树模型工具与面板使用的一个隔离 worktree 服务作为插件页中默认禁用的一个条目发布。
|
||||
|
||||
评审与工作树控件服务于同一个代码修改流程。统一插件入口让用户可以一次启用这些能力,无需根据底层包的拆分判断应该开启哪张卡片。包与两套工作树服务仍保持独立,分别负责会话开始前和对话中创建工作树的流程。
|
||||
|
||||
Remote 通过 Session 的子进程提供方,按其[解析出的权限与查询限制](../bug-fix/2026-10-28-forge-git-session-permissions.zh.md)检查检出,因此它读到的与 Session 命令看到的相同。检查读取顶层目录、Git 目录与公共 Git 目录、分支或缩写的 HEAD 提交、上游与远端,以及分支相对于其分叉点的变更规模。它在 Session 被关注时、每个 `working-directory/change`、`tool/result` 与 `sandbox/mode` 事件之后以及刷新时运行,一次只运行一个,最多再追加一次。
|
||||
|
||||
@@ -89,4 +91,4 @@ Session 所在的 worktree 可能在 Session 之外被删除。每次检查都
|
||||
|
||||
## Consequences
|
||||
|
||||
面板只在打开时显示当前状态;没有任何界面挂载时检查结束不会有通知。Session 之外的改动要等到刷新或下一个工具结果才会出现。只有一个远端时,每次检查最多在 Session 的执行环境中启动十二个短暂的 Git 进程;为寻找分叉点每多尝试一个远端,就多启动两个。只有在第一步之前提出请求的 Session 才会有 worktree,失败或被取消的尝试可能留下它的分支或检出。Agent 正在运行一轮或其他维护任务时进入的消息不会被暂留,因此它的 worktree 在第一轮之内创建,已删除的 worktree 也不会被检查。消息等待期间,Agent 以空闲状态处于维护任务中:输入框不提供停止,等待 Agent 空闲的调用方也会一并等待。恢复会丢失从未提交的修改。要求合并队列的基准分支无法从面板合并。同时启用 Git Worktrees 时会存在两个互不影响的 worktree 服务;模型的工具看不到面板的服务。本地或变更请求的 diff 超过 `maxDiffBytes`,或被托管平台拒绝时,不显示任何文件。展开变更请求中未变更的行时,每个文件只从托管平台读取一次。
|
||||
面板只在打开时显示当前状态;没有任何界面挂载时检查结束不会有通知。Session 之外的改动要等到刷新或下一个工具结果才会出现。只有一个远端时,每次检查最多在 Session 的执行环境中启动十二个短暂的 Git 进程;为寻找分叉点每多尝试一个远端,就多启动两个。只有在第一步之前提出请求的 Session 才会有 worktree,失败或被取消的尝试可能留下它的分支或检出。Agent 正在运行一轮或其他维护任务时进入的消息不会被暂留,因此它的 worktree 在第一轮之内创建,已删除的 worktree 也不会被检查。消息等待期间,Agent 以空闲状态处于维护任务中:输入框不提供停止,等待 Agent 空闲的调用方也会一并等待。恢复会丢失从未提交的修改。要求合并队列的基准分支无法从面板合并。组合包挂载两个互不影响的 worktree 服务;模型的工具看不到面板的服务。本地或变更请求的 diff 超过 `maxDiffBytes`,或被托管平台拒绝时,不显示任何文件。展开变更请求中未变更的行时,每个文件只从托管平台读取一次。
|
||||
|
||||
@@ -30,8 +30,8 @@
|
||||
en: 641cbfefc76f2f95
|
||||
zh: e3e718473eace85f
|
||||
/agent-note-direct-deepseek-search-and-fetch-apis/consequences:
|
||||
en: a55da02f4d3c06c0
|
||||
zh: 3c1a624f03f34f28
|
||||
en: 124a35cad810c2fc
|
||||
zh: 504da2a59c3873be
|
||||
/agent-note-direct-deepseek-search-and-fetch-apis/verification-and-remaining-api-adoption:
|
||||
en: 4c84e7bd035b52a7
|
||||
zh: 4e5687bda0a47706
|
||||
|
||||
@@ -70,7 +70,7 @@ The retained Messages path explicitly sends `thinking: { type: 'disabled' }` and
|
||||
|
||||
Retrieval no longer requires auxiliary model tokens in direct mode, but its results still consume the main model's context. One Messages web search through a gateway on 2026-10-16 consumed 8,154 input and 828 output tokens for a single server-side search. The guide prices Search and Fetch independently at CNY 5 per 1,000 successful requests with results; result count does not change a request's price. Multiple queries remain multiple requests. The stated Claude Code and Codex promotion does not establish a Harness exemption. Search API mode also sends up to 128 characters of the user's prompt to the search service with each query unless `sendOriginalQuestion` is off.
|
||||
|
||||
Search and Fetch each have an account-wide 5 QPS limit, independent of model concurrency and shared across that account's API keys. Fetch also has a target-domain limit. The current tool can issue four queries concurrently and fails the batch if one query fails. The provider has no shared rate scheduler, automatic retries, or fallback; other Sessions or processes can exhaust the same account allowance.
|
||||
Search and Fetch each have an account-wide 5 QPS limit, independent of model concurrency and shared across that account's API keys. Fetch also has a target-domain limit. The tool accepts up to `searchMaxQueries` queries per call, 10 by default, and fails the batch if one query fails. The Search provider sends at most `searchRequestsPerSecond` requests, 5 by default, in any one-second window of one plugin instance, so the second five queries of a full batch start one second after the first. The pacing is not shared across processes, and the provider has no automatic retries or fallback; other processes or clients can exhaust the same account allowance.
|
||||
|
||||
## Verification and remaining API adoption
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@ HTTP 提供方继续负责自身的 URL 校验和资源限制。选择 DeepSeek
|
||||
|
||||
直接模式中的检索不再需要辅助模型 token,但结果仍消耗主模型上下文。2026-10-16 通过网关进行的一次 Messages 网页搜索,为一次服务端搜索消耗了 8,154 个输入 token 和 828 个输出 token。指南对 Search 和 Fetch 分别按每 1,000 次成功且有结果的请求收取 5 元;结果条数不影响单次价格。多个查询仍对应多次请求。文档中的 Claude Code 与 Codex 优惠不能作为 Harness 免收费用的依据。除非关闭 `sendOriginalQuestion`,Search API 模式还会随每个查询把用户提示的至多 128 个字符发送给搜索服务。
|
||||
|
||||
Search 和 Fetch 各有账号级 5 QPS 限制,独立于模型并发,同一账号的 API 密钥共享额度。Fetch 还有目标域名限制。当前工具可并发发送四个查询,任一查询失败会使整批失败。提供方没有共享限速调度器、自动重试或回退;其他会话或进程也会消耗同一账号的额度。
|
||||
Search 和 Fetch 各有账号级 5 QPS 限制,独立于模型并发,同一账号的 API 密钥共享额度。Fetch 还有目标域名限制。工具每次调用最多接受 `searchMaxQueries` 个查询,默认 10 个;任一查询失败会使整批失败。Search 提供方在单个插件实例的任意一秒窗口内最多发送 `searchRequestsPerSecond` 个请求,默认 5 个,因此满批次的后五个查询比前五个晚一秒开始。该限速不跨进程共享,提供方也没有自动重试或回退;其他进程或客户端也会消耗同一账号的额度。
|
||||
|
||||
## 验证与尚未接入的 API 能力
|
||||
|
||||
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
# Bilingual-pair consistency record for 2026-11-02-liveshare-plugin-method-catalog.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-11-02-liveshare-plugin-method-catalog.md
|
||||
/agent-note-liveshare-plugin-method-catalog:
|
||||
en: 1daaf32c7ea8d3cd
|
||||
zh: 70a94350263d22c9
|
||||
/agent-note-liveshare-plugin-method-catalog/problem:
|
||||
en: 2122ac5fdd75f900
|
||||
zh: dc135aaa0d1dddc0
|
||||
/agent-note-liveshare-plugin-method-catalog/decision:
|
||||
en: 6e0b897ba6460894
|
||||
zh: f05866ccde01a5af
|
||||
/agent-note-liveshare-plugin-method-catalog/alternatives-considered:
|
||||
en: 57896311cada7804
|
||||
zh: 01fbd1cbd05353ef
|
||||
/agent-note-liveshare-plugin-method-catalog/consequences:
|
||||
en: e2069a5f9801f767
|
||||
zh: 0698f0b9e1e49201
|
||||
@@ -0,0 +1,23 @@
|
||||
# Agent Note: LiveShare plugin method catalog
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-11-02-liveshare-plugin-method-catalog.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
LiveShare used to expose plugin endpoints through a declaration API. The API let a plugin name arbitrary endpoints, so a catalog mistake could widen a viewer's reach without a corresponding Gateway check. The replacement needs one source of truth that says which plugin methods a share may expose.
|
||||
|
||||
## Decision
|
||||
|
||||
The catalog is enumerated from the Typert local registry at runtime. One entry is emitted per bundle, using the bundle's localized title when available and the component package title otherwise. Endpoint descriptions come from the registered Remote descriptors. A share stores only method-level policies: the Host intersects each policy with the catalog before the Gateway dispatches a call.
|
||||
|
||||
The catalog excludes the DSH core bundles. Their methods remain governed by the existing Session and Workspace sharing roles, not by plugin permissions. The core bundle names are derived from `PROFILE_TEMPLATES`, so the exclusion follows the shipped profile composition instead of a second hand-maintained list. LiveShare's own bundle is excluded because it is the sharing implementation, not a capability a share can grant itself.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
Keeping the declaration API would leave two sources of truth: a plugin-declared endpoint and the Gateway's own classification. A hand-maintained allowlist in LiveShare would drift from the shipped profile templates. Deriving the exclusion from the actual profile templates keeps the catalog aligned with composition without a second list.
|
||||
|
||||
## Consequences
|
||||
|
||||
A method that is not in the Typert local registry cannot be selected or called through plugin permissions. A plugin that uses an owner-only exact route is absent from the catalog; the viewer sees an `owner-route-not-shared` notice when it calls that route. Adding a shareable method is therefore a plugin-side registration decision, not a LiveShare catalog edit.
|
||||
@@ -0,0 +1,23 @@
|
||||
# Agent Note:LiveShare 插件方法目录
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-11-02-liveshare-plugin-method-catalog.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
LiveShare 过去通过声明 API 暴露插件端点。该 API 允许插件声明任意端点,因此目录写错时,viewer 的可达范围会在没有对应 Gateway 校验的情况下被扩大。替代方案需要一个唯一事实来源,明确一条分享可以暴露哪些插件方法。
|
||||
|
||||
## 决定
|
||||
|
||||
目录在运行时从 Typert 本地注册表枚举。每个 bundle 生成一行,优先使用 bundle 的本地化标题,没有时使用组件包标题;端点说明来自已注册的 Remote descriptor。分享记录只保存方法级策略,Host 在 Gateway 派发前把策略与目录取交集。
|
||||
|
||||
目录排除 DSH 核心 bundle。它们的方法仍由原有的 Session 和 Workspace 分享角色控制,而不是插件权限。核心 bundle 名从 `PROFILE_TEMPLATES` 派生,使排除规则跟随实际下发的 profile 组合,而不是再维护一份手写名单。LiveShare 自身的 bundle 也被排除,因为它是分享实现,不是分享可以授予自己的能力。
|
||||
|
||||
## 备选方案
|
||||
|
||||
保留声明 API 会留下两个事实来源:插件声明的端点和 Gateway 自己的分类。由 LiveShare 维护手写白名单又会与实际下发的 profile 模板漂移。从真实 profile 模板派生排除规则,让目录跟随组合而不需要第二份名单。
|
||||
|
||||
## 影响
|
||||
|
||||
不在 Typert 本地注册表中的方法无法被插件权限选中或调用。使用 owner-only exact route 的插件不会出现在目录中;viewer 调用该路由时会看到 `owner-route-not-shared` 提示。因此新增可分享方法是插件侧的注册决定,而不是修改 LiveShare 目录。
|
||||
+2
-2
@@ -9,8 +9,8 @@
|
||||
en: 3e229ecf646af866
|
||||
zh: d358507b1e1d381f
|
||||
/agent-note-wine-and-native-windows-ci/decision:
|
||||
en: 1125b44a170f70fb
|
||||
zh: 37838b9da2003651
|
||||
en: 9038773113cf3553
|
||||
zh: 67875d393093c105
|
||||
/agent-note-wine-and-native-windows-ci/alternatives-considered:
|
||||
en: 0e9a322cfc38d313
|
||||
zh: f7bdeb86ba4fea1a
|
||||
|
||||
@@ -30,7 +30,7 @@ Native watchers use `canonicalizeWatchPath()` to realpath the deepest existing a
|
||||
|
||||
Windows durable JSONL paths keep drive roots in native spelling and apply the extended-length namespace only to descendants and staging paths. The ACP teardown ladder uses real Node children, proves graceful and forced tiers with host-appropriate outcomes, and avoids claiming POSIX signal delivery on Windows. Executable fixtures provide `.cmd` shims and `PATHEXT` where the product accepts a bare command. Repository-cache helpers live inside the selected Git subpath so their declared `file:` dependencies expose command shims identically on Windows. The bundled installer exports pnpm's own workspace-ignore configuration, retains `PNPM_HOME` for pnpm data while removing that directory from lifecycle `PATH`, and prioritizes `.CMD` in `PATHEXT`; nested Git-package installation therefore cannot rejoin the enclosing workspace or let an inherited Windows pnpm executable outrank the transaction-owned wrapper.
|
||||
|
||||
Post-boot profile watcher setup proceeds only while the root fiber and Loader are both live. A concurrent setup error is contained only when the same invocation's recorded signal already owns shutdown; unrelated HMR failures remain loud. The [process-shutdown controller](../../../../apps/cli/src/process-shutdown.ts) lets a successful one-shot completion drain Node's remaining handles after root disposal, while teardown failure, deadline, and signal escalation retain forced exit. The vendored Include serializes debounced writes, retries only transient access or busy failures with bounded backoff, and observes every timer rejection. A terminal persistence failure remains on the queue and is rethrown to the teardown owner, while successful teardown drains the latest write.
|
||||
Post-boot profile watcher setup proceeds only while the root fiber and Loader are both live. A concurrent setup error is contained only when the same invocation's recorded signal already owns shutdown; unrelated HMR failures remain loud. The [process-shutdown controller](../../../../apps/cli/src/process-shutdown.ts) lets a successful one-shot completion drain Node's remaining handles after root disposal, while teardown failure, deadline, and signal escalation retain forced exit. The vendored Include journals runtime changes and writes them through an atomic rename, retrying only transient access or busy failures with bounded backoff. `flush()` and `stop()` reject while changes remain unsaved, and a later successful flush clears the previous error.
|
||||
|
||||
Shiki disables lazy TextMate-regex compilation and warms each boot grammar before user content enters the unchanged per-line tokenization budget, so scheduler contention cannot publish a partial highlighted stream. The Codex real-product fixture is pinned to stable 0.153.4 schemas and selects an actually advertised command tool and argument shape, preserving the provider-owned protocol while proving unattended rejection and whole-tree exit on each host.
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ Wine 在 Linux 内核与区分大小写的 ext4 之上采用 hoisted 依赖布
|
||||
|
||||
Windows 的持久 JSONL 路径会保留驱动器根目录的原生写法,并仅对后代路径与暂存路径应用扩展长度命名空间。ACP(Agent Client Protocol)拆卸阶梯使用真实 Node 子进程,以符合宿主语义的结果证明优雅终止与强制终止两个层级,并避免声称 Windows 会交付 POSIX 信号。产品接受裸命令时,可执行 fixture 会提供 `.cmd` 包装脚本与 `PATHEXT`。repository-cache 辅助包位于所选 Git 子路径内,因此它们声明的 `file:` 依赖会在 Windows 上以相同方式暴露命令包装脚本。随附的安装器会导出 pnpm 自有的 workspace-ignore 配置,保留 `PNPM_HOME` 作为 pnpm 数据配置,同时从生命周期 `PATH` 中移除该目录,并在 `PATHEXT` 中优先选择 `.CMD`;因此,嵌套 Git 包安装既不会重新加入外层 workspace,也不会让继承的 Windows pnpm 可执行文件抢在事务持有的 wrapper 之前。
|
||||
|
||||
启动后,只有根 fiber 与 Loader 均处于活跃状态时,系统才会继续设置 profile watcher。只有当同一次调用所记录的信号已取得关闭流程所有权时,系统才会隔离并发设置错误;无关 HMR 故障仍会响亮失败。[进程关闭控制器](../../../../apps/cli/src/process-shutdown.ts)会在根级 dispose 成功后让单次任务的正常完成流程排空 Node 剩余句柄,同时让拆卸失败、截止时间到期和信号升级继续强制退出。vendored Include 会串行化防抖写入,只对瞬时访问或忙碌故障执行有界退避重试,并确保每个由计时器触发的拒绝都得到观察。持久化最终失败后,该故障会保留在队列中,并重新抛给拆卸责任方;成功拆卸则会排空最新写入。
|
||||
启动后,只有根 fiber 与 Loader 均处于活跃状态时,系统才会继续设置 profile watcher。只有当同一次调用所记录的信号已取得关闭流程所有权时,系统才会隔离并发设置错误;无关 HMR 故障仍会响亮失败。[进程关闭控制器](../../../../apps/cli/src/process-shutdown.ts)会在根级 dispose 成功后让单次任务的正常完成流程排空 Node 剩余句柄,同时让拆卸失败、截止时间到期和信号升级继续强制退出。vendored Include 通过 journal 记录运行时变更,并以原子 rename 写入,只对瞬时访问或忙碌故障执行有界退避重试。仍有未保存变更时,`flush()` 和 `stop()` 会拒绝;之后一次成功的 flush 会清除先前的错误。
|
||||
|
||||
Shiki 会禁用 TextMate 正则的延迟编译,并在用户内容进入保持不变的逐行 tokenization(词元化)预算前预热每种启动语法,从而避免调度器争用发布不完整的高亮流。Codex 真实产品 fixture 固定使用稳定版 0.153.4 schema,并选择实际提供的命令工具与对应参数形态;这样既保留由提供方负责的协议,也能在每种宿主上证明无人值守拒绝和整棵进程树退出。
|
||||
|
||||
|
||||
+4
-4
@@ -9,11 +9,11 @@
|
||||
en: d4ab65a39cf4fd7c
|
||||
zh: 62144be9b4fd7956
|
||||
/agent-note-keep-loader-mutations-non-transactional/decision:
|
||||
en: 9b661159de63d421
|
||||
zh: 15697f468a90edb9
|
||||
en: 83268169e9436127
|
||||
zh: ce740df03e9fe24a
|
||||
/agent-note-keep-loader-mutations-non-transactional/alternatives-considered:
|
||||
en: 72b871064c7ae315
|
||||
zh: a09e5168f8124c4c
|
||||
en: 645db43a48b9cfc1
|
||||
zh: d03fabd1e82dadbd
|
||||
/agent-note-keep-loader-mutations-non-transactional/consequences:
|
||||
en: 100f687b9d498308
|
||||
zh: 1c4bc2d989e43ec7
|
||||
|
||||
@@ -12,19 +12,19 @@ Transactional config reload preserves an old plugin generation after a failed ed
|
||||
|
||||
Revert the five commits in #932, resolving package moves and retaining independent later behavior. The reported merge commit belongs to the larger #936 dependency chain; reverting its first-parent diff would remove unrelated repository-plugin support. The [vendor ledger](../../../../vendor/README.md#local-modifications) records every retained source change against the unchanged pins.
|
||||
|
||||
Loader changes entry options eagerly. EntryGroup starts siblings concurrently and logs application failures; EntryTree waits for outstanding work without rejecting failed fibers. Neither restores a previous plugin or configuration. Include retains parse validation and patch reapplication, but plugin failures can leave a partially applied tree.
|
||||
Loader changes entry options eagerly. EntryGroup starts siblings concurrently and logs application failures; EntryTree waits for outstanding work without rejecting failed fibers. Neither restores a previous plugin or configuration. Include keeps the running tree after an invalid file, but plugin failures can leave a partially applied tree.
|
||||
|
||||
Application consumers own their completion checks. The CLI waits for its fallback HMR service before installing live patch watchers. The directory chooser checks the entries it mounts. The chooser and browser package runner capture the first fiber-disposal result before removing the entry, then await it before reporting teardown complete. Preset mounting waits for its subtree and reports import, activation, and missing-service failures. [App boot](../../../../packages/boot/app-boot/README.md) owns exact patch-file watching, activation audits, and partial-context cleanup. Web startup audits activation before printing a URL or opening the browser. These adaptations preserve existing consumer behavior after the reverse patch.
|
||||
|
||||
Fiber, Entry, and isolate keep their upstream update return behavior. App boot observes discarded restart promises through the existing `internal/update` waterfall and waits for fibers before auditing a patch reload. The detached import-completion observer handles both fiber outcomes; the fiber still retains its failure for an explicit audit. Durable Include writes drain before and after child removal so a later teardown write cannot erase an earlier terminal write failure.
|
||||
Fiber, Entry, and isolate keep their upstream update return behavior. App boot observes discarded restart promises through the existing `internal/update` waterfall and waits for fibers before auditing a patch reload. The detached import-completion observer handles both fiber outcomes; the fiber still retains its failure for an explicit audit. Include persistence follows the journal adaptation in the vendor ledger: `flush()` and `stop()` reject while changes remain unsaved.
|
||||
|
||||
Two #932-specific vendor changes remain: awaited initial-file creation and forced rereading in Include, and Schemastery conditional exports. Restoring the pre-#932 debounced write/read sequence reproduces `ENOENT` in the missing-file initialization test. Keeping these two lines preserves the existing `initial` option without an application-side file writer or a second YAML serializer. Removing Schemastery exports reproduces `ERR_REQUIRE_ESM_RACE_CONDITION` while the Web preset suite boots: Node falls back to the CJS entry during concurrent ESM imports. The HMR injection decorators, conditional patch cloning, and update return values use the pre-#932 behavior. Explicit `workspace:` dependencies make the #932 workspace-link switch and dedicated lockfile check unnecessary.
|
||||
One #932-specific vendor change remains: Schemastery conditional exports. Include 1.1.1 writes the `initial` file before reading it, so missing-file initialization no longer needs a local change. Removing Schemastery exports reproduces `ERR_REQUIRE_ESM_RACE_CONDITION` while the Web preset suite boots: Node falls back to the CJS entry during concurrent ESM imports. The HMR injection decorators and update return values use the pre-#932 behavior; patch cloning follows upstream Include. Explicit `workspace:` dependencies make the #932 workspace-link switch and dedicated lockfile check unnecessary.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Keep transactional Loader updates.** They provide automatic recovery from a rejected plugin candidate, but retain the vendored lifecycle machinery being removed. Parse failures can be contained without plugin rollback.
|
||||
|
||||
**Restore every vendored file verbatim.** This would also remove lazy injected config evaluation, conditional disabled entries, lifecycle disposal fixes, durable writes, and module-loader compatibility. Those changes have independent consumers and remain recorded in the vendor ledger.
|
||||
**Restore every vendored file verbatim.** This would also remove lazy injected config evaluation, conditional disabled entries, lifecycle disposal fixes, and module-loader compatibility. Those changes have independent consumers and remain recorded in the vendor ledger.
|
||||
|
||||
**Move generic rollback into app boot.** This would retain the same candidate-generation and restoration obligations under another owner. Applications instead report failures and allow a later valid edit to recover.
|
||||
|
||||
|
||||
@@ -12,19 +12,19 @@ Status: implemented
|
||||
|
||||
撤销 #932 中的五个提交,解决包移动冲突并保留后续独立行为。记录的合并提交属于更大的 #936 依赖链;撤销其第一父提交差异还会删除无关的仓库插件支持。[Vendor 修改记录](../../../../vendor/README.md#local-modifications) 按不变的固定来源记录每项保留的源码更改。
|
||||
|
||||
Loader 立即更改条目选项。EntryGroup 并发启动同级条目并记录应用失败;EntryTree 等待未完成的工作,但不因失败的 fiber 而拒绝。两者均不恢复旧插件或配置。Include 保留解析校验和 patch 重应用,但插件失败可能留下部分应用的配置树。
|
||||
Loader 立即更改条目选项。EntryGroup 并发启动同级条目并记录应用失败;EntryTree 等待未完成的工作,但不因失败的 fiber 而拒绝。两者均不恢复旧插件或配置。Include 在文件无效时保留正在运行的配置树,但插件失败可能留下部分应用的配置树。
|
||||
|
||||
应用消费者负责完成检查。CLI 在安装实时 patch 监视器前等待其回退 HMR 服务。目录选择器检查其挂载的条目。选择器和浏览器包运行器在移除条目前取得首次 fiber 释放的结果,并等待该结果后才报告拆卸完成。预设挂载等待其子树,并报告导入、激活和缺少服务的失败。[应用启动](../../../../packages/boot/app-boot/README.zh.md) 负责精确 patch 文件监视、激活检查和部分上下文清理。Web 启动在打印 URL 或打开浏览器前检查激活状态。这些适配维持应用反向补丁后已有的消费者行为。
|
||||
|
||||
Fiber、Entry 和 isolate 保持上游的更新返回行为。App boot 通过现有的 `internal/update` waterfall 观察被丢弃的重启 promise,并在检查 patch 重载前等待 fiber。游离的导入完成观察器处理 fiber 的两种结果;fiber 仍保留失败信息供显式检查。Include 的持久写入在删除子条目前后均排空,防止后续拆卸写入掩盖更早的终止性写入失败。
|
||||
Fiber、Entry 和 isolate 保持上游的更新返回行为。App boot 通过现有的 `internal/update` waterfall 观察被丢弃的重启 promise,并在检查 patch 重载前等待 fiber。游离的导入完成观察器处理 fiber 的两种结果;fiber 仍保留失败信息供显式检查。Include 的持久化遵循 vendor 修改记录中的 journal 适配:仍有未保存变更时,`flush()` 和 `stop()` 会拒绝。
|
||||
|
||||
保留两项 #932 专属 vendor 改动:Include 中等待初始文件创建并强制重新读取,以及 Schemastery 条件导出。恢复 #932 前的防抖写入和读取顺序,会在缺失文件初始化测试中复现 `ENOENT`。保留这两行可以维持已有 `initial` 选项,而无需在应用侧增加文件写入器或另一份 YAML 序列化逻辑。移除 Schemastery exports 后,Web preset 测试在启动时复现 `ERR_REQUIRE_ESM_RACE_CONDITION`:并发 ESM 导入使 Node 回退到 CJS 入口。HMR 注入装饰器、条件 patch 克隆及更新返回值采用 #932 前的行为。显式 `workspace:` 依赖使 #932 的 workspace 链接开关与专用锁文件检查不再必要。
|
||||
保留一项 #932 专属 vendor 改动:Schemastery 条件导出。Include 1.1.1 会先写入 `initial` 文件再读取,因此缺失文件的初始化不再需要本地改动。移除 Schemastery exports 后,Web preset 测试在启动时复现 `ERR_REQUIRE_ESM_RACE_CONDITION`:并发 ESM 导入使 Node 回退到 CJS 入口。HMR 注入装饰器及更新返回值采用 #932 前的行为;patch 克隆遵循上游 Include。显式 `workspace:` 依赖使 #932 的 workspace 链接开关与专用锁文件检查不再必要。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
**保留事务化 Loader 更新。** 它们能从被拒绝的插件候选自动恢复,但会保留本次删除的 vendor 生命周期机制。解析失败可以独立于插件回滚进行处理。
|
||||
|
||||
**逐字恢复所有 vendor 文件。** 这还会删除延迟注入配置求值、条件禁用条目、生命周期释放修复、持久写入和模块加载器兼容性。这些更改具有独立消费者,并继续记录在 vendor 修改记录中。
|
||||
**逐字恢复所有 vendor 文件。** 这还会删除延迟注入配置求值、条件禁用条目、生命周期释放修复和模块加载器兼容性。这些更改具有独立消费者,并继续记录在 vendor 修改记录中。
|
||||
|
||||
**将通用回滚移入应用启动。** 这会在另一归属下保留相同的候选代次与恢复义务。应用改为报告失败,并允许后续有效编辑恢复。
|
||||
|
||||
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
# Bilingual-pair consistency record for 2026-11-02-deepseek-deferred-tool-failures.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/simplification/2026-11-02-deepseek-deferred-tool-failures.md
|
||||
/agent-note-expose-deepseek-deferred-only-request-failures:
|
||||
en: 1c4af8b4e2ec0e46
|
||||
zh: fcb806d33973025e
|
||||
/agent-note-expose-deepseek-deferred-only-request-failures/problem:
|
||||
en: c974406f21854541
|
||||
zh: 701cd64cabdfee79
|
||||
/agent-note-expose-deepseek-deferred-only-request-failures/decision:
|
||||
en: 6878caaaebd91a41
|
||||
zh: 2ebd5da04dfb1ac6
|
||||
/agent-note-expose-deepseek-deferred-only-request-failures/alternatives-considered:
|
||||
en: d10efff97379cfbb
|
||||
zh: 572b124ddbb2d86d
|
||||
/agent-note-expose-deepseek-deferred-only-request-failures/consequences:
|
||||
en: d60323c595b076f4
|
||||
zh: 1bf1068424777b7e
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
# Agent Note: Expose DeepSeek deferred-only request failures
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-11-02-deepseek-deferred-tool-failures.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
A synthetic immediate tool can make an all-deferred DeepSeek request acceptable while hiding an invalid tool-update projection. It also changes the request prefix and advertises a tool with no executor.
|
||||
|
||||
## Decision
|
||||
|
||||
The DeepSeek adapter sends deferred declarations unchanged and propagates provider rejection. This replaces the placeholder accommodation described by the [tool-update decision](../architecture/2026-09-20-dynamic-tool-updates.md), whose shared history projection remains authoritative.
|
||||
|
||||
Ordinary addition-only histories start with native tools or an empty set. Removals and first additions to an empty set rebuild native declarations; later deferred additions retain a native baseline. Explicit deferred-only registrations, direct adapter calls, and custom in-history routes can still produce all-deferred requests. Those configurations receive no adapter workaround.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Keep a synthetic immediate tool.** It conceals unexpected deferred-only requests and cannot preserve a prefix that lacked that tool. Automatically activating a real tool instead changes its declared availability.
|
||||
|
||||
**Reject locally.** Provider validation owns acceptance; another local restriction would duplicate it. Reconsider accommodation only for a supported workflow that requires deferred-only requests and cannot retain a native declaration.
|
||||
|
||||
## Consequences
|
||||
|
||||
Deferred-only compatibility is deliberately given up so request errors remain visible. Serializer and HTTP tests verify exact declarations and propagation of a provider rejection. The all-deferred keyless snapshot verifies request construction only; replay success does not establish provider acceptance. Replacement and empty-set scenarios verify the supported addition-only path.
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
# Agent Note: 显露 DeepSeek 全延迟请求失败
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-11-02-deepseek-deferred-tool-failures.md) | 中文
|
||||
|
||||
## Problem
|
||||
|
||||
合成的立即加载工具可使 DeepSeek 接受全延迟请求,却掩盖无效的工具更新投影。它还会改变请求前缀,并声明一个没有执行器的工具。
|
||||
|
||||
## Decision
|
||||
|
||||
DeepSeek 适配器原样发送延迟声明,并传递提供方的拒绝错误。这取代了[工具更新决策](../architecture/2026-09-20-dynamic-tool-updates.zh.md)中描述的占位工具兼容机制;其中的共享历史投影仍是权威依据。
|
||||
|
||||
普通 addition-only 历史以原生工具或空集开始。移除工具或首次向空集添加工具会重建原生声明;后续延迟添加保留原生基线。显式全延迟注册、直接调用适配器和自定义 in-history 路由仍可产生全延迟请求。适配器不为这些配置提供变通处理。
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**保留合成的立即加载工具。** 它会掩盖意外的全延迟请求,也无法保留原本没有该工具的前缀。改为自动激活真实工具则会改变其声明的可用性。
|
||||
|
||||
**在本地拒绝。** 是否接受请求由提供方验证决定;额外的本地限制会重复该验证。只有受支持工作流确实需要全延迟请求,且无法保留原生声明时,才重新考虑兼容处理。
|
||||
|
||||
## Consequences
|
||||
|
||||
有意放弃全延迟兼容性,使请求错误保持可见。序列化和 HTTP 测试验证精确声明以及提供方拒绝错误的传递。全延迟无密钥快照仅验证请求构造;回放成功不代表提供方接受。替换和空集场景验证受支持的 addition-only 路径。
|
||||
+4
-4
@@ -21,11 +21,11 @@
|
||||
en: f07bb6fbbdd01fe4
|
||||
zh: 9313a0ef2a22af12
|
||||
/agent-note-linear-credential-matching-and-performance-gates-for-user-text/decision/performance-gate:
|
||||
en: 37faaffb4662fd54
|
||||
zh: df0a4bf016393be9
|
||||
en: 3ceb80f455e9a47d
|
||||
zh: 89d8bd11b42d31b0
|
||||
/agent-note-linear-credential-matching-and-performance-gates-for-user-text/alternatives-considered:
|
||||
en: 559a8037caa9e3aa
|
||||
zh: 4f3719ed09445cc2
|
||||
en: b5bfb4cfa231ce0f
|
||||
zh: 631b9f2978241bc1
|
||||
/agent-note-linear-credential-matching-and-performance-gates-for-user-text/consequences:
|
||||
en: 8f9d25b72d3e7660
|
||||
zh: e5c6ae0194905876
|
||||
|
||||
@@ -43,7 +43,7 @@ English | [中文](2026-10-11-user-text-credential-matching.zh.md)
|
||||
| Item | Content |
|
||||
|---|---|
|
||||
| Workloads | Without credentials: ordinary messages, and long prose, code, base64, Markdown, log, JSON, and shell text. With credentials: messages each carrying one kind (model key, GitHub token, cloud key ID, JWT, address password, private-key block), and batches of `.env` keys. Adversarial: repeated input shaped against each format's prefix, length, and separator rules, at two sizes. All generated from constants; credential samples are assembled at run time |
|
||||
| Timing | Ten unmeasured pairs warm each workload with the same evaluation count as measured pairs. Scaling inputs share one group: every warm-up and measured round evaluates both sizes adjacently and alternates which size goes first, limiting drift between sizes. Five measured pairs alternate baseline-first and matcher-first order; each evaluation builds a fresh registry, document, and matcher instance and times evaluation only. A single-document sample averages twenty evaluations, or forty for the half-sized input of a scaling pair, so both sizes process the same total keys or characters per sample; a 5,000-message sample evaluates the batch once. Garbage collection during evaluation remains included. Each side selects its minimum sample mean, with the mean process CPU time from that sample. The worker JSON in CI logs includes every measured pair and averaging count; the HTML report shows the selected with-credential mean. Registry disposal completes outside the timer before the next evaluation, preventing cleanup and retained registries from accumulating. Warm-up retains no matcher memo |
|
||||
| Timing | Ten unmeasured pairs warm each workload with the same evaluation count as measured pairs. Scaling inputs share one group: every warm-up and measured round evaluates both sizes adjacently and alternates which size goes first, limiting drift between sizes. Five measured pairs alternate baseline-first and matcher-first order; each evaluation builds a fresh registry, document, and matcher instance and times evaluation only. A single-document sample averages twenty evaluations, or forty for the half-sized input of a scaling pair, so both sizes process the same total keys or characters per sample; a 5,000-message sample evaluates the batch once. Garbage collection during evaluation remains included. Absolute time budgets use each side's minimum sample mean, with the mean process CPU time from that sample. Doubling checks take the median of the five within-round ratios, keeping the adjacent samples paired through aggregation. The worker JSON in CI logs includes every measured pair and averaging count; the HTML report shows the selected with-credential mean. Registry disposal completes outside the timer before the next evaluation, preventing cleanup and retained registries from accumulating. Warm-up retains no matcher memo |
|
||||
| Allocation | A separate process with `--expose-gc` and a fixed semi-space larger than any evaluation; after warm-up and GC, the matcher alone answers every query once and the young-generation growth is taken; GC event timestamps confirm no GC ran inside the interval |
|
||||
| Assertions | Without credentials: absolute budgets and a cap on the with/without time ratio; with credentials: absolute budgets and the time ratio when the key count doubles; adversarial: absolute budgets and doubling ratios; allocation: a floor plus caps per credential, per line break inside masks, and per masked character |
|
||||
| Excluded | Cold code compilation, React rendering, browser paint and layout; allocation results are asserted and printed to the console, not added to the HTML report |
|
||||
@@ -54,7 +54,7 @@ The reference constants are calibrated for minima of five single evaluations. Mi
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
- **Finishing one scaling size before measuring the other**: separate phases compare different load and V8 tiering conditions. Identical code measured JWT doubling at 1.91× in one CI run and 2.70× in another; a local Node 24 run also exceeded the unchanged 2.5× limit on a different input. Adjacent, alternating size rounds retain the existing sample counts, minimum-of-five aggregation, GC inclusion, workloads, and limits. A controlled extra evaluation cost on only the 128 KiB input must still fail the doubling assertion.
|
||||
- **Finishing one scaling size before measuring the other**: separate phases compare different load and V8 tiering conditions. Identical code measured JWT doubling at 1.91× in one CI run and 2.70× in another; a local Node 24 run also exceeded the unchanged 2.5× limit on a different input. Adjacent, alternating size rounds retain the sample counts, GC inclusion, workloads, and limits. Independent minima can still select rounds at different speeds; the median of paired ratios prevents one faster half-sized sample from deciding the scaling result. A controlled extra evaluation cost on only the 128 KiB input must still fail the doubling assertion.
|
||||
- **Equal evaluation counts at both scaling sizes**: the larger input allocates twice as much per sample, so its fastest sample can include a collection while the smaller input's fastest sample includes none. Equal total input per sample makes the per-evaluation means compare similar allocation volumes without excluding GC or changing the doubling limit.
|
||||
- **Unwarmed, separate baseline and matcher batches**: sub-millisecond baseline measurements can reach optimized code before the matcher’s newly exercised paths do, and separate batches amplify clock-speed drift. Per-workload warm-up, adjacent pairs, and fixed batches compare warmed code while limiting noise in short intervals and preserving fresh documents and matcher state.
|
||||
- **One quantified pattern per format, run from every query**: patterns without a hit scan to the end of the text each time, so cost grows quadratically with credentials, and length quantifiers backtrack on failure.
|
||||
|
||||
@@ -43,7 +43,7 @@ Status: implemented
|
||||
| 项 | 内容 |
|
||||
|---|---|
|
||||
| 负载 | 无凭据:普通消息,以及散文、代码、base64、Markdown、日志、JSON、shell 长文本。含凭据:每条消息含一种凭据(模型密钥、GitHub 令牌、云密钥 ID、JWT、地址密码、私钥块),以及成批的 `.env` 密钥。对抗:针对每种格式的前缀、长度与分隔规则构造的重复输入,两种长度。全部由常量生成,凭据样本运行时拼接 |
|
||||
| 计时 | 每个负载先用十组未计入结果的配对预热,每组的求值次数与测量配对相同。翻倍比较的两个输入属于同一组:每轮预热和测量相邻求值两个尺寸,并交替先测的尺寸,减少尺寸之间的漂移。五组测量交替以基线或凭据匹配器开头;每次求值新建注册表、文档和匹配器实例,只计求值。单文档样本取二十次求值的平均值;翻倍比较中的小输入取四十次,使两种大小的每个样本处理相同总量的密钥或字符;5,000 条消息的样本则整批求值一次。求值期间的垃圾回收仍计入耗时。两侧各取样本均值的最小值,并保留对应样本的平均进程 CPU 时间。CI 日志中的 worker JSON 包含全部测量配对和求平均值的次数,HTML 报告展示选中的带凭据匹配器均值。每次求值在计时结束后等待注册表清理完成,再开始下一次,避免清理工作和未释放注册表积累。预热不保留匹配器记忆 |
|
||||
| 计时 | 每个负载先用十组未计入结果的配对预热,每组的求值次数与测量配对相同。翻倍比较的两个输入属于同一组:每轮预热和测量相邻求值两个尺寸,并交替先测的尺寸,减少尺寸之间的漂移。五组测量交替以基线或凭据匹配器开头;每次求值新建注册表、文档和匹配器实例,只计求值。单文档样本取二十次求值的平均值;翻倍比较中的小输入取四十次,使两种大小的每个样本处理相同总量的密钥或字符;5,000 条消息的样本则整批求值一次。求值期间的垃圾回收仍计入耗时。绝对耗时预算取两侧样本均值的最小值,并保留对应样本的平均进程 CPU 时间。翻倍检查取五个同轮次耗时比的中位数,使相邻样本在汇总时仍保持配对。CI 日志中的 worker JSON 包含全部测量配对和求平均值的次数,HTML 报告展示选中的带凭据匹配器均值。每次求值在计时结束后等待注册表清理完成,再开始下一次,避免清理工作和未释放注册表积累。预热不保留匹配器记忆 |
|
||||
| 分配 | 单独进程,`--expose-gc`,semi-space 固定为大于任何一次求值的大小;预热后 GC,再单独驱动匹配器回答全部查询一遍,取新生代增量;用 GC 事件的时间戳确认区间内没有 GC |
|
||||
| 断言 | 无凭据:绝对预算,以及"有/无凭据匹配器"的耗时比上限;含凭据:绝对预算,密钥数翻倍时的耗时比;对抗:绝对预算与翻倍耗时比;分配:底噪加每个凭据、每个遮挡内换行和每个遮挡字符的上限 |
|
||||
| 排除 | 冷代码编译、React 渲染、浏览器绘制与布局;分配结果只用于断言并输出到控制台,不进 HTML 报告 |
|
||||
@@ -54,7 +54,7 @@ Status: implemented
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
- **测完一个翻倍尺寸后再测另一个**:两个阶段比较的负载和 V8 编译层级状态不同。同一份代码的 JWT 翻倍比在两轮 CI 中分别为 1.91 倍和 2.70 倍;本地 Node 24 也在另一个输入上超过既有 2.5 倍上限。相邻、交替的尺寸轮次保留既有样本数、五组取最小值的汇总方式、GC 计入规则、负载与上限。仅给 128 KiB 输入增加受控求值开销时,翻倍断言仍须失败。
|
||||
- **测完一个翻倍尺寸后再测另一个**:两个阶段比较的负载和 V8 编译层级状态不同。同一份代码的 JWT 翻倍比在两轮 CI 中分别为 1.91 倍和 2.70 倍;本地 Node 24 也在另一个输入上超过既有 2.5 倍上限。相邻、交替的尺寸轮次保留样本数、GC 计入规则、负载与上限。独立选取最小值仍可能选中运行速度不同的轮次;配对比值的中位数避免单个更快的小输入样本决定翻倍结果。仅给 128 KiB 输入增加受控求值开销时,翻倍断言仍须失败。
|
||||
- **两种大小使用相同求值次数**:大输入每个样本的分配量翻倍,因而其最快样本可能包含一次垃圾回收,而小输入的最快样本没有。让每个样本的输入总量相等,使单次求值均值比较相近的分配量,无需排除垃圾回收或修改翻倍上限。
|
||||
- **不预热、分批测量基线和匹配器**:亚毫秒基线测量可能先进入优化后的代码,而匹配器新触及的路径还未完成优化;分批测量也会放大时钟频率漂移。逐负载预热、相邻配对和固定批次比较预热后的代码,降低短区间测量的噪声,同时保持文档和匹配器状态全新。
|
||||
- **每次查询对每个格式各跑一个带量词的正则**:没有命中的正则每次都扫到文末,凭据多时平方增长;长度量词在失败时回溯。
|
||||
|
||||
+1
-1
@@ -4,6 +4,6 @@ Run jobs on Windows runners (`windows-*` labels) under native `pwsh`. Native Win
|
||||
|
||||
The manual [Windows packaging workflow](workflows/windows-package.yml) uses the dedicated self-hosted Windows x64 label `dsh-win-package-trial` under native `pwsh`; preserve this routing separately from the automatic CI lanes.
|
||||
|
||||
The merge checks run from three workflows named `Policy`, which run on `pull_request_target` so their definitions come from master, and call the `deepseek-harness-check` action directly: `pr-check-on-head-branch.yml` on pull-request updates gives `Policy / Master Change` and `Policy / Pre-Merge Check`, `pr-check-after-approve.yml` on pull-request updates and submitted or dismissed reviews gives `Policy / Approve Check`, which also publishes the review verdict as the check run `Policy / Reviewer Check`, and `pr-check-on-base-branch.yml` on master pushes re-runs the head-branch checks as `Policy / Recheck`. Branch rules require those check names, so keep the job names and the workflow name. Each job reads its mode from a repository variable: `MERGE_POLICY_MASTER_DIFF`, `MERGE_POLICY_PRE_MERGE`, and `MERGE_POLICY_HEAD_DIFF`, each `off` (skip), `report` (run without failing the job), or `block` (fail the job); a missing value or an unknown one counts as `report`. `MERGE_POLICY_HEAD_DIFF` decides whether `Policy / Approve Check` runs and fails; while it runs, `Policy / Reviewer Check` is published whatever that mode says, with `success` when every file that needs review has one and `action_required` with the reviewers to ask otherwise. `Policy / Pre-Merge Check` merges the commit `Policy / Master Change` reports, so it runs only after that job and stays off while `MERGE_POLICY_MASTER_DIFF` is `off`; a merge it cannot do and a build that fails both follow its own mode, so `report` warns and skips the build while `block` fails the job. `Policy / Pre-Merge Check` runs `scripts/pre-merge-check.ts` under plain Node and knows nothing else about the merged tree: that script checks the released manifests against the root version, installs, and builds the Host and Client contracts, so change it in one pull request together with the build it orchestrates.
|
||||
The merge checks run from three workflows named `Policy`, which run on `pull_request_target` and cover pull requests into master or a `release/dsh-x.y.z` branch, so their definitions come from the base branch, and call the `deepseek-harness-check` action directly: `pr-check-on-head-branch.yml` on pull-request updates gives `Policy / Master Change` and `Policy / Pre-Merge Check`, `pr-check-after-approve.yml` on pull-request updates and submitted or dismissed reviews gives `Policy / Approve Check`, which also publishes the review verdict as the check run `Policy / Reviewer Check`, and `pr-check-on-base-branch.yml` on pushes to master or a release branch re-runs the head-branch checks as `Policy / Recheck`. Branch rules require those check names, so keep the job names and the workflow name. Each job reads its mode from a repository variable: `MERGE_POLICY_MASTER_DIFF`, `MERGE_POLICY_PRE_MERGE`, and `MERGE_POLICY_HEAD_DIFF`, each `off` (skip), `report` (run without failing the job), or `block` (fail the job); a missing value or an unknown one counts as `report`. `MERGE_POLICY_HEAD_DIFF` decides whether `Policy / Approve Check` runs and fails; while it runs, `Policy / Reviewer Check` is published whatever that mode says, with `success` when every file that needs review has one and `action_required` with the reviewers to ask otherwise. `Policy / Pre-Merge Check` merges the commit `Policy / Master Change` reports, so it runs only after that job and stays off while `MERGE_POLICY_MASTER_DIFF` is `off`; a merge it cannot do and a build that fails both follow its own mode, so `report` warns and skips the build while `block` fails the job. `Policy / Pre-Merge Check` runs `scripts/pre-merge-check.ts` under plain Node and knows nothing else about the merged tree: that script checks the released manifests against the root version, installs, and builds the Host and Client contracts, so change it in one pull request together with the build it orchestrates.
|
||||
|
||||
The manual Desktop publication workflow selects a successful package run and attempt, then routes each retained build through `DESKTOP_PUBLISH_RUNNERS` to its original machine's unique label. Keep Windows run steps on native PowerShell and Mac run steps on the default POSIX shell; shared entrypoints are Node scripts. Test and production jobs use separate `desktop-test` and `desktop-production` approval Environments after a credential-free preparation job.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
name: Policy
|
||||
on:
|
||||
pull_request_target:
|
||||
branches: [master]
|
||||
branches: [master, 'release/dsh-[0-9]+.[0-9]+.[0-9]+']
|
||||
types: [opened, synchronize, reopened, ready_for_review]
|
||||
pull_request_review:
|
||||
types: [submitted, dismissed]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
name: Policy
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
branches: [master, 'release/dsh-[0-9]+.[0-9]+.[0-9]+']
|
||||
workflow_dispatch:
|
||||
permissions: {}
|
||||
concurrency:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
name: Policy
|
||||
on:
|
||||
pull_request_target:
|
||||
branches: [master]
|
||||
branches: [master, 'release/dsh-[0-9]+.[0-9]+.[0-9]+']
|
||||
types: [opened, synchronize, reopened, ready_for_review, review_requested, auto_merge_enabled, labeled]
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
@@ -27,5 +27,5 @@
|
||||
en: f40edcf903a51f3f
|
||||
zh: 8a9a915451047c8b
|
||||
/dsh-cli-behavior-reference/source-execution:
|
||||
en: 05c2b23790f7b296
|
||||
zh: 51f809020fb230a1
|
||||
en: 0fd6c72ba9a7eb3b
|
||||
zh: 011c0ac4008ee480
|
||||
|
||||
@@ -143,4 +143,4 @@ Install external plugin bundles through `dsh plugin --profile <name> add <packag
|
||||
<a id="source-execution"></a>
|
||||
## Source execution
|
||||
|
||||
From the repository root, run `pnpm run build` separately after a fresh checkout and whenever artifacts need updating, then use `pnpm dsh <args...>`. The `package.json` script launches `apps/cli/src/bin.ts` with `node --import tsx/esm` without building and forwards every argument. Missing Typert host artifacts fail profile boot through module-resolution errors without a build instruction. Once those host artifacts exist, missing frontend or client-plugin bundles fail at startup with an instruction to run `pnpm run build`. The launcher does not check freshness, so existing stale bundles can run older browser code until rebuilt. The process inherits the launch environment, and `runProfile` resolves the outbound proxy from that snapshot before any entry mounts, so `HTTP_PROXY`/`HTTPS_PROXY` (and a proxy declared in a `.env` layer) apply without any further flag. The source entry explicitly selects linked profile resolution so tsx workspace imports and profile plugins share module identities. The installed form launches the built `apps/cli/lib/bin.js` with runtime profile resolution without rebuilding the repository.
|
||||
From the repository root, run `pnpm run build` separately after a fresh checkout and whenever artifacts need updating, then use `pnpm dsh <args...>`. The `package.json` script launches `apps/cli/src/bin.ts` with `node --import tsx/esm` without building and forwards every argument. Missing Typert host artifacts fail profile boot through module-resolution errors without a build instruction. Once those host artifacts exist, missing frontend or client-plugin bundles fail at startup with an instruction to run `pnpm run build`. The launcher does not check freshness, so existing stale bundles can run older browser code until rebuilt. The process inherits the launch environment, and `runProfile` resolves the outbound proxy from that snapshot before any entry mounts, so `HTTP_PROXY`/`HTTPS_PROXY` (and a proxy declared in a `.env` layer) apply without any further flag. The source entry explicitly selects linked profile resolution so tsx workspace imports and profile plugins share module identities. On Windows, the installed command expands 8.3 paths before loading profile dependencies. The installed form launches the built `apps/cli/lib/bin.js` with runtime profile resolution without rebuilding the repository.
|
||||
|
||||
@@ -145,4 +145,4 @@ Web、headless 与 SDK 继承 base bundle 的原生工具执行;使用 [PTC ov
|
||||
<a id="source-execution"></a>
|
||||
## 源码执行
|
||||
|
||||
请在仓库根目录中,于全新 checkout 之后及产物需要更新时单独运行 `pnpm run build`,然后使用 `pnpm dsh <args...>`。`package.json` 中的脚本不会构建,而是通过 `node --import tsx/esm` 启动 `apps/cli/src/bin.ts`,并转发所有参数。Typert Host 产物缺失时,profile 启动会因不含构建指引的模块解析错误而失败。这些 Host 产物存在后,如果前端或 Client plugin 组合包缺失,启动会失败并提示运行 `pnpm run build`。启动器不会检查产物是否为最新,因此已有的陈旧组合包可能继续运行旧版浏览器代码,直至重新构建。该进程会继承启动环境,且 `runProfile` 会在任何 entry 挂载之前从该快照解析出站代理,因此 `HTTP_PROXY`/`HTTPS_PROXY`(以及写在 `.env` 层中的代理)无需任何额外开关即可生效。源码入口显式选择链接式 profile 解析,使 tsx workspace 导入与 profile 插件共享模块身份。安装形式会使用 runtime profile 解析直接启动构建后的 `apps/cli/lib/bin.js`,不会重新构建仓库。
|
||||
请在仓库根目录中,于全新 checkout 之后及产物需要更新时单独运行 `pnpm run build`,然后使用 `pnpm dsh <args...>`。`package.json` 中的脚本不会构建,而是通过 `node --import tsx/esm` 启动 `apps/cli/src/bin.ts`,并转发所有参数。Typert Host 产物缺失时,profile 启动会因不含构建指引的模块解析错误而失败。这些 Host 产物存在后,如果前端或 Client plugin 组合包缺失,启动会失败并提示运行 `pnpm run build`。启动器不会检查产物是否为最新,因此已有的陈旧组合包可能继续运行旧版浏览器代码,直至重新构建。该进程会继承启动环境,且 `runProfile` 会在任何 entry 挂载之前从该快照解析出站代理,因此 `HTTP_PROXY`/`HTTPS_PROXY`(以及写在 `.env` 层中的代理)无需任何额外开关即可生效。源码入口显式选择链接式 profile 解析,使 tsx workspace 导入与 profile 插件共享模块身份。在 Windows 上,安装版命令在加载 profile 依赖前展开 8.3 短路径。安装形式会使用 runtime profile 解析直接启动构建后的 `apps/cli/lib/bin.js`,不会重新构建仓库。
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Installed CLI entry for disk launchers and embedded runtimes.
|
||||
* @module @deepseek-ai/dsh/installed-bin
|
||||
*/
|
||||
import { realpathSync } from 'node:fs'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
import type { RunCliOptions } from './bin.ts'
|
||||
|
||||
/**
|
||||
* Run the CLI in the importing carrier's module tree.
|
||||
* @param options - Installation-owned runtime options.
|
||||
* @returns Settlement of the selected command.
|
||||
*/
|
||||
export async function runCli(options?: RunCliOptions): Promise<void> {
|
||||
const cli = await import('./bin.ts')
|
||||
await cli.runCli(options)
|
||||
}
|
||||
|
||||
if (import.meta.main) {
|
||||
// Windows ESM can retain an 8.3 spelling while profile resolution expands it.
|
||||
const entry = process.platform === 'win32' ? pathToFileURL(realpathSync.native(import.meta.filename)).href : import.meta.url
|
||||
if (entry === import.meta.url) await runCli()
|
||||
else {
|
||||
const canonical = await import(entry) as typeof import('./installed-bin.ts')
|
||||
await canonical.runCli()
|
||||
}
|
||||
}
|
||||
@@ -344,6 +344,20 @@ function startStartupProfile(fixture: StartupFixture, args: readonly string[]) {
|
||||
describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)', () => {
|
||||
testProfileResolution('lib')
|
||||
|
||||
it.runIf(process.platform === 'win32')('resolves the installed entry before loading CLI dependencies', async () => {
|
||||
const home = mkdtempSync(join(tmpdir(), 'dsh-cli-alias-'))
|
||||
try {
|
||||
const alias = join(home, 'launcher')
|
||||
symlinkSync(join(repoRoot, 'apps/cli/lib'), alias, 'junction')
|
||||
const { stdout } = await execa(process.execPath, [
|
||||
'--preserve-symlinks-main', join(alias, 'bin.js'), '--version',
|
||||
], { timeout: SPAWN_TIMEOUT_MS, env: { DSH_HOME: home, DSH_TELEMETRY_DISABLED: '1' } })
|
||||
expect(stdout).toBe(cliVersion)
|
||||
} finally {
|
||||
rmSync(home, { recursive: true, force: true })
|
||||
}
|
||||
}, SPAWN_TIMEOUT_MS + 5_000)
|
||||
|
||||
it('requires a profile and rejects removed flags', async () => {
|
||||
const bare = await runBuiltBin()
|
||||
expect(bare.code).toBe(1)
|
||||
|
||||
@@ -40,3 +40,8 @@
|
||||
- id: session-log-deepseek
|
||||
config:
|
||||
enabled: false
|
||||
|
||||
# The recorded tool catalog includes the optional patch tool.
|
||||
- id: tool-fs
|
||||
config:
|
||||
applyPatch: true
|
||||
|
||||
@@ -57,6 +57,7 @@ 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: 'allows an explicit patch to enable apply_patch', envValue: undefined, editorEnabled: false, patchEnabled: true },
|
||||
{ label: 'uses the PTC executor overlay despite the retired environment selector', envValue: undefined, editorEnabled: false, ptc: true },
|
||||
// The shipped sdk profile mounts no observation policy: an edit of a file the model never read lands on disk.
|
||||
{ label: 'edits a file the model never read through the shipped profile', envValue: undefined, editorEnabled: false, unreadEdit: true },
|
||||
@@ -64,11 +65,14 @@ describe('Python SDK dsh profile keyless smoke', () => {
|
||||
const { envValue, editorEnabled } = scenario
|
||||
const ptc = 'ptc' in scenario && scenario.ptc
|
||||
const unreadEdit = 'unreadEdit' in scenario && scenario.unreadEdit
|
||||
const patchEnabled = 'patchEnabled' in scenario && scenario.patchEnabled
|
||||
|
||||
const root = await mkdtemp(join(tmpdir(), 'dsh-python-sdk-runtime-smoke-'))
|
||||
const unreadFile = join(root, 'notes.txt')
|
||||
if (unreadEdit) await writeFile(unreadFile, 'alpha\n')
|
||||
const editorPatch = join(root, 'editor.patch.yml')
|
||||
const fsPatch = join(root, 'fs.patch.yml')
|
||||
if (patchEnabled) await writeFile(fsPatch, '- {id: tool-fs, config: {applyPatch: true}}\n')
|
||||
if (editorEnabled) await writeFile(editorPatch, [
|
||||
'- insert:',
|
||||
' - id: tool-str-replace-editor',
|
||||
@@ -103,6 +107,7 @@ describe('Python SDK dsh profile keyless smoke', () => {
|
||||
'--profile',
|
||||
'sdk',
|
||||
...(editorEnabled ? ['--patch', editorPatch] : []),
|
||||
...(patchEnabled ? ['--patch', fsPatch] : []),
|
||||
...(ptc ? ['--patch', join(repoRoot, 'apps/cli/config/examples/ptc.yml')] : []),
|
||||
], {
|
||||
cwd: repoRoot,
|
||||
@@ -189,16 +194,16 @@ 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)
|
||||
const expectedTools = ['read', 'write', 'edit', 'apply_patch', 'web_fetch', 'web_search', 'subagent', 'subagent_fork',
|
||||
const expectedTools = ['read', 'write', 'edit', 'web_fetch', 'web_search', 'subagent', 'subagent_fork',
|
||||
'present', 'ask_user_question', 'schedule_create', 'schedule_edit', 'schedule_list', 'schedule_view', 'plugin_manager']
|
||||
const requestText = JSON.stringify(modelRequests[0])
|
||||
if (ptc) {
|
||||
expect(toolNames).toEqual(['run_code'])
|
||||
// The PTC overlay keeps the pre-apply_patch tool set, as the Web ptc preset does.
|
||||
for (const tool of expectedTools.filter(tool => tool !== 'apply_patch')) expect(requestText.includes(`${tool}:`), tool).toBe(true)
|
||||
for (const tool of expectedTools) expect(requestText.includes(`${tool}:`), tool).toBe(true)
|
||||
expect(requestText.includes('apply_patch:')).toBe(false)
|
||||
} else {
|
||||
expect(toolNames).toEqual(expect.arrayContaining(expectedTools))
|
||||
expect(toolNames.includes('apply_patch')).toBe(patchEnabled)
|
||||
expect(toolNames.includes('str_replace_editor')).toBe(editorEnabled)
|
||||
expect(toolNames).not.toContain('list_subagent_models')
|
||||
}
|
||||
|
||||
@@ -300,7 +300,7 @@ describe('the shipped Web composition', () => {
|
||||
// excluded for the reason the TUI composition e2e excludes them — they
|
||||
// depend on ripgrep being present on the machine.
|
||||
expect(toolNames(ctx, handle.agent).filter(name => name !== 'glob' && name !== 'grep')).toEqual([
|
||||
'apply_patch', 'ask_user_question', 'bash', 'create_goal', 'edit', 'exit_plan_mode',
|
||||
'ask_user_question', 'bash', 'create_goal', 'edit', 'exit_plan_mode',
|
||||
'get_goal', 'interrupt_agent', 'job_kill', 'job_list', 'job_output', 'list_agents', 'list_subagent_models', 'plugin_manager', 'present', 'read', 'read_image',
|
||||
'schedule_create', 'schedule_edit', 'schedule_list', 'schedule_view',
|
||||
'send_message', 'skill',
|
||||
|
||||
@@ -5,7 +5,7 @@ import { defineConfig } from 'tsdown'
|
||||
* Declarations come from `tsc -b` (dts: false), matching every package.
|
||||
*/
|
||||
export default defineConfig({
|
||||
entry: ['lib/types/bin.js', 'lib/types/profile-boot.js'],
|
||||
entry: { bin: 'lib/types/installed-bin.js', 'profile-boot': 'lib/types/profile-boot.js' },
|
||||
outDir: 'lib',
|
||||
format: ['esm'],
|
||||
platform: 'node',
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
# After editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write apps/web/tests/README.md
|
||||
/apps-web-browser-e2e:
|
||||
en: 2deef0df72a6465c
|
||||
zh: b24989ffb2d8586a
|
||||
en: 88e00180b920a42e
|
||||
zh: 8ddba0f4bbb4f1f6
|
||||
/apps-web-browser-e2e/completion-observations:
|
||||
en: 8c139966983959cc
|
||||
zh: 54f09f9b7ac84cda
|
||||
|
||||
@@ -16,6 +16,8 @@ Ordinary scenarios begin with no registered Workspace or Session and a durable m
|
||||
|
||||
Same-day message goldens use the shared `WEB_FIXTURE_TIME`: seeded transcripts pass it as `createdAt` and pin the browser calendar with `pinBrowserClock`; scenarios that submit live messages also use `pinHostClock` and restore it after teardown. These clocks advance with real elapsed time while keeping native timers intact, so midnight does not change a scenario's date labels.
|
||||
|
||||
Every scenario requires the whole client composition to activate. The product boot only warns about inactive entries outside its required list, but the `watchConsole` tripwire, the [real-host smoke](smoke-real.e2e.ts), and the [assembled jsdom boot](assembled-boot.ts) treat a `web boot: warning` as a failure. No scenario option relaxes this: an inactive plugin in a product composition is a defect to fix.
|
||||
|
||||
## Completion observations
|
||||
|
||||
State-sensitive cases use Workspace, admission, attachment, and model-stream barriers to separate visible intermediate states from completed operations. Model-picker persistence assertions wait for the saved default, independently of menu closure. Details close waits for frame transitions; archive verification assigns an explicit title to the seeded Session and follows that identity across reload. The [CI test reliability workflow](../../../.agents/skills/dsh-ci-test-reliability/SKILL.md#synchronize-on-state) owns these synchronization rules.
|
||||
|
||||
@@ -16,6 +16,8 @@ pnpm --filter @deepseek-ai/dsh-web-frontend exec playwright install --with-deps
|
||||
|
||||
当天消息的 golden 使用共享的 `WEB_FIXTURE_TIME`:预置会话将它传为 `createdAt`,并通过 `pinBrowserClock` 固定浏览器日期;提交实时消息的场景还使用 `pinHostClock`,在清理后恢复。固定后的时钟随真实耗时前进,保留原生定时器,因此跨午夜不会改变场景的日期标签。
|
||||
|
||||
每个场景都要求整个客户端组合全部激活。产品启动只对必需名单外的未激活 entry 告警,但 `watchConsole` 绊线、[真实 Host 冒烟](smoke-real.e2e.ts)与[组装 jsdom 启动](assembled-boot.ts)把 `web boot: warning` 视为失败。没有任何场景选项放宽这一点:产品组合中出现未激活插件就是需要修复的缺陷。
|
||||
|
||||
## 完成状态观察
|
||||
|
||||
依赖状态的用例使用 Workspace、接纳、附件和模型流屏障,区分可见中间状态与已完成操作。模型选择器的持久化断言等待默认设置保存完成,不以菜单关闭作为完成信号。详情关闭等待框架过渡结束;归档验证为 seed Session 设置显式标题,并跨重载跟踪该身份。[CI 测试可靠性工作流](../../../.agents/skills/dsh-ci-test-reliability/SKILL.md#synchronize-on-state)负责这些同步规则。
|
||||
|
||||
@@ -59,3 +59,37 @@ it.each(languages)('selects %s from the browser, localizes metadata and persists
|
||||
expect(tripwire.warnings).toEqual([])
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
})
|
||||
|
||||
const forgePreviews = [
|
||||
['de', 'de-DE', 'Plugins', 'Eine Unterhaltung mit verknüpftem GitHub-Pull-Request, Prüfungen, Reviews, Codeunterschieden und einem separaten Arbeitsbaum'],
|
||||
['es', 'es-MX', 'Complementos', 'Una conversación vinculada a un pull request de GitHub, con comprobaciones, revisiones, diferencias de código y un árbol de trabajo aislado'],
|
||||
['fr', 'fr-CA', 'Plugins', 'Une conversation liée à une pull request GitHub, avec vérifications, revues, différences de code et un arbre de travail isolé'],
|
||||
['ja', 'ja-JP', 'プラグイン', 'GitHub のプルリクエストに関連付けられた会話、チェックとレビューの状況、コードの差分、独立したワークツリー'],
|
||||
['pt', 'pt-BR', 'Plug-ins', 'Uma conversa vinculada a um pull request do GitHub, com verificações, revisões, diferenças de código e uma árvore de trabalho isolada'],
|
||||
['ru', 'ru-RU', 'Плагины', 'Беседа, связанная с pull request GitHub: проверки, рецензии, различия в коде и отдельное рабочее дерево'],
|
||||
] as const
|
||||
|
||||
it.each(forgePreviews)('reads the Forge preview in %s before activation', async (id, tag, plugins, alt) => {
|
||||
const resources: { scaffold?: WebScaffold; browser?: Browser } = {}
|
||||
onTestFinished(async () => {
|
||||
try { await resources.browser?.close() } finally { await resources.scaffold?.close() }
|
||||
})
|
||||
const scaffold = await launchWebScaffold()
|
||||
resources.scaffold = scaffold
|
||||
const browser = await chromium.launch()
|
||||
resources.browser = browser
|
||||
const page = await browser.newPage({ locale: tag, viewport: { width: 1440, height: 960 } })
|
||||
const tripwire = watchConsole(page)
|
||||
onTestFailed(() => saveFailureShot(page, `forge-preview-${id}`))
|
||||
await page.goto(scaffold.authenticatedUrl)
|
||||
await expect.poll(() => page.locator('html').getAttribute('lang')).toBe(id)
|
||||
await page.getByRole('button', { name: plugins, exact: true }).click()
|
||||
const card = page.locator('[data-plugin-package="@deepseek-ai/dsh-experimental-forge-bundle"]')
|
||||
expect(await card.getByRole('switch').getAttribute('aria-checked')).toBe('false')
|
||||
await card.getByRole('button').click()
|
||||
const image = page.locator('[data-detail-document]').getByRole('img', { name: alt, exact: true })
|
||||
await image.waitFor()
|
||||
await image.evaluate(async (node: HTMLImageElement) => { await node.decode() })
|
||||
expect(await image.evaluate((node: HTMLImageElement) => [node.naturalWidth, node.naturalHeight])).toEqual([640, 300])
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
})
|
||||
|
||||
@@ -196,13 +196,16 @@ class EventSourceStub {
|
||||
const win = window as FixtureWindow
|
||||
let unmount: (() => Promise<void>) | undefined
|
||||
let mountedRemote: RemoteMock | undefined
|
||||
/** Boot audit warnings of the current test; the product boot tolerates inactive optional entries, this lane does not. */
|
||||
let bootWarnings: string[] = []
|
||||
|
||||
/**
|
||||
* Register the per-test jsdom setup and teardown the assembled boot needs:
|
||||
* English pinned before boot so role/text locators stay deterministic across
|
||||
* localized component migrations (the newEnglishPage e2e convention), the
|
||||
* observers, font events, and frame callbacks jsdom lacks, and a full reset of the document,
|
||||
* the boot globals, and the injected plugin styles afterwards.
|
||||
* the boot globals, and the injected plugin styles afterwards. A boot audit warning about an
|
||||
* inactive entry fails the test at teardown: every assembled composition must activate completely.
|
||||
*/
|
||||
export function installAssembledBootEnv(): void {
|
||||
// jsdom implements no scroll geometry: the trigger menu reveals its
|
||||
@@ -219,7 +222,15 @@ export function installAssembledBootEnv(): void {
|
||||
})
|
||||
}
|
||||
let fontsDescriptor: PropertyDescriptor | undefined
|
||||
let restoreWarn: (() => void) | undefined
|
||||
beforeEach(() => {
|
||||
bootWarnings = []
|
||||
const warn = console.warn.bind(console)
|
||||
const spy = vi.spyOn(console, 'warn').mockImplementation((message?: unknown, ...rest: unknown[]) => {
|
||||
if (typeof message === 'string' && message.startsWith('web boot: warning')) bootWarnings.push(message)
|
||||
warn(message, ...rest)
|
||||
})
|
||||
restoreWarn = () => { spy.mockRestore() }
|
||||
fontsDescriptor = Object.getOwnPropertyDescriptor(document, 'fonts')
|
||||
Object.defineProperty(document, 'fonts', { configurable: true, value: new EventTarget() })
|
||||
localStorage.clear()
|
||||
@@ -267,6 +278,9 @@ export function installAssembledBootEnv(): void {
|
||||
vi.unstubAllGlobals()
|
||||
if (fontsDescriptor === undefined) Reflect.deleteProperty(document, 'fonts')
|
||||
else Object.defineProperty(document, 'fonts', fontsDescriptor)
|
||||
restoreWarn?.()
|
||||
restoreWarn = undefined
|
||||
if (bootWarnings.length > 0) failures.push(new Error(`assembled boot: every entry must activate\n${bootWarnings.join('\n')}`))
|
||||
if (failures.length > 0) throw new AggregateError(failures, 'assembled boot teardown failed')
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,235 +0,0 @@
|
||||
/** Saved credentials and endpoints reach the assembled editor and provider requests. */
|
||||
import { once } from 'node:events'
|
||||
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
|
||||
import { createServer } from 'node:http'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { chromium } from 'playwright'
|
||||
import { expect, it, onTestFinished } from 'vitest'
|
||||
import { BlockAssembler } from '@deepseek-ai/dsh-llm'
|
||||
import { credentialRef } from '@deepseek-ai/dsh-credentials'
|
||||
import { createLaunchEnvironmentSnapshot } from '@deepseek-ai/dsh-launch-environment'
|
||||
import { captureStableAria, compareOrRefreshGolden, launchWebScaffold, webSnapshotMode } from './scaffold.ts'
|
||||
import { openSettings, ZH_BROWSER_LOCALE } from './support.ts'
|
||||
|
||||
it.each([false, true])('edits saved DeepSeek keys and restores environment detection (environment present: %s)', async (hasEnvironment) => {
|
||||
const scaffold = await launchWebScaffold({
|
||||
deepSeekMissingCredential: true,
|
||||
extraOverlayPath: fileURLToPath(new URL('./fixtures/credential-editor.patch.yml', import.meta.url)),
|
||||
openInAppEnvironment: createLaunchEnvironmentSnapshot([{
|
||||
source: 'process', values: hasEnvironment ? { DEEPSEEK_API_KEY: 'fixture-environment-key' } : {},
|
||||
}]),
|
||||
})
|
||||
try {
|
||||
const browser = await chromium.launch()
|
||||
try {
|
||||
const page = await browser.newPage({ viewport: { width: 1440, height: 960 }, locale: ZH_BROWSER_LOCALE })
|
||||
const ref = credentialRef('DEEPSEEK_API_KEY')
|
||||
await scaffold.ctx.credentials.set(ref, 'fixture-saved-deepseek-key')
|
||||
await page.goto(scaffold.authenticatedUrl, { waitUntil: 'load' })
|
||||
await openSettings(page, 'zh')
|
||||
const dialog = page.getByRole('dialog', { name: '设置' })
|
||||
await dialog.getByRole('button', { name: '模型', exact: true }).click()
|
||||
const edit = dialog.getByRole('button', { name: '编辑 DeepSeek (deepseek-official)', exact: true })
|
||||
await edit.click()
|
||||
const key = dialog.getByLabel('API 密钥', { exact: true })
|
||||
await expect.poll(() => key.inputValue()).toBe('fixture-saved-deepseek-key')
|
||||
expect(await key.getAttribute('type')).toBe('password')
|
||||
await compareOrRefreshGolden(
|
||||
fileURLToPath(new URL('./expected/credential-editor/saved-key.expected.md', import.meta.url)),
|
||||
await captureStableAria(page, '[role="dialog"]', scaffold.workspaceCwd), webSnapshotMode(),
|
||||
)
|
||||
await dialog.getByRole('button', { name: '显示密钥', exact: true }).click()
|
||||
expect(await key.getAttribute('type')).toBe('text')
|
||||
await key.fill('fixture-cancelled-key')
|
||||
await dialog.getByRole('button', { name: '取消', exact: true }).click()
|
||||
expect(await scaffold.ctx.credentials.readStored(ref)).toBe('fixture-saved-deepseek-key')
|
||||
await edit.click()
|
||||
await expect.poll(() => key.inputValue()).toBe('fixture-saved-deepseek-key')
|
||||
expect(await key.getAttribute('type')).toBe('password')
|
||||
await key.fill('fixture-replacement-key')
|
||||
await dialog.getByRole('button', { name: '保存', exact: true }).click()
|
||||
await dialog.getByText('已保存 DeepSeek (deepseek-official)。', { exact: true }).waitFor()
|
||||
expect(await scaffold.ctx.credentials.readStored(ref)).toBe('fixture-replacement-key')
|
||||
await page.reload({ waitUntil: 'load' })
|
||||
await openSettings(page, 'zh')
|
||||
await dialog.getByRole('button', { name: '模型', exact: true }).click()
|
||||
await edit.click()
|
||||
await expect.poll(() => key.inputValue()).toBe('fixture-replacement-key')
|
||||
await key.fill('')
|
||||
expect(await key.getAttribute('placeholder')).toBe('留空使用环境认证(DEEPSEEK_API_KEY)')
|
||||
await dialog.getByText('环境凭据状态尚未确认', { exact: true }).waitFor()
|
||||
expect(await scaffold.ctx.credentials.readStored(ref)).toBe('fixture-replacement-key')
|
||||
await dialog.getByRole('button', { name: '保存', exact: true }).click()
|
||||
await dialog.getByText('已保存 DeepSeek (deepseek-official)。', { exact: true }).waitFor()
|
||||
expect(await scaffold.ctx.credentials.readStored(ref)).toBeUndefined()
|
||||
// A missing credential opens the first-run settings card automatically;
|
||||
// an environment credential keeps an ordinary row whose editor is closed.
|
||||
if (hasEnvironment) await edit.click()
|
||||
await expect.poll(() => key.inputValue()).toBe('')
|
||||
expect(await key.getAttribute('placeholder')).toBe('留空使用环境认证(DEEPSEEK_API_KEY)')
|
||||
const hint = hasEnvironment
|
||||
? '已检测到环境变量(DEEPSEEK_API_KEY)'
|
||||
: '未检测到环境变量。请填写 API 密钥,或设置环境变量 DEEPSEEK_API_KEY。'
|
||||
const detection = dialog.getByText(hint, { exact: true })
|
||||
await detection.waitFor()
|
||||
const colors = await detection.evaluate((element, present) => {
|
||||
const label = element as HTMLElement
|
||||
const actual = getComputedStyle(label).color
|
||||
const previous = label.style.color
|
||||
label.style.color = present ? 'var(--dsw-alias-state-success-primary)' : 'var(--dsw-alias-state-warn-label)'
|
||||
const expected = getComputedStyle(label).color
|
||||
label.style.color = previous
|
||||
return { actual, expected }
|
||||
}, hasEnvironment)
|
||||
expect(colors.actual).toBe(colors.expected)
|
||||
expect(await dialog.getByRole('button', { name: '显示密钥', exact: true }).count()).toBe(0)
|
||||
expect((await scaffold.ctx.credentials.resolve(ref))?.source).toBe(hasEnvironment ? 'env' : undefined)
|
||||
await compareOrRefreshGolden(
|
||||
fileURLToPath(new URL(`./expected/credential-editor/${hasEnvironment ? 'detected' : 'missing'}-environment.expected.md`, import.meta.url)),
|
||||
await captureStableAria(page, '[role="dialog"]', scaffold.workspaceCwd), webSnapshotMode(),
|
||||
)
|
||||
} finally { await browser.close() }
|
||||
} finally { await scaffold.close() }
|
||||
})
|
||||
|
||||
/** Local provider endpoint recording the destination path and authentication actually received. */
|
||||
async function connectionProbe(messages: boolean) {
|
||||
const calls: { path: string; key: string | undefined }[] = []
|
||||
const server = createServer((request, response) => {
|
||||
request.on('end', () => {
|
||||
const key = messages ? request.headers['x-api-key'] : request.headers.authorization
|
||||
calls.push({ path: request.url ?? '', key: typeof key === 'string' ? key : undefined })
|
||||
const events = messages ? [
|
||||
{ type: 'message_start', message: { id: 'probe', type: 'message', role: 'assistant', model: 'credential-editor-model', content: [], usage: { input_tokens: 1, output_tokens: 0 } } },
|
||||
{ type: 'content_block_start', index: 0, content_block: { type: 'text', text: '' } },
|
||||
{ type: 'content_block_delta', index: 0, delta: { type: 'text_delta', text: 'connection-ok' } },
|
||||
{ type: 'content_block_stop', index: 0 },
|
||||
{ type: 'message_delta', delta: { stop_reason: 'end_turn' }, usage: { output_tokens: 1 } },
|
||||
{ type: 'message_stop' },
|
||||
] : [
|
||||
{ choices: [{ delta: { role: 'assistant', content: 'connection-ok' }, index: 0, finish_reason: null }] },
|
||||
{ choices: [{ delta: {}, index: 0, finish_reason: 'stop' }], usage: { prompt_tokens: 1, completion_tokens: 1 } },
|
||||
]
|
||||
response.writeHead(200, { 'content-type': 'text/event-stream' })
|
||||
response.end(events.map(event => `${'type' in event ? `event: ${event.type}\n` : ''}data: ${JSON.stringify(event)}\n\n`).join('')
|
||||
+ (messages ? '' : 'data: [DONE]\n\n'))
|
||||
})
|
||||
request.resume()
|
||||
})
|
||||
onTestFinished(async () => {
|
||||
await new Promise<void>((resolve) => { server.close(() => { resolve() }); server.closeAllConnections() })
|
||||
})
|
||||
server.listen(0, '127.0.0.1')
|
||||
await once(server, 'listening')
|
||||
const address = server.address()
|
||||
if (address === null || typeof address === 'string') throw new Error('connection probe did not bind TCP')
|
||||
return { url: `http://127.0.0.1:${String(address.port)}`, calls }
|
||||
}
|
||||
|
||||
it.each([
|
||||
{ provider: 'deepseek-official', ref: 'DEEPSEEK_API_KEY', messages: true },
|
||||
{ provider: 'openai', ref: 'OPENAI_API_KEY', messages: false },
|
||||
{ provider: 'anthropic', ref: 'ANTHROPIC_API_KEY', messages: true },
|
||||
{ provider: 'credential-fixture', ref: 'CREDENTIAL_FIXTURE_API_KEY', messages: false },
|
||||
])('uses the saved endpoint and key on the wire for $provider, then restores fallbacks', async ({ provider, ref, messages }) => {
|
||||
const ambient = await connectionProbe(messages)
|
||||
const saved = await connectionProbe(messages)
|
||||
const root = await mkdtemp(join(tmpdir(), 'dsh-credential-routing-'))
|
||||
onTestFinished(() => rm(root, { recursive: true, force: true }))
|
||||
const overlay = join(root, 'connection.patch.yml')
|
||||
const models = [{ id: 'credential-editor-model' }]
|
||||
const baseSuffix = provider === 'anthropic' ? '' : '/v1'
|
||||
const operation = provider === 'anthropic' ? '/v1/messages?beta=true' : messages ? '/messages' : '/chat/completions'
|
||||
const ambientURL = `${ambient.url}/ambient${baseSuffix}`
|
||||
await writeFile(overlay, JSON.stringify([
|
||||
{ id: 'llm-deepseek', config: { models } },
|
||||
...provider === 'deepseek-official' ? [] : [{
|
||||
id: 'llm-pi-ai', config: { providers: { [provider]: {
|
||||
api: messages ? 'anthropic-messages' : 'openai-completions',
|
||||
baseURL: ambientURL, models,
|
||||
...provider === 'credential-fixture' ? {} : { apiKeyEnv: ref },
|
||||
} } },
|
||||
}],
|
||||
]))
|
||||
const scaffold = await launchWebScaffold({
|
||||
deepSeekMissingCredential: true,
|
||||
extraOverlayPath: overlay,
|
||||
openInAppEnvironment: createLaunchEnvironmentSnapshot([{
|
||||
source: 'process', values: {
|
||||
[ref]: 'fixture-environment-key',
|
||||
DEEPSEEK_BASE_URL: ambientURL,
|
||||
OPENAI_BASE_URL: ambientURL,
|
||||
ANTHROPIC_BASE_URL: ambientURL,
|
||||
},
|
||||
}]),
|
||||
})
|
||||
try {
|
||||
const browser = await chromium.launch()
|
||||
try {
|
||||
const page = await browser.newPage({ locale: ZH_BROWSER_LOCALE })
|
||||
await page.goto(scaffold.authenticatedUrl, { waitUntil: 'load' })
|
||||
await openSettings(page, 'zh')
|
||||
const dialog = page.getByRole('dialog', { name: '设置' })
|
||||
await dialog.getByRole('button', { name: '模型', exact: true }).click()
|
||||
const edit = dialog.getByRole('button', {
|
||||
name: `编辑 ${provider === 'deepseek-official' ? 'DeepSeek (deepseek-official)' : provider}`, exact: true,
|
||||
})
|
||||
const key = dialog.getByLabel('API 密钥', { exact: true })
|
||||
const endpoint = dialog.getByLabel('API 地址', { exact: true })
|
||||
const savedURL = `${saved.url}/configured${baseSuffix}`
|
||||
const generate = async (): Promise<void> => {
|
||||
const assembler = new BlockAssembler()
|
||||
for await (const chunk of scaffold.ctx.llm.stream({ provider, model: 'credential-editor-model', messages: [] })) assembler.push(chunk)
|
||||
expect(assembler.message({ provider, model: 'credential-editor-model' }).content).toContainEqual({ type: 'text', text: 'connection-ok' })
|
||||
}
|
||||
await edit.click()
|
||||
await key.fill('fixture-saved-key')
|
||||
await dialog.getByText('自定义设置', { exact: true }).click()
|
||||
await endpoint.fill(savedURL)
|
||||
await dialog.getByRole('button', { name: '保存', exact: true }).click()
|
||||
await expect.poll(() => key.count()).toBe(0)
|
||||
await generate()
|
||||
expect(saved.calls).toEqual([{ path: `/configured${baseSuffix}${operation}`, key: messages ? 'fixture-saved-key' : 'Bearer fixture-saved-key' }])
|
||||
expect(ambient.calls).toEqual([])
|
||||
await page.reload({ waitUntil: 'load' })
|
||||
await openSettings(page, 'zh')
|
||||
await dialog.getByRole('button', { name: '模型', exact: true }).click()
|
||||
await edit.click()
|
||||
expect(await key.inputValue()).toBe('fixture-saved-key')
|
||||
expect(await key.getAttribute('type')).toBe('password')
|
||||
await dialog.getByText('自定义设置', { exact: true }).click()
|
||||
expect(await endpoint.inputValue()).toBe(savedURL)
|
||||
await generate()
|
||||
expect(saved.calls).toHaveLength(2)
|
||||
expect(saved.calls[1]).toEqual(saved.calls[0])
|
||||
expect(ambient.calls).toEqual([])
|
||||
await key.fill('')
|
||||
await dialog.getByRole('button', { name: '保存', exact: true }).click()
|
||||
await expect.poll(() => key.count()).toBe(0)
|
||||
await generate()
|
||||
expect(saved.calls).toHaveLength(3)
|
||||
expect(saved.calls[2]).toEqual({ path: `/configured${baseSuffix}${operation}`, key: messages ? 'fixture-environment-key' : 'Bearer fixture-environment-key' })
|
||||
expect(ambient.calls).toEqual([])
|
||||
await edit.click()
|
||||
await key.fill('fixture-saved-key')
|
||||
await dialog.getByText('自定义设置', { exact: true }).click()
|
||||
await endpoint.fill('')
|
||||
await dialog.getByRole('button', { name: '保存', exact: true }).click()
|
||||
await expect.poll(() => key.count()).toBe(0)
|
||||
await generate()
|
||||
expect(saved.calls).toHaveLength(3)
|
||||
expect(ambient.calls).toEqual([{ path: `/ambient${baseSuffix}${operation}`, key: messages ? 'fixture-saved-key' : 'Bearer fixture-saved-key' }])
|
||||
await edit.click()
|
||||
await key.fill('')
|
||||
await dialog.getByRole('button', { name: '保存', exact: true }).click()
|
||||
await expect.poll(() => key.count()).toBe(0)
|
||||
await generate()
|
||||
expect(saved.calls).toHaveLength(3)
|
||||
expect(ambient.calls[1]).toEqual({ path: `/ambient${baseSuffix}${operation}`, key: messages ? 'fixture-environment-key' : 'Bearer fixture-environment-key' })
|
||||
expect(ambient.calls).toHaveLength(2)
|
||||
expect(await scaffold.ctx.credentials.readStored(credentialRef(ref))).toBeUndefined()
|
||||
} finally { await browser.close() }
|
||||
} finally { await scaffold.close() }
|
||||
})
|
||||
@@ -1,25 +0,0 @@
|
||||
- dialog "设置":
|
||||
- text: 设置
|
||||
- navigation:
|
||||
- button "通用设置"
|
||||
- button "模型"
|
||||
- button "内置插件"
|
||||
- button "Agent 预设"
|
||||
- button "打开配置文件"
|
||||
- button "关闭"
|
||||
- heading "模型" [level=2]
|
||||
- paragraph: 填入各提供商的 API 密钥即可使用其模型。
|
||||
- list:
|
||||
- listitem:
|
||||
- text: DeepSeek
|
||||
- img "API 密钥已配置"
|
||||
- button "编辑 DeepSeek (deepseek-official)": 编辑
|
||||
- text: DeepSeek deepseek-official API 密钥
|
||||
- button "认证说明": "?"
|
||||
- textbox "API 密钥":
|
||||
- /placeholder: 留空使用环境认证(DEEPSEEK_API_KEY)
|
||||
- paragraph: 已检测到环境变量(DEEPSEEK_API_KEY)
|
||||
- group: 自定义设置
|
||||
- button "取消"
|
||||
- button "保存"
|
||||
- button "添加模型提供商"
|
||||
@@ -1,23 +0,0 @@
|
||||
- dialog "设置":
|
||||
- text: 设置
|
||||
- navigation:
|
||||
- button "通用设置"
|
||||
- button "模型"
|
||||
- button "内置插件"
|
||||
- button "Agent 预设"
|
||||
- button "打开配置文件"
|
||||
- button "关闭"
|
||||
- heading "模型" [level=2]
|
||||
- paragraph: 填入各提供商的 API 密钥即可使用其模型。
|
||||
- status: 已保存 DeepSeek (deepseek-official)。
|
||||
- list:
|
||||
- listitem:
|
||||
- text: DeepSeek deepseek-official API 密钥
|
||||
- button "认证说明": "?"
|
||||
- textbox "API 密钥":
|
||||
- /placeholder: 留空使用环境认证(DEEPSEEK_API_KEY)
|
||||
- paragraph: 未检测到环境变量。请填写 API 密钥,或设置环境变量 DEEPSEEK_API_KEY。
|
||||
- group: 自定义设置
|
||||
- button "取消"
|
||||
- button "保存"
|
||||
- button "添加模型提供商"
|
||||
@@ -1,26 +0,0 @@
|
||||
- dialog "设置":
|
||||
- text: 设置
|
||||
- navigation:
|
||||
- button "通用设置"
|
||||
- button "模型"
|
||||
- button "内置插件"
|
||||
- button "Agent 预设"
|
||||
- button "打开配置文件"
|
||||
- button "关闭"
|
||||
- heading "模型" [level=2]
|
||||
- paragraph: 填入各提供商的 API 密钥即可使用其模型。
|
||||
- list:
|
||||
- listitem:
|
||||
- text: DeepSeek
|
||||
- img "API 密钥已配置"
|
||||
- button "编辑 DeepSeek (deepseek-official)": 编辑
|
||||
- text: DeepSeek deepseek-official API 密钥
|
||||
- button "认证说明": "?"
|
||||
- textbox "API 密钥":
|
||||
- /placeholder: 留空使用环境认证(DEEPSEEK_API_KEY)
|
||||
- text: fixture-saved-deepseek-key
|
||||
- button "显示密钥"
|
||||
- group: 自定义设置
|
||||
- button "取消"
|
||||
- button "保存"
|
||||
- button "添加模型提供商"
|
||||
@@ -17,9 +17,7 @@
|
||||
- text: DeepSeek deepseek-official API 密钥
|
||||
- button "认证说明": "?"
|
||||
- textbox "API 密钥":
|
||||
- /placeholder: 留空使用环境认证(DEEPSEEK_API_KEY)
|
||||
- text: sk-messages-onboarding
|
||||
- button "显示密钥"
|
||||
- /placeholder: 留空使用已保存凭据
|
||||
- group:
|
||||
- text: 自定义设置 API 地址
|
||||
- textbox "API 地址":
|
||||
|
||||
@@ -17,9 +17,7 @@
|
||||
- text: DeepSeek deepseek-official API 密钥
|
||||
- button "认证说明": "?"
|
||||
- textbox "API 密钥":
|
||||
- /placeholder: 留空使用环境认证(DEEPSEEK_API_KEY)
|
||||
- text: dsh_onboarding_fixture_key
|
||||
- button "显示密钥"
|
||||
- /placeholder: 留空使用已保存凭据
|
||||
- group:
|
||||
- text: 自定义设置 API 地址
|
||||
- textbox "API 地址":
|
||||
|
||||
@@ -17,9 +17,7 @@
|
||||
- text: DeepSeek deepseek-official API 密钥
|
||||
- button "认证说明": "?"
|
||||
- textbox "API 密钥":
|
||||
- /placeholder: 留空使用环境认证(DEEPSEEK_API_KEY)
|
||||
- text: dsh_onboarding_fixture_key
|
||||
- button "显示密钥"
|
||||
- /placeholder: 留空使用已保存凭据
|
||||
- group:
|
||||
- text: 自定义设置 API 地址
|
||||
- textbox "API 地址":
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
- switch "启用 自动授权审查"
|
||||
- listitem:
|
||||
- button "查看 变更请求与工作树": 变更请求与工作树
|
||||
- text: 实验性 跟踪每个会话的变更请求及其检查与评审,新会话也可以在 Git 工作树中开始;GitHub 通过 gh 命令行访问。
|
||||
- text: 实验性 在会话侧栏集中跟踪关联的 GitHub PR:查看检查状态、评审意见与代码差异,并把修改建议交给智能体继续处理。也可查看本地改动,用独立工作树隔离任务。GitHub 功能需要本机安装 gh 并登录。
|
||||
- status
|
||||
- switch "启用 变更请求与工作树"
|
||||
- heading "已安装插件" [level=3]
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
- button "返回插件": 插件
|
||||
- status
|
||||
- switch "启用 变更请求与工作树"
|
||||
- heading "变更请求与工作树" [level=3]
|
||||
- text: v{{version}} 实验性
|
||||
- paragraph:
|
||||
- code: "@deepseek-ai/dsh-experimental-forge-bundle"
|
||||
- paragraph: 在会话侧栏集中跟踪关联的 GitHub PR:查看检查状态、评审意见与代码差异,并把修改建议交给智能体继续处理。也可查看本地改动,用独立工作树隔离任务。GitHub 功能需要本机安装 gh 并登录。
|
||||
- region "详细介绍":
|
||||
- paragraph:
|
||||
- img "会话关联 GitHub PR,跟踪检查、评审与代码差异,并用工作树隔离任务改动"
|
||||
- heading "包含的组件" [level=4]
|
||||
- text: 共 7 个 · 7 已停用
|
||||
- list:
|
||||
- listitem:
|
||||
- text: 工作树服务 创建命名的 Git 工作树,并切换当前会话的工作目录。
|
||||
- code: worktree
|
||||
- code: "@deepseek-ai/dsh-experimental-worktree"
|
||||
- text: 已关闭
|
||||
- listitem:
|
||||
- text: Git 工作树 要求在新分支上工作时,智能体会创建单独的 Git 工作树并在其中继续,你当前的工作树保持原样。
|
||||
- code: tool-worktree
|
||||
- code: "@deepseek-ai/dsh-experimental-tool-worktree"
|
||||
- text: 已关闭
|
||||
- listitem:
|
||||
- text: 变更请求 通过一个与提供方无关的服务注册代码托管提供方,并轮询变更请求状态。
|
||||
- code: forge
|
||||
- code: "@deepseek-ai/dsh-experimental-forge"
|
||||
- text: 已关闭
|
||||
- listitem:
|
||||
- text: GitHub 变更请求 通过宿主上的 GitHub CLI 为 forge 注册表读取 GitHub 变更请求。
|
||||
- code: forge-github
|
||||
- code: "@deepseek-ai/dsh-experimental-forge-github"
|
||||
- text: 已关闭
|
||||
- listitem:
|
||||
- text: 会话变更请求 把变更请求绑定到创建、推送或跟踪它们的会话,并监听其状态。
|
||||
- code: forge-session
|
||||
- code: "@deepseek-ai/dsh-experimental-forge-session"
|
||||
- text: 已关闭
|
||||
- listitem:
|
||||
- text: 工作树服务 创建命名的 Git 工作树,并切换当前会话的工作目录。
|
||||
- code: forge-worktree
|
||||
- code: "@deepseek-ai/dsh-experimental-worktree"
|
||||
- text: 已关闭
|
||||
- listitem:
|
||||
- text: 变更请求面板 在侧栏标签页中显示会话的变更请求,并让新会话可以在工作树中开始。
|
||||
- code: forge-panel
|
||||
- code: "@deepseek-ai/dsh-experimental-client-ui-forge"
|
||||
- text: 已关闭
|
||||
- heading "来源信息" [level=4]
|
||||
- term: 代码来源
|
||||
- definition: 内置
|
||||
- term: 当前版本
|
||||
- definition: {{version}}
|
||||
@@ -39,7 +39,7 @@
|
||||
- switch "启用 自动授权审查"
|
||||
- listitem:
|
||||
- button "查看 变更请求与工作树": 变更请求与工作树
|
||||
- text: 实验性 跟踪每个会话的变更请求及其检查与评审,新会话也可以在 Git 工作树中开始;GitHub 通过 gh 命令行访问。
|
||||
- text: 实验性 在会话侧栏集中跟踪关联的 GitHub PR:查看检查状态、评审意见与代码差异,并把修改建议交给智能体继续处理。也可查看本地改动,用独立工作树隔离任务。GitHub 功能需要本机安装 gh 并登录。
|
||||
- status
|
||||
- switch "启用 变更请求与工作树"
|
||||
- heading "已安装插件" [level=3]
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
- switch "启用 自动授权审查"
|
||||
- listitem:
|
||||
- button "查看 变更请求与工作树": 变更请求与工作树
|
||||
- text: 实验性 跟踪每个会话的变更请求及其检查与评审,新会话也可以在 Git 工作树中开始;GitHub 通过 gh 命令行访问。
|
||||
- text: 实验性 在会话侧栏集中跟踪关联的 GitHub PR:查看检查状态、评审意见与代码差异,并把修改建议交给智能体继续处理。也可查看本地改动,用独立工作树隔离任务。GitHub 功能需要本机安装 gh 并登录。
|
||||
- status
|
||||
- switch "启用 变更请求与工作树"
|
||||
- heading "已安装插件" [level=3]
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
- id: llm-deepseek
|
||||
config:
|
||||
baseURL: http://127.0.0.1
|
||||
models:
|
||||
- id: credential-editor-model
|
||||
@@ -447,6 +447,8 @@ it.skipIf(MODE === 'record')('automatically shows a numbered PR when gh JSON omi
|
||||
const { settled } = await sendPrompt(page, scaffold)
|
||||
const sessionId = await settled
|
||||
const events = await readPersistedEvents(scaffold, sessionId)
|
||||
const request = events.find(event => event.type === 'request/header')
|
||||
expect(request?.data.header.tools?.filter(tool => tool.name === 'create_worktree')).toHaveLength(1)
|
||||
const toolOutput = events.flatMap(event => event.type === 'tool/result'
|
||||
? event.data.message.content.flatMap(block => block.type === 'text' ? [block.text] : []) : []).join('\n')
|
||||
expect(toolOutput).toContain('Add the pull request panel')
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// wire — the one add card offers the dormant pi-ai catalog on its third-party
|
||||
// mode and the create form on its custom-API mode, a blank key saves a
|
||||
// reference-free profile for provider-native auth, and typing an API key later
|
||||
// stores it under the derived reference (`MINIMAX_CN_API_KEY`)
|
||||
// stores it write-only under the derived reference (`MINIMAX_CN_API_KEY`)
|
||||
// while the settings document records only that reference. Each saved row
|
||||
// appears after route topology invalidation without presenting liveness as
|
||||
// provider status. The customized-settings fold writes its curated fields —
|
||||
@@ -154,7 +154,8 @@ describe('web e2e: Models settings page configures a dormant provider', () => {
|
||||
await dialog.getByText('该 API 密钥格式错误,请检查。').waitFor({ timeout: 10_000 })
|
||||
await expect.poll(async () => save.isEnabled(), { timeout: 10_000 }).toBe(false)
|
||||
|
||||
// An empty field leaves this new profile on native authentication.
|
||||
// Clearing it restores submit: an empty field means "keep what is stored",
|
||||
// never a refusal, or editing any other setting would demand the key.
|
||||
await key.fill('')
|
||||
await expect.poll(async () => save.isEnabled(), { timeout: 10_000 }).toBe(true)
|
||||
expect(await dialog.getByText('该 API 密钥格式错误,请检查。').count()).toBe(0)
|
||||
@@ -220,9 +221,7 @@ describe('web e2e: Models settings page configures a dormant provider', () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-models-customized'))
|
||||
const dialog = page.getByRole('dialog', { name: '设置' })
|
||||
await dialog.getByRole('button', { name: '编辑 minimax-cn' }).click()
|
||||
const savedKey = dialog.getByLabel('API 密钥', { exact: true })
|
||||
await expect.poll(() => savedKey.inputValue(), { timeout: 10_000 }).toBe('sk-e2e-minimax')
|
||||
expect(await savedKey.getAttribute('type')).toBe('password')
|
||||
await dialog.getByPlaceholder('留空使用已保存凭据', { exact: true }).waitFor({ timeout: 10_000 })
|
||||
expect(await dialog.textContent()).not.toContain('API 密钥来源:')
|
||||
expect(await dialog.textContent()).not.toMatch(/oauth/i)
|
||||
await dialog.getByText('自定义设置').click()
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// credential is absent, both ordered steps share the shipped modal chrome,
|
||||
// and the inline key write lands in an isolated harness home without a reload
|
||||
// or model call.
|
||||
import { randomBytes } from 'node:crypto'
|
||||
import { assertModelInputLayout } from './model-input-layout.ts'
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
@@ -32,7 +33,6 @@ describe.skipIf(MODE === 'record')('web e2e: first-run DeepSeek credential setup
|
||||
let page: Page
|
||||
let tripwire: ReturnType<typeof watchConsole>
|
||||
const browserConsole: string[] = []
|
||||
const secret = 'dsh_onboarding_fixture_key'
|
||||
|
||||
beforeAll(async () => {
|
||||
scaffold = await launchWebScaffold({ deepSeekMissingCredential: true, welcomeNoticePending: true })
|
||||
@@ -50,7 +50,7 @@ describe.skipIf(MODE === 'record')('web e2e: first-run DeepSeek credential setup
|
||||
await scaffold?.close()
|
||||
})
|
||||
|
||||
it('stores a key and loads it masked only in an explicitly opened editor', async () => {
|
||||
it('stores a key write-only and observes configured state without restarting', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-onboarding-deepseek-config'))
|
||||
const welcome = page.getByRole('dialog', { name: WELCOME_NOTICE_COPY.zh.title })
|
||||
await welcome.waitFor({ timeout: 15_000 })
|
||||
@@ -86,6 +86,7 @@ describe.skipIf(MODE === 'record')('web e2e: first-run DeepSeek credential setup
|
||||
const initial = await captureStableAria(page, '[role="dialog"]', scaffold.workspaceCwd)
|
||||
await compareOrRefreshGolden(MISSING_EXPECTED, initial, MODE)
|
||||
|
||||
const secret = `dsh_onboarding_${randomBytes(12).toString('hex')}`
|
||||
await keyInput.fill(secret)
|
||||
await credentialStep.getByRole('button', { name: '保存并继续' }).click()
|
||||
await credentialStep.waitFor({ state: 'detached', timeout: 15_000 })
|
||||
@@ -100,7 +101,8 @@ describe.skipIf(MODE === 'record')('web e2e: first-run DeepSeek credential setup
|
||||
const acknowledgedSettings = await readFile(join(scaffold.harnessHome, 'profiles', 'scaffold', 'cordis.patch.yml'), 'utf8')
|
||||
expect(acknowledgedSettings).toContain(`${WELCOME_NOTICE_ACK_FIELD}: ${WELCOME_NOTICE_VERSION}`)
|
||||
|
||||
// Opening the ordinary Models editor loads the saved key masked.
|
||||
// The ordinary Models surface reuses the refreshed join and exposes the
|
||||
// configured write-only placeholder without a reload.
|
||||
await openSettings(page, 'zh')
|
||||
const settings = page.getByRole('dialog', { name: '设置' })
|
||||
await settings.waitFor({ timeout: 10_000 })
|
||||
@@ -111,10 +113,9 @@ describe.skipIf(MODE === 'record')('web e2e: first-run DeepSeek credential setup
|
||||
const configuredInput = settings.getByLabel('API 密钥', { exact: true })
|
||||
await configuredInput.waitFor({ timeout: 10_000 })
|
||||
await expect.poll(
|
||||
() => configuredInput.inputValue(),
|
||||
() => configuredInput.getAttribute('placeholder'),
|
||||
{ timeout: 10_000 },
|
||||
).toBe(secret)
|
||||
expect(await configuredInput.getAttribute('type')).toBe('password')
|
||||
).toBe('留空使用已保存凭据')
|
||||
|
||||
const secondReloadWarnings = tripwire.warnings.length
|
||||
await page.reload({ waitUntil: 'load' })
|
||||
@@ -254,9 +255,9 @@ describe.skipIf(MODE === 'record')('web e2e: first-run DeepSeek credential setup
|
||||
await settings.getByRole('group', { name: '输入类型 1' }).getByRole('checkbox', { name: '图片' }).check()
|
||||
|
||||
await expect.poll(
|
||||
() => settings.getByLabel('API 密钥', { exact: true }).inputValue(),
|
||||
() => settings.getByLabel('API 密钥', { exact: true }).getAttribute('placeholder'),
|
||||
{ timeout: 10_000 },
|
||||
).toBe(secret)
|
||||
).toBe('留空使用已保存凭据')
|
||||
const modelEditor = await captureStableAria(page, '[role="dialog"]', scaffold.workspaceCwd)
|
||||
await compareOrRefreshGolden(MODELS_EXPECTED, modelEditor, MODE)
|
||||
await settings.getByRole('button', { name: '保存', exact: true }).click()
|
||||
|
||||
@@ -9,6 +9,7 @@ import { readFile } from 'node:fs/promises'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import type { Browser, Locator, Page } from 'playwright'
|
||||
import { chromium } from 'playwright'
|
||||
import { HIDDEN_BUNDLES } from '@deepseek-ai/dsh-official-bundles'
|
||||
import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
|
||||
import { join } from 'node:path'
|
||||
import {
|
||||
@@ -115,17 +116,14 @@ describe('web e2e: plugin configuration pages', () => {
|
||||
// The More list omits the shipped bundles whose own rows do not run (the visibility rule lives in
|
||||
// ui-plugin-manager), so it holds fewer cards than the installed optional-bundle list.
|
||||
const bundles = await scaffold.ctx.pluginManager.listBundles()
|
||||
const hiddenBundles = new Set([
|
||||
'@deepseek-ai/dsh-experimental-terminal-bundle',
|
||||
'@deepseek-ai/dsh-experimental-liveshare',
|
||||
])
|
||||
expect(await panel.locator('[data-plugin-group="more"] [data-plugin-package]').count())
|
||||
.toBe(bundles.filter(bundle => !hiddenBundles.has(bundle.name)
|
||||
.toBe(bundles.filter(bundle => !HIDDEN_BUNDLES.has(bundle.name)
|
||||
&& bundle.name !== '@deepseek-ai/dsh-experimental-liveshare'
|
||||
&& bundle.official && (bundle.listedByDefault !== false || bundle.enabled || bundle.installed)).length)
|
||||
expect(await basic.count()).toBe(0)
|
||||
expect(await extensions.count()).toBe(0)
|
||||
expect(await panel.locator('[data-plugin-group="bundles"]').count()).toBe(0)
|
||||
await panel.getByRole('button', { name: '查看 Git 工作树', exact: true }).click()
|
||||
await panel.getByRole('button', { name: '查看 开发者工具', exact: true }).click()
|
||||
await panel.locator('[data-plugin-detail]').waitFor()
|
||||
await panel.getByRole('button', { name: '返回 实验性插件', exact: true }).click()
|
||||
await panel.getByRole('heading', { name: '实验性插件', exact: true, level: 1 }).waitFor()
|
||||
|
||||
@@ -9,6 +9,7 @@ import type { Browser, Locator, Page } from 'playwright'
|
||||
import { chromium } from 'playwright'
|
||||
import { FiberState } from '@deepseek-ai/cordis'
|
||||
import { readProfileManifest } from '@deepseek-ai/dsh-app-boot'
|
||||
import { HIDDEN_BUNDLES } from '@deepseek-ai/dsh-official-bundles'
|
||||
import { afterAll, beforeAll, describe, expect, it, onTestFailed, onTestFinished, vi } from 'vitest'
|
||||
import { join } from 'node:path'
|
||||
import {
|
||||
@@ -852,7 +853,7 @@ describe('web e2e: plugin manager', () => {
|
||||
// the eligible installed optional-bundle list.
|
||||
expect(await panel.locator('[data-plugin-group="more"] [data-plugin-package="@deepseek-ai/dsh-experimental-liveshare"]').count()).toBe(0)
|
||||
expect(await panel.locator('[data-plugin-group="more"] [data-plugin-package]').count())
|
||||
.toBe(bundles.filter(bundle => bundle.name !== '@deepseek-ai/dsh-experimental-terminal-bundle'
|
||||
.toBe(bundles.filter(bundle => !HIDDEN_BUNDLES.has(bundle.name)
|
||||
&& bundle.name !== '@deepseek-ai/dsh-experimental-liveshare'
|
||||
&& bundle.official && (bundle.listedByDefault !== false || bundle.enabled || bundle.installed)).length)
|
||||
// Computer Use ships with the installation and is listed only on the macOS Hosts it supports.
|
||||
@@ -878,7 +879,6 @@ describe('web e2e: plugin manager', () => {
|
||||
['@deepseek-ai/dsh-personalized-recommendations', 1],
|
||||
['@deepseek-ai/dsh-experimental-inspector-profile', 1],
|
||||
...cuaSupported ? [['@deepseek-ai/dsh-cua-bundle', 1]] : [],
|
||||
['@deepseek-ai/dsh-experimental-tool-worktree', 1],
|
||||
['@deepseek-ai/dsh-experimental-cot-translation-bundle', 1],
|
||||
['@deepseek-ai/dsh-experimental-session-titles-bundle', 1],
|
||||
['@deepseek-ai/dsh-experimental-badge-skill-bundle', 1],
|
||||
@@ -1065,6 +1065,51 @@ describe('web e2e: plugin manager', () => {
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
})
|
||||
|
||||
it('introduces code review and worktrees with localized artwork before activation', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-forge-preview'))
|
||||
const panel = await openPluginsPanel()
|
||||
await panel.getByRole('button', { name: '查看 变更请求与工作树', exact: true }).click()
|
||||
const detail = panel.locator('[data-plugin-detail]')
|
||||
expect(await detail.getByRole('switch').getAttribute('aria-checked')).toBe('false')
|
||||
const sources: (string | null)[] = []
|
||||
let english = false
|
||||
try {
|
||||
for (const [language, alt] of [
|
||||
['zh', '会话关联 GitHub PR,跟踪检查、评审与代码差异,并用工作树隔离任务改动'],
|
||||
['en', 'A conversation linked to a GitHub pull request, with checks, reviews, code diffs, and an isolated worktree'],
|
||||
] as const) {
|
||||
if (language === 'en') {
|
||||
await setLanguage(language)
|
||||
english = true
|
||||
}
|
||||
const document = detail.locator('[data-detail-document]')
|
||||
const image = document.getByRole('img', { name: alt, exact: true })
|
||||
await image.waitFor()
|
||||
await image.evaluate(async (node: HTMLImageElement) => { await node.decode() })
|
||||
expect(await image.evaluate((node: HTMLImageElement) => [node.naturalWidth, node.naturalHeight])).toEqual([640, 300])
|
||||
sources.push(await image.getAttribute('src'))
|
||||
for (const width of [1680, 390]) {
|
||||
await page.setViewportSize({ width, height: 1000 })
|
||||
expect(await image.evaluate((node: HTMLImageElement) => {
|
||||
const bounds = node.getBoundingClientRect()
|
||||
return bounds.width > 0 && bounds.left >= 0 && bounds.right <= window.innerWidth
|
||||
&& window.document.documentElement.scrollWidth <= window.innerWidth
|
||||
})).toBe(true)
|
||||
}
|
||||
await page.setViewportSize({ width: 1680, height: 1000 })
|
||||
}
|
||||
expect(sources[0]).not.toBe(sources[1])
|
||||
} finally {
|
||||
await page.setViewportSize({ width: 1680, height: 1000 })
|
||||
if (english) await setLanguage('zh')
|
||||
}
|
||||
await compareOrRefreshGolden(join(SNAPSHOT_DIR, 'forge-preview.expected.md'),
|
||||
await captureStableAria(page, '[data-plugin-panel]', scaffold.workspaceCwd, {
|
||||
replacements: [[badgeVersion, '{{version}}']],
|
||||
}), MODE)
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
})
|
||||
|
||||
it('localizes independent exports and falls back per field without activating the plugins', async () => {
|
||||
onTestFinished(closeSettings)
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-plugin-manager-exports'))
|
||||
@@ -1437,8 +1482,8 @@ describe('web e2e: plugin manager', () => {
|
||||
// on, so writing the patch file recomposes the running loader; each step first waits for that
|
||||
// recomposition and then re-reads the Host bundle list through the page's own Refresh.
|
||||
const patchFile = join(visibilityScaffold.harnessHome, 'profiles', 'scaffold', 'cordis.patch.yml')
|
||||
const declarationsRow = (): boolean => [...visibilityScaffold.ctx.loader.entries()]
|
||||
.some(entry => entry.options.id === 'liveshare-viewer-declarations')
|
||||
const shareHostRow = (): boolean => [...visibilityScaffold.ctx.loader.entries()]
|
||||
.some(entry => entry.options.id === 'liveshare-share-host')
|
||||
const refresh = panel.getByRole('button', { name: '刷新', exact: true })
|
||||
|
||||
// Default install: the package ships, but no row it declares runs, so neither list offers it.
|
||||
@@ -1456,12 +1501,12 @@ describe('web e2e: plugin manager', () => {
|
||||
// selects no such bundle: the card joins the experimental list.
|
||||
await writeFile(patchFile, [
|
||||
'- insert:',
|
||||
' - id: liveshare-viewer-declarations',
|
||||
" name: '@deepseek-ai/dsh-experimental-liveshare/viewer-declarations'",
|
||||
' - id: liveshare-share-host',
|
||||
" name: '@deepseek-ai/dsh-experimental-liveshare'",
|
||||
'',
|
||||
].join('\n'))
|
||||
await expect.poll(declarationsRow, {
|
||||
timeout: 30_000, message: 'the profile patch did not compose the LiveShare declarations row',
|
||||
await expect.poll(shareHostRow, {
|
||||
timeout: 30_000, message: 'the profile patch did not compose the LiveShare share-host row',
|
||||
}).toBe(true)
|
||||
await refresh.click()
|
||||
await expect.poll(() => card.count(), {
|
||||
@@ -1470,8 +1515,8 @@ describe('web e2e: plugin manager', () => {
|
||||
|
||||
// Removing the row stops it: the card leaves both lists again.
|
||||
await writeFile(patchFile, '[]\n')
|
||||
await expect.poll(declarationsRow, {
|
||||
timeout: 30_000, message: 'the profile patch did not remove the LiveShare declarations row',
|
||||
await expect.poll(shareHostRow, {
|
||||
timeout: 30_000, message: 'the profile patch did not remove the LiveShare share-host row',
|
||||
}).toBe(false)
|
||||
await refresh.click()
|
||||
await expect.poll(() => card.count(), {
|
||||
@@ -1494,7 +1539,7 @@ describe('web e2e: plugin manager', () => {
|
||||
await assertFixtureInventory(SNAPSHOT_DIR, [
|
||||
'manager.expected.md', 'live-enabled.expected.md', 'missing-bundle.expected.md', 'exports.expected.md', 'exports-en.expected.md', 'icons.expected.md', 'loading.expected.md', 'refresh.expected.md',
|
||||
'ime-enter.expected.md', 'header-responsive.expected.md', 'header-natural-motion.expected.md', 'preview.expected.md',
|
||||
'touch-tooltips.expected.md',
|
||||
'touch-tooltips.expected.md', 'forge-preview.expected.md',
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -59,6 +59,7 @@ import {
|
||||
type NormalizeContext,
|
||||
} from '@deepseek-ai/dsh-session-snapshot'
|
||||
import type { Profile, ProfileContext } from '@deepseek-ai/dsh-app-boot'
|
||||
import type { PresetDefinition } from '@deepseek-ai/dsh-agent-preset-registry'
|
||||
import { dshHomePath } from '@deepseek-ai/dsh-home-paths'
|
||||
import { LlmAdapter } from '@deepseek-ai/dsh-llm'
|
||||
import type {
|
||||
@@ -713,6 +714,20 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise<We
|
||||
{ id: 'llm-deepseek', disabled: mode !== 'record' && !maskDeepSeekCredential },
|
||||
]
|
||||
|
||||
// Recorded standard sessions pin the optional patch tool in their request headers.
|
||||
if (options.replayFixture !== undefined || options.compareReplaySession === true) {
|
||||
const config = composedRows.find(row => row.id === 'preset-standard')!.config as PresetDefinition
|
||||
overlayPatches.push({
|
||||
id: 'preset-standard',
|
||||
config: {
|
||||
...config,
|
||||
plugins: config.plugins.map(plugin => plugin.id === 'tool-fs'
|
||||
? { ...plugin, config: { ...plugin.config, applyPatch: true } }
|
||||
: plugin),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Live fields use a shared deployment layer; process-specific ports and roots stay in CLI overlays.
|
||||
const formEntries = new Set(['agent-default-model', 'agent-preset-registry', 'llm-deepseek', 'llm-pi-ai',
|
||||
'web', 'web-search-deepseek', 'web-deepseek-api', 'agent-loop', 'subagent', 'bash-sandbox', 'pwsh-sandbox',
|
||||
@@ -1761,10 +1776,12 @@ export async function assertFixtureInventory(dir: string, expected: string[]): P
|
||||
|
||||
/**
|
||||
* Console tripwires: reconnect/gap-repair self-healing or a pageerror must
|
||||
* fail the scenario, not mask a dead wire behind eventual consistency.
|
||||
* fail the scenario, not mask a dead wire behind eventual consistency. A boot
|
||||
* audit warning fails it too: the product boot tolerates inactive optional
|
||||
* entries, but every e2e composition must activate completely.
|
||||
*/
|
||||
export interface WebConsoleTripwire {
|
||||
/** Console warnings matching reconnect/gap-repair/discontinuity copy. */
|
||||
/** Console warnings matching reconnect/gap-repair/discontinuity copy or the boot audit's inactive-entry warning. */
|
||||
warnings: string[]
|
||||
/** Uncaught page errors. */
|
||||
pageErrors: string[]
|
||||
@@ -1780,7 +1797,7 @@ export function watchConsole(page: Page): WebConsoleTripwire {
|
||||
const pageErrors: string[] = []
|
||||
page.on('console', (message) => {
|
||||
const text = message.text()
|
||||
if (/connection lost|gap repair|discontinuous/i.test(text)) warnings.push(text)
|
||||
if (/connection lost|gap repair|discontinuous|web boot: warning/i.test(text)) warnings.push(text)
|
||||
})
|
||||
page.on('pageerror', (error) => { pageErrors.push(String(error)) })
|
||||
return { warnings, pageErrors }
|
||||
|
||||
@@ -1,7 +1,51 @@
|
||||
/** Manual task creation and editing through the shipped Web composition. */
|
||||
/** Schedule creation, prompt prefill, and editing through the shipped Web composition. */
|
||||
import { chromium } from 'playwright'
|
||||
import { expect, it } from 'vitest'
|
||||
import { launchWebScaffold, watchConsole } from './scaffold.ts'
|
||||
import { writeComposerDraft } from './support.ts'
|
||||
|
||||
it.each([
|
||||
{ locale: 'en', panel: 'Automation tasks', create: 'New', menu: 'Choose a creation method', chat: 'Create in a session',
|
||||
prompt: 'Please create an automation task based on the requirements.' },
|
||||
{ locale: 'zh', panel: '自动化任务', create: '新建', menu: '选择创建方式', chat: '在会话中创建',
|
||||
prompt: '请根据需求创建一个自动化任务。' },
|
||||
])('prefills an unsent Schedule creation draft in $locale through both conversation entries', async (copy) => {
|
||||
const scaffold = await launchWebScaffold({ firstUse: true })
|
||||
let browser: Awaited<ReturnType<typeof chromium.launch>> | undefined
|
||||
try {
|
||||
browser = await chromium.launch()
|
||||
const page = await browser.newPage({ viewport: { width: 1440, height: 960 }, locale: copy.locale })
|
||||
await page.addInitScript((locale) => { localStorage.setItem('dsh.locale', locale) }, copy.locale)
|
||||
const tripwire = watchConsole(page)
|
||||
await page.goto(scaffold.authenticatedUrl, { waitUntil: 'load' })
|
||||
const input = page.locator('[data-composer-input][contenteditable="true"]').first()
|
||||
for (const { entry, draft } of [
|
||||
{ entry: 'primary', draft: '' },
|
||||
{ entry: 'primary', draft: 'Existing unsent draft' },
|
||||
{ entry: 'menu', draft: '' },
|
||||
{ entry: 'menu', draft: 'Existing unsent draft' },
|
||||
]) {
|
||||
await writeComposerDraft(page, input, draft)
|
||||
await page.getByRole('button', { name: copy.panel, exact: true }).click()
|
||||
const manager = page.getByTestId('task-manager-page')
|
||||
if (entry === 'primary') await manager.getByRole('button', { name: copy.create, exact: true }).click()
|
||||
else {
|
||||
await manager.getByRole('button', { name: copy.menu, exact: true }).click()
|
||||
await page.getByRole('menuitem', { name: copy.chat, exact: true }).click()
|
||||
}
|
||||
await manager.waitFor({ state: 'detached' })
|
||||
await input.getByText(copy.prompt, { exact: true }).waitFor()
|
||||
expect(await input.evaluate(element => [...element.children].map(paragraph => paragraph.textContent).join('\n')))
|
||||
.toBe(draft === '' ? copy.prompt : `${draft}\n${copy.prompt}`)
|
||||
const events = scaffold.ctx.agents.list().flatMap(agent => agent.session.snapshotEvents())
|
||||
expect(events.filter(event => event.type === 'user/message' || event.type === 'turn/start')).toEqual([])
|
||||
expect(await scaffold.ctx.schedule.catalog()).toEqual([])
|
||||
}
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
} finally {
|
||||
try { await browser?.close() } finally { await scaffold.close() }
|
||||
}
|
||||
})
|
||||
|
||||
it('creates from a paused empty state, selects the task, and saves an inline edit without starting an Agent', async () => {
|
||||
// First use registers the default Workspace, which a new task's directory starts with.
|
||||
|
||||
@@ -84,6 +84,24 @@ class RestartableServer {
|
||||
logs(): string { return this.output.replace(/([?&]token=)[A-Za-z0-9_-]+/gu, '$1<redacted>') }
|
||||
}
|
||||
|
||||
type HostRpc = <T>(endpoint: string, args: object) => Promise<T>
|
||||
|
||||
/** Authenticate against one server process and return its Host RPC caller. */
|
||||
async function hostRpc(url: string): Promise<HostRpc> {
|
||||
const authenticated = await fetch(url, { redirect: 'manual' })
|
||||
const cookie = authenticated.headers.get('set-cookie')?.split(';', 1)[0]
|
||||
if (cookie === undefined) throw new Error('Server did not issue an authentication cookie')
|
||||
return async <T>(endpoint: string, args: object): Promise<T> => {
|
||||
const response = await fetch(new URL('/api/' + endpoint, url), {
|
||||
method: 'POST', headers: { 'content-type': 'application/json', cookie },
|
||||
body: JSON.stringify({ type: 'client-request', rpcId: randomUUID(), method: endpoint, payload: { args } }),
|
||||
})
|
||||
const result = await response.json() as { result: { ok: boolean; value: T; error?: { message: string } } }
|
||||
if (!result.result.ok) throw new Error(`${endpoint}: ${String(result.result.error?.message)}`)
|
||||
return result.result.value
|
||||
}
|
||||
}
|
||||
|
||||
it.each([false, true])('keeps the same revision, Session and page across a server restart (delayed startup: %s)', async (holdStartup) => {
|
||||
const world = await mkdtemp(join(tmpdir(), 'dsh-server-restart-'))
|
||||
onTestFinished(() => rm(world, { recursive: true, force: true }))
|
||||
@@ -118,18 +136,7 @@ it.each([false, true])('keeps the same revision, Session and page across a serve
|
||||
|
||||
const url = await server.start(0)
|
||||
const port = Number(new URL(url).port)
|
||||
const authenticated = await fetch(url, { redirect: 'manual' })
|
||||
const cookie = authenticated.headers.get('set-cookie')?.split(';', 1)[0]
|
||||
if (cookie === undefined) throw new Error('Server did not issue an authentication cookie')
|
||||
const rpc = async <T>(endpoint: string, args: object): Promise<T> => {
|
||||
const response = await fetch(new URL('/api/' + endpoint, url), {
|
||||
method: 'POST', headers: { 'content-type': 'application/json', cookie },
|
||||
body: JSON.stringify({ type: 'client-request', rpcId: randomUUID(), method: endpoint, payload: { args } }),
|
||||
})
|
||||
const result = await response.json() as { result: { ok: boolean; value: T; error?: { message: string } } }
|
||||
if (!result.result.ok) throw new Error(`${endpoint}: ${String(result.result.error?.message)}`)
|
||||
return result.result.value
|
||||
}
|
||||
const rpc = await hostRpc(url)
|
||||
await rpc('workspace/create', { request: { path: workspacePath } })
|
||||
const scenarioId = randomUUID()
|
||||
|
||||
@@ -234,3 +241,30 @@ it.each([false, true])('keeps the same revision, Session and page across a serve
|
||||
expect(streamErrors).toEqual([])
|
||||
expect(navigations).toBe(0)
|
||||
})
|
||||
|
||||
it('keeps a live blank Session blank across a graceful server restart', async () => {
|
||||
const world = await mkdtemp(join(tmpdir(), 'dsh-server-restart-'))
|
||||
onTestFinished(() => rm(world, { recursive: true, force: true }))
|
||||
// The scenario runs no model turn, so the model endpoint is never contacted.
|
||||
const server = new RestartableServer(world, 'http://127.0.0.1:9')
|
||||
onTestFinished(() => server.stop())
|
||||
const workspacePath = join(world, 'workspace')
|
||||
await mkdir(workspacePath)
|
||||
onTestFailed(async () => {
|
||||
await writeFile(join(REPO_ROOT, '.artifacts', 'server-restart-blank-' + randomUUID() + '.json'), JSON.stringify({ server: server.logs() }, null, 2))
|
||||
})
|
||||
|
||||
const before = await hostRpc(await server.start(0))
|
||||
const { workspace } = await before<{ workspace: { id: string } }>('workspace/create', { request: { path: workspacePath } })
|
||||
const { sessionId } = await before<{ sessionId: string }>('session/create', { request: { workspaceId: workspace.id } })
|
||||
const listed = async (rpc: HostRpc) => (await rpc<{
|
||||
items: { sessionId: string; blank: boolean; agentAvailable: boolean }[]
|
||||
}>('session/list', { _request: {} })).items.find(item => item.sessionId === sessionId)
|
||||
expect(await listed(before)).toMatchObject({ blank: true, agentAvailable: true })
|
||||
|
||||
// SIGTERM unloads the Web layer, including the list projection, before its live Session detaches;
|
||||
// stopping right after creation also precedes the throttled checkpoint of the Session's setup events.
|
||||
await server.stop()
|
||||
const after = await hostRpc(await server.start(0))
|
||||
expect(await listed(after)).toMatchObject({ blank: true, agentAvailable: false })
|
||||
})
|
||||
|
||||
@@ -473,7 +473,6 @@ function assertLeanChildRecord(agent: Agent): void {
|
||||
* Agent Note owns the rationale and its sources.
|
||||
*/
|
||||
const EXPECTED_TOOLS = [
|
||||
'apply_patch',
|
||||
'ask_user_question',
|
||||
'bash',
|
||||
'create_goal',
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
/** Browser geometry for sidebar controls, text clearance, and drag previews. */
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import { mkdtemp, readFile, rm } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import type { Browser, Locator, Page } from 'playwright'
|
||||
import { chromium } from 'playwright'
|
||||
@@ -179,6 +181,44 @@ describe('web e2e: sidebar search alignment and row action targets', () => {
|
||||
expect(tripwire.warnings).toEqual([])
|
||||
})
|
||||
|
||||
it.each(['Search sessions', 'View options', 'Add workspace'])('keeps the %s tooltip visible across its full bounds', async (name) => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-sidebar-toolbar-tooltip'))
|
||||
const trigger = page.getByRole('button', { name, exact: true })
|
||||
await trigger.hover()
|
||||
const tooltip = page.getByRole('tooltip')
|
||||
await tooltip.waitFor({ state: 'visible', timeout: 10_000 })
|
||||
const bounds = await tooltip.evaluate((element: HTMLElement) => {
|
||||
const rect = element.getBoundingClientRect()
|
||||
const sidebar = document.querySelector('[class*="sidebarCol"]')!.getBoundingClientRect()
|
||||
const originalStyle = element.getAttribute('style')
|
||||
try {
|
||||
// Tooltips ignore pointer input; enabling it only for this read exposes ancestor clipping.
|
||||
element.style.pointerEvents = 'auto'
|
||||
const points = [
|
||||
[rect.left + 2, rect.top + rect.height / 2],
|
||||
[rect.right - 2, rect.top + rect.height / 2],
|
||||
[rect.left + rect.width / 2, rect.top + 2],
|
||||
[rect.left + rect.width / 2, rect.bottom - 2],
|
||||
]
|
||||
return {
|
||||
outsideSidebar: rect.right > sidebar.right,
|
||||
hits: points.map(([x, y]) => {
|
||||
const hit = document.elementFromPoint(x!, y!)
|
||||
return hit !== null && element.contains(hit)
|
||||
}),
|
||||
}
|
||||
} finally {
|
||||
if (originalStyle === null) element.removeAttribute('style')
|
||||
else element.setAttribute('style', originalStyle)
|
||||
}
|
||||
})
|
||||
if (name === 'Add workspace') expect(bounds.outsideSidebar).toBe(true)
|
||||
expect(bounds.hits).toEqual([true, true, true, true])
|
||||
await page.mouse.move(1000, 400)
|
||||
await tooltip.waitFor({ state: 'detached' })
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
})
|
||||
|
||||
it('interpolates header spacing with the search expansion and collapse', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-sidebar-search-transition'))
|
||||
const header = page.locator('[class*="sectionHeader"]').filter({ has: page.getByRole('button', { name: 'Search sessions', exact: true }) })
|
||||
@@ -368,6 +408,55 @@ describe('web e2e: sidebar search alignment and row action targets', () => {
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
})
|
||||
|
||||
it('spans the first root workspace with its insertion marker during a native drag', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-sidebar-first-workspace-drop'))
|
||||
const directory = await mkdtemp(join(tmpdir(), 'dsh-sidebar-drag-'))
|
||||
try {
|
||||
const extra = await scaffold.ctx.workspaceRegistry.create(directory, 'Sidebar drag workspace')
|
||||
try {
|
||||
const rows = page.locator('[role="tree"][aria-label="Sessions"] [data-row-key^="workspace:"]')
|
||||
await expect.poll(() => rows.count()).toBe(2)
|
||||
const before = await rows.evaluateAll(elements => elements.map(element => element.getAttribute('data-row-key')))
|
||||
await rows.last().hover()
|
||||
const source = await rows.last().boundingBox()
|
||||
const target = await rows.first().boundingBox()
|
||||
if (source === null || target === null) throw new Error('Workspace drag rows have no layout boxes')
|
||||
await page.mouse.move(source.x + 30, source.y + source.height / 2)
|
||||
await page.mouse.down()
|
||||
try {
|
||||
await page.mouse.move(source.x + 40, source.y + source.height / 2, { steps: 3 })
|
||||
await page.mouse.move(target.x + 30, target.y + 2, { steps: 5 })
|
||||
await page.mouse.move(target.x + 31, target.y + 2)
|
||||
const marker = page.locator('[class*="listTopDropIndicator"]')
|
||||
await marker.waitFor({ state: 'attached', timeout: 10_000 })
|
||||
const geometry = await marker.evaluate((element) => {
|
||||
const rect = element.getBoundingClientRect()
|
||||
const tree = element.parentElement!.querySelector('[role="tree"]')!.getBoundingClientRect()
|
||||
const first = element.parentElement!.querySelector('[data-row-key^="workspace:"]')!.getBoundingClientRect()
|
||||
return {
|
||||
width: rect.width, height: rect.height,
|
||||
treeWidth: tree.width, leftInset: rect.left - first.left,
|
||||
rightInset: first.right - rect.right, topOffset: rect.top - first.top,
|
||||
}
|
||||
})
|
||||
expect(geometry.width).toBeGreaterThan(0)
|
||||
expect(geometry.width).toBe(geometry.treeWidth)
|
||||
expect(geometry).toMatchObject({ height: 12, leftInset: 0, rightInset: 0, topOffset: -8 })
|
||||
} finally {
|
||||
await page.mouse.up()
|
||||
}
|
||||
await expect.poll(() => rows.evaluateAll(elements => elements.map(element => element.getAttribute('data-row-key'))))
|
||||
.toEqual(before.toReversed())
|
||||
} finally {
|
||||
await scaffold.ctx.workspaceRegistry.delete(extra.id)
|
||||
}
|
||||
} finally {
|
||||
await rm(directory, { recursive: true, force: true })
|
||||
}
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
expect(tripwire.warnings).toEqual([])
|
||||
})
|
||||
|
||||
it.each(['session', 'workspace'] as const)('retains %s row spacing in the native drag preview', async (kind) => {
|
||||
onTestFailed(() => saveFailureShot(page, `web-e2e-sidebar-${kind}-drag-preview`))
|
||||
const row = kind === 'session'
|
||||
@@ -496,7 +585,7 @@ describe('web e2e: sidebar search alignment and row action targets', () => {
|
||||
await row.getByRole('button', { name: labels[0], exact: true }).waitFor()
|
||||
const metrics = await measureRowActions(row)
|
||||
expect(await row.boundingBox()).toEqual(restingBox)
|
||||
expect(metrics.height).toBe(kind === 'session' ? 32 : 34)
|
||||
expect(metrics.height).toBe(32)
|
||||
expect(metrics.overflows).toBe(false)
|
||||
expect(metrics.actions.map(action => action.label)).toEqual(labels)
|
||||
for (const [index, action] of metrics.actions.entries()) {
|
||||
|
||||
@@ -56,7 +56,7 @@ interface ListMetrics {
|
||||
*/
|
||||
function measureList(page: Page): Promise<ListMetrics> {
|
||||
return page.evaluate(() => {
|
||||
const list = document.querySelector<HTMLElement>('[role="tree"][aria-label="Sessions"]')
|
||||
const list = document.querySelector<HTMLElement>('[data-sidebar-list-area]')
|
||||
if (list === null) throw new Error('sidebar session list not in the DOM')
|
||||
const time = list.querySelector<HTMLElement>('[class*="time"]')
|
||||
if (time === null) throw new Error('no row relative-time element in the sidebar list')
|
||||
@@ -94,8 +94,8 @@ function measureList(page: Page): Promise<ListMetrics> {
|
||||
const pseudoWidth = getComputedStyle(list, '::-webkit-scrollbar').width
|
||||
const barWidth = pseudoWidth === 'auto' ? 15 : Number.parseFloat(pseudoWidth)
|
||||
const listRect = list.getBoundingClientRect()
|
||||
const listParentRight = list.parentElement?.getBoundingClientRect().right
|
||||
if (listParentRight === undefined) throw new Error('sidebar session list has no layout parent')
|
||||
const sidebarEdge = list.closest('[class*="sidebarCol"]')?.getBoundingClientRect().right
|
||||
if (sidebarEdge === undefined) throw new Error('sidebar session list has no layout parent')
|
||||
return {
|
||||
gutter: style.scrollbarGutter,
|
||||
width: pseudoWidth,
|
||||
@@ -107,9 +107,9 @@ function measureList(page: Page): Promise<ListMetrics> {
|
||||
hoverToken: resolve('--dsh-scrollbar-thumb-hover'),
|
||||
overflows: list.scrollHeight > list.clientHeight,
|
||||
band: listRect.width - list.clientWidth,
|
||||
scrollbarEdgeOffset: listParentRight - listRect.right,
|
||||
rowEdgeInset: listParentRight - row.getBoundingClientRect().right,
|
||||
rowPaintInset: listParentRight - row.getBoundingClientRect().right + Number.parseFloat(getComputedStyle(row).borderRightWidth),
|
||||
scrollbarEdgeOffset: sidebarEdge - listRect.right,
|
||||
rowEdgeInset: sidebarEdge - row.getBoundingClientRect().right,
|
||||
rowPaintInset: sidebarEdge - row.getBoundingClientRect().right + Number.parseFloat(getComputedStyle(row).borderRightWidth),
|
||||
clientRight: listRect.left + list.clientWidth,
|
||||
borderRight: listRect.right,
|
||||
timeRight: time.getBoundingClientRect().right,
|
||||
@@ -133,16 +133,16 @@ function measureList(page: Page): Promise<ListMetrics> {
|
||||
*/
|
||||
function measureRowInset(page: Page): Promise<Pick<ListMetrics, 'overflows' | 'rowEdgeInset' | 'rowPaintInset'>> {
|
||||
return page.evaluate(() => {
|
||||
const list = document.querySelector<HTMLElement>('[role="tree"][aria-label="Sessions"]')
|
||||
const list = document.querySelector<HTMLElement>('[data-sidebar-list-area]')
|
||||
if (list === null) throw new Error('sidebar session list not in the DOM')
|
||||
const row = list.querySelector<HTMLElement>('[role="treeitem"]')
|
||||
if (row === null) throw new Error('no row in the sidebar list')
|
||||
const listParentRight = list.parentElement?.getBoundingClientRect().right
|
||||
if (listParentRight === undefined) throw new Error('sidebar session list has no layout parent')
|
||||
const sidebarEdge = list.closest('[class*="sidebarCol"]')?.getBoundingClientRect().right
|
||||
if (sidebarEdge === undefined) throw new Error('sidebar session list has no layout parent')
|
||||
return {
|
||||
overflows: list.scrollHeight > list.clientHeight,
|
||||
rowEdgeInset: listParentRight - row.getBoundingClientRect().right,
|
||||
rowPaintInset: listParentRight - row.getBoundingClientRect().right + Number.parseFloat(getComputedStyle(row).borderRightWidth),
|
||||
rowEdgeInset: sidebarEdge - row.getBoundingClientRect().right,
|
||||
rowPaintInset: sidebarEdge - row.getBoundingClientRect().right + Number.parseFloat(getComputedStyle(row).borderRightWidth),
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -195,9 +195,9 @@ function renderGeometry(light: PaletteMetrics, dark: PaletteMetrics): string {
|
||||
`- --dsh-scrollbar-thumb-hover, pointer over the list: ${metrics.hoverToken}`,
|
||||
`- list overflows: ${String(metrics.overflows)}`,
|
||||
`- reserved band: ${String(metrics.band)}px`,
|
||||
`- scrollbar inset from the list parent edge: ${String(metrics.scrollbarEdgeOffset)}px`,
|
||||
`- row hitbox inset from the list parent edge: ${String(metrics.rowEdgeInset)}px`,
|
||||
`- row background inset from the list parent edge: ${String(metrics.rowPaintInset)}px`,
|
||||
`- scrollbar inset from the sidebar edge: ${String(metrics.scrollbarEdgeOffset)}px`,
|
||||
`- row hitbox inset from the sidebar edge: ${String(metrics.rowEdgeInset)}px`,
|
||||
`- row background inset from the sidebar edge: ${String(metrics.rowPaintInset)}px`,
|
||||
`- relative time covered by the bar: ${String(metrics.timeCoveredBy)}px`,
|
||||
`- relative time ends inside the content area: ${String(metrics.timeRight <= metrics.clientRight)}`,
|
||||
`- content area ends before the border box: ${String(metrics.clientRight < metrics.borderRight)}`,
|
||||
@@ -220,7 +220,7 @@ function renderGeometry(light: PaletteMetrics, dark: PaletteMetrics): string {
|
||||
*/
|
||||
function resolveThumb(page: Page): Promise<string> {
|
||||
return page.evaluate(() => {
|
||||
const list = document.querySelector<HTMLElement>('[role="tree"][aria-label="Sessions"]')
|
||||
const list = document.querySelector<HTMLElement>('[data-sidebar-list-area]')
|
||||
if (list === null) throw new Error('sidebar session list not in the DOM')
|
||||
const probe = document.createElement('span')
|
||||
probe.style.color = 'var(--dsh-scrollbar-thumb)'
|
||||
@@ -243,7 +243,7 @@ const NO_THUMB = 'rgba(0, 0, 0, 0)'
|
||||
* of the viewport (the conversation column).
|
||||
*/
|
||||
async function pointAt(page: Page, where: 'list' | 'away'): Promise<void> {
|
||||
const box = await page.locator('[role="tree"][aria-label="Sessions"]').boundingBox()
|
||||
const box = await page.locator('[data-sidebar-list-area]').boundingBox()
|
||||
if (box === null) throw new Error('sidebar session list has no layout box')
|
||||
const viewport = page.viewportSize()
|
||||
if (viewport === null) throw new Error('page has no viewport')
|
||||
@@ -272,18 +272,63 @@ async function expandSeededSessions(page: Page): Promise<void> {
|
||||
}
|
||||
const showMore = page.getByRole('button', { name: /Show \d+ more sessions/ })
|
||||
if (await bucket.getAttribute('aria-expanded') === 'true'
|
||||
&& await rows.count() <= SEED_COUNT / 2
|
||||
&& await rows.count() < SEED_COUNT
|
||||
&& await showMore.count() > 0) {
|
||||
await showMore.click()
|
||||
}
|
||||
if (await bucket.getAttribute('aria-expanded') === 'true' && await rows.count() > SEED_COUNT / 2) return
|
||||
if (await bucket.getAttribute('aria-expanded') === 'true' && await rows.count() >= SEED_COUNT) return
|
||||
if (Date.now() > deadline) {
|
||||
throw new Error(`Ungrouped bucket never revealed more than ${SEED_COUNT / 2} rows`)
|
||||
throw new Error(`Ungrouped bucket never revealed all ${SEED_COUNT} rows`)
|
||||
}
|
||||
await page.waitForTimeout(200)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Scroll positions of the sidebar viewport and the session list, with the fixed controls around them.
|
||||
* @param page - the page under test.
|
||||
* @returns both scroll offsets and ranges, plus the toolbar, navigation, and fixed-control edges.
|
||||
*/
|
||||
function nestedScrollGeometry(page: Page) {
|
||||
return page.evaluate(() => {
|
||||
const viewport = document.querySelector<HTMLElement>('[data-sidebar-scrollport]')!
|
||||
const list = document.querySelector<HTMLElement>('[data-sidebar-list-area]')!
|
||||
const column = viewport.closest('[class*="sidebarCol"]')!
|
||||
const edge = (selector: string) => column.querySelector(selector)!.getBoundingClientRect()
|
||||
return {
|
||||
viewportTop: viewport.getBoundingClientRect().top,
|
||||
viewportScroll: viewport.scrollTop,
|
||||
viewportRange: viewport.scrollHeight - viewport.clientHeight,
|
||||
listScroll: list.scrollTop,
|
||||
listRange: list.scrollHeight - list.clientHeight,
|
||||
headerTop: edge('[data-sidebar-workspace-header]').top,
|
||||
navBottom: edge('[class*="panelList"]').bottom,
|
||||
buttonTop: edge('button[class*="newSession"]').top,
|
||||
footerTop: edge('[class*="footArea"]').top,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Return both scrollers to their tops.
|
||||
* @param page - the page under test.
|
||||
*/
|
||||
async function resetNestedScroll(page: Page): Promise<void> {
|
||||
await page.locator('[data-sidebar-list-area]').evaluate((el) => { el.scrollTop = 0 })
|
||||
await page.locator('[data-sidebar-scrollport]').evaluate((el) => { el.scrollTop = 0 })
|
||||
}
|
||||
|
||||
/**
|
||||
* Park the pointer at the center of the first element matching `selector`.
|
||||
* @param page - the page under test.
|
||||
* @param selector - the element to point at.
|
||||
*/
|
||||
async function pointInto(page: Page, selector: string): Promise<void> {
|
||||
const box = await page.locator(selector).first().boundingBox()
|
||||
if (box === null) throw new Error(`${selector} has no layout box`)
|
||||
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2)
|
||||
}
|
||||
|
||||
describe('web e2e: sidebar session list scrollbar (reserved gutter / themed thumb)', () => {
|
||||
let scaffold: WebScaffold
|
||||
let browser: Browser
|
||||
@@ -322,6 +367,12 @@ describe('web e2e: sidebar session list scrollbar (reserved gutter / themed thum
|
||||
await expect.poll(async () => (await measureList(page)).overflows, { timeout: 10_000 }).toBe(true)
|
||||
const metrics = await measureList(page)
|
||||
expect(metrics.gutter).toBe('stable')
|
||||
const horizontal = await page.locator('[data-sidebar-scrollport]').evaluate(viewport => ({
|
||||
overflow: getComputedStyle(viewport).overflowX,
|
||||
scrollbarHeight: viewport.getBoundingClientRect().height - viewport.clientHeight,
|
||||
}))
|
||||
expect(horizontal.overflow).toBe('hidden')
|
||||
expect(horizontal.scrollbarHeight).toBe(0)
|
||||
// Pin presence, not width, because the width is platform-dependent.
|
||||
expect(metrics.band).toBeGreaterThan(0)
|
||||
expect(metrics.scrollbarEdgeOffset).toBe(2)
|
||||
@@ -354,7 +405,7 @@ describe('web e2e: sidebar session list scrollbar (reserved gutter / themed thum
|
||||
// leaves the column quiet. This is the one deliberate loss, and
|
||||
// it is pinned here rather than only described, so making a scroll
|
||||
// re-reveal the bar has to be a decision rather than a side effect.
|
||||
await page.locator('[role="tree"][aria-label="Sessions"]').evaluate((el) => { el.scrollTop += 200 })
|
||||
await page.locator('[data-sidebar-list-area]').evaluate((el) => { el.scrollTop += 200 })
|
||||
await page.waitForTimeout(500)
|
||||
expect(await resolveThumb(page)).toBe(NO_THUMB)
|
||||
await pointAt(page, 'list')
|
||||
@@ -362,16 +413,103 @@ describe('web e2e: sidebar session list scrollbar (reserved gutter / themed thum
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
}, 60_000)
|
||||
|
||||
it('keeps the row background inset when overflow disappears', async () => {
|
||||
it('removes both scroll ranges without moving row edges when grouped content fits', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-sidebar-scrollbar-stable-inset'))
|
||||
const viewport = page.locator('[data-sidebar-list-area]')
|
||||
const height = await viewport.evaluate(el => el.scrollHeight)
|
||||
expect(await measureRowInset(page)).toEqual({ overflows: true, rowEdgeInset: 8, rowPaintInset: 11 })
|
||||
const bucket = page.getByText('Ungrouped', { exact: true }).locator('..').locator('..')
|
||||
await bucket.click()
|
||||
try {
|
||||
await expect.poll(async () => (await measureRowInset(page)).overflows, { timeout: 10_000 }).toBe(false)
|
||||
await expect.poll(async () => viewport.evaluate(el => el.scrollHeight)).toBeLessThan(height)
|
||||
await expect.poll(async () => (await measureRowInset(page)).overflows).toBe(false)
|
||||
expect(await measureRowInset(page)).toEqual({ overflows: false, rowEdgeInset: 8, rowPaintInset: 11 })
|
||||
// A section that fits leaves the sidebar viewport nothing to scroll, so the navigation stays.
|
||||
expect((await nestedScrollGeometry(page)).viewportRange).toBe(0)
|
||||
} finally {
|
||||
await expandSeededSessions(page)
|
||||
await viewport.evaluate((el) => { el.scrollTop = 0 })
|
||||
}
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
}, 60_000)
|
||||
|
||||
it('hands one downward wheel event from the navigation to the session list and back', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-sidebar-nested-scroll'))
|
||||
const size = page.viewportSize()!
|
||||
try {
|
||||
await page.setViewportSize({ width: size.width, height: 500 })
|
||||
await resetNestedScroll(page)
|
||||
const before = await nestedScrollGeometry(page)
|
||||
expect(before.viewportRange).toBeGreaterThan(0)
|
||||
expect(before.listRange).toBeGreaterThan(60)
|
||||
expect(before.headerTop).toBeGreaterThan(before.viewportTop)
|
||||
await pointInto(page, '[data-sidebar-list-area] [role="treeitem"]')
|
||||
await page.mouse.wheel(0, before.viewportRange + 60)
|
||||
await expect.poll(async () => (await nestedScrollGeometry(page)).listScroll).toBe(60)
|
||||
const after = await nestedScrollGeometry(page)
|
||||
expect(after.viewportScroll).toBe(before.viewportRange)
|
||||
expect(after.headerTop).toBe(after.viewportTop)
|
||||
expect(after.navBottom).toBeLessThanOrEqual(after.viewportTop)
|
||||
expect(after.buttonTop).toBe(before.buttonTop)
|
||||
expect(after.footerTop).toBe(before.footerTop)
|
||||
await page.mouse.wheel(0, -110)
|
||||
await expect.poll(async () => (await nestedScrollGeometry(page)).viewportScroll).toBe(before.viewportRange - 50)
|
||||
expect((await nestedScrollGeometry(page)).listScroll).toBe(0)
|
||||
} finally {
|
||||
await page.setViewportSize(size)
|
||||
await resetNestedScroll(page)
|
||||
}
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
}, 60_000)
|
||||
|
||||
it('continues a wheel event that starts over the navigation into the session list', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-sidebar-nested-scroll-navigation'))
|
||||
const size = page.viewportSize()!
|
||||
try {
|
||||
await page.setViewportSize({ width: size.width, height: 500 })
|
||||
await resetNestedScroll(page)
|
||||
const before = await nestedScrollGeometry(page)
|
||||
await pointInto(page, '[data-sidebar-scrollport] [class*="panelList"]')
|
||||
await page.mouse.wheel(0, before.viewportRange + 30)
|
||||
await expect.poll(async () => (await nestedScrollGeometry(page)).listScroll).toBe(30)
|
||||
expect((await nestedScrollGeometry(page)).viewportScroll).toBe(before.viewportRange)
|
||||
} finally {
|
||||
await page.setViewportSize(size)
|
||||
await resetNestedScroll(page)
|
||||
}
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
}, 60_000)
|
||||
|
||||
it('keeps the toolbar at the top through empty search results', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-sidebar-nested-scroll-search'))
|
||||
const header = page.locator('[data-sidebar-workspace-header]')
|
||||
const input = header.locator('input')
|
||||
const size = page.viewportSize()!
|
||||
try {
|
||||
await page.setViewportSize({ width: size.width, height: 500 })
|
||||
await resetNestedScroll(page)
|
||||
const range = (await nestedScrollGeometry(page)).viewportRange
|
||||
await pointInto(page, '[data-sidebar-list-area] [role="treeitem"]')
|
||||
await page.mouse.wheel(0, range)
|
||||
await expect.poll(async () => {
|
||||
const geometry = await nestedScrollGeometry(page)
|
||||
return geometry.headerTop === geometry.viewportTop
|
||||
}).toBe(true)
|
||||
const searchButton = await page.getByRole('button', { name: 'Search sessions', exact: true }).boundingBox()
|
||||
if (searchButton === null) throw new Error('Search sessions has no layout box')
|
||||
await page.mouse.click(searchButton.x + searchButton.width / 2, searchButton.y + searchButton.height / 2)
|
||||
expect(await input.evaluate(el => el === document.activeElement)).toBe(true)
|
||||
await input.fill('no-sidebar-scroll-matches-9f4c')
|
||||
await page.locator('[class*="searchTree"]').locator('..').locator('[class*="empty"]').waitFor()
|
||||
const filtered = await nestedScrollGeometry(page)
|
||||
expect(filtered.viewportScroll).toBe(range)
|
||||
expect(filtered.headerTop).toBe(filtered.viewportTop)
|
||||
expect((await header.boundingBox())?.height).toBe(36)
|
||||
} finally {
|
||||
if (await input.isVisible()) { await input.fill(''); await input.press('Escape') }
|
||||
await page.setViewportSize(size)
|
||||
await expandSeededSessions(page)
|
||||
await resetNestedScroll(page)
|
||||
}
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
}, 60_000)
|
||||
|
||||
@@ -706,6 +706,8 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY || notReady.length > 0)('web smoke
|
||||
let browser: Browser
|
||||
let page: Page
|
||||
const pageErrors: string[] = []
|
||||
// The product boot only warns about inactive optional entries; this lane requires the whole composition.
|
||||
const bootWarnings: string[] = []
|
||||
|
||||
beforeAll(async () => {
|
||||
requireDist()
|
||||
@@ -742,6 +744,9 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY || notReady.length > 0)('web smoke
|
||||
browser = await chromium.launch()
|
||||
page = await newEnglishPage(browser)
|
||||
page.on('pageerror', e => pageErrors.push(String(e)))
|
||||
page.on('console', (message) => {
|
||||
if (message.text().startsWith('web boot: warning')) bootWarnings.push(message.text())
|
||||
})
|
||||
await page.goto(baseUrl, { waitUntil: 'load' })
|
||||
}, 120_000)
|
||||
|
||||
@@ -760,6 +765,7 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY || notReady.length > 0)('web smoke
|
||||
onTestFailed(() => saveFailureShot(page, 'w5-cold-start'))
|
||||
await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
|
||||
expect(await page.locator('text=Failed to load plugins').count()).toBe(0)
|
||||
expect(bootWarnings).toEqual([])
|
||||
const template = await page.locator('[class*="frame"]').evaluate(el => getComputedStyle(el).gridTemplateColumns)
|
||||
expect(template.split(' ').length).toBe(3)
|
||||
await screen(page, '01-cold-start')
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/** Git Worktrees opt-in through the shipped plugin manager, profile, and Loader. */
|
||||
/** Unified review and worktree opt-in through the shipped plugin manager, profile, and Loader. */
|
||||
|
||||
import { execFile } from 'node:child_process'
|
||||
import { readFile, writeFile } from 'node:fs/promises'
|
||||
@@ -6,16 +6,19 @@ import { join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { promisify } from 'node:util'
|
||||
import { FiberState } from '@deepseek-ai/cordis'
|
||||
import { ToolCallId } from '@deepseek-ai/dsh-llm'
|
||||
import type {} from '@deepseek-ai/dsh-experimental-worktree'
|
||||
import type {} from '@deepseek-ai/dsh-experimental-client-ui-forge'
|
||||
import { chromium } from 'playwright'
|
||||
import { expect, it, onTestFailed, onTestFinished } from 'vitest'
|
||||
import { launchWebScaffold, watchConsole } from './scaffold.ts'
|
||||
import { connectFreshWorkspace, newEnglishPage, PLUGIN_TOGGLE_SETTLE_MS, saveFailureShot } from './support.ts'
|
||||
|
||||
const BUNDLE = '@deepseek-ai/dsh-experimental-tool-worktree'
|
||||
const BUNDLE = '@deepseek-ai/dsh-experimental-forge-bundle'
|
||||
const STANDALONE = '@deepseek-ai/dsh-experimental-tool-worktree'
|
||||
const exec = promisify(execFile)
|
||||
|
||||
it('persists the Git Worktrees switch and removes its capability while retaining checkouts', async () => {
|
||||
it('persists the unified review and worktree switch and retains checkouts after disabling', async () => {
|
||||
const scaffold = await launchWebScaffold({
|
||||
extraOverlayPath: fileURLToPath(new URL('./pin-browse-picker.overlay.yml', import.meta.url)),
|
||||
})
|
||||
@@ -70,13 +73,15 @@ it('persists the Git Worktrees switch and removes its capability while retaining
|
||||
}
|
||||
let panel = await openPlugins()
|
||||
let card = panel.locator(`[data-plugin-group="more"] [data-plugin-package="${BUNDLE}"]`)
|
||||
await card.getByRole('button', { name: 'View Git worktrees', exact: true }).waitFor()
|
||||
await card.getByRole('button', { name: 'View Change requests & worktrees', exact: true }).waitFor()
|
||||
expect(await panel.locator(`[data-plugin-package="${STANDALONE}"]`).count()).toBe(0)
|
||||
expect(await card.getByText('Experimental', { exact: true }).count()).toBe(1)
|
||||
let toggle = card.getByRole('switch', { name: 'Enable Git worktrees', exact: true })
|
||||
let toggle = card.getByRole('switch', { name: 'Enable Change requests & worktrees', exact: true })
|
||||
expect(await toggle.getAttribute('aria-checked')).toBe('false')
|
||||
expect(await selectedBundles()).not.toContain(BUNDLE)
|
||||
expect(rows()).toEqual([])
|
||||
expect(scaffold.ctx.get('worktrees')).toBeUndefined()
|
||||
expect(scaffold.ctx.get('forgePanel')).toBeUndefined()
|
||||
expect(tools()).toContain('working_directory')
|
||||
expect(tools()).not.toContain('create_worktree')
|
||||
|
||||
@@ -85,14 +90,21 @@ it('persists the Git Worktrees switch and removes its capability while retaining
|
||||
await expect.poll(() => toggle.getAttribute('aria-checked'), { timeout: PLUGIN_TOGGLE_SETTLE_MS }).toBe('true')
|
||||
expect(await selectedBundles()).toContain(BUNDLE)
|
||||
expect(tools()).toContain('create_worktree')
|
||||
const created = await scaffold.ctx.worktrees.create(agent, { name: 'gui-opt-in' })
|
||||
expect(scaffold.ctx.get('forgePanel')).toBeDefined()
|
||||
const created = { path: join(repository, '.agents/worktrees/gui-opt-in'), baseCommit: (await git('rev-parse', 'HEAD')).stdout.trim() }
|
||||
const result = await scaffold.ctx.tools.execute({
|
||||
agent, signal: new AbortController().signal,
|
||||
calls: [{ type: 'tool-call', id: ToolCallId('gui-opt-in'), name: 'create_worktree', arguments: JSON.stringify({ name: 'gui-opt-in' }) }],
|
||||
})
|
||||
expect(result.results[0]?.result.isError).not.toBe(true)
|
||||
expect(result.results[0]?.result.value).toEqual({ ...created, branch: 'gui-opt-in', repositoryRoot: repository })
|
||||
expect(await readFile(join(created.path, 'retained.txt'), 'utf8')).toBe('retained checkout\n')
|
||||
expect(scaffold.ctx.workingDirectory.get(agent.session)).toBe(created.path)
|
||||
|
||||
await page.reload({ waitUntil: 'load' })
|
||||
panel = await openPlugins()
|
||||
card = panel.locator(`[data-plugin-group="more"] [data-plugin-package="${BUNDLE}"]`)
|
||||
toggle = card.getByRole('switch', { name: 'Enable Git worktrees', exact: true })
|
||||
toggle = card.getByRole('switch', { name: 'Enable Change requests & worktrees', exact: true })
|
||||
await expect.poll(() => toggle.getAttribute('aria-checked')).toBe('true')
|
||||
expect(tools()).toContain('create_worktree')
|
||||
await toggle.click()
|
||||
@@ -100,6 +112,7 @@ it('persists the Git Worktrees switch and removes its capability while retaining
|
||||
await expect.poll(() => toggle.getAttribute('aria-checked'), { timeout: PLUGIN_TOGGLE_SETTLE_MS }).toBe('false')
|
||||
expect(await selectedBundles()).not.toContain(BUNDLE)
|
||||
expect(scaffold.ctx.get('worktrees')).toBeUndefined()
|
||||
expect(scaffold.ctx.get('forgePanel')).toBeUndefined()
|
||||
expect(tools()).not.toContain('create_worktree')
|
||||
expect(tools()).toContain('working_directory')
|
||||
expect(await readFile(join(created.path, 'retained.txt'), 'utf8')).toBe('retained checkout\n')
|
||||
@@ -108,7 +121,7 @@ it('persists the Git Worktrees switch and removes its capability while retaining
|
||||
|
||||
await page.reload({ waitUntil: 'load' })
|
||||
panel = await openPlugins()
|
||||
await expect.poll(() => panel.getByRole('switch', { name: 'Enable Git worktrees', exact: true })
|
||||
await expect.poll(() => panel.getByRole('switch', { name: 'Enable Change requests & worktrees', exact: true })
|
||||
.getAttribute('aria-checked')).toBe('false')
|
||||
expect(tools()).not.toContain('create_worktree')
|
||||
expect(await selectedBundles()).not.toContain(BUNDLE)
|
||||
|
||||
@@ -23,7 +23,6 @@
|
||||
// this client-registered project must not also hold them (one program
|
||||
// cannot see both sides of the cordis Context merges).
|
||||
"exclude": [
|
||||
"tests/credential-editor.e2e.ts",
|
||||
"tests/sidebar-drawer.e2e.ts",
|
||||
"tests/creation-recommendations.e2e.ts",
|
||||
"tests/plugin-recommendations.e2e.ts",
|
||||
|
||||
@@ -92,6 +92,25 @@ async function worker<Report>(args: readonly string[], semiSpaceMb?: number): Pr
|
||||
return run.report
|
||||
}
|
||||
|
||||
/** Compare adjacent scaling samples before aggregating, so unrelated fastest rounds cannot set the ratio. */
|
||||
function doublingRatio(half: readonly number[], full: readonly number[]): number {
|
||||
const ratios = half.map((duration, index) => full[index]! / duration).sort((a, b) => a - b)
|
||||
return ratios[Math.floor(ratios.length / 2)]!
|
||||
}
|
||||
|
||||
describe('credential-matcher scaling measurement', () => {
|
||||
it('compares adjacent rounds when the runner changes speed', () => {
|
||||
const half = [1, 2, 2, 2, 2]
|
||||
const full = [4, 4, 4, 4, 4]
|
||||
expect(Math.min(...full) / Math.min(...half)).toBeGreaterThan(MAX_DOUBLING_SCALING)
|
||||
expect(doublingRatio(half, full)).toBe(2)
|
||||
})
|
||||
|
||||
it('rejects superlinear growth even when one round is faster', () => {
|
||||
expect(doublingRatio([1, 2, 2, 2, 2], [2, 6, 6, 6, 6])).toBeGreaterThan(MAX_DOUBLING_SCALING)
|
||||
})
|
||||
})
|
||||
|
||||
describe('sent user text with the credential matcher', () => {
|
||||
let timings: ReadonlyMap<string, UserTextWorkloadTiming>
|
||||
let allocations: ReadonlyMap<string, UserTextWorkloadAllocation>
|
||||
@@ -115,6 +134,11 @@ describe('sent user text with the credential matcher', () => {
|
||||
measures: `${benchmark.measures} Minimum of ${ATTEMPTS} per-sample means of ${measured.evaluationsPerSample} fresh evaluations after warm-up.`,
|
||||
}, { withSecretMs: { ms: measured.withSecretMs, cpuMs: measured.withSecretCpuMs } }, { withSecretMs: budgetMs })
|
||||
}
|
||||
const expectLinear = (half: UserTextWorkloadTiming, full: UserTextWorkloadTiming): void => {
|
||||
const ratio = doublingRatio(half.samples.map(sample => sample.withSecretMs), full.samples.map(sample => sample.withSecretMs))
|
||||
console.log(JSON.stringify({ benchmark: `user-text/${full.id}/scaling`, ratio, budget: MAX_DOUBLING_SCALING }))
|
||||
expect(ratio).toBeLessThanOrEqual(MAX_DOUBLING_SCALING)
|
||||
}
|
||||
|
||||
it.each(Object.entries(UNMATCHED_EXPECTED))('evaluates %s without credentials within its budget and ratio', (id, expected) => {
|
||||
const measured = timing(id)
|
||||
@@ -139,7 +163,7 @@ describe('sent user text with the credential matcher', () => {
|
||||
const half = timing('env-keys-1000')
|
||||
const full = timing('env-keys-2000')
|
||||
expect([half.secrets, full.secrets]).toEqual([1_000, 2_000])
|
||||
expect(full.withSecretMs / half.withSecretMs).toBeLessThanOrEqual(MAX_DOUBLING_SCALING)
|
||||
expectLinear(half, full)
|
||||
})
|
||||
|
||||
it.each(ADVERSARIAL)('scans adversarial %s input linearly without backtracking', (name) => {
|
||||
@@ -148,7 +172,7 @@ describe('sent user text with the credential matcher', () => {
|
||||
const budgetMs = ciTimeBudget(ADVERSARIAL_EXPECTED_MS)
|
||||
record(benchmarkCase(`adversarial-${name}`, `One 128 KiB document repeating a ${name} unit.`), full, budgetMs)
|
||||
expect(full.withSecretMs).toBeLessThanOrEqual(budgetMs)
|
||||
expect(full.withSecretMs / half.withSecretMs).toBeLessThanOrEqual(MAX_DOUBLING_SCALING)
|
||||
expectLinear(half, full)
|
||||
})
|
||||
|
||||
it.each(workloadIds())('allocates only result nodes and masks for %s', (id) => {
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
en: 41d772192075e133
|
||||
zh: 5ef3da69b4de1d8a
|
||||
/capability-seams-and-core-services:
|
||||
en: 450fff6fec93a249
|
||||
zh: 6618c874955091b5
|
||||
en: a8d9e569b86c1cf4
|
||||
zh: fa9b276a0e8c8222
|
||||
|
||||
@@ -701,7 +701,7 @@ flowchart LR
|
||||
| `ctx.configEditor` | `core` | [`config-editor`](../packages/boot/config-editor) | - | [`settings`](../packages/settings/settings), [`agent-default-model`](../packages/core/agent-default-model) | - | Persists profile config patches under the application file lock and HMR queue, then reconciles Loader entries. |
|
||||
| `ctx.settings` | `core` | [`settings`](../packages/settings/settings) | - | [`api-settings-controller`](../packages/api/settings-controller) | - | Forms project volatile Config fields from active profile entries and delegate validated edits to config-editor. Plugins consume their own Config references. |
|
||||
| `ctx.subagentModelSelection` | `core` | [`tool-subagent`](../packages/subagent/tool-subagent) | - | [`tool-subagent`](../packages/subagent/tool-subagent) | - | Owns the default-off settings namespace that Agent-scoped delegation tools sample when composing a new top-level Session. |
|
||||
| `ctx.credentials` | `seam` | [`credentials`](../packages/credentials/credentials) | [`credentials-local`](../packages/credentials/credentials-local) | [`api-settings-controller`](../packages/api/settings-controller), [`llm-deepseek`](../packages/llm/llm-deepseek), [`llm-pi-ai`](../packages/llm/llm-pi-ai) | - | Configuration carries references to secrets; providers own the values. Consumers resolve per operation, so a rotated credential reaches the very next request; the settings controller exposes value-free metadata, writes, and owner-only reads of stored keys. |
|
||||
| `ctx.credentials` | `seam` | [`credentials`](../packages/credentials/credentials) | [`credentials-local`](../packages/credentials/credentials-local) | [`api-settings-controller`](../packages/api/settings-controller), [`llm-deepseek`](../packages/llm/llm-deepseek), [`llm-pi-ai`](../packages/llm/llm-pi-ai) | - | Configuration carries references to secrets; providers own the values. Consumers resolve per operation, so a rotated credential reaches the very next request; the settings controller exposes value-free views and write-only storage. |
|
||||
| `ctx.deepseekAccount` | `seam` | [`deepseek-account`](../packages/credentials/deepseek-account) | [`deepseek-account-platform`](../packages/credentials/deepseek-account-platform) | [`api-account-controller`](../packages/api/account-controller), [`llm-deepseek`](../packages/llm/llm-deepseek) | - | The Host owns browser authorization and local credentials; UI consumers receive state without tokens. |
|
||||
| `ctx.authorization` | `seam` | [`authorization`](../packages/credentials/authorization) | - | [`llm-pi-ai`](../packages/llm/llm-pi-ai) | - | Flows are registered by the plugin that knows how to obtain one credential and keyed by the record they write; the seam owns the conversation and the one-attempt-per-key lifecycle, never the protocol. |
|
||||
| `ctx.productAnalytics` | `service` | [`client-product-analytics`](../packages/client/product-analytics) | - | - | - | Accepts selected Desktop events, enriches available login identity, and observes live compaction under the live Host collection policy. |
|
||||
|
||||
@@ -703,7 +703,7 @@ flowchart LR
|
||||
| `ctx.configEditor` | `core` | [`config-editor`](../packages/boot/config-editor) | - | [`settings`](../packages/settings/settings), [`agent-default-model`](../packages/core/agent-default-model) | - | Persists profile config patches under the application file lock and HMR queue, then reconciles Loader entries. |
|
||||
| `ctx.settings` | `core` | [`settings`](../packages/settings/settings) | - | [`api-settings-controller`](../packages/api/settings-controller) | - | Forms project volatile Config fields from active profile entries and delegate validated edits to config-editor. Plugins consume their own Config references. |
|
||||
| `ctx.subagentModelSelection` | `core` | [`tool-subagent`](../packages/subagent/tool-subagent) | - | [`tool-subagent`](../packages/subagent/tool-subagent) | - | 拥有默认关闭的设置命名空间;Agent 作用域的委派工具会在组合新顶层 Session 时读取它。 |
|
||||
| `ctx.credentials` | `seam` | [`credentials`](../packages/credentials/credentials) | [`credentials-local`](../packages/credentials/credentials-local) | [`api-settings-controller`](../packages/api/settings-controller), [`llm-deepseek`](../packages/llm/llm-deepseek), [`llm-pi-ai`](../packages/llm/llm-pi-ai) | - | 配置携带对机密信息的引用;提供方拥有实际值。消费方按操作解析,因此轮换后的凭据会在紧接着的下一次请求中生效;settings controller 提供不含实际值的元数据和写入操作,并仅向所有者读取已保存密钥。 |
|
||||
| `ctx.credentials` | `seam` | [`credentials`](../packages/credentials/credentials) | [`credentials-local`](../packages/credentials/credentials-local) | [`api-settings-controller`](../packages/api/settings-controller), [`llm-deepseek`](../packages/llm/llm-deepseek), [`llm-pi-ai`](../packages/llm/llm-pi-ai) | - | 配置携带对机密信息的引用;提供方拥有实际值。消费方按操作解析,因此轮换后的凭据会在紧接着的下一次请求中生效;settings controller 提供不含实际值的视图和只写存储。 |
|
||||
| `ctx.deepseekAccount` | `seam` | [`deepseek-account`](../packages/credentials/deepseek-account) | [`deepseek-account-platform`](../packages/credentials/deepseek-account-platform) | [`api-account-controller`](../packages/api/account-controller), [`llm-deepseek`](../packages/llm/llm-deepseek) | - | Host 负责浏览器授权和本地凭证;UI 使用方只接收不含 token 的状态。 |
|
||||
| `ctx.authorization` | `seam` | [`authorization`](../packages/credentials/authorization) | - | [`llm-pi-ai`](../packages/llm/llm-pi-ai) | - | flow 由知道如何取得某份凭据的插件注册,并以其写入的记录为键;seam 拥有这段对话与"每个键同时只跑一次尝试"的生命周期,而非协议本身。 |
|
||||
| `ctx.productAnalytics` | `service` | [`client-product-analytics`](../packages/client/product-analytics) | - | - | - | 根据 Host 动态采集策略接收指定桌面事件、补充可用登录身份并监听实时压缩。 |
|
||||
|
||||
@@ -4511,9 +4511,9 @@ export interface Config {
|
||||
search?: boolean
|
||||
/** Register `web_fetch`. Defaults to true. */
|
||||
fetch?: boolean
|
||||
/** Upper bound on sources returned by one `web_search` call. */
|
||||
/** Upper bound on sources returned by one `web_search` call. Defaults to 10. */
|
||||
searchMaxResults?: number
|
||||
/** Upper bound on queries accepted by one `web_search` call. */
|
||||
/** Upper bound on queries accepted by one `web_search` call. Defaults to 10. */
|
||||
searchMaxQueries?: number
|
||||
/** Cap on each source summary in `web_search` output, in characters. Defaults to 800. */
|
||||
searchSummaryMaxChars?: number
|
||||
@@ -4693,7 +4693,7 @@ export interface Config {
|
||||
|
||||
- `inject`: `web`
|
||||
- `refs`: `Volatile` (`@deepseek-ai/cordis`)
|
||||
- `source`: [`packages/web/web-deepseek-api/src/index.ts:36`](../packages/web/web-deepseek-api/src/index.ts)
|
||||
- `source`: [`packages/web/web-deepseek-api/src/index.ts:38`](../packages/web/web-deepseek-api/src/index.ts)
|
||||
|
||||
```ts config-catalog
|
||||
/** Settings read by the next search or fetch without re-registering either provider. */
|
||||
@@ -4706,6 +4706,12 @@ export interface Config {
|
||||
baseURL: Volatile<string>
|
||||
/** Search API results per search, from 1 through 50; a smaller request bound wins. Defaults to 10. */
|
||||
count: Volatile<number>
|
||||
/**
|
||||
* Most Search requests this plugin instance sends in any one-second window; later searches wait,
|
||||
* and the wait counts against the caller's timeout. Other processes and clients on the same account
|
||||
* share the account limit without coordination. Defaults to 5.
|
||||
*/
|
||||
searchRequestsPerSecond: Volatile<number>
|
||||
/** Send the triggering user question, bounded to 128 characters, as `ori_question`. Defaults to true. */
|
||||
sendOriginalQuestion: Volatile<boolean>
|
||||
}
|
||||
|
||||
@@ -4513,9 +4513,9 @@ export interface Config {
|
||||
search?: boolean
|
||||
/** Register `web_fetch`. Defaults to true. */
|
||||
fetch?: boolean
|
||||
/** Upper bound on sources returned by one `web_search` call. */
|
||||
/** Upper bound on sources returned by one `web_search` call. Defaults to 10. */
|
||||
searchMaxResults?: number
|
||||
/** Upper bound on queries accepted by one `web_search` call. */
|
||||
/** Upper bound on queries accepted by one `web_search` call. Defaults to 10. */
|
||||
searchMaxQueries?: number
|
||||
/** Cap on each source summary in `web_search` output, in characters. Defaults to 800. */
|
||||
searchSummaryMaxChars?: number
|
||||
@@ -4695,7 +4695,7 @@ export interface Config {
|
||||
|
||||
- `inject`: `web`
|
||||
- `refs`: `Volatile` (`@deepseek-ai/cordis`)
|
||||
- `source`: [`packages/web/web-deepseek-api/src/index.ts:36`](../packages/web/web-deepseek-api/src/index.ts)
|
||||
- `source`: [`packages/web/web-deepseek-api/src/index.ts:38`](../packages/web/web-deepseek-api/src/index.ts)
|
||||
|
||||
```ts config-catalog
|
||||
/** Settings read by the next search or fetch without re-registering either provider. */
|
||||
@@ -4708,6 +4708,12 @@ export interface Config {
|
||||
baseURL: Volatile<string>
|
||||
/** Search API results per search, from 1 through 50; a smaller request bound wins. Defaults to 10. */
|
||||
count: Volatile<number>
|
||||
/**
|
||||
* Most Search requests this plugin instance sends in any one-second window; later searches wait,
|
||||
* and the wait counts against the caller's timeout. Other processes and clients on the same account
|
||||
* share the account limit without coordination. Defaults to 5.
|
||||
*/
|
||||
searchRequestsPerSecond: Volatile<number>
|
||||
/** Send the triggering user question, bounded to 128 characters, as `ori_question`. Defaults to true. */
|
||||
sendOriginalQuestion: Volatile<boolean>
|
||||
}
|
||||
|
||||
@@ -23,11 +23,11 @@ Application listeners are outside this package matrix. Desktop Host subscribes t
|
||||
| `agent/request-error` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:389`](../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:309`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`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:417`](../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:700`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `api-session/added` | `emit` | [`packages/api/session-controller/src/types.ts:680`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `api-session/error` | `emit` | [`packages/api/session-controller/src/types.ts:707`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `api-session/removed` | `emit` | [`packages/api/session-controller/src/types.ts:686`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `api-session/status` | `emit` | [`packages/api/session-controller/src/types.ts:693`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `api-session/activity` | `emit` | [`packages/api/session-controller/src/types.ts:701`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `api-session/added` | `emit` | [`packages/api/session-controller/src/types.ts:681`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `api-session/error` | `emit` | [`packages/api/session-controller/src/types.ts:708`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `api-session/removed` | `emit` | [`packages/api/session-controller/src/types.ts:687`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `api-session/status` | `emit` | [`packages/api/session-controller/src/types.ts:694`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `app-boot/config-reload` | `emit` | [`packages/boot/app-boot/src/index.ts:55`](../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`, `server` |
|
||||
| `authorization/settled` | `emit` | [`packages/credentials/authorization/src/index.ts:57`](../packages/credentials/authorization/src/index.ts) | [`authorization`](../packages/credentials/authorization) (`events.dispatch`) | - |
|
||||
|
||||
@@ -25,11 +25,11 @@
|
||||
| `agent/request-error` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:389`](../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:309`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`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:417`](../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:700`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `api-session/added` | `emit` | [`packages/api/session-controller/src/types.ts:680`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `api-session/error` | `emit` | [`packages/api/session-controller/src/types.ts:707`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `api-session/removed` | `emit` | [`packages/api/session-controller/src/types.ts:686`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `api-session/status` | `emit` | [`packages/api/session-controller/src/types.ts:693`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `api-session/activity` | `emit` | [`packages/api/session-controller/src/types.ts:701`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `api-session/added` | `emit` | [`packages/api/session-controller/src/types.ts:681`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `api-session/error` | `emit` | [`packages/api/session-controller/src/types.ts:708`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `api-session/removed` | `emit` | [`packages/api/session-controller/src/types.ts:687`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `api-session/status` | `emit` | [`packages/api/session-controller/src/types.ts:694`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `app-boot/config-reload` | `emit` | [`packages/boot/app-boot/src/index.ts:55`](../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`, `server` |
|
||||
| `authorization/settled` | `emit` | [`packages/credentials/authorization/src/index.ts:57`](../packages/credentials/authorization/src/index.ts) | [`authorization`](../packages/credentials/authorization) (`events.dispatch`) | - |
|
||||
|
||||
@@ -1362,8 +1362,8 @@
|
||||
en: 4147b170f52814af
|
||||
zh: e78991ac1c41ebec
|
||||
/session-persistence-event-catalog/resolved-persistence-types/string:
|
||||
en: c178efefdf01cade
|
||||
zh: 200b1ab00ed8bfd3
|
||||
en: caa52cd38cec3aee
|
||||
zh: 3ff0f1fceaac2eb1
|
||||
/session-persistence-event-catalog/resolved-persistence-types/string~2:
|
||||
en: 0089620bf4638263
|
||||
zh: 1922bef1713094bb
|
||||
@@ -1884,8 +1884,8 @@
|
||||
en: 4e102391d685eff5
|
||||
zh: 9762243a13014f3d
|
||||
/session-persistence-event-catalog/resolved-persistence-types/kind-user:
|
||||
en: 984cac7296874653
|
||||
zh: 13b4ee707a8ce305
|
||||
en: eaecebdbcd5cc4e4
|
||||
zh: f519d7ade666f48a
|
||||
/session-persistence-event-catalog/resolved-persistence-types/kind-user-approval:
|
||||
en: a52d2dfbaf844127
|
||||
zh: 4c5306ca40f20873
|
||||
|
||||
@@ -5639,7 +5639,7 @@ Array of `number`.
|
||||
|
||||
SHA-256: `4cd48f3e5108bd6ebedf7301c3638839b8b2b9db8dbd493974177fbdeb91a01b`
|
||||
|
||||
Sources: [`packages/api/session-controller/src/types.ts:388`](../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/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/team/agent-team/src/types.ts:20`](../packages/team/agent-team/src/types.ts) · [`packages/team/agent-team/src/types.ts:32`](../packages/team/agent-team/src/types.ts) · [`packages/team/agent-team/src/types.ts:8`](../packages/team/agent-team/src/types.ts) · [`packages/workflow/workflow/src/types.ts:13`](../packages/workflow/workflow/src/types.ts)
|
||||
Sources: [`packages/api/session-controller/src/types.ts:389`](../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/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/team/agent-team/src/types.ts:20`](../packages/team/agent-team/src/types.ts) · [`packages/team/agent-team/src/types.ts:32`](../packages/team/agent-team/src/types.ts) · [`packages/team/agent-team/src/types.ts:8`](../packages/team/agent-team/src/types.ts) · [`packages/workflow/workflow/src/types.ts:13`](../packages/workflow/workflow/src/types.ts)
|
||||
|
||||
`string`
|
||||
|
||||
@@ -7759,7 +7759,7 @@ Sources: [`packages/core/tools/src/index.ts:28`](../packages/core/tools/src/inde
|
||||
|
||||
SHA-256: `a4afec59b75aad5d13ef82d6931f35cc2d98918503916cf069355b77c5e42302`
|
||||
|
||||
Sources: [`packages/api/session-controller/src/types.ts:397`](../packages/api/session-controller/src/types.ts)
|
||||
Sources: [`packages/api/session-controller/src/types.ts:398`](../packages/api/session-controller/src/types.ts)
|
||||
|
||||
| Property | Presence | Type |
|
||||
|---|---|---|
|
||||
|
||||
@@ -5641,7 +5641,7 @@ SHA-256: `5d03ba38734809bcbd2a55221bd938b3b3fc34b49bb686c6113e9e4931e3aa78`
|
||||
|
||||
SHA-256: `4cd48f3e5108bd6ebedf7301c3638839b8b2b9db8dbd493974177fbdeb91a01b`
|
||||
|
||||
来源:[`packages/api/session-controller/src/types.ts:388`](../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/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/team/agent-team/src/types.ts:20`](../packages/team/agent-team/src/types.ts) · [`packages/team/agent-team/src/types.ts:32`](../packages/team/agent-team/src/types.ts) · [`packages/team/agent-team/src/types.ts:8`](../packages/team/agent-team/src/types.ts) · [`packages/workflow/workflow/src/types.ts:13`](../packages/workflow/workflow/src/types.ts)
|
||||
来源:[`packages/api/session-controller/src/types.ts:389`](../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/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/team/agent-team/src/types.ts:20`](../packages/team/agent-team/src/types.ts) · [`packages/team/agent-team/src/types.ts:32`](../packages/team/agent-team/src/types.ts) · [`packages/team/agent-team/src/types.ts:8`](../packages/team/agent-team/src/types.ts) · [`packages/workflow/workflow/src/types.ts:13`](../packages/workflow/workflow/src/types.ts)
|
||||
|
||||
`string`
|
||||
|
||||
@@ -7761,7 +7761,7 @@ SHA-256: `8fcb0b44a575e998665c053643d1ab04c82f39253ebd190ac0c975b78e40b543`
|
||||
|
||||
SHA-256: `a4afec59b75aad5d13ef82d6931f35cc2d98918503916cf069355b77c5e42302`
|
||||
|
||||
来源:[`packages/api/session-controller/src/types.ts:397`](../packages/api/session-controller/src/types.ts)
|
||||
来源:[`packages/api/session-controller/src/types.ts:398`](../packages/api/session-controller/src/types.ts)
|
||||
|
||||
| 属性 | 存在性 | 类型 |
|
||||
|---|---|---|
|
||||
|
||||
Generated
+2
-2
@@ -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:198"]},
|
||||
{"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/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/team/agent-team/src/types.ts#TeamId","packages/team/agent-team/src/types.ts#TeamMessageId","packages/team/agent-team/src/types.ts#TeamTaskId","packages/workflow/workflow/src/types.ts#WorkflowRunId"],"sources":["packages/api/session-controller/src/types.ts:388","packages/attachment/attachment/src/brand.ts:6","packages/compaction/compaction/src/brand.ts:4","packages/core/session/src/types.ts:21","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/team/agent-team/src/types.ts:20","packages/team/agent-team/src/types.ts:32","packages/team/agent-team/src/types.ts:8","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/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/team/agent-team/src/types.ts#TeamId","packages/team/agent-team/src/types.ts#TeamMessageId","packages/team/agent-team/src/types.ts#TeamTaskId","packages/workflow/workflow/src/types.ts#WorkflowRunId"],"sources":["packages/api/session-controller/src/types.ts:389","packages/attachment/attachment/src/brand.ts:6","packages/compaction/compaction/src/brand.ts:4","packages/core/session/src/types.ts:21","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/team/agent-team/src/types.ts:20","packages/team/agent-team/src/types.ts:32","packages/team/agent-team/src/types.ts:8","packages/workflow/workflow/src/types.ts:13"]},
|
||||
{"digest":"4d60dddaa65d3da8d4cd014d85ce3d2c2875664dd0433e331d8e9d395289686b","names":["packages/experimental/liveshare/src/host/viewer-principal.ts#ShareViewerPrincipal"],"sources":["packages/experimental/liveshare/src/host/viewer-principal.ts:24"]},
|
||||
{"digest":"4e011753f8263f436efaabfbb3b4db53a98fc55342c5ded2daf7208b217be159","names":[],"sources":[]},
|
||||
{"digest":"4e44c9118152f6829634bfd508e47669ab0497caffa380c94b4be91f9390d35d","names":[],"sources":[]},
|
||||
@@ -474,7 +474,7 @@
|
||||
{"digest":"a3c6f1ea38e9a61d7a9706057ba65aabe02dfe6d9dd90d4736aa62325d874531","names":[],"sources":[]},
|
||||
{"digest":"a3edd1efd1fef21b72c55e213c8a8b24196b091abde2b89db52be0b2aaae3cfa","names":[],"sources":[]},
|
||||
{"digest":"a40d12070f6f4a124f32fb3cc86e7857554702e32f0eda3080a25c4b9ac9b18c","names":[],"sources":["packages/team/agent-team/src/types.ts:175"]},
|
||||
{"digest":"a4afec59b75aad5d13ef82d6931f35cc2d98918503916cf069355b77c5e42302","names":[],"sources":["packages/api/session-controller/src/types.ts:397"]},
|
||||
{"digest":"a4afec59b75aad5d13ef82d6931f35cc2d98918503916cf069355b77c5e42302","names":[],"sources":["packages/api/session-controller/src/types.ts:398"]},
|
||||
{"digest":"a6567ccb2e530606b775371eb4fa31468d72084339968e8b0440a516a23b39dc","names":[],"sources":[]},
|
||||
{"digest":"a6b41f5bede8094d7bf635be5d69b1bd7a25e006ca9cf9415bceb9322f1a7feb","names":["packages/experimental/hook-protocol/src/types.ts#HookDialect"],"sources":["packages/experimental/hook-protocol/src/types.ts:48"]},
|
||||
{"digest":"a6da9ad5d42ae83a6833a8288b076688e8e0801633ea36cbdd8a30a6d9f3160c","names":["packages/workflow/tool-workflow/src/types.ts#ToolWorkflowRunStartData"],"sources":["packages/workflow/tool-workflow/src/types.ts:14"]},
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
en: c8aa4d3384ea1306
|
||||
zh: 5aa030587d5b634d
|
||||
/vendored-package-rescope/name-mapping:
|
||||
en: 239fa7993825ac62
|
||||
zh: df35fa9f5ef42a05
|
||||
en: 50a68b9b3724eff8
|
||||
zh: 9facad930deeb014
|
||||
/vendored-package-rescope/what-the-rename-does-not-touch:
|
||||
en: 9bb7d09f8cccf695
|
||||
zh: 2a8f3f60ae5116f3
|
||||
|
||||
+2
-2
@@ -11,8 +11,8 @@ The Cordis framework and its foundation libraries are vendored under [`vendor/`]
|
||||
| `vendor/cordis/` | `cordis` | `@deepseek-ai/cordis` | 4.0.0-rc.11 | Framework core: `Context`, `Service`, `Fiber`, events |
|
||||
| `vendor/cosmokit/` | `cosmokit` | `@deepseek-ai/cosmokit` | 1.8.1 | Shared utilities the framework and Schemastery build on |
|
||||
| `vendor/schemastery/` | `schemastery` | `@deepseek-ai/schemastery` | 3.18.0 | Config schemas (`Schema`) behind every plugin's `Config` |
|
||||
| `vendor/loader/` | `@cordisjs/plugin-loader` | `@deepseek-ai/cordis-plugin-loader` | 1.0.0-rc.5 | `cordis.yml` loading, plugin resolution, repository cache |
|
||||
| `vendor/include/` | `@cordisjs/plugin-include` | `@deepseek-ai/cordis-plugin-include` | 1.0.4 | Config includes and patch overlays |
|
||||
| `vendor/loader/` | `@cordisjs/plugin-loader` | `@deepseek-ai/cordis-plugin-loader` | 1.0.0-rc.8 | `cordis.yml` loading, plugin resolution, repository cache |
|
||||
| `vendor/include/` | `@cordisjs/plugin-include` | `@deepseek-ai/cordis-plugin-include` | 1.1.1 | Config includes and patch overlays |
|
||||
| `vendor/group/` | `@cordisjs/plugin-group` | `@deepseek-ai/cordis-plugin-group` | 1.0.0 | Nested plugin groups |
|
||||
| `vendor/timer/` | `@cordisjs/plugin-timer` | `@deepseek-ai/cordis-plugin-timer` | 1.1.4 | Disposal-aware timers on `ctx` |
|
||||
| `vendor/hmr/` | `@cordisjs/plugin-hmr` | `@deepseek-ai/cordis-plugin-hmr` | 1.0.15 | Hot module replacement for plugins and config |
|
||||
|
||||
+2
-2
@@ -11,8 +11,8 @@ Cordis 框架及其基础库以源码形式 vendored 在 [`vendor/`](../vendor/R
|
||||
| `vendor/cordis/` | `cordis` | `@deepseek-ai/cordis` | 4.0.0-rc.11 | 框架核心:`Context`、`Service`、`Fiber`、事件 |
|
||||
| `vendor/cosmokit/` | `cosmokit` | `@deepseek-ai/cosmokit` | 1.8.1 | 框架与 Schemastery 共用的基础工具 |
|
||||
| `vendor/schemastery/` | `schemastery` | `@deepseek-ai/schemastery` | 3.18.0 | 配置 schema(`Schema`),每个插件的 `Config` 都基于它 |
|
||||
| `vendor/loader/` | `@cordisjs/plugin-loader` | `@deepseek-ai/cordis-plugin-loader` | 1.0.0-rc.5 | `cordis.yml` 装载、插件解析、repository 缓存 |
|
||||
| `vendor/include/` | `@cordisjs/plugin-include` | `@deepseek-ai/cordis-plugin-include` | 1.0.4 | 配置包含与 patch 叠加 |
|
||||
| `vendor/loader/` | `@cordisjs/plugin-loader` | `@deepseek-ai/cordis-plugin-loader` | 1.0.0-rc.8 | `cordis.yml` 装载、插件解析、repository 缓存 |
|
||||
| `vendor/include/` | `@cordisjs/plugin-include` | `@deepseek-ai/cordis-plugin-include` | 1.1.1 | 配置包含与 patch 叠加 |
|
||||
| `vendor/group/` | `@cordisjs/plugin-group` | `@deepseek-ai/cordis-plugin-group` | 1.0.0 | 嵌套插件分组 |
|
||||
| `vendor/timer/` | `@cordisjs/plugin-timer` | `@deepseek-ai/cordis-plugin-timer` | 1.1.4 | `ctx` 上随 disposal 回收的定时器 |
|
||||
| `vendor/hmr/` | `@cordisjs/plugin-hmr` | `@deepseek-ai/cordis-plugin-hmr` | 1.0.15 | 插件与配置的热替换 |
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
en: 0083032b9d19467d
|
||||
zh: 15d7a7520bac094e
|
||||
/user-credentials/resolution:
|
||||
en: 0583aeab49d80a4b
|
||||
zh: 1ffd60d7e79f67cf
|
||||
en: baaab4bca76bea6b
|
||||
zh: 54ccdc03ffd44f63
|
||||
/user-credentials/description:
|
||||
en: f7b09588c0f34952
|
||||
zh: 1b93d673c5bf6a80
|
||||
|
||||
@@ -20,8 +20,6 @@ type CredentialRef = Branded<'CredentialRef'>
|
||||
|
||||
`resolve(ref)` returns the value with the provider-defined source layer that supplied it, or `undefined` while unconfigured. Consumers re-resolve at each operation and never cache across operations — that per-operation read is the hot-update mechanism.
|
||||
|
||||
`readStored(ref)` returns only the provider-managed value for editing, or `undefined` when none is stored. It never reads environment fallbacks. The Remote editor operation restricts this read to the owner.
|
||||
|
||||
```ts type-equiv
|
||||
/** One resolved credential value and the source layer that supplied it. */
|
||||
interface ResolvedCredential {
|
||||
@@ -154,13 +152,6 @@ A CredentialKey answers "what credential does this plugin hold for this id". Not
|
||||
*/
|
||||
abstract resolve(ref: CredentialRef): Promise<ResolvedCredential | undefined>
|
||||
|
||||
/**
|
||||
* Read the provider-managed value without consulting environment fallbacks.
|
||||
* @param ref - the reference to read.
|
||||
* @returns the stored secret, or undefined when no non-empty value is stored.
|
||||
*/
|
||||
abstract readStored(ref: CredentialRef): Promise<string | undefined>
|
||||
|
||||
/**
|
||||
* Describe one reference for configuration surfaces without exposing the
|
||||
* value.
|
||||
@@ -237,7 +228,7 @@ Source: [`packages/credentials/credentials/src/index.ts`](../../packages/credent
|
||||
|
||||
### `ctx.credentialsController` — `CredentialsController`
|
||||
|
||||
Host service backing the generated `ctx.remote.credentials` namespace. It carries every wire obligation the credential seam itself does not: the batch fan-out bound, the field-by-field view projection, the reference-grammar guard, and the refusal mapping. Metadata reads exclude secrets; readStored returns only a provider-managed value for an explicitly opened editor.
|
||||
Host service backing the generated `ctx.remote.credentials` namespace. It carries every wire obligation the credential seam itself does not: the batch fan-out bound, the field-by-field view projection, the reference-grammar guard, and the refusal mapping. Secret values cross in one direction only — no method here returns one.
|
||||
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
@@ -252,15 +243,8 @@ Host service backing the generated `ctx.remote.credentials` namespace. It carrie
|
||||
@Remote async describe(refs: string[]): Promise<Record<string, CredentialInfo>>
|
||||
|
||||
/**
|
||||
* Read one stored value for a credential editor, excluding environment fallbacks.
|
||||
* @param ref - reference name to read.
|
||||
* @returns the stored secret, or undefined when the provider stores none.
|
||||
* @throws RemoteError for a non-owner Remote caller, an invalid reference, or an absent provider.
|
||||
*/
|
||||
@Remote async readStored(ref: string): Promise<string | undefined>
|
||||
|
||||
/**
|
||||
* Store one value from a configuration surface.
|
||||
* Store one value from a configuration surface. The value crosses the wire in
|
||||
* this direction only: no read path returns it.
|
||||
* @param ref - reference name to store under.
|
||||
* @param value - the non-empty secret value.
|
||||
* @throws RemoteError when the request is invalid, no provider is mounted, or the provider refuses the write.
|
||||
|
||||
@@ -20,8 +20,6 @@ type CredentialRef = Branded<'CredentialRef'>
|
||||
|
||||
`resolve(ref)` 返回值及提供该值的来源层(由提供方定义);未配置期间返回 `undefined`。消费方在每个操作中重新解析,绝不跨操作缓存——这种按操作进行的读取正是热更新机制。
|
||||
|
||||
`readStored(ref)` 仅返回用于编辑的提供方托管值;未存储时返回 `undefined`。它不读取环境后备值。编辑器的 Remote 操作仅允许所有者执行此读取。
|
||||
|
||||
```ts type-equiv
|
||||
/** One resolved credential value and the source layer that supplied it. */
|
||||
interface ResolvedCredential {
|
||||
@@ -154,13 +152,6 @@ A CredentialKey answers "what credential does this plugin hold for this id". Not
|
||||
*/
|
||||
abstract resolve(ref: CredentialRef): Promise<ResolvedCredential | undefined>
|
||||
|
||||
/**
|
||||
* Read the provider-managed value without consulting environment fallbacks.
|
||||
* @param ref - the reference to read.
|
||||
* @returns the stored secret, or undefined when no non-empty value is stored.
|
||||
*/
|
||||
abstract readStored(ref: CredentialRef): Promise<string | undefined>
|
||||
|
||||
/**
|
||||
* Describe one reference for configuration surfaces without exposing the
|
||||
* value.
|
||||
@@ -237,7 +228,7 @@ Source: [`packages/credentials/credentials/src/index.ts`](../../packages/credent
|
||||
|
||||
### `ctx.credentialsController` — `CredentialsController`
|
||||
|
||||
Host service backing the generated `ctx.remote.credentials` namespace. It carries every wire obligation the credential seam itself does not: the batch fan-out bound, the field-by-field view projection, the reference-grammar guard, and the refusal mapping. Metadata reads exclude secrets; readStored returns only a provider-managed value for an explicitly opened editor.
|
||||
Host service backing the generated `ctx.remote.credentials` namespace. It carries every wire obligation the credential seam itself does not: the batch fan-out bound, the field-by-field view projection, the reference-grammar guard, and the refusal mapping. Secret values cross in one direction only — no method here returns one.
|
||||
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
@@ -252,15 +243,8 @@ Host service backing the generated `ctx.remote.credentials` namespace. It carrie
|
||||
@Remote async describe(refs: string[]): Promise<Record<string, CredentialInfo>>
|
||||
|
||||
/**
|
||||
* Read one stored value for a credential editor, excluding environment fallbacks.
|
||||
* @param ref - reference name to read.
|
||||
* @returns the stored secret, or undefined when the provider stores none.
|
||||
* @throws RemoteError for a non-owner Remote caller, an invalid reference, or an absent provider.
|
||||
*/
|
||||
@Remote async readStored(ref: string): Promise<string | undefined>
|
||||
|
||||
/**
|
||||
* Store one value from a configuration surface.
|
||||
* Store one value from a configuration surface. The value crosses the wire in
|
||||
* this direction only: no read path returns it.
|
||||
* @param ref - reference name to store under.
|
||||
* @param value - the non-empty secret value.
|
||||
* @throws RemoteError when the request is invalid, no provider is mounted, or the provider refuses the write.
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user