diff --git a/.agents/notes/archived/architecture/2026-09-24-web-default-schedule-composition.i18n.yaml b/.agents/notes/archived/architecture/2026-09-24-web-default-schedule-composition.i18n.yaml new file mode 100644 index 0000000000..b5321c036f --- /dev/null +++ b/.agents/notes/archived/architecture/2026-09-24-web-default-schedule-composition.i18n.yaml @@ -0,0 +1,6 @@ +# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each +# side as of the last confirmed-consistent state. Both languages carry equal authority; +# after editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write +2026-09-24-web-default-schedule-composition.md: 3f27bb09ebc00d42c32563e17a1de82c8e525be1 +2026-09-24-web-default-schedule-composition.zh.md: 9e0e1fb77ce89b579e1bc8b0d4d0a1b9f4e9fea5 diff --git a/.agents/notes/implemented/architecture/2026-09-24-web-default-schedule-composition.md b/.agents/notes/archived/architecture/2026-09-24-web-default-schedule-composition.md similarity index 99% rename from .agents/notes/implemented/architecture/2026-09-24-web-default-schedule-composition.md rename to .agents/notes/archived/architecture/2026-09-24-web-default-schedule-composition.md index 91f533950a..3f27bb09eb 100644 --- a/.agents/notes/implemented/architecture/2026-09-24-web-default-schedule-composition.md +++ b/.agents/notes/archived/architecture/2026-09-24-web-default-schedule-composition.md @@ -1,6 +1,7 @@ # Agent Note: Schedule in the shipped Web composition Status: implemented +Archived: 2026-09-25 English | [中文](2026-09-24-web-default-schedule-composition.zh.md) diff --git a/.agents/notes/implemented/architecture/2026-09-24-web-default-schedule-composition.zh.md b/.agents/notes/archived/architecture/2026-09-24-web-default-schedule-composition.zh.md similarity index 99% rename from .agents/notes/implemented/architecture/2026-09-24-web-default-schedule-composition.zh.md rename to .agents/notes/archived/architecture/2026-09-24-web-default-schedule-composition.zh.md index af561fe63e..9e0e1fb77c 100644 --- a/.agents/notes/implemented/architecture/2026-09-24-web-default-schedule-composition.zh.md +++ b/.agents/notes/archived/architecture/2026-09-24-web-default-schedule-composition.zh.md @@ -1,6 +1,7 @@ # Agent Note: 默认 Web 组合中的 Schedule Status: implemented +Archived: 2026-09-25 [English](2026-09-24-web-default-schedule-composition.md) | 中文 diff --git a/.agents/notes/archived/manifest.json b/.agents/notes/archived/manifest.json index 5ed6850038..b4e085deb2 100644 --- a/.agents/notes/archived/manifest.json +++ b/.agents/notes/archived/manifest.json @@ -376,6 +376,9 @@ "architecture/2026-09-11-wasm-preview-font-and-image-budgets.i18n.yaml": "sha256:af027c7dae53a181e4d73fd2309f69e9ae3c48df0d3b1dfc53cae274aa60d69e", "architecture/2026-09-11-wasm-preview-font-and-image-budgets.md": "sha256:44dd52839cdb67644c0dbcbc4beb6f36802628c4a8d17a0aad11db1f9be649ed", "architecture/2026-09-11-wasm-preview-font-and-image-budgets.zh.md": "sha256:8285432ee1676be8b79a05953c4b6ac4a7ca5d8e701173b3a2b533608b584d0a", + "architecture/2026-09-24-web-default-schedule-composition.i18n.yaml": "sha256:8c3d5c614924d0e81f361a299427ac697aefc6189c41eb3f4151f5c42e371ca0", + "architecture/2026-09-24-web-default-schedule-composition.md": "sha256:3a139593ade13f95cbb67094c1eec66cb0a2c14bcc154512799803b2a7d22ab4", + "architecture/2026-09-24-web-default-schedule-composition.zh.md": "sha256:59908f7987058742d1226dc8a801d03c265c84d51172249ba51716b91f084f0c", "bug-fix/2026-07-19-windows-atomic-write-dacl-preservation.i18n.yaml": "sha256:8be5b0afd8820c593e2ec254d35fb8c384267814104f6234bb9af824c5f974ba", "bug-fix/2026-07-19-windows-atomic-write-dacl-preservation.md": "sha256:861e6130e893489271478def5f54f212a106a1580b744e6e40f56356f73c7e10", "bug-fix/2026-07-19-windows-atomic-write-dacl-preservation.zh.md": "sha256:3549257e06f074591de580ebfaa71284c7c384fb0ffb1963c30258d7a7c26db7", diff --git a/.agents/notes/implemented/architecture/2026-09-16-host-schedule-storage.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-16-host-schedule-storage.i18n.yaml index 159fee0bf9..8bc4eaeaa5 100644 --- a/.agents/notes/implemented/architecture/2026-09-16-host-schedule-storage.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-09-16-host-schedule-storage.i18n.yaml @@ -15,5 +15,5 @@ en: 21d7ec9a9ff3cbf2 zh: 93d03b8b3012aa7e /agent-note-host-owned-scheduled-messages/consequences: - en: 0358aab730e4bf56 - zh: 61e89e0374891f8a + en: a48f13f89a1944b1 + zh: f8fa677122ce2e12 diff --git a/.agents/notes/implemented/architecture/2026-09-16-host-schedule-storage.md b/.agents/notes/implemented/architecture/2026-09-16-host-schedule-storage.md index 79a4f72071..3d90e8ad7e 100644 --- a/.agents/notes/implemented/architecture/2026-09-16-host-schedule-storage.md +++ b/.agents/notes/implemented/architecture/2026-09-16-host-schedule-storage.md @@ -58,7 +58,7 @@ Session inbox persistence and the task put occur in separate persistence systems **Keep the mock's hourly choice out of the stored vocabulary.** The approved mock lists 每小时整点 beside the daily, weekly and one-shot choices. Hourly and fixed-rate intervals are the same elapsed-time rule, so the card maps that menu slot onto the existing interval kind instead of adding a persisted kind. A dedicated hourly kind would need a new durable variant, its decoder, its target resolution, and its tool schema, all to express what an interval already expresses. -Task discovery and timers do not depend on loaded Sessions. The after, absolute-time, and fixed-rate selectors retain their semantics alongside Daily, Weekly, and Cron; reading existing saved tasks does not rewrite them. The Host must have its ordinary Session persistence and recovery services available, and the Web bundle mounts Schedule in its host row list. Explicit deletion stops future delivery and removes the task from `list` and `catalog` without deleting conversation messages; the removed row takes its saved receipts with it. History is pruned to the configured window and record cap when an acknowledgment is appended, loaded into Host memory, and rewritten with the task on each acknowledgment. Pagination limits the returned record count; the plugin configuration (`deliveryHistoryDays`, default 30, and `deliveryHistoryRecords`, default 200) limits storage growth. Earlier overwritten receipts and deliveries whose task write failed cannot be reconstructed. Pause and execution-history operations remain unsupported. +Task discovery and timers do not depend on loaded Sessions. The after, absolute-time, and fixed-rate selectors retain their semantics alongside Daily, Weekly, and Cron; reading existing saved tasks does not rewrite them. The Host must have its ordinary Session persistence and recovery services available, and enabling the optional experimental bundle `@deepseek-ai/dsh-experimental-schedule-bundle` mounts Schedule; the Web composition carries its row disabled. Explicit deletion stops future delivery and removes the task from `list` and `catalog` without deleting conversation messages; the removed row takes its saved receipts with it. History is pruned to the configured window and record cap when an acknowledgment is appended, loaded into Host memory, and rewritten with the task on each acknowledgment. Pagination limits the returned record count; the plugin configuration (`deliveryHistoryDays`, default 30, and `deliveryHistoryRecords`, default 200) limits storage growth. Earlier overwritten receipts and deliveries whose task write failed cannot be reconstructed. Pause and execution-history operations remain unsupported. Required coverage includes startup from stored tasks, missing-status normalization, cold Session restoration, latest-only catch-up, inactive-task retention without repeat dispatch, real message receipts, deletion across restart, removal of a deleted task's saved deliveries, queue/persistence failures, and disposal. Wall-clock obligations include gaps and skipped dates, earlier-only overlaps, fractional seconds, UTC-range edges, committed-target and alias preservation, and post-restoration clock rollback. Weekly adds weekday normalization and validation, next-target selection across a gap and an overlap, and stored set preservation. Historical decoders must reject Daily, Weekly, and Cron while Host storage accepts them. Client coverage includes every recurring kind, exact rule time, zone, and weekday set, All/Enabled/Inactive status filters, retained inactive details and original-Session navigation, authoritative refresh after confirmed deletion and notification, empty lists, reconnect, and late responses. Presentation coverage includes the page's New action starting a Session, the transcript card's narrowing of persisted result JSON including an incomplete or replayed result, the header clock's direct open for exactly one task and its count label, the idle-only row mark with the hover-card two-task limit and omission line. History coverage includes legacy byte preservation, append and restart, immutable sent instructions, strict latest-receipt consistency, stable cursor pagination, binding checks, failed persistence, and late page replies during refresh or task changes. Cross-store crash duplication remains an explicit limitation rather than a completion claim. diff --git a/.agents/notes/implemented/architecture/2026-09-16-host-schedule-storage.zh.md b/.agents/notes/implemented/architecture/2026-09-16-host-schedule-storage.zh.md index 393a773fa0..f7b9a8f889 100644 --- a/.agents/notes/implemented/architecture/2026-09-16-host-schedule-storage.zh.md +++ b/.agents/notes/implemented/architecture/2026-09-16-host-schedule-storage.zh.md @@ -58,7 +58,7 @@ Session 收件箱持久化和任务 put 发生在两个持久化系统中。收 **不把 mock 的「每小时整点」引入持久词表。** 设计稿把它与每天、每周、仅一次并列。每小时整点与固定间隔在语义上是同一种按经过时间计量的规则,因此详情里的该项直接映射到既有的间隔类型,而不是新增一个持久种类。若真要新增,需要新的持久变体、解码器、目标解析与工具 schema,而它表达的能力现有间隔已经覆盖。 -任务发现和 timer 不依赖已加载的 Session。延迟、绝对时间和固定速率选择器在与 Daily、Weekly、Cron 并存时保留自身语义;读取已有任务不会重写它们。Host 必须提供普通 Session 持久化和恢复服务,Web bundle 在其宿主行列表中挂载 Schedule。显式删除会阻止后续投递,并将任务从 `list` 和 `catalog` 中移除,不删除对话消息;被移除的行会带走其已保存回执。历史在追加确认时按配置的窗口与条数上限裁剪,随后加载到 Host 内存,并在每次确认时随任务一起重写。分页限制返回记录数量,不限制存储增长或指令字节数。此前被覆盖的回执,以及任务写入失败的投递记录,无法重建。暂停和执行历史操作仍不受支持。 +任务发现和 timer 不依赖已加载的 Session。延迟、绝对时间和固定速率选择器在与 Daily、Weekly、Cron 并存时保留自身语义;读取已有任务不会重写它们。Host 必须提供普通 Session 持久化和恢复服务,而挂载 Schedule 需要启用可选实验 bundle `@deepseek-ai/dsh-experimental-schedule-bundle`;Web 组合以禁用状态携带其条目。显式删除会阻止后续投递,并将任务从 `list` 和 `catalog` 中移除,不删除对话消息;被移除的行会带走其已保存回执。历史在追加确认时按配置的窗口与条数上限裁剪,随后加载到 Host 内存,并在每次确认时随任务一起重写。分页限制返回记录数量,不限制存储增长或指令字节数。此前被覆盖的回执,以及任务写入失败的投递记录,无法重建。暂停和执行历史操作仍不受支持。 必要覆盖范围包括从已存储任务启动、缺失状态规范化、冷 Session 恢复、仅补发最新发生时点、保留未运行任务且不重复投递、真实消息回执、重启后的删除结果、删除会移除已保存发送记录、排队或持久化失败,以及销毁。钟表规则的验证要求包括缺口和跳过的日期、仅选择较早时点的重叠、小数秒、UTC 范围边缘、已提交目标和别名保留,以及恢复后的时钟回拨。Weekly 另需覆盖星期集合的规范化与校验、跨缺口与重叠的下一目标选择,以及已存集合的保留。历史解码器必须拒绝 Daily、Weekly 与 Cron,而宿主存储接受它们。客户端覆盖范围包括每种重复规则、精确规则时间、时区和星期集合、全部/已开启/未运行状态筛选、保留未运行任务详情和原 Session 导航、确认删除和通知后的权威刷新、空列表、重新连接和晚到响应。呈现覆盖范围包括页面“新建”操作打开新会话、transcript 卡片从不完整或回放的结果 JSON 中收窄任务、页头时钟在恰好一个任务时直接打开并给出数量标签、仅空闲行标记及其悬浮卡片两个任务上限与遗漏行。历史覆盖范围包括旧文件字节保留、追加与重启、不可变的发送指令、最近回执一致性严格校验、稳定游标分页、绑定校验、持久化失败,以及刷新或切换任务期间晚到的分页响应。跨存储崩溃导致的重复投递仍是明确限制,不能被表述为完成保证。 diff --git a/.agents/notes/implemented/architecture/2026-09-21-experimental-capabilities-as-optional-bundles.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-21-experimental-capabilities-as-optional-bundles.i18n.yaml index e8573b2e72..521567dd7e 100644 --- a/.agents/notes/implemented/architecture/2026-09-21-experimental-capabilities-as-optional-bundles.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-09-21-experimental-capabilities-as-optional-bundles.i18n.yaml @@ -9,11 +9,11 @@ en: 2ba6c43b20a3d347 zh: 5647179ddaa38ad9 /agent-note-ship-experimental-capabilities-as-optional-bundles/decision: - en: 1c5a43b3ce2d3056 - zh: b59f549a12cf67d1 + en: fdd54ede661f7a83 + zh: a41ed1a872bb8ba3 /agent-note-ship-experimental-capabilities-as-optional-bundles/alternatives-considered: en: a65d84004e48b3ef zh: 17221f08e7a8a89b /agent-note-ship-experimental-capabilities-as-optional-bundles/consequences: - en: d52ed0a248131a37 - zh: 779b8d415281c5da + en: e7fb2a93a89ba9b6 + zh: 37ed9f0a3ca44583 diff --git a/.agents/notes/implemented/architecture/2026-09-21-experimental-capabilities-as-optional-bundles.md b/.agents/notes/implemented/architecture/2026-09-21-experimental-capabilities-as-optional-bundles.md index a2a83645c9..b7139f6d9a 100644 --- a/.agents/notes/implemented/architecture/2026-09-21-experimental-capabilities-as-optional-bundles.md +++ b/.agents/notes/implemented/architecture/2026-09-21-experimental-capabilities-as-optional-bundles.md @@ -10,7 +10,7 @@ The Web plugin page offered two optional bundles, Agent Teams and voice input. A ## Decision -`OPTIONAL_BUNDLES` lists Agent Teams, voice input, and Auto review. Every optional bundle declares `icon` and exports `./locale/*.json` with `meta.title` and `meta.description`, so the Official group renders a localized title, description, and image; `verify-default-product-isolation` rejects an optional bundle without them. The Inspector's `cordis.patch.yml` names the package instead of a built-artifact path, so the same file is the bundle patch and the `--patch` overlay for a built Web launch. +`OPTIONAL_BUNDLES` lists Agent Teams, voice input, Auto review, and the Schedule bundle. Every optional bundle declares `icon` and exports `./locale/*.json` with `meta.title` and `meta.description`, so the Official group renders a localized title, description, and image; `verify-default-product-isolation` rejects an optional bundle without them. The Inspector's `cordis.patch.yml` names the package instead of a built-artifact path, so the same file is the bundle patch and the `--patch` overlay for a built Web launch. An optional bundle is a runtime dependency of the installation, so its dependency graph is downloaded by every `dsh` install. The list therefore admits only packages whose graph is already in the installation's closure; Auto review adds nothing. The Inspector stays available through explicit installation and does not appear in the default plugin list. The browser-use and computer-use providers stay explicit compositions: Playwright MCP, Chrome DevTools MCP, and the native Cua Driver runtime binaries add about 85 MB and 21 packages to every install whether or not the bundle is enabled, and a shipped Cua Driver MCP switch would offer a capability whose executable the installation does not carry. Those provider packages keep locale display metadata for their component rows. Two more packages stay out for other reasons: `ptc-runtime-python` replaces the PTC runtime, and `workflow-ptc` rejects a non-TypeScript runtime at load while the Web presets carry `workflow-ptc` rows a bundle patch cannot reach; `browser-use-stagehand-native` requires a native model name and API key at schema validation with no configuration form on the page. @@ -22,4 +22,4 @@ An optional bundle is a runtime dependency of the installation, so its dependenc ## Consequences -The Official group grows from two to three entries, each tagged experimental by its experimental name. The runtime dependency closure of the installation is unchanged. A browser-use or computer-use provider still needs its Service Definition and the provider in a profile patch or composition. +The Official group carries four optional-bundle entries, each tagged experimental by its experimental name. The runtime dependency closure of the installation is unchanged. A browser-use or computer-use provider still needs its Service Definition and the provider in a profile patch or composition. diff --git a/.agents/notes/implemented/architecture/2026-09-21-experimental-capabilities-as-optional-bundles.zh.md b/.agents/notes/implemented/architecture/2026-09-21-experimental-capabilities-as-optional-bundles.zh.md index 62e248e5ae..af4048bc5e 100644 --- a/.agents/notes/implemented/architecture/2026-09-21-experimental-capabilities-as-optional-bundles.zh.md +++ b/.agents/notes/implemented/architecture/2026-09-21-experimental-capabilities-as-optional-bundles.zh.md @@ -10,7 +10,7 @@ Web 插件页只提供两个可选 bundle:Agent Teams 与语音输入。Auto r ## 决策 -`OPTIONAL_BUNDLES` 列出 Agent Teams、语音输入与 Auto review。每个可选 bundle 都声明 `icon` 并导出带 `meta.title` 与 `meta.description` 的 `./locale/*.json`,官方分组因此能渲染本地化标题、描述和图片;`verify-default-product-isolation` 会拒绝缺少这些声明的可选 bundle。Inspector 的 `cordis.patch.yml` 直接写包名而非构建产物路径,所以同一文件既是 bundle patch,也是已构建 Web 启动的 `--patch` overlay。 +`OPTIONAL_BUNDLES` 列出 Agent Teams、语音输入、Auto review 与 Schedule bundle。每个可选 bundle 都声明 `icon` 并导出带 `meta.title` 与 `meta.description` 的 `./locale/*.json`,官方分组因此能渲染本地化标题、描述和图片;`verify-default-product-isolation` 会拒绝缺少这些声明的可选 bundle。Inspector 的 `cordis.patch.yml` 直接写包名而非构建产物路径,所以同一文件既是 bundle patch,也是已构建 Web 启动的 `--patch` overlay。 可选 bundle 是安装的运行时依赖,其依赖图会随每次 `dsh` 安装一起下载。因此名单只接纳依赖图已在安装闭包内的包;Auto review 不增加任何依赖。Inspector 保留显式安装方式,不在默认插件列表中显示。浏览器操作与电脑操作提供方仍保持显式组合:Playwright MCP、Chrome DevTools MCP 与原生 Cua Driver 运行时二进制会让每次安装多出约 85 MB、21 个包,无论 bundle 是否启用;而随附的 Cua Driver MCP 开关会提供一个安装本身并不携带其可执行文件的能力。这些提供方包保留 locale 显示元数据,供组件行使用。另有两个包因其他原因不纳入:`ptc-runtime-python` 会替换 PTC 运行时,而 `workflow-ptc` 在加载时拒绝非 TypeScript 运行时,且 Web preset 内含 bundle patch 无法触及的 `workflow-ptc` 行;`browser-use-stagehand-native` 在 schema 校验时就要求原生模型名称与 API 密钥,而插件页没有对应的配置表单。 @@ -22,4 +22,4 @@ Web 插件页只提供两个可选 bundle:Agent Teams 与语音输入。Auto r ## 后果 -官方分组从两项增至三项,每项都因实验包名而带实验性标签。安装的运行时依赖闭包保持不变。浏览器操作或电脑操作提供方仍需在 profile patch 或组合中同时挂载其 Service Definition 与提供方。 +官方分组包含四个可选 bundle 条目,每项都因实验包名而带实验性标签。安装的运行时依赖闭包保持不变。浏览器操作或电脑操作提供方仍需在 profile patch 或组合中同时挂载其 Service Definition 与提供方。 diff --git a/.agents/notes/implemented/architecture/2026-09-24-schedule-opt-in-optional-bundle.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-24-schedule-opt-in-optional-bundle.i18n.yaml new file mode 100644 index 0000000000..ae54aff2a9 --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-09-24-schedule-opt-in-optional-bundle.i18n.yaml @@ -0,0 +1,19 @@ +# Bilingual-pair consistency record for 2026-09-24-schedule-opt-in-optional-bundle.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-24-schedule-opt-in-optional-bundle.md +/agent-note-schedule-as-an-opt-in-optional-bundle: + en: d234bf064c5990f1 + zh: c54b941429b4658c +/agent-note-schedule-as-an-opt-in-optional-bundle/problem: + en: 2409ea3a006ea5d9 + zh: dbcda58a4c6b5b9f +/agent-note-schedule-as-an-opt-in-optional-bundle/decision: + en: 5aa944749a231645 + zh: c14651be6367ee13 +/agent-note-schedule-as-an-opt-in-optional-bundle/alternatives-considered: + en: 1782c8ecdbe6bf87 + zh: dd6997d067257562 +/agent-note-schedule-as-an-opt-in-optional-bundle/consequences: + en: 8ea1d9aaf25f1702 + zh: 32e721347ea55475 diff --git a/.agents/notes/implemented/architecture/2026-09-24-schedule-opt-in-optional-bundle.md b/.agents/notes/implemented/architecture/2026-09-24-schedule-opt-in-optional-bundle.md new file mode 100644 index 0000000000..bd4b47d25a --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-09-24-schedule-opt-in-optional-bundle.md @@ -0,0 +1,34 @@ +# Agent Note: Schedule as an opt-in optional bundle + +Status: implemented + +English | [中文](2026-09-24-schedule-opt-in-optional-bundle.zh.md) + +> Supersedes [Schedule in the shipped Web composition](../../archived/architecture/2026-09-24-web-default-schedule-composition.md): the three rows now ship disabled, and switching them on is this bundle's job. + +## Problem + +`packages/bundle/web-app/cordis.patch.yml` inserts `time-context`, `schedule`, and `ui-schedule` with `disabled: true`, so the shipped Web composition exposes no Schedule surface: no `schedule_*` tools, no Session reminder catalog, no Automation tasks page, and no per-step clock reading. The rows have no switch on the product surface; a deployment that wants them edits a profile patch layer or passes a `--patch` overlay, and both are configuration files a person using the shipped Web profile cannot reach. + +## Decision + +The rows stay in the composition because an id-targeted patch overrides an existing row: a profile patch or a bundle sets a row's `disabled` to `false` without inserting it again. + +`@deepseek-ai/dsh-experimental-schedule-bundle` (`packages/experimental/schedule-bundle/`) carries three id-targeted patches in its `cordis.patch.yml`, one per row, each setting `disabled: false`; it inserts no row. `OPTIONAL_BUNDLES` in `packages/boot/app-boot/src/profile.ts` names the package, and `apps/cli` declares it as a runtime dependency, so every installation ships it switched off. The [experimental-as-optional-bundles decision](2026-09-21-experimental-capabilities-as-optional-bundles.md) owns the `OPTIONAL_BUNDLES` conventions and the localized `icon` and `meta.title` / `meta.description` metadata the Web Plugins page renders in its Official group. + +Enabling the bundle turns on `time-context` (a per-step clock reading with the sampled instant, the browser zone attached to the open request, and the elapsed time since the preceding model-visible message), `schedule` (durable reminders plus `schedule_create`, `schedule_list`, `schedule_update`, and `schedule_delete` on live root Agents), and `ui-schedule` (the Session reminder catalog and the Automation tasks page). The bundle switches `time-context` on with `schedule` because a reminder request states a wall-clock target: the sampled instant is what lets the model turn a request such as "tomorrow at nine" into an offset-bearing `at` value. The [Schedule subsystem](../../../../docs/subsystems/schedule.md) owns the durable records, delivery, and management operations; the [Web bundle](../../../../packages/bundle/web-app/README.md) owns the composition that carries the disabled rows. Disabling the bundle restores the shipped composition, and the Host stops scheduling while the `schedule` row stays disabled; stored task records remain in the Schedule domain. + +## Alternatives considered + +**Ship Schedule on by default.** Every Web session then pays four tool schemas in each request header and one durable user message per eligible step, and a conversation that never creates a reminder pays both costs. The arrangement also left the capability without an off switch on the product surface. + +**Keep a `--patch` overlay file.** An overlay is a launch-time argument rather than a product-surface switch, so a person using the shipped Web profile cannot reach it. Re-declaring the rows through its `insert` list does not override them: `applyEntryPatches` appends that list without de-duplicating ids, and the Loader collapses the composed list to one entry per id with the last declaration winning, so the overlay replaces the Web bundle's rows instead of setting fields on them. + +**Extract the three rows out of the Web composition into the bundle.** The bundle would insert the rows instead of overriding them, so a profile that also declares one of those ids would have two declarations for it, of which the Loader keeps only the last. The disabled rows are what an override-style switch targets. + +## Consequences + +- A default `dsh web` session carries no Schedule tool schemas, no Session reminder catalog, no Automation tasks page, and no per-step clock reading. An installation that wants them enables the optional bundle from the Plugins page or lists the package in a profile's `dsh.profile.bundles`. +- An enabled installation adds four tool schemas to every live root Agent and one durable clock message per eligible step; the reading is model-visible and durable, so it replays, compacts, and appears in exported Session logs like any other user message. +- A composition that does not carry the three Web rows reports one `patch: entry not found` warning per patch and mounts nothing from this bundle, and its package declares no plugin dependency, so enabling it cannot mount a Host service twice. +- The switch is configuration-only: `src/index.ts` is an empty module, the patch carries the runtime content, and the package owns no mutable runtime state, so it publishes no invariant companion. diff --git a/.agents/notes/implemented/architecture/2026-09-24-schedule-opt-in-optional-bundle.zh.md b/.agents/notes/implemented/architecture/2026-09-24-schedule-opt-in-optional-bundle.zh.md new file mode 100644 index 0000000000..eaa59aa17d --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-09-24-schedule-opt-in-optional-bundle.zh.md @@ -0,0 +1,34 @@ +# Agent Note: Schedule 作为按需开启的可选 bundle + +Status: implemented + +[English](2026-09-24-schedule-opt-in-optional-bundle.md) | 中文 + +> 取代[默认 Web 组合中的 Schedule](../../archived/architecture/2026-09-24-web-default-schedule-composition.md):这三行现在默认禁用,打开它们是此 bundle 的职责。 + +## Problem + +`packages/bundle/web-app/cordis.patch.yml` 以 `disabled: true` 插入 `time-context`、`schedule` 与 `ui-schedule`,因此随发行版交付的 Web 组合不提供任何定时能力:没有 `schedule_*` 工具、没有 Session 提醒目录、没有自动化任务页面,也没有逐步时钟读数。这些条目在产品界面上没有开关;想要它们的部署要改 profile patch 层或传 `--patch` overlay,而两者都是使用随发行版 Web profile 的人无法触及的配置文件。 + +## Decision + +这三行仍留在组合中,因为按 id 定位的 patch 覆盖的是已存在的行:profile patch 或 bundle 把某行的 `disabled` 设为 `false`,无需再次插入它。 + +`@deepseek-ai/dsh-experimental-schedule-bundle`(`packages/experimental/schedule-bundle/`)在其 `cordis.patch.yml` 中携带三个按 id 定位的 patch,每行一个,各自把 `disabled` 设为 `false`;它不插入任何行。`packages/boot/app-boot/src/profile.ts` 的 `OPTIONAL_BUNDLES` 列出该包,`apps/cli` 将其声明为运行时依赖,因此每次安装都随包携带且默认关闭。[实验性能力作为可选 bundle 的决策](2026-09-21-experimental-capabilities-as-optional-bundles.zh.md)负责 `OPTIONAL_BUNDLES` 的约定,以及 Web 插件管理页在 Official 分组中渲染的本地化 `icon` 与 `meta.title` / `meta.description` 元数据。 + +启用该 bundle 会打开 `time-context`(每个步骤的时钟读数,包含采样瞬时、附加到当前开放请求的浏览器时区,以及自前一条模型可见消息以来的经过时长)、`schedule`(持久提醒,以及每个活跃根 Agent 上的 `schedule_create`、`schedule_list`、`schedule_update` 与 `schedule_delete`),以及 `ui-schedule`(Session 提醒目录与自动化任务页面)。该 bundle 把 `time-context` 与 `schedule` 一起打开,因为提醒请求给出的是挂钟时间目标:采样瞬时正是让模型把“明天九点”这类请求转换成带偏移量的 `at` 值的依据。持久记录、投递与管理操作由 [Schedule 子系统](../../../../docs/subsystems/schedule.zh.md)负责;承载这些禁用行的组合由 [Web bundle](../../../../packages/bundle/web-app/README.zh.md)负责。禁用该 bundle 会恢复随发行版交付的组合,`schedule` 行保持禁用期间 Host 不再调度;已存储的任务记录保留在 Schedule domain 中。 + +## Alternatives considered + +**默认发布 Schedule。** 那样每个 Web 会话都要在每个请求头中带上四个工具 schema,并在每个符合条件的步骤追加一条持久 user 消息,从不创建提醒的对话也要承担这两项成本。这种安排还让该能力在产品界面上没有关闭开关。 + +**保留一个 `--patch` overlay 文件。** overlay 是启动时的参数,而不是产品界面上的开关,因此使用随发行版 `web` profile 的人无法触及它。通过它的 `insert` 列表重新声明这些条目并不能覆盖它们:`applyEntryPatches` 追加该列表时不对 id 去重,而 Loader 会把组合出的列表按 id 收敛为一个条目、最后一个声明生效,因此该 overlay 会替换 Web Bundle 的条目,而不是给它们设值。 + +**把这三行从 Web 组合中抽出、移入 bundle。** 这样 bundle 会插入这些行而不是覆盖它们;某个 profile 若也声明了这些 id,同一个 id 就会有两份声明,而 Loader 只保留最后一份。按需开关针对的正是这些禁用行。 + +## Consequences + +- 默认的 `dsh web` 会话不带 Schedule 工具 schema、Session 提醒目录、自动化任务页面与逐步时钟读数。需要它们的安装可从插件管理页启用该可选 bundle,或把该包列进 profile 的 `dsh.profile.bundles`。 +- 启用后的安装会给每个活跃根 Agent 增加四个工具 schema,并在每个符合条件的步骤追加一条持久时钟消息;该读数对模型可见且持久,因此它与其他 user 消息一样参与回放、压缩,并出现在导出的 Session 日志中。 +- 在不携带这三行 Web 行的组合中,该 bundle 的每个补丁都会报告一条 `patch: entry not found` 警告且不挂载任何东西;其包不声明插件依赖,因此启用它不会重复挂载同一个 Host 服务。 +- 该开关只涉及配置:`src/index.ts` 是空模块,运行时内容由 patch 承载,包本身不拥有可变的运行时状态,因此不发布不变量伴随模块。 diff --git a/.agents/notes/implemented/architecture/2026-09-24-web-default-schedule-composition.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-24-web-default-schedule-composition.i18n.yaml deleted file mode 100644 index fec9a85467..0000000000 --- a/.agents/notes/implemented/architecture/2026-09-24-web-default-schedule-composition.i18n.yaml +++ /dev/null @@ -1,22 +0,0 @@ -# Bilingual-pair consistency record for 2026-09-24-web-default-schedule-composition.md (docs/i18n/README.md): per heading -# section, a hash of its English and Chinese blocks outside code blocks and generated regions. -# After editing either side, bring the other along and re-record with: -# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-24-web-default-schedule-composition.md -/agent-note-schedule-in-the-shipped-web-composition: - en: 39229d7ab6ed433b - zh: 54287471d371a11c -/agent-note-schedule-in-the-shipped-web-composition/problem: - en: 2871fd5ed881c007 - zh: d03c78d402d186da -/agent-note-schedule-in-the-shipped-web-composition/decision: - en: bb19ad12b2ab0ffc - zh: 971f33e95474a36c -/agent-note-schedule-in-the-shipped-web-composition/recorded-sessions: - en: 5539a42052f4ddb7 - zh: 98cc190ae63b4549 -/agent-note-schedule-in-the-shipped-web-composition/alternatives-considered: - en: ca032a407c2ee8cb - zh: 748a4fcf2485a3da -/agent-note-schedule-in-the-shipped-web-composition/consequences: - en: 4f23bda8fda37987 - zh: 9f7c4a3538bd2979 diff --git a/.agents/notes/implemented/architecture/2026-09-25-session-log-otel-byte-limits.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-25-session-log-otel-byte-limits.i18n.yaml new file mode 100644 index 0000000000..15f69188be --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-09-25-session-log-otel-byte-limits.i18n.yaml @@ -0,0 +1,19 @@ +# Bilingual-pair consistency record for 2026-09-25-session-log-otel-byte-limits.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-25-session-log-otel-byte-limits.md +/agent-note-session-log-otlp-strings-and-bounded-requests: + en: 041f7e2b55202ea3 + zh: e01834ae6c9fc7bc +/agent-note-session-log-otlp-strings-and-bounded-requests/problem: + en: 20f659a15fb55352 + zh: 49d39c95da1b8aa5 +/agent-note-session-log-otlp-strings-and-bounded-requests/decision: + en: 0f0866232326658b + zh: aa608d17ba712d8a +/agent-note-session-log-otlp-strings-and-bounded-requests/alternatives-considered: + en: 0dc933b18a1a3573 + zh: 77655740322c4e45 +/agent-note-session-log-otlp-strings-and-bounded-requests/consequences: + en: 3859569c9cdea30c + zh: 517449b8c49807f6 diff --git a/.agents/notes/implemented/architecture/2026-09-25-session-log-otel-byte-limits.md b/.agents/notes/implemented/architecture/2026-09-25-session-log-otel-byte-limits.md new file mode 100644 index 0000000000..57fbe72dc5 --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-09-25-session-log-otel-byte-limits.md @@ -0,0 +1,29 @@ +# Agent Note: Session-log OTLP strings and bounded requests + +Status: implemented + +English | [中文](2026-09-25-session-log-otel-byte-limits.zh.md) + +## Problem + +Session logs contain nested JSON and large tool results. The product collector accepts named records with string content and limits request size; count-only SDK batching cannot bound bytes after UTF-8 encoding and JSON escaping. + +## Decision + +The shared `otel` Cordis plugin owns ordinary-event and Session-log transport implementations. Business adapters inject it and create independent caller-owned channels; their existing UI/RPC entry points retain authorization, redaction, identity, configuration, and bounded shutdown. Mounting the shared service creates no unused queues. Each canonical event becomes `eventName: "session-log"`, with `sessionId` and JSON-stringified event values in `content`; serialization does not preserve JSONL key ordering. Redaction runs before serialization and the copied envelope excludes the payload. + +Session logs use a separate provider and byte/count queue. Each record is measured once with the SDK serializer including a full envelope; the sum conservatively bounds a combined request. Greedy packing avoids full-candidate serialization and recursive re-encoding. Single oversized records receive one diagnostic and do not block later events. One HTTP request owns the transport slot until settlement, even after its watchdog fires; shutdown expiry stops the remaining queue. Scope name/version and explicit transport options remain owned by the backend. + +The [telemetry revival](../feature/2026-07-23-session-telemetry-otel-revival.md) remains authoritative for redaction, capture, and best-effort handoff. This decision replaces its SDK-only queue/batching rule: SDK batch deadlines cannot own an operation split into multiple HTTP requests. The shared Session-log channel schedules complete requests; the SDK still owns transport/retry. Feedback authorization and the separate DeepSeek model-request contribution remain unchanged. + +## Alternatives considered + +**One string for the entire Session.** Rejected because one large Session would exceed the request ceiling even when each event fits. One event per record preserves event identity and permits batching. + +**Truncate oversized records or add client-only fragments.** Rejected because truncation loses content and fragments require a collector reassembly protocol. Explicit rejection retains the one-event-per-record meaning. + +**Count compressed bytes or characters.** Rejected because collector limits can apply after decompression, and characters omit UTF-8 and escaping overhead. Measuring full uncompressed requests gives a conservative bound. + +## Consequences + +Product and Session records never share a request or queue, and neither package imports the other. Conservative per-record envelope accounting can produce smaller batches than exact combined accounting. Queue overflow, oversize rejection, exit, and transport failure can lose data; no durable outbox or acknowledgement is added. Tests cover byte limits, linear measurement, slow-request serialization, metadata, redaction-copy isolation, and feedback-authorized replay. diff --git a/.agents/notes/implemented/architecture/2026-09-25-session-log-otel-byte-limits.zh.md b/.agents/notes/implemented/architecture/2026-09-25-session-log-otel-byte-limits.zh.md new file mode 100644 index 0000000000..f6e8306978 --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-09-25-session-log-otel-byte-limits.zh.md @@ -0,0 +1,29 @@ +# Agent Note: Session 日志的 OTLP 字符串与有界请求 + +Status: implemented + +[English](2026-09-25-session-log-otel-byte-limits.md) | 中文 + +## 问题 + +Session 日志包含嵌套 JSON 和大型工具结果。产品采集端接收带名称和字符串内容的记录,并限制请求大小;仅按数量分批的 SDK 无法约束 UTF-8 编码与 JSON 转义后的字节数。 + +## 决策 + +共享 `otel` Cordis 插件负责普通事件和 Session 日志的传输实现。业务适配器注入它并创建独立、由调用方持有的通道;现有 UI/RPC 入口保留授权、脱敏、身份、配置和有期限的关闭。只挂载共享服务不会创建无用队列。每条规范事件转换为 `eventName: "session-log"`,带有 `sessionId` 和在 `content` 中 JSON 字符串化的事件值;序列化不保留 JSONL 键顺序。脱敏先于序列化,复制的 envelope 不包含 payload。 + +Session 日志使用独立 provider 和字节/条数队列。每条记录使用 SDK 序列化器计量一次,包含完整 envelope;其总和保守约束合并请求大小。顺序组包避免候选批次的完整序列化和递归重复编码。单条超限记录只产生一次诊断,不阻塞后续事件。每个 HTTP 请求在结束前始终占有传输槽位,即使其监测期限已经触发;关闭期限到期后停止剩余队列。scope 名称/版本和显式传输选项仍由后端拥有。 + +[遥测恢复决策](../feature/2026-07-23-session-telemetry-otel-revival.zh.md) 继续约束脱敏、捕获和尽力交接。本决策替代其仅使用 SDK 队列/聚合的规则:SDK 批次期限无法拥有拆成多个 HTTP 请求的操作。共享 Session 日志通道调度完整请求,SDK 仍负责传输和重试。反馈授权和独立的 DeepSeek 模型请求附加路径不变。 + +## 考虑过的替代方案 + +**整个 Session 合为一个字符串。** 拒绝,因为即使每条事件都能放入请求,大型 Session 仍会超过请求上限。每条事件对应一条记录,既保留事件身份又允许分批。 + +**截断超限记录或仅在客户端分片。** 拒绝,因为截断会丢失内容,而分片需要采集端重组协议。显式拒绝保留一条事件对应一条记录的含义。 + +**计算压缩字节数或字符数。** 拒绝,因为采集端限制可能作用于解压后内容,而字符数不包含 UTF-8 和转义开销。计量完整未压缩请求提供保守上限。 + +## 影响 + +产品事件和 Session 记录不共享请求或队列,两个包也不相互导入。保守的逐记录 envelope 计量可能比精确合并计量产生更小批次。队列溢出、超限拒绝、退出和传输失败可能丢失数据;不新增持久 outbox 或确认机制。测试覆盖字节上限、线性计量、慢请求串行调度、元数据、脱敏复制隔离和反馈授权回放。 diff --git a/.agents/notes/implemented/feature/2026-07-23-session-telemetry-otel-revival.i18n.yaml b/.agents/notes/implemented/feature/2026-07-23-session-telemetry-otel-revival.i18n.yaml index 8f2fe2b211..a06d830891 100644 --- a/.agents/notes/implemented/feature/2026-07-23-session-telemetry-otel-revival.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-07-23-session-telemetry-otel-revival.i18n.yaml @@ -9,8 +9,8 @@ en: d3383b495424d118 zh: 4e6ffc8ebfa7f741 /agent-note-session-telemetry-seam-with-mandatory-redaction-and-the-otel-backend/decision: - en: 85b950a67315023c - zh: cb4c08338c30b226 + en: 739b871ae583dcbf + zh: d17053e3b0a35942 /agent-note-session-telemetry-seam-with-mandatory-redaction-and-the-otel-backend/alternatives-considered: en: 01838eefca2bc117 zh: fc71f5449d095b27 diff --git a/.agents/notes/implemented/feature/2026-07-23-session-telemetry-otel-revival.md b/.agents/notes/implemented/feature/2026-07-23-session-telemetry-otel-revival.md index dc52ee87ef..b3b50d4be5 100644 --- a/.agents/notes/implemented/feature/2026-07-23-session-telemetry-otel-revival.md +++ b/.agents/notes/implemented/feature/2026-07-23-session-telemetry-otel-revival.md @@ -16,6 +16,8 @@ Every deployment that wants harness sessions in an observability stack must hand - **The `session-telemetry/record` waterfall** — the delta over the branch version and the seam's redaction extension point. Every record passes it before reaching any backend; the seam ships NO rules of its own — the innermost `next()` is a pass-through, deployments mount their rules as listeners (stacking by transforming `next()`'s return value), and a throwing rule withholds the record fail-closed. Redaction applies to the exported copy only; the canonical log is never rewritten. - **`@deepseek-ai/dsh-session-telemetry-otel`** — the reference backend: OTel JS SDK log pipeline (`LoggerProvider` → `BatchLogRecordProcessor` → OTLP/HTTP exporter), configured verbatim through `exporter`/`processor` passthroughs. `DISABLED` is the plugin default and constructs no transport. `FEEDBACK_ONLY` requires `exporter.url`; the [explicit-feedback policy](../architecture/2026-09-05-nonofficial-feedback-otel.md) requires a new submission for every bounded capture, for all providers. [Buffer-free feedback replay](../../archived/simplification/2026-08-06-buffer-free-feedback-telemetry.md) avoids a second in-memory copy of the session prefix. +The [Session-log request limits](../architecture/2026-09-25-session-log-otel-byte-limits.md) supersede the SDK-only batching rule below for the OTel Session sender; capture, redaction, and handoff rationale remain applicable. + The boundary axiom holds: the harness's aspect ends at `emit()`. Batching, retry, queueing, and loss policy are the reporting SDK's, configured through passthroughs. Delivery is best-effort: a crash can lose queued records. Backend retry or losing the same live object's module-scope cursor can duplicate lifecycle-local rows, so receivers deduplicate on `(session.id, session.format_version, event.seq)`. ## Alternatives considered diff --git a/.agents/notes/implemented/feature/2026-07-23-session-telemetry-otel-revival.zh.md b/.agents/notes/implemented/feature/2026-07-23-session-telemetry-otel-revival.zh.md index f647ea2896..349cd47e59 100644 --- a/.agents/notes/implemented/feature/2026-07-23-session-telemetry-otel-revival.zh.md +++ b/.agents/notes/implemented/feature/2026-07-23-session-telemetry-otel-revival.zh.md @@ -16,6 +16,8 @@ Status: implemented - **`session-telemetry/record` waterfall(瀑布式事件)** —— 相对分支版本的增量,也是该 seam 的脱敏扩展点。每条记录抵达任何后端前必经此处;seam 自身不带任何规则——最内层 `next()` 原样透传,部署方以监听器挂载自己的规则(通过变换 `next()` 的返回值堆叠),抛异常的规则将该记录 fail-closed 扣下。脱敏只作用于导出副本;canonical log 永不改写。 - **`@deepseek-ai/dsh-session-telemetry-otel`** —— 参考后端:OTel JS SDK 日志流水线(`LoggerProvider` → `BatchLogRecordProcessor` → OTLP/HTTP exporter),经 `exporter`/`processor` passthrough 原样配置。`DISABLED` 是插件默认值,且不构造任何传输。`FEEDBACK_ONLY` 要求 `exporter.url`;[显式反馈策略](../architecture/2026-09-05-nonofficial-feedback-otel.zh.md)要求每次有界捕获都由新提交触发,适用于所有提供方。[无缓冲反馈回放](../../archived/simplification/2026-08-06-buffer-free-feedback-telemetry.md)避免在内存中创建会话前缀的第二份副本。 +[Session 日志请求上限](../architecture/2026-09-25-session-log-otel-byte-limits.zh.md) 在 OTel Session 发送器中替代下述仅由 SDK 分批的规则;采集、脱敏和交接的理由仍然适用。 + 边界公理保持不变:harness 的职责止于 `emit()`。批处理、重试、排队与丢失策略属于 reporting SDK,并经 passthrough 配置。投递是尽力而为:崩溃可能丢失已排队记录。后端重试或丢失同一 live 对象的模块作用域游标可能重复生命周期本地 row,因此接收端基于 `(session.id, session.format_version, event.seq)` 去重。 ## 考虑过的替代方案 diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index 3f325012a0..e08afa0c1c 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -56,7 +56,6 @@ External packages installed for runtime use or distributed inside the prebuilt b | [`@opentelemetry/api`](https://github.com/open-telemetry/opentelemetry-js) | Apache-2.0 | | [`@opentelemetry/api-logs`](https://github.com/open-telemetry/opentelemetry-js) | Apache-2.0 | | [`@opentelemetry/core`](https://github.com/open-telemetry/opentelemetry-js) | Apache-2.0 | -| [`@opentelemetry/exporter-logs-otlp-http`](https://github.com/open-telemetry/opentelemetry-js) | Apache-2.0 | | [`@opentelemetry/otlp-exporter-base`](https://github.com/open-telemetry/opentelemetry-js) | Apache-2.0 | | [`@opentelemetry/otlp-transformer`](https://github.com/open-telemetry/opentelemetry-js) | Apache-2.0 | | [`@opentelemetry/resources`](https://github.com/open-telemetry/opentelemetry-js) | Apache-2.0 | diff --git a/apps/cli/composition.md b/apps/cli/composition.md index e72a1f0ce6..e627d9e47d 100644 --- a/apps/cli/composition.md +++ b/apps/cli/composition.md @@ -74,6 +74,8 @@ flowchart LR cfg --> plugin_dsh_base_storage_domain plugin_dsh_base_session_projection_cache["session-projection-cache
@deepseek-ai/dsh-session-projection-cache"] cfg --> plugin_dsh_base_session_projection_cache + plugin_dsh_base_otel["otel
@deepseek-ai/dsh-otel"] + cfg --> plugin_dsh_base_otel plugin_dsh_base_session_telemetry_otel["session-telemetry-otel
@deepseek-ai/dsh-session-telemetry-otel"] cfg --> plugin_dsh_base_session_telemetry_otel plugin_dsh_base_subprocess["subprocess
@deepseek-ai/dsh-subprocess-local"] @@ -231,6 +233,7 @@ flowchart LR | `storage-json` | `@deepseek-ai/dsh-storage-json` | | `storage-domain` | `@deepseek-ai/dsh-storage-domain` | | `session-projection-cache` | `@deepseek-ai/dsh-session-projection-cache` | +| `otel` | `@deepseek-ai/dsh-otel` | | `session-telemetry-otel` | `@deepseek-ai/dsh-session-telemetry-otel` | | `subprocess` | `@deepseek-ai/dsh-subprocess-local` | | `sandbox` | `@deepseek-ai/dsh-sandbox-local` | diff --git a/apps/cli/package.json b/apps/cli/package.json index 8b9dbbac17..be98d17216 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -39,6 +39,7 @@ "@deepseek-ai/dsh-compaction-tool-result-pruner": "workspace:*", "@deepseek-ai/dsh-cordis-client-runner": "workspace:*", "@deepseek-ai/dsh-experimental-agent-team-profile": "workspace:*", + "@deepseek-ai/dsh-experimental-schedule-bundle": "workspace:*", "@deepseek-ai/dsh-fs-local": "workspace:*", "@deepseek-ai/dsh-goal": "workspace:*", "@deepseek-ai/dsh-goal-round-driver": "workspace:*", diff --git a/apps/cli/tests/profiles/web/tests/fixtures/schedule.patch.yml b/apps/cli/tests/profiles/web/tests/fixtures/schedule.patch.yml deleted file mode 100644 index fd53f05e15..0000000000 --- a/apps/cli/tests/profiles/web/tests/fixtures/schedule.patch.yml +++ /dev/null @@ -1,2 +0,0 @@ -- id: schedule - disabled: false diff --git a/apps/cli/tests/profiles/web/tests/schedule-timing.expected.e2e.ts b/apps/cli/tests/profiles/web/tests/schedule-timing.expected.e2e.ts index 0cf380636f..a6846024da 100644 --- a/apps/cli/tests/profiles/web/tests/schedule-timing.expected.e2e.ts +++ b/apps/cli/tests/profiles/web/tests/schedule-timing.expected.e2e.ts @@ -8,7 +8,8 @@ import type { DailyScheduleRecord, ScheduleCatalogEntry } from '@deepseek-ai/dsh import { expect, it } from 'vitest' import { withDefaultWeb, webGet, webRequest } from './default-web-process.ts' -const patches = [fileURLToPath(new URL('./fixtures/schedule.patch.yml', import.meta.url))] +/** The optional Schedule bundle is the switch this built-Web scenario turns on. */ +const patches = [fileURLToPath(new URL('../../../../../../packages/experimental/schedule-bundle/cordis.patch.yml', import.meta.url))] const id = 'retained-remote-task' as DailyScheduleRecord['id'] const sessionId = 'unloaded-owner' as ScheduleCatalogEntry['sessionId'] diff --git a/apps/desktop/README.i18n.yaml b/apps/desktop/README.i18n.yaml index 5eef9404af..90652c6c58 100644 --- a/apps/desktop/README.i18n.yaml +++ b/apps/desktop/README.i18n.yaml @@ -6,8 +6,8 @@ en: 2a9d7d1de31172d4 zh: 6680e1bf85abb6f1 /deepseek-harness-desktop/closing-the-window-and-quitting: - en: fb852f3bd70d8602 - zh: 3d2b0a8d580d7f0a + en: c323e02e3f0305fe + zh: 3c730db632136443 /deepseek-harness-desktop/key-technical-decisions: en: 069fdfd72d2ea26a zh: 76ae549d43c99bb4 @@ -15,8 +15,8 @@ en: 31cbb7452be8e1ad zh: 502ec556e2ec301e /deepseek-harness-desktop/installation-ownership: - en: 8307baf6c5b691d0 - zh: 86e46b9552017ff6 + en: d186d3cfc9597688 + zh: 5f7094355c5f562c /deepseek-harness-desktop/installation-ownership/runtime-and-plugin-activation: en: 0d11af8639977a6d zh: 86b086b12d2210d3 diff --git a/apps/desktop/README.md b/apps/desktop/README.md index 56750b7bbf..102ef4cded 100644 --- a/apps/desktop/README.md +++ b/apps/desktop/README.md @@ -30,6 +30,8 @@ Windows keeps a tray icon for the whole run. Its tooltip is the product name, a Every ordinary quit entry — ⌘Q, the application menu, the Dock menu, the Windows tray and caption Application menu, and the quits caused by closing the mandatory-update or welcome window — asks the Host what it would interrupt. The Host answers over the private IPC channel with two facts: active tasks under the same rule as the update restart check (running agents including subagents and approval waits, queued messages, running or stopping jobs), and armed scheduled reminders reported by the `schedule` family of `workspace/session-activity` for the sessions loaded during this run. Neither fact, the quit proceeds silently. Otherwise a native message box without an owner window — hidden windows stay hidden — shows **Quit DeepSeek Harness?** with one of three localized explanations: running tasks will be interrupted, scheduled tasks will not run while the application is closed, or both. Quit is the default button and Cancel answers Esc; macOS places Cancel left of Quit, Windows places Quit left of Cancel, and the Windows task dialog shows the application icon and stays light regardless of the application theme. A Host that has not reached ready or has failed cannot run tasks, so the quit proceeds without asking. An inspection failure or a Host that misses the two-second deadline is treated as running tasks. While the box is open, another quit request joins it instead of stacking a second one (macOS also raises it; Electron exposes no handle to the Windows task dialog), the explanation does not change when work starts or ends, Quit stops the application without re-inspecting, and Cancel changes nothing. Cancelling a quit that closing the welcome window started shows the welcome window again. +The mandatory update confirmation includes the installation wait notice on Windows; macOS uses the shorter restart explanation. + The confirmation is skipped when the installer restart already confirmed task interruption, when the fatal-recovery dialog exits or restarts, when the development Restart App and Host command runs, and during operating-system shutdown, restart, or log-off: Windows sets this on the definitive session-end message; macOS sets it on the power-off notification, and because another application can still cancel that shutdown, the next focus or show of the main window clears it. Installer-owned quit cancels any pending ordinary quit decision; late inspection results and dialog answers cannot open another confirmation or repeat cleanup. A user-initiated update download that finishes while the window is hidden defers its Install and Restart confirmation until the window is shown again; the mandatory flow keeps its taskbar and Dock attention. The Windows installer and uninstaller tell a user whose application is still running to quit it from the system tray. Desktop does not enable scheduled tasks by default, so the scheduled-task explanations appear only once that feature is on; reminders fire only in loaded sessions, and unloaded sessions are neither counted nor resumed until they are opened. The tray renderer enlarges the whale by 20% around the tile center while preserving the background and aspect ratio; application and installer icons retain their original proportions. @@ -86,6 +88,8 @@ The `dsh-app://shell/` origin serves packaged update documents, scripts, and sty The product UI retains Web actions, including "Open In..." through the shared authenticated HTTP routes. Desktop uses Web's automatic directory-picker selection and initializes new profiles with the shared Web template's bundles. +Update copy preserves the complete version, including prerelease suffixes, without adding v or V. When no update is available, the dialog title reports the result and the body shows the current version. + Electron chooses typed English or Chinese shell copy from its application locale and falls back to English. The macOS application bundle declares English and Simplified Chinese in `CFBundleLocalizations`, allowing macOS to match the initial application language to the user’s preferred languages. Saved Client UI language preferences still take precedence for the main interface. On Windows, the main document's language updates desktop menus, recovery and update prompts. The repository Client UI i18n gate checks desktop sources. Windows uses a 40-DIP caption with native window controls and colors synchronized from the application palette. Localized Application and Edit entries beside the sidebar toggle open native popup menus. They mount only after the application frame publishes its shell overlay seat, and remain absent during startup loading. Application provides Check for Updates and Exit; Edit provides undo, redo, cut, copy, paste, delete, and select all by sending the corresponding keys to the focused editor, independently of custom shortcut bindings. Plugin management uses the main application's Plugins page. No separate native menu row appears on Alt. Other platforms retain their native menus. Editable fields retain keyboard commands and a context menu without shortcut labels; Chromium supplies command availability, and selected read-only text offers Copy. diff --git a/apps/desktop/README.zh.md b/apps/desktop/README.zh.md index 4288b99f2e..d4076bc380 100644 --- a/apps/desktop/README.zh.md +++ b/apps/desktop/README.zh.md @@ -30,6 +30,8 @@ Windows 在整个运行期间常驻托盘图标。悬停提示为产品名,单 所有普通退出入口——⌘Q、应用菜单、Dock 菜单、Windows 托盘和标题栏"应用程序"菜单,以及关闭强制更新窗口或欢迎窗口引起的退出——都先向 Host 查询退出会中断什么。Host 通过私有 IPC 通道回答两项事实:与更新重启检查同一口径的运行中任务(运行中的 agent,包括子代理和等待审批的回合、排队消息、运行中或停止中的后台任务),以及本次运行中已加载会话里由 `workspace/session-activity` 的 `schedule` family 报告的已挂定时器的提醒。两项都没有时直接退出,不弹框。否则弹出一个没有父窗口的原生消息框——隐藏的窗口保持隐藏——标题为**退出 DeepSeek Harness?**,正文为三种本地化说明之一:正在运行的任务将会中断、应用关闭期间定时任务不会运行,或两者兼有。"退出"是默认按钮,Esc 等同"取消";macOS 上"取消"在"退出"左侧,Windows 上"退出"在"取消"左侧,Windows 任务对话框显示应用图标且不跟随应用主题、始终为浅色。Host 尚未就绪或已失败时不可能有任务在跑,直接退出。查询失败或 Host 超过两秒截止时间未答复,按运行中任务处理。弹框打开期间,再次请求退出只会并入同一弹框而不叠加新弹框(macOS 上还会把它提到前面;Electron 不暴露 Windows 任务对话框的句柄);任务开始或结束不会改变文案;点"退出"不再重新查询即停止应用;点"取消"不发生任何变化。取消由关闭欢迎窗口引起的退出时,欢迎窗口会重新显示。 +强制更新的安装确认在 Windows 上额外显示等待提示,macOS 只显示简短的重启说明。 + 以下情况跳过确认:安装更新的重启已确认过任务中断、致命错误恢复对话框中的退出或重启、开发版"重启应用与 Host"命令,以及操作系统关机、重启或注销:Windows 在确定性的会话结束消息上设置该状态;macOS 在关机通知上设置,而其他应用仍可能取消这次关机,因此主窗口下一次获得焦点或显示时会清除它。安装器接管退出时会取消尚未结束的普通退出决策;晚到的查询结果和弹框答复不会再次打开确认框或重复清理。窗口隐藏期间完成的用户主动发起的更新下载,把"安装并重启"确认推迟到窗口再次显示时;强制更新流程沿用其任务栏和 Dock 提醒。Windows 安装程序和卸载程序在应用仍在运行时提示用户先在系统托盘中退出。Desktop 默认未开启定时任务,定时任务的说明只在该功能开启后出现;提醒只在已加载的会话中触发,未加载的会话既不计入,也要等到打开后才会继续。 托盘渲染器以底板中心为基准将鲸鱼放大 20%,保留背景和宽高比;应用和安装器图标保持原有比例。 @@ -88,6 +90,8 @@ Electron 拥有 `$DSH_HOME/profiles/desktop`。其 `dependencies` 包含 pnpm 产品 UI 保留 Web 操作,包括通过共享认证 HTTP 路由执行的“打开方式…”。Desktop 使用 Web 的自动目录选择机制,并以共享 Web 模板的 bundle 列表初始化新 profile。 +升级文案完整展示版本号(包括预发布后缀),不额外添加 v 或 V。无可用更新时,弹窗标题显示检查结果,正文显示当前版本。 + Electron 根据应用语言选择类型化的英文或中文 shell 文案,并回退到英文。macOS 应用包通过 `CFBundleLocalizations` 声明支持英语和简体中文,让 macOS 根据用户的首选语言匹配初始应用语言。主界面仍优先使用已保存的 Client UI 语言偏好。在 Windows 上,主文档的语言会更新桌面菜单、恢复与更新提示。仓库 Client UI i18n 检查覆盖桌面端源码。 Windows 使用 40 DIP 顶栏,保留原生窗口按钮,颜色随应用调色板同步。侧栏开关旁的本地化“应用”和“编辑”入口打开原生弹出菜单。仅当应用框架发布 shell overlay 席位后才挂载菜单,启动加载期间不显示。“应用”提供检查更新和退出;“编辑”向当前编辑器发送对应按键,提供撤销、重做、剪切、复制、粘贴、删除和全选,不受自定义快捷键绑定影响。插件管理使用主应用的“插件”页面。按 Alt 不会出现额外的原生菜单行。其他平台保留原生菜单。可编辑区域保留快捷键和不带快捷键标注的右键菜单;命令可用状态由 Chromium 提供,选中的只读文本提供“复制”命令。 diff --git a/apps/desktop/renderer/mandatory-update.css b/apps/desktop/renderer/mandatory-update.css index 89cf0cf0d5..591a82b6ab 100644 --- a/apps/desktop/renderer/mandatory-update.css +++ b/apps/desktop/renderer/mandatory-update.css @@ -9,4 +9,8 @@ #copy { min-height: 32px; padding: 3px 0; background: transparent; color: #4d6bfe; font-size: 12px; } #copy-message { color: #d93025 !important; } #manual-copy { display: block; margin-top: 8px; } -#progress { display: block; width: 100%; height: 4px; margin-top: 12px; border: 0; accent-color: #4d6bfe; } +#progress { display: block; width: 100%; height: 4px; margin-top: 12px; border: 0; appearance: none; -webkit-appearance: none; background: #e6e7eb; accent-color: #0f1115; } + +#progress::-webkit-progress-bar { background: #e6e7eb; } +#progress::-webkit-progress-value { background: #0f1115; } +#progress::-moz-progress-bar { background: #0f1115; } diff --git a/apps/desktop/src/locale.ts b/apps/desktop/src/locale.ts index b3f9e535c9..066418e6bb 100644 --- a/apps/desktop/src/locale.ts +++ b/apps/desktop/src/locale.ts @@ -77,17 +77,17 @@ export const en = { updateCheckFailed: 'Could not check for updates. Please try again later.', updateDownloadFailed: 'Could not download the update. Please try again.', updateInstallFailed: 'Could not install the update. Please try again later.', - updateCheckNetworkFailed: 'Could not check for updates. Please try again later. The connection was interrupted. Check your network and try again.', - updateDownloadNetworkFailed: 'Could not download the update. Please try again. The connection was interrupted. Check your network and try again.', - updateInstallNetworkFailed: 'Could not install the update. Please try again later. The connection was interrupted. Check your network and try again.', + updateCheckNetworkFailed: 'Could not check for updates. Check your connection and try again.', + updateDownloadNetworkFailed: 'Could not download the update. Check your connection and try again.', + updateInstallNetworkFailed: 'Could not install the update. Check your connection and try again.', unknownError: 'Unknown error', updateCheckTitle: 'Check for Updates', - updateCurrent: 'No updates available. Current version: V{version}', + updateCurrentDetail: 'Current version: {version}', + updateCurrent: 'You’re up to date!', updateChecking: 'Checking for updates…', updateDownload: 'Download update', - updateDownloadedTitle: 'DeepSeek Harness v{version} downloaded', - updateDownloadedDetail: 'The update package has downloaded. Select “Install and Restart” to restart the app and begin installation.', - updateDownloadedTitleWindows: 'New version v{version} is ready', + updateDownloadedTitle: 'Version {version} is ready to install', + updateDownloadedDetail: 'The app will close during the update and reopen automatically when it is complete.', updateDownloadedDetailWindows: 'The app will close temporarily during the update and reopen automatically when it is complete.\n\nThe update may take some time. Please wait and do not launch the app again during installation.', updateClose: 'Close', updateAcknowledge: 'OK', @@ -97,47 +97,47 @@ export const en = { updateInstalling: 'Preparing to restart…', updateRetry: 'Retry update', updateActiveTasks: 'Tasks are still in progress', - updateActiveTasksDetail: 'Restarting to update may interrupt these tasks. Continue updating?', + updateActiveTasksDetail: 'Updating will stop the tasks in progress and restart the app. Continue?', updateStopTasks: 'Stop tasks and update', - updateTasksChanged: 'New tasks started. Review the update confirmation again.', + updateTasksChanged: 'New tasks have started. Confirm again to stop the tasks and update.', updateTasksUnavailable: 'Task status is unavailable. Try updating again when the workspace is ready.', - updateStopFailed: 'Tasks could not be stopped safely. The update was not installed. Please try again later.', + updateStopFailed: 'Could not safely stop the tasks. The update has not been installed. Please try again later.', updateTechnicalDetails: 'View technical details', updateTitle: 'DeepSeek Harness Update', - updateAvailable: 'An update is available', - updateDetail: 'DeepSeek Harness {version}\n\nDeepSeek Harness will restart to complete the update.', + updateAvailable: 'New version available: {version}', + updateDetail: 'Once the download is complete, you can install the update and restart the app.', installAndRestart: 'Install and Restart', later: 'Later', updateFailedTitle: 'Update Failed', - mandatoryTitle: 'Update required', - mandatoryDetail: 'This version is no longer supported. Update to continue. Existing tasks can keep running until you approve a restart.', - mandatoryUnavailable: 'The update requirement could not be checked. Retry when the connection is available.', + mandatoryTitle: 'Update to continue', + mandatoryDetail: 'This version is no longer supported. Update to continue. Tasks in progress will keep running until you confirm installation and restart.', + mandatoryUnavailable: 'Could not check update requirements. Please try again later.', policyLoginTitle: 'Sign in to the test environment', - policyLoginRequired: 'This is a test build. Checking update requirements needs Feishu sign-in. Signing in does not download or install an update.', + policyLoginRequired: 'Sign in with Feishu to check update requirements for this test build. Signing in will not download or install an update.', policyLogin: 'Sign in with Feishu', - policyLoginFailed: 'Test environment sign-in did not complete. Please check your connection and try again.', + policyLoginFailed: 'Feishu sign-in did not complete. Please try again.', policyLoginLoading: 'Loading sign-in page…', - mandatoryNoRelease: 'No applicable update is available. Check again or contact support.', + mandatoryNoRelease: 'No compatible update was found. Check again or contact support.', mandatoryRefresh: 'Check again', - mandatoryPage: 'Open download page', - mandatoryCopy: 'Copy download address', - mandatoryPageFailed: 'The download page could not be opened. Copy the address below and open it in your browser.', - mandatoryActionFailed: 'The update action failed. Retry; the update requirement remains active.', + mandatoryPage: 'Download from the official website', + mandatoryCopy: 'Copy download link', + mandatoryPageFailed: 'Could not open the official download page. Copy the link and open it in your browser.', + mandatoryActionFailed: 'The action failed. Please try again. You can use the app again after the update is complete.', mandatoryReady: 'Update ready', - mandatoryVersion: 'V{version}', - mandatoryReadyDetail: 'Installing the update will restart the application.', - mandatoryDeferred: 'Existing tasks can keep running. Update to continue using the application.', - mandatoryContinue: 'Continue installing update', + mandatoryVersion: 'New version: {version}', + mandatoryReadyDetail: 'The app will close during the update and reopen automatically when it is complete.', + mandatoryDeferred: 'Tasks in progress will keep running. Complete the update before using the app again.', + mandatoryContinue: 'Continue update', mandatoryInspecting: 'Checking tasks…', - mandatoryStopping: 'Safely stopping tasks in the application.', - mandatoryRestarting: 'The application will restart shortly. Please wait.', + mandatoryStopping: 'Stopping tasks…', + mandatoryRestarting: 'The app will restart shortly. Please wait.', mandatoryDownloadFailed: 'The update files could not be downloaded or prepared. Please retry.', - mandatoryInstallFailed: 'The update was not installed. Check tasks again and retry.', - mandatoryOpenHelp: 'If the page did not open, you can', - mandatoryReopen: 'Open download page again', + mandatoryInstallFailed: 'The update has not been installed. Check the tasks again and retry.', + mandatoryOpenHelp: 'Page didn’t open?', + mandatoryReopen: 'Open the official download page again', mandatoryCopied: 'Link copied', - mandatoryCopyFailed: 'Copy failed. Select and copy the address below manually.', - mandatoryAddress: 'Download address', + mandatoryCopyFailed: 'Could not copy the link. Select and copy it below.', + mandatoryAddress: 'Download link', mandatoryNotification: 'Return to the application to confirm installation and restart.', } as const @@ -221,17 +221,17 @@ export const zh = { updateCheckFailed: '检查更新失败,请稍后重试。', updateDownloadFailed: '下载更新失败,请重试。', updateInstallFailed: '安装更新失败,请稍后重试。', - updateCheckNetworkFailed: '检查更新失败,请稍后重试。网络连接异常,请检查网络后重试。', - updateDownloadNetworkFailed: '下载更新失败,请重试。网络连接异常,请检查网络后重试。', - updateInstallNetworkFailed: '安装更新失败,请稍后重试。网络连接异常,请检查网络后重试。', + updateCheckNetworkFailed: '检查更新失败,请检查网络连接后重试。', + updateDownloadNetworkFailed: '下载更新失败,请检查网络连接后重试。', + updateInstallNetworkFailed: '安装更新失败,请检查网络连接后重试。', unknownError: '未知错误', updateCheckTitle: '检查更新', - updateCurrent: '当前暂无可用更新。当前版本:V{version}', + updateCurrentDetail: '当前版本:{version}', + updateCurrent: '已是最新版本', updateChecking: '正在检查更新…', updateDownload: '下载更新', - updateDownloadedTitle: 'DeepSeek Harness v{version} 下载完成', - updateDownloadedDetail: '安装包已下载完毕,点击“安装并重启”,即刻重启客户端,开始部署。', - updateDownloadedTitleWindows: '新版本 v{version} 已准备就绪', + updateDownloadedTitle: '新版本 {version} 已准备就绪', + updateDownloadedDetail: '更新期间应用将暂时关闭,完成后会自动打开。', updateDownloadedDetailWindows: '更新期间应用将暂时关闭,完成后会自动打开。\n\n更新可能需要一些时间,请耐心等待,期间请勿重复启动应用。', updateClose: '关闭', updateAcknowledge: '确定', @@ -241,47 +241,47 @@ export const zh = { updateInstalling: '正在准备重启…', updateRetry: '重试更新', updateActiveTasks: '仍有进行中的任务', - updateActiveTasksDetail: '重启更新可能中断这些任务,是否要继续更新?', + updateActiveTasksDetail: '更新将停止进行中的任务并重启应用,是否继续?', updateStopTasks: '停止任务并更新', - updateTasksChanged: '有新任务开始,请重新确认更新。', + updateTasksChanged: '有新任务开始运行,请重新确认是否停止任务并更新。', updateTasksUnavailable: '无法确认任务状态,请在工作区就绪后重试更新。', - updateStopFailed: '未能安全停止任务,更新未安装。请稍后重试。', + updateStopFailed: '未能安全停止任务,更新尚未安装,请稍后重试。', updateTechnicalDetails: '查看技术详情', updateTitle: 'DeepSeek Harness 更新', - updateAvailable: '发现可用更新', - updateDetail: 'DeepSeek Harness {version}\n\nDeepSeek Harness 将重启以完成更新。', + updateAvailable: '发现新版本 {version}', + updateDetail: '下载完成后,可安装并重启应用。', installAndRestart: '安装并重启', later: '稍后', updateFailedTitle: '更新失败', - mandatoryTitle: '需要更新', - mandatoryDetail: '当前版本已停止支持,请更新后继续使用。在您确认重启之前,现有任务可以继续运行。', - mandatoryUnavailable: '暂时无法检查更新要求,请在网络恢复后重试。', + mandatoryTitle: '请更新后继续使用', + mandatoryDetail: '当前版本已停止支持,请更新后继续使用。确认安装并重启前,进行中的任务会继续运行。', + mandatoryUnavailable: '暂时无法检查更新要求,请稍后重试。', policyLoginTitle: '登录测试环境', - policyLoginRequired: '这是测试版应用,检查更新要求需要先通过飞书登录。登录不会下载或安装更新。', + policyLoginRequired: '此测试版需要先通过飞书登录,才能检查更新要求。登录不会下载或安装更新。', policyLogin: '通过飞书登录', - policyLoginFailed: '测试环境登录未完成,请检查网络后重试。', + policyLoginFailed: '飞书登录未完成,请重试。', policyLoginLoading: '正在加载登录页面…', - mandatoryNoRelease: '暂时没有可用的更新,请重新检查或联系支持人员。', + mandatoryNoRelease: '暂未找到适用的更新,请重新检查或联系支持人员。', mandatoryRefresh: '重新检查', mandatoryPage: '前往官网下载', mandatoryCopy: '复制下载链接', - mandatoryPageFailed: '无法打开浏览器,请复制下载链接后手动打开。', - mandatoryActionFailed: '更新操作失败,请重试;应用仍需更新后才能继续使用。', + mandatoryPageFailed: '无法打开官网下载页面,请复制链接后在浏览器中打开。', + mandatoryActionFailed: '操作失败,请重试。完成更新后才能继续使用应用。', mandatoryReady: '更新已准备就绪', - mandatoryVersion: 'V{version}', - mandatoryReadyDetail: '安装后将重新启动应用。', - mandatoryDeferred: '现有任务可以继续运行。完成更新后才能继续操作应用。', - mandatoryContinue: '继续安装更新', + mandatoryVersion: '新版本:{version}', + mandatoryReadyDetail: '更新期间应用将暂时关闭,完成后会自动打开。', + mandatoryDeferred: '进行中的任务会继续运行。请完成更新后再操作应用。', + mandatoryContinue: '继续更新', mandatoryInspecting: '正在检查任务状态…', - mandatoryStopping: '正在安全结束应用中的任务。', + mandatoryStopping: '正在停止任务…', mandatoryRestarting: '应用即将重启,请稍候。', mandatoryDownloadFailed: '更新文件下载或准备失败,请重试。', mandatoryInstallFailed: '更新尚未安装,请重新检查任务后重试。', - mandatoryOpenHelp: '若页面未打开,可', - mandatoryReopen: '重新打开官网', - mandatoryCopied: '已复制链接', - mandatoryCopyFailed: '复制失败,请手动选择下方地址复制。', - mandatoryAddress: '下载地址', + mandatoryOpenHelp: '页面未打开?', + mandatoryReopen: '重新前往官网下载', + mandatoryCopied: '链接已复制', + mandatoryCopyFailed: '复制失败,请手动选择并复制下方链接。', + mandatoryAddress: '下载链接', mandatoryNotification: '返回应用确认安装并重启。', } as const satisfies DesktopMessages @@ -325,7 +325,7 @@ export function formatDesktopMessage( /** * Select localized copy for an ordinary downloaded-update confirmation. * @param messages - Selected Desktop dictionary. - * @param version - Prepared update version without a leading v. + * @param version - Prepared update version, including any prerelease suffix. * @param platform - Operating system presenting the confirmation. * @returns The versioned title and installation guidance. */ @@ -335,7 +335,7 @@ export function desktopUpdateReadyConfirmation( platform: string, ): { message: string; detail: string } { return { - message: formatDesktopMessage(platform === 'win32' ? messages.updateDownloadedTitleWindows : messages.updateDownloadedTitle, { version }), + message: formatDesktopMessage(messages.updateDownloadedTitle, { version }), detail: platform === 'win32' ? messages.updateDownloadedDetailWindows : messages.updateDownloadedDetail, } } diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index 7849662e02..d59334bff7 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -795,7 +795,8 @@ async function main(): Promise { if (state.phase === 'error' && state.failedOperation === 'check') { await showUpdateFailure(state); return } if (state.phase === 'idle') { await ordinaryMessageBox({ type: 'info', title: locale.messages.updateCheckTitle, - message: formatDesktopMessage(locale.messages.updateCurrent, { version: app.getVersion() }) }) + message: locale.messages.updateCurrent, + detail: formatDesktopMessage(locale.messages.updateCurrentDetail, { version: app.getVersion() }) }) return } if (state.phase === 'ready' || (state.phase === 'error' && state.failedOperation === 'install')) { @@ -807,8 +808,9 @@ async function main(): Promise { } if (state.phase !== 'available' && !(state.phase === 'error' && state.failedOperation === 'download')) return if (manual) { - const result = await ordinaryMessageBox({ title: locale.messages.updateCheckTitle, message: locale.messages.updateAvailable, - detail: formatDesktopMessage(locale.messages.updateDetail, { version: state.version ?? '' }), + const result = await ordinaryMessageBox({ title: locale.messages.updateCheckTitle, + message: formatDesktopMessage(locale.messages.updateAvailable, { version: state.version ?? '' }), + detail: locale.messages.updateDetail, buttons: [locale.messages.updateDownload], cancelId: 1 }) if (result.response !== 0) return } diff --git a/apps/desktop/src/mandatory-update-window.ts b/apps/desktop/src/mandatory-update-window.ts index 724df4ff97..3713883600 100644 --- a/apps/desktop/src/mandatory-update-window.ts +++ b/apps/desktop/src/mandatory-update-window.ts @@ -243,7 +243,11 @@ export class DesktopMandatoryUpdateWindow { } private view(): MandatoryUpdateView { - return { locale: this.options.locale, policy: this.options.policy(), update: this.options.update(), + const locale = process.platform === 'win32' + ? { ...this.options.locale, messages: { ...this.options.locale.messages, + mandatoryReadyDetail: this.options.locale.messages.updateDownloadedDetailWindows } } + : this.options.locale + return { locale, policy: this.options.policy(), update: this.options.update(), deferred: this.deferred, ...(this.confirmation === undefined ? {} : { confirmation: { version: this.confirmation.version, active: this.confirmation.active, revision: this.confirmation.revision } }), diff --git a/apps/desktop/tests/expected/mandatory-update-zh.json b/apps/desktop/tests/expected/mandatory-update-zh.json index fbe0356bcc..13a960e51e 100644 --- a/apps/desktop/tests/expected/mandatory-update-zh.json +++ b/apps/desktop/tests/expected/mandatory-update-zh.json @@ -1,8 +1,8 @@ { "title": "仍有进行中的任务", - "detail": "重启更新可能中断这些任务,是否要继续更新?", + "detail": "更新将停止进行中的任务并重启应用,是否继续?", "status": "", - "version": "V1.0.1-nightly.1", + "version": "新版本:1.0.1-nightly.1", "buttons": [ "停止任务并更新", "稍后更新" diff --git a/apps/desktop/tests/expected/policy-login-en.json b/apps/desktop/tests/expected/policy-login-en.json index 3415d20680..822c1a0f30 100644 --- a/apps/desktop/tests/expected/policy-login-en.json +++ b/apps/desktop/tests/expected/policy-login-en.json @@ -1,18 +1,18 @@ { "returned": [ "Checking for updates…", - "No updates available. Current version: V1.0.0", - "This is a test build. Checking update requirements needs Feishu sign-in. Signing in does not download or install an update." + "You’re up to date!", + "Sign in with Feishu to check update requirements for this test build. Signing in will not download or install an update." ], "cancelled": [ "Checking for updates…", - "No updates available. Current version: V1.0.0", - "This is a test build. Checking update requirements needs Feishu sign-in. Signing in does not download or install an update." + "You’re up to date!", + "Sign in with Feishu to check update requirements for this test build. Signing in will not download or install an update." ], "failed": [ "Checking for updates…", - "No updates available. Current version: V1.0.0", - "This is a test build. Checking update requirements needs Feishu sign-in. Signing in does not download or install an update.", - "Test environment sign-in did not complete. Please check your connection and try again." + "You’re up to date!", + "Sign in with Feishu to check update requirements for this test build. Signing in will not download or install an update.", + "Feishu sign-in did not complete. Please try again." ] } diff --git a/apps/desktop/tests/expected/update-guidance-en.json b/apps/desktop/tests/expected/update-guidance-en.json new file mode 100644 index 0000000000..13becc60a1 --- /dev/null +++ b/apps/desktop/tests/expected/update-guidance-en.json @@ -0,0 +1,49 @@ +[ + { + "title": "Update to continue", + "detail": "This version is no longer supported. Update to continue. Tasks in progress will keep running until you confirm installation and restart.", + "version": "New version: 0.1.7-alpha.2", + "update": "Download update" + }, + { + "title": "Update ready", + "detail": "The app will close during the update and reopen automatically when it is complete.", + "version": "New version: 0.1.7-alpha.2", + "update": "Install and Restart" + }, + { + "title": "Tasks are still in progress", + "detail": "Updating will stop the tasks in progress and restart the app. Continue?", + "version": "New version: 0.1.7-alpha.2", + "update": "Stop tasks and update", + "later": "Update later" + }, + { + "title": "Update ready", + "detail": "Tasks in progress will keep running. Complete the update before using the app again.", + "version": "New version: 0.1.7-alpha.2", + "update": "Continue update" + }, + { + "title": "Preparing to restart…", + "detail": "Stopping tasks…", + "version": "New version: 0.1.7-alpha.2" + }, + { + "title": "Update to continue", + "detail": "This version is no longer supported. Update to continue. Tasks in progress will keep running until you confirm installation and restart.", + "version": "New version: 0.1.7-alpha.2", + "error": "The update has not been installed. Check the tasks again and retry.", + "update": "Retry update", + "page": "Download from the official website" + }, + { + "title": "Update to continue", + "detail": "This version is no longer supported. Update to continue. Tasks in progress will keep running until you confirm installation and restart.", + "version": "", + "error": "No compatible update was found. Check again or contact support.", + "page": "Open the official download page again", + "browser-message": "Could not open the official download page. Copy the link and open it in your browser.", + "copy-message": "Could not copy the link. Select and copy it below." + } +] diff --git a/apps/desktop/tests/expected/update-guidance-zh-CN.json b/apps/desktop/tests/expected/update-guidance-zh-CN.json new file mode 100644 index 0000000000..8ff226d70e --- /dev/null +++ b/apps/desktop/tests/expected/update-guidance-zh-CN.json @@ -0,0 +1,49 @@ +[ + { + "title": "请更新后继续使用", + "detail": "当前版本已停止支持,请更新后继续使用。确认安装并重启前,进行中的任务会继续运行。", + "version": "新版本:0.1.7-alpha.2", + "update": "下载更新" + }, + { + "title": "更新已准备就绪", + "detail": "更新期间应用将暂时关闭,完成后会自动打开。", + "version": "新版本:0.1.7-alpha.2", + "update": "安装并重启" + }, + { + "title": "仍有进行中的任务", + "detail": "更新将停止进行中的任务并重启应用,是否继续?", + "version": "新版本:0.1.7-alpha.2", + "update": "停止任务并更新", + "later": "稍后更新" + }, + { + "title": "更新已准备就绪", + "detail": "进行中的任务会继续运行。请完成更新后再操作应用。", + "version": "新版本:0.1.7-alpha.2", + "update": "继续更新" + }, + { + "title": "正在准备重启…", + "detail": "正在停止任务…", + "version": "新版本:0.1.7-alpha.2" + }, + { + "title": "请更新后继续使用", + "detail": "当前版本已停止支持,请更新后继续使用。确认安装并重启前,进行中的任务会继续运行。", + "version": "新版本:0.1.7-alpha.2", + "error": "更新尚未安装,请重新检查任务后重试。", + "update": "重试更新", + "page": "前往官网下载" + }, + { + "title": "请更新后继续使用", + "detail": "当前版本已停止支持,请更新后继续使用。确认安装并重启前,进行中的任务会继续运行。", + "version": "", + "error": "暂未找到适用的更新,请重新检查或联系支持人员。", + "page": "重新前往官网下载", + "browser-message": "无法打开官网下载页面,请复制链接后在浏览器中打开。", + "copy-message": "复制失败,请手动选择并复制下方链接。" + } +] diff --git a/apps/desktop/tests/expected/update-restart-darwin-en-US.json b/apps/desktop/tests/expected/update-restart-darwin-en-US.json index 599f39fa68..744475cb22 100644 --- a/apps/desktop/tests/expected/update-restart-darwin-en-US.json +++ b/apps/desktop/tests/expected/update-restart-darwin-en-US.json @@ -1,6 +1,6 @@ { - "message": "DeepSeek Harness v0.1.99 downloaded", - "detail": "The update package has downloaded. Select “Install and Restart” to restart the app and begin installation.", + "message": "Version 0.1.7-alpha.2 is ready to install", + "detail": "The app will close during the update and reopen automatically when it is complete.", "buttons": [ "Install and Restart" ] diff --git a/apps/desktop/tests/expected/update-restart-darwin-zh-CN.json b/apps/desktop/tests/expected/update-restart-darwin-zh-CN.json index d7ea2daa57..0bc196f79a 100644 --- a/apps/desktop/tests/expected/update-restart-darwin-zh-CN.json +++ b/apps/desktop/tests/expected/update-restart-darwin-zh-CN.json @@ -1,6 +1,6 @@ { - "message": "DeepSeek Harness v0.1.99 下载完成", - "detail": "安装包已下载完毕,点击“安装并重启”,即刻重启客户端,开始部署。", + "message": "新版本 0.1.7-alpha.2 已准备就绪", + "detail": "更新期间应用将暂时关闭,完成后会自动打开。", "buttons": [ "安装并重启" ] diff --git a/apps/desktop/tests/expected/update-restart-win32-en-US.json b/apps/desktop/tests/expected/update-restart-win32-en-US.json index 0e8af77ca2..71a7f43253 100644 --- a/apps/desktop/tests/expected/update-restart-win32-en-US.json +++ b/apps/desktop/tests/expected/update-restart-win32-en-US.json @@ -1,5 +1,5 @@ { - "message": "New version v0.1.99 is ready", + "message": "Version 0.1.7-alpha.2 is ready to install", "detail": "The app will close temporarily during the update and reopen automatically when it is complete.\n\nThe update may take some time. Please wait and do not launch the app again during installation.", "buttons": [ "Install and Restart" diff --git a/apps/desktop/tests/expected/update-restart-win32-zh-CN.json b/apps/desktop/tests/expected/update-restart-win32-zh-CN.json index 104f40b266..520dda5711 100644 --- a/apps/desktop/tests/expected/update-restart-win32-zh-CN.json +++ b/apps/desktop/tests/expected/update-restart-win32-zh-CN.json @@ -1,5 +1,5 @@ { - "message": "新版本 v0.1.99 已准备就绪", + "message": "新版本 0.1.7-alpha.2 已准备就绪", "detail": "更新期间应用将暂时关闭,完成后会自动打开。\n\n更新可能需要一些时间,请耐心等待,期间请勿重复启动应用。", "buttons": [ "安装并重启" diff --git a/apps/desktop/tests/fixtures/workspace-updates.mjs b/apps/desktop/tests/fixtures/workspace-updates.mjs index b136c79ce0..b7f79c777b 100644 --- a/apps/desktop/tests/fixtures/workspace-updates.mjs +++ b/apps/desktop/tests/fixtures/workspace-updates.mjs @@ -218,7 +218,7 @@ async function qualify() { const checking = await dialogWith(messages.updateChecking) await screenshot(checking, 'checking.png') server.release() - const current = await dialogWith(messages.updateCurrent.replace('{version}', app.getVersion())) + const current = await dialogWith(messages.updateCurrent) assert.equal(current, checking) await clickText(current, messages.updateAcknowledge) assert.equal(server.requests.filter(path => path === '/payload.exe').length, 0) @@ -227,7 +227,7 @@ async function qualify() { server.select('healthy', '0.1.6-nightly.1') checkMenu.click() - const available = await dialogWith(messages.updateAvailable) + const available = await dialogWith(messages.updateAvailable.replace('{version}', '0.1.6-nightly.1')) server.select('corrupt', '0.1.6-nightly.1') await clickText(available, messages.updateDownload) await waitFor(() => fixture.coordinator.state.phase === 'error', 'checksum failure') diff --git a/apps/desktop/tests/main-startup.spec.ts b/apps/desktop/tests/main-startup.spec.ts index ef750e185a..91091eaa19 100644 --- a/apps/desktop/tests/main-startup.spec.ts +++ b/apps/desktop/tests/main-startup.spec.ts @@ -555,7 +555,7 @@ describe('desktop main startup', () => { expect(testAuth.login).toHaveBeenCalledOnce() const messages = harness.dialog.showMessageBox.mock.calls.map(call => (call.at(-1) as { message: string }).message) expect(messages.filter(message => message === en.policyLoginRequired)).toHaveLength(1) - expect(messages).toContain('No updates available. Current version: V1.0.0') + expect(messages).toContain('You’re up to date!') } finally { explanation.resolve({ response: 1 }) login.resolve('cancelled') @@ -1074,7 +1074,7 @@ describe('desktop main startup', () => { vi.stubGlobal('process', { ...process, platform }) vi.spyOn(harness.app, 'getPreferredSystemLanguages').mockReturnValue([language]) await readyForUpdate() - harness.updateState = { phase: 'ready', version: '0.1.99' } + harness.updateState = { phase: 'ready', version: '0.1.7-alpha.2' } harness.dialog.showMessageBox.mockResolvedValueOnce({ response: 1 }) await expect(harness.prepareUpdate()).resolves.toBe(false) const { message, detail, buttons } = harness.dialog.showMessageBox.mock.lastCall![0] as MessageBoxOptions @@ -1453,7 +1453,7 @@ describe('desktop main startup', () => { await prompt expect(signal.aborted).toBe(true) expect(harness.dialog.showMessageBox).toHaveBeenLastCalledWith(expect.objectContaining({ - message: 'No updates available. Current version: V1.0.0', + message: 'You’re up to date!', detail: 'Current version: 1.0.0', })) }) @@ -1518,7 +1518,7 @@ describe('desktop main startup', () => { vi.stubGlobal('fetch', vi.fn().mockImplementation(() => policy.promise)) harness.updateCheck.mockResolvedValue({ phase: 'available', version: '1.0.1-nightly.1' }) harness.dialog.showMessageBox.mockImplementation(({ signal, message }: { signal?: AbortSignal; message: string }) => { - if (message !== en.updateAvailable || signal === undefined) return Promise.resolve({ response: 1 }) + if (message !== en.updateAvailable.replace('{version}', '1.0.1-nightly.1') || signal === undefined) return Promise.resolve({ response: 1 }) available.resolve(signal) return new Promise((resolve) => { signal.addEventListener('abort', () => { resolve({ response: 0 }) }, { once: true }) }) }) @@ -1555,7 +1555,7 @@ describe('desktop main startup', () => { const ordinaryShown = Promise.withResolvers() const policyShown = Promise.withResolvers() harness.dialog.showMessageBox.mockImplementation(({ message }: { message: string }) => { - if (message.startsWith('No updates available.')) { + if (message === en.updateCurrent) { ordinaryShown.resolve(undefined) return ordinaryResult.promise } diff --git a/apps/desktop/tests/mandatory-update-window.spec.ts b/apps/desktop/tests/mandatory-update-window.spec.ts index 484d01065f..901205bdb6 100644 --- a/apps/desktop/tests/mandatory-update-window.spec.ts +++ b/apps/desktop/tests/mandatory-update-window.spec.ts @@ -205,3 +205,10 @@ it('rebinds policy updates and reloads to a replacement Windows main window', () sender: window.webContents, senderFrame: window.webContents.mainFrame, })).not.toThrow() }) + +it.each(['darwin', 'win32'] as const)('includes the installation wait notice only on Windows: %s', (platform) => { + const f = setup(platform) + expect(f.view().locale.messages.mandatoryReadyDetail).toBe(platform === 'win32' + ? '更新期间应用将暂时关闭,完成后会自动打开。\n\n更新可能需要一些时间,请耐心等待,期间请勿重复启动应用。' + : '更新期间应用将暂时关闭,完成后会自动打开。') +}) diff --git a/apps/desktop/tests/update-error-renderer.spec.ts b/apps/desktop/tests/update-error-renderer.spec.ts index ad92f2df8f..54bc5d5e2e 100644 --- a/apps/desktop/tests/update-error-renderer.spec.ts +++ b/apps/desktop/tests/update-error-renderer.spec.ts @@ -95,7 +95,7 @@ it('keeps mandatory diagnostics expandable without clearing the block or authori expect([p.element('error').textContent, p.element('technical-details-label').textContent, p.element('update').textContent]).toMatchInlineSnapshot(` [ - "未能安全停止任务,更新未安装。请稍后重试。", + "未能安全停止任务,更新尚未安装,请稍后重试。", "查看技术详情", "重试更新", ] @@ -182,7 +182,7 @@ it('uses the same modal for download, verification, inspected confirmation and t p.publish({ ...p.initial, update: { phase: 'installing' }, restart: 'preparing' }) expect(p.element('detail').textContent).toBe('应用即将重启,请稍候。') p.publish({ ...p.initial, update: { phase: 'installing' }, restart: 'stopping-tasks' }) - expect(p.element('detail').textContent).toBe('正在安全结束应用中的任务。') + expect(p.element('detail').textContent).toBe('正在停止任务…') expect(p.document.querySelector('main')).toBe(modal) }) @@ -191,7 +191,7 @@ it('reveals the complete selectable address only after copy failure, without rep await expect.poll(() => p.element('error').textContent).toBe('更新文件下载或准备失败,请重试。') const originalError = p.element('error').textContent p.publish({ ...p.initial, navigation: { page: 'requested' } }) - expect(p.element('browser-message').textContent).toBe('若页面未打开,可') + expect(p.element('browser-message').textContent).toBe('页面未打开?') expect(p.element('manual-copy').hidden).toBe(true) p.publish({ ...p.initial, navigation: { page: 'failed', copy: 'failed' } }) expect(p.element('manual-copy').hidden).toBe(false) @@ -199,11 +199,11 @@ it('reveals the complete selectable address only after copy failure, without rep expect((p.element('address') as HTMLTextAreaElement).readOnly).toBe(true) expect(p.element('error').textContent).toBe(originalError) expect(p.element('technical-details-content').textContent).toBe('HASH_MISMATCH') - expect(p.element('copy-message').textContent).toBe('复制失败,请手动选择下方地址复制。') + expect(p.element('copy-message').textContent).toBe('复制失败,请手动选择并复制下方链接。') p.publish({ ...p.initial, navigation: { page: 'failed', copy: 'copied' } }) expect(p.element('manual-copy').hidden).toBe(true) expect((p.element('address') as HTMLTextAreaElement).value).toBe('') - expect(p.element('copy').textContent).toBe('已复制链接') + expect(p.element('copy').textContent).toBe('链接已复制') expect(p.action).not.toHaveBeenCalled() }) @@ -215,3 +215,31 @@ it('renders server markup literally in a dedicated safety case', async () => { expect(p.element('title').childElementCount).toBe(0) expect(p.element('detail').childElementCount).toBe(0) }) + +it.each(['en', 'zh-CN'])('records mandatory update guidance and actions across its states: %s', async (language) => { + const p = page('mandatory-update') + const locale = resolveDesktopLocale(language) + const base: MandatoryUpdateView = { locale, deferred: false, policy: { blocking: true, checking: false, + page: 'https://example.invalid/download' }, update: { phase: 'available', version: '0.1.7-alpha.2' } } + let publish!: (view: MandatoryUpdateView) => void + const api: MandatoryUpdateApi = { status: async () => base, action: async () => {}, + subscribe: (listener) => { publish = listener; return () => {} } } + Object.defineProperty(p.dom.window, 'dshMandatoryUpdate', { value: api }) + p.run() + await expect.poll(() => p.element('title').textContent).toBe(locale.messages.mandatoryTitle) + const states: MandatoryUpdateView[] = [base, + { ...base, update: { ...base.update, phase: 'ready' }, confirmation: { active: false, version: '0.1.7-alpha.2', revision: 1 } }, + { ...base, update: { ...base.update, phase: 'ready' }, confirmation: { active: true, version: '0.1.7-alpha.2', revision: 1 } }, + { ...base, update: { ...base.update, phase: 'ready' }, deferred: true }, + { ...base, update: { ...base.update, phase: 'installing' }, restart: 'stopping-tasks' }, + { ...base, update: { ...base.update, phase: 'error', failedOperation: 'install' } }, + { ...base, update: { phase: 'idle' }, navigation: { page: 'failed', copy: 'failed' } }, + ] + const snapshots = states.map((state) => { + publish(state) + return Object.fromEntries(['title', 'detail', 'version', 'error', 'update', 'later', 'page', 'browser-message', 'copy-message'] + .filter(id => !p.element(id).hidden && !p.element(id).closest('[hidden]')) + .map(id => [id, p.element(id).textContent])) + }) + await expect(JSON.stringify(snapshots, null, 2) + '\n').toMatchFileSnapshot(`./expected/update-guidance-${language}.json`) +}) diff --git a/apps/web/tests/assembled-remote.ts b/apps/web/tests/assembled-remote.ts index 7569106fc6..e8d0141ba1 100644 --- a/apps/web/tests/assembled-remote.ts +++ b/apps/web/tests/assembled-remote.ts @@ -223,8 +223,10 @@ export function createAssembledRemote(options: AssembledRemoteOptions = {}): Ass return ok(undefined) }) mock.unary('session/list', () => ok({ items: structuredClone(sessions) })) - // The shipped Web composition mounts the Schedule client, whose catalog and - // per-Session list read empty here: these scenarios drive no reminder. + // The shipped bundle layers this graph composes carry the `ui-schedule` row + // disabled, so the Schedule client does not mount and no case here reads the + // catalog or the per-Session list. Both stay declared so an enabled + // composition reads empty lists instead of a missing-rule failure. mock.unary('schedule/catalog', () => ok([])) mock.unary('schedule/list', () => ok([])) mock.unary('session/projections', (request: unknown) => { diff --git a/apps/web/tests/expected/plugin-config/official.expected.md b/apps/web/tests/expected/plugin-config/official.expected.md index d408f284f3..247d6ac308 100644 --- a/apps/web/tests/expected/plugin-config/official.expected.md +++ b/apps/web/tests/expected/plugin-config/official.expected.md @@ -3,7 +3,7 @@ - button "刷新" - button "添加插件" - heading "官方" [level=3] -- text: "7" +- text: "8" - list: - listitem: - button "查看 智能体团队": 智能体团队 @@ -13,6 +13,10 @@ - button "查看 自动授权审查": 自动授权审查 - text: 实验性 提供自动审查权限模式,由模型在每次工具调用前判断是否授权。 - switch "启用 自动授权审查" + - listitem: + - button "查看 定时与时间上下文": 定时与时间上下文 + - text: 实验性 启用持久提醒、自动化任务页面与逐步时钟上下文。 + - switch "启用 定时与时间上下文" - listitem: - button "查看 语音输入": 语音输入 - text: 实验性 在本机使用 SenseVoice 转写录音;首次使用需安装依赖 diff --git a/apps/web/tests/expected/plugin-manager/live-enabled.expected.md b/apps/web/tests/expected/plugin-manager/live-enabled.expected.md index 2f7b9bc832..4bd8ed064e 100644 --- a/apps/web/tests/expected/plugin-manager/live-enabled.expected.md +++ b/apps/web/tests/expected/plugin-manager/live-enabled.expected.md @@ -3,7 +3,7 @@ - button "刷新" - button "添加插件" - heading "官方" [level=3] -- text: "7" +- text: "8" - list: - listitem: - button "查看 智能体团队": 智能体团队 @@ -13,6 +13,10 @@ - button "查看 自动授权审查": 自动授权审查 - text: 实验性 提供自动审查权限模式,由模型在每次工具调用前判断是否授权。 - switch "启用 自动授权审查" + - listitem: + - button "查看 定时与时间上下文": 定时与时间上下文 + - text: 实验性 启用持久提醒、自动化任务页面与逐步时钟上下文。 + - switch "启用 定时与时间上下文" - listitem: - button "查看 语音输入": 语音输入 - text: 实验性 在本机使用 SenseVoice 转写录音;首次使用需安装依赖 diff --git a/apps/web/tests/expected/plugin-manager/manager.expected.md b/apps/web/tests/expected/plugin-manager/manager.expected.md index 61c517953b..b188be8787 100644 --- a/apps/web/tests/expected/plugin-manager/manager.expected.md +++ b/apps/web/tests/expected/plugin-manager/manager.expected.md @@ -3,7 +3,7 @@ - button "刷新" - button "添加插件" - heading "官方" [level=3] -- text: "7" +- text: "8" - list: - listitem: - button "查看 智能体团队": 智能体团队 @@ -13,6 +13,10 @@ - button "查看 自动授权审查": 自动授权审查 - text: 实验性 提供自动审查权限模式,由模型在每次工具调用前判断是否授权。 - switch "启用 自动授权审查" + - listitem: + - button "查看 定时与时间上下文": 定时与时间上下文 + - text: 实验性 启用持久提醒、自动化任务页面与逐步时钟上下文。 + - switch "启用 定时与时间上下文" - listitem: - button "查看 语音输入": 语音输入 - text: 实验性 在本机使用 SenseVoice 转写录音;首次使用需安装依赖 diff --git a/apps/web/tests/expected/workspace-new-session-folding/first-batch.expected.md b/apps/web/tests/expected/workspace-new-session-folding/first-batch.expected.md index 6009227833..ec04e12324 100644 --- a/apps/web/tests/expected/workspace-new-session-folding/first-batch.expected.md +++ b/apps/web/tests/expected/workspace-new-session-folding/first-batch.expected.md @@ -1,18 +1,18 @@ - tree "Sessions": - treeitem "{{workspace}}" [expanded] - treeitem "New Session" [selected] - - treeitem "{{workspace}} 1min" - - treeitem "{{workspace}} 1min" - - treeitem "{{workspace}} 1min" - - treeitem "{{workspace}} 1min" - - treeitem "{{workspace}} 1min" - - treeitem "{{workspace}} Session actions for {{workspace}} Archive session Pin session": - - text: {{workspace}} - - button "Session actions for {{workspace}}" + - treeitem "Untitled 1min" + - treeitem "Untitled 1min" + - treeitem "Untitled 1min" + - treeitem "Untitled 1min" + - treeitem "Untitled 1min" + - treeitem "Untitled Session actions for Untitled Archive session Pin session": + - text: Untitled + - button "Session actions for Untitled" - button "Archive session" - button "Pin session" - - treeitem "{{workspace}} 1min" - - treeitem "{{workspace}} 1min" - - treeitem "{{workspace}} 1min" - - treeitem "{{workspace}} 1min" + - treeitem "Untitled 1min" + - treeitem "Untitled 1min" + - treeitem "Untitled 1min" + - treeitem "Untitled 1min" - button "Show 6 more sessions" diff --git a/apps/web/tests/expected/workspace-new-session-folding/sidebar.expected.md b/apps/web/tests/expected/workspace-new-session-folding/sidebar.expected.md index 8778baac12..1aeb3e5a77 100644 --- a/apps/web/tests/expected/workspace-new-session-folding/sidebar.expected.md +++ b/apps/web/tests/expected/workspace-new-session-folding/sidebar.expected.md @@ -4,9 +4,9 @@ - button "Workspace actions for {{workspace}}" - button "New session in {{workspace}}" - treeitem "New Session" [selected] - - treeitem "{{workspace}} 1min" - - treeitem "{{workspace}} 1min" - - treeitem "{{workspace}} 1min" - - treeitem "{{workspace}} 1min" - - treeitem "{{workspace}} 1min" + - treeitem "Untitled 1min" + - treeitem "Untitled 1min" + - treeitem "Untitled 1min" + - treeitem "Untitled 1min" + - treeitem "Untitled 1min" - button "Show 11 more sessions" diff --git a/apps/web/tests/feedback-release.e2e.ts b/apps/web/tests/feedback-release.e2e.ts index 4643da9cf8..3de0ea2fca 100644 --- a/apps/web/tests/feedback-release.e2e.ts +++ b/apps/web/tests/feedback-release.e2e.ts @@ -27,6 +27,7 @@ const MODE = webSnapshotMode() interface OtlpCapture { resourceLogs: { scopeLogs: { logRecords: { + eventName: string attributes: { key: string; value: { stringValue?: string; intValue?: number | string } }[] }[] }[] }[] } @@ -53,8 +54,11 @@ describe.each(MODE === 'record' ? ['deepseek-official'] : ['deepseek-official', return capture.resourceLogs.flatMap(resource => resource.scopeLogs.flatMap(scope => scope.logRecords.map((record) => { const attribute = (key: string) => record.attributes.find(value => value.key === key)?.value - return [attribute('session.id')?.stringValue, Number(attribute('event.seq')?.intValue), - attribute('event.type')?.stringValue] as [string | undefined, number, string | undefined] + expect(record.eventName).toBe('session-log') + const content = attribute('content')?.stringValue + expect(typeof content).toBe('string') + const event = JSON.parse(content!) as SessionEvent + return [attribute('sessionId')?.stringValue, event.seq, event.type] as [string | undefined, number, string | undefined] }))) }) } @@ -71,6 +75,10 @@ describe.each(MODE === 'record' ? ['deepseek-official'] : ['deepseek-official', const expected = authorized.map(event => [sessionId, event.seq, event.type]) // No teardown, flush hook, or subsequent interaction may cause this delivery. await expect.poll(captured, { timeout: 10_000 }).toEqual(expected) + const uploadedEvents = uploads.flatMap(upload => (JSON.parse(upload) as OtlpCapture).resourceLogs + .flatMap(resource => resource.scopeLogs.flatMap(scope => scope.logRecords)) + .map(record => JSON.parse(record.attributes.find(attribute => attribute.key === 'content')!.value.stringValue!) as SessionEvent)) + expect(uploadedEvents).toEqual(authorized) suffixes.push(authorized.slice(releasedCount).map(event => event.type)) releasedCount = authorized.length expect(scaffold.ctx.agents.get(sessionId)).toBe(agent) @@ -268,7 +276,9 @@ describe.each(MODE === 'record' ? ['deepseek-official'] : ['deepseek-official', } }) await compareOrRefreshGolden(RELEASE_EXPECTED, JSON.stringify({ - mode: 'FEEDBACK_ONLY', feedback, + mode: 'FEEDBACK_ONLY', + eventName: (JSON.parse(uploads[0]!) as OtlpCapture).resourceLogs[0]!.scopeLogs[0]!.logRecords[0]!.eventName, + feedback, // The prefix is compared with each provider's actual canonical log above. laterSubmissionSuffixes: suffixes.slice(1), }, null, 2), MODE) diff --git a/apps/web/tests/fixtures/time-context-every-step.patch.yml b/apps/web/tests/fixtures/time-context-every-step.patch.yml index dcff9f2b61..7a1937d44d 100644 --- a/apps/web/tests/fixtures/time-context-every-step.patch.yml +++ b/apps/web/tests/fixtures/time-context-every-step.patch.yml @@ -1,9 +1,6 @@ # Browser-zone assertions need a fresh reading after the non-browser preparation turn. +# The Schedule rows stay disabled in the shipped composition; the scenario enables them +# with the shipped `@deepseek-ai/dsh-experimental-schedule-bundle` patch. - id: time-context - disabled: false config: refreshIntervalMs: 0 -- id: schedule - disabled: false -- id: ui-schedule - disabled: false diff --git a/apps/web/tests/models-settings.e2e.ts b/apps/web/tests/models-settings.e2e.ts index 68857956f3..4a206e7299 100644 --- a/apps/web/tests/models-settings.e2e.ts +++ b/apps/web/tests/models-settings.e2e.ts @@ -345,6 +345,8 @@ describe('web e2e: Models settings page configures a dormant provider', () => { await expect(scaffold.ctx.llm.resolveModelInfo('acme-gateway', 'acme-large')).resolves.toMatchObject({ inputModalities: ['image'], }) + // The saved notice confirms the browser directory refresh after the editor closes. + await dialog.getByText('已保存 Acme 网关 (acme-gateway)。', { exact: true }).waitFor({ timeout: 10_000 }) await dialog.getByRole('button', { name: '编辑 Acme 网关 (acme-gateway)' }).click() await dialog.getByText('自定义设置').click() await dialog.getByRole('button', { name: '模型选项 1' }).click() diff --git a/apps/web/tests/schedule-after.e2e.ts b/apps/web/tests/schedule-after.e2e.ts index 84cd580b8b..c0f131f3d3 100644 --- a/apps/web/tests/schedule-after.e2e.ts +++ b/apps/web/tests/schedule-after.e2e.ts @@ -44,6 +44,9 @@ declare module '@deepseek-ai/dsh-llm' { } const MODE = webSnapshotMode() +/** The optional Schedule bundle is the switch these scenarios turn on. */ +const SCHEDULE_BUNDLE = fileURLToPath(new URL('../../../packages/experimental/schedule-bundle/cordis.patch.yml', import.meta.url)) +const TIME_CONTEXT_EVERY_STEP = fileURLToPath(new URL('./fixtures/time-context-every-step.patch.yml', import.meta.url)) const SNAPSHOT_DIR = fileURLToPath(new URL('./expected/schedule-after', import.meta.url)) const AFTER_EXPECTED = join(SNAPSHOT_DIR, 'conversation.expected.md') const AT_EXPECTED = join(SNAPSHOT_DIR, 'at-conversation.expected.md') @@ -272,7 +275,7 @@ describe.skipIf(MODE === 'record')('web e2e: conversational reminders', () => { beforeAll(async () => { scaffold = await launchWebScaffold({ - extraOverlayPath: fileURLToPath(new URL('./fixtures/time-context-every-step.patch.yml', import.meta.url)), + extraOverlayPath: [SCHEDULE_BUNDLE, TIME_CONTEXT_EVERY_STEP], }) scaffold.ctx.effect( () => scaffold.ctx.llm.registerAdapter([AFTER_PROVIDER], afterAdapter), @@ -710,7 +713,7 @@ describe.skipIf(MODE === 'record')('web e2e: active Schedule catalog', () => { beforeAll(async () => { const fixture = await readFile(CATALOG_FIXTURE, 'utf8') scaffold = await launchWebScaffold({ - extraOverlayPath: fileURLToPath(new URL('./fixtures/time-context-every-step.patch.yml', import.meta.url)), + extraOverlayPath: [SCHEDULE_BUNDLE, TIME_CONTEXT_EVERY_STEP], }) await seedSession(scaffold, fixture, CATALOG_SESSION_ID, 'standard') const records = foldScheduleEvents(fixture.trim().split('\n').slice(1).map(line => JSON.parse(line) as SessionEvent)).active diff --git a/apps/web/tests/workspace-new-session-folding.e2e.ts b/apps/web/tests/workspace-new-session-folding.e2e.ts index e23e0e9a80..9567153471 100644 --- a/apps/web/tests/workspace-new-session-folding.e2e.ts +++ b/apps/web/tests/workspace-new-session-folding.e2e.ts @@ -71,7 +71,7 @@ describe('web e2e: blank New Session folding quota', () => { const sidebar = page.getByRole('tree', { name: 'Sessions' }) await expect.poll(() => sidebar.getByRole('treeitem').count(), { timeout: 15_000 }).toBe(7) expect(await sidebar.getByText('New Session', { exact: true }).count()).toBe(1) - expect(await sidebar.getByText(basename(scaffold.workspaceCwd), { exact: true }).count()).toBe(6) + expect(await sidebar.getByText('Untitled', { exact: true }).count()).toBe(5) const showMore = sidebar.getByRole('button', { name: 'Show 11 more sessions' }) await showMore.waitFor({ timeout: 15_000 }) await compareOrRefreshGolden( @@ -82,7 +82,7 @@ describe('web e2e: blank New Session folding quota', () => { await showMore.click() await expect.poll(() => sidebar.getByRole('treeitem').count(), { timeout: 10_000 }).toBe(12) - expect(await sidebar.getByText(basename(scaffold.workspaceCwd), { exact: true }).count()).toBe(11) + expect(await sidebar.getByText('Untitled', { exact: true }).count()).toBe(10) await compareOrRefreshGolden( join(EXPECTED_DIR, 'first-batch.expected.md'), await captureStableAria(page, '[role="tree"][aria-label="Sessions"]', scaffold.workspaceCwd), diff --git a/docs/capability-seams.i18n.yaml b/docs/capability-seams.i18n.yaml index 7e957d5cdd..abf47a1663 100644 --- a/docs/capability-seams.i18n.yaml +++ b/docs/capability-seams.i18n.yaml @@ -6,5 +6,5 @@ en: 41d772192075e133 zh: 5ef3da69b4de1d8a /capability-seams-and-core-services: - en: 47c0acd730775da8 - zh: e333f017e46a4659 + en: 00ee1236e43f5538 + zh: aed1f351bf657eea diff --git a/docs/capability-seams.md b/docs/capability-seams.md index ffa8abf4a2..4d6fd35fa2 100644 --- a/docs/capability-seams.md +++ b/docs/capability-seams.md @@ -114,11 +114,13 @@ flowchart LR svc_authorization["ctx.authorization
Authorization flow registry"] pkg_client_product_analytics["client-product-analytics"] svc_productAnalytics["ctx.productAnalytics
Desktop interaction collection"] + pkg_otel["otel"] + svc_otel["ctx.otel
Shared OTel reporting channels"] pkg_host_product_telemetry_otel["host-product-telemetry-otel"] + pkg_session_telemetry_otel["session-telemetry-otel"] svc_productTelemetry["ctx.productTelemetry
Product usage event sender"] pkg_session_telemetry["session-telemetry"] svc_sessionTelemetry["ctx.sessionTelemetry
Session telemetry seam"] - pkg_session_telemetry_otel["session-telemetry-otel"] pkg_storage["storage"] svc_storage["ctx.storage
Non-session storage hub"] pkg_storage_json["storage-json"] @@ -351,6 +353,7 @@ flowchart LR pkg_mcp_resources --> svc_mcpResources pkg_message_feedback --> svc_messageFeedback pkg_office_to_pdf --> svc_officeToPdf + pkg_otel --> svc_otel pkg_permission_presets --> svc_permissionPresets pkg_plan_mode --> svc_planMode pkg_plugin_manager --> svc_pluginManager @@ -475,6 +478,8 @@ flowchart LR svc_lsp --> pkg_tool_lsp svc_mcpResources --> pkg_mcp_resources svc_officeToPdf --> pkg_client_ui_sidebar_documentpreview + svc_otel --> pkg_host_product_telemetry_otel + svc_otel --> pkg_session_telemetry_otel svc_pluginManager --> pkg_plugin_manager svc_pluginManager --> pkg_ui_settings_plugin_inventory svc_pluginRegistryProbe --> pkg_client_ui_plugin_manager @@ -608,7 +613,8 @@ flowchart LR | `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 launch-time collection policy. | +| `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. | +| `ctx.otel` | `service` | [`otel`](../packages/telemetry/otel) | - | [`host-product-telemetry-otel`](../packages/host/product-telemetry-otel), [`session-telemetry-otel`](../packages/session/session-telemetry-otel) | - | Product analytics and Session feedback adapters create independent reporting channels through one injected service. | | `ctx.productTelemetry` | `service` | [`host-product-telemetry-otel`](../packages/host/product-telemetry-otel) | - | - | - | Exports explicitly submitted analytics events through OTLP/HTTP; mounting alone collects nothing. | | `ctx.sessionTelemetry` | `seam` | [`session-telemetry`](../packages/session/session-telemetry) | [`session-telemetry-otel`](../packages/session/session-telemetry-otel) | - | - | The seam captures, redacts, and hands session records to one backend; nothing else consumes the service — its output leaves the process. | | `ctx.storage` | `seam` | [`storage`](../packages/storage/storage) | [`storage-json`](../packages/storage/storage-json), [`storage-sqlite`](../packages/storage/storage-sqlite) | [`storage-domain`](../packages/storage/storage-domain) | - | Backends register side by side under names; data forms (domain first) mount on the hub and translate typed operations into opaque KV-unit primitives. | diff --git a/docs/capability-seams.zh.md b/docs/capability-seams.zh.md index f66d643eeb..2068e7f369 100644 --- a/docs/capability-seams.zh.md +++ b/docs/capability-seams.zh.md @@ -116,11 +116,13 @@ flowchart LR svc_authorization["ctx.authorization
Authorization flow registry"] pkg_client_product_analytics["client-product-analytics"] svc_productAnalytics["ctx.productAnalytics
Desktop interaction collection"] + pkg_otel["otel"] + svc_otel["ctx.otel
Shared OTel reporting channels"] pkg_host_product_telemetry_otel["host-product-telemetry-otel"] + pkg_session_telemetry_otel["session-telemetry-otel"] svc_productTelemetry["ctx.productTelemetry
Product usage event sender"] pkg_session_telemetry["session-telemetry"] svc_sessionTelemetry["ctx.sessionTelemetry
Session telemetry seam"] - pkg_session_telemetry_otel["session-telemetry-otel"] pkg_storage["storage"] svc_storage["ctx.storage
Non-session storage hub"] pkg_storage_json["storage-json"] @@ -353,6 +355,7 @@ flowchart LR pkg_mcp_resources --> svc_mcpResources pkg_message_feedback --> svc_messageFeedback pkg_office_to_pdf --> svc_officeToPdf + pkg_otel --> svc_otel pkg_permission_presets --> svc_permissionPresets pkg_plan_mode --> svc_planMode pkg_plugin_manager --> svc_pluginManager @@ -477,6 +480,8 @@ flowchart LR svc_lsp --> pkg_tool_lsp svc_mcpResources --> pkg_mcp_resources svc_officeToPdf --> pkg_client_ui_sidebar_documentpreview + svc_otel --> pkg_host_product_telemetry_otel + svc_otel --> pkg_session_telemetry_otel svc_pluginManager --> pkg_plugin_manager svc_pluginManager --> pkg_ui_settings_plugin_inventory svc_pluginRegistryProbe --> pkg_client_ui_plugin_manager @@ -610,7 +615,8 @@ flowchart LR | `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) | - | - | - | 根据启动时的采集策略接收指定桌面事件、补充可用登录身份并监听实时压缩。 | +| `ctx.productAnalytics` | `service` | [`client-product-analytics`](../packages/client/product-analytics) | - | - | - | 根据 Host 动态采集策略接收指定桌面事件、补充可用登录身份并监听实时压缩。 | +| `ctx.otel` | `service` | [`otel`](../packages/telemetry/otel) | - | [`host-product-telemetry-otel`](../packages/host/product-telemetry-otel), [`session-telemetry-otel`](../packages/session/session-telemetry-otel) | - | 产品埋点和 Session 反馈适配器通过同一个注入服务创建独立上报通道。 | | `ctx.productTelemetry` | `service` | [`host-product-telemetry-otel`](../packages/host/product-telemetry-otel) | - | - | - | 通过 OTLP/HTTP 发送显式提交的分析事件;仅挂载插件不会采集信息。 | | `ctx.sessionTelemetry` | `seam` | [`session-telemetry`](../packages/session/session-telemetry) | [`session-telemetry-otel`](../packages/session/session-telemetry-otel) | - | - | 该 seam 捕获会话记录、进行脱敏并交给一个后端;没有其他组件消费该服务,其输出会离开当前进程。 | | `ctx.storage` | `seam` | [`storage`](../packages/storage/storage) | [`storage-json`](../packages/storage/storage-json), [`storage-sqlite`](../packages/storage/storage-sqlite) | [`storage-domain`](../packages/storage/storage-domain) | - | 各后端以不同名称并列注册;数据形态(领域优先)挂载到枢纽上,并将类型化操作转换为不透明的 KV 单元原语。 | diff --git a/docs/config-catalog.md b/docs/config-catalog.md index 3fbfefd92e..df067525d7 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -1455,7 +1455,8 @@ export interface Config { ## `@deepseek-ai/dsh-host-product-telemetry-otel` -- `source`: [`packages/host/product-telemetry-otel/src/index.ts:37`](../packages/host/product-telemetry-otel/src/index.ts) +- `inject`: `otel` +- `source`: [`packages/host/product-telemetry-otel/src/index.ts:18`](../packages/host/product-telemetry-otel/src/index.ts) ```ts config-catalog /** Collector routing, application identity, and bounded in-memory batch settings. */ @@ -2721,36 +2722,36 @@ export interface Config { ## `@deepseek-ai/dsh-session-telemetry-otel` -- `inject`: `sessions` +- `inject`: `sessions` · `otel` - `refs`: `BatchLogRecordProcessorOptions` (`@opentelemetry/sdk-logs`) · `OTLPExporterNodeConfigBase` (`@opentelemetry/otlp-exporter-base`) -- `source`: [`packages/session/session-telemetry-otel/src/index.ts:100`](../packages/session/session-telemetry-otel/src/index.ts) +- `source`: [`packages/session/session-telemetry-otel/src/index.ts:87`](../packages/session/session-telemetry-otel/src/index.ts) ```ts config-catalog /** - * Plugin configuration: one sharing policy, two verbatim SDK option objects, - * and one DSH-owned shutdown bound. Uploading modes validate their endpoint + * Plugin configuration: sharing policy, SDK transport options, byte/count queue + * settings, and an overall shutdown bound. Uploading modes validate their endpoint * and shutdown deadline at plugin load; `DISABLED` reads neither. */ export interface Config { /** Defaults to `FEEDBACK_ONLY`: capture session history only when feedback is explicitly submitted. */ mode?: SessionTelemetryMode /** - * Passed verbatim to the SDK's OTLP/HTTP log exporter — the complete - * `OTLPExporterNodeConfigBase` shape (`headers`, `timeoutMillis`, - * `compression`, `keepAlive`, …), owned and documented by the SDK. `url` - * is the one field this package requires and validates itself. + * Explicit SDK HTTP transport settings, including optional routing headers. + * Ambient credentials are not inherited. URL is required while uploading. */ exporter?: OTLPExporterNodeConfigBase & { /** Full logs endpoint (e.g. `https://collector.example.com/v1/logs`). Required outside `DISABLED`; validated at load. */ url?: string } /** - * Passed verbatim to `BatchLogRecordProcessor` (minus the exporter slot, - * which this plugin fills); the SDK owns and documents these knobs. + * Count, queue, cadence, and per-request watchdog settings for the byte-bounded + * processor. A watchdog warning never releases an unsettled transport slot. */ processor?: Omit /** Maximum time spent awaiting the SDK provider's complete shutdown path. */ shutdownTimeoutMillis?: number + /** Uncompressed OTLP request byte limit, at most 4,000,000. */ + maxRequestBytes?: number } /** Session-sharing policy selected by {@link Config.mode}. */ @@ -4374,6 +4375,7 @@ These load from a `cordis.yml` entry with no `config:` block; they declare no co | `@deepseek-ai/dsh-llm` | — | [`packages/llm/llm/src/index.ts`](../packages/llm/llm/src/index.ts) | | `@deepseek-ai/dsh-lsp` | — | [`packages/lsp/lsp/src/index.ts`](../packages/lsp/lsp/src/index.ts) | | `@deepseek-ai/dsh-mcp-resources` | `tools` | [`packages/mcp/mcp-resources/src/index.ts`](../packages/mcp/mcp-resources/src/index.ts) | +| `@deepseek-ai/dsh-otel` | — | [`packages/telemetry/otel/src/index.ts`](../packages/telemetry/otel/src/index.ts) | | `@deepseek-ai/dsh-sandbox-ssh` | `ssh` | [`packages/ssh/sandbox-ssh/src/index.ts`](../packages/ssh/sandbox-ssh/src/index.ts) | | `@deepseek-ai/dsh-session` | — | [`packages/core/session/src/index.ts`](../packages/core/session/src/index.ts) | | `@deepseek-ai/dsh-session-checkpoint-policy` | `llm` · `sessionPersistence` · `sessions` · `tools` | [`packages/session/session-checkpoint-policy/src/index.ts`](../packages/session/session-checkpoint-policy/src/index.ts) | @@ -4444,6 +4446,7 @@ Imported as libraries by other packages; a `cordis.yml` cannot load them. | `@deepseek-ai/dsh-deque` | — | [`packages/util/deque/src/index.ts`](../packages/util/deque/src/index.ts) | | `@deepseek-ai/dsh-experimental-agent-team-profile` | — | [`packages/experimental/agent-team-profile/src/index.ts`](../packages/experimental/agent-team-profile/src/index.ts) | | `@deepseek-ai/dsh-experimental-browser-use-runtime` | — | [`packages/experimental/browser-use-runtime/src/index.ts`](../packages/experimental/browser-use-runtime/src/index.ts) | +| `@deepseek-ai/dsh-experimental-schedule-bundle` | — | [`packages/experimental/schedule-bundle/src/index.ts`](../packages/experimental/schedule-bundle/src/index.ts) | | `@deepseek-ai/dsh-experimental-voice-input-bundle` | — | [`packages/experimental/voice-input-bundle/src/index.ts`](../packages/experimental/voice-input-bundle/src/index.ts) | | `@deepseek-ai/dsh-experimental-webworker-packer` | — | [`packages/experimental/webworker-packer/src/index.ts`](../packages/experimental/webworker-packer/src/index.ts) | | `@deepseek-ai/dsh-experimental-webworker-runtime` | — | [`packages/experimental/webworker-runtime/src/index.ts`](../packages/experimental/webworker-runtime/src/index.ts) | diff --git a/docs/config-catalog.zh.md b/docs/config-catalog.zh.md index 6ea803f177..52a94c7f54 100644 --- a/docs/config-catalog.zh.md +++ b/docs/config-catalog.zh.md @@ -1457,7 +1457,8 @@ export interface Config { ## `@deepseek-ai/dsh-host-product-telemetry-otel` -- `source`: [`packages/host/product-telemetry-otel/src/index.ts:37`](../packages/host/product-telemetry-otel/src/index.ts) +- `inject`: `otel` +- `source`: [`packages/host/product-telemetry-otel/src/index.ts:18`](../packages/host/product-telemetry-otel/src/index.ts) ```ts config-catalog /** Collector routing, application identity, and bounded in-memory batch settings. */ @@ -2723,36 +2724,36 @@ export interface Config { ## `@deepseek-ai/dsh-session-telemetry-otel` -- `inject`: `sessions` +- `inject`: `sessions` · `otel` - `refs`: `BatchLogRecordProcessorOptions` (`@opentelemetry/sdk-logs`) · `OTLPExporterNodeConfigBase` (`@opentelemetry/otlp-exporter-base`) -- `source`: [`packages/session/session-telemetry-otel/src/index.ts:100`](../packages/session/session-telemetry-otel/src/index.ts) +- `source`: [`packages/session/session-telemetry-otel/src/index.ts:87`](../packages/session/session-telemetry-otel/src/index.ts) ```ts config-catalog /** - * Plugin configuration: one sharing policy, two verbatim SDK option objects, - * and one DSH-owned shutdown bound. Uploading modes validate their endpoint + * Plugin configuration: sharing policy, SDK transport options, byte/count queue + * settings, and an overall shutdown bound. Uploading modes validate their endpoint * and shutdown deadline at plugin load; `DISABLED` reads neither. */ export interface Config { /** Defaults to `FEEDBACK_ONLY`: capture session history only when feedback is explicitly submitted. */ mode?: SessionTelemetryMode /** - * Passed verbatim to the SDK's OTLP/HTTP log exporter — the complete - * `OTLPExporterNodeConfigBase` shape (`headers`, `timeoutMillis`, - * `compression`, `keepAlive`, …), owned and documented by the SDK. `url` - * is the one field this package requires and validates itself. + * Explicit SDK HTTP transport settings, including optional routing headers. + * Ambient credentials are not inherited. URL is required while uploading. */ exporter?: OTLPExporterNodeConfigBase & { /** Full logs endpoint (e.g. `https://collector.example.com/v1/logs`). Required outside `DISABLED`; validated at load. */ url?: string } /** - * Passed verbatim to `BatchLogRecordProcessor` (minus the exporter slot, - * which this plugin fills); the SDK owns and documents these knobs. + * Count, queue, cadence, and per-request watchdog settings for the byte-bounded + * processor. A watchdog warning never releases an unsettled transport slot. */ processor?: Omit /** Maximum time spent awaiting the SDK provider's complete shutdown path. */ shutdownTimeoutMillis?: number + /** Uncompressed OTLP request byte limit, at most 4,000,000. */ + maxRequestBytes?: number } /** Session-sharing policy selected by {@link Config.mode}. */ @@ -4376,6 +4377,7 @@ export interface Config { | `@deepseek-ai/dsh-llm` | — | [`packages/llm/llm/src/index.ts`](../packages/llm/llm/src/index.ts) | | `@deepseek-ai/dsh-lsp` | — | [`packages/lsp/lsp/src/index.ts`](../packages/lsp/lsp/src/index.ts) | | `@deepseek-ai/dsh-mcp-resources` | `tools` | [`packages/mcp/mcp-resources/src/index.ts`](../packages/mcp/mcp-resources/src/index.ts) | +| `@deepseek-ai/dsh-otel` | — | [`packages/telemetry/otel/src/index.ts`](../packages/telemetry/otel/src/index.ts) | | `@deepseek-ai/dsh-sandbox-ssh` | `ssh` | [`packages/ssh/sandbox-ssh/src/index.ts`](../packages/ssh/sandbox-ssh/src/index.ts) | | `@deepseek-ai/dsh-session` | — | [`packages/core/session/src/index.ts`](../packages/core/session/src/index.ts) | | `@deepseek-ai/dsh-session-checkpoint-policy` | `llm` · `sessionPersistence` · `sessions` · `tools` | [`packages/session/session-checkpoint-policy/src/index.ts`](../packages/session/session-checkpoint-policy/src/index.ts) | @@ -4446,6 +4448,7 @@ export interface Config { | `@deepseek-ai/dsh-deque` | — | [`packages/util/deque/src/index.ts`](../packages/util/deque/src/index.ts) | | `@deepseek-ai/dsh-experimental-agent-team-profile` | — | [`packages/experimental/agent-team-profile/src/index.ts`](../packages/experimental/agent-team-profile/src/index.ts) | | `@deepseek-ai/dsh-experimental-browser-use-runtime` | — | [`packages/experimental/browser-use-runtime/src/index.ts`](../packages/experimental/browser-use-runtime/src/index.ts) | +| `@deepseek-ai/dsh-experimental-schedule-bundle` | — | [`packages/experimental/schedule-bundle/src/index.ts`](../packages/experimental/schedule-bundle/src/index.ts) | | `@deepseek-ai/dsh-experimental-voice-input-bundle` | — | [`packages/experimental/voice-input-bundle/src/index.ts`](../packages/experimental/voice-input-bundle/src/index.ts) | | `@deepseek-ai/dsh-experimental-webworker-packer` | — | [`packages/experimental/webworker-packer/src/index.ts`](../packages/experimental/webworker-packer/src/index.ts) | | `@deepseek-ai/dsh-experimental-webworker-runtime` | — | [`packages/experimental/webworker-runtime/src/index.ts`](../packages/experimental/webworker-runtime/src/index.ts) | diff --git a/docs/event-producer-consumer.md b/docs/event-producer-consumer.md index fba44c996f..4385771ec8 100644 --- a/docs/event-producer-consumer.md +++ b/docs/event-producer-consumer.md @@ -60,7 +60,7 @@ This matrix shows which packages dispatch each harness-owned event and which pac | `plugin-manager/install-log` | `emit` | [`packages/boot/plugin-manager/src/types.ts:249`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` | | `plugin-manager/install-state` | `emit` | [`packages/boot/plugin-manager/src/types.ts:256`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` | | `schedule/changed` | `emit` | [`packages/schedule/schedule/src/types.ts:471`](../packages/schedule/schedule/src/types.ts) | [`schedule`](../packages/schedule/schedule) (`emit`) | `remotes` | -| `session-telemetry/record` | `waterfall` | [`packages/session/session-telemetry/src/index.ts:43`](../packages/session/session-telemetry/src/index.ts) | [`session-telemetry`](../packages/session/session-telemetry) (`waterfall`) | - | +| `session-telemetry/record` | `waterfall` | [`packages/session/session-telemetry/src/index.ts:44`](../packages/session/session-telemetry/src/index.ts) | [`session-telemetry`](../packages/session/session-telemetry) (`waterfall`) | - | | `session/created` | `emit` | [`packages/core/session/src/index.ts:55`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`compaction`](../packages/compaction/compaction), [`goal`](../packages/goal/goal), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission-presets`](../packages/interaction/permission-presets), [`plan-mode`](../packages/plan/plan-mode), [`schedule`](../packages/schedule/schedule), `server`, [`session`](../packages/core/session), `session-controller`, [`session-log-deepseek`](../packages/session/session-log-deepseek), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title), [`time-context`](../packages/context/time-context), [`tool-todo`](../packages/todo/tool-todo), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) | | `session/disposed` | `emit` | [`packages/core/session/src/index.ts:65`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `agent-team`, `file-upload`, `session-controller`, [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title), [`workspace-changes`](../packages/deliverables/workspace-changes) | | `session/event` | `emit` | [`packages/core/session/src/index.ts:77`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`agent-instructions`](../packages/context/agent-instructions), [`agent-loop`](../packages/core/agent-loop), [`agent-preset-registry`](../packages/preset/agent-preset-registry), `agent-team`, [`compaction`](../packages/compaction/compaction), [`compaction-basic`](../packages/compaction/compaction-basic), [`file-reference-local`](../packages/context/file-reference-local), `file-upload`, [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`headless`](../packages/bundle/headless), [`hook-protocol`](../packages/hooks/hook-protocol), [`loader-smoke`](../packages/test-support/loader-smoke), `product-analytics`, `server`, [`session`](../packages/core/session), `session-controller`, [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-telemetry-otel`](../packages/session/session-telemetry-otel), [`session-title`](../packages/session/session-title), [`token-meter`](../packages/llm/token-meter), [`tool-todo`](../packages/todo/tool-todo), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval), [`workspace-changes`](../packages/deliverables/workspace-changes) | diff --git a/docs/event-producer-consumer.zh.md b/docs/event-producer-consumer.zh.md index f34de01a6a..b3edadf12e 100644 --- a/docs/event-producer-consumer.zh.md +++ b/docs/event-producer-consumer.zh.md @@ -62,7 +62,7 @@ | `plugin-manager/install-log` | `emit` | [`packages/boot/plugin-manager/src/types.ts:249`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` | | `plugin-manager/install-state` | `emit` | [`packages/boot/plugin-manager/src/types.ts:256`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` | | `schedule/changed` | `emit` | [`packages/schedule/schedule/src/types.ts:471`](../packages/schedule/schedule/src/types.ts) | [`schedule`](../packages/schedule/schedule) (`emit`) | `remotes` | -| `session-telemetry/record` | `waterfall` | [`packages/session/session-telemetry/src/index.ts:43`](../packages/session/session-telemetry/src/index.ts) | [`session-telemetry`](../packages/session/session-telemetry) (`waterfall`) | - | +| `session-telemetry/record` | `waterfall` | [`packages/session/session-telemetry/src/index.ts:44`](../packages/session/session-telemetry/src/index.ts) | [`session-telemetry`](../packages/session/session-telemetry) (`waterfall`) | - | | `session/created` | `emit` | [`packages/core/session/src/index.ts:55`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`compaction`](../packages/compaction/compaction), [`goal`](../packages/goal/goal), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission-presets`](../packages/interaction/permission-presets), [`plan-mode`](../packages/plan/plan-mode), [`schedule`](../packages/schedule/schedule), `server`, [`session`](../packages/core/session), `session-controller`, [`session-log-deepseek`](../packages/session/session-log-deepseek), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title), [`time-context`](../packages/context/time-context), [`tool-todo`](../packages/todo/tool-todo), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) | | `session/disposed` | `emit` | [`packages/core/session/src/index.ts:65`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `agent-team`, `file-upload`, `session-controller`, [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title), [`workspace-changes`](../packages/deliverables/workspace-changes) | | `session/event` | `emit` | [`packages/core/session/src/index.ts:77`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`agent-instructions`](../packages/context/agent-instructions), [`agent-loop`](../packages/core/agent-loop), [`agent-preset-registry`](../packages/preset/agent-preset-registry), `agent-team`, [`compaction`](../packages/compaction/compaction), [`compaction-basic`](../packages/compaction/compaction-basic), [`file-reference-local`](../packages/context/file-reference-local), `file-upload`, [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`headless`](../packages/bundle/headless), [`hook-protocol`](../packages/hooks/hook-protocol), [`loader-smoke`](../packages/test-support/loader-smoke), `product-analytics`, `server`, [`session`](../packages/core/session), `session-controller`, [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-telemetry-otel`](../packages/session/session-telemetry-otel), [`session-title`](../packages/session/session-title), [`token-meter`](../packages/llm/token-meter), [`tool-todo`](../packages/todo/tool-todo), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval), [`workspace-changes`](../packages/deliverables/workspace-changes) | diff --git a/docs/module-graph.md b/docs/module-graph.md index 12b0a3f67b..bda8dc9b51 100644 --- a/docs/module-graph.md +++ b/docs/module-graph.md @@ -259,6 +259,7 @@ flowchart TD pkg_experimental_computer_use_cua_driver_native["experimental-computer-use-cua-driver-native"] pkg_experimental_inspector["experimental-inspector"] pkg_experimental_ptc_runtime_python["experimental-ptc-runtime-python"] + pkg_experimental_schedule_bundle["experimental-schedule-bundle"] pkg_experimental_speech_to_text["experimental-speech-to-text"] pkg_experimental_speech_to_text_sensevoice["experimental-speech-to-text-sensevoice"] pkg_experimental_tool_agent_team["experimental-tool-agent-team"] @@ -395,6 +396,9 @@ flowchart TD pkg_subprocess_local["subprocess-local"] pkg_win32_process["win32-process"] end + subgraph group_telemetry["packages/telemetry"] + pkg_otel["otel"] + end subgraph group_terminal["packages/terminal"] pkg_terminal["terminal"] pkg_terminal_bash["terminal-bash"] @@ -510,6 +514,7 @@ flowchart TD pkg_settings --> pkg_brand pkg_settings --> pkg_invariants pkg_settings --> pkg_session + pkg_otel --> pkg_session pkg_session_snapshot --> pkg_http_proxy pkg_session_snapshot --> pkg_session pkg_fs --> pkg_brand @@ -522,6 +527,7 @@ flowchart TD pkg_hmr --> pkg_app_boot pkg_hmr --> pkg_cmdline pkg_experimental_speech_to_text --> pkg_settings + pkg_host_product_telemetry_otel --> pkg_otel pkg_ptc_runtime --> pkg_sandbox pkg_sandbox_local --> pkg_llm pkg_sandbox_local --> pkg_sandbox @@ -1037,6 +1043,7 @@ flowchart TD pkg_session_telemetry_otel --> pkg_command_feedback pkg_session_telemetry_otel --> pkg_llm pkg_session_telemetry_otel --> pkg_message_feedback + pkg_session_telemetry_otel --> pkg_otel pkg_session_telemetry_otel --> pkg_session pkg_session_telemetry_otel --> pkg_session_telemetry pkg_tool_bash --> pkg_agent @@ -1454,6 +1461,7 @@ flowchart TD | [`client-web`](../packages/client/web) | `client` | — | | [`office-to-pdf`](../packages/document/office-to-pdf) | `document` | — | | [`experimental-agent-team-profile`](../packages/experimental/agent-team-profile) | `experimental` | — | +| [`experimental-schedule-bundle`](../packages/experimental/schedule-bundle) | `experimental` | — | | [`experimental-voice-input-bundle`](../packages/experimental/voice-input-bundle) | `experimental` | — | | [`experimental-webworker-packer`](../packages/experimental/webworker-packer) | `experimental` | — | | [`client-ui-cordis`](../packages/extensions/ui-cordis) | `extensions` | — | @@ -1462,7 +1470,6 @@ flowchart TD | [`host-directory-picker-browse`](../packages/host/directory-picker-browse) | `host` | — | | [`host-directory-picker-native`](../packages/host/directory-picker-native) | `host` | — | | [`host-open-in-app`](../packages/host/open-in-app) | `host` | — | -| [`host-product-telemetry-otel`](../packages/host/product-telemetry-otel) | `host` | — | | [`host-webserver`](../packages/host/webserver) | `host` | — | | [`invariants`](../packages/runtime-diagnostics/invariants) | `runtime-diagnostics` | — | | [`session-format`](../packages/session/session-format) | `session` | — | @@ -1522,12 +1529,14 @@ flowchart TD | [`session-persistence`](../packages/session/session-persistence) | `session` | [`brand`](../packages/util/brand), [`session`](../packages/core/session), [`timeout`](../packages/util/timeout) | | [`session-projection`](../packages/session/session-projection) | `session` | [`session`](../packages/core/session) | | [`settings`](../packages/settings/settings) | `settings` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) | +| [`otel`](../packages/telemetry/otel) | `telemetry` | [`session`](../packages/core/session) | | [`session-snapshot`](../packages/test-support/session-snapshot) | `test-support` | [`http-proxy`](../packages/util/http-proxy), [`session`](../packages/core/session) | | [`fs`](../packages/fs/fs) | `fs` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox) | | [`spill-local`](../packages/spill/spill-local) | `spill` | [`spill`](../packages/spill/spill) | | [`session-log-export`](../packages/session-query/session-log-export) | `session-query` | [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence) | | [`hmr`](../packages/boot/hmr) | `boot` | [`app-boot`](../packages/boot/app-boot), [`cmdline`](../packages/boot/cmdline) | | [`experimental-speech-to-text`](../packages/experimental/speech-to-text) | `experimental` | [`settings`](../packages/settings/settings) | +| [`host-product-telemetry-otel`](../packages/host/product-telemetry-otel) | `host` | [`otel`](../packages/telemetry/otel) | | [`ptc-runtime`](../packages/ptc-runtime/ptc-runtime) | `ptc-runtime` | [`sandbox`](../packages/sandbox/sandbox) | | [`sandbox-local`](../packages/sandbox/sandbox-local) | `sandbox` | [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`session`](../packages/core/session) | | [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl) | `session` | [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence) | @@ -1637,7 +1646,7 @@ flowchart TD | [`host-plugin-inventory`](../packages/host/plugin-inventory) | `host` | [`agent-preset-registry`](../packages/preset/agent-preset-registry), [`brand`](../packages/util/brand), [`typert-protocol`](../packages/typert/protocol) | | [`mcp-client`](../packages/mcp/mcp-client) | `mcp` | [`attachment`](../packages/attachment/attachment), [`llm`](../packages/llm/llm), [`mcp-resources`](../packages/mcp/mcp-resources), [`scope`](../packages/core/scope), [`subprocess`](../packages/subprocess/subprocess), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) | | [`agent-preset`](../packages/preset/agent-preset) | `preset` | [`agent-preset-registry`](../packages/preset/agent-preset-registry) | -| [`session-telemetry-otel`](../packages/session/session-telemetry-otel) | `session` | [`anonymous-user-id`](../packages/identity/anonymous-user-id), [`command-feedback`](../packages/feedback/command-feedback), [`llm`](../packages/llm/llm), [`message-feedback`](../packages/feedback/message-feedback), [`session`](../packages/core/session), [`session-telemetry`](../packages/session/session-telemetry) | +| [`session-telemetry-otel`](../packages/session/session-telemetry-otel) | `session` | [`anonymous-user-id`](../packages/identity/anonymous-user-id), [`command-feedback`](../packages/feedback/command-feedback), [`llm`](../packages/llm/llm), [`message-feedback`](../packages/feedback/message-feedback), [`otel`](../packages/telemetry/otel), [`session`](../packages/core/session), [`session-telemetry`](../packages/session/session-telemetry) | | [`tool-bash`](../packages/shell/tool-bash) | `shell` | [`agent`](../packages/core/agent), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`shell`](../packages/shell/shell), [`shell-env`](../packages/shell/shell-env), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) | | [`tool-pwsh`](../packages/shell/tool-pwsh) | `shell` | [`agent`](../packages/core/agent), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`shell`](../packages/shell/shell), [`shell-env`](../packages/shell/shell-env), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) | | [`fs-ssh`](../packages/ssh/fs-ssh) | `ssh` | [`fs`](../packages/fs/fs), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`ssh`](../packages/ssh/ssh) | diff --git a/docs/module-graph.zh.md b/docs/module-graph.zh.md index ad8f26e7e0..566b51ba41 100644 --- a/docs/module-graph.zh.md +++ b/docs/module-graph.zh.md @@ -261,6 +261,7 @@ flowchart TD pkg_experimental_computer_use_cua_driver_native["experimental-computer-use-cua-driver-native"] pkg_experimental_inspector["experimental-inspector"] pkg_experimental_ptc_runtime_python["experimental-ptc-runtime-python"] + pkg_experimental_schedule_bundle["experimental-schedule-bundle"] pkg_experimental_speech_to_text["experimental-speech-to-text"] pkg_experimental_speech_to_text_sensevoice["experimental-speech-to-text-sensevoice"] pkg_experimental_tool_agent_team["experimental-tool-agent-team"] @@ -397,6 +398,9 @@ flowchart TD pkg_subprocess_local["subprocess-local"] pkg_win32_process["win32-process"] end + subgraph group_telemetry["packages/telemetry"] + pkg_otel["otel"] + end subgraph group_terminal["packages/terminal"] pkg_terminal["terminal"] pkg_terminal_bash["terminal-bash"] @@ -512,6 +516,7 @@ flowchart TD pkg_settings --> pkg_brand pkg_settings --> pkg_invariants pkg_settings --> pkg_session + pkg_otel --> pkg_session pkg_session_snapshot --> pkg_http_proxy pkg_session_snapshot --> pkg_session pkg_fs --> pkg_brand @@ -524,6 +529,7 @@ flowchart TD pkg_hmr --> pkg_app_boot pkg_hmr --> pkg_cmdline pkg_experimental_speech_to_text --> pkg_settings + pkg_host_product_telemetry_otel --> pkg_otel pkg_ptc_runtime --> pkg_sandbox pkg_sandbox_local --> pkg_llm pkg_sandbox_local --> pkg_sandbox @@ -1039,6 +1045,7 @@ flowchart TD pkg_session_telemetry_otel --> pkg_command_feedback pkg_session_telemetry_otel --> pkg_llm pkg_session_telemetry_otel --> pkg_message_feedback + pkg_session_telemetry_otel --> pkg_otel pkg_session_telemetry_otel --> pkg_session pkg_session_telemetry_otel --> pkg_session_telemetry pkg_tool_bash --> pkg_agent @@ -1456,6 +1463,7 @@ flowchart TD | [`client-web`](../packages/client/web) | `client` | — | | [`office-to-pdf`](../packages/document/office-to-pdf) | `document` | — | | [`experimental-agent-team-profile`](../packages/experimental/agent-team-profile) | `experimental` | — | +| [`experimental-schedule-bundle`](../packages/experimental/schedule-bundle) | `experimental` | — | | [`experimental-voice-input-bundle`](../packages/experimental/voice-input-bundle) | `experimental` | — | | [`experimental-webworker-packer`](../packages/experimental/webworker-packer) | `experimental` | — | | [`client-ui-cordis`](../packages/extensions/ui-cordis) | `extensions` | — | @@ -1464,7 +1472,6 @@ flowchart TD | [`host-directory-picker-browse`](../packages/host/directory-picker-browse) | `host` | — | | [`host-directory-picker-native`](../packages/host/directory-picker-native) | `host` | — | | [`host-open-in-app`](../packages/host/open-in-app) | `host` | — | -| [`host-product-telemetry-otel`](../packages/host/product-telemetry-otel) | `host` | — | | [`host-webserver`](../packages/host/webserver) | `host` | — | | [`invariants`](../packages/runtime-diagnostics/invariants) | `runtime-diagnostics` | — | | [`session-format`](../packages/session/session-format) | `session` | — | @@ -1524,12 +1531,14 @@ flowchart TD | [`session-persistence`](../packages/session/session-persistence) | `session` | [`brand`](../packages/util/brand), [`session`](../packages/core/session), [`timeout`](../packages/util/timeout) | | [`session-projection`](../packages/session/session-projection) | `session` | [`session`](../packages/core/session) | | [`settings`](../packages/settings/settings) | `settings` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session) | +| [`otel`](../packages/telemetry/otel) | `telemetry` | [`session`](../packages/core/session) | | [`session-snapshot`](../packages/test-support/session-snapshot) | `test-support` | [`http-proxy`](../packages/util/http-proxy), [`session`](../packages/core/session) | | [`fs`](../packages/fs/fs) | `fs` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox) | | [`spill-local`](../packages/spill/spill-local) | `spill` | [`spill`](../packages/spill/spill) | | [`session-log-export`](../packages/session-query/session-log-export) | `session-query` | [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence) | | [`hmr`](../packages/boot/hmr) | `boot` | [`app-boot`](../packages/boot/app-boot), [`cmdline`](../packages/boot/cmdline) | | [`experimental-speech-to-text`](../packages/experimental/speech-to-text) | `experimental` | [`settings`](../packages/settings/settings) | +| [`host-product-telemetry-otel`](../packages/host/product-telemetry-otel) | `host` | [`otel`](../packages/telemetry/otel) | | [`ptc-runtime`](../packages/ptc-runtime/ptc-runtime) | `ptc-runtime` | [`sandbox`](../packages/sandbox/sandbox) | | [`sandbox-local`](../packages/sandbox/sandbox-local) | `sandbox` | [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`session`](../packages/core/session) | | [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl) | `session` | [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence) | @@ -1639,7 +1648,7 @@ flowchart TD | [`host-plugin-inventory`](../packages/host/plugin-inventory) | `host` | [`agent-preset-registry`](../packages/preset/agent-preset-registry), [`brand`](../packages/util/brand), [`typert-protocol`](../packages/typert/protocol) | | [`mcp-client`](../packages/mcp/mcp-client) | `mcp` | [`attachment`](../packages/attachment/attachment), [`llm`](../packages/llm/llm), [`mcp-resources`](../packages/mcp/mcp-resources), [`scope`](../packages/core/scope), [`subprocess`](../packages/subprocess/subprocess), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) | | [`agent-preset`](../packages/preset/agent-preset) | `preset` | [`agent-preset-registry`](../packages/preset/agent-preset-registry) | -| [`session-telemetry-otel`](../packages/session/session-telemetry-otel) | `session` | [`anonymous-user-id`](../packages/identity/anonymous-user-id), [`command-feedback`](../packages/feedback/command-feedback), [`llm`](../packages/llm/llm), [`message-feedback`](../packages/feedback/message-feedback), [`session`](../packages/core/session), [`session-telemetry`](../packages/session/session-telemetry) | +| [`session-telemetry-otel`](../packages/session/session-telemetry-otel) | `session` | [`anonymous-user-id`](../packages/identity/anonymous-user-id), [`command-feedback`](../packages/feedback/command-feedback), [`llm`](../packages/llm/llm), [`message-feedback`](../packages/feedback/message-feedback), [`otel`](../packages/telemetry/otel), [`session`](../packages/core/session), [`session-telemetry`](../packages/session/session-telemetry) | | [`tool-bash`](../packages/shell/tool-bash) | `shell` | [`agent`](../packages/core/agent), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`shell`](../packages/shell/shell), [`shell-env`](../packages/shell/shell-env), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) | | [`tool-pwsh`](../packages/shell/tool-pwsh) | `shell` | [`agent`](../packages/core/agent), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`shell`](../packages/shell/shell), [`shell-env`](../packages/shell/shell-env), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) | | [`fs-ssh`](../packages/ssh/fs-ssh) | `ssh` | [`fs`](../packages/fs/fs), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`ssh`](../packages/ssh/ssh) | diff --git a/docs/subsystems/README.i18n.yaml b/docs/subsystems/README.i18n.yaml index a7934d3e58..7051478b51 100644 --- a/docs/subsystems/README.i18n.yaml +++ b/docs/subsystems/README.i18n.yaml @@ -3,5 +3,5 @@ # After editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/subsystems/README.md /subsystems: - en: 58466f6fd60755eb - zh: ab46a374f5a3c3ed + en: ab6cd11804f6b644 + zh: 7a82dc5b22c40703 diff --git a/docs/subsystems/README.md b/docs/subsystems/README.md index 479dbe7cb3..e2f8dc51e2 100644 --- a/docs/subsystems/README.md +++ b/docs/subsystems/README.md @@ -67,6 +67,7 @@ One page per subsystem of the DeepSeek Harness: what it is, the data structures | [conversation.md](conversation.md) | target-neutral Session-event assembly: Context identity, Location data, replay paths, view builders, and target-owned render nodes | | [session-projection.md](session-projection.md) | the projection seam: `SessionProjectionMap`, the pure `ProjectionDefinition` unit, `ProjectionSnapshot`'s consistent cut, the change feed | | [session-telemetry.md](session-telemetry.md) | the outbound session-reporting capability seam: `SessionTelemetryRecord`/`SessionTelemetrySeverity`, the `SessionTelemetrySink` contract, and the `session-telemetry/record` redact waterfall | +| [otel.md](otel.md) | Shared ordinary-event and Session-log OTLP channels | | [product-telemetry.md](product-telemetry.md) | Explicit product analytics submission and OTLP/HTTP transport | > Type declarations and their JSDoc on these pages are source-equivalent and drift-checked by `pnpm run verify-type-equiv` (see [development.md](../development.md#documenting-types-verbatim-ts-type-equiv)). Ordinary blocks preserve complete declarations; `public-api` blocks preserve body-stripped public class declarations. Cordis services and events use each page's generated **Cordis API** section. diff --git a/docs/subsystems/README.zh.md b/docs/subsystems/README.zh.md index a6b9226df6..31f4489d43 100644 --- a/docs/subsystems/README.zh.md +++ b/docs/subsystems/README.zh.md @@ -67,6 +67,7 @@ | [conversation.md](conversation.zh.md) | 目标无关的会话事件组装:上下文标识、位置数据、回放路径、视图构建器与目标自有的渲染节点 | | [session-projection.md](session-projection.zh.md) | 投影 seam:`SessionProjectionMap`、纯函数 `ProjectionDefinition` 单元、`ProjectionSnapshot` 的一致切面、变更馈送 | | [session-telemetry.md](session-telemetry.zh.md) | 对外会话上报能力 seam:`SessionTelemetryRecord`/`SessionTelemetrySeverity`、`SessionTelemetrySink` 约定和 `session-telemetry/record` 脱敏 waterfall(瀑布式事件) | +| [otel.md](otel.zh.md) | 共享普通事件与 Session 日志 OTLP 通道 | | [product-telemetry.md](product-telemetry.zh.md) | 显式产品分析事件提交与 OTLP/HTTP 传输 | > 这些页面上的类型声明及其 JSDoc 与源码等价,并由 `pnpm run verify-type-equiv` 检查漂移(见 [development.md](../development.zh.md#documenting-types-verbatim-ts-type-equiv))。普通块保留完整声明;`public-api` 块保留去除实现体的公开 class 声明。Cordis 服务与事件使用每页生成的 **Cordis API** 小节。 diff --git a/docs/subsystems/otel.i18n.yaml b/docs/subsystems/otel.i18n.yaml new file mode 100644 index 0000000000..7547678647 --- /dev/null +++ b/docs/subsystems/otel.i18n.yaml @@ -0,0 +1,7 @@ +# Bilingual-pair consistency record for otel.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write docs/subsystems/otel.md +/otel-reporting: + en: adf67758d37c46f2 + zh: 9e41b63531fa875a diff --git a/docs/subsystems/otel.md b/docs/subsystems/otel.md new file mode 100644 index 0000000000..bcba92f51b --- /dev/null +++ b/docs/subsystems/otel.md @@ -0,0 +1,44 @@ +# OTel reporting + +English | [中文](otel.zh.md) + +The [OTel plugin](../../packages/telemetry/otel/README.md) registers `ctx.otel`, a shared factory for independent ordinary-event and Session-log channels. It owns SDK transport and batching implementations. Business consumers inject the service, supply explicit transport/resource/scope options, and own each returned channel's shutdown. + +UI feedback reaches the Session feedback services through their existing Remote APIs. Feedback recording authorizes the Session adapter's canonical-prefix capture and redaction before it reports through an OTel channel. Ordinary product consumers submit selected fields through the product adapter. The shared service has no automatic identity or capture policy, and channels never share queues or requests. + +The [product adapter](../../packages/host/product-telemetry-otel/README.md) and [Session adapter](../../packages/session/session-telemetry-otel/README.md) retain their deployment configuration and disposal deadlines. Consumers must not reuse a channel after shutdown. Mounting only the OTel service allocates no provider or transport. + + + + + +## Cordis API + +Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — the language sides differ only in locale-specific paired document paths. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md). + + + +### `ctx.otel` — `OTel` + +Shared transport provider. Mounting creates no queue, identity, or network connection. + +```ts cordis-catalog +/** + * Create an independent ordinary-event channel with count-based batching. + * The injected consumer must drain it during its fiber disposal. + * @param options - transport, scope, resource, queue, and diagnostic settings selected by the consumer. + * @returns the caller-owned channel; no state is shared with other channels. + */ +createEventReporter(options: EventLogOptions): EventLogReporter + +/** + * Create an independent byte-bounded Session-log channel. + * Authorization and redaction precede reporting; the consumer owns shutdown and its outer deadline. + * @param options - transport, scope, resource, queue, and diagnostic settings selected by the consumer. + * @returns the caller-owned channel, preserving complete accepted events within the request byte ceiling. + */ +createSessionLogReporter(options: SessionLogOptions): SessionLogReporter +``` + +Source: [`packages/telemetry/otel/src/index.ts`](../../packages/telemetry/otel/src/index.ts) + diff --git a/docs/subsystems/otel.zh.md b/docs/subsystems/otel.zh.md new file mode 100644 index 0000000000..dbe7f3cb9e --- /dev/null +++ b/docs/subsystems/otel.zh.md @@ -0,0 +1,44 @@ +# OTel 上报 + +[English](otel.md) | 中文 + +[OTel 插件](../../packages/telemetry/otel/README.zh.md) 注册 `ctx.otel`,作为独立普通事件通道和 Session 日志通道的共享工厂。它持有 SDK 传输与聚合实现。业务调用方注入服务,显式提供传输、resource 和 scope 选项,并负责返回通道的关闭。 + +UI 反馈通过现有 Remote API 到达 Session 反馈服务。记录反馈后,Session 适配器才获得规范日志前缀的捕获授权,完成脱敏后通过 OTel 通道上报。普通产品调用方通过产品适配器提交选定字段。共享服务没有自动身份或捕获策略,通道之间不共享队列或请求。 + +[产品适配器](../../packages/host/product-telemetry-otel/README.zh.md) 和 [Session 适配器](../../packages/session/session-telemetry-otel/README.zh.md) 保留各自部署配置及销毁期限。通道关闭后调用方不得复用。只挂载 OTel 服务不会分配 provider 或 transport。 + + + + + +## Cordis API + +Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — the language sides differ only in locale-specific paired document paths. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.zh.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md). + + + +### `ctx.otel` — `OTel` + +Shared transport provider. Mounting creates no queue, identity, or network connection. + +```ts cordis-catalog +/** + * Create an independent ordinary-event channel with count-based batching. + * The injected consumer must drain it during its fiber disposal. + * @param options - transport, scope, resource, queue, and diagnostic settings selected by the consumer. + * @returns the caller-owned channel; no state is shared with other channels. + */ +createEventReporter(options: EventLogOptions): EventLogReporter + +/** + * Create an independent byte-bounded Session-log channel. + * Authorization and redaction precede reporting; the consumer owns shutdown and its outer deadline. + * @param options - transport, scope, resource, queue, and diagnostic settings selected by the consumer. + * @returns the caller-owned channel, preserving complete accepted events within the request byte ceiling. + */ +createSessionLogReporter(options: SessionLogOptions): SessionLogReporter +``` + +Source: [`packages/telemetry/otel/src/index.ts`](../../packages/telemetry/otel/src/index.ts) + diff --git a/docs/subsystems/product-telemetry.md b/docs/subsystems/product-telemetry.md index c68451b73b..00c70a45c8 100644 --- a/docs/subsystems/product-telemetry.md +++ b/docs/subsystems/product-telemetry.md @@ -11,24 +11,13 @@ Enqueue is synchronous and does not acknowledge delivery. The SDK owns batching ## Record types ```ts type-equiv -/** Scalar values accepted by the collector's Arrow attributes map. */ -type ProductTelemetryScalar = string | number | boolean +/** Scalar values accepted in product attributes. */ +type ProductTelemetryScalar = OTelEventScalar ``` ```ts type-equiv -/** Explicitly selected analytics fields; object values may contain scalars only. */ -interface ProductTelemetryRecord { - /** Product/DA-owned event name. */ - eventName: string - /** Human-readable summary; never a prompt, response, credential, or file contents. */ - body: string - /** Event occurrence time in Unix milliseconds. Observation time is assigned on enqueue. */ - timestamp: number - /** OTel severity; omitted values use INFO. */ - severityNumber?: SeverityNumber - /** Business fields selected by the caller; no automatic device or account identity. */ - attributes?: Record> -} +/** Caller-selected ordinary analytics record. */ +type ProductTelemetryRecord = OTelEventRecord ``` Source: [`packages/host/product-telemetry-otel/src/index.ts`](../../packages/host/product-telemetry-otel/src/index.ts) diff --git a/docs/subsystems/product-telemetry.zh.md b/docs/subsystems/product-telemetry.zh.md index cd3635ad67..5b04bf6fdd 100644 --- a/docs/subsystems/product-telemetry.zh.md +++ b/docs/subsystems/product-telemetry.zh.md @@ -11,24 +11,13 @@ ## 记录类型 ```ts type-equiv -/** Scalar values accepted by the collector's Arrow attributes map. */ -type ProductTelemetryScalar = string | number | boolean +/** Scalar values accepted in product attributes. */ +type ProductTelemetryScalar = OTelEventScalar ``` ```ts type-equiv -/** Explicitly selected analytics fields; object values may contain scalars only. */ -interface ProductTelemetryRecord { - /** Product/DA-owned event name. */ - eventName: string - /** Human-readable summary; never a prompt, response, credential, or file contents. */ - body: string - /** Event occurrence time in Unix milliseconds. Observation time is assigned on enqueue. */ - timestamp: number - /** OTel severity; omitted values use INFO. */ - severityNumber?: SeverityNumber - /** Business fields selected by the caller; no automatic device or account identity. */ - attributes?: Record> -} +/** Caller-selected ordinary analytics record. */ +type ProductTelemetryRecord = OTelEventRecord ``` Source: [`packages/host/product-telemetry-otel/src/index.ts`](../../packages/host/product-telemetry-otel/src/index.ts) diff --git a/docs/subsystems/schedule.i18n.yaml b/docs/subsystems/schedule.i18n.yaml index d6d12d5a95..13c6d192fe 100644 --- a/docs/subsystems/schedule.i18n.yaml +++ b/docs/subsystems/schedule.i18n.yaml @@ -9,8 +9,8 @@ en: 41cd197b5c74bfa3 zh: 18ed11259036d726 /host-wide-schedule/absolute-time-input: - en: e730fda52ba2854f - zh: 01a6c682e327a133 + en: 0876a14a60ba2160 + zh: e7d4bd9099c14fb2 /host-wide-schedule/daily-wall-clock-input: en: 2d249edc3dfccb8a zh: 33fecda132b03b84 @@ -27,8 +27,8 @@ en: 0656173de50ee132 zh: 54176540371d0f28 /host-wide-schedule/web-catalog: - en: 39f24636dd788034 - zh: ba3ff541fee5854a + en: 60c88b08abde9bb0 + zh: 0a3644cfa86d014d /host-wide-schedule/timing-edits: en: ce5d2095a3ad2e82 zh: 167b3715f8eaffc1 diff --git a/docs/subsystems/schedule.md b/docs/subsystems/schedule.md index 0784c0b92a..571084d0ae 100644 --- a/docs/subsystems/schedule.md +++ b/docs/subsystems/schedule.md @@ -116,7 +116,7 @@ interface LocalAtInput { type AtInput = string | LocalAtInput ``` -The shipped Web bundle mounts time-context, which samples the browser's IANA zone for every prompt. Time-context tells the model to interpret otherwise-unqualified natural-language dates and times in that request-local zone when the open turn has one unambiguous browser zone; mixed or missing browser-zone records tell the model to ask. That guidance is not a durable Session default: the model must still pass an offset in the string form or `time_zone` in the local form, and Schedule never reads browser, Session, process, or model context. +The shipped Web composition carries the `time-context` row disabled; the optional experimental bundle `@deepseek-ai/dsh-experimental-schedule-bundle`, enabled from the Plugins page, mounts time-context, which samples the browser's IANA zone for every prompt. Time-context tells the model to interpret otherwise-unqualified natural-language dates and times in that request-local zone when the open turn has one unambiguous browser zone; mixed or missing browser-zone records tell the model to ask. That guidance is not a durable Session default: the model must still pass an offset in the string form or `time_zone` in the local form, and Schedule never reads browser, Session, process, or model context. Schedule rejects invalid offsets and zones, offset-free strings, non-future targets, and local times inside daylight-saving gaps. A daylight-saving overlap chooses its first, earlier instant. Successful creation stores only canonical UTC `scheduledAt`, so replay never depends on ambient time-zone state. @@ -339,7 +339,7 @@ type ScheduleCatalogEntry = ScheduleRecord & { The Remote `schedule.list({ sessionId })`, model `schedule_list`, and Session-header catalog return only active tasks. A model view's derived timing `state` remains distinct from stored lifecycle `status`. Deletion uses `schedule.delete({ sessionId, id })` with the entry's original binding; a mismatched binding returns not found. Model tools supply the current Agent's Session, whereas the global user interface supplies the selected task's binding. The binding check alone does not establish caller authorization. The payload-free `schedule/changed` event invalidates client lists; reconnecting clients fetch current state again. -The Web bundle mounts `ui-schedule` with the Host capability. The [client package](../../packages/client/ui-schedule/README.md) owns the catalog, empty state, and deletion controls. The page separately filters all, active, and inactive tasks, retains inactive details and an original-Session control in the detail tab strip, and requires explicit confirmed deletion. Rules and Delivery records separate task settings from lazily paged saved receipts. Neither a receipt nor inactive status confirms model execution. +The shipped Web composition carries the `ui-schedule` row disabled; the optional experimental bundle `@deepseek-ai/dsh-experimental-schedule-bundle`, enabled from the Plugins page, mounts `ui-schedule` with the Host capability. The [client package](../../packages/client/ui-schedule/README.md) owns the catalog, empty state, and deletion controls. The page separately filters all, active, and inactive tasks, retains inactive details and an original-Session control in the detail tab strip, and requires explicit confirmed deletion. Rules and Delivery records separate task settings from lazily paged saved receipts. Neither a receipt nor inactive status confirms model execution. ## Timing edits diff --git a/docs/subsystems/schedule.zh.md b/docs/subsystems/schedule.zh.md index d1a726fa97..bd210a11a2 100644 --- a/docs/subsystems/schedule.zh.md +++ b/docs/subsystems/schedule.zh.md @@ -116,7 +116,7 @@ interface LocalAtInput { type AtInput = string | LocalAtInput ``` -发布的 Web bundle 会挂载 time-context,它为每条提示词采样浏览器的 IANA 时区。当 open turn 只有一个无歧义的浏览器时区时,Time-context 会告诉模型按该请求本地时区解释未明确限定时区的自然语言日期和时间;浏览器时区记录混合或缺失时,则告诉模型询问用户。该指引不是持久 Session 默认值:模型仍必须在字符串形式中传入偏移量,或在本地形式中传入 `time_zone`;Schedule 绝不会读取浏览器、Session、进程或模型上下文。 +随发行版交付的 Web 组合把 `time-context` 行保持禁用;在插件管理页启用可选实验性 bundle `@deepseek-ai/dsh-experimental-schedule-bundle` 会挂载 time-context,它为每条提示词采样浏览器的 IANA 时区。当 open turn 只有一个无歧义的浏览器时区时,Time-context 会告诉模型按该请求本地时区解释未明确限定时区的自然语言日期和时间;浏览器时区记录混合或缺失时,则告诉模型询问用户。该指引不是持久 Session 默认值:模型仍必须在字符串形式中传入偏移量,或在本地形式中传入 `time_zone`;Schedule 绝不会读取浏览器、Session、进程或模型上下文。 Schedule 会拒绝无效偏移量与时区、不带偏移量的字符串、非未来目标,以及落在夏令时缺口内的本地时间。遇到夏令时重叠时,会选择第一次出现的较早时点。创建成功后只存储规范化后的 UTC `scheduledAt`,因此回放绝不依赖环境时区状态。 @@ -339,7 +339,7 @@ type ScheduleCatalogEntry = ScheduleRecord & { Remote 方法 `schedule.list({ sessionId })`、模型 `schedule_list` 和 Session 页头目录仅返回活动任务。模型视图派生的时间 `state` 与存储的生命周期 `status` 仍是不同概念。删除通过 `schedule.delete({ sessionId, id })` 使用条目的原始绑定;绑定不匹配时返回未找到。模型工具传入当前 Agent 的 Session,全局用户界面则传入所选任务的绑定。仅校验绑定并不构成调用者鉴权。无 payload 的 `schedule/changed` 事件通知客户端刷新列表;重新连接后,客户端再次获取当前状态。 -Web bundle 将 `ui-schedule` 与宿主能力一起挂载。[客户端包](../../packages/client/ui-schedule/README.zh.md) 负责目录、空状态与删除控件。页面单独筛选全部、活动和未运行任务,保留未运行任务详情及详情页签条内的原 Session 入口,并要求显式确认删除。“规则”和“发送记录”将任务设置与按需分页加载的已保存回执分开。回执与未运行状态均不确认模型执行。 +随发行版交付的 Web 组合把 `ui-schedule` 行保持禁用;在插件管理页启用可选实验性 bundle `@deepseek-ai/dsh-experimental-schedule-bundle` 会将它与宿主能力一起挂载。[客户端包](../../packages/client/ui-schedule/README.zh.md) 负责目录、空状态与删除控件。页面单独筛选全部、活动和未运行任务,保留未运行任务详情及详情页签条内的原 Session 入口,并要求显式确认删除。“规则”和“发送记录”将任务设置与按需分页加载的已保存回执分开。回执与未运行状态均不确认模型执行。 ## 修改时间 diff --git a/docs/subsystems/session-telemetry.i18n.yaml b/docs/subsystems/session-telemetry.i18n.yaml index b2c2f42fb9..553e00308c 100644 --- a/docs/subsystems/session-telemetry.i18n.yaml +++ b/docs/subsystems/session-telemetry.i18n.yaml @@ -3,8 +3,8 @@ # After editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/subsystems/session-telemetry.md /sessiontelemetrybackend: - en: d400c8668cd76829 - zh: 67689676c19daa7f + en: b82e4db22a4d58aa + zh: 439181fa2c27542d /sessiontelemetrybackend/the-logical-record: en: 9695489076496a76 zh: 457c54698bc38c2f diff --git a/docs/subsystems/session-telemetry.md b/docs/subsystems/session-telemetry.md index 18fe935e5e..4b99fc8ff9 100644 --- a/docs/subsystems/session-telemetry.md +++ b/docs/subsystems/session-telemetry.md @@ -2,7 +2,7 @@ English | [中文](session-telemetry.zh.md) -Outbound session reporting is split as a [capability seam](../capability-seams.md): the Service Definition and capture coordinator ([dsh-session-telemetry](../../packages/session/session-telemetry), `ctx.sessionTelemetry`) own complete canonical-event capture, the `session-telemetry/record` redaction waterfall, the handoff cursor, and the minimal backend contract; the Service Provider a deployment loads ([dsh-session-telemetry-otel](../../packages/session/session-telemetry-otel)) is the OpenTelemetry JS SDK's log pipeline configured verbatim. It is one optional capability, not part of the agent-loop spine, and nothing here reaches a model request. The boundary axiom — the harness's aspect ends at `emit()`; batching, retry, queueing, and loss policy belong to the reporting SDK — and the rejected alternatives are pinned in the [revival Agent Note](../../.agents/notes/implemented/feature/2026-07-23-session-telemetry-otel-revival.md); the capture and cursor contracts live in the [Service Definition README](../../packages/session/session-telemetry/README.md). +The [capture service](../../packages/session/session-telemetry/README.md) owns canonical-event capture, redaction, and handoff cursors. The [feedback backend](../../packages/session/session-telemetry-otel/README.md) authorizes Session prefixes and owns their independent queue, per-request scheduling, and 4,000,000-byte ceiling. No reporting data reaches model requests. Source: [`packages/session/session-telemetry/src/index.ts`](../../packages/session/session-telemetry/src/index.ts) @@ -29,6 +29,8 @@ type SessionTelemetrySeverity = 'info' | 'warn' | 'error' * identity so they can never be mistaken for ledger rows. */ interface SessionTelemetryRecord { + /** Canonical envelope without data; body carries the separately redacted payload. Absent for operational records. */ + sourceEvent?: { sessionId: SessionId; envelope: Omit } /** Ledger (session-log mirror) or ops (operational signal) channel; backends keep the two under separate instrumentation scopes. */ channel: 'ledger' | 'ops' /** Unix epoch milliseconds — the source event's append time for ledger records, the emission time for ops records. */ diff --git a/docs/subsystems/session-telemetry.zh.md b/docs/subsystems/session-telemetry.zh.md index 1b3807d2e3..00480938d9 100644 --- a/docs/subsystems/session-telemetry.zh.md +++ b/docs/subsystems/session-telemetry.zh.md @@ -2,7 +2,7 @@ [English](session-telemetry.md) | 中文 -对外的会话上报拆分为一项[能力 seam](../capability-seams.zh.md):Service Definition 与捕获协调器([dsh-session-telemetry](../../packages/session/session-telemetry),`ctx.sessionTelemetry`)拥有完整的权威事件捕获、`session-telemetry/record` 脱敏 waterfall(瀑布式事件)、handoff 游标与最小后端约定;部署方加载的 Service Provider([dsh-session-telemetry-otel](../../packages/session/session-telemetry-otel))则是原样配置的 OpenTelemetry JS SDK 日志流水线。它是一项可选能力,不属于 agent loop(智能体循环)主干,这里也没有任何内容会进入模型请求。边界公理(harness 的职责止于 `emit()`;批处理、重试、排队与丢失策略都属于上报 SDK)连同被否决的替代方案,均已在[复活 Agent Note](../../.agents/notes/implemented/feature/2026-07-23-session-telemetry-otel-revival.zh.md)中定案;捕获与游标约定见 [Service Definition README](../../packages/session/session-telemetry/README.zh.md)。 +[捕获服务](../../packages/session/session-telemetry/README.zh.md) 负责规范事件捕获、脱敏和交接游标。[反馈后端](../../packages/session/session-telemetry-otel/README.zh.md) 授权 Session 前缀,并负责独立队列、逐请求调度及 4,000,000 字节上限。上报数据不会进入模型请求。 源码:[`packages/session/session-telemetry/src/index.ts`](../../packages/session/session-telemetry/src/index.ts) @@ -29,6 +29,8 @@ type SessionTelemetrySeverity = 'info' | 'warn' | 'error' * identity so they can never be mistaken for ledger rows. */ interface SessionTelemetryRecord { + /** Canonical envelope without data; body carries the separately redacted payload. Absent for operational records. */ + sourceEvent?: { sessionId: SessionId; envelope: Omit } /** Ledger (session-log mirror) or ops (operational signal) channel; backends keep the two under separate instrumentation scopes. */ channel: 'ledger' | 'ops' /** Unix epoch milliseconds — the source event's append time for ledger records, the emission time for ops records. */ diff --git a/docs/user/guide/schedule.i18n.yaml b/docs/user/guide/schedule.i18n.yaml index a6033b0b82..1bf88dec7c 100644 --- a/docs/user/guide/schedule.i18n.yaml +++ b/docs/user/guide/schedule.i18n.yaml @@ -12,8 +12,8 @@ en: b3b2a6e0ce02bdee zh: 2ddb168de79fd19c /schedule-reminders/create-reminders: - en: 70ae56895847c453 - zh: 19b38057c7462123 + en: c76ee99f59529942 + zh: be1d75c226231dd7 /schedule-reminders/inspect-and-delete-tasks: en: a44fc1aabc2ab957 zh: 31f444d4c7fd5c7a diff --git a/docs/user/guide/schedule.md b/docs/user/guide/schedule.md index b0058b0cba..abb86b7f8e 100644 --- a/docs/user/guide/schedule.md +++ b/docs/user/guide/schedule.md @@ -17,7 +17,7 @@ Create reminders in a conversation, then inspect active and inactive tasks and e ## Create reminders -The shipped Web profile mounts Schedule with the clock context that gives the model the current time and the browser's zone. Configure a model provider before asking it to create reminders. +The shipped Web profile carries the `time-context`, `schedule`, and `ui-schedule` rows disabled. Open Plugins and enable `Scheduling and time context` in the Official group; the optional experimental bundle `@deepseek-ai/dsh-experimental-schedule-bundle` then mounts Schedule with the clock context that gives the model the current time and the browser's zone. Configure a model provider before asking it to create reminders. Ask the model to create, list, edit, or delete reminders. It uses `schedule_create`, `schedule_list`, `schedule_update`, and `schedule_delete` (update changes one reminder in place and keeps its id and saved delivery records); the Automation tasks page's New action opens a New Session for a creation instead, with the request already written in its composer. Supported choices are a one-time delay in positive whole seconds, an absolute date and time, a fixed interval of at least one minute, a daily local time with an IANA time zone, a weekly local time with an IANA time zone and ISO weekdays from Monday 1 through Sunday 7, or a five-field cron expression with an explicit IANA time zone, stored in canonical form. diff --git a/docs/user/guide/schedule.zh.md b/docs/user/guide/schedule.zh.md index 60829d82ae..bd10a06fe5 100644 --- a/docs/user/guide/schedule.zh.md +++ b/docs/user/guide/schedule.zh.md @@ -17,7 +17,7 @@ ## 创建提醒 -发布的 Web profile 会挂载 Schedule 及其时钟上下文,后者把当前时间和浏览器时区交给模型。在请求模型创建提醒前,先配置模型 provider。 +发布的 Web profile 把 `time-context`、`schedule` 与 `ui-schedule` 三行保持禁用。打开插件管理页,在 Official 分组中启用“定时与时间上下文”;可选实验性 bundle `@deepseek-ai/dsh-experimental-schedule-bundle` 随即挂载 Schedule 及其时钟上下文,后者把当前时间和浏览器时区交给模型。在请求模型创建提醒前,先配置模型 provider。 向模型提出创建、列出、修改或删除提醒的请求。模型使用 `schedule_create`、`schedule_list`、`schedule_update` 和 `schedule_delete`(更新原地修改一条提醒,保留其 id 与已保存的投递记录);「自动化任务」页面的「新建」操作则为创建打开一个新会话,其 composer 已写好该请求。支持正整数秒的一次性延迟、绝对日期时间、至少一分钟的固定间隔、带 IANA 时区的每日本地时间、带 IANA 时区和 ISO 星期(周一 1 至周日 7)的每周本地时间,以及带显式 IANA 时区的五字段 cron 表达式(表达式以规范化形式保存)。 diff --git a/packages/README.i18n.yaml b/packages/README.i18n.yaml index d091e6d74c..f9879d6dcd 100644 --- a/packages/README.i18n.yaml +++ b/packages/README.i18n.yaml @@ -15,8 +15,8 @@ en: ffc55792d6e922e1 zh: 83bd759638857e2d /packages/package-groups: - en: 2818a70fa6bbb1b6 - zh: 800b34102e82c192 + en: d865abbfa65d1d4f + zh: ea079b795b51dd55 /packages/release-expectations: en: 1ab880da3642bde1 zh: 6e168322f026a9cf diff --git a/packages/README.md b/packages/README.md index 54c4cdc758..cfb5ea6c74 100644 --- a/packages/README.md +++ b/packages/README.md @@ -34,6 +34,7 @@ Every package lives in exactly one group; new packages join existing groups, and | [`goal/`](goal/README.md) | Same-session goal persistence and lifecycle | | [`schedule/`](schedule/README.md) | Host-owned scheduled follow-ups | | [`feedback/`](feedback/README.md) | Human feedback capture and command | +| [`telemetry/`](telemetry/README.md) | Shared Cordis OTel reporting channels | | [`identity/`](identity/README.md) | Shared anonymous identity | | [`llm/`](llm/README.md) | LLM capability family: abstract service + provider adapters | | [`subprocess/`](subprocess/README.md) | Subprocess capability family: Service Definition + local process-tree provider | diff --git a/packages/README.zh.md b/packages/README.zh.md index 9ea4426082..2edb62d8cd 100644 --- a/packages/README.zh.md +++ b/packages/README.zh.md @@ -34,6 +34,7 @@ harness 由 `packages/` 下的 npm 包组装而成,按能力系列分组:会 | [`goal/`](goal/README.zh.md) | 同会话 goal 的持久化与生命周期 | | [`schedule/`](schedule/README.zh.md) | Host 拥有的定时后续操作 | | [`feedback/`](feedback/README.zh.md) | 人类反馈的采集与命令 | +| [`telemetry/`](telemetry/README.zh.md) | 共享 Cordis OTel 上报通道 | | [`identity/`](identity/README.zh.md) | 共享匿名身份 | | [`llm/`](llm/README.zh.md) | LLM(大语言模型)能力系列:抽象服务 + 提供方适配器 | | [`subprocess/`](subprocess/README.zh.md) | 子进程能力系列:Service Definition + 本地进程树提供方 | diff --git a/packages/boot/app-boot/src/profile.ts b/packages/boot/app-boot/src/profile.ts index da580c762e..e17e125a75 100644 --- a/packages/boot/app-boot/src/profile.ts +++ b/packages/boot/app-boot/src/profile.ts @@ -214,6 +214,7 @@ export const OPTIONAL_BUNDLES: readonly string[] = [ '@deepseek-ai/dsh-experimental-agent-team-profile', '@deepseek-ai/dsh-experimental-voice-input-bundle', '@deepseek-ai/dsh-experimental-auto-review', + '@deepseek-ai/dsh-experimental-schedule-bundle', ] const PROFILE_PATCH_TEMPLATE = `# Your patch layer for this dsh profile, applied after every bundle layer: diff --git a/packages/bundle/base/cordis.patch.yml b/packages/bundle/base/cordis.patch.yml index db6701a9b6..9c35ebf5c2 100644 --- a/packages/bundle/base/cordis.patch.yml +++ b/packages/bundle/base/cordis.patch.yml @@ -185,6 +185,9 @@ writeEveryEvents: 200 writeIntervalMs: 5000 + - id: otel + name: '@deepseek-ai/dsh-otel' + # OTel releases a Session-log prefix only after explicit user feedback, # regardless of model provider. Ordinary activity never triggers capture. # DSH_TELEMETRY_OTLP_URL overrides the production endpoint. A non-empty @@ -192,22 +195,20 @@ # process out (the launchers patch the row disabled; config cannot disable # a row). Exports carry the harness home's anonymous user id ($DSH_HOME/.anonymous-user-id, # random UUID; delete the file to reset the identity) as the Resource's - # user.id. The exporter/processor values normally bound the shutdown drain - # to ~1s against an unreachable collector: exporter.timeoutMillis is both - # the per-attempt socket timeout and the retry deadline (1s effectively - # disables the SDK's 5-try backoff), while maxExportBatchSize == maxQueueSize - # (both explicit) makes the drain a single batch. The SDK awaits - # exporter.forceFlush() outside exportTimeoutMillis, so the backend's 3s - # shutdownTimeoutMillis is the load-bearing outer bound when a transport - # promise never settles. Every CLI exit path drains it by disposing the root - # on SIGINT/SIGTERM. + # user.id. Byte-bounded batching may turn an authorized prefix into + # several HTTP requests. Requests run serially; each has its own 1s + # transport timeout and 1.5s watchdog. A watchdog does not release the + # active request slot. The outer 3s shutdown deadline stops queued sends; + # large prefixes or an unsettled transport may leave records unsent. + # Every CLI exit path drains by disposing the root on SIGINT/SIGTERM. - id: session-telemetry-otel name: '@deepseek-ai/dsh-session-telemetry-otel' config: mode: !!js process.env.DSH_TELEMETRY_MODE || 'FEEDBACK_ONLY' shutdownTimeoutMillis: 3000 + maxRequestBytes: 4000000 exporter: - url: !!js process.env.DSH_TELEMETRY_OTLP_URL ?? 'https://harness-telemetry.deepseeksvc.com/v1/logs' + url: !!js process.env.DSH_TELEMETRY_OTLP_URL ?? 'https://dsh-otel-collector.deepseeksvc.com/v1/logs' compression: gzip timeoutMillis: 1000 processor: diff --git a/packages/bundle/base/package.json b/packages/bundle/base/package.json index e5b0025eaa..51b37cf282 100644 --- a/packages/bundle/base/package.json +++ b/packages/bundle/base/package.json @@ -125,7 +125,8 @@ "@deepseek-ai/dsh-authorization": "workspace:*", "@deepseek-ai/dsh-deepseek-account-platform": "workspace:*", "@deepseek-ai/dsh-config-editor": "workspace:*", - "@deepseek-ai/dsh-settings": "workspace:*" + "@deepseek-ai/dsh-settings": "workspace:*", + "@deepseek-ai/dsh-otel": "workspace:*" }, "peerDependencies": { "@deepseek-ai/cordis": "workspace:~" diff --git a/packages/bundle/web-app/README.i18n.yaml b/packages/bundle/web-app/README.i18n.yaml index 753bb40fd8..f69476f693 100644 --- a/packages/bundle/web-app/README.i18n.yaml +++ b/packages/bundle/web-app/README.i18n.yaml @@ -21,8 +21,8 @@ en: b7ab01cfe9fed40f zh: 23703ab7727c3ab5 /deepseek-ai-dsh-web-app/use-this-package/configuration: - en: 4f18797d9de48038 - zh: 62387505417e4813 + en: cea7ecccb2a03b79 + zh: 0af72c22e46045d3 /deepseek-ai-dsh-web-app/use-this-package/lan-access-and-trusted-hosts: en: 2522c806db384ddc zh: 7e96f11194af9f9f diff --git a/packages/bundle/web-app/README.md b/packages/bundle/web-app/README.md index 4ee7449a94..42370ff203 100644 --- a/packages/bundle/web-app/README.md +++ b/packages/bundle/web-app/README.md @@ -53,7 +53,7 @@ Most users never set these; the command-line flags feed the four settings below | `surfaceContext` | `true` | Give the agent GUI-orientation context and expose `DSH_WEB_URL` to its shell commands | | `trustedHosts` | `[]` | Extra hosts allowed to reach the GUI from the network | -The generated [configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-web-app) is the exhaustive source for every accepted field and its JSDoc. The shipped composition disables `schedule`, `ui-schedule`, and `time-context` by default. +The generated [configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-web-app) is the exhaustive source for every accepted field and its JSDoc. The shipped composition disables `schedule`, `ui-schedule`, and `time-context` by default, and the optional experimental bundle `@deepseek-ai/dsh-experimental-schedule-bundle` switches those three rows on from the Plugins page. ### LAN access and trusted hosts diff --git a/packages/bundle/web-app/README.zh.md b/packages/bundle/web-app/README.zh.md index d54afd6e76..fe276317be 100644 --- a/packages/bundle/web-app/README.zh.md +++ b/packages/bundle/web-app/README.zh.md @@ -53,7 +53,7 @@ dsh --profile web --no-open --port 8080 | `surfaceContext` | `true` | 给 agent 提供 GUI 定位上下文,并把 `DSH_WEB_URL` 暴露给其 shell 命令 | | `trustedHosts` | `[]` | 允许从网络访问 GUI 的额外主机 | -生成的[配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-web-app)是每个受支持字段及其 JSDoc 的穷尽式真源。 随发行版交付的组合默认禁用 `schedule`、`ui-schedule` 和 `time-context`。 +生成的[配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-web-app)是每个受支持字段及其 JSDoc 的穷尽式真源。 随发行版交付的组合默认禁用 `schedule`、`ui-schedule` 和 `time-context`,可选实验性 bundle `@deepseek-ai/dsh-experimental-schedule-bundle` 可在插件管理页把这三行打开。 ### LAN 访问与可信主机 diff --git a/packages/bundle/web-app/package.json b/packages/bundle/web-app/package.json index 0c12abee8d..35c558314a 100644 --- a/packages/bundle/web-app/package.json +++ b/packages/bundle/web-app/package.json @@ -198,6 +198,7 @@ "@deepseek-ai/dsh-shell-env": "workspace:*", "@deepseek-ai/dsh-system-prompt": "workspace:*", "@deepseek-ai/dsh-typert-registry": "workspace:*", - "@deepseek-ai/libreoffice-kit": "^0.1.1" + "@deepseek-ai/libreoffice-kit": "^0.1.1", + "@deepseek-ai/dsh-otel": "workspace:*" } } diff --git a/packages/bundle/web-app/tests/product-analytics.spec.ts b/packages/bundle/web-app/tests/product-analytics.spec.ts index e76e3652e2..fbafdaae4d 100644 --- a/packages/bundle/web-app/tests/product-analytics.spec.ts +++ b/packages/bundle/web-app/tests/product-analytics.spec.ts @@ -1,3 +1,4 @@ +import OTel from '@deepseek-ai/dsh-otel' /** Desktop-only collector policy and bounded shutdown against an unresponsive receiver. */ import { createServer } from 'node:http' import { once } from 'node:events' @@ -29,6 +30,7 @@ it.each(['desktop', 'web'])('limits collection and its shutdown to the Desktop l }) const identity = vi.fn().mockResolvedValue(undefined) ctx.provide('deepseekAccount', { getDeviceIdentity: identity } as never) + await ctx.plugin(OTel) ctx.baseUrl = 'file:///' await ctx.plugin(Loader).await() ctx.loader.builtins.telemetry = Telemetry diff --git a/packages/client/product-analytics/package.json b/packages/client/product-analytics/package.json index 90867f2cfe..b3e2fffa7d 100644 --- a/packages/client/product-analytics/package.json +++ b/packages/client/product-analytics/package.json @@ -62,7 +62,8 @@ "@deepseek-ai/dsh-deepseek-account": "workspace:*", "@deepseek-ai/dsh-host-webserver": "workspace:*", "@deepseek-ai/dsh-settings": "workspace:*", - "@deepseek-ai/cordis-plugin-loader": "workspace:~" + "@deepseek-ai/cordis-plugin-loader": "workspace:~", + "@deepseek-ai/dsh-otel": "workspace:*" }, "files": [ "lib/index.js", diff --git a/packages/client/product-analytics/tests/collection.host.spec.ts b/packages/client/product-analytics/tests/collection.host.spec.ts index 67e6015f12..c7878a8940 100644 --- a/packages/client/product-analytics/tests/collection.host.spec.ts +++ b/packages/client/product-analytics/tests/collection.host.spec.ts @@ -1,3 +1,4 @@ +import OTel from '@deepseek-ai/dsh-otel' import { Context } from '@deepseek-ai/cordis' import { createServer } from 'node:http' import { once } from 'node:events' @@ -76,6 +77,7 @@ it('writes the selected event through the real exporter to an isolated collector ctx.provide('credentials', { readRecord: async () => undefined } as never) ctx.provide('deepseekAccount', { getDeviceIdentity: async () => undefined } as never) ctx.provide('webServer', {} as never) + await ctx.plugin(OTel) const exporter = await ctx.plugin(ProductTelemetry, TelemetryConfig({ endpoint: `http://127.0.0.1:${address.port}/v1/logs`, serviceName: 'test', serviceVersion: '1', compression: 'none' })) await ctx.plugin(Analytics, { enabled: true }) await ctx.productAnalytics.report({ eventName: 'api_key_save_click', timestamp: 1_800_000_000_000, attributes: {} }) diff --git a/packages/client/ui-schedule/README.i18n.yaml b/packages/client/ui-schedule/README.i18n.yaml index 9797b69beb..ec785c3dda 100644 --- a/packages/client/ui-schedule/README.i18n.yaml +++ b/packages/client/ui-schedule/README.i18n.yaml @@ -15,8 +15,8 @@ en: d152484eb41ac6b4 zh: 405b30feaf48c087 /deepseek-ai-dsh-client-ui-schedule/use-this-package: - en: 3594ab6704cbd7b9 - zh: 99a965f43187501b + en: bf8096e6e576ac07 + zh: 1936c0a75dc5cd4b /deepseek-ai-dsh-client-ui-schedule/use-this-package/time-zone-data-source: en: 6bd6882dbbbefcb0 zh: 03aa15f1a65d13dc diff --git a/packages/client/ui-schedule/README.md b/packages/client/ui-schedule/README.md index 577e9e4ccc..54b0b172b5 100644 --- a/packages/client/ui-schedule/README.md +++ b/packages/client/ui-schedule/README.md @@ -25,7 +25,7 @@ Manage active and inactive tasks from the Automation tasks page and the right Si ## Use this package -The shipped Web bundle mounts `ui-schedule` with the Host Schedule service and the clock context that supplies the model's current time. The Automation tasks sidebar entry opens a global page without selecting or activating a Session. The page header's New action starts a new Session, where the reminder is created by asking the model; the page itself has no creation form. Its task list supports text search and one All, Enabled, and Inactive status filter row; search matches the stored task name, the instruction, and the internal Session id, not a resolved Session title, and the empty state keeps the heading's New action beside the notice. Selecting an active or inactive task opens a second detail level with Rules and Delivery records tabs. Every list row shows the task's stored title with its status, frequency, and next run and never the original Session name; the detail's name control shows the stored title. Rules contains the complete reminder, status, next target for active tasks, original recurrence rule, and task id. Daily rule labels show the saved local time and exact stored IANA zone, preserving nonzero seconds and milliseconds rather than substituting the browser's zone. The Linked session entry in the detail tab strip names the linked Session by its catalog title, falling back to the Session id when no title is available. It opens an available, unarchived Session after metadata loads; pending Session or Workspace metadata, an archived Session, missing Session membership, or a Workspace query error keeps it disabled with an explanation. The click rechecks availability; browsing does not unarchive the Session. Only this action requests Session activation. Deletion is available from the More task actions menu in the detail's top strip, separate from navigation. +The shipped Web composition carries the `ui-schedule` row disabled; enable the optional experimental bundle `@deepseek-ai/dsh-experimental-schedule-bundle` from the Plugins page (Official group), or list it in a profile's `dsh.profile.bundles`, to mount `ui-schedule` with the Host Schedule service and the clock context that supplies the model's current time. The Automation tasks sidebar entry opens a global page without selecting or activating a Session. The page header's New action starts a new Session, where the reminder is created by asking the model; the page itself has no creation form. Its task list supports text search and one All, Enabled, and Inactive status filter row; search matches the stored task name, the instruction, and the internal Session id, not a resolved Session title, and the empty state keeps the heading's New action beside the notice. Selecting an active or inactive task opens a second detail level with Rules and Delivery records tabs. Every list row shows the task's stored title with its status, frequency, and next run and never the original Session name; the detail's name control shows the stored title. Rules contains the complete reminder, status, next target for active tasks, original recurrence rule, and task id. Daily rule labels show the saved local time and exact stored IANA zone, preserving nonzero seconds and milliseconds rather than substituting the browser's zone. The Linked session entry in the detail tab strip names the linked Session by its catalog title, falling back to the Session id when no title is available. It opens an available, unarchived Session after metadata loads; pending Session or Workspace metadata, an archived Session, missing Session membership, or a Workspace query error keeps it disabled with an explanation. The click rechecks availability; browsing does not unarchive the Session. Only this action requests Session activation. Deletion is available from the More task actions menu in the detail's top strip, separate from navigation. Selecting a task from the list opens Rules; refreshing that task preserves the selected tab. Rules edits an active task's name, instruction, and run time; the edits stay in a local draft until saved, and inactive tasks remain read-only. Unmodified Left/Right arrows cycle the tabs, Home/End select the first/last tab, and keyboard focus follows the selection. The tabs do not consume these keys with Alt, Ctrl, Meta, or Shift. The linked-Session control in the detail tab strip stays available on the Rules tab, and confirmed deletion is available from either view. diff --git a/packages/client/ui-schedule/README.zh.md b/packages/client/ui-schedule/README.zh.md index 743d8a5525..4a4fab6b96 100644 --- a/packages/client/ui-schedule/README.zh.md +++ b/packages/client/ui-schedule/README.zh.md @@ -25,7 +25,7 @@ kind: "package-reference" ## 使用本包 -发布的 Web bundle 将 `ui-schedule` 与 Host Schedule 服务、以及为模型提供当前时间的时钟上下文一起挂载。侧栏的自动化任务入口打开全局页面,不选择或激活会话。页面页头的“新建”操作会打开一个新会话,提醒由模型在该会话中创建;页面本身没有创建表单。列表支持文本搜索和一行“全部/已开启/未运行”状态筛选;搜索匹配任务已存储的名称、指令和内部会话 id,不匹配解析出的会话标题,空状态在提示旁保留页头的“新建”操作。选择活动或未运行任务后,进入含“规则”和“任务运行记录”页签的第二层详情。每条列表行显示任务已存储的标题及其状态、频率和下次运行,不显示原会话名称;详情中的名称输入框显示任务已存储的标题。“规则”显示完整提醒、状态、活动任务的下一目标时间、原始重复规则和任务 id。每天的规则标签显示已保存的本地时间与精确的存储 IANA 时区,保留非零秒与毫秒,而不替换为浏览器时区。详情页签条内的“关联会话”入口按会话标题显示,取不到标题时回退显示会话 id。元数据加载后,它可打开可用且未归档的会话;会话或 Workspace 元数据尚未就绪、会话已归档、会话不在列表中或 Workspace 查询失败时,入口保持禁用并显示原因。点击时会重新检查可用性;浏览不会取消会话归档。仅此操作请求激活会话。删除位于详情顶部条带的“更多任务操作”菜单。 +随发行版交付的 Web 组合把 `ui-schedule` 行保持禁用;在插件管理页(Official 分组)启用可选实验性 bundle `@deepseek-ai/dsh-experimental-schedule-bundle`,或把它列进 profile 的 `dsh.profile.bundles`,即可将 `ui-schedule` 与 Host Schedule 服务、以及为模型提供当前时间的时钟上下文一起挂载。侧栏的自动化任务入口打开全局页面,不选择或激活会话。页面页头的“新建”操作会打开一个新会话,提醒由模型在该会话中创建;页面本身没有创建表单。列表支持文本搜索和一行“全部/已开启/未运行”状态筛选;搜索匹配任务已存储的名称、指令和内部会话 id,不匹配解析出的会话标题,空状态在提示旁保留页头的“新建”操作。选择活动或未运行任务后,进入含“规则”和“任务运行记录”页签的第二层详情。每条列表行显示任务已存储的标题及其状态、频率和下次运行,不显示原会话名称;详情中的名称输入框显示任务已存储的标题。“规则”显示完整提醒、状态、活动任务的下一目标时间、原始重复规则和任务 id。每天的规则标签显示已保存的本地时间与精确的存储 IANA 时区,保留非零秒与毫秒,而不替换为浏览器时区。详情页签条内的“关联会话”入口按会话标题显示,取不到标题时回退显示会话 id。元数据加载后,它可打开可用且未归档的会话;会话或 Workspace 元数据尚未就绪、会话已归档、会话不在列表中或 Workspace 查询失败时,入口保持禁用并显示原因。点击时会重新检查可用性;浏览不会取消会话归档。仅此操作请求激活会话。删除位于详情顶部条带的“更多任务操作”菜单。 从列表选择任务时默认打开“规则”;刷新同一任务会保留当前页签。活动任务的“规则”修改名称、指令和运行时间;这些修改保留在本地草稿中直到保存,未运行任务保持只读。不带修饰键的左右方向键循环切换页签,Home/End 选择第一个/最后一个页签,键盘焦点随选择移动。页签不会拦截这些键与 Alt、Ctrl、Meta 或 Shift 的组合。共用的会话入口位于详情页签条内,在“规则”页签可见;确认删除操作在两种页签下都可用。 diff --git a/packages/client/ui-settings-general/README.i18n.yaml b/packages/client/ui-settings-general/README.i18n.yaml index 1b6dd64dde..42484f8a4a 100644 --- a/packages/client/ui-settings-general/README.i18n.yaml +++ b/packages/client/ui-settings-general/README.i18n.yaml @@ -15,8 +15,8 @@ en: aeb9c4bfdb19bc56 zh: 45a29bea90cff239 /deepseek-ai-dsh-client-ui-settings-general/use-this-package: - en: da896a9cf06fe5a5 - zh: 27545af457695988 + en: 473e1f0843f7a896 + zh: acfbc21efc458bbf /deepseek-ai-dsh-client-ui-settings-general/use-this-package/the-general-section: en: 8b8a7c6287bb303b zh: 19535112531aedd5 diff --git a/packages/client/ui-settings-general/README.md b/packages/client/ui-settings-general/README.md index 95b38c7103..29a1a7d544 100644 --- a/packages/client/ui-settings-general/README.md +++ b/packages/client/ui-settings-general/README.md @@ -31,7 +31,7 @@ Users reach the shell through the sidebar's bottom Settings control; feature plu When the section navigation exceeds the panel's available height, the list scrolls independently of the settings content and keeps the Settings title fixed. -In Desktop, the account-row update control shows availability, progress, verification, readiness, and persistent retry feedback. It shares the connection indicator’s 28px height, 8px corners, 14px icon slot, 4px icon gap, and 12px medium text with an 18px line height; update-state border, fill, and text colors remain independently defined. Retry text and its dot use the same brand blue as other update labels; numeric download progress has no ellipsis. The preload carries semantic phase, version, progress, and classified failures; the component resolves every visible and accessible string from the active `settings` locale, including after an in-application language change. Selecting an available update starts downloading; installation requires a separate shell-owned confirmation. A collapsed sidebar shows the same status as a brand-blue dot on its top expand button, including failures. Connection feedback takes priority except during shell-reported installation, when the expected backend disconnect must not hide update status. Failure restores connection feedback. Both controls share one carrier subscription; browser code cannot choose packages or authorize installation. [Desktop updates](../../../apps/desktop/README.md) owns the release workflow. +In Desktop, the account-row update control shows availability, progress, verification, readiness, and persistent retry feedback. It shares the connection indicator’s 28px height, 8px corners, 14px icon slot, 4px icon gap, and 12px medium text with an 18px line height; update-state border, fill, and text colors remain independently defined. Retry text and its dot use the same brand blue as other update labels; numeric download progress has no ellipsis. The preload carries semantic phase, version, progress, and classified failures; the component resolves every visible and accessible string from the active `settings` locale, including after an in-application language change. Selecting an available update starts downloading; installation requires a separate shell-owned confirmation. A collapsed sidebar shows the same status as a brand-blue dot on its top expand button, including failures. Connection feedback takes priority except during shell-reported installation, when the expected backend disconnect must not hide update status. Failure restores connection feedback. Both controls share one carrier subscription; browser code cannot choose packages or authorize installation. [Desktop updates](../../../apps/desktop/README.md) owns the release workflow. Checking, verifying, and preparing-to-restart tooltips show only the supplied version; without a version, they retain the state label. Desktop update tooltips preserve the complete version and prerelease suffix without adding v or V; their error summaries match the shell update dialogs. Settings visibility and section selection live in the shell owner store. The shell supplies the effective Settings binding to the contributed launcher for menu keycaps and `aria-keyshortcuts`; the fallback button uses the same binding for hover and keyboard-focus hints and `aria-keyshortcuts`. The Settings command (`Mod+,` by default) toggles the dialog when Settings is in front or no modal is open; the sidebar control opens the same dialog. The command cannot open or close Settings while the shortcut reference or another modal is in front. Held-key repeats do nothing. Initial focus lands on the selected section in the navigation, or on the title when no sections are available, without drawing a focus outline. Tab and directional navigation retain their visible focus indicators. `Mod+/` can open the shortcut reference above Settings; `Escape` closes the top dialog and restores focus to its invoking control. Return focus omits outlines after shortcut, Escape, or pointer dismissal. diff --git a/packages/client/ui-settings-general/README.zh.md b/packages/client/ui-settings-general/README.zh.md index 6874f1e5f8..f43966253f 100644 --- a/packages/client/ui-settings-general/README.zh.md +++ b/packages/client/ui-settings-general/README.zh.md @@ -31,7 +31,7 @@ kind: "package-reference" 当分区导航超出面板可用高度时,列表独立于设置内容滚动,并保持「设置」标题固定。 -在 Desktop 中,账户行更新控件显示可用版本、进度、验证、就绪和持久重试反馈。它与连接指示器共用 28px 高度、8px 圆角、14px 图标占位、4px 图文间距,以及 12px 中等字重文字和 18px 行高;更新状态的边框、填充和文字颜色独立定义。重试文字及其圆点与其他更新文案使用相同的品牌蓝色;数字下载进度不带省略号。预加载仅传递语义化阶段、版本、进度和失败类别;组件从当前 `settings` locale 解析全部可见与无障碍文案,并在应用内语言变化后同步更新。选择可用更新即开始下载;安装需要独立的壳拥有的确认。侧栏收起时,其顶部展开按钮以品牌蓝色圆点显示相同状态,包括失败状态。连接反馈通常优先展示,但壳报告正在安装时,预期的后端断开不能遮盖更新状态;失败后恢复连接反馈。两个控件共用一次载体订阅;浏览器代码不能选择安装包或授权安装。[Desktop 更新](../../../apps/desktop/README.zh.md)负责发布流程。 +在 Desktop 中,账户行更新控件显示可用版本、进度、验证、就绪和持久重试反馈。它与连接指示器共用 28px 高度、8px 圆角、14px 图标占位、4px 图文间距,以及 12px 中等字重文字和 18px 行高;更新状态的边框、填充和文字颜色独立定义。重试文字及其圆点与其他更新文案使用相同的品牌蓝色;数字下载进度不带省略号。预加载仅传递语义化阶段、版本、进度和失败类别;组件从当前 `settings` locale 解析全部可见与无障碍文案,并在应用内语言变化后同步更新。选择可用更新即开始下载;安装需要独立的壳拥有的确认。侧栏收起时,其顶部展开按钮以品牌蓝色圆点显示相同状态,包括失败状态。连接反馈通常优先展示,但壳报告正在安装时,预期的后端断开不能遮盖更新状态;失败后恢复连接反馈。两个控件共用一次载体订阅;浏览器代码不能选择安装包或授权安装。[Desktop 更新](../../../apps/desktop/README.zh.md)负责发布流程。检查中、校验中和准备重启状态的悬浮提示只显示已提供的版本号;没有版本号时保留状态文案。桌面更新提示完整保留版本号及预发布后缀,不额外添加 v 或 V;错误提示与桌面升级弹窗保持一致。 设置的可见状态和分区选择由 shell owner store 保存。外壳将设置命令的有效绑定传给入口贡献,用于菜单键帽和 `aria-keyshortcuts`;回退按钮使用相同绑定提供悬停、键盘聚焦提示和 `aria-keyshortcuts`。设置位于最前方或没有模态弹窗时,设置命令(默认为 `Mod+,`)切换设置弹窗的开关;侧栏控件打开同一弹窗。快捷键速查或其他模态弹窗位于前方时,该命令不能打开或关闭设置。长按产生的重复按键不执行操作。初始焦点落在导航中选中的分区;没有可用分区时落在标题上,均不绘制焦点外轮廓线。Tab 和方向键导航保留可见焦点提示。`Mod+/` 可在设置上方打开快捷键速查;`Escape` 关闭最上层弹窗,焦点返回触发该弹窗的控件。通过快捷键、Esc 或鼠标关闭后的回焦均不绘制外轮廓线。 diff --git a/packages/client/ui-settings-general/src/client/DesktopUpdateIndicator.tsx b/packages/client/ui-settings-general/src/client/DesktopUpdateIndicator.tsx index 15c671155b..588b3befdb 100644 --- a/packages/client/ui-settings-general/src/client/DesktopUpdateIndicator.tsx +++ b/packages/client/ui-settings-general/src/client/DesktopUpdateIndicator.tsx @@ -23,6 +23,9 @@ function updateCopy(state: DesktopUpdatePresentation, t: SettingsTranslate): { l error: t('desktop.update.retry'), } const label = labels[state.phase] + if (state.phase === 'checking' || state.phase === 'verifying' || state.phase === 'installing') { + return { label, detail: state.version ?? label } + } if (state.phase === 'error') { const failures: Readonly> = { check: t('desktop.update.checkFailed'), diff --git a/packages/client/ui-settings-general/src/client/locales.ts b/packages/client/ui-settings-general/src/client/locales.ts index e5a3bd1855..777af69935 100644 --- a/packages/client/ui-settings-general/src/client/locales.ts +++ b/packages/client/ui-settings-general/src/client/locales.ts @@ -11,16 +11,16 @@ export const zh = { 'desktop.update.installing': '正在准备重启…', 'desktop.update.ready': '安装并重启', 'desktop.update.retry': '重试更新', - 'desktop.update.versionDetail': '{label} — V{version}', - 'desktop.update.downloadDetail': '正在下载更新:{percent}%\n目标版本:V{version}', + 'desktop.update.versionDetail': '{label}:{version}', + 'desktop.update.downloadDetail': '正在下载更新:{percent}%\n目标版本:{version}', 'desktop.update.checkFailed': '检查更新失败,请稍后重试。', 'desktop.update.downloadFailed': '下载更新失败,请重试。', 'desktop.update.installFailed': '安装更新失败,请稍后重试。', - 'desktop.update.checkNetworkFailed': '检查更新失败,请稍后重试。网络连接异常,请检查网络后重试。', - 'desktop.update.downloadNetworkFailed': '下载更新失败,请重试。网络连接异常,请检查网络后重试。', - 'desktop.update.installNetworkFailed': '安装更新失败,请稍后重试。网络连接异常,请检查网络后重试。', - 'desktop.update.stopFailed': '未能安全停止任务,更新未安装。请稍后重试。', - 'desktop.update.tasksChanged': '有新任务开始,请重新确认更新。', + 'desktop.update.checkNetworkFailed': '检查更新失败,请检查网络连接后重试。', + 'desktop.update.downloadNetworkFailed': '下载更新失败,请检查网络连接后重试。', + 'desktop.update.installNetworkFailed': '安装更新失败,请检查网络连接后重试。', + 'desktop.update.stopFailed': '未能安全停止任务,更新尚未安装,请稍后重试。', + 'desktop.update.tasksChanged': '有新任务开始运行,请重新确认是否停止任务并更新。', 'desktop.update.tasksUnavailable': '无法确认任务状态,请在工作区就绪后重试更新。', 'title': '设置', 'close': '关闭', @@ -52,16 +52,16 @@ export const en = { 'desktop.update.installing': 'Preparing to restart…', 'desktop.update.ready': 'Install and Restart', 'desktop.update.retry': 'Retry update', - 'desktop.update.versionDetail': '{label} — V{version}', - 'desktop.update.downloadDetail': 'Downloading update: {percent}%\nTarget version: V{version}', + 'desktop.update.versionDetail': '{label}: {version}', + 'desktop.update.downloadDetail': 'Downloading update: {percent}%\nTarget version: {version}', 'desktop.update.checkFailed': 'Could not check for updates. Please try again later.', 'desktop.update.downloadFailed': 'Could not download the update. Please try again.', 'desktop.update.installFailed': 'Could not install the update. Please try again later.', - 'desktop.update.checkNetworkFailed': 'Could not check for updates. Please try again later. The connection was interrupted. Check your network and try again.', - 'desktop.update.downloadNetworkFailed': 'Could not download the update. Please try again. The connection was interrupted. Check your network and try again.', - 'desktop.update.installNetworkFailed': 'Could not install the update. Please try again later. The connection was interrupted. Check your network and try again.', - 'desktop.update.stopFailed': 'Tasks could not be stopped safely. The update was not installed. Please try again later.', - 'desktop.update.tasksChanged': 'New tasks started. Review the update confirmation again.', + 'desktop.update.checkNetworkFailed': 'Could not check for updates. Check your connection and try again.', + 'desktop.update.downloadNetworkFailed': 'Could not download the update. Check your connection and try again.', + 'desktop.update.installNetworkFailed': 'Could not install the update. Check your connection and try again.', + 'desktop.update.stopFailed': 'Could not safely stop the tasks. The update has not been installed. Please try again later.', + 'desktop.update.tasksChanged': 'New tasks have started. Confirm again to stop the tasks and update.', 'desktop.update.tasksUnavailable': 'Task status is unavailable. Try updating again when the workspace is ready.', 'title': 'Settings', 'close': 'Close', diff --git a/packages/client/ui-settings-general/tests/desktop-update.client.spec.tsx b/packages/client/ui-settings-general/tests/desktop-update.client.spec.tsx index 7e81c3f649..505a02d969 100644 --- a/packages/client/ui-settings-general/tests/desktop-update.client.spec.tsx +++ b/packages/client/ui-settings-general/tests/desktop-update.client.spec.tsx @@ -152,3 +152,28 @@ it('shows fallback progress and error details when the shell omits optional fiel expect((await screen.findByRole('tooltip')).textContent).toBe('安装更新失败,请稍后重试。') } finally { f.view.unmount(); f.status.resolve({ phase: 'idle' }) } }) + +it.each(['checking', 'verifying', 'installing'] as const)('shows only the version during %s in both locales', async (phase) => { + const f = fixture() + try { + await f.emit({ phase, version: '0.1.7-alpha.2' }) + fireEvent.focus(screen.getByRole('button')) + expect((await screen.findByRole('tooltip')).textContent).toBe('0.1.7-alpha.2') + f.view.rerender() + fireEvent.focus(screen.getByRole('button')) + expect((await screen.findByRole('tooltip')).textContent).toBe('0.1.7-alpha.2') + } finally { f.view.unmount(); f.status.resolve({ phase: 'idle' }) } +}) + +it.each([ + ['checking', '正在检查更新…'], + ['verifying', '正在校验更新文件…'], + ['installing', '正在准备重启…'], +] as const)('retains the state label when %s has no version', async (phase, label) => { + const f = fixture() + try { + await f.emit({ phase }) + fireEvent.focus(screen.getByRole('button', { name: label })) + expect((await screen.findByRole('tooltip')).textContent).toBe(label) + } finally { f.view.unmount(); f.status.resolve({ phase: 'idle' }) } +}) diff --git a/packages/client/ui-workspace/README.i18n.yaml b/packages/client/ui-workspace/README.i18n.yaml index 4cd1c994c4..bf8d40aa1d 100644 --- a/packages/client/ui-workspace/README.i18n.yaml +++ b/packages/client/ui-workspace/README.i18n.yaml @@ -15,8 +15,8 @@ en: d152484eb41ac6b4 zh: 09388d293f9be9cb /deepseek-ai-dsh-client-ui-workspace/use-this-package: - en: c8d579e1e65bc8c5 - zh: 56f8e45351445ec7 + en: b164014ee565be16 + zh: 6a741647932b93e6 /deepseek-ai-dsh-client-ui-workspace/use-this-package/reordering-and-view-options: en: 1fe0693758e0536b zh: ef30198bc767fc04 diff --git a/packages/client/ui-workspace/README.md b/packages/client/ui-workspace/README.md index 97f6e78bcd..7ac6134f4e 100644 --- a/packages/client/ui-workspace/README.md +++ b/packages/client/ui-workspace/README.md @@ -29,6 +29,8 @@ This package lets users browse grouped or flat Session lists, choose a Workspace Use the sidebar to browse Workspaces and their Sessions, reorder them, and start new ones; use the picker in the Session Intent hero to choose a Workspace for a new session. An open Workspace shows five idle, non-blank Sessions by default. Running Sessions, including parents with running children, remain visible in their ordered positions without using that quota; the selected blank **New Session** is also an extra row until its first prompt. Each **Show more** click reveals up to five more idle Sessions; after the final batch, **Show less** restores the initial rows while keeping running Sessions visible. Closing and reopening the Workspace also restores this folded projection. +History rows without a stored title use the localized unnamed label (未命名 / Untitled), rather than a directory name. The current blank row remains New Session; other blank rows remain hidden. Rename drafts use the stored title, or start empty when unnamed; unnamed rows do not offer title copying. + ### Reordering and view options Pinned Sessions lead ordinary Sessions in both grouped and flat views. **Last updated** sorts each partition strictly by the latest user prompt or steer time, newest first; pin time does not affect it. **Manual** uses the relative positions in one complete Session sequence, including hidden archives. Returning to Last updated discards the manual layout, and entering Manual again freezes the then-current chronological order. The browser defaults to Last updated and remembers the selected mode across reloads. diff --git a/packages/client/ui-workspace/README.zh.md b/packages/client/ui-workspace/README.zh.md index 62da6b8394..a9bd250a6d 100644 --- a/packages/client/ui-workspace/README.zh.md +++ b/packages/client/ui-workspace/README.zh.md @@ -29,6 +29,8 @@ kind: "package-reference" 用侧边栏浏览 Workspace 及其 Session、重排它们并新建会话;在 Session Intent 主视觉区用选择器为新会话选择 Workspace。打开的 Workspace 默认显示五条空闲的非空白 Session。正在运行的 Session(包括有子会话正在运行的父会话)始终按原顺序显示,不占用这五条配额;当前选中的空白**新会话**在首条提示词落地前也作为额外行。每次点击**展开其余**最多再显示五条空闲 Session;全部显示后,**收起**恢复初始行数,但仍显示正在运行的 Session。关闭再打开 Workspace 也会恢复该折叠投影。 +未保存标题的历史会话显示本地化名称「未命名 / Untitled」,不使用目录名兜底。当前空会话仍显示「新会话」,其他空会话仍隐藏。重命名草稿使用已保存标题,无标题时留空;无标题行不提供标题复制。 + ### 重排序与视图选项 分组和平铺视图都先显示置顶 Session,再显示普通 Session。**最近更新**在各分区内严格按最近一次用户提示词或 steering(中途引导)时间降序排列,置顶时间不影响它。**手动排序**使用同一条完整 Session 序列中的相对位置,包括隐藏的归档项。返回最近更新会丢弃手动布局,再次进入手动排序时冻结当时的时间顺序。浏览器默认采用最近更新,并在重新加载后记住所选模式。 diff --git a/packages/client/ui-workspace/src/client/contract/slots.ts b/packages/client/ui-workspace/src/client/contract/slots.ts index 830cc3570a..45f8564c8e 100644 --- a/packages/client/ui-workspace/src/client/contract/slots.ts +++ b/packages/client/ui-workspace/src/client/contract/slots.ts @@ -76,7 +76,7 @@ export interface DirectoryFlowOwnerProps { export interface SessionRowOwnerProps { /** Session the row shows. */ sessionId: SessionId - /** Row display title: persisted title, project basename, or Session id. */ + /** Row display title: persisted title, or empty when the Session has none. */ displayTitle: string } diff --git a/packages/client/ui-workspace/src/client/locales.ts b/packages/client/ui-workspace/src/client/locales.ts index 5a615c5509..1ce42fab28 100644 --- a/packages/client/ui-workspace/src/client/locales.ts +++ b/packages/client/ui-workspace/src/client/locales.ts @@ -9,6 +9,7 @@ export const zh = { 'defaultWorkspace.failed': '无法创建默认工作区,请通过“选择工作区”选择文件夹', 'group.ungrouped': '未分组', 'session.new': '新会话', + 'session.untitled': '未命名', 'shortcut.noSession': '请先选择一个会话', 'shortcut.noPicker': '目录选择器不可用', 'shortcut.directoryBusy': '正在选择或添加工作区', @@ -127,6 +128,7 @@ export const en = { 'defaultWorkspace.failed': 'Unable to create default workspace. Use Choose workspace to select a folder.', 'group.ungrouped': 'Ungrouped', 'session.new': 'New Session', + 'session.untitled': 'Untitled', 'shortcut.noSession': 'Select a session first', 'shortcut.noPicker': 'Directory picker unavailable', 'shortcut.directoryBusy': 'Selecting or adding a workspace', diff --git a/packages/client/ui-workspace/src/client/rows/Rows.tsx b/packages/client/ui-workspace/src/client/rows/Rows.tsx index 635770fd47..9179773872 100644 --- a/packages/client/ui-workspace/src/client/rows/Rows.tsx +++ b/packages/client/ui-workspace/src/client/rows/Rows.tsx @@ -46,7 +46,7 @@ type RowRenderSlots = PropsRenderSlots< /** Row display title: blank rows show the localized New Session label. */ function displayTitle(node: SessionNode, t: RowTranslate): string { - return node.blank ? t('session.new') : node.title + return node.blank ? t('session.new') : (node.title || t('session.untitled')) } /* Overflow this small hides no meaningful tail; scrolling for it reads as an @@ -495,7 +495,7 @@ export function SearchResultItem({ result, currentId, onOpen, onUnarchive, t }: )} - {result.title} + {result.title || t('session.untitled')} {result.archived && ( @@ -695,7 +695,7 @@ export function SessionNodeItem({ content={} openDelayMs={800} disabled={menuOpen || drag?.active === true} - copyText={row.blank ? undefined : row.title} + copyText={row.blank || row.title === '' ? undefined : row.title} copyLabel={t('copy')} copiedLabel={t('hover.copied')} /> diff --git a/packages/client/ui-workspace/src/client/session-actions/RenameSession.tsx b/packages/client/ui-workspace/src/client/session-actions/RenameSession.tsx index ee9819ace2..5e67673e77 100644 --- a/packages/client/ui-workspace/src/client/session-actions/RenameSession.tsx +++ b/packages/client/ui-workspace/src/client/session-actions/RenameSession.tsx @@ -40,7 +40,7 @@ export function RenameSessionMenuItem({ * one dialog per request (keyed by the Session, so a new request starts a * fresh draft). Sessions have no client-side name-conflict rule (the host * normalizes), and unlike Workspace rename an unchanged title is NOT - * blocked: confirming the current automatic title is the gesture that pins it. + * blocked. An unnamed Session starts with an empty draft and requires a name. * @param props - the request hook, its settlement, the rename hop, and the locale seat. * @returns the open dialog, or null. */ diff --git a/packages/client/ui-workspace/src/client/shortcuts.ts b/packages/client/ui-workspace/src/client/shortcuts.ts index d4f21a720e..669b240b32 100644 --- a/packages/client/ui-workspace/src/client/shortcuts.ts +++ b/packages/client/ui-workspace/src/client/shortcuts.ts @@ -96,7 +96,7 @@ export function installWorkspaceShortcuts( register('session.rename', () => t('rename.session.title'), ['rename session'], 'KeyR', ['primary', 'alt'], ['primary', 'shift'], () => { const target = current() return target === undefined ? { status: 'blocked', reason: t('shortcut.noSession') } - : { status: 'handled', run: () => { controls.rename(target.id, target.displayTitle) } } + : { status: 'handled', run: () => { controls.rename(target.id, target.title?.trim() ?? '') } } }) register('session.fork', () => t('menu.fork'), ['fork session'], 'KeyF', ['primary', 'alt'], ['primary', 'shift'], () => { const target = current() diff --git a/packages/client/ui-workspace/src/client/tree.ts b/packages/client/ui-workspace/src/client/tree.ts index 0be603f662..eb67d0ebe3 100644 --- a/packages/client/ui-workspace/src/client/tree.ts +++ b/packages/client/ui-workspace/src/client/tree.ts @@ -43,7 +43,7 @@ function mainSessionId(list: SessionListState): SessionId | undefined { /** One top-level session row in a group or the flat list. */ export interface SessionNode { id: SessionId - /** Stored display title; the renderer substitutes the localized New Session label for blank rows. */ + /** Stored title, or empty; the renderer localizes blank and unnamed row labels. */ title: string /** The provisional blank session (renderer shows the localized New Session title). */ blank: boolean @@ -289,10 +289,11 @@ function sectionMembers( /** * A blank session is the selected Workspace's provisional New Session row; * its canonical title never enters search (blank rows are query-excluded) - * and the renderer localizes its display label. + * and the renderer localizes its display label. Unnamed history also yields an + * empty title for localization and does not match a directory-name title search. */ function sessionTitle(session: SessionSummary): string { - return session.blank ? '' : session.displayTitle + return session.blank ? '' : (session.title?.trim() ?? '') } /** Build one group without projecting session lineage into presentation. */ diff --git a/packages/client/ui-workspace/tests/rename-assembly.client.spec.tsx b/packages/client/ui-workspace/tests/rename-assembly.client.spec.tsx index cc785a041b..0187272d53 100644 --- a/packages/client/ui-workspace/tests/rename-assembly.client.spec.tsx +++ b/packages/client/ui-workspace/tests/rename-assembly.client.spec.tsx @@ -119,8 +119,8 @@ describe('session rename through the assembled browser', () => { registerAction('last', 600, -2, 'Last action') const view = runtime.renderRoot() - const row = (await view.findByText('Session title')).closest('[role="treeitem"]')! - const trigger = within(row as HTMLElement).getByLabelText('会话“Session title”的操作') + const row = (await view.findByText('Persisted title')).closest('[role="treeitem"]')! + const trigger = within(row as HTMLElement).getByLabelText('会话“Persisted title”的操作') fireEvent.click(trigger) expect(view.getAllByRole('menuitem').map(item => item.textContent)).toEqual([ '置顶会话', '重命名', '分叉会话', '归档会话', 'Export action', 'Last action', @@ -134,7 +134,7 @@ describe('session rename through the assembled browser', () => { fireEvent.keyDown(last, { key: 'ArrowUp' }) expect(document.activeElement).toBe(exportRow) fireEvent.click(exportRow) - expect(selected).toHaveBeenCalledWith('export', SID, 'Session title') + expect(selected).toHaveBeenCalledWith('export', SID, 'Persisted title') // The plugin row dismissed the menu through the bound open-state hook; // the list returns focus to the trigger. expect(view.queryByRole('menu')).toBeNull() diff --git a/packages/client/ui-workspace/tests/rows.client.spec.tsx b/packages/client/ui-workspace/tests/rows.client.spec.tsx index cbe570e527..6a32265a17 100644 --- a/packages/client/ui-workspace/tests/rows.client.spec.tsx +++ b/packages/client/ui-workspace/tests/rows.client.spec.tsx @@ -1080,3 +1080,24 @@ describe('session row schedule seats', () => { } }) }) + +it.each([['未命名', t], ['Untitled', tEn]])('labels unnamed history as %s', (label, translate) => { + const node: SessionNode = { id: sid('unnamed'), title: '', blank: false, running: false, + runningSubagentCount: 0, completed: false, updatedAt: 0, pinned: false, archived: false } + const onRenameRequest = vi.fn() + render( null} onRenameRequest={onRenameRequest} t={translate} />) + expect(screen.getByText(label)).toBeTruthy() + fireEvent.doubleClick(screen.getByText(label)) + expect(onRenameRequest).toHaveBeenCalledWith(node.id, '') +}) + +it.each([['未命名', t], ['Untitled', tEn]])('labels unnamed search results as %s', (label, translate) => { + const result: SearchResultNode = { id: sid('unnamed-search'), title: '', workspace: 'Project', + running: false, runningSubagentCount: 0, completed: false, archived: false } + const onOpen = vi.fn() + render() + fireEvent.click(screen.getByText(label)) + expect(onOpen).toHaveBeenCalledWith(result.id) +}) diff --git a/packages/client/ui-workspace/tests/shortcuts.client.spec.ts b/packages/client/ui-workspace/tests/shortcuts.client.spec.ts index 0db600df11..5d48fc495d 100644 --- a/packages/client/ui-workspace/tests/shortcuts.client.spec.ts +++ b/packages/client/ui-workspace/tests/shortcuts.client.spec.ts @@ -168,3 +168,15 @@ describe('workspace shortcut ownership', () => { expect(b.loadOlder).not.toHaveBeenCalled() }) }) + +it('opens an empty rename draft for unnamed history instead of its directory label', async () => { + const b = await bench() + const unnamed = row('a', true) + delete unnamed.title + unnamed.displayTitle = 'Default workspace' + b.list.set({ ...b.list.getSnapshot(), byId: { [unnamed.id]: unnamed } }) + const command = b.commands.get('session.rename')!.resolve(context) + expect(command.status).toBe('handled') + if (command.status === 'handled') command.run() + expect(b.controls.state.getSnapshot().renameTarget).toEqual({ sessionId: 'a', currentTitle: '' }) +}) diff --git a/packages/client/ui-workspace/tests/tree.client.spec.ts b/packages/client/ui-workspace/tests/tree.client.spec.ts index f5dc42aa81..9fb549c5bd 100644 --- a/packages/client/ui-workspace/tests/tree.client.spec.ts +++ b/packages/client/ui-workspace/tests/tree.client.spec.ts @@ -16,7 +16,7 @@ import { createWorkspaceViewStore } from '../src/client/stores.ts' const sid = (id: string) => id as SessionId const wid = (id: string) => id as WorkspaceId const summary = (id: string, updatedAt: number, cwd?: string): SessionSummary => ({ - id: sid(id), displayTitle: id, running: false, blank: false, + id: sid(id), title: id, displayTitle: id, running: false, blank: false, updatedAt, ...(cwd === undefined ? {} : { cwd }), retainedBy: {}, }) const list = (...items: SessionSummary[]): SessionListState => ({ @@ -586,9 +586,9 @@ describe('deriveFlat', () => { describe('deriveSearchResults archive filtering', () => { it('archived sessions never match — not by title and not via a backend content hit', () => { const hit = summary('hit', 2) - hit.displayTitle = 'Needle row' + hit.title = hit.displayTitle = 'Needle row' const gone = summary('gone', 1) - gone.displayTitle = 'Needle archived' + gone.title = gone.displayTitle = 'Needle archived' const result = deriveSearchResults( list(hit, gone), [], @@ -604,7 +604,7 @@ describe('deriveSearchResults archive filtering', () => { it('matches archived sessions and flags them while the view shows them', () => { const gone = summary('gone', 1) - gone.displayTitle = 'Needle archived' + gone.title = gone.displayTitle = 'Needle archived' const result = deriveSearchResults( list(gone), [], 'needle', archived('gone'), 'show', noAttention, { items: [], hasMore: false }, 10, ) @@ -613,9 +613,9 @@ describe('deriveSearchResults archive filtering', () => { it('matches only archived sessions under the only filter', () => { const hit = summary('hit', 2) - hit.displayTitle = 'Needle row' + hit.title = hit.displayTitle = 'Needle row' const gone = summary('gone', 1) - gone.displayTitle = 'Needle archived' + gone.title = gone.displayTitle = 'Needle archived' const result = deriveSearchResults( list(hit, gone), [], 'needle', archived('gone'), 'only', noAttention, { items: [], hasMore: false }, 10, ) @@ -626,9 +626,9 @@ describe('deriveSearchResults archive filtering', () => { describe('deriveSearchResults', () => { it('merges local title/Workspace matches before ranked content hits and enriches duplicates', () => { const titleHit = summary('title-hit', 30, '/projects/a') - titleHit.displayTitle = 'Needle title' + titleHit.title = titleHit.displayTitle = 'Needle title' const workspaceHit = summary('workspace-hit', 20, '/projects/b') - workspaceHit.displayTitle = 'Ordinary title' + workspaceHit.title = workspaceHit.displayTitle = 'Ordinary title' const contentHit = summary('content-hit', 10, '/projects/c') const sessions = list(titleHit, workspaceHit, contentHit) const result = deriveSearchResults( @@ -721,7 +721,7 @@ describe('deriveSearchResults', () => { it('uses the supplied cap and preserves either local overflow or backend hasMore', () => { const rows = Array.from({ length: 5 }, (_, index) => { const item = summary(`s-${String(index).padStart(2, '0')}`, index) - item.displayTitle = `Needle ${String(index)}` + item.title = item.displayTitle = `Needle ${String(index)}` return item }) const overflow = deriveSearchResults( @@ -877,3 +877,11 @@ describe('parent folder membership', () => { expect(owningParentFolder(path, parents)).toBe(expected) }) }) + +it('leaves unnamed history titles empty for locale-owned row labels', () => { + const item = summary('unnamed', 1, '/work/Default workspace') + delete item.title + item.displayTitle = 'Default workspace' + const sessions = list(item) + expect(deriveFlat(sessions, [item.id], noRows, noAttention)[0]?.title).toBe('') +}) diff --git a/packages/client/ui-workspace/tests/workspace-browser.client.spec.tsx b/packages/client/ui-workspace/tests/workspace-browser.client.spec.tsx index 20fc53ea6c..f3ee447c1c 100644 --- a/packages/client/ui-workspace/tests/workspace-browser.client.spec.tsx +++ b/packages/client/ui-workspace/tests/workspace-browser.client.spec.tsx @@ -39,7 +39,7 @@ const t: WorkspaceBrowserProps['t'] = makeTranslate(zh, commonZh) const sid = (id: string) => id as SessionId const wid = (id: string) => id as WorkspaceId const summary = (id: string, updatedAt: number, overrides: Partial = {}): SessionSummary => ({ - id: sid(id), displayTitle: id, running: false, blank: false, updatedAt, ...overrides, + id: sid(id), title: overrides.displayTitle ?? id, displayTitle: id, running: false, blank: false, updatedAt, ...overrides, retainedBy: overrides.retainedBy ?? {}, }) const sessionState = ( diff --git a/packages/context/README.i18n.yaml b/packages/context/README.i18n.yaml index cbf433cd29..0aa51cfac3 100644 --- a/packages/context/README.i18n.yaml +++ b/packages/context/README.i18n.yaml @@ -9,8 +9,8 @@ en: 05a169f23a64b7e4 zh: cf57e471b7f13509 /context-request-context-plugins/summary: - en: 4a48e859cbeca00f - zh: 312b8621ba74a3ec + en: c301d0f01494ac06 + zh: 00e51efcde0d1267 /context-request-context-plugins/table-of-contents: en: 1eb35b015a6c423f zh: a8a3dceee2c9e943 diff --git a/packages/context/README.md b/packages/context/README.md index 6e6d38a5f8..3ddd0c5105 100644 --- a/packages/context/README.md +++ b/packages/context/README.md @@ -9,7 +9,7 @@ English | [中文](README.zh.md) ## Summary -The context group provides plugins that add model-visible context to each request without defining any tool: workspace instruction files become guidance, `@file` mentions offer path completion, other sessions can be referenced as bounded snapshots, and the model can see the current time and the agent's tmux location. Only `agent-instructions` (`dsh-base`) and `time-context` (`dsh-web-app`) ship by default; the rest are opt-in. Context is durable: injected instructions and references enter session history as user-role messages, so they persist, replay, and compact like other conversation content. This page maps the group; each package README owns the per-package contract. +The context group provides plugins that add model-visible context to each request without defining any tool: workspace instruction files become guidance, `@file` mentions offer path completion, other sessions can be referenced as bounded snapshots, and the model can see the current time and the agent's tmux location. Only `agent-instructions` (`dsh-base`) ships mounted by default; the rest are opt-in. Context is durable: injected instructions and references enter session history as user-role messages, so they persist, replay, and compact like other conversation content. This page maps the group; each package README owns the per-package contract. ## Table of Contents diff --git a/packages/context/README.zh.md b/packages/context/README.zh.md index 2392f1c644..98b4133036 100644 --- a/packages/context/README.zh.md +++ b/packages/context/README.zh.md @@ -9,7 +9,7 @@ kind: "package-group" ## 概述 -context 组提供不定义任何工具、为每次请求添加模型可见上下文的插件:工作区指令文件成为指引,`@file` 提及提供路径补全,其他会话可以作为有界快照被引用,模型还能看到当前时间与 agent(智能体)的 tmux 位置。默认只包含 `agent-instructions`(`dsh-base`)与 `time-context`(`dsh-web-app`);其余全部需主动启用。上下文是持久的:注入的指令与引用以用户角色消息的形式进入会话历史,因此与其他对话内容一样持久保留、可回放、可压缩。本页概述本组;包级约定由各包 README 负责。 +context 组提供不定义任何工具、为每次请求添加模型可见上下文的插件:工作区指令文件成为指引,`@file` 提及提供路径补全,其他会话可以作为有界快照被引用,模型还能看到当前时间与 agent(智能体)的 tmux 位置。默认只挂载 `agent-instructions`(`dsh-base`);其余全部需主动启用。上下文是持久的:注入的指令与引用以用户角色消息的形式进入会话历史,因此与其他对话内容一样持久保留、可回放、可压缩。本页概述本组;包级约定由各包 README 负责。 ## 目录 diff --git a/packages/context/time-context/README.i18n.yaml b/packages/context/time-context/README.i18n.yaml index 770857eb2c..526a79b8d9 100644 --- a/packages/context/time-context/README.i18n.yaml +++ b/packages/context/time-context/README.i18n.yaml @@ -9,8 +9,8 @@ en: ec051915b3d81cda zh: 3a0ef832e3a5e419 /deepseek-ai-dsh-time-context/summary: - en: e4e308b9fd3fcd56 - zh: 9fb4d40ebe621d6a + en: 8160d6d20a8bc98c + zh: b7010a1edf57079b /deepseek-ai-dsh-time-context/table-of-contents: en: d152484eb41ac6b4 zh: 09388d293f9be9cb diff --git a/packages/context/time-context/README.md b/packages/context/time-context/README.md index 7bfb2b1606..01af143af1 100644 --- a/packages/context/time-context/README.md +++ b/packages/context/time-context/README.md @@ -9,7 +9,7 @@ English | [中文](README.zh.md) ## Summary -`dsh-time-context` gives the model a clock: on eligible steps it appends a durable, source-attributed reading with the current time, the browser zone attached to the open request, and the elapsed time since the preceding model-visible message. It helps the model interpret otherwise-unqualified dates and times in the user's browser zone, and tells it to ask when current-turn browser zones are mixed or missing. The shipped Web bundle mounts it with Schedule. Readings default to a 10-minute minimum interval; `refreshIntervalMs: 0` injects at every eligible step. +`dsh-time-context` gives the model a clock: on eligible steps it appends a durable, source-attributed reading with the current time, the browser zone attached to the open request, and the elapsed time since the preceding model-visible message. It helps the model interpret otherwise-unqualified dates and times in the user's browser zone, and tells it to ask when current-turn browser zones are mixed or missing. The shipped Web composition carries its row disabled; the optional `@deepseek-ai/dsh-experimental-schedule-bundle` mounts it with Schedule from the Plugins page. Readings default to a 10-minute minimum interval; `refreshIntervalMs: 0` injects at every eligible step. ## Table of Contents diff --git a/packages/context/time-context/README.zh.md b/packages/context/time-context/README.zh.md index 84b18bc96b..95afe6acb0 100644 --- a/packages/context/time-context/README.zh.md +++ b/packages/context/time-context/README.zh.md @@ -9,7 +9,7 @@ kind: "package-reference" ## 概述 -`dsh-time-context` 给模型一只时钟:在符合条件的步骤上,它追加一条持久、带来源的读数,包含当前时间、附加到当前开放请求的浏览器时区,以及自前一条模型可见消息以来的经过时长。它帮助模型按用户的浏览器时区解释未明确限定时区的日期与时间;时区来源混杂或缺失时,它告诉模型去询问。发布的 Web bundle 会随 Schedule 一起挂载它。读数默认采用 10 分钟的最小间隔;`refreshIntervalMs: 0` 会在每个符合条件的步骤注入。 +`dsh-time-context` 给模型一只时钟:在符合条件的步骤上,它追加一条持久、带来源的读数,包含当前时间、附加到当前开放请求的浏览器时区,以及自前一条模型可见消息以来的经过时长。它帮助模型按用户的浏览器时区解释未明确限定时区的日期与时间;时区来源混杂或缺失时,它告诉模型去询问。随发行版交付的 Web 组合把该行保持禁用;可选的 `@deepseek-ai/dsh-experimental-schedule-bundle` 在插件管理页会随 Schedule 一起挂载它。读数默认采用 10 分钟的最小间隔;`refreshIntervalMs: 0` 会在每个符合条件的步骤注入。 ## 目录 diff --git a/packages/experimental/README.i18n.yaml b/packages/experimental/README.i18n.yaml index c3c5329e44..a979c5d99b 100644 --- a/packages/experimental/README.i18n.yaml +++ b/packages/experimental/README.i18n.yaml @@ -9,14 +9,14 @@ en: 18bc48bb66b3e516 zh: 51f2340a5c921f36 /packages-experimental/summary: - en: 9341a408afcd5dfa - zh: 07541055b5f41ff8 + en: 882aada84ecc5413 + zh: daedbca3726e6ac2 /packages-experimental/table-of-contents: en: 1eb35b015a6c423f zh: a8a3dceee2c9e943 /packages-experimental/packages: - en: ad10945dac9dbb51 - zh: 245b454406ce99f8 + en: 74635f0629fe1e84 + zh: 6119c5bbc6b807ac /packages-experimental/related-documentation: en: 31282a28dd828c81 zh: ebb1d3cacdc1bd6d diff --git a/packages/experimental/README.md b/packages/experimental/README.md index a49181f7de..32411baad8 100644 --- a/packages/experimental/README.md +++ b/packages/experimental/README.md @@ -9,7 +9,7 @@ English | [中文](README.zh.md) ## Summary -The experimental group contains prototype capabilities whose contracts can change and carry no support promise. All current packages publish under their `@deepseek-ai/dsh-experimental-*` names, including the opt-in Agent Teams composition, Auto review, Cua Driver providers, browser-use backends, cross-realm Inspector, CPython PTC backend, and browser-worker preview libraries. Released products outside this group must not depend on experimental packages. The dsh installation ships the Agent Teams, voice input, and Auto review packages as optional bundles switched on from the Web sidebar's Plugins page ([decision](../../.agents/notes/implemented/architecture/2026-09-21-experimental-capabilities-as-optional-bundles.md)); the other packages are libraries or explicit compositions. +The experimental group contains prototype capabilities whose contracts can change and carry no support promise. All current packages publish under their `@deepseek-ai/dsh-experimental-*` names, including the opt-in Agent Teams composition, Auto review, Cua Driver providers, browser-use backends, cross-realm Inspector, CPython PTC backend, and browser-worker preview libraries. Released products outside this group must not depend on experimental packages. The dsh installation ships the Agent Teams, voice input, Auto review, and Schedule packages as optional bundles switched on from the Web sidebar's Plugins page ([decision](../../.agents/notes/implemented/architecture/2026-09-21-experimental-capabilities-as-optional-bundles.md)); the other packages are libraries or explicit compositions. ## Table of Contents @@ -33,6 +33,7 @@ The experimental group contains prototype capabilities whose contracts can chang | [`agent-team`](agent-team/README.md) | Named teammates with durable messages and a shared task board | `ctx.agentTeams` | | [`client-ui-agent-team`](client-ui-agent-team/README.md) | Team roster, task board, and teammate navigation for Web | — | | [`auto-review`](auto-review/README.md) | Explicit Web layer for same-model review before each native or PTC inner tool call | — | +| [`schedule-bundle`](schedule-bundle/README.md) | Opt-in Schedule and time-context switch over the shipped Web composition | — | | [`ptc-runtime-python`](ptc-runtime-python/README.md) | CPython subprocess backend for the PTC execution seam | `ctx.ptcRuntime` | | [`computer-use-cua-driver-mcp`](computer-use-cua-driver-mcp/README.md) | Use an installed Cua Driver through MCP | `ctx.computerUse` | | [`computer-use-cua-driver-native`](computer-use-cua-driver-native/README.md) | Embed the Cua Driver native npm runtime | `ctx.computerUse` | diff --git a/packages/experimental/README.zh.md b/packages/experimental/README.zh.md index 68b905673f..85c609b043 100644 --- a/packages/experimental/README.zh.md +++ b/packages/experimental/README.zh.md @@ -9,7 +9,7 @@ kind: "package-group" ## 概述 -实验组包含约定可能变更且不提供支持承诺的原型能力。所有当前包都以 `@deepseek-ai/dsh-experimental-*` 名称发布,包括显式启用的 Agent Teams 组合、Auto review、Cua Driver 提供方、浏览器操作后端、跨 realm Inspector、CPython PTC 后端与浏览器 worker 预览库。组外已发布产品不得依赖实验性包。dsh 安装将 Agent Teams、语音输入与 Auto review 包作为可选 bundle 一起发布,可从 Web 侧边栏“插件”页启用([决策](../../.agents/notes/implemented/architecture/2026-09-21-experimental-capabilities-as-optional-bundles.zh.md));其余包是库或显式组合。 +实验组包含约定可能变更且不提供支持承诺的原型能力。所有当前包都以 `@deepseek-ai/dsh-experimental-*` 名称发布,包括显式启用的 Agent Teams 组合、Auto review、Cua Driver 提供方、浏览器操作后端、跨 realm Inspector、CPython PTC 后端与浏览器 worker 预览库。组外已发布产品不得依赖实验性包。dsh 安装将 Agent Teams、语音输入、Auto review 与定时包作为可选 bundle 一起发布,可从 Web 侧边栏“插件”页启用([决策](../../.agents/notes/implemented/architecture/2026-09-21-experimental-capabilities-as-optional-bundles.zh.md));其余包是库或显式组合。 ## 目录 @@ -33,6 +33,7 @@ kind: "package-group" | [`agent-team`](agent-team/README.zh.md) | 具名 teammate,成员之间持久消息与共享任务板 | `ctx.agentTeams` | | [`client-ui-agent-team`](client-ui-agent-team/README.zh.md) | Web Team roster、任务板与 teammate 导航 | — | | [`auto-review`](auto-review/README.zh.md) | 显式 Web 层,在每个原生或 PTC inner 工具调用前使用同一模型审查 | — | +| [`schedule-bundle`](schedule-bundle/README.zh.md) | 在出厂 Web 组合上按需开启定时与时间上下文的开关 | — | | [`ptc-runtime-python`](ptc-runtime-python/README.zh.md) | PTC 执行 seam 的 CPython 子进程后端 | `ctx.ptcRuntime` | | [`computer-use-cua-driver-mcp`](computer-use-cua-driver-mcp/README.zh.md) | 通过 MCP 使用已安装的 Cua Driver | `ctx.computerUse` | | [`computer-use-cua-driver-native`](computer-use-cua-driver-native/README.zh.md) | 嵌入 Cua Driver 原生 npm 运行时 | `ctx.computerUse` | diff --git a/packages/experimental/schedule-bundle/README.i18n.yaml b/packages/experimental/schedule-bundle/README.i18n.yaml new file mode 100644 index 0000000000..bf7fe80742 --- /dev/null +++ b/packages/experimental/schedule-bundle/README.i18n.yaml @@ -0,0 +1,46 @@ +# Bilingual-pair consistency record for README.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 packages/experimental/schedule-bundle/README.md +/: + en: 7504834d27adc717 + zh: 464587026083c661 +/deepseek-ai-dsh-experimental-schedule-bundle: + en: d7b8b6126df4e3c4 + zh: 546d528efd4edbde +/deepseek-ai-dsh-experimental-schedule-bundle/summary: + en: 832752e963df6177 + zh: 30d1d292230a17c1 +/deepseek-ai-dsh-experimental-schedule-bundle/table-of-contents: + en: d152484eb41ac6b4 + zh: 364c80ee00a374ff +/deepseek-ai-dsh-experimental-schedule-bundle/use-this-package: + en: d84cae0d3f3869c6 + zh: c19c74c0671f3127 +/deepseek-ai-dsh-experimental-schedule-bundle/understand-the-implementation: + en: b17882af7c7eaf2c + zh: bfc71d0152557af5 +/deepseek-ai-dsh-experimental-schedule-bundle/further-exploration: + en: 99a29dddf9805540 + zh: 13a6df3ee31abfc1 +/deepseek-ai-dsh-experimental-schedule-bundle/model-experience: + en: 215e7ba838619b7b + zh: a311da3843709f90 +/deepseek-ai-dsh-experimental-schedule-bundle/model-experience/reminder-tools-and-clock-readings: + en: c7fec3a37d945018 + zh: 164346dc63d188dd +/deepseek-ai-dsh-experimental-schedule-bundle/model-experience/reminder-tools-and-clock-readings/what-the-model-sees: + en: fb1906621d02c594 + zh: 394dc933ed63b349 +/deepseek-ai-dsh-experimental-schedule-bundle/model-experience/reminder-tools-and-clock-readings/token-effect: + en: 7c44bfc062010b7b + zh: 15f431db31ad53eb +/deepseek-ai-dsh-experimental-schedule-bundle/model-experience/reminder-tools-and-clock-readings/kv-cache-effect: + en: 0b01d8c40b422f5b + zh: 57fede4db59a62b8 +/deepseek-ai-dsh-experimental-schedule-bundle/known-limitations-and-deferred-work: + en: a4a2674fb3c5a101 + zh: cb4c54ba37730708 +/deepseek-ai-dsh-experimental-schedule-bundle/known-limitations-and-deferred-work/dev-note: + en: 0225ba087c5266f3 + zh: 4e706f1b2b984e63 diff --git a/packages/experimental/schedule-bundle/README.md b/packages/experimental/schedule-bundle/README.md new file mode 100644 index 0000000000..b65f8586a1 --- /dev/null +++ b/packages/experimental/schedule-bundle/README.md @@ -0,0 +1,94 @@ +--- +description: "Enable the shipped Schedule services, reminder catalog, and Automation tasks page from the plugin manager." +kind: "package-bundle" +--- + +# @deepseek-ai/dsh-experimental-schedule-bundle + +English | [中文](README.zh.md) + +## Summary + +This optional bundle switches on the three Schedule rows that `@deepseek-ai/dsh-web-app` inserts with `disabled: true`: `time-context`, `schedule`, and `ui-schedule`. Its patch appends no row and declares no plugin dependency, so enabling it cannot mount a Host service twice. Shipped profiles leave it switched off. + +## Table of Contents + +- [Use this package](#use-this-package) +- [Understand the implementation](#understand-the-implementation) +- [Further Exploration](#further-exploration) +- [Model Experience](#model-experience) +- [Known Limitations and Deferred Work](#known-limitations-and-deferred-work) +- [Dev Note](#dev-note) + +----- + + +## Use this package + +Open Plugins in the Web sidebar and enable Scheduling and time context, marked by a clock icon. A live root Agent then receives `schedule_create`, `schedule_list`, `schedule_update`, and `schedule_delete`, the Session header shows its reminder catalog, the sidebar's Automation tasks entry opens the task-management page and the right Sidebar holds the selected task's detail, and each eligible step appends one clock reading with the current time, the browser zone attached to the open request, and the elapsed time since the preceding model-visible message. Disabling the bundle restores the shipped composition; stored tasks remain on disk. + +----- + + +## Understand the implementation + +
+Maintainer details — click to expand + +`cordis.patch.yml` carries three id-targeted patches, each setting `disabled: false` on a row the Web bundle already inserts. `OPTIONAL_BUNDLES` in `packages/boot/app-boot/src/profile.ts` names this package and `apps/cli` depends on it, so every installation ships it switched off and the plugin manager offers it in the Official group. Selecting it appends the bundle to the tail of the profile's `dsh.profile.bundles` list; bundle layers apply in list order, so the patches reach the rows only because that list places `@deepseek-ai/dsh-web-app` before this package. A hand-ordered list with this package first leaves the rows disabled while the Plugins page still shows the switch as on. No runtime invariant companion is published because this configuration-only package owns no mutable runtime state. + +| File | Role | +|---|---| +| [`cordis.patch.yml`](cordis.patch.yml) | Three `disabled: false` patches over the shipped Web rows | +| [`locale/en.json`](locale/en.json), [`locale/zh.json`](locale/zh.json) | Plugin-manager title and description | +| [`icon.svg`](icon.svg) | Plugin-manager icon | +| [`src/index.ts`](src/index.ts) | Empty module entry; the patch is the runtime content | + +
+ +----- + + +## Further Exploration + +- [Schedule subsystem](../../../docs/subsystems/schedule.md) — durable tasks, occurrence resolution, and delivery. +- [Schedule service](../../schedule/schedule/README.md) — Host task storage, activation, and the record format. +- [Web bundle](../../bundle/web-app/README.md) — the composition that carries these rows. + +----- + + +## Model Experience + +### Reminder tools and clock readings + +#### What the model sees + +A live root Agent gains `schedule_create`, `schedule_list`, `schedule_update`, and `schedule_delete`. `time-context` appends one durable user message per eligible step carrying the sampled instant, the browser zone attached to the open request, and the elapsed time since the preceding model-visible message. + +#### Token effect + +Selecting the bundle adds four Schedule tool schemas to every live root Agent request and one durable clock reading per eligible step; a conversation that never creates a reminder pays both. + +#### KV Cache effect + +The tool schemas change the request prefix once when the bundle mounts; each appended reading is new visible content after that prefix, so existing entries are not invalidated. + +## Known Limitations and Deferred Work + + + +- The switch targets rows `@deepseek-ai/dsh-web-app` inserts. In a profile without that bundle the three patches match no row: the loader reports one `patch: entry not found` warning per row and mounts nothing from this bundle; such a profile mounts no Plugins page either. +- The bundle's detail page has no per-row switches. The Host reports the three overridden ids under `overrides`, and the Web client renders only the rows a bundle inserts, so opening this card shows "This plugin pack contains no components." The switch for the three rows is on the Official-group list row. + +----- + + +### Dev Note + +
+Maintainer details — click to expand + +None. + +
diff --git a/packages/experimental/schedule-bundle/README.zh.md b/packages/experimental/schedule-bundle/README.zh.md new file mode 100644 index 0000000000..0224507932 --- /dev/null +++ b/packages/experimental/schedule-bundle/README.zh.md @@ -0,0 +1,94 @@ +--- +description: "从插件管理页启用出厂的定时服务、提醒目录与自动化任务页面。" +kind: "package-bundle" +--- + +# @deepseek-ai/dsh-experimental-schedule-bundle + +[English](README.md) | 中文 + +## 概述 + +此可选 Bundle 打开 `@deepseek-ai/dsh-web-app` 以 `disabled: true` 插入的三个定时条目:`time-context`、`schedule` 与 `ui-schedule`。它的补丁不追加条目,也不声明插件依赖,因此启用它不会重复挂载同一个 Host 服务。随包配置默认禁用。 + +## 目录 + +- [使用此包](#use-this-package) +- [理解实现](#understand-the-implementation) +- [进一步探索](#further-exploration) +- [模型体验](#model-experience) +- [已知限制与后续工作](#known-limitations-and-deferred-work) +- [开发备注](#dev-note) + +----- + + +## 使用此包 + +打开 Web 侧栏的插件管理页并启用带时钟图标的“定时与时间上下文”。此后,活跃的根智能体获得 `schedule_create`、`schedule_list`、`schedule_update` 与 `schedule_delete`,会话头部显示提醒目录,侧栏的自动化任务入口打开任务管理页面、右侧栏承载所选任务的详情,每个符合条件的步骤追加一条时钟读数,包含当前时间、打开请求所带的浏览器时区,以及距上一条模型可见消息的经过时间。禁用此 Bundle 会恢复随包组合;已存储的任务保留在磁盘上。 + +----- + + +## 理解实现 + +
+维护者信息 — 点击展开 + +`cordis.patch.yml` 包含三个按 id 定位的补丁,各自把 Web Bundle 已插入条目的 `disabled` 设为 `false`。`packages/boot/app-boot/src/profile.ts` 的 `OPTIONAL_BUNDLES` 列出此包,`apps/cli` 依赖它,因此每次安装都随包携带且默认禁用,插件管理页在 Official 分组中提供它。选中后会把该 Bundle 追加到 profile 的 `dsh.profile.bundles` 列表末尾;Bundle 层按列表顺序应用,因此这些补丁能命中条目,只是因为该列表把 `@deepseek-ai/dsh-web-app` 排在此包之前。手写顺序把此包排在最前时,这些条目保持禁用,而插件管理页仍把该开关显示为已启用。此纯配置包不拥有可变的运行时状态,因此不发布不变量伴随模块。 + +| 文件 | 作用 | +|---|---| +| [`cordis.patch.yml`](cordis.patch.yml) | 针对出厂 Web 条目的三个 `disabled: false` 补丁 | +| [`locale/en.json`](locale/en.json)、[`locale/zh.json`](locale/zh.json) | 插件管理页的标题与描述 | +| [`icon.svg`](icon.svg) | 插件管理页图标 | +| [`src/index.ts`](src/index.ts) | 空的模块入口;补丁即运行时内容 | + +
+ +----- + + +## 进一步探索 + +- [定时子系统](../../../docs/subsystems/schedule.zh.md) — 持久任务、发生时刻解析与投递。 +- [定时服务](../../schedule/schedule/README.zh.md) — Host 任务存储、激活与记录格式。 +- [Web Bundle](../../bundle/web-app/README.zh.md) — 携带这些条目的组合。 + +----- + + +## 模型体验 + +### 提醒工具与时钟读数 + +#### 模型看到的内容 + +活跃的根智能体获得 `schedule_create`、`schedule_list`、`schedule_update` 与 `schedule_delete`。`time-context` 在每个符合条件的步骤追加一条持久用户消息,携带采样时刻、打开请求所带的浏览器时区,以及距上一条模型可见消息的经过时间。 + +#### Token 影响 + +启用该 bundle 会给每个活跃根 Agent 请求增加四个定时工具的 schema,并为每个符合条件的步骤增加一条持久时钟读数;即使对话从不创建提醒也会承担这两项开销。 + +#### KV 缓存影响 + +四个工具的 schema 在 bundle 挂载时改变一次请求前缀;每条追加的读数都是该前缀之后的新可见内容,因此不会使已有条目失效。 + +## 已知限制与后续工作 + + + +- 此开关针对 `@deepseek-ai/dsh-web-app` 插入的条目。在没有该 Bundle 的 profile 中,三个补丁匹配不到任何条目:加载器为每个条目报告一条 `patch: entry not found` 警告,且不从该 Bundle 挂载任何东西;这样的 profile 也不挂载插件管理页。 +- 该 Bundle 的详情页没有逐条目开关。Host 把被覆盖的三个 id 报告在 `overrides` 下,而 Web 客户端只渲染 Bundle 插入的条目,因此打开这张卡片会显示“这个插件包不包含任何组件。”。控制这三个条目的开关在 Official 分组列表行上。 + +----- + + +### 开发备注 + +
+维护者信息 — 点击展开 + +无。 + +
diff --git a/packages/experimental/schedule-bundle/cordis.patch.yml b/packages/experimental/schedule-bundle/cordis.patch.yml new file mode 100644 index 0000000000..3ae14343f2 --- /dev/null +++ b/packages/experimental/schedule-bundle/cordis.patch.yml @@ -0,0 +1,12 @@ +# Experimental Schedule switch over the shipped Web composition. `dsh-web-app` +# inserts the three rows with `disabled: true`; this layer flips them on and +# appends nothing, so enabling it cannot mount a Host service twice. + +- id: time-context + disabled: false + +- id: schedule + disabled: false + +- id: ui-schedule + disabled: false diff --git a/packages/experimental/schedule-bundle/icon.svg b/packages/experimental/schedule-bundle/icon.svg new file mode 100644 index 0000000000..4299a86395 --- /dev/null +++ b/packages/experimental/schedule-bundle/icon.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/packages/experimental/schedule-bundle/locale/en.json b/packages/experimental/schedule-bundle/locale/en.json new file mode 100644 index 0000000000..f748924183 --- /dev/null +++ b/packages/experimental/schedule-bundle/locale/en.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Scheduling and time context", + "description": "Enable durable reminders, the Automation tasks page, and per-step clock context." + } +} diff --git a/packages/experimental/schedule-bundle/locale/zh.json b/packages/experimental/schedule-bundle/locale/zh.json new file mode 100644 index 0000000000..47abb9b169 --- /dev/null +++ b/packages/experimental/schedule-bundle/locale/zh.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "定时与时间上下文", + "description": "启用持久提醒、自动化任务页面与逐步时钟上下文。" + } +} diff --git a/packages/experimental/schedule-bundle/package.json b/packages/experimental/schedule-bundle/package.json new file mode 100644 index 0000000000..d64d82eb8f --- /dev/null +++ b/packages/experimental/schedule-bundle/package.json @@ -0,0 +1,48 @@ +{ + "name": "@deepseek-ai/dsh-experimental-schedule-bundle", + "description": "Optional bundle that switches the shipped Schedule, time-context, and reminder-catalog rows on", + "version": "0.1.7-rc.2", + "icon": "./icon.svg", + "publishConfig": { + "access": "public" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/deepseek-ai/deepseek-harness.git", + "directory": "packages/experimental/schedule-bundle" + }, + "type": "module", + "main": "lib/index.js", + "types": "lib/types/index.d.ts", + "exports": { + ".": { + "types": "./lib/types/index.d.ts", + "default": "./lib/index.js" + }, + "./src/*": "./src/*", + "./package.json": "./package.json", + "./cordis.patch.yml": "./cordis.patch.yml", + "./locale/*.json": "./locale/*.json" + }, + "files": [ + "icon.svg", + "locale/*.json", + "lib/index.js", + "cordis.patch.yml", + "lib/types/**/*.d.ts" + ], + "license": "MIT", + "peerDependencies": { + "@deepseek-ai/cordis": "workspace:~" + }, + "devDependencies": { + "@deepseek-ai/cordis": "workspace:~", + "@deepseek-ai/cordis-plugin-include": "workspace:~", + "js-yaml": "^4.2.0" + }, + "dsh": { + "bundle": { + "patch": "./cordis.patch.yml" + } + } +} diff --git a/packages/experimental/schedule-bundle/src/index.ts b/packages/experimental/schedule-bundle/src/index.ts new file mode 100644 index 0000000000..bebbdde0ec --- /dev/null +++ b/packages/experimental/schedule-bundle/src/index.ts @@ -0,0 +1,2 @@ +/** Experimental Schedule composition switch; the patched rows live in cordis.patch.yml. */ +export {} diff --git a/packages/experimental/schedule-bundle/tests/patch.spec.ts b/packages/experimental/schedule-bundle/tests/patch.spec.ts new file mode 100644 index 0000000000..0e61e1c91d --- /dev/null +++ b/packages/experimental/schedule-bundle/tests/patch.spec.ts @@ -0,0 +1,56 @@ +/** The experimental Schedule switch must flip exactly the three rows the Web bundle inserts. */ + +import { readFileSync } from 'node:fs' +import { resolve } from 'node:path' +import { fileURLToPath } from 'node:url' +import { describe, expect, it } from 'vitest' +import * as yaml from 'js-yaml' +import { entryListSchema } from '@deepseek-ai/cordis-plugin-include' + +const root = fileURLToPath(new URL('..', import.meta.url)) + +interface Manifest { + name?: string + icon?: string + private?: boolean + publishConfig?: { access?: string } + exports?: Record + dependencies?: Record + dsh?: { bundle?: { patch?: string } } +} + +interface Patch { + id?: string + name?: string + disabled?: boolean + insert?: unknown[] +} + +describe('experimental Schedule bundle', () => { + const manifest = JSON.parse(readFileSync(resolve(root, 'package.json'), 'utf8')) as Manifest + + it('publishes as an experimental bundle with plugin-manager display metadata', () => { + expect(manifest.name).toBe('@deepseek-ai/dsh-experimental-schedule-bundle') + expect(manifest.private).toBeUndefined() + expect(manifest.publishConfig?.access).toBe('public') + expect(manifest.icon).toBe('./icon.svg') + expect(manifest.dsh?.bundle?.patch).toBe('./cordis.patch.yml') + expect(manifest.exports?.['./locale/*.json']).toBe('./locale/*.json') + expect(manifest.exports?.['./cordis.patch.yml']).toBe('./cordis.patch.yml') + // The patch only overrides rows another layer inserts, so it depends on no plugin package. + expect(manifest.dependencies).toBeUndefined() + }) + + it('flips the three shipped rows on and appends none', () => { + const parsed = yaml.load(readFileSync(resolve(root, './cordis.patch.yml'), 'utf8'), { schema: entryListSchema }) + expect(parsed).toEqual([ + { id: 'time-context', disabled: false }, + { id: 'schedule', disabled: false }, + { id: 'ui-schedule', disabled: false }, + ]) + for (const patch of parsed as Patch[]) { + expect(patch.insert).toBeUndefined() + expect(patch.name).toBeUndefined() + } + }) +}) diff --git a/packages/experimental/schedule-bundle/tsconfig.json b/packages/experimental/schedule-bundle/tsconfig.json new file mode 100644 index 0000000000..f1a449634c --- /dev/null +++ b/packages/experimental/schedule-bundle/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "lib/types" + }, + "include": [ + "src" + ], + "references": [ + { + "path": "../../../vendor/cordis" + } + ] +} diff --git a/packages/experimental/schedule-bundle/tsdown.config.ts b/packages/experimental/schedule-bundle/tsdown.config.ts new file mode 100644 index 0000000000..be29690579 --- /dev/null +++ b/packages/experimental/schedule-bundle/tsdown.config.ts @@ -0,0 +1,3 @@ +import { defineConfig } from 'tsdown' + +export default defineConfig({ entry: ['lib/types/index.js'], outDir: 'lib', format: ['esm'], platform: 'node', target: 'es2024', fixedExtension: false, dts: false, clean: false }) diff --git a/packages/extensions/cordis-client-runner/src/client/slot-catalog.ts b/packages/extensions/cordis-client-runner/src/client/slot-catalog.ts index 1f0ffab903..6659ef98b7 100644 --- a/packages/extensions/cordis-client-runner/src/client/slot-catalog.ts +++ b/packages/extensions/cordis-client-runner/src/client/slot-catalog.ts @@ -3849,7 +3849,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ }, ], ownerProps: [ - '/** Owner share of one Session row action occurrence: the row the action belongs to. */\nexport interface SessionRowOwnerProps {\n /** Session the row shows. */\n sessionId: SessionId\n /** Row display title: persisted title, project basename, or Session id. */\n displayTitle: string\n}', + '/** Owner share of one Session row action occurrence: the row the action belongs to. */\nexport interface SessionRowOwnerProps {\n /** Session the row shows. */\n sessionId: SessionId\n /** Row display title: persisted title, or empty when the Session has none. */\n displayTitle: string\n}', ], ownerPropsReferences: [ 'SessionId', @@ -3904,7 +3904,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ }, ], ownerProps: [ - '/** Owner share of one Session row action occurrence: the row the action belongs to. */\nexport interface SessionRowOwnerProps {\n /** Session the row shows. */\n sessionId: SessionId\n /** Row display title: persisted title, project basename, or Session id. */\n displayTitle: string\n}', + '/** Owner share of one Session row action occurrence: the row the action belongs to. */\nexport interface SessionRowOwnerProps {\n /** Session the row shows. */\n sessionId: SessionId\n /** Row display title: persisted title, or empty when the Session has none. */\n displayTitle: string\n}', ], ownerPropsReferences: [ 'SessionId', diff --git a/packages/extensions/tool-cordis/src/api-catalog.ts b/packages/extensions/tool-cordis/src/api-catalog.ts index 76593e0116..a17df31798 100644 --- a/packages/extensions/tool-cordis/src/api-catalog.ts +++ b/packages/extensions/tool-cordis/src/api-catalog.ts @@ -1547,6 +1547,25 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [ }, ], }, + { + key: 'otel', + summary: 'Shared transport provider.', + description: 'Shared transport provider. Mounting creates no queue, identity, or network connection.', + methods: [ + { + signature: 'createEventReporter(options: EventLogOptions): EventLogReporter', + description: 'Create an independent ordinary-event channel with count-based batching. The injected consumer must drain it during its fiber disposal.', + parameters: [{ name: 'options', description: 'transport, scope, resource, queue, and diagnostic settings selected by the consumer.' }], + returns: 'the caller-owned channel; no state is shared with other channels.', + }, + { + signature: 'createSessionLogReporter(options: SessionLogOptions): SessionLogReporter', + description: 'Create an independent byte-bounded Session-log channel. Authorization and redaction precede reporting; the consumer owns shutdown and its outer deadline.', + parameters: [{ name: 'options', description: 'transport, scope, resource, queue, and diagnostic settings selected by the consumer.' }], + returns: 'the caller-owned channel, preserving complete accepted events within the request byte ceiling.', + }, + ], + }, { key: 'permissionPresets', summary: 'Owns the deployment\'s configured permission presets, the fixed Auto integration hook, and their write path.', @@ -5112,6 +5131,14 @@ export const TYPE_API: readonly TypeApiEntry[] = [ name: 'EpochHeader', declaration: 'export interface EpochHeader {\n config: LlmCallConfig;\n adapterDefaults?: LlmCallConfigAdapterDefaults;\n tools?: ToolSchema[];\n system?: never;\n}', }, + { + name: 'EventLogOptions', + declaration: 'export interface EventLogOptions {\n exporter: SessionLogOptions[\'exporter\'];\n resourceAttributes: Attributes;\n scope: {\n name: string;\n version?: string;\n };\n processor: Omit;\n onFailure: SessionLogOptions[\'onFailure\'];\n}', + }, + { + name: 'EventLogReporter', + declaration: 'export class EventLogReporter {\n constructor(options: EventLogOptions);\n emit(record: OTelEventRecord): void;\n shutdown(): Promise;\n}', + }, { name: 'EveryScheduleRecord', declaration: 'export interface EveryScheduleRecord {\n readonly id: ScheduleId;\n readonly kind: \'every\';\n readonly title: string;\n readonly prompt: string;\n readonly everySeconds: number;\n readonly scheduledAt: string;\n}', @@ -5836,6 +5863,14 @@ export const TYPE_API: readonly TypeApiEntry[] = [ name: 'OptionalSessionSeq', declaration: 'export type OptionalSessionSeq = SessionSeq | null;', }, + { + name: 'OTelEventRecord', + declaration: 'export interface OTelEventRecord {\n eventName: string;\n body: string;\n timestamp: number;\n severityNumber?: SeverityNumber;\n attributes?: Record>;\n}', + }, + { + name: 'OTelEventScalar', + declaration: 'export type OTelEventScalar = string | number | boolean;', + }, { name: 'PackageResult', declaration: 'export interface PackageResult {\n exitCode: number;\n output: string;\n truncated: boolean;\n logPath: string;\n kind?: PluginInstallFailureKind;\n timedOut?: boolean;\n incompatible?: IncompatiblePlugin[];\n}', @@ -5974,11 +6009,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [ }, { name: 'ProductTelemetryRecord', - declaration: 'export interface ProductTelemetryRecord {\n eventName: string;\n body: string;\n timestamp: number;\n severityNumber?: SeverityNumber;\n attributes?: Record>;\n}', - }, - { - name: 'ProductTelemetryScalar', - declaration: 'export type ProductTelemetryScalar = string | number | boolean;', + declaration: 'export type ProductTelemetryRecord = OTelEventRecord;', }, { name: 'ProfilePnpmInvocation', @@ -6604,6 +6635,18 @@ export const TYPE_API: readonly TypeApiEntry[] = [ name: 'SessionLogOffset', declaration: 'export type SessionLogOffset = BrandedNumber<\'SessionLogOffset\'>;', }, + { + name: 'SessionLogOptions', + declaration: 'export interface SessionLogOptions {\n exporter: OTLPExporterNodeConfigBase & {\n url: string;\n };\n processor?: Omit;\n maxRequestBytes?: number;\n scope: {\n name: string;\n version?: string;\n };\n resourceAttributes: Attributes;\n onFailure: (message: string, error?: Error) => void;\n}', + }, + { + name: 'SessionLogRecord', + declaration: 'export interface SessionLogRecord {\n sessionId: SessionId;\n event: Omit & {\n data: unknown;\n };\n attributes?: Attributes;\n severityNumber?: SeverityNumber;\n}', + }, + { + name: 'SessionLogReporter', + declaration: 'export class SessionLogReporter {\n constructor(options: SessionLogOptions);\n reportSessionLog(record: SessionLogRecord): void;\n stopPending(): void;\n shutdown(): Promise;\n}', + }, { name: 'SessionLogSnapshot', declaration: 'export interface SessionLogSnapshot {\n session: SessionHeader;\n inheritedEventCount: SessionLogOffset;\n events: SessionEvent[];\n}', @@ -6810,7 +6853,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [ }, { name: 'SessionTelemetryRecord', - declaration: 'export interface SessionTelemetryRecord {\n channel: \'ledger\' | \'ops\';\n time: number;\n severity: SessionTelemetrySeverity;\n attributes: Record;\n body: unknown;\n}', + declaration: 'export interface SessionTelemetryRecord {\n sourceEvent?: {\n sessionId: SessionId;\n envelope: Omit;\n };\n channel: \'ledger\' | \'ops\';\n time: number;\n severity: SessionTelemetrySeverity;\n attributes: Record;\n body: unknown;\n}', }, { name: 'SessionTelemetrySeverity', diff --git a/packages/host/product-telemetry-otel/README.i18n.yaml b/packages/host/product-telemetry-otel/README.i18n.yaml index 0c330f9c0d..f02cccd867 100644 --- a/packages/host/product-telemetry-otel/README.i18n.yaml +++ b/packages/host/product-telemetry-otel/README.i18n.yaml @@ -18,8 +18,8 @@ en: a62d2f83bd8b4186 zh: f439a20ac0c4858e /deepseek-ai-dsh-host-product-telemetry-otel/understand-the-implementation: - en: 5d2c06a357a1c06a - zh: 9a1a17535889f5e3 + en: d7807324fdeb2b42 + zh: 1bfe1e943e513500 /deepseek-ai-dsh-host-product-telemetry-otel/further-exploration: en: 6bfb5e8a723ff0a9 zh: fffc6b02e2c1a3cb diff --git a/packages/host/product-telemetry-otel/README.md b/packages/host/product-telemetry-otel/README.md index b387f3b3f4..3ea8563ee8 100644 --- a/packages/host/product-telemetry-otel/README.md +++ b/packages/host/product-telemetry-otel/README.md @@ -28,6 +28,7 @@ Send selected product usage events to an OTLP/HTTP collector. Events carry a nam Mount the plugin in a Cordis composition with the application identity; override the collector endpoint when needed. The Desktop composition mounts it when [product analytics](../../client/product-analytics/README.md) is enabled; ordinary Web does not. The standalone example below reads `DSH_APP_VERSION`; Desktop supplies `DSH_PRODUCT_ANALYTICS_APP_VERSION` through its native launcher. Both must name the running release; the schema rejects an absent version. ```yaml +- name: '@deepseek-ai/dsh-otel' - name: '@deepseek-ai/dsh-host-product-telemetry-otel' config: endpoint: https://dsh-otel-collector.deepseeksvc.com/v1/logs @@ -65,7 +66,7 @@ The collector expects a string body and attributes containing strings, numbers,
Implementation internals — click to expand -A private OTel logger feeds `BatchLogRecordProcessor` and the SDK HTTP delegate with its JSON log serializer. The delegate receives explicit headers and an HTTP agent; only shared timeout and compression settings use SDK environment resolution. The direct `@opentelemetry/core` dependency matches `sdk-logs` at 2.9.0 so exporter result enums share one TypeScript identity. The SDK owns queueing, transient-error retries, and compression; plugin disposal drains pending records with a bounded wait. Export completion is observed separately because SDK shutdown can resolve after a rejected export. No global OTel provider is installed. +The adapter injects `otel` and creates an independent ordinary-event channel through `ctx.otel.createEventReporter()`. The [shared OTel plugin](../../telemetry/otel/README.md) owns SDK transport and batching; this adapter owns analytics configuration and its bounded shutdown wait. No global OTel provider is installed. [`src/index.ts`](src/index.ts) owns configuration and submission. No runtime invariant companion is published: delivery has no independent local acknowledgement to compare with the SDK's queue. diff --git a/packages/host/product-telemetry-otel/README.zh.md b/packages/host/product-telemetry-otel/README.zh.md index dea0d39e24..e2674730da 100644 --- a/packages/host/product-telemetry-otel/README.zh.md +++ b/packages/host/product-telemetry-otel/README.zh.md @@ -28,6 +28,7 @@ kind: "package-reference" 在 Cordis 组合中挂载插件并提供应用标识;需要时可覆盖接收地址。桌面组合在开启[产品埋点](../../client/product-analytics/README.zh.md)时挂载本插件;普通 Web 不挂载。下方独立示例读取 `DSH_APP_VERSION`;桌面端由原生启动器提供 `DSH_PRODUCT_ANALYTICS_APP_VERSION`。两者均须为运行中的发布版本;缺少版本时 schema 会拒绝配置。 ```yaml +- name: '@deepseek-ai/dsh-otel' - name: '@deepseek-ai/dsh-host-product-telemetry-otel' config: endpoint: https://dsh-otel-collector.deepseeksvc.com/v1/logs @@ -65,7 +66,7 @@ kind: "package-reference"
实现细节——点击展开 -私有 OTel logger 将记录交给 `BatchLogRecordProcessor`,再由 SDK HTTP delegate 和 JSON 日志序列化器发送。delegate 使用显式请求头和 HTTP agent,只有共享的超时与压缩配置使用 SDK 环境变量解析。直接依赖的 `@opentelemetry/core` 与 `sdk-logs` 对齐为 2.9.0,使导出结果枚举共享同一个 TypeScript 类型身份。SDK 负责队列、临时错误重试与压缩;插件卸载时在限定时间内发送剩余记录。发送完成结果单独观测,因为 SDK 在发送被拒绝后仍可能正常完成退出。不安装全局 OTel provider。 +适配器注入 `otel`,通过 `ctx.otel.createEventReporter()` 创建独立的普通事件通道。[共享 OTel 插件](../../telemetry/otel/README.zh.md) 负责 SDK 传输和聚合;本适配器负责埋点配置及有期限的关闭等待。不安装全局 OTel provider。 [`src/index.ts`](src/index.ts) 负责配置与提交。不发布运行时不变量伴随模块:本地没有独立的送达确认可与 SDK 队列比较。 diff --git a/packages/host/product-telemetry-otel/package.json b/packages/host/product-telemetry-otel/package.json index 29c291020d..6a5ba372cb 100644 --- a/packages/host/product-telemetry-otel/package.json +++ b/packages/host/product-telemetry-otel/package.json @@ -28,21 +28,19 @@ "license": "MIT", "dependencies": { "@deepseek-ai/schemastery": "workspace:~", - "@opentelemetry/api": "^1.9.1", "@opentelemetry/api-logs": "^0.220.0", - "@opentelemetry/core": "2.9.0", "@opentelemetry/otlp-exporter-base": "^0.220.0", - "@opentelemetry/otlp-transformer": "^0.220.0", - "@opentelemetry/resources": "^2.9.0", "@opentelemetry/sdk-logs": "^0.220.0" }, "peerDependencies": { - "@deepseek-ai/cordis": "workspace:~" + "@deepseek-ai/cordis": "workspace:~", + "@deepseek-ai/dsh-otel": "workspace:*" }, "devDependencies": { "@deepseek-ai/cordis": "workspace:~", "@deepseek-ai/cordis-plugin-loader": "workspace:~", "@deepseek-ai/dsh-app-boot": "workspace:*", - "@deepseek-ai/dsh-loader-smoke": "workspace:*" + "@deepseek-ai/dsh-loader-smoke": "workspace:*", + "@deepseek-ai/dsh-otel": "workspace:*" } } diff --git a/packages/host/product-telemetry-otel/src/index.ts b/packages/host/product-telemetry-otel/src/index.ts index eef6ec5dce..ec2eda239e 100644 --- a/packages/host/product-telemetry-otel/src/index.ts +++ b/packages/host/product-telemetry-otel/src/index.ts @@ -1,37 +1,18 @@ -/** Explicit product usage events over OTLP/HTTP; no automatic collection or Session access. */ +/** Product analytics policy adapter for the shared Cordis OTel service. */ import { Context, Service } from '@deepseek-ai/cordis' import z from '@deepseek-ai/schemastery' -import { SeverityNumber, type Logger } from '@opentelemetry/api-logs' import { validateHeaderValue } from 'node:http' -import { JsonLogsSerializer } from '@opentelemetry/otlp-transformer' -import { createOtlpHttpExportDelegate, getSharedConfigurationFromEnvironment, httpAgentFactoryFromOptions } from '@opentelemetry/otlp-exporter-base/node-http' -import { CompressionAlgorithm, getSharedConfigurationDefaults, mergeOtlpSharedConfigurationWithDefaults, OTLPExporterBase } from '@opentelemetry/otlp-exporter-base' -import { ExportResultCode } from '@opentelemetry/core' -import { resourceFromAttributes } from '@opentelemetry/resources' -import { BatchLogRecordProcessor, LoggerProvider } from '@opentelemetry/sdk-logs' +import { CompressionAlgorithm } from '@opentelemetry/otlp-exporter-base' +import type { EventLogReporter, OTelEventRecord, OTelEventScalar } from '@deepseek-ai/dsh-otel' declare module '@deepseek-ai/cordis' { - interface Context { - productTelemetry: ProductTelemetry - } + interface Context { productTelemetry: ProductTelemetry } } -/** Scalar values accepted by the collector's Arrow attributes map. */ -export type ProductTelemetryScalar = string | number | boolean - -/** Explicitly selected analytics fields; object values may contain scalars only. */ -export interface ProductTelemetryRecord { - /** Product/DA-owned event name. */ - eventName: string - /** Human-readable summary; never a prompt, response, credential, or file contents. */ - body: string - /** Event occurrence time in Unix milliseconds. Observation time is assigned on enqueue. */ - timestamp: number - /** OTel severity; omitted values use INFO. */ - severityNumber?: SeverityNumber - /** Business fields selected by the caller; no automatic device or account identity. */ - attributes?: Record> -} +/** Caller-selected ordinary analytics record. */ +export type ProductTelemetryRecord = OTelEventRecord +/** Scalar values accepted in product attributes. */ +export type ProductTelemetryScalar = OTelEventScalar /** Collector routing, application identity, and bounded in-memory batch settings. */ export interface Config { @@ -78,8 +59,9 @@ export const Config: z, Config> = z.object({ /** Host analytics sender. Mounting alone sends nothing; the owning fiber drains it on unload. */ export default class ProductTelemetry extends Service { + static inject = ['otel'] static Config = Config - private readonly logger: Logger + private readonly reporter: EventLogReporter constructor(ctx: Context, config: Config) { let endpoint: URL @@ -100,44 +82,20 @@ export default class ProductTelemetry extends Service { throw new Error('product-telemetry-otel: maxExportBatchSize must not exceed maxQueueSize') } super(ctx, 'productTelemetry') - const shared = mergeOtlpSharedConfigurationWithDefaults({ - timeoutMillis: config.timeoutMillis, - ...(config.compression === undefined ? {} : { - compression: config.compression === 'gzip' ? CompressionAlgorithm.GZIP : CompressionAlgorithm.NONE, - }), - }, getSharedConfigurationFromEnvironment('LOGS'), getSharedConfigurationDefaults()) - const transport = { - ...shared, - url: config.endpoint, - // This collector must not inherit another endpoint's headers or TLS client identity. - headers: () => Promise.resolve({ 'Content-Type': 'application/json', 'x-channel': config.channel }), - agentFactory: httpAgentFactoryFromOptions({ keepAlive: true }), - } - const exporter = new OTLPExporterBase(createOtlpHttpExportDelegate(transport, JsonLogsSerializer)) - const provider = new LoggerProvider({ - resource: resourceFromAttributes({ - 'service.name': config.serviceName, - 'service.version': config.serviceVersion, - }), - processors: [new BatchLogRecordProcessor({ - maxExportBatchSize: config.maxExportBatchSize, - maxQueueSize: config.maxQueueSize, - scheduledDelayMillis: config.scheduledDelayMillis, - exportTimeoutMillis: config.exportTimeoutMillis, - exporter: { - export: (records, callback) => { - exporter.export(records, (result) => { - // SDK flush/shutdown can resolve after export failure; observe the actual completion. - if (result.code !== ExportResultCode.SUCCESS) ctx.logger.warn('Product telemetry export failed', result.error) - callback(result) - }) - }, - forceFlush: () => exporter.forceFlush(), - shutdown: () => exporter.shutdown(), - }, - })], + const reporter = ctx.otel.createEventReporter({ + exporter: { + url: config.endpoint, headers: { 'x-channel': config.channel }, timeoutMillis: config.timeoutMillis, + ...(config.compression === undefined ? {} : { compression: config.compression === 'gzip' ? CompressionAlgorithm.GZIP : CompressionAlgorithm.NONE }), + }, + resourceAttributes: { 'service.name': config.serviceName, 'service.version': config.serviceVersion }, + scope: { name: '@deepseek-ai/dsh-host-product-telemetry-otel' }, + processor: { + maxExportBatchSize: config.maxExportBatchSize, maxQueueSize: config.maxQueueSize, + scheduledDelayMillis: config.scheduledDelayMillis, exportTimeoutMillis: config.exportTimeoutMillis, + }, + onFailure: (message, error) => { ctx.logger.warn(message, error) }, }) - this.logger = provider.getLogger('@deepseek-ai/dsh-host-product-telemetry-otel') + this.reporter = reporter ctx.effect(() => async () => { let timer!: ReturnType const deadline = new Promise((resolve) => { @@ -147,7 +105,7 @@ export default class ProductTelemetry extends Service { }, config.shutdownTimeoutMillis) }) try { - await Promise.race([provider.shutdown(), deadline]) + await Promise.race([reporter.shutdown(), deadline]) } finally { clearTimeout(timer) } @@ -160,12 +118,6 @@ export default class ProductTelemetry extends Service { * @param record - caller-owned event containing only approved analytics fields. */ emit(record: ProductTelemetryRecord): void { - const severityNumber = record.severityNumber ?? SeverityNumber.INFO - this.logger.emit({ - ...record, - observedTimestamp: Date.now(), - severityNumber, - severityText: SeverityNumber[severityNumber], - }) + this.reporter.emit(record) } } diff --git a/packages/host/product-telemetry-otel/tests/telemetry.spec.ts b/packages/host/product-telemetry-otel/tests/telemetry.spec.ts index 22f7df5c30..eadaf49f12 100644 --- a/packages/host/product-telemetry-otel/tests/telemetry.spec.ts +++ b/packages/host/product-telemetry-otel/tests/telemetry.spec.ts @@ -1,3 +1,4 @@ +import OTel from '@deepseek-ai/dsh-otel' import { createServer, type IncomingHttpHeaders } from 'node:http' import { once } from 'node:events' import { gunzipSync } from 'node:zlib' @@ -58,7 +59,8 @@ function config(endpoint: string, overrides: Partial = {}): Config { function context() { const ctx = new Context() - cleanup.push(() => ctx.fiber.dispose()) + const ready = ctx.plugin(OTel) + cleanup.push(async () => { await ready; await ctx.fiber.dispose() }) return ctx } @@ -201,3 +203,16 @@ describe('explicit product telemetry', () => { await vi.advanceTimersByTimeAsync(0) }) }) + +it('drains the dependent product channel when the shared OTel plugin unloads', async () => { + const target = await collector() + const ctx = new Context() + cleanup.push(() => ctx.fiber.dispose()) + const service = await ctx.plugin(OTel) + await ctx.plugin(ProductTelemetry, config(target.endpoint)) + ctx.productTelemetry.emit(event) + await service.dispose() + expect(ctx.get('otel')).toBeUndefined() + expect(ctx.get('productTelemetry')).toBeUndefined() + expect(target.captures).toHaveLength(1) +}) diff --git a/packages/host/product-telemetry-otel/tsconfig.json b/packages/host/product-telemetry-otel/tsconfig.json index f0063ae28d..2c542e3a1f 100644 --- a/packages/host/product-telemetry-otel/tsconfig.json +++ b/packages/host/product-telemetry-otel/tsconfig.json @@ -16,6 +16,9 @@ }, { "path": "../../../vendor/schemastery" + }, + { + "path": "../../telemetry/otel" } ] } diff --git a/packages/llm/llm-deepseek/README.i18n.yaml b/packages/llm/llm-deepseek/README.i18n.yaml index 9bd72dd10a..04a7e52140 100644 --- a/packages/llm/llm-deepseek/README.i18n.yaml +++ b/packages/llm/llm-deepseek/README.i18n.yaml @@ -39,8 +39,8 @@ en: f863390198cc177c zh: 8856c62ba3fb36ca /deepseek-ai-dsh-llm-deepseek/use-this-package/failures-and-recovery: - en: d31bd24c8c419f88 - zh: 91205cb3cd754f39 + en: 7a09ab81593cd9c1 + zh: 919dc0eb6347e9fd /deepseek-ai-dsh-llm-deepseek/understand-the-implementation: en: 1598ac8a8bb9b6bf zh: fdd089c5ae7722cd diff --git a/packages/llm/llm-deepseek/README.md b/packages/llm/llm-deepseek/README.md index 3dc3efacb3..651258fcbb 100644 --- a/packages/llm/llm-deepseek/README.md +++ b/packages/llm/llm-deepseek/README.md @@ -115,7 +115,7 @@ Configuration accepts Messages only and has no `protocol` field. If resolution r Successful Files responses must contain valid JSON. JSON decoding failures from upload, list, retrieve, and delete throw `INVALID_RESPONSE` with the operation and HTTP status in the message, the status in `LlmError.failure`, and the original parser error as `cause`. Body-read transport and cancellation errors retain their identity. -Non-2xx responses fail with stable codes: `AUTH` (401/403), `QUOTA`, `RATE_LIMIT`, `CONTEXT_WINDOW_EXCEEDED`, `INVALID_REQUEST`, `SERVER`, and `HTTP_` otherwise; pre-response transport failures throw `TRANSPORT`, caller aborts throw `ABORTED`, and stream-idle expiry throws `TIMEOUT`. Request-extension preparation, field collision, or post-2xx acceptance fails with `REQUEST_EXTENSION`. A normalized-image rejection names every plausible attachment and its durable position when the provider does not identify a file id. Stale-file rejection invalidates the named mappings (or every mapping used by the attempt) and permits one replacement model request. Protocol violations throw `STREAM_CLOSED` or `MALFORMED_RESPONSE`, and a terminal `stop` with no content blocks becomes `EMPTY_RESPONSE`, which the default retry policy retries. A request on the official route without an API key fails with `MISSING_CREDENTIAL`, and a malformed credential fails with `INVALID_CREDENTIAL` naming the reference to fix — never any part of the key. +Non-2xx responses fail with stable codes: `AUTH` (401/403), `QUOTA`, `RATE_LIMIT`, `CONTEXT_WINDOW_EXCEEDED`, `INVALID_REQUEST`, `SERVER`, and `HTTP_` otherwise; pre-response transport failures throw `TRANSPORT`, caller aborts throw `ABORTED`, and stream-idle expiry throws `TIMEOUT`. Request-extension preparation, field collision, or post-2xx acceptance fails with `REQUEST_EXTENSION`. A normalized-image rejection names every plausible attachment and its durable position when the provider does not identify a file id. Stale-file rejection removes every named mapping (or every mapping used by the attempt) in one upload-index update and permits one replacement model request. Protocol violations throw `STREAM_CLOSED` or `MALFORMED_RESPONSE`, and a terminal `stop` with no content blocks becomes `EMPTY_RESPONSE`, which the default retry policy retries. A request on the official route without an API key fails with `MISSING_CREDENTIAL`, and a malformed credential fails with `INVALID_CREDENTIAL` naming the reference to fix — never any part of the key. Provider plugins own catalog availability; only the account route requires a stored grant for discovery. Their catalogs are configured independently; the transport supplies shared default model metadata and capability resolution. diff --git a/packages/llm/llm-deepseek/README.zh.md b/packages/llm/llm-deepseek/README.zh.md index 257765c0fd..0c4694bd8f 100644 --- a/packages/llm/llm-deepseek/README.zh.md +++ b/packages/llm/llm-deepseek/README.zh.md @@ -115,7 +115,7 @@ Files 模式通过 `maxRequestFilesBytes` 与 `maxImagesPerRequest` 限制保留 成功的 Files 响应必须包含有效 JSON。上传、列举、获取和删除操作的 JSON 解码失败抛出 `INVALID_RESPONSE`,消息包含操作名称与 HTTP 状态,`LlmError.failure` 保留该状态,`cause` 保留原始解析错误。读取响应体时的传输和取消错误保留其原有身份。 -非 2xx 响应以稳定 code 失败:`AUTH`(401/403)、`QUOTA`、`RATE_LIMIT`、`CONTEXT_WINDOW_EXCEEDED`、`INVALID_REQUEST`、`SERVER` 以及其他情况的 `HTTP_`;响应前传输失败抛出 `TRANSPORT`,调用方中止抛出 `ABORTED`,流空闲超时抛出 `TIMEOUT`。请求扩展准备、字段冲突或 2xx 后接受失败使用 `REQUEST_EXTENSION`。当提供方未指出 file id 时,规范化图片拒绝会列出所有可能附件及其持久位置。陈旧文件拒绝会使点名映射(或该次尝试使用的全部映射)失效,并允许一次替换模型请求。协议违规抛出 `STREAM_CLOSED` 或 `MALFORMED_RESPONSE`;不带内容块的终止 `stop` 变成 `EMPTY_RESPONSE`,默认重试策略会重试它。官方路由缺少 API Key 的请求以 `MISSING_CREDENTIAL` 失败;格式错误的凭据以 `INVALID_CREDENTIAL` 失败,并点名需要修复的引用——绝不包含密钥的任何部分。 +非 2xx 响应以稳定 code 失败:`AUTH`(401/403)、`QUOTA`、`RATE_LIMIT`、`CONTEXT_WINDOW_EXCEEDED`、`INVALID_REQUEST`、`SERVER` 以及其他情况的 `HTTP_`;响应前传输失败抛出 `TRANSPORT`,调用方中止抛出 `ABORTED`,流空闲超时抛出 `TIMEOUT`。请求扩展准备、字段冲突或 2xx 后接受失败使用 `REQUEST_EXTENSION`。当提供方未指出 file id 时,规范化图片拒绝会列出所有可能附件及其持久位置。陈旧文件拒绝会在一次上传索引更新中删除全部点名映射(或该次尝试使用的全部映射),并允许一次替换模型请求。协议违规抛出 `STREAM_CLOSED` 或 `MALFORMED_RESPONSE`;不带内容块的终止 `stop` 变成 `EMPTY_RESPONSE`,默认重试策略会重试它。官方路由缺少 API Key 的请求以 `MISSING_CREDENTIAL` 失败;格式错误的凭据以 `INVALID_CREDENTIAL` 失败,并点名需要修复的引用——绝不包含密钥的任何部分。 提供方插件负责目录可用性;仅账号路由要求存有凭据才能发现模型。两者的目录独立配置;传输层提供共享的默认模型元数据和能力解析。 diff --git a/packages/llm/llm-deepseek/src/file-store.ts b/packages/llm/llm-deepseek/src/file-store.ts index cebff0809b..da6f87654a 100644 --- a/packages/llm/llm-deepseek/src/file-store.ts +++ b/packages/llm/llm-deepseek/src/file-store.ts @@ -244,21 +244,15 @@ export class DeepSeekFileStore { } /** - * Invalidate one exact local mapping after a model request rejects its remote id. - * @param version - request-image version whose remote generation failed. - * @param fileId - exact rejected file id. + * Invalidate exact local mappings in one index update after a model request rejects their remote ids. + * @param generations - request-image variants with the exact file id the request used for each. * @param connection - endpoint and API-key snapshot. */ async invalidate( - version: RequestImageAttachment, - fileId: DeepSeekFileId, + generations: readonly Pick[], connection: DeepSeekFileConnection, ): Promise { - await this.index.remove( - fileScope(connection), - version.variantId, - fileId, - ) + await this.index.remove(fileScope(connection), generations) } /** @@ -284,7 +278,7 @@ export class DeepSeekFileStore { ) if (record === undefined) return false await this.client(connection).delete(record.fileId, signal) - await this.index.remove(scope, version.variantId, record.fileId) + await this.index.remove(scope, [{ variantId: version.variantId, fileId: record.fileId }]) return true } diff --git a/packages/llm/llm-deepseek/src/request-files.ts b/packages/llm/llm-deepseek/src/request-files.ts index 4b391420ab..85e1460903 100644 --- a/packages/llm/llm-deepseek/src/request-files.ts +++ b/packages/llm/llm-deepseek/src/request-files.ts @@ -128,13 +128,16 @@ export class RequestFiles { } /** - * Invalidate rejected mappings; only the first stale-id response permits another request. + * Invalidate every rejected mapping in one index update; only the first stale-id response permits another request. * @param detail - provider error fields used for stale-id classification. * @returns whether the caller should serialize and dispatch again. */ async retry(detail: string): Promise { if (this.used.length === 0 || !providerRejectedFileId(detail)) return false - await Promise.all(staleMappings(this.used, detail).map(file => this.files.invalidate(file.version, file.fileId, this.connection))) + await this.files.invalidate( + staleMappings(this.used, detail).map(file => ({ variantId: file.version.variantId, fileId: file.fileId })), + this.connection, + ) if (this.retried) return false this.retried = true return true diff --git a/packages/llm/llm-deepseek/src/upload-index.ts b/packages/llm/llm-deepseek/src/upload-index.ts index 1467e7d93a..27a8ea89e7 100644 --- a/packages/llm/llm-deepseek/src/upload-index.ts +++ b/packages/llm/llm-deepseek/src/upload-index.ts @@ -190,21 +190,20 @@ export class DeepSeekUploadIndex { } /** - * Remove one exact mapping without deleting a concurrently installed successor. + * Remove exact mappings in one locked rewrite without deleting concurrently installed successors. * @param scope - endpoint/API-key namespace. - * @param variantId - complete request-image transformation identity. - * @param fileId - exact remote generation being invalidated. + * @param generations - exact remote generations being invalidated; pairs absent from the index are ignored. */ async remove( scope: DeepSeekFileScopeType, - variantId: ImageVariantIdType, - fileId: DeepSeekFileIdType, + generations: readonly Pick[], ): Promise { + const invalidated = new Set(generations.map(generation => `${generation.variantId}\0${generation.fileId}`)) await mkdir(dirname(this.path), { recursive: true, mode: 0o700 }) await withFileLock(this.path, async () => { const index = await this.load() const records = index.records.filter(record => !( - record.scope === scope && record.variantId === variantId && record.fileId === fileId + record.scope === scope && invalidated.has(`${record.variantId}\0${record.fileId}`) )) if (records.length !== index.records.length) await this.save({ formatVersion: 3, records }) }) diff --git a/packages/llm/llm-deepseek/tests/file-store.spec.ts b/packages/llm/llm-deepseek/tests/file-store.spec.ts index b821304c02..a8cb1458a4 100644 --- a/packages/llm/llm-deepseek/tests/file-store.spec.ts +++ b/packages/llm/llm-deepseek/tests/file-store.spec.ts @@ -98,10 +98,10 @@ describe('DeepSeekFileStore', () => { expect((await store.ensureUploaded(VERSION, { ...CONNECTION, baseURL: `${CONNECTION.baseURL}/v1/` }, POLICY)).record).toEqual(first.record) const reopened = new DeepSeekFileStore({ index, fetch: remote.fetchImpl, now: () => now }) expect((await reopened.ensureUploaded(VERSION, CONNECTION, POLICY)).record).toEqual(first.record) - await reopened.invalidate(VERSION, other.record.fileId, CONNECTION) + await reopened.invalidate([{ variantId: VERSION.variantId, fileId: other.record.fileId }], CONNECTION) expect((await reopened.ensureUploaded(VERSION, CONNECTION, POLICY)).record).toEqual(first.record) expect(remote.uploads()).toBe(2) - await reopened.invalidate(VERSION, first.record.fileId, CONNECTION) + await reopened.invalidate([{ variantId: VERSION.variantId, fileId: first.record.fileId }], CONNECTION) const replacement = await reopened.ensureUploaded(VERSION, CONNECTION, POLICY) expect(replacement.record.fileId).not.toBe(first.record.fileId) expect((await reopened.ensureUploaded(VERSION, alternate, POLICY)).record).toEqual(other.record) diff --git a/packages/llm/llm-deepseek/tests/files.spec.ts b/packages/llm/llm-deepseek/tests/files.spec.ts index 7ec3129e1c..bb44326318 100644 --- a/packages/llm/llm-deepseek/tests/files.spec.ts +++ b/packages/llm/llm-deepseek/tests/files.spec.ts @@ -73,7 +73,7 @@ describe('Messages Files requests', () => { vi.stubGlobal('fetch', fetchImpl) const h = harness() await chunks(h.adapter.stream(request([ref, second]))) - expect(h.invalidate).toHaveBeenCalledExactlyOnceWith(expect.objectContaining({ attachment: ref }), 'file-a', expect.objectContaining({})) + expect(h.invalidate).toHaveBeenCalledExactlyOnceWith([{ variantId: version(ref).variantId, fileId: 'file-a' }], expect.objectContaining({})) expect(fetchImpl).toHaveBeenCalledTimes(2) expect(h.prepare).toHaveBeenCalledTimes(2) expect(h.ensureUploaded).toHaveBeenCalledTimes(4) @@ -85,7 +85,7 @@ describe('Messages Files requests', () => { const h = harness() await expect(chunks(h.adapter.stream(request([ref, second])))).rejects.toMatchObject({ code: 'INVALID_REQUEST' }) expect(fetchImpl).toHaveBeenCalledTimes(2) - expect(h.invalidate).toHaveBeenCalledTimes(4) + expect(h.invalidate.mock.calls.map(call => call[0].length)).toEqual([2, 2]) expect(fetchImpl.mock.calls.every(call => !body(call[1]).includes('base64'))).toBe(true) }) diff --git a/packages/llm/llm-deepseek/tests/runtime.spec.ts b/packages/llm/llm-deepseek/tests/runtime.spec.ts index 4c505e1fe2..e051f6f909 100644 --- a/packages/llm/llm-deepseek/tests/runtime.spec.ts +++ b/packages/llm/llm-deepseek/tests/runtime.spec.ts @@ -1017,6 +1017,57 @@ describe('DeepSeekAdapter against a mock server', () => { ]) }) + it('recovers in one replacement request when the provider lists dozens of missing file ids', async () => { + const refs = Array.from({ length: 40 }, (_, index): ImageAttachmentRef => ({ + ...imageRef, + attachmentId: AttachmentId(`sha256:${(index + 1).toString(16).padStart(64, '0')}`), + })) + const missing = Array.from({ length: 30 }, (_, index) => `file-api-${index + 1}`) + const server = await mockServer([ + { + kind: 'http-error', + status: 400, + body: JSON.stringify({ + error: { + type: 'invalid_request_error', + message: 'messages.0.content.1.tool_result: the following file_ids do not exist or are not created under your account: ' + + `${missing.join(', ')} (request_id: 00000000-0000-4000-8000-000000000002)`, + }, + }), + }, + { kind: 'sse', events: textEvents }, + ]) + const attachments = attachmentStoreOf(ref => Promise.resolve({ + ...requestImage(ref), + variantId: ImageVariantId(String(ref.attachmentId)), + })).store + const adapter = adapterOf({ + baseURL: server.url, + models: [{ id: 'deepseek-v4-flash-vision-exp', inputModalities: ['text', 'image'] }], + }, attachments) + + await drain(adapter.stream({ + provider: 'deepseek-official', + model: 'deepseek-v4-flash-vision-exp', + messages: [createUserMessage({ + content: refs.map(ref => ({ type: 'image' as const, attachment: ref })), + source: { kind: 'model', provider: 'deepseek-official', model: 'deepseek-v4-flash' }, + })], + })) + + expect(server.requests).toHaveLength(2) + expect(server.fileRequests.filter(request => request.method === 'POST')).toHaveLength(70) + const attempts = server.requests as Array<{ messages: Array<{ content: Array<{ type: string; source?: { file_id?: string } }> }> }> + const fileIds = (attempt: number) => attempts[attempt]?.messages[0]?.content + .filter(block => block.type === 'image') + .map(block => block.source?.file_id) + expect(fileIds(0)).toEqual(refs.map((_, index) => `file-api-${index + 1}`)) + expect(fileIds(1)).toEqual([ + ...missing.map((_, index) => `file-api-${41 + index}`), + ...refs.slice(30).map((_, index) => `file-api-${31 + index}`), + ]) + }) + it('invalidates every used mapping when a stale-file response does not identify one file id', async () => { const secondRef: ImageAttachmentRef = { ...imageRef, diff --git a/packages/llm/llm-deepseek/tests/upload-index.spec.ts b/packages/llm/llm-deepseek/tests/upload-index.spec.ts index 989a921e69..1a57409ed7 100644 --- a/packages/llm/llm-deepseek/tests/upload-index.spec.ts +++ b/packages/llm/llm-deepseek/tests/upload-index.spec.ts @@ -56,12 +56,36 @@ describe('DeepSeekUploadIndex', () => { await index.commit(first, 1, 1) await expect(index.commit(duplicate, 2, 1)).resolves.toEqual({ record: first, accepted: false }) - await index.remove(scope, VARIANT, duplicate.fileId) + await index.remove(scope, [{ variantId: VARIANT, fileId: duplicate.fileId }]) await expect(index.get(scope, VARIANT, 2, 1)).resolves.toEqual(first) - await index.remove(scope, VARIANT, first.fileId) + await index.remove(scope, [{ variantId: VARIANT, fileId: first.fileId }]) await expect(index.get(scope, VARIANT, 2, 1)).resolves.toBeUndefined() }) + it('removes several exact generations in one update and keeps unlisted mappings', async () => { + const dir = await mkdtemp(join(tmpdir(), 'dsh-upload-index-')) + roots.push(dir) + const index = new DeepSeekUploadIndex(join(dir, 'index.json')) + const scope = deepSeekFileScope('https://api.deepseek.com', 'key') + const record = (digest: string) => ({ + scope, attachmentId: ATTACHMENT, variantId: ImageVariantId(`sha256:${digest.repeat(64)}`), + fileId: DeepSeekFileId(`file-api-${digest}`), bytes: 3, createdAt: 1, expiresAt: 10_000, + }) + const first = record('b') + const second = record('c') + const third = record('d') + for (const entry of [first, second, third]) await index.commit(entry, 1, 1) + + await index.remove(scope, [ + { variantId: first.variantId, fileId: first.fileId }, + { variantId: second.variantId, fileId: second.fileId }, + { variantId: third.variantId, fileId: DeepSeekFileId('file-api-superseded') }, + ]) + await expect(index.get(scope, first.variantId, 2, 1)).resolves.toBeUndefined() + await expect(index.get(scope, second.variantId, 2, 1)).resolves.toBeUndefined() + await expect(index.get(scope, third.variantId, 2, 1)).resolves.toEqual(third) + }) + it('treats a corrupt upload cache as empty and repairs it on the next commit', async () => { const dir = await mkdtemp(join(tmpdir(), 'dsh-upload-index-')) roots.push(dir) diff --git a/packages/preset/agent-preset/skills/cordis-composition-reference/references/packages.md b/packages/preset/agent-preset/skills/cordis-composition-reference/references/packages.md index 64b58f59ae..7ccf6a43ce 100644 --- a/packages/preset/agent-preset/skills/cordis-composition-reference/references/packages.md +++ b/packages/preset/agent-preset/skills/cordis-composition-reference/references/packages.md @@ -356,7 +356,7 @@ Every package below exports a Cordis plugin that a bundle patch can name in a Lo | `@deepseek-ai/dsh-session-projection` | no | Session-projection seam: the merge-extensible projection type table, the provider contract, and the ctx.sessionProjections registry serving whole current values of log-derived per-session state | | `@deepseek-ai/dsh-session-projection-cache` | yes | Persisted projection cache (ctx.sessionProjectionCache): durable per-session checkpoint records on the session_projcache storage domain (per-record layout), throttled write-behind, and the cached listing read | | `@deepseek-ai/dsh-session-stats` | no | Whole-log conversation counts and wall times projection (sessionStats) for the DeepSeek Harness | -| `@deepseek-ai/dsh-session-telemetry-otel` | yes | OpenTelemetry backend for the DeepSeek Harness telemetry seam: hands captured session records to the OTel JS SDK's log pipeline | +| `@deepseek-ai/dsh-session-telemetry-otel` | yes | Feedback-authorized Session logs over byte-bounded OpenTelemetry HTTP requests | | `@deepseek-ai/dsh-session-title` | yes | Log-backed session title service and provider registry for the DeepSeek Harness | | `@deepseek-ai/dsh-session-title-all-prompts-llm` | yes | All-user-messages LLM provider plugin for DeepSeek Harness session titles | | `@deepseek-ai/dsh-session-title-first-prompt-llm` | yes | First-message LLM provider plugin for DeepSeek Harness session titles | @@ -446,6 +446,12 @@ Every package below exports a Cordis plugin that a bundle patch can name in a Lo |---|---|---| | `@deepseek-ai/dsh-subprocess-local` | no | Local-subprocess implementation of the DeepSeek Harness subprocess seam | +## telemetry + +| Package | Config | Description | +|---|---|---| +| `@deepseek-ai/dsh-otel` | no | Cordis service for independent ordinary-event and byte-bounded Session-log OTLP channels | + ## terminal | Package | Config | Description | diff --git a/packages/schedule/schedule/README.i18n.yaml b/packages/schedule/schedule/README.i18n.yaml index 86176d8c0e..80bd55f6f7 100644 --- a/packages/schedule/schedule/README.i18n.yaml +++ b/packages/schedule/schedule/README.i18n.yaml @@ -15,8 +15,8 @@ en: 23386452a66b2fbd zh: 4a8d91b2f313fead /deepseek-ai-dsh-schedule/use-this-package: - en: 401d9737620b86d9 - zh: 0ebbc36e2fe157f3 + en: db292f4e03cef41b + zh: 064cb553698d548c /deepseek-ai-dsh-schedule/understand-the-implementation: en: 676b7c508f37a1b7 zh: c5e8520b3350f0b0 diff --git a/packages/schedule/schedule/README.md b/packages/schedule/schedule/README.md index cc39506ee6..b932e52e13 100644 --- a/packages/schedule/schedule/README.md +++ b/packages/schedule/schedule/README.md @@ -23,7 +23,7 @@ Schedule delivers one-shot, fixed-rate, daily, weekly, and cron wall-clock remin ## Use this package -The shipped Web bundle mounts the service alongside storage-domain and the Session controller. Its `Config` states `deliveryHistoryDays` (default 30) and `deliveryHistoryRecords` (default 200). Storage backend routing belongs to storage-domain; Session model and preset restoration belong to the Session controller. Schedule cannot be mounted alone in a headless or SDK-only composition: delivery requires the Host Web Session controller and a Session persistence backend, because a delivery commits only after the Session acknowledges `session/flush`. +The shipped Web composition carries the `schedule` row disabled; enable the optional experimental bundle `@deepseek-ai/dsh-experimental-schedule-bundle` from the Plugins page (Official group), or list it in a profile's `dsh.profile.bundles`, to mount the service alongside storage-domain and the Session controller. Its `Config` states `deliveryHistoryDays` (default 30) and `deliveryHistoryRecords` (default 200). Storage backend routing belongs to storage-domain; Session model and preset restoration belong to the Session controller. Schedule cannot be mounted alone in a headless or SDK-only composition: delivery requires the Host Web Session controller and a Session persistence backend, because a delivery commits only after the Session acknowledges `session/flush`. The Agent receives `schedule_create`, `schedule_list`, `schedule_delete`, and `schedule_update`. Update replaces the name, instruction, or timing of one reminder in place and keeps its id and saved records; it is not offered for the relative `after` delay. Creation requires a non-empty prompt, a title, and exactly one of six selectors: diff --git a/packages/schedule/schedule/README.zh.md b/packages/schedule/schedule/README.zh.md index 93f67b6cab..6b4e70c9c1 100644 --- a/packages/schedule/schedule/README.zh.md +++ b/packages/schedule/schedule/README.zh.md @@ -23,7 +23,7 @@ Schedule 将一次性、固定周期、按每日、按每周以及 cron 本地 ## 使用此包 -发布的 Web bundle 将此服务与 storage-domain、Session controller 一起挂载。其 `Config` 声明 `deliveryHistoryDays`(默认 30)与 `deliveryHistoryRecords`(默认 200)。存储后端路由由 storage-domain 管理;会话模型与 preset 恢复由 Session controller 管理。Schedule 无法在 headless 或仅 SDK 的组合中单独挂载:投递需要 Host 的 Web Session controller 和 Session 持久化后端,因为只有在 Session 确认 `session/flush` 之后一次投递才会提交。 +随发行版交付的 Web 组合把 `schedule` 行保持禁用;在插件管理页(Official 分组)启用可选实验性 bundle `@deepseek-ai/dsh-experimental-schedule-bundle`,或把它列进 profile 的 `dsh.profile.bundles`,即可将此服务与 storage-domain、Session controller 一起挂载。其 `Config` 声明 `deliveryHistoryDays`(默认 30)与 `deliveryHistoryRecords`(默认 200)。存储后端路由由 storage-domain 管理;会话模型与 preset 恢复由 Session controller 管理。Schedule 无法在 headless 或仅 SDK 的组合中单独挂载:投递需要 Host 的 Web Session controller 和 Session 持久化后端,因为只有在 Session 确认 `session/flush` 之后一次投递才会提交。 Agent 获得 `schedule_create`、`schedule_list`、`schedule_delete` 和 `schedule_update`。更新原地替换一条提醒的名称、指令或时间,保留其 id 与已保存记录;相对的 `after` 延迟不支持更新。创建时需要非空提示文本、标题,且必须只提供以下六个选择器之一: diff --git a/packages/session/session-telemetry-otel/README.i18n.yaml b/packages/session/session-telemetry-otel/README.i18n.yaml index b89f0da8de..d26ad40f35 100644 --- a/packages/session/session-telemetry-otel/README.i18n.yaml +++ b/packages/session/session-telemetry-otel/README.i18n.yaml @@ -9,8 +9,8 @@ en: f8ea869792c8b207 zh: bc076172ce22ba9b /deepseek-ai-dsh-session-telemetry-otel/summary: - en: 1c5d2f15cb13e318 - zh: 5e061a16cd38c4c5 + en: 828903209ef34983 + zh: 8eac63f1e8df15d8 /deepseek-ai-dsh-session-telemetry-otel/table-of-contents: en: d152484eb41ac6b4 zh: 09388d293f9be9cb @@ -21,29 +21,29 @@ en: bc7e598148a7f8c3 zh: 0209c6aa258ec134 /deepseek-ai-dsh-session-telemetry-otel/use-this-package/minimal-configuration: - en: 3de0d5db3156a7e4 - zh: 71199133cd9998c4 + en: 11845fd50dc0a494 + zh: ee990bfca3621bd3 /deepseek-ai-dsh-session-telemetry-otel/use-this-package/what-leaves-the-machine: - en: b94ca8299ad60c5d - zh: 3791131f3440e3b3 + en: 2a5ddd227e400455 + zh: 74b7bd9b9161b054 /deepseek-ai-dsh-session-telemetry-otel/use-this-package/failures-and-shutdown: - en: 20c6e78df8ad2c2a - zh: fd6a00f98c4cc05c + en: a66831cb9de57e04 + zh: 3ada28bb868e0932 /deepseek-ai-dsh-session-telemetry-otel/understand-the-implementation: en: ee672a6a6f0fa02d zh: c3bab8e6b60e768c /deepseek-ai-dsh-session-telemetry-otel/understand-the-implementation/design-concept: - en: d5e1f8249d3eea34 - zh: 7a6915e33390f4ae + en: 34f8a88224a1f6cf + zh: 40839dabaea39602 /deepseek-ai-dsh-session-telemetry-otel/understand-the-implementation/source-map: en: 0b99d2e5c27213cd zh: 3cbdaa7f6a45d81a /deepseek-ai-dsh-session-telemetry-otel/understand-the-implementation/capture-wiring: - en: 9bdc01d0fa130b3a - zh: 63bf30c4cc983ca0 + en: 5c85eb0992eb5b2b + zh: a19cdd5814c0e2b9 /deepseek-ai-dsh-session-telemetry-otel/understand-the-implementation/field-mapping: - en: 38590f3afee40bb8 - zh: b2fc1f4341008d79 + en: a42e9e081fb93bee + zh: b8e817c2455278b9 /deepseek-ai-dsh-session-telemetry-otel/further-exploration: en: 155b75490ec65d86 zh: 07d37d6e4ac90c89 @@ -57,5 +57,5 @@ en: e43087aea7af4c9e zh: f8b529e2012b4c63 /deepseek-ai-dsh-session-telemetry-otel/known-limitations-and-deferred-work/dev-note: - en: a770daec456212ab - zh: 1b861566d591837d + en: 0065df2648b09ac9 + zh: b5bac41fc3aec70e diff --git a/packages/session/session-telemetry-otel/README.md b/packages/session/session-telemetry-otel/README.md index 35dc351bd5..f3fc3fa417 100644 --- a/packages/session/session-telemetry-otel/README.md +++ b/packages/session/session-telemetry-otel/README.md @@ -9,7 +9,9 @@ English | [中文](README.zh.md) ## Summary -`dsh-session-telemetry-otel` exports session records through the OTel JS SDK only after new explicit feedback, for all users and providers, including `deepseek-official`. `FEEDBACK_ONLY` releases the canonical prefix through that feedback, including context; later records wait for the next explicit feedback. `DISABLED` constructs no transport. SDK batching can finish an authorized upload without another user interaction or model call. Deployments own their redaction rules. +The adapter injects `otel`; the [shared OTel plugin](../../telemetry/otel/README.md) creates its independent Session-log channel. Authorization, redaction, identity, scope version, configuration, and the shutdown deadline remain owned here. + +`dsh-session-telemetry-otel` exports session records through the OTel JS SDK only after new explicit feedback, for all users and providers, including `deepseek-official`. `FEEDBACK_ONLY` releases the canonical prefix through that feedback, including context; later records wait for the next explicit feedback. `DISABLED` constructs no transport. Scheduled batching can finish an authorized upload without another user interaction or model call. Deployments own their redaction rules. ## Table of Contents @@ -38,7 +40,7 @@ Programmatic TypeScript configuration uses the exported `SessionTelemetryMode` e ### Minimal configuration -Uploading modes require an exporter URL and accept the SDK option blocks verbatim: +Uploading modes require an exporter URL. Processor settings control the independent Session-log queue; routing headers are explicit `exporter.headers` values. ```yaml - id: sessionTelemetry-otel @@ -46,31 +48,34 @@ Uploading modes require an exporter URL and accept the SDK option blocks verbati config: mode: FEEDBACK_ONLY # optional; defaults to FEEDBACK_ONLY shutdownTimeoutMillis: 3000 # optional; defaults to 3000 - exporter: # passed verbatim to the SDK's OTLP/HTTP log exporter + exporter: # explicit SDK transport settings url: https://collector.example.com/v1/logs headers: authorization: !!js `Bearer ${process.env.OTLP_TOKEN}` - processor: {} # optional; passed verbatim to BatchLogRecordProcessor + processor: {} # optional; byte/count batching and per-request watchdog ``` | Field | Default | Meaning | |---|---|---| | `mode` | `FEEDBACK_ONLY` | Sharing policy: `FEEDBACK_ONLY` or `DISABLED` | | `exporter.url` | required in uploading modes | Full OTLP logs endpoint; must parse as `http(s)` | -| `exporter`, `processor` | — | Passed verbatim to the SDK exporter and batch processor | -| `shutdownTimeoutMillis` | `3,000` | Outer deadline for the SDK's complete shutdown sequence | +| `exporter`, `processor` | — | SDK transport plus byte/count batching; headers/TLS identity are not inherited from the environment. Agent factories own their returned agent settings, including keepAlive | +| `shutdownTimeoutMillis` | `3,000` | Outer deadline for all queued HTTP requests; remaining queued sends stop at expiry | +| `maxRequestBytes` | `4,000,000` | Maximum complete OTLP JSON request bytes before gzip; may only be lowered | Direct `ctx.sessionTelemetry.emit()` calls are no-ops in every mode and cannot bypass feedback authorization. Inherited parent feedback does not authorize a child export: the child needs new feedback of its own. Its authorized prefix then includes inherited context. -Model requests, request headers, Session creation or adoption, restoration, and plugin mount or HMR do not authorize capture. Stored feedback alone triggers nothing. SDK scheduled flush and shutdown may finish batches authorized earlier, but never capture new records. +Model requests, request headers, Session creation or adoption, restoration, and plugin mount or HMR do not authorize capture. Stored feedback alone triggers nothing. Scheduled flush and shutdown may finish batches authorized earlier, but never capture new records. ### What leaves the machine -In uploading modes, records carry the complete `event.data` as the seam's `sessionTelemetry/record` waterfall returns it — message content, tool arguments and results, the system prompt and tool schemas, todo text, compaction summaries, feedback text, and the session `cwd`. Provider credentials never appear: adapter API keys are constructor parameters, not session events, so they are structurally absent from the log and therefore from telemetry. `DISABLED` constructs no SDK pipeline and hands no capture to a backend. +Each Session event becomes one `eventName: "session-log"` record. `attributes.sessionId` is the collector Session identity; `attributes.content` encodes the complete event envelope with redacted `event.data`. JSON values are preserved, not the original JSONL bytes or key ordering. Legacy `session.id`, `event.seq`, and `event.type` metadata remain for existing consumers. Resources carry application and anonymous-user identity; scope carries the backend package name and version. The base profile uses `https://dsh-otel-collector.deepseeksvc.com/v1/logs`; `DSH_TELEMETRY_OTLP_URL` overrides it. No channel header is added implicitly. + +The shared OTel channel measures each record once with the SDK OTLP JSON serializer, including its resource/scope envelope, then greedily packs requests using those conservative sizes. A single oversized event produces one rejection diagnostic without truncation. Session logs never mix with product analytics in a request. Capture handoff and shutdown are not collector acknowledgements. ### Failures and shutdown -Misconfiguration fails at plugin load: a missing or non-`http(s)` `exporter.url`, a non-positive-integer `processor.maxExportBatchSize` (which the SDK accepts but then hangs on at shutdown), and an invalid `shutdownTimeoutMillis` all reject before any record is exported. During shutdown, OTel awaits `exporter.forceFlush()` before the processor's bounded completion promise; if that transport promise never settles, this package abandons the wait at `shutdownTimeoutMillis`, logs the contained failure, and lets application teardown continue — records still pending then may be lost at process exit. +Invalid byte limits, non-positive queue/timer values, `maxExportBatchSize > maxQueueSize`, invalid endpoints, and invalid shutdown deadlines fail at load. Processor defaults are 2,048 queued records, 512 records per request, a 1,000 ms scheduling delay, and a 30,000 ms request watchdog. Byte limits may split a count batch into multiple serial HTTP requests. Each request waits for SDK export-queue cleanup after its callback before the next starts. `exportTimeoutMillis` warns per request but never frees an unsettled transport slot; the SDK transport owns network timeout/retry. Shutdown drains these requests until `shutdownTimeoutMillis`; expiry discards the remaining queue and prevents further sends, while an active request may still settle. Large prefixes can therefore remain partially unsent at CLI exit. ----- @@ -84,7 +89,7 @@ This section explains the backend's composition; the observable behavior is full ### Design concept -The backend is a thin adapter over the OTel JS SDK: it owns feedback authorization, resource identity, and an outer shutdown deadline. Canonical ledger records use the `@deepseek-ai/dsh-session-telemetry-otel` instrumentation scope; this backend captures no operational records. Resource identity carries `service.name`/`service.version` from `dsh-llm`'s `APP_IDENTITY` plus the anonymous `user.id` (from `$DSH_HOME/.anonymous-user-id`), once per export batch rather than per record. +The backend owns feedback authorization, identity, and shutdown. The shared OTel service owns channel creation, byte/count scheduling, record construction, JSON transport, compression, and retries. Resource identity carries `service.name`/`service.version` from `APP_IDENTITY` and anonymous `user.id`; the scope retains this package’s name and version. ### Source map @@ -94,11 +99,11 @@ The backend is a thin adapter over the OTel JS SDK: it owns feedback authorizati ### Capture wiring -The backend uses on-demand capture with stored history included. Only new own `feedback/record`, `feedback/message-put`, or `feedback/message-delete` events trigger live capture, bounded by that event. A cold `feedback/committed` notification supplies its committed canonical snapshot without publishing a live Session or Agent. Same-object handoff cursors suppress repeated capture. The backend implements no `flush()`; the SDK owns batching and shutdown drain. +The backend captures history on demand through new own feedback events or committed cold snapshots. Its private reporter serializes queued HTTP requests even after a watchdog fires; an outer shutdown deadline stops the remaining queue. It exposes no extra flush entry point. ### Field mapping -Each telemetry record maps to one SDK log record with captured timestamp, severity, body, and attributes. Feedback authorizes the complete unhanded prefix, not only the feedback payload. +Each capture record supplies a separately copied event envelope and redacted payload to `SessionLogReporter`; serialization retains optional surface metadata and the event sequence/time. Feedback authorizes the complete unhanded prefix, not only the feedback payload.
@@ -148,4 +153,4 @@ None.
-**Runtime invariant:** No companion is published. Mode selection changes capture handoff, SDK setup, and local diagnostics without mutating session or service state an independent companion can compare. Export remains inside the SDK past the backend boundary. +**Runtime invariant:** No companion is published. Mode selection changes capture handoff, SDK setup, and local diagnostics without mutating session or service state an independent companion can compare. Collector delivery cannot be inferred from local queue state. diff --git a/packages/session/session-telemetry-otel/README.zh.md b/packages/session/session-telemetry-otel/README.zh.md index c414e6461a..2220cc4291 100644 --- a/packages/session/session-telemetry-otel/README.zh.md +++ b/packages/session/session-telemetry-otel/README.zh.md @@ -9,7 +9,9 @@ kind: "package-reference" ## 概述 -`dsh-session-telemetry-otel` 仅在新的显式反馈后通过 OTel JS SDK 导出会话记录,适用于所有用户和提供方,包括 `deepseek-official`。`FEEDBACK_ONLY` 释放截至该反馈的权威日志前缀,包含上下文;后续记录等待下一次显式反馈。`DISABLED` 不构造传输。SDK 批处理可完成已授权的上传,无需另一次用户交互或模型调用。部署方负责脱敏规则。 +适配器注入 `otel`;[共享 OTel 插件](../../telemetry/otel/README.zh.md) 创建其独立 Session 日志通道。授权、脱敏、身份、scope 版本、配置和关闭期限仍由本包负责。 + +`dsh-session-telemetry-otel` 仅在新的显式反馈后通过 OTel JS SDK 导出会话记录,适用于所有用户和提供方,包括 `deepseek-official`。`FEEDBACK_ONLY` 释放截至该反馈的权威日志前缀,包含上下文;后续记录等待下一次显式反馈。`DISABLED` 不构造传输。定时批处理可完成已授权的上传,无需另一次用户交互或模型调用。部署方负责脱敏规则。 ## 目录 @@ -38,7 +40,7 @@ kind: "package-reference" ### 最小配置 -上传模式需要导出器 URL,并原样接受 SDK 选项块: +启用上传的模式必须提供 exporter URL。processor 设置控制独立的 Session 日志队列;路由头通过 `exporter.headers` 显式配置。 ```yaml - id: sessionTelemetry-otel @@ -46,31 +48,34 @@ kind: "package-reference" config: mode: FEEDBACK_ONLY # optional; defaults to FEEDBACK_ONLY shutdownTimeoutMillis: 3000 # optional; defaults to 3000 - exporter: # passed verbatim to the SDK's OTLP/HTTP log exporter + exporter: # explicit SDK transport settings url: https://collector.example.com/v1/logs headers: authorization: !!js `Bearer ${process.env.OTLP_TOKEN}` - processor: {} # optional; passed verbatim to BatchLogRecordProcessor + processor: {} # optional; byte/count batching and per-request watchdog ``` | 字段 | 默认值 | 含义 | |---|---|---| | `mode` | `FEEDBACK_ONLY` | 共享策略:`FEEDBACK_ONLY` 或 `DISABLED` | | `exporter.url` | 上传模式必填 | 完整 OTLP 日志端点;必须能解析为 `http(s)` | -| `exporter`、`processor` | — | 原样传给 SDK 导出器与批处理器 | -| `shutdownTimeoutMillis` | `3,000` | SDK 完整关闭序列的外层截止时间 | +| `exporter`, `processor` | — | SDK 传输及字节/条数聚合;不继承环境中的头部和 TLS 身份。agent 工厂负责返回实例的设置,包括 keepAlive | +| `shutdownTimeoutMillis` | `3,000` | 所有排队 HTTP 请求的外层期限;到期后停止剩余排队发送 | +| `maxRequestBytes` | `4,000,000` | gzip 前完整 OTLP JSON 请求的最大字节数;只能调低 | 直接调用 `ctx.sessionTelemetry.emit()` 在任何模式下都是空操作,不能绕过反馈授权。继承的父会话反馈不授权子会话导出:子会话需要新的自身反馈。授权后的前缀包含继承的上下文。 -模型请求、请求头、Session 创建或接纳、恢复,以及插件挂载或 HMR(热模块替换)均不授权捕获。仅凭已存储的反馈不会触发任何操作。SDK 定时刷新和关闭可以完成先前已授权的批次,但绝不捕获新记录。 +模型请求、请求头、Session 创建或接纳、恢复,以及插件挂载或 HMR(热模块替换)均不授权捕获。仅凭已存储的反馈不会触发任何操作。定时刷新和关闭可以完成先前已授权的批次,但绝不捕获新记录。 ### 哪些数据会离开本机 -在上传模式中,记录携带 seam 的 `sessionTelemetry/record` waterfall(瀑布式事件)返回的完整 `event.data`——消息内容、工具参数与结果、系统提示词与工具 schema、todo 文本、压缩(compaction)摘要、反馈文本,以及会话 `cwd`。提供方凭据绝不会出现:适配器的 API key 是构造函数参数而非会话事件,因此它们在结构上就不存在于日志中,也就不存在于遥测中。`DISABLED` 不构造 SDK 流水线,也不把任何捕获内容交给后端。 +每条 Session 事件对应一个 `eventName: "session-log"` 记录。`attributes.sessionId` 是 collector 使用的 Session 身份;`attributes.content` 编码完整事件 envelope 和脱敏后的 `event.data`。保留的是 JSON 值,不保证原 JSONL 字节或键顺序相同。为现有消费者保留 `session.id`、`event.seq` 和 `event.type` 元数据。Resource 携带应用和匿名用户身份;scope 携带后端包名和版本。基础配置使用 `https://dsh-otel-collector.deepseeksvc.com/v1/logs`,可用 `DSH_TELEMETRY_OTLP_URL` 覆盖。不隐式添加 channel 头。 + +共享 OTel 通道使用 SDK 的 OTLP JSON 序列化器对每条记录计量一次,包含其 resource/scope envelope,再按保守大小顺序组包。单条超限事件产生一次拒绝诊断且不截断。Session 日志不会与产品埋点混在一个请求中。捕获交接和关闭完成不代表 collector 确认。 ### 失败与关闭 -配置错误会在插件加载时失败:缺少或非 `http(s)` 的 `exporter.url`、非正整数的 `processor.maxExportBatchSize`(SDK 会接受该值,随后却在关闭时挂起)以及无效的 `shutdownTimeoutMillis` 都会在任何记录导出前被拒绝。关闭期间,OTel 会先等待 `exporter.forceFlush()`,再等待处理器有界完成 promise;如果该传输 promise 始终不结算,本包会在 `shutdownTimeoutMillis` 到期时放弃等待、记录已隔离的失败,并让应用继续拆卸——届时仍待处理的记录可能在进程退出时丢失。 +无效字节上限、非正队列/计时值、`maxExportBatchSize > maxQueueSize`、无效 endpoint 或关闭期限在加载时失败。processor 默认队列为 2,048 条、单请求最多 512 条、调度延迟 1,000 ms、请求监测期限 30,000 ms。字节上限可能将按条数划分的批次拆成多个串行 HTTP 请求。每个请求在回调后等待 SDK 导出队列清理完成,才启动下一个请求。`exportTimeoutMillis` 针对每个请求告警,但不会释放未结束的传输槽位;SDK transport 负责网络超时和重试。关闭时发送这些请求直到 `shutdownTimeoutMillis`,到期后丢弃剩余队列并禁止后续发送,在途请求仍可能结束。因此较大的授权前缀在 CLI 退出时可能只发送了一部分。 ----- @@ -84,7 +89,7 @@ kind: "package-reference" ### 设计理念 -后端是对 OTel JS SDK 的薄适配层:它拥有反馈授权、资源身份与外层关闭截止时间。权威 ledger 记录使用 `@deepseek-ai/dsh-session-telemetry-otel` 插桩作用域;此后端不捕获运维记录。资源身份携带 `service.name`/`service.version`(来自 `dsh-llm` 的 `APP_IDENTITY`)以及匿名 `user.id`(来自 `$DSH_HOME/.anonymous-user-id`),按导出批次携带一次,而非逐条记录。 +后端负责反馈授权、身份和关闭。共享 OTel 服务负责通道创建、字节和条数调度、记录构造、JSON 传输、压缩与重试。Resource 身份携带 `APP_IDENTITY` 中的 `service.name` / `service.version` 以及匿名 `user.id`;scope 保留本包的名称和版本。 ### 源码地图 @@ -94,11 +99,11 @@ kind: "package-reference" ### 捕获接线 -后端使用包含存储历史的按需捕获。只有新的自身 `feedback/record`、`feedback/message-put` 或 `feedback/message-delete` 事件触发活跃会话捕获,并以该事件为上限。冷会话 `feedback/committed` 通知提供已提交的权威快照,不发布存活 Session 或 Agent。同对象交接游标抑制重复捕获。后端不实现 `flush()`;SDK 负责批处理和关闭排空。 +后端按需捕获历史,由新的自身反馈事件或已提交的冷会话快照触发。私有 reporter 串行发送排队 HTTP 请求,即使监测期限已触发也不会重叠;外层关闭期限停止剩余队列。不额外暴露 flush 入口。 ### 字段映射 -每条遥测记录映射为一条 SDK 日志记录,携带捕获的时间戳、严重级别、正文和属性。反馈授权的是尚未交接的完整前缀,而非只有反馈载荷。 +每条采集记录向 `SessionLogReporter` 提供单独复制的事件信封和脱敏后的载荷;序列化保留可选的呈现元数据及事件序号和时间。反馈授权整个尚未交接的前缀,而不只是反馈载荷。 @@ -148,4 +153,4 @@ kind: "package-reference" -**运行时不变式:** 不发布伴生入口。模式选择只改变 capture handoff、SDK setup 与本地 diagnostics,不改变可由独立 companion 对照的会话或服务状态。导出在越过后端边界后仍由 SDK 内部处理。 +**运行时不变式:** 不发布伴生入口。模式选择只改变 capture handoff、SDK setup 与本地 diagnostics,不改变可由独立 companion 对照的会话或服务状态。无法根据本地队列状态推断 collector 是否收到记录。 diff --git a/packages/session/session-telemetry-otel/package.json b/packages/session/session-telemetry-otel/package.json index c1ae23c564..7777001d47 100644 --- a/packages/session/session-telemetry-otel/package.json +++ b/packages/session/session-telemetry-otel/package.json @@ -1,6 +1,6 @@ { "name": "@deepseek-ai/dsh-session-telemetry-otel", - "description": "OpenTelemetry backend for the DeepSeek Harness telemetry seam: hands captured session records to the OTel JS SDK's log pipeline", + "description": "Feedback-authorized Session logs over byte-bounded OpenTelemetry HTTP requests", "version": "0.1.7-rc.2", "publishConfig": { "access": "public" @@ -27,11 +27,8 @@ ], "license": "MIT", "dependencies": { - "@opentelemetry/api": "^1.9.1", "@opentelemetry/api-logs": "^0.220.0", - "@opentelemetry/exporter-logs-otlp-http": "^0.220.0", "@opentelemetry/otlp-exporter-base": "^0.220.0", - "@opentelemetry/resources": "^2.9.0", "@opentelemetry/sdk-logs": "^0.220.0", "@deepseek-ai/schemastery": "workspace:~" }, @@ -42,7 +39,8 @@ "@deepseek-ai/dsh-session": "workspace:*", "@deepseek-ai/dsh-session-telemetry": "workspace:*", "@deepseek-ai/dsh-anonymous-user-id": "workspace:*", - "@deepseek-ai/cordis": "workspace:~" + "@deepseek-ai/cordis": "workspace:~", + "@deepseek-ai/dsh-otel": "workspace:*" }, "devDependencies": { "@deepseek-ai/cordis-plugin-logger-console": "workspace:~", @@ -60,6 +58,7 @@ "@deepseek-ai/dsh-subprocess-local": "workspace:*", "@deepseek-ai/dsh-anonymous-user-id": "workspace:*", "@deepseek-ai/cordis": "workspace:~", - "@deepseek-ai/dsh-http-proxy": "workspace:*" + "@deepseek-ai/dsh-http-proxy": "workspace:*", + "@deepseek-ai/dsh-otel": "workspace:*" } } diff --git a/packages/session/session-telemetry-otel/src/index.ts b/packages/session/session-telemetry-otel/src/index.ts index 48792dbe67..6d351a585f 100644 --- a/packages/session/session-telemetry-otel/src/index.ts +++ b/packages/session/session-telemetry-otel/src/index.ts @@ -1,13 +1,9 @@ /** * OpenTelemetry Service Provider for the DeepSeek Harness telemetry capability. * - * Composes the OTel JS SDK as-is — a `LoggerProvider` with a - * `BatchLogRecordProcessor` and an OTLP/HTTP log exporter — and maps each - * record handed over by the capture coordinator onto `logger.emit()`. After that call, - * batching, retry, queueing, and loss policy use the SDK's documented behavior, configured - * verbatim through the `exporter`/`processor` passthroughs. This package owns - * capture mode and an outer shutdown deadline: the SDK's export timeout does - * not bound its preceding `forceFlush()` wait. + * Authorizes feedback-bounded capture and hands complete event strings to the + * Session-log reporter. This plugin owns resource identity and an outer + * shutdown deadline; the reporter owns byte-bounded SDK delivery. * * @module @deepseek-ai/dsh-session-telemetry-otel */ @@ -28,19 +24,10 @@ import { } from '@deepseek-ai/dsh-session-telemetry' import { APP_IDENTITY } from '@deepseek-ai/dsh-llm' import { getOrCreateAnonymousUserId } from '@deepseek-ai/dsh-anonymous-user-id' -import { - BatchLogRecordProcessor, - LoggerProvider, - type BatchLogRecordProcessorOptions, -} from '@opentelemetry/sdk-logs' -import { OTLPLogExporter } from '@opentelemetry/exporter-logs-otlp-http' +import type { SessionLogReporter } from '@deepseek-ai/dsh-otel' +import type { BatchLogRecordProcessorOptions } from '@opentelemetry/sdk-logs' import type { OTLPExporterNodeConfigBase } from '@opentelemetry/otlp-exporter-base' -import { SeverityNumber, type AnyValue } from '@opentelemetry/api-logs' -import { resourceFromAttributes } from '@opentelemetry/resources' - -// The package's own manifest is the single source of the instrumentation-scope -// version (same pattern as dsh-llm's attribution identity). -const { version } = createRequire(import.meta.url)('../package.json') as { version: string } +import { SeverityNumber } from '@opentelemetry/api-logs' /** Session-sharing policy selected by {@link Config.mode}. */ export enum SessionTelemetryMode { @@ -93,44 +80,44 @@ function sharingStatusFor(mode: SessionTelemetryMode): SessionTelemetrySharingSt } /** - * Plugin configuration: one sharing policy, two verbatim SDK option objects, - * and one DSH-owned shutdown bound. Uploading modes validate their endpoint + * Plugin configuration: sharing policy, SDK transport options, byte/count queue + * settings, and an overall shutdown bound. Uploading modes validate their endpoint * and shutdown deadline at plugin load; `DISABLED` reads neither. */ export interface Config { /** Defaults to `FEEDBACK_ONLY`: capture session history only when feedback is explicitly submitted. */ mode?: SessionTelemetryMode /** - * Passed verbatim to the SDK's OTLP/HTTP log exporter — the complete - * `OTLPExporterNodeConfigBase` shape (`headers`, `timeoutMillis`, - * `compression`, `keepAlive`, …), owned and documented by the SDK. `url` - * is the one field this package requires and validates itself. + * Explicit SDK HTTP transport settings, including optional routing headers. + * Ambient credentials are not inherited. URL is required while uploading. */ exporter?: OTLPExporterNodeConfigBase & { /** Full logs endpoint (e.g. `https://collector.example.com/v1/logs`). Required outside `DISABLED`; validated at load. */ url?: string } /** - * Passed verbatim to `BatchLogRecordProcessor` (minus the exporter slot, - * which this plugin fills); the SDK owns and documents these knobs. + * Count, queue, cadence, and per-request watchdog settings for the byte-bounded + * processor. A watchdog warning never releases an unsettled transport slot. */ processor?: Omit /** Maximum time spent awaiting the SDK provider's complete shutdown path. */ shutdownTimeoutMillis?: number + /** Uncompressed OTLP request byte limit, at most 4,000,000. */ + maxRequestBytes?: number } /** * Schemastery validator for {@link Config}; cordis runs it before the plugin - * starts. It checks only the top-level fields; value checks live in the constructor - * so their errors name the fields. Both SDK option objects pass through unchanged: - * the SDK defines and validates their fields. Re-declaring them here would - * silently drop every field this plugin did not repeat. + * starts. The constructor validates endpoint and shutdown requirements; the + * reporter validates Session byte and queue limits. SDK transport and + * processor settings retain their upstream types. */ export const Config: z = z.object({ mode: z.union(Object.values(SessionTelemetryMode)).default(DEFAULT_TELEMETRY_MODE), exporter: z.any(), processor: z.any(), shutdownTimeoutMillis: z.number(), + maxRequestBytes: z.number().step(1).min(1).max(4_000_000), }) /** Default outer allowance for the SDK's complete shutdown sequence. */ @@ -141,10 +128,10 @@ export const DEFAULT_SHUTDOWN_TIMEOUT_MILLIS = 3_000 const MAX_TIMER_DELAY_MILLIS = 2_147_483_647 /** Severity mapping from the Service Definition's three-level vocabulary to OTel severity numbers. */ -const SEVERITY: Record = { - info: { severityNumber: SeverityNumber.INFO, severityText: 'INFO' }, - warn: { severityNumber: SeverityNumber.WARN, severityText: 'WARN' }, - error: { severityNumber: SeverityNumber.ERROR, severityText: 'ERROR' }, +const SEVERITY: Record = { + info: SeverityNumber.INFO, + warn: SeverityNumber.WARN, + error: SeverityNumber.ERROR, } /** @@ -154,10 +141,10 @@ const SEVERITY: Record { ctx.logger.warn(message, error) }, }) - const ledger = this.provider.getLogger('@deepseek-ai/dsh-session-telemetry-otel', version) + this.provider = reporter const enqueue: SessionTelemetrySink['emit'] = (record) => { - ledger.emit({ - timestamp: record.time, - observedTimestamp: record.time, - ...SEVERITY[record.severity], - // JSON-serializable by the seam's contract (validated at Session.append), - // which is exactly the AnyValue subset. - body: record.body as AnyValue, + if (record.sourceEvent === undefined) { + ctx.logger.warn('Session log record withheld: redaction removed sourceEvent') + return + } + reporter.reportSessionLog({ + sessionId: record.sourceEvent.sessionId, + event: { ...record.sourceEvent.envelope, data: record.body }, + severityNumber: SEVERITY[record.severity], attributes: record.attributes, }) } @@ -273,23 +249,11 @@ export class OpenTelemetrySessionBackend extends SessionTelemetryBackend { */ emit(_record: SessionTelemetryRecord): void {} - // The Service Definition's optional flush() hint is deliberately NOT implemented. The - // batch processor exports on its own cadence (`processor.scheduledDelayMillis`, - // the SDK's documented knob), and this backend is the SDK pipeline's only - // caller — forwarding the hint to `forceFlush()` would be the sole source of - // concurrent flushes, whose undocumented interactions with shutdown's - // internal drain (concurrent-flush guard, provider-level flush timeout) - // silently drop tail records. Rationale and the revival trigger: the - // revival Agent Note. - /** - * Ask the SDK to drain and quiesce, but reject after the backend-owned - * deadline. OTel's processor export timeout wraps `exportCompleted` only; - * shutdown awaits `exporter.forceFlush()` first, which can remain pending - * when the transport never obtains a socket. The provider promise remains - * observed after the deadline so a later rejection cannot become unhandled. - * `DISABLED` has no provider and resolves immediately. - * @returns resolves when the SDK pipeline quiesces or is disabled, or rejects at the configured deadline. + * Drain queued HTTP requests until the deployment deadline. The watchdog + * never releases an unsettled transport slot. At the outer deadline, + * queued records are abandoned and no further requests may start. + * @returns completion after transport shutdown, or rejection at the configured deadline. */ async shutdown(): Promise { if (this.provider === undefined) return @@ -297,6 +261,7 @@ export class OpenTelemetrySessionBackend extends SessionTelemetryBackend { let timer: ReturnType | undefined const deadline = new Promise((_resolve, reject) => { timer = setTimeout(() => { + this.provider?.stopPending() reject(new Error(`session-telemetry-otel: provider shutdown exceeded ${this.shutdownTimeoutMillis}ms`)) }, this.shutdownTimeoutMillis) }) diff --git a/packages/session/session-telemetry-otel/tests/egress.spec.ts b/packages/session/session-telemetry-otel/tests/egress.spec.ts index 72f7c35617..5dea6f9473 100644 --- a/packages/session/session-telemetry-otel/tests/egress.spec.ts +++ b/packages/session/session-telemetry-otel/tests/egress.spec.ts @@ -1,3 +1,4 @@ +import OTel from '@deepseek-ai/dsh-otel' import { createServer, type Server } from 'node:http' import { once } from 'node:events' import { mkdtempSync, rmSync } from 'node:fs' @@ -80,6 +81,7 @@ describe('session-telemetry-otel egress', () => { captures.length = 0 const disposeProxy = await installProxyFromEnvironment(proxyEnv(), () => undefined) const ctx = new Context() + ctx.plugin(OTel) try { // The positive control proves a fetch-based exporter would reach the proxy. const response = await fetch(collectorUrl) diff --git a/packages/session/session-telemetry-otel/tests/loader-composition.e2e.ts b/packages/session/session-telemetry-otel/tests/loader-composition.e2e.ts index af28e1e892..3d0d01e64d 100644 --- a/packages/session/session-telemetry-otel/tests/loader-composition.e2e.ts +++ b/packages/session/session-telemetry-otel/tests/loader-composition.e2e.ts @@ -4,6 +4,7 @@ * only the redacted authorized prefix; the canonical log keeps every event. */ +import type { SessionEvent } from '@deepseek-ai/dsh-session' import { readFile, readdir } from 'node:fs/promises' import { join } from 'node:path' import { fileURLToPath } from 'node:url' @@ -24,6 +25,7 @@ const FIXTURE_SECRET = 'sk-e2efixture1234567890' const FIXTURE_PLACEHOLDER = '[E2E-REDACTED]' interface OtlpLogRecord { + eventName: string attributes?: { key: string; value: Record }[] body?: unknown } @@ -102,6 +104,17 @@ describe('session-telemetry-otel through the production headless profile', () => const records = allRecords(output.captures) expect(records.length).toBeGreaterThan(0) + for (const { record } of records) { + expect(record.eventName).toBe('session-log') + expect(record.attributes?.find(attribute => attribute.key === 'sessionId')?.value.stringValue).toBeTypeOf('string') + const content = record.attributes?.find(attribute => attribute.key === 'content')?.value.stringValue + expect(typeof content).toBe('string') + const event = JSON.parse(content as string) as SessionEvent + expect(event.type).toBeTypeOf('string') + expect(event.seq).toBeTypeOf('number') + expect(event.time).toBeTypeOf('number') + expect(event).toHaveProperty('data') + } const types = eventTypes(output.captures) for (const expected of ['turn/start', 'user/message', 'tool/call', 'tool/result', 'assistant/message', 'turn/end']) { diff --git a/packages/session/session-telemetry-otel/tests/otel.spec.ts b/packages/session/session-telemetry-otel/tests/otel.spec.ts index 72a8e5ef10..86082a026b 100644 --- a/packages/session/session-telemetry-otel/tests/otel.spec.ts +++ b/packages/session/session-telemetry-otel/tests/otel.spec.ts @@ -1,3 +1,4 @@ +import OTel from '@deepseek-ai/dsh-otel' /** * OTel backend unit tier: wire assertions against a scripted `node:http` * mock collector through the SDK's REAL pipeline (BatchLogRecordProcessor → @@ -102,6 +103,7 @@ async function mockCollector( async function boot(url: string) { const ctx = new Context() + ctx.plugin(OTel) await ctx.plugin(SessionStore) const fiber = await ctx.plugin(OpenTelemetrySessionBackend, { mode: SessionTelemetryMode.FEEDBACK_ONLY, @@ -187,58 +189,13 @@ describe('OpenTelemetrySessionBackend wire', () => { expect(end?.record.severityText).toBe('ERROR') const assistant = ledger.find(r => r.record.attributes?.some(a => a.key === 'event.type' && a.value.stringValue === 'assistant/message')) - const body = assistant?.record.body as { - kvlistValue: { values: { key: string; value: unknown }[] } + const content = assistant?.record.attributes?.find(a => a.key === 'content')?.value.stringValue + expect(typeof content).toBe('string') + expect(JSON.parse(content as string)).toEqual(session.snapshotEvents().find(event => event.type === 'assistant/message')) + for (const { record } of ledger) { + expect(record).toMatchObject({ eventName: 'session-log', body: { stringValue: 'session-log' } }) + expect(record.attributes).toContainEqual({ key: 'sessionId', value: { stringValue: session.id } }) } - expect(body.kvlistValue.values.find(value => value.key === 'stream')?.value).toEqual({ - arrayValue: { - values: [ - { - kvlistValue: { - values: [ - { key: 'type', value: { stringValue: 'text-chunks' } }, - { key: 'time0', value: { intValue: 1_000 } }, - { key: 'index', value: { intValue: 0 } }, - { key: 'dt', value: { arrayValue: { values: [{ intValue: 7 }] } } }, - { - key: 'texts', - value: { - arrayValue: { - values: [ - { stringValue: 'first complete chunk' }, - { stringValue: 'second complete chunk' }, - ], - }, - }, - }, - ], - }, - }, - { - kvlistValue: { - values: [ - { key: 'type', value: { stringValue: 'chunk' } }, - { key: 'time', value: { intValue: 1_007 } }, - { - key: 'chunk', - value: { - kvlistValue: { - values: [ - { key: 'type', value: { stringValue: 'finish' } }, - { - key: 'reason', - value: { kvlistValue: { values: [{ key: 'kind', value: { stringValue: 'stop' } }] } }, - }, - ], - }, - }, - }, - ], - }, - }, - ], - }, - }) expect(eventTypes(captures)).not.toContain('manual') expect(eventTypes(captures)).toEqual(session.snapshotEvents().map(event => event.type)) expect(ops).toHaveLength(0) @@ -255,6 +212,7 @@ describe('OpenTelemetrySessionBackend wire', () => { } }) const ctx = new Context() + ctx.plugin(OTel) await ctx.plugin(SessionStore) const fiber = await ctx.plugin(OpenTelemetrySessionBackend, { mode: SessionTelemetryMode.FEEDBACK_ONLY, @@ -280,7 +238,7 @@ describe('OpenTelemetrySessionBackend wire', () => { expect(allRecords(captures).some(r => r.scope.endsWith('/ops'))).toBe(false) }) - it('bounds the SDK forceFlush wait when an in-flight transport never settles', async () => { + it('stops queued sends at the shutdown deadline while observing the active transport', async () => { const gate = Promise.withResolvers() const arrived = Promise.withResolvers() const { url, captures } = await mockCollector(async (index) => { @@ -290,6 +248,7 @@ describe('OpenTelemetrySessionBackend wire', () => { } }) const ctx = new Context() + ctx.plugin(OTel) await ctx.plugin(SessionStore) const fiber = await ctx.plugin(OpenTelemetrySessionBackend, { mode: SessionTelemetryMode.FEEDBACK_ONLY, @@ -313,12 +272,14 @@ describe('OpenTelemetrySessionBackend wire', () => { // the real provider promise remains clean after the test has proved the // Cordis disposer no longer waits for it. gate.resolve(true) - await expect.poll(() => captures.length).toBeGreaterThanOrEqual(2) + await expect.poll(() => captures.length).toBe(1) + expect(JSON.stringify(captures)).not.toContain('second report') }) it('passes exporter options beyond url and headers through to the SDK exporter', async () => { const { url, captures } = await mockCollector() const ctx = new Context() + ctx.plugin(OTel) await ctx.plugin(SessionStore) // `compression` is a documented SDK exporter option; the advertised // verbatim passthrough must hand it (and every other field) to the @@ -360,6 +321,7 @@ describe('OpenTelemetrySessionBackend wire', () => { it('replays each session suffix only at the next feedback event', async () => { const { url, captures } = await mockCollector() const ctx = new Context() + ctx.plugin(OTel) await ctx.plugin(SessionStore) const fiber = await ctx.plugin(OpenTelemetrySessionBackend, { mode: SessionTelemetryMode.FEEDBACK_ONLY, @@ -396,6 +358,7 @@ describe('OpenTelemetrySessionBackend wire', () => { it('ignores direct emits and non-canonical feedback in feedback-only mode', async () => { const { url, captures } = await mockCollector() const ctx = new Context() + ctx.plugin(OTel) await ctx.plugin(SessionStore) const warn = vi.spyOn(ctx.logger, 'warn').mockImplementation(() => {}) const fiber = await ctx.plugin(OpenTelemetrySessionBackend, { @@ -425,6 +388,7 @@ describe('OpenTelemetrySessionBackend wire', () => { it('constructs no disabled transport even when exporter options are present', async () => { const { url, captures } = await mockCollector() const ctx = new Context() + ctx.plugin(OTel) await ctx.plugin(SessionStore) const warn = vi.spyOn(ctx.logger, 'warn').mockImplementation(() => {}) const fiber = await ctx.plugin(OpenTelemetrySessionBackend, { @@ -458,18 +422,24 @@ describe('OpenTelemetrySessionBackend wire', () => { const { url, captures } = await mockCollector() const gatedCtx = new Context() + + gatedCtx.plugin(OTel) await gatedCtx.plugin(SessionStore) const gated = await gatedCtx.plugin(OpenTelemetrySessionBackend, { mode: SessionTelemetryMode.FEEDBACK_ONLY, exporter: { url } }) expect(gatedCtx.sessionTelemetry.sharing).toBe('feedback-only') await gated.dispose() const disabledCtx = new Context() + + disabledCtx.plugin(OTel) await disabledCtx.plugin(SessionStore) const disabled = await disabledCtx.plugin(OpenTelemetrySessionBackend, { mode: SessionTelemetryMode.DISABLED }) expect(disabledCtx.sessionTelemetry.sharing).toBe('disabled') await disabled.dispose() const defaultCtx = new Context() + + defaultCtx.plugin(OTel) await defaultCtx.plugin(SessionStore) const defaulted = await defaultCtx.plugin(OpenTelemetrySessionBackend, { exporter: { url } }) expect(defaultCtx.sessionTelemetry.sharing).toBe('feedback-only') @@ -482,6 +452,7 @@ describe('OpenTelemetrySessionBackend wire', () => { it('defaults direct construction to feedback-only delivery', async () => { const { url, captures } = await mockCollector() const ctx = new Context() + ctx.plugin(OTel) try { await ctx.plugin(SessionStore) new OpenTelemetrySessionBackend(ctx, { @@ -508,6 +479,7 @@ describe('OpenTelemetrySessionBackend route and feedback', () => { it.each(['deepseek-official', 'mock', undefined])('uploads new text feedback for %s while the host stays alive', async (provider) => { const { url, captures } = await mockCollector() const ctx = new Context() + ctx.plugin(OTel) try { await ctx.plugin(SessionStore) await ctx.plugin(OpenTelemetrySessionBackend, { @@ -532,6 +504,7 @@ describe('OpenTelemetrySessionBackend route and feedback', () => { it('does not upload ordinary events, inherited feedback, new/open/resume/fork or HMR', async () => { const { url, captures } = await mockCollector() const ctx = new Context() + ctx.plugin(OTel) await ctx.plugin(SessionStore) const donor = Session.create(SessionId('stored-feedback')) recordFeedback(donor, { text: 'old feedback is not a submission' }) @@ -561,6 +534,7 @@ describe('OpenTelemetrySessionBackend route and feedback', () => { const { url, captures } = await mockCollector() const root = mkdtempSync(join(tmpdir(), 'dsh-otel-live-')) const ctx = new Context() + ctx.plugin(OTel) try { await ctx.plugin(SessionStore) await ctx.plugin(JsonlPersistence, { root, compression: 'none' }) @@ -637,6 +611,7 @@ describe('OpenTelemetrySessionBackend route and feedback', () => { const { url, captures } = await mockCollector() const root = mkdtempSync(join(tmpdir(), 'dsh-otel-cold-fork-')) const ctx = new Context() + ctx.plugin(OTel) try { await ctx.plugin(SessionStore) await ctx.plugin(JsonlPersistence, { root, compression: 'none' }) @@ -692,6 +667,7 @@ describe('OpenTelemetrySessionBackend route and feedback', () => { const { url, captures } = await mockCollector() const root = mkdtempSync(join(tmpdir(), 'dsh-otel-cold-')) const ctx = new Context() + ctx.plugin(OTel) const warn = vi.spyOn(ctx.logger, 'warn').mockImplementation(() => {}) try { await ctx.plugin(SessionStore) @@ -775,12 +751,14 @@ describe('OpenTelemetrySessionBackend config fails loud', () => { [{ mode: SessionTelemetryMode.FEEDBACK_ONLY, exporter: { url: 'http://c/v1/logs' }, shutdownTimeoutMillis: Number.POSITIVE_INFINITY }, /shutdownTimeoutMillis/], ])('rejects %j at plugin load', async (config, message) => { const ctx = new Context() + ctx.plugin(OTel) await ctx.plugin(SessionStore) await expect(ctx.plugin(OpenTelemetrySessionBackend, config as Config)).rejects.toThrow(message) }) it.each(['INVALID', 'FULL'])('rejects direct mode %s before reading transport config', async (mode) => { const ctx = new Context() + ctx.plugin(OTel) await ctx.plugin(SessionStore) let exporterRead = false const config = { @@ -797,6 +775,7 @@ describe('OpenTelemetrySessionBackend config fails loud', () => { it('does not read any transport setting in disabled mode', async () => { const ctx = new Context() + ctx.plugin(OTel) await ctx.plugin(SessionStore) const transportRead = vi.fn(() => { throw new Error('transport config was read') @@ -826,7 +805,7 @@ describe('dsh-session-telemetry-otel real-load-path guard', () => { const loader = Object.create(Loader.prototype) as Loader const unwrapped = loader.unwrapExports(module) as typeof OpenTelemetrySessionBackend expect(unwrapped).toBe(OpenTelemetrySessionBackend) - expect(unwrapped.inject).toEqual(['sessions']) + expect(unwrapped.inject).toEqual(['sessions', 'otel']) expect(typeof unwrapped.Config).toBe('function') }) @@ -836,9 +815,48 @@ describe('dsh-session-telemetry-otel real-load-path guard', () => { const loader = Object.create(Loader.prototype) as Loader const unwrapped = loader.unwrapExports(module) as Parameters[0] const ctx = new Context() + ctx.plugin(OTel) await ctx.plugin(SessionStore) const fiber = await ctx.plugin(unwrapped, { mode: SessionTelemetryMode.FEEDBACK_ONLY, exporter: { url } }) expect(ctx.sessionTelemetry).toBeInstanceOf(OpenTelemetrySessionBackend) await fiber.dispose() }) }) + +it('reports oversized records without failing feedback or exporting their contents', async () => { + const { url, captures } = await mockCollector() + const ctx = new Context() + ctx.plugin(OTel) + try { + await ctx.plugin(SessionStore) + const warn = vi.spyOn(ctx.logger, 'warn') + const fiber = await ctx.plugin(OpenTelemetrySessionBackend, { + mode: SessionTelemetryMode.FEEDBACK_ONLY, exporter: { url }, maxRequestBytes: 1, + }) + const session = ctx.sessions.create(SessionId('oversized')) + expect(() => { recordFeedback(session, { text: 'feedback remains local' }) }).not.toThrow() + await fiber.dispose() + expect(captures).toEqual([]) + expect(warn).toHaveBeenCalledWith('Session log record rejected; content was not truncated', expect.any(Error)) + } finally { + await ctx.fiber.dispose() + } +}) + +it('does not upload a record whose redaction policy removes its canonical envelope', async () => { + const { url, captures } = await mockCollector() + const { ctx, fiber } = await boot(url) + const warn = vi.spyOn(ctx.logger, 'warn') + try { + ctx.on('session-telemetry/record', (_record, next) => { + const { sourceEvent: _sourceEvent, ...redacted } = next() + return redacted + }) + recordFeedback(ctx.sessions.create(SessionId('withheld-envelope')), { text: 'withheld' }) + await fiber.dispose() + expect(captures).toEqual([]) + expect(warn).toHaveBeenCalledWith('Session log record withheld: redaction removed sourceEvent') + } finally { + await ctx.fiber.dispose() + } +}) diff --git a/packages/session/session-telemetry-otel/tsconfig.json b/packages/session/session-telemetry-otel/tsconfig.json index 87f43bdc9a..08f78d0e6c 100644 --- a/packages/session/session-telemetry-otel/tsconfig.json +++ b/packages/session/session-telemetry-otel/tsconfig.json @@ -34,6 +34,9 @@ }, { "path": "../../identity/anonymous-user-id" + }, + { + "path": "../../telemetry/otel" } ] } diff --git a/packages/session/session-telemetry/README.i18n.yaml b/packages/session/session-telemetry/README.i18n.yaml index 894c36f1e5..dda64730e1 100644 --- a/packages/session/session-telemetry/README.i18n.yaml +++ b/packages/session/session-telemetry/README.i18n.yaml @@ -24,8 +24,8 @@ en: e3cc8ee7774a0ad5 zh: 3c039339870b8cf9 /deepseek-ai-dsh-session-telemetry/use-this-package/what-gets-captured: - en: ed96f8ed3cf72fe8 - zh: b7f2b546b8639f43 + en: 5b7fbbe1c55997e8 + zh: d62d9118fd8df2f4 /deepseek-ai-dsh-session-telemetry/use-this-package/the-sharing-disclosure: en: 5c325a8aaadf17c7 zh: 5c493d2333f82b28 @@ -36,8 +36,8 @@ en: cad1e9d195916553 zh: 2192f3ce5e2196bd /deepseek-ai-dsh-session-telemetry/understand-the-implementation/design-concept: - en: 3bac6e75e9e442eb - zh: 88062e9d93a782eb + en: 56c449be11e6bac1 + zh: 6b62969e93313c3e /deepseek-ai-dsh-session-telemetry/understand-the-implementation/source-map: en: 5dc345ec3dda38b8 zh: 189d673734a2c20e diff --git a/packages/session/session-telemetry/README.md b/packages/session/session-telemetry/README.md index c7d7668641..14d697b93c 100644 --- a/packages/session/session-telemetry/README.md +++ b/packages/session/session-telemetry/README.md @@ -39,6 +39,8 @@ A backend implements three members: `emit(record)` must be a non-blocking enqueu Capture runs in one of two modes. `live` capture follows session events as they are appended, replays already-live sessions at mount time, and records lifecycle markers; `on-demand` capture reads the canonical session log only when the backend requests a prefix through `captureSession(session, throughSeq?)`. Coordinator options select whether stored history is included. Every canonical session event maps to one ledger record in order. An `assistant/message` or `assistant/attempt` record carries its complete embedded compact stream, including failed and retried output. Each ledger record also carries `session.id`, `session.format_version`, the numeric event identity, optional header facts, and a pre-mapped severity (`error` for `tool/result.isError`, `turn/end` error reasons, and `agent-error`; `info` otherwise). +Ledger records carry `sourceEvent` with the owning Session id and a copied event envelope excluding `data`. The redaction waterfall receives `body` as the only payload copy. Backends can reconstruct a complete exported event from the envelope and redacted body without bypassing redaction. + ### The sharing disclosure @@ -63,7 +65,7 @@ This section explains the capture design; the observable behavior is fully cover ### Design concept -The seam is built on one boundary: the harness's aspect ends at `emit()`. Complete event capture, redaction, and the handoff cursor live here; batching, retry, queueing, and loss policy are the reporting SDK's, deliberately not modelled or wrapped. The design and rejected alternatives are pinned in the [revival Agent Note](../../../.agents/notes/implemented/feature/2026-07-23-session-telemetry-otel-revival.md). +The capture package owns complete event capture, redaction, and handoff cursors. Redaction rules must preserve `sourceEvent` for OTel upload; returning a fresh record without it withholds the event with a diagnostic. Its cloned envelope excludes `data`, which is carried only in `body`. The OTel backend owns byte/count scheduling and uses SDK transport/retries. The [revival Agent Note](../../../.agents/notes/implemented/feature/2026-07-23-session-telemetry-otel-revival.md) owns capture and redaction rationale. ### Source map diff --git a/packages/session/session-telemetry/README.zh.md b/packages/session/session-telemetry/README.zh.md index 2e9bc7e661..5ac9405d53 100644 --- a/packages/session/session-telemetry/README.zh.md +++ b/packages/session/session-telemetry/README.zh.md @@ -39,6 +39,8 @@ kind: "package-library" 捕获以两种模式之一运行。`live` 捕获在追加时跟随会话事件、在挂载时回放已存活会话并记录生命周期标记;`on-demand` 捕获只在后端通过 `captureSession(session, throughSeq?)` 请求前缀时读取权威会话日志。协调器选项决定是否包含存储历史。每条权威会话事件都按顺序映射为一条 ledger 记录。`assistant/message` 或 `assistant/attempt` 记录会携带完整的嵌入式紧凑流,包括失败和重试输出。每条 ledger 记录还携带 `session.id`、`session.format_version`、数值型事件标识、可选 header 事实与预先映射的严重级别(`tool/result.isError`、`turn/end` 的错误原因与 `agent-error` 映射为 `error`;其余为 `info`)。 +ledger 记录携带 `sourceEvent`,其中包含所属 Session id 和不含 `data` 的事件信封副本。脱敏 waterfall 接收的 `body` 是唯一的载荷副本。后端可从信封和脱敏后的 body 重建完整导出事件,而不绕过脱敏。 + ### 共享披露 @@ -63,7 +65,7 @@ kind: "package-library" ### 设计理念 -seam 建立在一个边界之上:harness 的职责止于 `emit()`。完整事件捕获、脱敏与 handoff 游标都在这里;批处理、重试、排队与丢失策略属于上报 SDK,本包有意不建模也不包装。设计与被否决的替代方案见[复活 Agent Note](../../../.agents/notes/implemented/feature/2026-07-23-session-telemetry-otel-revival.zh.md)。 +捕获包负责完整事件捕获、脱敏和交接游标。脱敏规则必须保留 `sourceEvent` 才能通过 OTel 上传;返回不含它的新记录会阻止该事件上传并产生诊断。复制的 envelope 不含 `data`,数据仅由 `body` 携带。OTel 后端负责字节/条数调度并使用 SDK 传输和重试。[恢复遥测决策记录](../../../.agents/notes/implemented/feature/2026-07-23-session-telemetry-otel-revival.zh.md) 说明捕获和脱敏依据。 ### 源码地图 diff --git a/packages/session/session-telemetry/src/coordinator.ts b/packages/session/session-telemetry/src/coordinator.ts index 7f3b5d6052..fa21a5ae41 100644 --- a/packages/session/session-telemetry/src/coordinator.ts +++ b/packages/session/session-telemetry/src/coordinator.ts @@ -178,15 +178,17 @@ export class SessionTelemetryCoordinator { /** Copy, redact, and hand one canonical event to the backend. */ private captureEvent(session: Session, event: SessionEvent): void { + const { data, ...envelope } = event this.deliver(session, { record: this.redact({ + sourceEvent: { sessionId: session.id, envelope: structuredClone(envelope) }, channel: 'ledger', time: event.time, severity: severityOf(event), attributes: identityOf(session, event), // The canonical event object is mutable and the backend serializes // later; append-time validation guarantees this clone cannot throw. - body: structuredClone(event.data), + body: structuredClone(data), }), seq: event.seq, }) diff --git a/packages/session/session-telemetry/src/index.ts b/packages/session/session-telemetry/src/index.ts index 392df907f7..54347e4a16 100644 --- a/packages/session/session-telemetry/src/index.ts +++ b/packages/session/session-telemetry/src/index.ts @@ -7,7 +7,7 @@ * forwarding), live versus on-demand canonical-log capture, and the HMR * cursor. Everything downstream of * {@link SessionTelemetryBackend.emit} — batching, retry, queueing, and loss policy — is the - * reporting SDK's territory and is deliberately not modelled here. The + * backend's responsibility and is deliberately not modelled here. The * design and its trade-offs are pinned in * .agents/notes/implemented/feature/2026-07-23-session-telemetry-otel-revival.md. * @@ -15,6 +15,7 @@ */ import { Context, Service } from '@deepseek-ai/cordis' +import type { SessionEvent, SessionId } from '@deepseek-ai/dsh-session' declare module '@deepseek-ai/cordis' { interface Context { @@ -62,6 +63,8 @@ export type SessionTelemetrySeverity = 'info' | 'warn' | 'error' * identity so they can never be mistaken for ledger rows. */ export interface SessionTelemetryRecord { + /** Canonical envelope without data; body carries the separately redacted payload. Absent for operational records. */ + sourceEvent?: { sessionId: SessionId; envelope: Omit } /** Ledger (session-log mirror) or ops (operational signal) channel; backends keep the two under separate instrumentation scopes. */ channel: 'ledger' | 'ops' /** Unix epoch milliseconds — the source event's append time for ledger records, the emission time for ops records. */ diff --git a/packages/session/session-telemetry/tests/telemetry.spec.ts b/packages/session/session-telemetry/tests/telemetry.spec.ts index 2d8c02ed64..aa02cda863 100644 --- a/packages/session/session-telemetry/tests/telemetry.spec.ts +++ b/packages/session/session-telemetry/tests/telemetry.spec.ts @@ -135,6 +135,12 @@ describe('SessionTelemetryCoordinator capture', () => { ;(message.body as { content: { text: string }[] }).content[0]!.text = 'tampered' const logged = session.snapshotEvents()[1] as SessionEvent<'user/message'> expect(logged.data.content[0]).toMatchObject({ text: 'hello' }) + expect(message.sourceEvent?.sessionId).toBe(session.id) + expect(message.sourceEvent?.envelope).not.toHaveProperty('data') + expect(message.sourceEvent?.envelope).toMatchObject({ seq: logged.seq, time: logged.time, type: logged.type, surfaceOp: 'append' }) + expect(message.sourceEvent?.envelope).not.toBe(logged) + message.sourceEvent!.envelope.time = 0 + expect(logged.time).not.toBe(0) }) it('captures a live request header without replaying previously withheld events', async () => { diff --git a/packages/telemetry/README.i18n.yaml b/packages/telemetry/README.i18n.yaml new file mode 100644 index 0000000000..8febf5c4f2 --- /dev/null +++ b/packages/telemetry/README.i18n.yaml @@ -0,0 +1,25 @@ +# Bilingual-pair consistency record for README.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 packages/telemetry/README.md +/: + en: 53661eb6551dbf31 + zh: 8cc1a84f4cb8a69f +/telemetry-shared-reporting: + en: 0c5ec106d3250ce7 + zh: ab9e19ea56f1f75f +/telemetry-shared-reporting/summary: + en: 9dbe398017c89288 + zh: 91b8c2de8176de4f +/telemetry-shared-reporting/table-of-contents: + en: 09848f4d056e4fc1 + zh: 8df6139cff0288f6 +/telemetry-shared-reporting/packages: + en: 0bc9ee219231fa9a + zh: 873150749266f259 +/telemetry-shared-reporting/related-documentation: + en: f550e93c6228156b + zh: 7414955502825abd +/telemetry-shared-reporting/dev-note: + en: b75c8458928b72b9 + zh: 003ba4e17b4b5269 diff --git a/packages/telemetry/README.md b/packages/telemetry/README.md new file mode 100644 index 0000000000..e77964022e --- /dev/null +++ b/packages/telemetry/README.md @@ -0,0 +1,35 @@ +--- +description: "Shared telemetry transport plugins for business consumers." +kind: "package-group" +--- + +# telemetry/ — shared reporting + +English | [中文](README.zh.md) + +## Summary + +This group provides reporting infrastructure shared by product analytics and feedback-authorized Session uploads. Business plugins select events, identities, authorization, and redaction; transport plugins own encoding and delivery. + +## Table of Contents + +- [Packages](#packages) +- [Related documentation](#related-documentation) +- [Dev Note](#dev-note) + + +## Packages + +| Package | Role | +|---|---| +| [`otel`](otel/README.md) | Cordis service creating independent ordinary-event and byte-bounded Session-log channels | + + +## Related documentation + +- [OTel subsystem](../../docs/subsystems/otel.md) — shared service and consumer ownership. + + +## Dev Note + +None. diff --git a/packages/telemetry/README.zh.md b/packages/telemetry/README.zh.md new file mode 100644 index 0000000000..61e3ecd3df --- /dev/null +++ b/packages/telemetry/README.zh.md @@ -0,0 +1,35 @@ +--- +description: "供业务调用方共享的遥测传输插件。" +kind: "package-group" +--- + +# telemetry/ — 共享上报 + +[English](README.md) | 中文 + +## 概述 + +本组提供产品埋点和反馈授权 Session 上传共用的上报基础设施。业务插件选择事件、身份、授权和脱敏规则;传输插件负责编码和投递。 + +## 目录 + +- [包](#packages) +- [相关文档](#related-documentation) +- [开发备注](#dev-note) + + +## 包 + +| 包 | 职责 | +|---|---| +| [`otel`](otel/README.zh.md) | 创建独立普通事件通道和受字节上限约束的 Session 日志通道的 Cordis 服务 | + + +## 相关文档 + +- [OTel 子系统](../../docs/subsystems/otel.zh.md) — 共享服务与调用方职责。 + + +## 开发备注 + +无。 diff --git a/packages/telemetry/otel/README.i18n.yaml b/packages/telemetry/otel/README.i18n.yaml new file mode 100644 index 0000000000..3eee76e8c9 --- /dev/null +++ b/packages/telemetry/otel/README.i18n.yaml @@ -0,0 +1,37 @@ +# Bilingual-pair consistency record for README.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 packages/telemetry/otel/README.md +/: + en: 49f35d8bc0fe3689 + zh: ef2fd903cca7d129 +/deepseek-ai-dsh-otel: + en: 925bc4650e9895d6 + zh: 02f8917c4048f6e2 +/deepseek-ai-dsh-otel/summary: + en: e32c122f34c04593 + zh: bea48183ab438454 +/deepseek-ai-dsh-otel/table-of-contents: + en: 23386452a66b2fbd + zh: 55b2121ae6059425 +/deepseek-ai-dsh-otel/use-this-package: + en: a295efbb5b488489 + zh: 4baa7f0f78ce862d +/deepseek-ai-dsh-otel/understand-the-implementation: + en: 79e1c09af1c84c06 + zh: 10bf2f50a59c01f6 +/deepseek-ai-dsh-otel/further-exploration: + en: ee17b991de098cc0 + zh: f90257e411921f2b +/deepseek-ai-dsh-otel/model-experience: + en: 5b364b9451ced69f + zh: 025feb91ef96bdb8 +/deepseek-ai-dsh-otel/model-experience/kv-cache-effect: + en: 8be9251f0d5256ed + zh: d46143a02855c8bc +/deepseek-ai-dsh-otel/known-limitations-and-deferred-work: + en: 5c3e29312d45ee52 + zh: b148e3a6decc0f7d +/deepseek-ai-dsh-otel/known-limitations-and-deferred-work/dev-note: + en: 0a1253c6f7b5e8b9 + zh: 9f56528562c7c914 diff --git a/packages/telemetry/otel/README.md b/packages/telemetry/otel/README.md new file mode 100644 index 0000000000..2f8bc0fe43 --- /dev/null +++ b/packages/telemetry/otel/README.md @@ -0,0 +1,70 @@ +--- +description: "Create independent OTLP reporting channels through the shared Cordis OTel service." +kind: "package-reference" +--- + +# @deepseek-ai/dsh-otel + +English | [中文](README.zh.md) + +## Summary + +Mount one `otel` service to create ordinary-event and Session-log reporting channels. Each channel has its own exporter, resource, instrumentation scope, and queue. Mounting alone creates no transport or identity and sends nothing. Business consumers own authorization, redaction, field selection, and their channel's disposal. + +## Table of Contents + +- [Use this package](#use-this-package) +- [Understand the implementation](#understand-the-implementation) +- [Further Exploration](#further-exploration) +- [Model Experience](#model-experience) +- [Known Limitations and Deferred Work](#known-limitations-and-deferred-work) +- [Dev Note](#dev-note) + + +## Use this package + +The base bundle mounts `@deepseek-ai/dsh-otel`. Independent compositions must mount it before consumers that inject `otel`. Call `ctx.otel.createEventReporter(options)` for ordinary analytics or `ctx.otel.createSessionLogReporter(options)` for complete Session events. Options explicitly supply endpoint, scope, resource attributes, queue settings, and a diagnostic callback; the service has no deployment defaults or automatic collection policy of its own. + +The returned channel belongs to the consumer. Register shutdown with the consumer's Cordis fiber and bound the complete drain with its configured deadline. Injection makes service replacement unload dependent consumers. Do not retain a channel after its consumer unloads. The product and Session adapters implement this ownership for UI and feedback callers. + +Ordinary channels use SDK count-based batching. Session channels preserve one complete event per record with `eventName: "session-log"`, `sessionId`, and a JSON string `content`, and enforce at most 4,000,000 uncompressed request bytes. Their byte limits, single-record rejection, serial transport settlement, and shutdown behavior are specified by the [Session adapter](../../session/session-telemetry-otel/README.md). A Session channel never shares a request or queue with ordinary events. + +Headers are explicit. The service does not add a channel header or inherit ambient authorization headers or TLS identity. Agent factories own their returned agents, including keepAlive. Scope names and versions are supplied by each consumer, so transport sharing does not change event attribution. + + +## Understand the implementation + +`src/index.ts` registers the service; `event-log.ts` owns ordinary SDK batching; `session-log.ts` owns byte/count scheduling; `transport.ts` supplies the SDK JSON HTTP delegate. No global OTel provider is installed. The Session processor measures each record once, groups conservative sizes, and waits for the SDK concurrency queue to clear after every callback before sending another request. + +No runtime invariant companion is published: channel creation registers no independent domain state, and collector delivery cannot be inferred from local queues. Composition tests exercise independent channels and service removal; adapter tests cover dependent-fiber cleanup and feedback authorization. + + +## Further Exploration + +- [OTel subsystem](../../../docs/subsystems/otel.md) — service ownership and API. +- [Product adapter](../../host/product-telemetry-otel/README.md) — explicit analytics policy and configuration. +- [Session adapter](../../session/session-telemetry-otel/README.md) — feedback authorization and configured upload limits. + + +## Model Experience + +None, as the service delivers caller-selected records without changing model context. + +#### KV Cache effect + +None; reporting does not change model requests. + +## Known Limitations and Deferred Work + + + +Channels provide best-effort, memory-only delivery with the following limits. + +- The caller owns consent, redaction, resource identity, renderer-to-host RPC, and the outer shutdown deadline. +- Queue overflow, oversized Session events, transport failure, and process exit can lose records. There is no durable outbox or warehouse acknowledgement. +- Ordinary-event channels batch by count; their consumers must select event and batch sizes suitable for the collector. + + +### Dev Note + +None. diff --git a/packages/telemetry/otel/README.zh.md b/packages/telemetry/otel/README.zh.md new file mode 100644 index 0000000000..111f30fc4b --- /dev/null +++ b/packages/telemetry/otel/README.zh.md @@ -0,0 +1,70 @@ +--- +description: "通过共享 Cordis OTel 服务创建独立的 OTLP 上报通道。" +kind: "package-reference" +--- + +# @deepseek-ai/dsh-otel + +[English](README.md) | 中文 + +## 概述 + +挂载一个 `otel` 服务即可创建普通事件和 Session 日志上报通道。每个通道拥有独立的 exporter、resource、instrumentation scope 和队列。仅挂载不会创建传输、身份或发送数据。业务调用方负责授权、脱敏、字段选择及通道销毁。 + +## 目录 + +- [使用本包](#use-this-package) +- [理解实现](#understand-the-implementation) +- [进一步探索](#further-exploration) +- [模型体验](#model-experience) +- [已知限制与后续工作](#known-limitations-and-deferred-work) +- [开发备注](#dev-note) + + +## 使用本包 + +base bundle 挂载 `@deepseek-ai/dsh-otel`。独立组合必须在注入 `otel` 的调用方之前挂载它。普通埋点调用 `ctx.otel.createEventReporter(options)`,完整 Session 事件调用 `ctx.otel.createSessionLogReporter(options)`。选项显式提供 endpoint、scope、resource attributes、队列设置和诊断回调;服务本身没有部署默认值或自动采集策略。 + +返回的通道由调用方持有。将关闭注册到调用方的 Cordis fiber,并用配置的期限约束完整清空过程。注入机制使服务替换时卸载依赖方。调用方卸载后不得继续保留通道。产品和 Session 适配器为 UI 和反馈调用方实现这些职责。 + +普通通道使用 SDK 按条数聚合。Session 通道将每条完整事件保留为一个记录,包含 `eventName: "session-log"`、`sessionId` 和 JSON 字符串 `content`,并保证未压缩请求不超过 4,000,000 字节。字节限制、单条拒绝、串行传输结束和关闭行为见 [Session 适配器](../../session/session-telemetry-otel/README.zh.md)。Session 通道不会与普通事件共享请求或队列。 + +Header 显式提供。服务不会附加 channel header,也不继承环境中的授权 header 或 TLS 身份。Agent factory 负责返回的 agent,包括 keepAlive。每个调用方提供 scope 名称和版本,因此共享传输不会改变事件归属。 + + +## 理解实现 + +`src/index.ts` 注册服务;`event-log.ts` 负责普通事件的 SDK 聚合;`session-log.ts` 负责字节和条数调度;`transport.ts` 提供 SDK JSON HTTP delegate。不安装全局 OTel provider。Session processor 对每条记录只计量一次,按保守大小组包,并在每次回调后等待 SDK 并发队列清理完成才发送下一请求。 + +不发布运行时不变式伴生入口:通道创建不注册独立领域状态,无法从本地队列推断 collector 是否收到记录。组合测试覆盖独立通道和服务移除;适配器测试覆盖依赖 fiber 清理与反馈授权。 + + +## 进一步探索 + +- [OTel 子系统](../../../docs/subsystems/otel.zh.md) — 服务职责与 API。 +- [产品适配器](../../host/product-telemetry-otel/README.zh.md) — 显式埋点策略与配置。 +- [Session 适配器](../../session/session-telemetry-otel/README.zh.md) — 反馈授权和上传限制配置。 + + +## 模型体验 + +无,因为服务只投递调用方选择的记录,不改变模型上下文。 + +#### KV Cache 影响 + +无;上报不改变模型请求。 + +## 已知限制与后续工作 + + + +通道提供尽力而为的纯内存投递,具有以下限制。 + +- 调用方负责同意、脱敏、resource 身份、renderer 到 host 的 RPC 及外层关闭期限。 +- 队列溢出、Session 事件超限、传输失败和进程退出可能丢失记录。不提供持久 outbox 或仓库确认。 +- 普通事件通道按条数聚合;调用方必须选择适合 collector 的事件和批次大小。 + + +### 开发备注 + +无。 diff --git a/packages/telemetry/otel/package.json b/packages/telemetry/otel/package.json new file mode 100644 index 0000000000..64d065bd62 --- /dev/null +++ b/packages/telemetry/otel/package.json @@ -0,0 +1,46 @@ +{ + "name": "@deepseek-ai/dsh-otel", + "description": "Cordis service for independent ordinary-event and byte-bounded Session-log OTLP channels", + "version": "0.1.7-rc.2", + "publishConfig": { + "access": "public" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/deepseek-ai/deepseek-harness.git", + "directory": "packages/telemetry/otel" + }, + "type": "module", + "main": "lib/index.js", + "types": "lib/types/index.d.ts", + "exports": { + ".": { + "types": "./lib/types/index.d.ts", + "default": "./lib/index.js" + }, + "./src/*": "./src/*", + "./package.json": "./package.json" + }, + "files": [ + "lib/index.js", + "lib/types/**/*.d.ts" + ], + "license": "MIT", + "dependencies": { + "@opentelemetry/api": "^1.9.1", + "@opentelemetry/api-logs": "^0.220.0", + "@opentelemetry/otlp-exporter-base": "^0.220.0", + "@opentelemetry/resources": "^2.9.0", + "@opentelemetry/sdk-logs": "^0.220.0", + "@opentelemetry/core": "2.9.0", + "@opentelemetry/otlp-transformer": "^0.220.0" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "workspace:~", + "@deepseek-ai/dsh-session": "workspace:*" + }, + "devDependencies": { + "@deepseek-ai/cordis": "workspace:~", + "@deepseek-ai/dsh-session": "workspace:*" + } +} diff --git a/packages/telemetry/otel/src/event-log.ts b/packages/telemetry/otel/src/event-log.ts new file mode 100644 index 0000000000..4f57f5fb1e --- /dev/null +++ b/packages/telemetry/otel/src/event-log.ts @@ -0,0 +1,77 @@ +/** Ordinary event batching over an explicit SDK HTTP transport. */ +import type { Attributes } from '@opentelemetry/api' +import { SeverityNumber, type Logger } from '@opentelemetry/api-logs' +import { ExportResultCode } from '@opentelemetry/core' +import { resourceFromAttributes } from '@opentelemetry/resources' +import { BatchLogRecordProcessor, LoggerProvider, type BatchLogRecordProcessorOptions } from '@opentelemetry/sdk-logs' +import type { SessionLogOptions } from './session-log.ts' +import { createLogExporter } from './transport.ts' + +/** Scalar values accepted by the collector's Arrow attributes map. */ +export type OTelEventScalar = string | number | boolean + +/** Explicitly selected analytics fields; object values may contain scalars only. */ +export interface OTelEventRecord { + /** Product/DA-owned event name. */ + eventName: string + /** Human-readable summary; never a prompt, response, credential, or file contents. */ + body: string + /** Event occurrence time in Unix milliseconds. Observation time is assigned on enqueue. */ + timestamp: number + /** OTel severity; omitted values use INFO. */ + severityNumber?: SeverityNumber + /** Business fields selected by the caller; no automatic device or account identity. */ + attributes?: Record> +} + +/** Ordinary-event transport, resource, scope, and count-batching options. */ +export interface EventLogOptions { + exporter: SessionLogOptions['exporter'] + resourceAttributes: Attributes + scope: { name: string; version?: string } + processor: Omit + onFailure: SessionLogOptions['onFailure'] +} + +/** One caller-owned ordinary-event queue, independent of every Session-log queue. */ +export class EventLogReporter { + private readonly provider: LoggerProvider + private readonly logger: Logger + + /** @param options - explicit transport, resource, scope, queue, and diagnostic settings. */ + constructor(options: EventLogOptions) { + const exporter = createLogExporter(options.exporter) + this.provider = new LoggerProvider({ + resource: resourceFromAttributes(options.resourceAttributes), + processors: [new BatchLogRecordProcessor({ + ...options.processor, + exporter: { + export: (records, callback) => { + exporter.export(records, (result) => { + if (result.code !== ExportResultCode.SUCCESS) options.onFailure('Product telemetry export failed', result.error) + callback(result) + }) + }, + forceFlush: () => exporter.forceFlush(), + shutdown: () => exporter.shutdown(), + }, + })], + }) + this.logger = this.provider.getLogger(options.scope.name, options.scope.version) + } + + /** + * Enqueue caller-selected analytics fields without acknowledging delivery. + * @param record - the ordinary event to report. + */ + emit(record: OTelEventRecord): void { + const severityNumber = record.severityNumber ?? SeverityNumber.INFO + this.logger.emit({ ...record, observedTimestamp: Date.now(), severityNumber, severityText: SeverityNumber[severityNumber] }) + } + + /** + * Drain the queue and release its SDK transport. + * @returns completion of SDK shutdown; callers own their outer deadline. + */ + shutdown(): Promise { return this.provider.shutdown() } +} diff --git a/packages/telemetry/otel/src/index.ts b/packages/telemetry/otel/src/index.ts new file mode 100644 index 0000000000..607b0c2908 --- /dev/null +++ b/packages/telemetry/otel/src/index.ts @@ -0,0 +1,34 @@ +/** Cordis entry for independent ordinary-event and Session-log OTLP channels. */ +import { Context, Service } from '@deepseek-ai/cordis' +import { EventLogReporter, type EventLogOptions } from './event-log.ts' +import { SessionLogReporter, type SessionLogOptions } from './session-log.ts' + +declare module '@deepseek-ai/cordis' { + interface Context { + otel: OTel + } +} + +/** Shared transport provider. Mounting creates no queue, identity, or network connection. */ +export default class OTel extends Service { + constructor(ctx: Context) { super(ctx, 'otel') } + + /** + * Create an independent ordinary-event channel with count-based batching. + * The injected consumer must drain it during its fiber disposal. + * @param options - transport, scope, resource, queue, and diagnostic settings selected by the consumer. + * @returns the caller-owned channel; no state is shared with other channels. + */ + createEventReporter(options: EventLogOptions): EventLogReporter { return new EventLogReporter(options) } + + /** + * Create an independent byte-bounded Session-log channel. + * Authorization and redaction precede reporting; the consumer owns shutdown and its outer deadline. + * @param options - transport, scope, resource, queue, and diagnostic settings selected by the consumer. + * @returns the caller-owned channel, preserving complete accepted events within the request byte ceiling. + */ + createSessionLogReporter(options: SessionLogOptions): SessionLogReporter { return new SessionLogReporter(options) } +} + +export type { EventLogOptions, OTelEventRecord, OTelEventScalar, EventLogReporter } from './event-log.ts' +export type { SessionLogRecord, SessionLogOptions, SessionLogReporter } from './session-log.ts' diff --git a/packages/telemetry/otel/src/session-log.ts b/packages/telemetry/otel/src/session-log.ts new file mode 100644 index 0000000000..59ee612d29 --- /dev/null +++ b/packages/telemetry/otel/src/session-log.ts @@ -0,0 +1,224 @@ +/** Session-log records in an independent byte-bounded OTLP queue. */ +import type { SessionEvent, SessionId } from '@deepseek-ai/dsh-session' +import type { Attributes } from '@opentelemetry/api' +import { SeverityNumber } from '@opentelemetry/api-logs' +import { ExportResultCode } from '@opentelemetry/core' +import type { OTLPExporterNodeConfigBase } from '@opentelemetry/otlp-exporter-base' +import { JsonLogsSerializer } from '@opentelemetry/otlp-transformer' +import { resourceFromAttributes } from '@opentelemetry/resources' +import { LoggerProvider, type BatchLogRecordProcessorOptions, type LogRecordExporter, type ReadableLogRecord, type LogRecordProcessor } from '@opentelemetry/sdk-logs' +import { createLogExporter } from './transport.ts' + +/** Collector request ceiling in uncompressed UTF-8 bytes, including the OTLP envelope. */ +export const SESSION_LOG_MAX_REQUEST_BYTES = 4_000_000 + +/** One canonical event with its separately owned Session identity and redacted payload. */ +export interface SessionLogRecord { + sessionId: SessionId + /** Complete event envelope; data is the capture policy's exported copy. */ + event: Omit & { data: unknown } + /** Additional capture metadata; sessionId and content are always assigned by the reporter. */ + attributes?: Attributes + /** Omitted values use INFO. */ + severityNumber?: SeverityNumber +} + +/** Session-log transport and byte/count queue settings. */ +export interface SessionLogOptions { + /** Explicit destination and SDK transport options. */ + exporter: OTLPExporterNodeConfigBase & { + /** Full HTTP(S) logs destination. */ + url: string + } + /** Session-only queue settings, independent of product-event aggregation. */ + processor?: Omit + /** May lower, but never exceed, the collector's 4,000,000-byte limit. */ + maxRequestBytes?: number + /** Instrumentation scope supplied by the business owner. */ + scope: { name: string; version?: string } + /** Application and anonymous identity carried on the OTLP resource. */ + resourceAttributes: Attributes + /** Report rejected single records and network failures without recording their content. */ + onFailure: (message: string, error?: Error) => void +} + +/** + * Validate byte and queue settings before constructing an SDK pipeline. + * @param options - Session-specific limits supplied by the owning composition. + * @returns the resolved collector request limit. + */ +export function resolveSessionLogLimits(options: Pick): number { + const limit = options.maxRequestBytes ?? SESSION_LOG_MAX_REQUEST_BYTES + if (!Number.isSafeInteger(limit) || limit < 1 || limit > SESSION_LOG_MAX_REQUEST_BYTES) { + throw new Error(`session log maxRequestBytes must be an integer between 1 and ${SESSION_LOG_MAX_REQUEST_BYTES}`) + } + for (const key of ['maxQueueSize', 'maxExportBatchSize', 'scheduledDelayMillis', 'exportTimeoutMillis'] as const) { + const value = options.processor?.[key] + if (value !== undefined && (!Number.isSafeInteger(value) || value < 1 || value > 2_147_483_647)) { + throw new Error(`session log processor.${key} must be a positive integer no greater than 2147483647`) + } + } + const queue = options.processor?.maxQueueSize ?? 2048 + const batch = options.processor?.maxExportBatchSize ?? 512 + if (batch > queue) throw new Error('session log maxExportBatchSize must not exceed maxQueueSize') + return limit +} + +/** Byte/count batching with one transport request in flight, including after a watchdog warning. */ +class SessionLogProcessor implements LogRecordProcessor { + private readonly queue: { record: ReadableLogRecord; bytes: number }[] = [] + private bytes = 0 + private timer: ReturnType | undefined + private active: Promise | undefined + private shutdownPromise: Promise | undefined + private stopped = false + + constructor( + private readonly exporter: LogRecordExporter, + private readonly limit: number, + private readonly config: Required>, + private readonly warn: SessionLogOptions['onFailure'], + ) {} + + onEmit(record: ReadableLogRecord): void { + if (this.stopped) return + if (this.queue.length >= this.config.maxQueueSize) { + this.warn('Session log queue is full; record rejected') + return + } + let bytes: number + try { + const serialized = JsonLogsSerializer.serializeRequest([record]) + if (serialized === undefined) throw new Error('Session log serialization produced no request') + bytes = serialized.byteLength + } catch (error) { + this.warn('Session log serialization failed; record rejected', error instanceof Error ? error : new Error(String(error))) + return + } + if (bytes > this.limit) { + this.warn('Session log record rejected; content was not truncated', new Error(`Session log record exceeds maxRequestBytes: ${bytes} > ${this.limit}`)) + return + } + this.queue.push({ record, bytes }) + this.bytes += bytes + if (this.active !== undefined) return + if (this.queue.length >= this.config.maxExportBatchSize || this.bytes >= this.limit) { + void this.forceFlush() + } else if (this.timer === undefined) { + this.timer = setTimeout(() => { void this.forceFlush() }, this.config.scheduledDelayMillis) + this.timer.unref() + } + } + + forceFlush(): Promise { + clearTimeout(this.timer) + this.timer = undefined + if (this.active !== undefined) return this.active + if (this.queue.length === 0) return Promise.resolve() + this.active = this.drain() + return this.active + } + + private async drain(): Promise { + try { + while (!this.stopped && this.queue.length > 0) { + let bytes = 0 + let count = 0 + for (const entry of this.queue) { + if (count === this.config.maxExportBatchSize || bytes + entry.bytes > this.limit) break + bytes += entry.bytes + count++ + } + // Each measured record includes its resource/scope envelope. The shared + // envelope in this provider's multi-record request cannot exceed their sum. + const records = this.queue.splice(0, count).map(entry => entry.record) + this.bytes -= bytes + await this.send(records) + // The export callback precedes removal from the SDK concurrency queue. + await this.exporter.forceFlush() + } + } finally { this.active = undefined } + } + + private send(records: ReadableLogRecord[]): Promise { + return new Promise((resolve) => { + const timer = setTimeout(() => { + this.warn('Session log request exceeded exportTimeoutMillis; waiting for transport settlement') + }, this.config.exportTimeoutMillis) + timer.unref() + const finish = (error?: Error): void => { + clearTimeout(timer) + if (error !== undefined) this.warn('Session log export failed', error) + resolve() + } + try { + this.exporter.export(records, (result) => { + finish(result.code === ExportResultCode.SUCCESS + ? undefined : result.error ?? new Error('Session log HTTP export failed')) + }) + } catch (error) { + finish(error instanceof Error ? error : new Error(String(error))) + } + }) + } + + stopPending(): void { + this.stopped = true + this.queue.length = 0 + this.bytes = 0 + clearTimeout(this.timer) + this.timer = undefined + } + + shutdown(): Promise { + this.shutdownPromise ??= this.forceFlush().then(() => this.exporter.shutdown()) + return this.shutdownPromise + } +} + +/** Owns feedback-authorized Session logs; no product-event provider or queue is mounted. */ +export class SessionLogReporter { + private readonly provider: LoggerProvider + private readonly processor: SessionLogProcessor + private readonly logger: ReturnType + + /** @param options - explicit transport, resource identity, queue limits, and diagnostics. */ + constructor(options: SessionLogOptions) { + const limit = resolveSessionLogLimits(options) + this.processor = new SessionLogProcessor(createLogExporter(options.exporter), limit, { + maxQueueSize: options.processor?.maxQueueSize ?? 2048, + maxExportBatchSize: options.processor?.maxExportBatchSize ?? 512, + scheduledDelayMillis: options.processor?.scheduledDelayMillis ?? 1000, + exportTimeoutMillis: options.processor?.exportTimeoutMillis ?? 30000, + }, options.onFailure) + this.provider = new LoggerProvider({ + logRecordLimits: { attributeValueLengthLimit: Infinity, attributeCountLimit: Infinity }, + resource: resourceFromAttributes(options.resourceAttributes), + processors: [this.processor], + }) + this.logger = this.provider.getLogger(options.scope.name, options.scope.version) + } + + /** + * Enqueue one complete event without acknowledging network delivery. + * @param record - event with redacted data and its original Session id. + */ + reportSessionLog(record: SessionLogRecord): void { + const severityNumber = record.severityNumber ?? SeverityNumber.INFO + this.logger.emit({ + eventName: 'session-log', body: 'session-log', + timestamp: record.event.time, observedTimestamp: record.event.time, + severityNumber, severityText: SeverityNumber[severityNumber], + attributes: { ...record.attributes, sessionId: record.sessionId, content: JSON.stringify(record.event) }, + }) + } + + /** Stop queued requests after the owning backend's shutdown deadline; an active transport may still settle. */ + stopPending(): void { this.processor.stopPending() } + + /** + * Drain queued requests and release the SDK transport. + * @returns completion after queued requests settle and the SDK transport shuts down. + */ + shutdown(): Promise { return this.provider.shutdown() } +} diff --git a/packages/telemetry/otel/src/transport.ts b/packages/telemetry/otel/src/transport.ts new file mode 100644 index 0000000000..1975a56f34 --- /dev/null +++ b/packages/telemetry/otel/src/transport.ts @@ -0,0 +1,27 @@ +/** Explicit OTLP JSON transport for feedback-authorized Session logs. */ +import { createOtlpHttpExportDelegate, getSharedConfigurationFromEnvironment, httpAgentFactoryFromOptions } from '@opentelemetry/otlp-exporter-base/node-http' +import { getSharedConfigurationDefaults, mergeOtlpSharedConfigurationWithDefaults, OTLPExporterBase, type OTLPExporterNodeConfigBase } from '@opentelemetry/otlp-exporter-base' +import { JsonLogsSerializer } from '@opentelemetry/otlp-transformer' +import type { LogRecordExporter } from '@opentelemetry/sdk-logs' + +/** + * Create an SDK JSON exporter without inheriting another collector's headers or TLS identity. + * @param options - explicit endpoint, headers, agent, and SDK transport settings. + * @returns the exporter owned by one independent log pipeline. + */ +export function createLogExporter(options: OTLPExporterNodeConfigBase & { url: string }): LogRecordExporter { + const shared = mergeOtlpSharedConfigurationWithDefaults(options, getSharedConfigurationFromEnvironment('LOGS'), getSharedConfigurationDefaults()) + const transport = { + ...shared, + url: options.url, + headers: async () => ({ + 'Content-Type': 'application/json', + ...typeof options.headers === 'function' ? await options.headers() : options.headers, + }), + // An agent factory owns the returned agent, including its keepAlive setting. + agentFactory: typeof options.httpAgentOptions === 'function' ? options.httpAgentOptions + : httpAgentFactoryFromOptions({ keepAlive: options.keepAlive ?? true, ...options.httpAgentOptions }), + ...(options.userAgent === undefined ? {} : { userAgent: options.userAgent }), + } + return new OTLPExporterBase(createOtlpHttpExportDelegate(transport, JsonLogsSerializer)) +} diff --git a/packages/telemetry/otel/tests/session-log.spec.ts b/packages/telemetry/otel/tests/session-log.spec.ts new file mode 100644 index 0000000000..10299d95e8 --- /dev/null +++ b/packages/telemetry/otel/tests/session-log.spec.ts @@ -0,0 +1,302 @@ +import { createRequire } from 'node:module' +import { Context } from '@deepseek-ai/cordis' +import OTel from '../src/index.ts' +import { Agent, createServer, type IncomingHttpHeaders } from 'node:http' +import { once } from 'node:events' +import { gunzipSync } from 'node:zlib' +import { afterEach, beforeEach, expect, it, vi } from 'vitest' +import { CompressionAlgorithm } from '@opentelemetry/otlp-exporter-base' +import { JsonLogsSerializer } from '@opentelemetry/otlp-transformer' +import { SessionId, SessionSeq } from '@deepseek-ai/dsh-session' +import { SESSION_LOG_MAX_REQUEST_BYTES, type SessionLogRecord, type SessionLogOptions } from '../src/session-log.ts' + +interface Capture { + bytes: number + headers: IncomingHttpHeaders + body: { resourceLogs: { resource: unknown; scopeLogs: { logRecords: Record[] }[] }[] } +} +const cleanup: (() => Promise)[] = [] +let ctx: Context +afterEach(async () => { + try { + for (const dispose of cleanup.splice(0).reverse()) await dispose() + } finally { + vi.unstubAllEnvs() + vi.restoreAllMocks() + vi.useRealTimers() + } +}) + +beforeEach(async () => { + ctx = new Context() + cleanup.push(() => ctx.fiber.dispose()) + await ctx.plugin(OTel) + vi.stubEnv('OTEL_EXPORTER_OTLP_COMPRESSION', undefined) + vi.stubEnv('OTEL_EXPORTER_OTLP_LOGS_COMPRESSION', undefined) +}) + +async function collector(statuses = [200]) { + const captures: Capture[] = [] + const server = createServer((req, res) => { + const chunks: Buffer[] = [] + req.on('data', chunk => chunks.push(chunk as Buffer)) + req.on('end', () => { + const raw = Buffer.concat(chunks) + const bytes = req.headers['content-encoding'] === 'gzip' ? gunzipSync(raw) : raw + captures.push({ bytes: bytes.length, headers: req.headers, body: JSON.parse(bytes.toString()) as Capture['body'] }) + res.writeHead(statuses.shift() ?? 200, { 'content-type': 'application/json' }).end('{}') + }) + }) + cleanup.push(async () => { + const closed = once(server, 'close') + server.close() + server.closeAllConnections() + await closed + }) + server.listen(0, '127.0.0.1') + await once(server, 'listening') + const address = server.address() + if (address === null || typeof address === 'string') throw new Error('collector has no port') + return { captures, endpoint: `http://127.0.0.1:${address.port}/v1/logs` } +} + +function sessionRecord(text: string, seq = 0): SessionLogRecord { + return { + sessionId: SessionId('synthetic-session'), + event: { type: 'user/message', seq: SessionSeq(seq), time: 1_800_000_000_000, surfaceOp: 'append', + data: { content: [{ type: 'text', text }], nested: { items: [null, true, false, 0, 1.5, { text }] } } }, + } +} + +function logs(capture: Capture) { + return capture.body.resourceLogs.flatMap(resource => resource.scopeLogs.flatMap(scope => scope.logRecords)) +} + +function contents(captures: Capture[]): string[] { + return captures.flatMap(capture => logs(capture).map((record) => { + const attributes = record['attributes'] as { key: string; value: { stringValue: string } }[] + return attributes.find(attribute => attribute.key === 'content')!.value.stringValue + })) +} + +function parseContent(content: string): unknown { + return JSON.parse(content) +} + +function reporter(endpoint: string, overrides: Partial = {}) { + const onFailure = vi.fn() + const sender = ctx.otel.createSessionLogReporter({ + scope: { name: '@deepseek-ai/dsh-session-telemetry-otel', version: (createRequire(import.meta.url)('../package.json') as { version: string }).version }, + exporter: { url: endpoint, timeoutMillis: 1000 }, resourceAttributes: { 'service.name': 'session-test' }, + processor: { scheduledDelayMillis: 60000 }, onFailure, ...overrides, + }) + cleanup.push(() => sender.shutdown()) + return { sender, onFailure } +} + +it('preserves full nested events without truncation or ambient headers and retains scope version', async () => { + vi.stubEnv('OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT', '8') + vi.stubEnv('OTEL_EXPORTER_OTLP_HEADERS', 'Authorization=Bearer%20unrelated') + const target = await collector() + const { sender } = reporter(target.endpoint) + const record = sessionRecord('中文\n"quote"\\path 😀') + sender.reportSessionLog(record) + await sender.shutdown() + expect(contents(target.captures).map(parseContent)).toEqual([record.event]) + expect(logs(target.captures[0]!)[0]).toMatchObject({ eventName: 'session-log' }) + expect(target.captures[0]!.headers).not.toHaveProperty('authorization') + expect(target.captures[0]!.headers).not.toHaveProperty('x-channel') + expect(target.captures[0]!.body.resourceLogs[0]!.scopeLogs[0]).toMatchObject({ + scope: { name: '@deepseek-ai/dsh-session-telemetry-otel', version: (createRequire(import.meta.url)('../package.json') as { version: string }).version }, + }) +}) + +it.each([CompressionAlgorithm.GZIP, CompressionAlgorithm.NONE])('bounds %s requests and preserves nested content', async (compression) => { + const target = await collector() + const { sender, onFailure } = reporter(target.endpoint, { exporter: { url: target.endpoint, compression, concurrencyLimit: 1 } }) + const records = [0,1,2].map(seq => sessionRecord('中"\\'.repeat(100000), seq)) + for (const record of records) sender.reportSessionLog(record) + await sender.shutdown() + expect(target.captures.length).toBeGreaterThan(1) + expect(target.captures.every(c => c.bytes <= SESSION_LOG_MAX_REQUEST_BYTES)).toBe(true) + expect(contents(target.captures).map(parseContent)).toEqual(records.map(r => r.event)) + expect(onFailure).not.toHaveBeenCalled() +}) + +it('admits an exact request and diagnoses an oversized record only once', async () => { + const target = await collector() + const record = sessionRecord('边界"\\') + const baseline = reporter(target.endpoint).sender + baseline.reportSessionLog(record) + await baseline.shutdown() + const limit = target.captures[0]!.bytes + const exact = reporter(target.endpoint, { maxRequestBytes: limit }).sender + exact.reportSessionLog(record) + await exact.shutdown() + const small = reporter(target.endpoint, { maxRequestBytes: limit - 1 }) + small.sender.reportSessionLog(record) + await small.sender.shutdown() + expect(target.captures.map(c => c.bytes)).toEqual([limit, limit]) + expect(small.onFailure).toHaveBeenCalledTimes(1) + expect(small.onFailure).toHaveBeenCalledWith('Session log record rejected; content was not truncated', expect.any(Error)) +}) + +it('continues after an oversized record and a failed HTTP request', async () => { + const target = await collector([400,200]) + const { sender, onFailure } = reporter(target.endpoint, { maxRequestBytes: 1300 }) + const records = [sessionRecord('first'.repeat(25)), sessionRecord('x'.repeat(4000000),1), sessionRecord('last'.repeat(25),2)] + for(const record of records) sender.reportSessionLog(record) + await sender.shutdown() + expect(contents(target.captures).map(parseContent)).toEqual([records[0]!.event,records[2]!.event]) + expect(onFailure).toHaveBeenCalledTimes(2) +}) + +it.each([0,-1,1.5,4000001,Infinity])('rejects byte limit %s', (maxRequestBytes) => { + expect(() => reporter('http://collector.test/logs',{ maxRequestBytes })).toThrow('maxRequestBytes') +}) + +it.each(['maxQueueSize','maxExportBatchSize','scheduledDelayMillis','exportTimeoutMillis'] as const)('rejects invalid %s', (key) => { + expect(() => reporter('http://collector.test/logs',{ processor:{ [key]:0 } })).toThrow(`processor.${key}`) +}) + +it('rejects a batch count above the queue capacity', () => { + expect(() => reporter('http://collector.test/logs',{ processor:{ maxQueueSize:1,maxExportBatchSize:2 } })).toThrow('maxExportBatchSize must not exceed maxQueueSize') +}) + +it('honors explicit routing headers and caller-owned agent settings', async () => { + const target = await collector() + const agent = new Agent({ keepAlive:false }) + cleanup.push(async () => {agent.destroy()}) + const { sender } = reporter(target.endpoint,{ exporter:{ url:target.endpoint,userAgent:'session-test',keepAlive:true,httpAgentOptions:async () => agent,headers:async () => ({ 'x-channel':'test-channel' }) } }) + sender.reportSessionLog(sessionRecord('agent')) + await sender.shutdown() + expect(target.captures[0]!.headers.connection).toBe('close') + expect(target.captures[0]!.headers['x-channel']).toBe('test-channel') + expect(target.captures[0]!.headers['user-agent']).toContain('session-test') +}) + +it.each([new Error('serialize'), 'serialize', undefined])('contains serializer failures %s', (failure) => { + const { sender,onFailure } = reporter('http://collector.test/logs') + vi.spyOn(JsonLogsSerializer,'serializeRequest').mockImplementationOnce(() => { if(failure===undefined)return undefined; throw failure }) + sender.reportSessionLog(sessionRecord('invalid')) + expect(onFailure).toHaveBeenCalledWith('Session log serialization failed; record rejected',expect.any(Error)) +}) + +it('measures each record once instead of serializing the full candidate batch', async () => { + const target = await collector() + const { sender } = reporter(target.endpoint,{ maxRequestBytes:1300 }) + const serialize = vi.spyOn(JsonLogsSerializer,'serializeRequest') + const records = Array.from({ length:100 },(_,seq) => sessionRecord('batch'.repeat(20),seq)) + for(const record of records)sender.reportSessionLog(record) + await sender.shutdown() + expect(contents(target.captures).map(parseContent)).toEqual(records.map(r=>r.event)) + expect(serialize.mock.calls.every(([records])=>records.length===1)).toBe(true) + expect(serialize).toHaveBeenCalledTimes(records.length+target.captures.length) +}) + +it('keeps one transport slot across multiple byte batches and times each request independently', async () => { + vi.useFakeTimers() + const transport = await import('../src/transport.ts') + const sent: number[] = [] + let active = 0 + let peak = 0 + vi.spyOn(transport, 'createLogExporter').mockReturnValue({ + export(records, callback) { + active++ + peak = Math.max(peak, active) + sent.push(...records.map(r => (JSON.parse(r.attributes.content as string) as { seq: number }).seq)) + setTimeout(() => { active--; callback({ code: 0 }) }, 800) + }, + forceFlush: async () => {}, shutdown: async () => {}, + }) + const { sender, onFailure } = reporter('http://collector.test/logs', { + exporter: { url: 'http://collector.test/logs', concurrencyLimit: 1 }, + maxRequestBytes: 1300, + processor: { maxExportBatchSize: 3, maxQueueSize: 6, scheduledDelayMillis: 60000, exportTimeoutMillis: 1500 }, + }) + for(let seq = 0; seq < 6; seq++) sender.reportSessionLog(sessionRecord('content'.repeat(20), seq)) + const done = sender.shutdown() + await vi.advanceTimersByTimeAsync(4800) + await done + expect(sent).toEqual([0,1,2,3,4,5]) + expect(peak).toBe(1) + expect(onFailure).not.toHaveBeenCalled() +}) + +it('does not release the transport slot on a watchdog timeout or send queued records after the shutdown deadline', async () => { + vi.useFakeTimers() + const transport = await import('../src/transport.ts') + const callback = Promise.withResolvers<(result: { code: 0 }) => void>() + const send = vi.fn((_records, complete: (result: { code: 0 }) => void) => { callback.resolve(complete) }) + vi.spyOn(transport, 'createLogExporter').mockReturnValue({ export: send, forceFlush: async () => {}, shutdown: async () => {} }) + const { sender, onFailure } = reporter('http://collector.test/logs', { + processor: { maxExportBatchSize: 1, maxQueueSize: 2, exportTimeoutMillis: 100, scheduledDelayMillis: 1000 }, + }) + sender.reportSessionLog(sessionRecord('first')) + const finish = await callback.promise + cleanup.push(async () => { sender.stopPending(); finish({ code: 0 }) }) + sender.reportSessionLog(sessionRecord('queued',1)) + await vi.advanceTimersByTimeAsync(500) + expect(send).toHaveBeenCalledTimes(1) + expect(onFailure).toHaveBeenCalledWith('Session log request exceeded exportTimeoutMillis; waiting for transport settlement') + sender.stopPending() + sender.reportSessionLog(sessionRecord('after deadline',2)) + const done = sender.shutdown() + finish({ code:0 }) + await done + sender.reportSessionLog(sessionRecord('late',2)) + expect(send).toHaveBeenCalledTimes(1) +}) + +it.each([new Error('transport'), 'transport', undefined])('contains transport failures %s and continues later requests', async (failure) => { + const transport = await import('../src/transport.ts') + const send = vi.fn() + .mockImplementationOnce((_records, callback) => { if(failure === undefined) callback({ code:1 }); else throw failure }) + .mockImplementation((_records, callback) => { callback({ code:0 }) }) + vi.spyOn(transport,'createLogExporter').mockReturnValue({ export:send,forceFlush:async()=>{},shutdown:async()=>{} }) + const { sender,onFailure } = reporter('http://collector.test/logs',{ processor:{ maxExportBatchSize:1 } }) + sender.reportSessionLog(sessionRecord('failed')) + sender.reportSessionLog(sessionRecord('next',1)) + await sender.shutdown() + expect(send).toHaveBeenCalledTimes(2) + expect(onFailure).toHaveBeenCalledTimes(1) +}) + +it('bounds the queued record count while a transport request is unsettled', async () => { + const transport = await import('../src/transport.ts') + const release = Promise.withResolvers<(result:{ code:0 })=>void>() + vi.spyOn(transport,'createLogExporter').mockReturnValue({ + export(_records,callback){release.resolve(callback)},forceFlush:async()=>{},shutdown:async()=>{}, + }) + const { sender,onFailure } = reporter('http://collector.test/logs',{ processor:{ maxExportBatchSize:1,maxQueueSize:2 } }) + for(let seq=0;seq<4;seq++)sender.reportSessionLog(sessionRecord('queued',seq)) + const finish = await release.promise + cleanup.push(async () => { sender.stopPending(); finish({ code: 0 }) }) + expect(onFailure).toHaveBeenCalledWith('Session log queue is full; record rejected') + sender.stopPending() + const done=sender.shutdown() + finish({ code:0 }) + await done +}) + +it('keeps ordinary events and Session logs in independent channels through one Cordis service', async () => { + const target = await collector() + const ordinary = ctx.otel.createEventReporter({ + exporter: { url: target.endpoint }, + resourceAttributes: { 'service.name': 'ordinary-test' }, + scope: { name: 'ordinary-consumer', version: '1' }, + processor: { scheduledDelayMillis: 60000 }, + onFailure: () => { throw new Error('unexpected export failure') }, + }) + cleanup.push(() => ordinary.shutdown()) + const { sender } = reporter(target.endpoint) + ordinary.emit({ eventName: 'ui.click', body: 'click', timestamp: Date.now() }) + sender.reportSessionLog(sessionRecord('authorized')) + expect(target.captures).toEqual([]) + await sender.shutdown() + expect(target.captures.flatMap(logs).map(r => r.eventName)).toEqual(['session-log']) + await ordinary.shutdown() + expect(target.captures.map(c => logs(c).map(r => r.eventName))).toEqual([['session-log'], ['ui.click']]) + await ctx.fiber.dispose() + expect(ctx.get('otel')).toBeUndefined() +}) diff --git a/packages/telemetry/otel/tsconfig.json b/packages/telemetry/otel/tsconfig.json new file mode 100644 index 0000000000..e88100b9a0 --- /dev/null +++ b/packages/telemetry/otel/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "lib/types" + }, + "include": [ + "src" + ], + "references": [ + { + "path": "../../../vendor/cordis" + }, + { + "path": "../../../vendor/cosmokit" + }, + { + "path": "../../core/session" + } + ] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c264dfc7c0..b87182ec5f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -247,6 +247,9 @@ importers: '@deepseek-ai/dsh-experimental-auto-review': specifier: workspace:* version: link:../../packages/experimental/auto-review + '@deepseek-ai/dsh-experimental-schedule-bundle': + specifier: workspace:* + version: link:../../packages/experimental/schedule-bundle '@deepseek-ai/dsh-experimental-voice-input-bundle': specifier: workspace:* version: link:../../packages/experimental/voice-input-bundle @@ -1989,6 +1992,9 @@ importers: '@deepseek-ai/dsh-mcp-resources': specifier: workspace:* version: link:../../mcp/mcp-resources + '@deepseek-ai/dsh-otel': + specifier: workspace:* + version: link:../../telemetry/otel '@deepseek-ai/dsh-permission-presets': specifier: workspace:* version: link:../../interaction/permission-presets @@ -2729,6 +2735,9 @@ importers: specifier: ^11.0.0 version: 11.0.4 devDependencies: + '@deepseek-ai/dsh-otel': + specifier: workspace:* + version: link:../../telemetry/otel '@deepseek-ai/cordis': specifier: workspace:~ version: link:../../../vendor/cordis @@ -2944,6 +2953,9 @@ importers: specifier: ^4.4.3 version: 4.4.3 devDependencies: + '@deepseek-ai/dsh-otel': + specifier: workspace:* + version: link:../../telemetry/otel '@deepseek-ai/cordis-plugin-loader': specifier: workspace:~ version: link:../../../vendor/loader @@ -7848,6 +7860,18 @@ importers: specifier: workspace:* version: link:../../util/values + packages/experimental/schedule-bundle: + devDependencies: + '@deepseek-ai/cordis': + specifier: workspace:~ + version: link:../../../vendor/cordis + '@deepseek-ai/cordis-plugin-include': + specifier: workspace:~ + version: link:../../../vendor/include + js-yaml: + specifier: ^4.2.0 + version: 4.3.1 + packages/experimental/speech-to-text: dependencies: '@deepseek-ai/dsh-brand': @@ -9056,24 +9080,12 @@ importers: '@deepseek-ai/schemastery': specifier: link:../../../vendor/schemastery version: link:../../../vendor/schemastery - '@opentelemetry/api': - specifier: ^1.9.1 - version: 1.9.1 '@opentelemetry/api-logs': specifier: ^0.220.0 version: 0.220.0 - '@opentelemetry/core': - specifier: 2.9.0 - version: 2.9.0(@opentelemetry/api@1.9.1) '@opentelemetry/otlp-exporter-base': specifier: ^0.220.0 version: 0.220.0(@opentelemetry/api@1.9.1) - '@opentelemetry/otlp-transformer': - specifier: ^0.220.0 - version: 0.220.0(@opentelemetry/api@1.9.1) - '@opentelemetry/resources': - specifier: ^2.9.0 - version: 2.10.0(@opentelemetry/api@1.9.1) '@opentelemetry/sdk-logs': specifier: ^0.220.0 version: 0.220.0(@opentelemetry/api@1.9.1) @@ -9090,6 +9102,9 @@ importers: '@deepseek-ai/dsh-loader-smoke': specifier: workspace:* version: link:../../test-support/loader-smoke + '@deepseek-ai/dsh-otel': + specifier: workspace:* + version: link:../../telemetry/otel packages/host/webserver: dependencies: @@ -10975,21 +10990,12 @@ importers: '@deepseek-ai/schemastery': specifier: link:../../../vendor/schemastery version: link:../../../vendor/schemastery - '@opentelemetry/api': - specifier: ^1.9.1 - version: 1.9.1 '@opentelemetry/api-logs': specifier: ^0.220.0 version: 0.220.0 - '@opentelemetry/exporter-logs-otlp-http': - specifier: ^0.220.0 - version: 0.220.0(@opentelemetry/api@1.9.1) '@opentelemetry/otlp-exporter-base': specifier: ^0.220.0 version: 0.220.0(@opentelemetry/api@1.9.1) - '@opentelemetry/resources': - specifier: ^2.9.0 - version: 2.10.0(@opentelemetry/api@1.9.1) '@opentelemetry/sdk-logs': specifier: ^0.220.0 version: 0.220.0(@opentelemetry/api@1.9.1) @@ -11027,6 +11033,9 @@ importers: '@deepseek-ai/dsh-message-feedback': specifier: workspace:* version: link:../../feedback/message-feedback + '@deepseek-ai/dsh-otel': + specifier: workspace:* + version: link:../../telemetry/otel '@deepseek-ai/dsh-session': specifier: workspace:* version: link:../../core/session @@ -12768,6 +12777,37 @@ importers: specifier: workspace:~ version: link:../../../vendor/cordis + packages/telemetry/otel: + dependencies: + '@opentelemetry/api': + specifier: ^1.9.1 + version: 1.9.1 + '@opentelemetry/api-logs': + specifier: ^0.220.0 + version: 0.220.0 + '@opentelemetry/core': + specifier: 2.9.0 + version: 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-exporter-base': + specifier: ^0.220.0 + version: 0.220.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-transformer': + specifier: ^0.220.0 + version: 0.220.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': + specifier: ^2.9.0 + version: 2.10.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-logs': + specifier: ^0.220.0 + version: 0.220.0(@opentelemetry/api@1.9.1) + devDependencies: + '@deepseek-ai/cordis': + specifier: workspace:~ + version: link:../../../vendor/cordis + '@deepseek-ai/dsh-session': + specifier: workspace:* + version: link:../../core/session + packages/terminal/terminal: devDependencies: '@deepseek-ai/cordis': @@ -14086,6 +14126,9 @@ importers: '@deepseek-ai/dsh-message-feedback': specifier: workspace:* version: link:../../packages/feedback/message-feedback + '@deepseek-ai/dsh-otel': + specifier: workspace:* + version: link:../../packages/telemetry/otel '@deepseek-ai/dsh-output-retention': specifier: workspace:* version: link:../../packages/util/output-retention @@ -16225,12 +16268,6 @@ packages: peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' - '@opentelemetry/exporter-logs-otlp-http@0.220.0': - resolution: {integrity: sha512-8186thl+pTw64iz/qEEen5oJZoZ/gO73XruChdaGlYdWOdBIQ42r+vHLf6a7vIDqTD4b8ZOoMlyxptanECaI9A==} - engines: {node: ^18.19.0 || >=20.6.0} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - '@opentelemetry/otlp-exporter-base@0.220.0': resolution: {integrity: sha512-CXYo8UD5Mn9YbgebO2EL4wejtA+gxLmLiu6HCk2KH2BR7XhFN6/6p1UlCb23DYCjeYkndevLHuejCCN1yx4+OQ==} engines: {node: ^18.19.0 || >=20.6.0} @@ -23541,15 +23578,6 @@ snapshots: '@opentelemetry/api': 1.9.1 '@opentelemetry/semantic-conventions': 1.43.0 - '@opentelemetry/exporter-logs-otlp-http@0.220.0(@opentelemetry/api@1.9.1)': - dependencies: - '@opentelemetry/api': 1.9.1 - '@opentelemetry/api-logs': 0.220.0 - '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.1) - '@opentelemetry/otlp-exporter-base': 0.220.0(@opentelemetry/api@1.9.1) - '@opentelemetry/otlp-transformer': 0.220.0(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-logs': 0.220.0(@opentelemetry/api@1.9.1) - '@opentelemetry/otlp-exporter-base@0.220.0(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 diff --git a/python/sdk-runtime/package.json b/python/sdk-runtime/package.json index 3083c4ad9f..6fde3d59f4 100644 --- a/python/sdk-runtime/package.json +++ b/python/sdk-runtime/package.json @@ -152,6 +152,7 @@ "@deepseek-ai/dsh-web-search-perplexity": "workspace:*", "@deepseek-ai/dsh-workflow": "workspace:*", "@deepseek-ai/dsh-workflow-ptc": "workspace:*", + "@deepseek-ai/dsh-otel": "workspace:*", "@deepseek-ai/schemastery": "workspace:~" } } diff --git a/scripts/gen-cordis-catalog.ts b/scripts/gen-cordis-catalog.ts index 54bf67a36e..582358bf60 100644 --- a/scripts/gen-cordis-catalog.ts +++ b/scripts/gen-cordis-catalog.ts @@ -52,6 +52,7 @@ export { REGION_BEGIN, REGION_END } export const SERVICE_PAGE: Record = { speechToText: 'voice-input.md', speechController: 'voice-input.md', + otel: 'otel.md', productTelemetry: 'product-telemetry.md', productAnalytics: 'product-telemetry.md', connection: 'web-server.md', @@ -274,6 +275,12 @@ export const EVENT_WALK_EXEMPTIONS: Record = { * appear on more than one page. */ export const LINK_MAP: Readonly> = { + EventLogOptions: 'otel.md', + EventLogReporter: 'otel.md', + SessionLogOptions: 'otel.md', + SessionLogReporter: 'otel.md', + OTelEventRecord: 'otel.md', + OTelEventScalar: 'otel.md', ProductTelemetryRecord: 'product-telemetry.md', ProductTelemetryScalar: 'product-telemetry.md', WorkspaceChangesSummary: 'deliverables.md', diff --git a/scripts/gen-doc-graphs.ts b/scripts/gen-doc-graphs.ts index f19e6dabaa..3e691d0b35 100644 --- a/scripts/gen-doc-graphs.ts +++ b/scripts/gen-doc-graphs.ts @@ -410,7 +410,15 @@ const SERVICE_ROLES: ServiceRole[] = [ pkg: 'client-product-analytics', title: 'Desktop interaction collection', mode: 'service', - note: 'Accepts selected Desktop events, enriches available login identity, and observes live compaction under the launch-time collection policy.', + note: 'Accepts selected Desktop events, enriches available login identity, and observes live compaction under the live Host collection policy.', + }, + { + key: 'otel', + pkg: 'otel', + title: 'Shared OTel reporting channels', + mode: 'service', + consumers: ['host-product-telemetry-otel', 'session-telemetry-otel'], + note: 'Product analytics and Session feedback adapters create independent reporting channels through one injected service.', }, { key: 'productTelemetry', diff --git a/scripts/optional-bundles.spec.ts b/scripts/optional-bundles.spec.ts index 71671e5b71..3158b7a0fb 100644 --- a/scripts/optional-bundles.spec.ts +++ b/scripts/optional-bundles.spec.ts @@ -41,13 +41,38 @@ describe('optional bundles', () => { it.each(OPTIONAL_BUNDLES)('%s composes over the Web profile without a skipped patch', (name) => { const { patches } = bundle(name) const warnings: string[] = [] - const ids = new Set(composeEntries([...shipped, patches], message => warnings.push(message)).map(entry => entry.id)) + const composed = composeEntries([...shipped, patches], message => warnings.push(message)) + const ids = new Set(composed.map(entry => entry.id)) expect(warnings).toEqual([]) // Inserted rows carry stable ids at the profile root, so a later profile patch can configure or disable them. for (const row of patches.flatMap(patch => patch.insert ?? [])) { expect(typeof row.id).toBe('string') expect(ids.has(row.id)).toBe(true) } + // One top-level row per id: a duplicate declaration leaves the Loader with the last one, silently + // replacing the layer that declared the id first. + const topLevelIds = composed.flatMap(entry => typeof entry.id === 'string' ? [entry.id] : []) + expect(topLevelIds).toHaveLength(new Set(topLevelIds).size) + // An id-targeted patch reaches a row another layer inserted: the id resolves to exactly one top-level + // row, and the override keeps the package the shipped layer declared on it. + const shippedComposed = composeEntries([...shipped]) + for (const patch of patches) { + if (patch.insert !== undefined || typeof patch.id !== 'string') continue + const matches = composed.filter(entry => entry.id === patch.id) + expect(matches).toHaveLength(1) + expect(matches[0]?.name).toBe(shippedComposed.find(entry => entry.id === patch.id)?.name) + } + }) + + it('composes the Schedule bundle over the three shipped Web rows', () => { + const { patches } = bundle('@deepseek-ai/dsh-experimental-schedule-bundle') + const composed = composeEntries([...shipped, patches]) + expect(composed.filter(entry => entry.id === 'time-context' || entry.id === 'schedule' || entry.id === 'ui-schedule')) + .toMatchObject([ + { id: 'time-context', name: '@deepseek-ai/dsh-time-context', disabled: false }, + { id: 'schedule', name: '@deepseek-ai/dsh-schedule', disabled: false }, + { id: 'ui-schedule', name: '@deepseek-ai/dsh-client-ui-schedule', disabled: false }, + ]) }) it.each(OPTIONAL_BUNDLES)('%s resolves a title, description, and icon in both shipped languages', (name) => { diff --git a/scripts/release/families.spec.ts b/scripts/release/families.spec.ts index a54d7c3fb2..003c97074d 100644 --- a/scripts/release/families.spec.ts +++ b/scripts/release/families.spec.ts @@ -62,6 +62,7 @@ describe('release families', () => { '@deepseek-ai/dsh-experimental-computer-use-cua-driver-native', '@deepseek-ai/dsh-experimental-inspector', '@deepseek-ai/dsh-experimental-ptc-runtime-python', + '@deepseek-ai/dsh-experimental-schedule-bundle', '@deepseek-ai/dsh-experimental-speech-to-text-sensevoice', '@deepseek-ai/dsh-experimental-speech-to-text', '@deepseek-ai/dsh-experimental-tool-agent-team', diff --git a/scripts/verify-package-readme-model-experience.ts b/scripts/verify-package-readme-model-experience.ts index 8d93abe461..da3427d48c 100644 --- a/scripts/verify-package-readme-model-experience.ts +++ b/scripts/verify-package-readme-model-experience.ts @@ -51,6 +51,7 @@ const SENTENCE_MODEL_EXPERIENCE: Readonly> = { 'packages/experimental/speech-to-text-sensevoice': { kind: 'none', reason: 'Local recognition returns transient text without modifying model context.' }, 'packages/experimental/client-ui-voice-input': { kind: 'none', reason: 'Inserts reviewable text into the unsent draft without submitting to the Agent.' }, 'packages/experimental/voice-input-bundle': { kind: 'none', reason: 'Composes dictation and preparation plugins without adding any model-facing contribution.' }, + 'packages/telemetry/otel': { kind: 'none', reason: 'The service delivers caller-selected records without changing model context.' }, 'packages/host/product-telemetry-otel': { kind: 'none', reason: 'The plugin exports explicit analytics records without contributing model context.' }, 'packages/api/terminal-controller': { kind: 'none', reason: 'User-owned terminal processes and screen streams never enter model requests or Session events.' }, 'packages/client/ui-sidebar-terminal': { kind: 'none', reason: 'The browser renders user terminal screens without exposing them to the model.' }, diff --git a/snapshots/web/feedback-release/feedback-release.expected.json b/snapshots/web/feedback-release/feedback-release.expected.json index 994953b4ef..501d8582bf 100644 --- a/snapshots/web/feedback-release/feedback-release.expected.json +++ b/snapshots/web/feedback-release/feedback-release.expected.json @@ -1,5 +1,6 @@ { "mode": "FEEDBACK_ONLY", + "eventName": "session-log", "feedback": [ { "type": "feedback/record", diff --git a/snapshots/web/navigation-panes/search-results.expected.md b/snapshots/web/navigation-panes/search-results.expected.md index 49de115594..6be6bf1767 100644 --- a/snapshots/web/navigation-panes/search-results.expected.md +++ b/snapshots/web/navigation-panes/search-results.expected.md @@ -1,2 +1,2 @@ - tree "Search results": - - 'treeitem "{{workspace}} {{workspace}} ## Navigation Summary - alpha nav - beta nav ``` echo WATERFALL ```"' + - 'treeitem "Untitled {{workspace}} ## Navigation Summary - alpha nav - beta nav ``` echo WATERFALL ```"' diff --git a/tsconfig.base.json b/tsconfig.base.json index 074ddf99d9..750f340b12 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -31,6 +31,7 @@ "@deepseek-ai/dsh-client-product-analytics": ["./packages/client/product-analytics/src/index.ts"], "@deepseek-ai/dsh-client-product-analytics/types": ["./packages/client/product-analytics/src/events.ts"], "@deepseek-ai/dsh-client-product-analytics/client": ["./packages/client/product-analytics/src/client/index.ts"], + "@deepseek-ai/dsh-otel": ["./packages/telemetry/otel/src"], "@deepseek-ai/dsh-client-ui-settings-account": ["./packages/client/ui-settings-account/src/index.ts"], "@deepseek-ai/dsh-client-ui-settings-account/client": ["./packages/client/ui-settings-account/src/client/index.ts"], "@deepseek-ai/dsh-api-gateway/stream-protocol": ["./packages/api/gateway/src/stream-protocol.ts"], @@ -322,6 +323,7 @@ "@deepseek-ai/dsh-experimental-inspector": ["./packages/experimental/inspector/src"], "@deepseek-ai/dsh-experimental-inspector/client": ["./packages/experimental/inspector/src/client/index.ts"], "@deepseek-ai/dsh-experimental-ptc-runtime-python": ["./packages/experimental/ptc-runtime-python/src"], + "@deepseek-ai/dsh-experimental-schedule-bundle": ["./packages/experimental/schedule-bundle/src/index.ts"], "@deepseek-ai/dsh-util-crypto": ["./packages/util/crypto/src"], "@deepseek-ai/dsh-util-values": ["./packages/util/values/src"], "@deepseek-ai/dsh-util-code-language": ["./packages/util/code-language/src"], diff --git a/tsconfig.host.json b/tsconfig.host.json index cac9aff913..74a449f60c 100644 --- a/tsconfig.host.json +++ b/tsconfig.host.json @@ -199,6 +199,7 @@ ], "references": [ { "path": "./packages/client/product-analytics/tsconfig.host.json" }, + { "path": "./packages/telemetry/otel" }, { "path": "./packages/experimental/speech-to-text" }, { "path": "./packages/experimental/speech-to-text-sensevoice" }, { "path": "./packages/experimental/api-speech-to-text" }, @@ -358,6 +359,7 @@ { "path": "./packages/terminal/tool-terminal" }, { "path": "./packages/ptc-runtime/ptc-runtime" }, { "path": "./packages/experimental/ptc-runtime-python" }, + { "path": "./packages/experimental/schedule-bundle" }, { "path": "./packages/ptc-runtime/ptc-runtime-node" }, { "path": "./packages/llm/llm-deepseek" }, { "path": "./packages/llm/llm-deepseek-api-key" },