From 5061ec2ef097cc33aab286d9416e14f096824680 Mon Sep 17 00:00:00 2001 From: creatixchu Date: Thu, 13 Aug 2026 13:47:09 +0800 Subject: [PATCH 01/24] =?UTF-8?q?feat(web):=20=E7=A7=BB=E9=99=A4=E9=A6=96?= =?UTF-8?q?=E5=90=AF=E5=86=85=E6=B5=8B=E5=A3=B0=E6=98=8E=EF=BC=8C=E9=81=A5?= =?UTF-8?q?=E6=B5=8B=E4=BF=9D=E6=8C=81=E9=BB=98=E8=AE=A4=E5=85=B3=E9=97=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...versioned-gui-welcome-onboarding.i18n.yaml | 4 +- ...-07-30-versioned-gui-welcome-onboarding.md | 12 +- ...-30-versioned-gui-welcome-onboarding.zh.md | 12 +- ...2026-08-10-telemetry-default-off.i18n.yaml | 4 +- .../2026-08-10-telemetry-default-off.md | 2 +- .../2026-08-10-telemetry-default-off.zh.md | 2 +- ...-13-remove-first-run-beta-notice.i18n.yaml | 6 + ...2026-08-13-remove-first-run-beta-notice.md | 25 +++ ...6-08-13-remove-first-run-beta-notice.zh.md | 25 +++ apps/web/tests/README.i18n.yaml | 4 +- apps/web/tests/README.md | 5 +- apps/web/tests/README.zh.md | 4 +- apps/web/tests/goal-bar.e2e.ts | 2 +- apps/web/tests/goal-bar.overlay.yml | 6 +- .../tests/onboarding-deepseek-config.e2e.ts | 50 +---- apps/web/tests/remote-welcome.e2e.ts | 53 ----- apps/web/tests/scaffold.ts | 21 -- .../welcome.expected.md | 9 - .../ui-settings-general/README.i18n.yaml | 4 +- packages/client/ui-settings-general/README.md | 4 +- .../client/ui-settings-general/README.zh.md | 4 +- .../src/client/WelcomeNotice.module.css | 155 --------------- .../src/client/WelcomeNotice.tsx | 91 --------- .../ui-settings-general/src/client/index.ts | 42 +--- .../ui-settings-general/src/client/locales.ts | 17 +- .../src/client/welcome-store.ts | 128 ------------ .../client/ui-settings-general/src/index.ts | 14 +- .../ui-settings-general/src/invariant.ts | 2 +- .../src/onboarding-copy.ts | 35 ---- .../tests/apply.client.spec.ts | 55 +----- .../tests/host.client.spec.ts | 8 +- .../tests/shell.client.spec.ts | 4 +- .../tests/welcome-notice.client.spec.tsx | 106 ---------- .../tests/welcome-store.client.spec.ts | 184 ------------------ .../src/client/slot-catalog.ts | 1 - 35 files changed, 122 insertions(+), 978 deletions(-) create mode 100644 .agents/notes/implemented/simplification/2026-08-13-remove-first-run-beta-notice.i18n.yaml create mode 100644 .agents/notes/implemented/simplification/2026-08-13-remove-first-run-beta-notice.md create mode 100644 .agents/notes/implemented/simplification/2026-08-13-remove-first-run-beta-notice.zh.md delete mode 100644 apps/web/tests/remote-welcome.e2e.ts delete mode 100644 apps/web/tests/snapshots/onboarding-deepseek-config/welcome.expected.md delete mode 100644 packages/client/ui-settings-general/src/client/WelcomeNotice.module.css delete mode 100644 packages/client/ui-settings-general/src/client/WelcomeNotice.tsx delete mode 100644 packages/client/ui-settings-general/src/client/welcome-store.ts delete mode 100644 packages/client/ui-settings-general/src/onboarding-copy.ts delete mode 100644 packages/client/ui-settings-general/tests/welcome-notice.client.spec.tsx delete mode 100644 packages/client/ui-settings-general/tests/welcome-store.client.spec.ts diff --git a/.agents/notes/implemented/feature/2026-07-30-versioned-gui-welcome-onboarding.i18n.yaml b/.agents/notes/implemented/feature/2026-07-30-versioned-gui-welcome-onboarding.i18n.yaml index abe1f687fb..66b59c3509 100644 --- a/.agents/notes/implemented/feature/2026-07-30-versioned-gui-welcome-onboarding.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-07-30-versioned-gui-welcome-onboarding.i18n.yaml @@ -2,5 +2,5 @@ # 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 .agents/notes/implemented/feature/2026-07-30-versioned-gui-welcome-onboarding.md -2026-07-30-versioned-gui-welcome-onboarding.md: 8897e28413df77d127a4d846f2a3e42789454490 -2026-07-30-versioned-gui-welcome-onboarding.zh.md: 9f5eb99033cd62154937808f26b0e1aa40966c9f +2026-07-30-versioned-gui-welcome-onboarding.md: 7a8c5c5b9ba1ec62ad8dd0f5bdf67c516ba0ddec +2026-07-30-versioned-gui-welcome-onboarding.zh.md: 646080b893bb6264a3a92f49400cb9207e29755b diff --git a/.agents/notes/implemented/feature/2026-07-30-versioned-gui-welcome-onboarding.md b/.agents/notes/implemented/feature/2026-07-30-versioned-gui-welcome-onboarding.md index 8897e28413..7a8c5c5b9b 100644 --- a/.agents/notes/implemented/feature/2026-07-30-versioned-gui-welcome-onboarding.md +++ b/.agents/notes/implemented/feature/2026-07-30-versioned-gui-welcome-onboarding.md @@ -10,15 +10,13 @@ The GUI's credential onboarding begins with a DeepSeek-specific readiness check, ## Decision -**The Settings shell coordinates ordered steps.** `settings.onboarding` remains a root-scoped list, but `ui-settings` projects its entry ids and order into one coordinator and mounts only the first incomplete step. The active registrant receives `complete()` and `openSection(id)`; no later step mounts until ownership transfers. The product welcome registers at order `-100`, while `ui-settings-models` retains only the conditional DeepSeek readiness and credential-routing step at order `0`. +**The Settings shell coordinates ordered steps.** `settings.onboarding` remains a root-scoped list, but `ui-settings` projects its entry ids and order into one coordinator and mounts only the first incomplete step. The active registrant receives `complete()` and `openSection(id)`; no later step mounts until ownership transfers. `ui-settings-models` registers the conditional DeepSeek readiness and credential-routing step at order `0`, the only shipped occupant since the [first-run beta notice removal](../simplification/2026-08-13-remove-first-run-beta-notice.md). -**Ownerless product onboarding belongs to `ui-settings-general`.** `src/onboarding-copy.ts` is the single editable source for the complete notice, the Continue label, and `WELCOME_NOTICE_VERSION`; both supported GUI locales intentionally render the same Chinese owner copy. Runtime locale dictionaries derive their welcome values from that file, and tests import the same owner instead of repeating paragraph text. The notice is browser UI only: it creates no Session event and contributes no model-visible content. The notice states that session telemetry is [disabled by default](2026-08-10-telemetry-default-off.md) and names the `FEEDBACK_ONLY` and `FULL` opt-in modes. +**The product welcome step is removed.** The versioned notice, its copy owner, and its acknowledgement store shipped from this decision until the [first-run beta notice removal](../simplification/2026-08-13-remove-first-run-beta-notice.md), which owns the removal rationale; `ui-settings-general` seats no onboarding step. -**Loopback acknowledgement is durable per Harness profile.** The Host half registers a `ui-onboarding` section in the user-settings seam, stored under the active `$DSH_HOME/settings.yaml`. The connection plugin publishes whether the current page uses a loopback authority as `ctx.connection.isLoopback`; hostname classification remains internal to the connection package, and other client plugins consume the service state instead of importing its implementation. A loopback browser shows the notice unless `welcomeNoticeVersion` equals the owner constant exactly. Continue applies one path mutation with the current version and calls `complete()` only after the Host commits it; a failed write leaves the notice open, and closing the page or process writes nothing. Bumping the constant intentionally makes every profile acknowledge the revised copy once. A non-loopback browser must not call the loopback-only settings API. It presents the same notice, but explicit Continue completes the step only in the current browser process; reload or a new process presents it again. +**The durable `ui-onboarding` section outlives the notice.** The Host half registers it in the user-settings seam under the active `$DSH_HOME/settings.yaml`; its `welcomeNoticeVersion` field keeps stored acknowledgements valid and has no reader. The connection plugin publishes whether the current page uses a loopback authority as `ctx.connection.isLoopback`; hostname classification remains internal to the connection package, and other client plugins consume the service state instead of importing its implementation. The API proxy exposes this one product namespace through a closed allowlist beside configurable-provider namespaces, without treating its changes as model-catalog invalidations. -**Concurrent loopback views converge without stale replacement.** The acknowledgement write omits `expectedRevision` deliberately: every loopback tab writes the same version to one path, so the operation is idempotent and preserves sibling fields instead of rebuilding the section. `settings/document-updated` reaches the client as an invalidation — through `host/settings-changed` then, and forwarded verbatim now ([forwarded Remote events](../architecture/2026-08-10-remote-event-delivery.md)); an already mounted loopback tab refetches and advances when another tab or an external editor commits the current version. The API proxy exposes this one product namespace through a closed allowlist beside configurable-provider namespaces, without treating its changes as model-catalog invalidations. - -**Onboarding temporarily owns the viewport as one continuous stage.** A solid product surface replaces the complete application view through a body-level portal and marks the underlying app root inert; the exact required mask remains mounted behind that surface with `position:absolute`, zero left/right/bottom offsets, `top:80px`, `rgba(0, 0, 0, 0.24)`, and `backdrop-filter: blur(2px)`. Welcome and conditional credential setup render as successive pages in this stage instead of independent modals. Both pages reuse the Web UI's black `BrandWordmark`. The welcome page preserves the four authored paragraphs verbatim under the `内测声明` title; every paragraph uses one 16/28 body scale, and only the requested action clause inside the final paragraph receives a subtle 500 weight. A short staggered opacity/vertical entrance supplies pacing without blocking interaction and disappears under reduced motion. The title receives initial focus, Continue is the sole button, and no close, Escape, or mask-click path exists. +**Onboarding temporarily owns the viewport as one continuous stage.** A solid product surface replaces the complete application view through a body-level portal and marks the underlying app root inert; the exact required mask remains mounted behind that surface with `position:absolute`, zero left/right/bottom offsets, `top:80px`, `rgba(0, 0, 0, 0.24)`, and `backdrop-filter: blur(2px)`. Onboarding steps render as successive pages in this stage instead of independent modals, reusing the Web UI's black `BrandWordmark`; the conditional credential setup is the only shipped page. ## Alternatives considered @@ -32,4 +30,4 @@ The GUI's credential onboarding begins with a DeepSeek-specific readiness check, ## Consequences -A fresh profile always sees the welcome notice before provider-specific onboarding; an already configured credential skips only the later DeepSeek step. On loopback, reloading after Continue stays past the acknowledged version, changing the owner version presents it again, and closing before Continue leaves the next launch unchanged. On non-loopback, Continue advances the live process without a privileged settings request and reload presents the notice again. Focused store and React tests pin both persistence modes, exact-version comparison, write failure, sole-action behavior, no-dismiss paths, coordinator ordering, conditional DeepSeek transfer, and HMR cleanup. The real Chromium scenario boots the shipped Web composition with an isolated harness home, verifies the exact mask geometry and computed styles, reloads before and after acknowledgement, continues into missing-credential setup, confirms an acknowledged-version mismatch returns while the credential is configured, and checks the browser console. +A fresh profile proceeds directly to provider-specific onboarding: the conditional DeepSeek step mounts when its credential is missing, and an already configured credential shows no onboarding page at all. Focused store and React tests pin coordinator ordering, conditional DeepSeek transfer, and HMR cleanup. The real Chromium scenario boots the shipped Web composition with an isolated harness home, verifies the exact mask geometry and computed styles while the credential step owns the viewport, continues into missing-credential setup, and checks the browser console. diff --git a/.agents/notes/implemented/feature/2026-07-30-versioned-gui-welcome-onboarding.zh.md b/.agents/notes/implemented/feature/2026-07-30-versioned-gui-welcome-onboarding.zh.md index 9f5eb99033..646080b893 100644 --- a/.agents/notes/implemented/feature/2026-07-30-versioned-gui-welcome-onboarding.zh.md +++ b/.agents/notes/implemented/feature/2026-07-30-versioned-gui-welcome-onboarding.zh.md @@ -10,15 +10,13 @@ GUI 的凭据引导从 DeepSeek 专用的就绪状态检查开始,但内部测 ## 决策 -**设置外壳协调有序步骤。** `settings.onboarding` 仍是根作用域 list,但 `ui-settings` 会把其中各条目的 id 和顺序投影到一个协调器中,并且只挂载第一个未完成的步骤。当前注册方会收到 `complete()` 和 `openSection(id)`;所有权转移前,不会挂载后续步骤。产品欢迎步骤的顺序为 `-100`,`ui-settings-models` 则只保留顺序为 `0` 的 DeepSeek 条件式就绪状态与凭据跳转步骤。 +**设置外壳协调有序步骤。** `settings.onboarding` 仍是根作用域 list,但 `ui-settings` 会把其中各条目的 id 和顺序投影到一个协调器中,并且只挂载第一个未完成的步骤。当前注册方会收到 `complete()` 和 `openSection(id)`;所有权转移前,不会挂载后续步骤。`ui-settings-models` 注册顺序为 `0` 的 DeepSeek 条件式就绪状态与凭据跳转步骤,自[移除首次启动内测声明](../simplification/2026-08-13-remove-first-run-beta-notice.md)起,它是当前唯一的注册方。 -**不属于单一功能的产品引导由 `ui-settings-general` 持有。** `src/onboarding-copy.ts` 是完整通知、「继续」按钮文案和 `WELCOME_NOTICE_VERSION` 的唯一可编辑来源;GUI 支持的两种 locale 都有意渲染同一份中文所有者文案。运行时 locale 字典从该文件派生欢迎文案,测试也导入同一个所有者,而不重复段落文本。该通知只存在于浏览器 UI:它不会创建会话事件,也不会贡献任何模型可见内容。通知说明会话遥测[默认禁用](2026-08-10-telemetry-default-off.md),并列出 `FEEDBACK_ONLY` 和 `FULL` 两种显式启用模式。 +**产品欢迎步骤已移除。** 版本化通知、其文案所有者文件和确认 store 自本决策起随产品发布,直至[移除首次启动内测声明](../simplification/2026-08-13-remove-first-run-beta-notice.md);移除理由由该 note 持有。`ui-settings-general` 不再注册任何引导步骤。 -**loopback 确认状态按 Harness profile 持久化。** 宿主端在 user-settings seam 中注册 `ui-onboarding` 分节,并存入当前 `$DSH_HOME/settings.yaml`。connection 插件通过 `ctx.connection.isLoopback` 统一发布当前页面是否使用 loopback authority;hostname 判定留在 connection 包内,其他客户端插件只消费服务状态,而不导入其实现。除非 `welcomeNoticeVersion` 与文案所有者文件中的常量精确相等,否则 loopback 浏览器会显示通知。「继续」会以当前版本执行一次路径变更,并且仅在宿主端提交成功后调用 `complete()`;写入失败时通知保持打开,关闭页面或进程则不会写入任何内容。更新该常量会有意要求每个 profile 对修订后的文案重新确认一次。非 loopback 浏览器不能调用仅限 loopback 的 settings API;它仍显示同一通知,但显式点击「继续」只会在当前浏览器进程中完成该步骤,重新加载或新进程会再次显示通知。 +**持久化的 `ui-onboarding` 分节在通知移除后继续存在。** 宿主端在 user-settings seam 中注册它,存入当前 `$DSH_HOME/settings.yaml`;其中的 `welcomeNoticeVersion` 字段让已存储的确认记录保持有效,没有读取方。connection 插件通过 `ctx.connection.isLoopback` 统一发布当前页面是否使用 loopback authority;hostname 判定留在 connection 包内,其他客户端插件只消费服务状态,而不导入其实现。API Proxy 在可配置提供方 namespace 之外,通过封闭的允许列表暴露这一个产品 namespace,同时不会把它的变更视为模型目录失效事件。 -**并发 loopback 视图无需陈旧的整体替换即可收敛。** 确认写入有意省略 `expectedRevision`:每个 loopback 标签页都向同一路径写入相同版本,因此该操作是幂等的,并会保留同级字段,而不是重建整个分节。`settings/document-updated` 以失效通知形式到达客户端——当时经 `host/settings-changed`,现在则是原样转发([转发的 Remote 事件](../architecture/2026-08-10-remote-event-delivery.md));另一个标签页或外部编辑器提交当前版本后,已挂载的 loopback 标签页会重新拉取状态并推进。API Proxy(`@deepseek-ai/dsh-host-apiproxy`)在可配置提供方 namespace 之外,通过封闭的允许列表暴露这一个产品 namespace,同时不会把它的变更视为模型目录失效事件。 - -**引导流程会暂时接管视口,形成一个连续阶段。** 纯色产品界面通过挂载到 `body` 的 portal 取代完整的应用视图,并将底层应用根节点标记为 inert;严格符合要求的遮罩仍挂载在该界面后方,并保留 `position:absolute`、left/right/bottom 偏移量为零、`top:80px`、`rgba(0, 0, 0, 0.24)` 和 `backdrop-filter: blur(2px)`。欢迎页和按条件显示的凭据设置页在这一阶段中依次呈现,而不是各自作为独立的模态窗口。两个页面都复用 Web UI 的黑色 `BrandWordmark`。欢迎页在 `内测声明` 标题下逐字保留既定的四段文案;所有段落统一采用 16/28 的正文字号与行高,只有最后一段中指定的行动语句使用较为克制的 500 字重。短暂的错落式透明度与纵向位移动画营造出节奏感,但不会阻碍交互,并会在用户启用减少动态效果时禁用。初始焦点落在标题上,「继续」是唯一按钮,且不存在关闭、Escape 或点击遮罩的退出路径。 +**引导流程会暂时接管视口,形成一个连续阶段。** 纯色产品界面通过挂载到 `body` 的 portal 取代完整的应用视图,并将底层应用根节点标记为 inert;严格符合要求的遮罩仍挂载在该界面后方,并保留 `position:absolute`、left/right/bottom 偏移量为零、`top:80px`、`rgba(0, 0, 0, 0.24)` 和 `backdrop-filter: blur(2px)`。引导步骤在这一阶段中依次呈现,而不是各自作为独立的模态窗口,并复用 Web UI 的黑色 `BrandWordmark`;按条件显示的凭据设置页是当前唯一的页面。 ## 曾考虑的替代方案 @@ -32,4 +30,4 @@ GUI 的凭据引导从 DeepSeek 专用的就绪状态检查开始,但内部测 ## 后果 -全新 profile 始终会在提供方专用引导之前看到欢迎通知;凭据已经配置时,只会跳过后续 DeepSeek 步骤。在 loopback 上,点击「继续」后重新加载不会再次显示已确认版本,更改文案所有者文件中的版本值会让通知重新出现,而确认前关闭窗口不会改变下次启动。在非 loopback 上,「继续」会在不发起受保护 settings 请求的情况下推进当前进程,重新加载则再次显示通知。针对性的 store 与 React 测试固化了两种持久化模式、精确版本比较、写入失败、单一操作、不可关闭路径、协调器顺序、按条件移交 DeepSeek 步骤和 HMR(热模块替换)清理行为。真实 Chromium 场景会使用隔离的 harness 家目录启动随产品提供的 Web 组合,验证遮罩的精确几何尺寸和计算样式,在确认前后分别重新加载,继续进入凭据缺失设置流程,确认凭据已配置时确认版本不匹配仍会使通知重新出现,并检查浏览器控制台。 +全新 profile 直接进入提供方专用引导:DeepSeek 步骤仅在其凭据缺失时挂载,凭据已配置时不会出现任何引导页面。针对性的 store 与 React 测试固化了协调器顺序、按条件移交 DeepSeek 步骤和 HMR(热模块替换)清理行为。真实 Chromium 场景会使用隔离的 harness 家目录启动随产品提供的 Web 组合,在凭据步骤占据视口时验证遮罩的精确几何尺寸和计算样式,继续进入凭据缺失设置流程,并检查浏览器控制台。 diff --git a/.agents/notes/implemented/feature/2026-08-10-telemetry-default-off.i18n.yaml b/.agents/notes/implemented/feature/2026-08-10-telemetry-default-off.i18n.yaml index be70f91180..7a3b82d3a6 100644 --- a/.agents/notes/implemented/feature/2026-08-10-telemetry-default-off.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-08-10-telemetry-default-off.i18n.yaml @@ -2,5 +2,5 @@ # 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 .agents/notes/implemented/feature/2026-08-10-telemetry-default-off.md -2026-08-10-telemetry-default-off.md: a4f0099565bb240cf62774fab2d5ad5f7de90e95 -2026-08-10-telemetry-default-off.zh.md: 8e2544eb7dee8b9bd6b8a4c81a28a8a03d3404d4 +2026-08-10-telemetry-default-off.md: 1979597eb81ecc7413996b7eb4fbe0e270abc629 +2026-08-10-telemetry-default-off.zh.md: 25d675c69568a30ad754b301139b30ef0e8ffe3d diff --git a/.agents/notes/implemented/feature/2026-08-10-telemetry-default-off.md b/.agents/notes/implemented/feature/2026-08-10-telemetry-default-off.md index a4f0099565..1979597eb8 100644 --- a/.agents/notes/implemented/feature/2026-08-10-telemetry-default-off.md +++ b/.agents/notes/implemented/feature/2026-08-10-telemetry-default-off.md @@ -14,7 +14,7 @@ Both feeds use `DSH_TELEMETRY_MODE` as their positive consent setting. Unset and The dsh-sdk launcher reads the same variable without parsing `cordis.yml` or booting Cordis. `FULL` permits reporting; `FEEDBACK_ONLY`, `DISABLED`, unset, and empty values deny it. Consent is frozen from the launching environment before the command runs, because `dsh-sdk start` loads a project `.env` and project code can mutate `process.env`: resolving afterwards would let a project grant reporting of its own configuration, which the [configuration source ownership decision](../architecture/2026-08-04-configuration-source-ownership.md) denies for the whole `DSH_*` namespace. An unsupported mode denies rather than throwing at that boundary, since telemetry may never change a command's result. This rule superseded the default-on launcher consent before the launcher and its proposal were deleted by the [SDK project toolchain removal](../simplification/2026-08-11-remove-sdk-project-toolchain.md). -The versioned Web welcome notice states that Session Log upload is off by default, names `DSH_TELEMETRY_MODE=FEEDBACK_ONLY` and `DSH_TELEMETRY_MODE=FULL` as the two opt-in choices, and discloses that `FULL` also enables dsh-sdk command telemetry. Its version changes with that material privacy statement so every profile acknowledges the current copy. +The repository README states that Session Log upload is off by default, names `DSH_TELEMETRY_MODE=FEEDBACK_ONLY` and `DSH_TELEMETRY_MODE=FULL` as the two opt-in choices, and discloses that `FULL` also enables dsh-sdk command telemetry. The product itself presents no prompt about enabling telemetry since the [first-run beta notice removal](../simplification/2026-08-13-remove-first-run-beta-notice.md). ## Alternatives considered diff --git a/.agents/notes/implemented/feature/2026-08-10-telemetry-default-off.zh.md b/.agents/notes/implemented/feature/2026-08-10-telemetry-default-off.zh.md index 8e2544eb7d..25d675c695 100644 --- a/.agents/notes/implemented/feature/2026-08-10-telemetry-default-off.zh.md +++ b/.agents/notes/implemented/feature/2026-08-10-telemetry-default-off.zh.md @@ -14,7 +14,7 @@ DeepSeek Harness 有两路出站遥测数据流。在内测阶段,共享基础 dsh-sdk 启动器读取同一变量,不解析 `cordis.yml`,也不启动 Cordis。`FULL` 允许上报;`FEEDBACK_ONLY`、`DISABLED`、未设置和空值都会拒绝。授权在命令执行前从启动环境冻结:`dsh-sdk start` 会加载项目 `.env`,项目代码也能修改 `process.env`,若在执行后解析,项目便能自行授权上报其自身配置,而[配置来源所有权决策](../architecture/2026-08-04-configuration-source-ownership.md)对整个 `DSH_*` 命名空间禁止这种行为。在该边界上,不受支持的模式按拒绝处理而非抛出,因为遥测不得改变命令结果。此规则在启动器及其提案被[SDK 项目工具链移除决策](../simplification/2026-08-11-remove-sdk-project-toolchain.md)删除之前,仅取代了启动器默认允许上报的规则。 -带版本的 Web 欢迎通知说明会话日志上传默认关闭,将 `DSH_TELEMETRY_MODE=FEEDBACK_ONLY` 和 `DSH_TELEMETRY_MODE=FULL` 列为两种显式启用选项,并披露 `FULL` 同时会启用 dsh-sdk 命令遥测。其版本随这项重要的隐私声明一同变更,使每个 profile 都确认当前文案。 +仓库 README 说明会话日志上传默认关闭,将 `DSH_TELEMETRY_MODE=FEEDBACK_ONLY` 和 `DSH_TELEMETRY_MODE=FULL` 列为两种显式启用选项,并披露 `FULL` 同时会启用 dsh-sdk 命令遥测。自[移除首次启动内测声明](../simplification/2026-08-13-remove-first-run-beta-notice.md)起,产品本身不提供任何关于开启遥测的提示。 ## 考虑过的替代方案 diff --git a/.agents/notes/implemented/simplification/2026-08-13-remove-first-run-beta-notice.i18n.yaml b/.agents/notes/implemented/simplification/2026-08-13-remove-first-run-beta-notice.i18n.yaml new file mode 100644 index 0000000000..a040b192f2 --- /dev/null +++ b/.agents/notes/implemented/simplification/2026-08-13-remove-first-run-beta-notice.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 .agents/notes/implemented/simplification/2026-08-13-remove-first-run-beta-notice.md +2026-08-13-remove-first-run-beta-notice.md: e0876e5e96e04fed5e47adc0f83b1c7bfa4f2519 +2026-08-13-remove-first-run-beta-notice.zh.md: 8598fc28dda1ff5404e3c06fc8e2aaadca4cd471 diff --git a/.agents/notes/implemented/simplification/2026-08-13-remove-first-run-beta-notice.md b/.agents/notes/implemented/simplification/2026-08-13-remove-first-run-beta-notice.md new file mode 100644 index 0000000000..e0876e5e96 --- /dev/null +++ b/.agents/notes/implemented/simplification/2026-08-13-remove-first-run-beta-notice.md @@ -0,0 +1,25 @@ +# Agent Note: Remove the first-run beta notice + +Status: implemented + +English | [中文](2026-08-13-remove-first-run-beta-notice.zh.md) + +## Problem + +Every GUI first launch opened with a full-viewport internal-test statement (内测声明): internal-beta framing plus instructions for enabling Session Log upload through `DSH_TELEMETRY_MODE`. Session telemetry already resolves to `DISABLED` when its mode is unset ([telemetry default-off](../feature/2026-08-10-telemetry-default-off.md)), so the only onboarding content about telemetry was a prompt explaining how to turn it on, and the internal-test framing itself must not ship in a release build. + +## Decision + +The first-run notice is removed from the assembled product rather than reworded. `ui-settings-general` seats no `settings.onboarding` step; the notice component, its durable acknowledgement store, its copy owner, and its locale keys are deleted. The `settings.onboarding` coordinator and its takeover stage stay ([ordered onboarding](../feature/2026-07-30-versioned-gui-welcome-onboarding.md)), and the conditional DeepSeek credential step is the only shipped occupant. The Host half still registers the `ui-onboarding` settings namespace: its `welcomeNoticeVersion` field keeps acknowledgements already stored in `$DSH_HOME/settings.yaml` valid, and nothing reads or writes it. Telemetry opt-in remains an explicit deployment environment choice documented in the repository README; the product presents no prompt about enabling it. + +## Alternatives considered + +**Keep the notice and only drop its telemetry paragraph.** Rejected: the internal-test framing is what a release must not present, and a mandatory first-run interstitial with no material statement left is pure friction. + +**Ask for upload consent instead (a versioned consent step).** Rejected for this release: a first-run question about enabling upload is still a telemetry prompt. A future consent flow can register through the unchanged `settings.onboarding` seam and use a fresh versioned field for re-acknowledgement. + +**Deregister the `ui-onboarding` namespace as well.** Rejected: existing settings documents already carry the section, and the settings seam validates stored documents against registered namespaces; keeping the registration keeps those documents valid at no cost. + +## Consequences + +A fresh profile boots into the credential step when the DeepSeek credential is missing and directly into the product otherwise; no full-viewport notice precedes either. The assembled onboarding scenario starts at the credential step, the remote-notice scenario is deleted with the feature, and the goal-bar fixture keeps the settings shell disabled because the fixture API client rejects settings traffic. Restoring a first-run notice requires a new onboarding registration and a new versioned field; the retained namespace does not resurrect the old acknowledgement semantics. diff --git a/.agents/notes/implemented/simplification/2026-08-13-remove-first-run-beta-notice.zh.md b/.agents/notes/implemented/simplification/2026-08-13-remove-first-run-beta-notice.zh.md new file mode 100644 index 0000000000..8598fc28dd --- /dev/null +++ b/.agents/notes/implemented/simplification/2026-08-13-remove-first-run-beta-notice.zh.md @@ -0,0 +1,25 @@ +# Agent Note: 移除首次启动内测声明 + +Status: implemented + +[English](2026-08-13-remove-first-run-beta-notice.md) | 中文 + +## 问题 + +GUI 每次首启都会先显示占满视口的内测声明:内部测试的定位表述,加上通过 `DSH_TELEMETRY_MODE` 开启 Session Log 上传的说明。会话遥测在 mode 未设置时已解析为 `DISABLED`([遥测默认关闭](../feature/2026-08-10-telemetry-default-off.md)),因此引导流程中关于遥测的全部内容就是一段教用户如何开启的提示,而内部测试的定位表述本身也不应出现在发布版本里。 + +## 决策 + +首启声明从组装后的产品中整体移除,而不是改写。`ui-settings-general` 不再注册任何 `settings.onboarding` 步骤;声明组件、其持久化确认 store、文案所有者文件和 locale 键全部删除。`settings.onboarding` 协调器及其接管式展示阶段保留([有序引导](../feature/2026-07-30-versioned-gui-welcome-onboarding.md)),按条件显示的 DeepSeek 凭据步骤是当前唯一的注册方。宿主端仍注册 `ui-onboarding` 设置 namespace:其中的 `welcomeNoticeVersion` 字段让 `$DSH_HOME/settings.yaml` 中已写入的确认记录保持有效,没有任何代码读取或写入它。遥测的开启仍是显式的部署环境变量选择,记录在仓库 README 中;产品界面不出现任何关于开启遥测的提示。 + +## 曾考虑的替代方案 + +**保留声明,只删除其中的遥测段落。** 不予采用:发布版本不应呈现的正是内部测试的定位表述本身,而一个没有实质内容的强制首启插页只剩下打扰。 + +**改为询问上传同意(版本化的同意步骤)。** 本次发布不予采用:首启询问是否开启上传仍然是一个遥测提示。未来的同意流程可以通过保持不变的 `settings.onboarding` seam 注册,并使用新的版本化字段做重新确认。 + +**连 `ui-onboarding` namespace 一起注销。** 不予采用:既有设置文档已经包含该分节,而设置 seam 会用已注册的 namespace 校验存储文档;保留注册就能让这些文档继续有效,且没有额外成本。 + +## 后果 + +全新 profile 在缺少 DeepSeek 凭据时直接进入凭据步骤,凭据已配置时直接进入产品,两种情况都没有占满视口的声明。组装级引导场景从凭据步骤开始,远程声明场景随功能一并删除,goal-bar fixture 继续禁用设置外壳,因为 fixture API 客户端会拒绝设置请求。将来若要恢复首启声明,需要新的引导注册和新的版本化字段;保留的 namespace 不会复活旧的确认语义。 diff --git a/apps/web/tests/README.i18n.yaml b/apps/web/tests/README.i18n.yaml index a6005ade32..fd0b89b541 100644 --- a/apps/web/tests/README.i18n.yaml +++ b/apps/web/tests/README.i18n.yaml @@ -2,5 +2,5 @@ # 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 apps/web/tests/README.md -README.md: 68e5db5af5f816cc982bacb7989d996c859be204 -README.zh.md: f366c28024dab89d0243a60d93a706f220fa8fb8 +README.md: 4d7236ef69189845dbc2847289d497fc54c94ae9 +README.zh.md: 21c2f2ce7ccc8fe5314d766c06235453aa292031 diff --git a/apps/web/tests/README.md b/apps/web/tests/README.md index 68e5db5af5..4d7236ef69 100644 --- a/apps/web/tests/README.md +++ b/apps/web/tests/README.md @@ -29,9 +29,8 @@ artifact it produces itself. When a scenario needs a Client-owned constant or pure function, mirror it here instead, next to the commented-out import that names the source module. A drift -then surfaces as a missed selector or an unsuppressed notice — a loud failure, -never a silent pass. `scaffold.ts` holds the mirrored welcome-notice values and -exports them for the scenarios that assert on them. +then surfaces as a missed selector or a stale mirrored value — a loud failure, +never a silent pass. Two kinds of Client import stand. `assembled-boot.ts` drives the shell itself, so it imports `AppWebEntry` from `@deepseek-ai/dsh-client-web` and the boot-manifest diff --git a/apps/web/tests/README.zh.md b/apps/web/tests/README.zh.md index f366c28024..21c2f2ce7c 100644 --- a/apps/web/tests/README.zh.md +++ b/apps/web/tests/README.zh.md @@ -23,8 +23,8 @@ Client face,而该 face 必须等 Host tsdown 生成 `@deepseek-ai/dsh-goal/re 于是 Host 构建阶段变成在等一个由它自己产出的产物。 当某个场景需要 Client 持有的常量或纯函数时,改为在此处镜像一份,并紧挨着一条注释掉的 -import 点明源模块。这样漂移会表现为选择器未命中或提示未被抑制——是响亮的失败,绝不会是静默 -通过。`scaffold.ts` 持有镜像的 welcome-notice 取值,并导出给断言它们的场景使用。 +import 点明源模块。这样漂移会表现为选择器未命中或镜像值过期——是响亮的失败,绝不会是静默 +通过。 有两类 Client import 是长期成立的。`assembled-boot.ts` 驱动 shell 本身,因此它从 `@deepseek-ai/dsh-client-web` import `AppWebEntry`、从 diff --git a/apps/web/tests/goal-bar.e2e.ts b/apps/web/tests/goal-bar.e2e.ts index 00260a79ee..f4a0d8d34b 100644 --- a/apps/web/tests/goal-bar.e2e.ts +++ b/apps/web/tests/goal-bar.e2e.ts @@ -26,7 +26,7 @@ describe('web e2e: goal bar clear convergence', () => { let tripwire: ReturnType beforeAll(async () => { - scaffold = await launchWebScaffold({ extraOverlayPath: OVERLAY, welcomeNoticePending: true }) + scaffold = await launchWebScaffold({ extraOverlayPath: OVERLAY }) browser = await chromium.launch() page = await newEnglishPage(browser) tripwire = watchConsole(page) diff --git a/apps/web/tests/goal-bar.overlay.yml b/apps/web/tests/goal-bar.overlay.yml index 2594d6a3e9..4e9e382bb8 100644 --- a/apps/web/tests/goal-bar.overlay.yml +++ b/apps/web/tests/goal-bar.overlay.yml @@ -1,5 +1,5 @@ -# The client-side FixtureApiClient intentionally rejects settings writes, so -# this goal-only scenario omits the durable welcome step that would otherwise -# cover the page. Onboarding owns separate assembled-browser coverage. +# The client-side FixtureApiClient intentionally rejects settings traffic, so +# this goal-only scenario omits the settings shell and the onboarding steps it +# would mount. Onboarding owns separate assembled-browser coverage. - id: ui-settings-general disabled: true diff --git a/apps/web/tests/onboarding-deepseek-config.e2e.ts b/apps/web/tests/onboarding-deepseek-config.e2e.ts index 5572e834ad..08cfeda4d3 100644 --- a/apps/web/tests/onboarding-deepseek-config.e2e.ts +++ b/apps/web/tests/onboarding-deepseek-config.e2e.ts @@ -10,15 +10,11 @@ import { chromium } from 'playwright' import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest' import { acknowledgeReloadConnectionLoss, assertFixtureInventory, captureStableAria, compareOrRefreshGolden, - WELCOME_NOTICE_SETTINGS_NAMESPACE, WELCOME_NOTICE_ACK_FIELD, - WELCOME_NOTICE_VERSION, WELCOME_NOTICE_COPY, launchWebScaffold, watchConsole, webSnapshotMode, type WebScaffold, } from './scaffold.ts' import { ZH_BROWSER_LOCALE, connectFreshWorkspaceZh, saveFailureShot } from './support.ts' -import { settingsNamespace } from '@deepseek-ai/dsh-settings' const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/onboarding-deepseek-config', import.meta.url)) -const WELCOME_EXPECTED = join(SNAPSHOT_DIR, 'welcome.expected.md') const MISSING_EXPECTED = join(SNAPSHOT_DIR, 'missing.expected.md') const MODELS_EXPECTED = join(SNAPSHOT_DIR, 'models.expected.md') const MODE = webSnapshotMode() @@ -31,7 +27,7 @@ describe.skipIf(MODE === 'record')('web e2e: first-run DeepSeek credential setup const browserConsole: string[] = [] beforeAll(async () => { - scaffold = await launchWebScaffold({ deepSeekMissingCredential: true, welcomeNoticePending: true }) + scaffold = await launchWebScaffold({ deepSeekMissingCredential: true }) browser = await chromium.launch() // The scenario asserts the shipped Chinese copy, so the browser asks for it. page = await browser.newPage({ viewport: { width: 1440, height: 960 }, locale: ZH_BROWSER_LOCALE }) @@ -48,13 +44,9 @@ describe.skipIf(MODE === 'record')('web e2e: first-run DeepSeek credential setup it('stores a key write-only and observes configured state without restarting', async () => { onTestFailed(() => saveFailureShot(page, 'web-e2e-onboarding-deepseek-config')) - const welcome = page.getByRole('region', { name: WELCOME_NOTICE_COPY.zh.title }) - await welcome.waitFor({ timeout: 15_000 }) + const credentialStep = page.getByRole('region', { name: '添加一个 API Key 开始使用' }) + await credentialStep.waitFor({ timeout: 15_000 }) expect(await page.locator('#root').evaluate(root => (root as HTMLElement).inert)).toBe(true) - const welcomeAria = await captureStableAria(page, '[role="region"]', scaffold.workspaceCwd) - await compareOrRefreshGolden(WELCOME_EXPECTED, welcomeAria, MODE) - expect(await welcome.getByRole('button').allTextContents()).toEqual([WELCOME_NOTICE_COPY.zh.continueLabel]) - expect(await welcome.locator('button').count()).toBe(1) const mask = page.locator('[class*="onboardingMask"]') expect(await mask.count()).toBe(1) @@ -83,17 +75,6 @@ describe.skipIf(MODE === 'record')('web e2e: first-run DeepSeek credential setup rect: { left: 0, top: 80, right: 1440, bottom: 960 }, }) - // Closing the process/page before acknowledgement writes nothing, so the - // same durable profile presents the notice again after reload. - const firstReloadWarnings = tripwire.warnings.length - await page.reload({ waitUntil: 'load' }) - acknowledgeReloadConnectionLoss(tripwire, firstReloadWarnings) - await welcome.waitFor({ timeout: 15_000 }) - - await welcome.getByRole('button', { name: WELCOME_NOTICE_COPY.zh.continueLabel }).click() - await welcome.waitFor({ state: 'detached', timeout: 15_000 }) - const credentialStep = page.getByRole('region', { name: '添加一个 API Key 开始使用' }) - await credentialStep.waitFor({ timeout: 15_000 }) expect(await credentialStep.getByRole('textbox').count()).toBe(0) const initial = await captureStableAria(page, '[role="region"]', scaffold.workspaceCwd) await compareOrRefreshGolden(MISSING_EXPECTED, initial, MODE) @@ -129,27 +110,10 @@ describe.skipIf(MODE === 'record')('web e2e: first-run DeepSeek credential setup { timeout: 10_000 }, ).toBe('已配置——输入新值可替换') - const acknowledgedSettings = await readFile(join(scaffold.harnessHome, 'settings.yaml'), 'utf8') - expect(acknowledgedSettings).toContain(`${WELCOME_NOTICE_ACK_FIELD}: ${WELCOME_NOTICE_VERSION}`) - const secondReloadWarnings = tripwire.warnings.length await page.reload({ waitUntil: 'load' }) acknowledgeReloadConnectionLoss(tripwire, secondReloadWarnings) await page.waitForSelector('[class*="frame"]', { timeout: 15_000 }) - expect(await page.getByRole('region', { name: WELCOME_NOTICE_COPY.zh.title }).count()).toBe(0) - expect(await page.getByRole('region', { name: '添加一个 API Key 开始使用' }).count()).toBe(0) - - // A different stored copy version represents an intentional version bump: - // the welcome step returns even though the credential is already ready. - await scaffold.ctx.settings.mutate(settingsNamespace(WELCOME_NOTICE_SETTINGS_NAMESPACE), [{ - op: 'set', path: [WELCOME_NOTICE_ACK_FIELD], value: 'previous-copy-version', - }]) - const thirdReloadWarnings = tripwire.warnings.length - await page.reload({ waitUntil: 'load' }) - acknowledgeReloadConnectionLoss(tripwire, thirdReloadWarnings) - await welcome.waitFor({ timeout: 15_000 }) - await welcome.getByRole('button', { name: WELCOME_NOTICE_COPY.zh.continueLabel }).click() - await welcome.waitFor({ state: 'detached', timeout: 15_000 }) expect(await page.getByRole('region', { name: '添加一个 API Key 开始使用' }).count()).toBe(0) expect((await page.content()).includes(secret)).toBe(false) @@ -161,8 +125,8 @@ describe.skipIf(MODE === 'record')('web e2e: first-run DeepSeek credential setup it('never paints the takeover chrome on a configured reload, even with the settings join held open', async () => { onTestFailed(() => saveFailureShot(page, 'web-e2e-onboarding-configured-reload')) - // Regression pin for the reload white flash: both steps are satisfied - // (welcome acknowledged, credential configured), yet each must LOAD its + // Regression pin for the reload white flash: the credential step is + // satisfied (credential configured), yet it must LOAD its // private join before it can decide not to show. The chrome lives inside // the step (OnboardingSurface), so the deciding window paints and blocks // nothing. Holding settings.describe widens that window from loopback @@ -214,7 +178,7 @@ describe.skipIf(MODE === 'record')('web e2e: first-run DeepSeek credential setup it('configures arbitrary DeepSeek models and prompts after the selected model is removed', async () => { onTestFailed(() => saveFailureShot(page, 'web-e2e-onboarding-deepseek-models')) // Opened here rather than inherited: the credential test reloads the page - // to exercise the welcome step, so nothing carries an open dialog across. + // after configuring the key, so nothing carries an open dialog across. await page.getByRole('button', { name: '设置', exact: true }).click() const settings = page.getByRole('dialog', { name: '设置' }) await settings.waitFor({ timeout: 10_000 }) @@ -264,7 +228,7 @@ describe.skipIf(MODE === 'record')('web e2e: first-run DeepSeek credential setup it('keeps the fixture inventory closed', async () => { await assertFixtureInventory( SNAPSHOT_DIR, - ['missing.expected.md', 'models.expected.md', 'welcome.expected.md'], + ['missing.expected.md', 'models.expected.md'], ) }) }) diff --git a/apps/web/tests/remote-welcome.e2e.ts b/apps/web/tests/remote-welcome.e2e.ts deleted file mode 100644 index 7187936ae9..0000000000 --- a/apps/web/tests/remote-welcome.e2e.ts +++ /dev/null @@ -1,53 +0,0 @@ -// Trusted non-loopback Web access must not wedge on the loopback-only -// settings API while the mandatory product notice owns the viewport. -import type { Browser, Page } from 'playwright' -import { chromium } from 'playwright' -import { afterAll, beforeAll, describe, expect, it } from 'vitest' -import { - acknowledgeReloadConnectionLoss, launchWebScaffold, watchConsole, webSnapshotMode, - WELCOME_NOTICE_COPY, - type WebScaffold, -} from './scaffold.ts' -import { ZH_BROWSER_LOCALE } from './support.ts' - -const MODE = webSnapshotMode() - -describe.skipIf(MODE === 'record')('web e2e: remote welcome notice', () => { - let scaffold: WebScaffold - let browser: Browser - let page: Page - let tripwire: ReturnType - - beforeAll(async () => { - scaffold = await launchWebScaffold({ remoteAuthority: 'remote.localhost', welcomeNoticePending: true }) - browser = await chromium.launch() - page = await browser.newPage({ viewport: { width: 1440, height: 960 }, locale: ZH_BROWSER_LOCALE }) - tripwire = watchConsole(page) - await page.goto(scaffold.baseUrl, { waitUntil: 'load' }) - await page.waitForSelector('#root', { timeout: 30_000 }) - }, 120_000) - - afterAll(async () => { - await browser?.close() - await scaffold?.close() - }) - - it('advances process-locally and presents the notice again after reload', async () => { - const welcome = page.getByRole('region', { name: WELCOME_NOTICE_COPY.zh.title }) - await welcome.waitFor({ timeout: 15_000 }) - expect(await page.locator('#root').evaluate(root => (root as HTMLElement).inert)).toBe(true) - - await welcome.getByRole('button', { name: WELCOME_NOTICE_COPY.zh.continueLabel }).click() - await welcome.waitFor({ state: 'detached', timeout: 15_000 }) - await expect.poll( - () => page.locator('#root').evaluate(root => (root as HTMLElement).inert), - { timeout: 15_000 }, - ).toBe(false) - const reloadWarnings = tripwire.warnings.length - await page.reload({ waitUntil: 'load' }) - acknowledgeReloadConnectionLoss(tripwire, reloadWarnings) - await welcome.waitFor({ timeout: 15_000 }) - expect(tripwire.warnings).toEqual([]) - expect(tripwire.pageErrors).toEqual([]) - }, 60_000) -}) diff --git a/apps/web/tests/scaffold.ts b/apps/web/tests/scaffold.ts index 970ff7b139..ecca13f0bc 100644 --- a/apps/web/tests/scaffold.ts +++ b/apps/web/tests/scaffold.ts @@ -41,20 +41,6 @@ import { loadOverlayPatches, } from '@deepseek-ai/dsh-app-boot' import { dshHomePath } from '@deepseek-ai/dsh-home-paths' -// Client packages must not be imported here: these e2e type-check in the Host -// aggregate, so a Client import pulls that package's whole project — and every -// project it references — into the Host build graph. Mirrored from -// packages/client/ui-settings-general/src/onboarding-copy.ts; a drift makes the -// pre-acknowledgement stop suppressing the notice, which fails loudly. -// import { -// WELCOME_NOTICE_ACK_FIELD, WELCOME_NOTICE_SETTINGS_NAMESPACE, WELCOME_NOTICE_VERSION, WELCOME_NOTICE_COPY, -// } from '@deepseek-ai/dsh-client-ui-settings-general' -export const WELCOME_NOTICE_SETTINGS_NAMESPACE = 'ui-onboarding' -export const WELCOME_NOTICE_ACK_FIELD = 'welcomeNoticeVersion' -export const WELCOME_NOTICE_VERSION = '2026-08-11.1' -export const WELCOME_NOTICE_COPY = { zh: { title: '内测声明', continueLabel: '继续' } } as const - -import { settingsNamespace } from '@deepseek-ai/dsh-settings' import { LlmAdapter } from '@deepseek-ai/dsh-llm' import type { LlmModelInfo, LlmProviderInfo, LlmResolvedModelInfo, StreamChunk, @@ -252,8 +238,6 @@ export interface LaunchOptions { /** The preset a session that names none is composed from. */ default: string } - /** Leave the current welcome notice unacknowledged; ordinary scenarios publish it as complete before browser boot. */ - welcomeNoticePending?: boolean /** * Mount the shipped telemetry row in FULL mode against this exporter URL * instead of disabling it. Used to pin a real backend disclosure in @@ -526,11 +510,6 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise - {paragraph.slice(0, index)} - {emphasis} - {paragraph.slice(index + emphasis.length)} - - ) -} - -/** Registrant-owned dependencies of {@link WelcomeNotice}. */ -export interface WelcomeNoticeInjected { - controller: WelcomeNoticeStore - useSnapshot: SnapshotSelectorHook -} - -/** Coordinator owner props plus the welcome step's injected face. */ -export type WelcomeNoticeProps = - PropsRuntime<'settings.onboarding'> & PropsLocale<'settings'> & WelcomeNoticeInjected - -/** Render the mandatory notice until its current version is acknowledged. */ -export function WelcomeNotice(props: WelcomeNoticeProps): ReactNode { - const { complete, controller, useSnapshot, t } = props - const state = useSnapshot(snapshot => snapshot) - const finished = useRef(false) - const titleRef = useRef(null) - const finish = useCallback((): void => { - if (finished.current) return - finished.current = true - complete() - }, [complete]) - - useEffect(() => { - if (state.status === 'idle') void controller.load() - }, [controller, state.status]) - - useEffect(() => { - if (state.acknowledged) finish() - }, [finish, state.acknowledged]) - - useEffect(() => { - if (state.status === 'ready' && !state.acknowledged) titleRef.current?.focus() - }, [state.acknowledged, state.status]) - - // Null while the acknowledgement fact is still loading (or already given): - // the takeover chrome below is part of THIS render, so deciding not to - // show paints and blocks nothing. - if (state.status === 'idle' || state.status === 'loading' || state.acknowledged) return null - - const acknowledge = async (): Promise => { - if (await controller.acknowledge()) finish() - } - - return ( - -
- -

{t('welcome.title')}

-

{t('welcome.paragraph.0')}

-
{t('welcome.paragraph.1')}
-

- {emphasizedFeedback(t('welcome.paragraph.2'), t('welcome.feedbackEmphasis'))} -

- {state.error === null ? null :

{t('welcome.error')}

} -
- -
-
-
- ) -} diff --git a/packages/client/ui-settings-general/src/client/index.ts b/packages/client/ui-settings-general/src/client/index.ts index 3a868dc3b4..e1a94934c7 100644 --- a/packages/client/ui-settings-general/src/client/index.ts +++ b/packages/client/ui-settings-general/src/client/index.ts @@ -17,9 +17,6 @@ import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-web-react' import type {} from '@deepseek-ai/dsh-client-ui-settings/client' // Type-only: pulls ctx.locale into this program. import type {} from '@deepseek-ai/dsh-client-locale/client' -// Type-only: pulls the ctx.remote merge and the forwarded-event key face -// (the settings invalidation rides the allowlist) into this program. -import type {} from '@deepseek-ai/dsh-api-remotes/client' import type { SettingsOnboardingStep, SettingsRootInjected, SettingsSectionRow, } from './shell-contract.ts' @@ -29,10 +26,6 @@ import { GeneralSection } from './GeneralSection.tsx' import { SettingsDocumentAction } from './SettingsDocumentAction.tsx' import type { SettingsDocumentActionInjected } from './SettingsDocumentAction.tsx' import { refreshDocumentIfLoaded, SettingsDocumentStore } from './settings-document-store.ts' -import type { WelcomeNoticeInjected } from './WelcomeNotice.tsx' -import { WelcomeNotice } from './WelcomeNotice.tsx' -import { refreshWelcomeIfLoaded, WelcomeNoticeStore } from './welcome-store.ts' -import { WELCOME_NOTICE_SETTINGS_NAMESPACE } from '../onboarding-copy.ts' import { en, zh, type SettingsKey } from './locales.ts' export type { @@ -44,8 +37,6 @@ export type { export type { SettingsDocumentActionInjected, SettingsDocumentActionProps } from './SettingsDocumentAction.tsx' export type { SettingsDocumentState } from './settings-document-store.ts' export { SettingsDocumentStore } from './settings-document-store.ts' -export type { WelcomeNoticeInjected, WelcomeNoticeProps } from './WelcomeNotice.tsx' -export type { WelcomeNoticeState } from './welcome-store.ts' export type { SettingsKey } from './locales.ts' declare module '@deepseek-ai/dsh-client-ui-slots' { @@ -63,7 +54,7 @@ const NS = 'settings' * ui-settings' apply, whose activation order relative to this one is NOT * constrained; registrations depend on their slots through `slots.inject()`. */ -export const inject = ['slots', 'locale', 'connection', 'remote'] +export const inject = ['slots', 'locale', 'connection'] /** * Register the `settings` dictionaries, the chrome content, and the General @@ -87,27 +78,9 @@ export function apply(ctx: ClientContext): void { const useSnapshot = bindSnapshotSelector(documentController.store) return (): SettingsDocumentActionInjected => ({ controller: documentController, useSnapshot }) })() - const welcomeController = new WelcomeNoticeStore(connection.api, connection.isLoopback ? 'host' : 'memory') - const useWelcomeSnapshot = bindSnapshotSelector(welcomeController.store) - const welcomeInjected = (): WelcomeNoticeInjected => ({ - controller: welcomeController, - useSnapshot: useWelcomeSnapshot, - }) - - ctx.effect(() => { - const refresh = (): void => { refreshWelcomeIfLoaded(welcomeController) } - const disposers = [ - ctx.remote.$on('settings/document-updated', (ns) => { - if (ns !== WELCOME_NOTICE_SETTINGS_NAMESPACE) return - refresh() - }), - ctx.on('connection/reset', () => { - refresh() - refreshDocumentIfLoaded(documentController) - }), - ] - return () => { for (const dispose of disposers) dispose() } - }, 'ui-settings-general: metadata invalidations') + ctx.effect(() => ctx.on('connection/reset', () => { + refreshDocumentIfLoaded(documentController) + }), 'ui-settings-general: metadata invalidations') // The settings shell: this package occupies the sidebar-owned hole and // declares the settings slots. Ledger → nav-row projection as an observable // source (uSES contract: getSnapshot returns the cached rows until the @@ -202,11 +175,4 @@ export function apply(ctx: ClientContext): void { locale: NS, children: { 'settings.general.item': { kind: 'list', scope: 'root' } }, }, GeneralSection)) - ctx.slots.inject('settings.onboarding', () => ctx.slots.register({ - name: 'settings.onboarding', - id: 'welcome-notice', - order: -100, - locale: NS, - inject: welcomeInjected, - }, WelcomeNotice)) } diff --git a/packages/client/ui-settings-general/src/client/locales.ts b/packages/client/ui-settings-general/src/client/locales.ts index 41a44eae3f..a557855323 100644 --- a/packages/client/ui-settings-general/src/client/locales.ts +++ b/packages/client/ui-settings-general/src/client/locales.ts @@ -1,5 +1,4 @@ -/** Shell chrome, General-nav, and welcome-notice dictionaries; feature rows own their copy. */ -import { WELCOME_NOTICE_COPY } from '../onboarding-copy.ts' +/** Shell chrome and General-nav dictionaries; feature rows own their copy. */ /** Simplified Chinese dictionary (the key-set source of truth). */ export const zh = { @@ -9,13 +8,6 @@ export const zh = { 'openDocument': '打开配置文件', 'openDocument.error': '无法打开配置文件', 'general.nav': '通用设置', - 'welcome.title': WELCOME_NOTICE_COPY.zh.title, - 'welcome.paragraph.0': WELCOME_NOTICE_COPY.zh.paragraphs[0], - 'welcome.paragraph.1': WELCOME_NOTICE_COPY.zh.paragraphs[1], - 'welcome.paragraph.2': WELCOME_NOTICE_COPY.zh.paragraphs[2], - 'welcome.feedbackEmphasis': WELCOME_NOTICE_COPY.zh.feedbackEmphasis, - 'welcome.continue': WELCOME_NOTICE_COPY.zh.continueLabel, - 'welcome.error': '暂时无法保存确认状态,请重试。', } satisfies Record /** The settings namespace key union. */ @@ -29,11 +21,4 @@ export const en = { 'openDocument': 'Open configuration file', 'openDocument.error': 'Could not open configuration file', 'general.nav': 'General', - 'welcome.title': WELCOME_NOTICE_COPY.en.title, - 'welcome.paragraph.0': WELCOME_NOTICE_COPY.en.paragraphs[0], - 'welcome.paragraph.1': WELCOME_NOTICE_COPY.en.paragraphs[1], - 'welcome.paragraph.2': WELCOME_NOTICE_COPY.en.paragraphs[2], - 'welcome.feedbackEmphasis': WELCOME_NOTICE_COPY.en.feedbackEmphasis, - 'welcome.continue': WELCOME_NOTICE_COPY.en.continueLabel, - 'welcome.error': 'The acknowledgement could not be saved. Please try again.', } satisfies Record diff --git a/packages/client/ui-settings-general/src/client/welcome-store.ts b/packages/client/ui-settings-general/src/client/welcome-store.ts deleted file mode 100644 index 48f96bcb94..0000000000 --- a/packages/client/ui-settings-general/src/client/welcome-store.ts +++ /dev/null @@ -1,128 +0,0 @@ -/** Welcome-notice state, durable when the browser may use Host settings. */ - -import type { IApiClient, SettingsNamespaceView } from '@deepseek-ai/dsh-api-remotes/client' -import type { SnapshotStore } from '@deepseek-ai/dsh-client-runtime/client' -import { createSnapshotStore } from '@deepseek-ai/dsh-client-runtime/client' -import { - WELCOME_NOTICE_ACK_FIELD, WELCOME_NOTICE_SETTINGS_NAMESPACE, WELCOME_NOTICE_VERSION, -} from '../onboarding-copy.ts' - -/** State rendered by the welcome step. */ -export interface WelcomeNoticeState { - status: 'idle' | 'loading' | 'ready' | 'saving' | 'error' - acknowledged: boolean - error: string | null -} - -function messageOf(error: unknown): string { - return error instanceof Error ? error.message : String(error) -} - -function acknowledgementOf(view: SettingsNamespaceView): string | undefined { - if (typeof view.value !== 'object' || view.value === null) return undefined - const value = (view.value as Record)[WELCOME_NOTICE_ACK_FIELD] - return typeof value === 'string' ? value : undefined -} - -/** Coordinates durable Host acknowledgement or a process-local remote fallback. */ -export class WelcomeNoticeStore { - /** uSES-safe state source shared by the registered welcome step. */ - readonly store: SnapshotStore = createSnapshotStore({ - status: 'idle', acknowledged: false, error: null, - }) - - private generation = 0 - - /** - * @param api - settings wire face used for durable reads and writes. - * @param persistence - remote browsers use memory because settings is loopback-only. - */ - constructor( - private readonly api: Pick, - private readonly persistence: 'host' | 'memory' = 'host', - ) {} - - /** Load the acknowledgement from Host settings or initialize process-local state. */ - async load(): Promise { - const generation = ++this.generation - if (this.persistence === 'memory') { - this.store.update((state) => { state.status = 'ready'; state.error = null }) - return - } - this.store.update((state) => { state.status = 'loading'; state.error = null }) - try { - const response = await this.api.settings.describe({}) - if (!response.result.ok) throw new Error(response.result.error.message) - const view = response.result.value.namespaces.find( - candidate => candidate.ns === WELCOME_NOTICE_SETTINGS_NAMESPACE, - ) - if (view === undefined) throw new Error('welcome acknowledgement settings are unavailable') - if (generation !== this.generation) return - this.store.update((state) => { - state.status = 'ready' - state.acknowledged = acknowledgementOf(view) === WELCOME_NOTICE_VERSION - state.error = null - }) - } catch (error) { - if (generation !== this.generation) return - this.store.update((state) => { - state.status = 'error' - state.acknowledged = false - state.error = messageOf(error) - }) - } - } - - /** - * Acknowledge this copy version. The Host path mutation is idempotent across - * tabs and preserves sibling settings; remote fallback changes only this store. - * @returns true when the selected persistence mode accepted the acknowledgement. - */ - async acknowledge(): Promise { - const generation = ++this.generation - if (this.persistence === 'memory') { - this.store.update((state) => { - state.status = 'ready' - state.acknowledged = true - state.error = null - }) - return true - } - this.store.update((state) => { state.status = 'saving'; state.error = null }) - try { - const response = await this.api.settings.mutate({ - ns: WELCOME_NOTICE_SETTINGS_NAMESPACE, - ops: [{ op: 'set', path: [WELCOME_NOTICE_ACK_FIELD], value: WELCOME_NOTICE_VERSION }], - }) - if (!response.result.ok) throw new Error(response.result.error.message) - if (generation === this.generation) { - this.store.update((state) => { - state.status = 'ready' - state.acknowledged = true - state.error = null - }) - } - return true - } catch (error) { - if (generation === this.generation) { - this.store.update((state) => { - state.status = 'error' - state.acknowledged = false - state.error = messageOf(error) - }) - } - return false - } - } -} - -/** - * Refresh only after welcome state has left idle. A memory-mode load retains - * acknowledgement so reconnect and settings-change refreshes do not reopen a - * process-local notice. - * @param controller - welcome state owner whose current status decides whether to load. - */ -export function refreshWelcomeIfLoaded(controller: WelcomeNoticeStore): void { - if (controller.store.getSnapshot().status === 'idle') return - void controller.load() -} diff --git a/packages/client/ui-settings-general/src/index.ts b/packages/client/ui-settings-general/src/index.ts index 0cea245db5..ce11f4e0be 100644 --- a/packages/client/ui-settings-general/src/index.ts +++ b/packages/client/ui-settings-general/src/index.ts @@ -3,28 +3,24 @@ import type { Context } from '@deepseek-ai/cordis' import z from '@deepseek-ai/schemastery' import { settingsNamespace } from '@deepseek-ai/dsh-settings' -import { - WELCOME_NOTICE_ACK_FIELD, WELCOME_NOTICE_SETTINGS_NAMESPACE, -} from './onboarding-copy.ts' -export { - WELCOME_NOTICE_ACK_FIELD, WELCOME_NOTICE_COPY, WELCOME_NOTICE_SETTINGS_NAMESPACE, - WELCOME_NOTICE_VERSION, -} from './onboarding-copy.ts' +/** Durable settings namespace for product-wide GUI onboarding facts. */ +const ONBOARDING_SETTINGS_NAMESPACE = 'ui-onboarding' interface OnboardingSettings { + /** Last welcome-notice version acknowledged by a since-removed first-run notice; kept so stored documents stay valid. */ welcomeNoticeVersion?: string } const OnboardingSettingsSchema: z = z.object({ - [WELCOME_NOTICE_ACK_FIELD]: z.string(), + welcomeNoticeVersion: z.string(), }) /** Register the durable GUI-onboarding section when a settings provider exists. */ export function apply(ctx: Context): void { ctx.inject(['settings'], (settingsCtx) => { settingsCtx.settings.register( - settingsNamespace(WELCOME_NOTICE_SETTINGS_NAMESPACE), + settingsNamespace(ONBOARDING_SETTINGS_NAMESPACE), OnboardingSettingsSchema, ) }) diff --git a/packages/client/ui-settings-general/src/invariant.ts b/packages/client/ui-settings-general/src/invariant.ts index adfd582ca6..10db92524e 100644 --- a/packages/client/ui-settings-general/src/invariant.ts +++ b/packages/client/ui-settings-general/src/invariant.ts @@ -16,7 +16,7 @@ export const inject = ['invariants'] /** * No runtime invariant: the settings seam validates and publishes the durable - * welcome section, while slot conflicts fail loud in the slot core. The local + * onboarding section, while slot conflicts fail loud in the slot core. The local * document action is browser state over typed RPC responses and is covered by * store/component tests rather than a Cordis runtime relationship. */ diff --git a/packages/client/ui-settings-general/src/onboarding-copy.ts b/packages/client/ui-settings-general/src/onboarding-copy.ts deleted file mode 100644 index 8b29402b4b..0000000000 --- a/packages/client/ui-settings-general/src/onboarding-copy.ts +++ /dev/null @@ -1,35 +0,0 @@ -/** Durable settings namespace for product-wide GUI onboarding facts. */ -export const WELCOME_NOTICE_SETTINGS_NAMESPACE = 'ui-onboarding' - -/** Field storing the last welcome notice version the user acknowledged. */ -export const WELCOME_NOTICE_ACK_FIELD = 'welcomeNoticeVersion' - -/** - * Bump only when the notice changes materially and every user should see it - * again. The acknowledgement is compared for exact equality. - */ -export const WELCOME_NOTICE_VERSION = '2026-08-11.1' - -/** The complete editable welcome notice in both supported GUI locales. */ -export const WELCOME_NOTICE_COPY = { - zh: { - title: '内测声明', - paragraphs: [ - '感谢您愿意拨冗试用 DeepSeek Harness。当前版本仍处于内部测试阶段,功能仍待完善,体验难免有些粗糙。', - '“如切如磋,如琢如磨。” 产品的成长,离不开一次次真实的碰撞与坦诚的反馈。您在真实使用中发现的问题,也可能促使我们重新审视,甚至推翻已有的设计。', - '内测版本默认不会上传 Session Log。如需在提交反馈时共享会话日志,可以设置环境变量 DSH_TELEMETRY_MODE=FEEDBACK_ONLY;如需持续上传,可以设置 DSH_TELEMETRY_MODE=FULL,但该模式同时会启用 dsh-sdk 命令遥测,上报匿名 ID、命令结果以及脱敏后的项目配置。另外,如果您有任何反馈与建议,请在企业微信群中留言告诉我们。每一条反馈,都会帮助我们把它打磨得更好。', - ], - feedbackEmphasis: '如果您有任何反馈与建议,请在企业微信群中留言告诉我们', - continueLabel: '继续', - }, - en: { - title: '内测声明', - paragraphs: [ - '感谢您愿意拨冗试用 DeepSeek Harness。当前版本仍处于内部测试阶段,功能仍待完善,体验难免有些粗糙。', - '“如切如磋,如琢如磨。” 产品的成长,离不开一次次真实的碰撞与坦诚的反馈。您在真实使用中发现的问题,也可能促使我们重新审视,甚至推翻已有的设计。', - '内测版本默认不会上传 Session Log。如需在提交反馈时共享会话日志,可以设置环境变量 DSH_TELEMETRY_MODE=FEEDBACK_ONLY;如需持续上传,可以设置 DSH_TELEMETRY_MODE=FULL,但该模式同时会启用 dsh-sdk 命令遥测,上报匿名 ID、命令结果以及脱敏后的项目配置。另外,如果您有任何反馈与建议,请在企业微信群中留言告诉我们。每一条反馈,都会帮助我们把它打磨得更好。', - ], - feedbackEmphasis: '如果您有任何反馈与建议,请在企业微信群中留言告诉我们', - continueLabel: '继续', - }, -} as const diff --git a/packages/client/ui-settings-general/tests/apply.client.spec.ts b/packages/client/ui-settings-general/tests/apply.client.spec.ts index 6c0f5131f2..d6c3ffff02 100644 --- a/packages/client/ui-settings-general/tests/apply.client.spec.ts +++ b/packages/client/ui-settings-general/tests/apply.client.spec.ts @@ -1,18 +1,15 @@ -/** Ownerless-copy registrations: the six seats, dictionaries, thunked labels, and HMR recovery. */ +/** Ownerless-copy registrations: the five seats, dictionaries, thunked labels, and HMR recovery. */ import { Context } from '@deepseek-ai/cordis' import { describe, expect, it, vi } from 'vitest' import { resolveSlotLabel } from '@deepseek-ai/dsh-client-ui-slots' import { SlotRegistry } from '@deepseek-ai/dsh-client-runtime/client' import { LocaleRuntime } from '@deepseek-ai/dsh-client-locale/client' -import { TestRemote, usePinnedBrowserLanguages } from '@deepseek-ai/dsh-client-test-runtime' +import { usePinnedBrowserLanguages } from '@deepseek-ai/dsh-client-test-runtime' import { apply, inject } from '@deepseek-ai/dsh-client-ui-settings-general/client' import { CloseLabel, HeaderContent, TriggerContent } from '../src/client/chrome.tsx' import { GeneralSection } from '../src/client/GeneralSection.tsx' import { SettingsDocumentAction } from '../src/client/SettingsDocumentAction.tsx' import type { SettingsDocumentActionInjected } from '../src/client/SettingsDocumentAction.tsx' -import { WelcomeNotice } from '../src/client/WelcomeNotice.tsx' -import type { WelcomeNoticeInjected } from '../src/client/WelcomeNotice.tsx' -import { WELCOME_NOTICE_SETTINGS_NAMESPACE } from '../src/onboarding-copy.ts' // The service reads its initial locale from the browser; these specs assert // the shipped Chinese copy, so they state the browser they assume. @@ -25,7 +22,6 @@ const SEATS = [ ['settings.action', SettingsDocumentAction], ['settings.close', CloseLabel], ['settings.section', GeneralSection], - ['settings.onboarding', WelcomeNotice], ] as const async function bench(isLoopback = true) { @@ -33,9 +29,6 @@ async function bench(isLoopback = true) { await ctx.plugin(SlotRegistry).await() const locale = new LocaleRuntime(ctx) ctx.provide('locale', locale) - // The plugins inject `remote`; forwarded events reach them through the - // same `$dispatch` handoff the connection sink makes. - new TestRemote(ctx) const settingsDescribe = vi.fn(() => Promise.resolve({ rpcId: 'settings-general' as never, result: { @@ -43,14 +36,7 @@ async function bench(isLoopback = true) { value: { writable: true, hasDocument: true, - namespaces: [{ - ns: WELCOME_NOTICE_SETTINGS_NAMESPACE, - schema: {}, - value: {}, - applies: 'live' as const, - secrets: [], - revision: 0, - }], + namespaces: [], }, }, })) @@ -89,10 +75,10 @@ function generalEntry(slots: SlotRegistry) { describe('ui-settings-general apply', () => { it('declares the services it uses', () => { - expect(inject).toEqual(['slots', 'locale', 'connection', 'remote']) + expect(inject).toEqual(['slots', 'locale', 'connection']) }) - it('fills all six seats for declarations before or after apply', async () => { + it('fills all five seats for declarations before or after apply', async () => { const before = await bench() declare(before.slots) await before.ctx.plugin({ inject: [...inject], apply }).await() @@ -105,8 +91,9 @@ describe('ui-settings-general apply', () => { expect(resolveSlotLabel(entry.options.label)).toBe('通用设置') expect(before.slots.spec('settings.general.item')).toEqual({ kind: 'list', scope: 'root' }) expect(before.slots.entries('settings.general.item')).toEqual([]) - const welcome = before.slots.entries('settings.onboarding')[0]! - expect(welcome.options).toMatchObject({ id: 'welcome-notice', order: -100 }) + // The onboarding hole stays declared for feature-owned steps; this plugin + // no longer seats one. + expect(before.slots.entries('settings.onboarding')).toEqual([]) const action = before.slots.entries('settings.action')[0]! const actionInjected = (action.inject as unknown as () => SettingsDocumentActionInjected)() expect(actionInjected.controller.store.getSnapshot().status).toBe('idle') @@ -162,22 +149,6 @@ describe('ui-settings-general apply', () => { expect(resolveSlotLabel(generalEntry(b.slots)!.options.label)).toBe('通用设置') }) - it('refreshes loaded welcome state only for its settings namespace or a reconnect', async () => { - const b = await bench() - declare(b.slots) - await b.ctx.plugin({ inject: [...inject], apply }).await() - const entry = b.slots.entries('settings.onboarding')[0]! - const { controller } = (entry.inject as unknown as () => WelcomeNoticeInjected)() - await controller.load() - expect(b.settingsDescribe).toHaveBeenCalledOnce() - b.ctx.remote.$dispatch('settings/document-updated', ['unrelated', 1]) - expect(b.settingsDescribe).toHaveBeenCalledOnce() - b.ctx.remote.$dispatch('settings/document-updated', [WELCOME_NOTICE_SETTINGS_NAMESPACE, 1]) - await vi.waitFor(() => { expect(b.settingsDescribe).toHaveBeenCalledTimes(2) }) - b.ctx.emit('connection/reset') - await vi.waitFor(() => { expect(b.settingsDescribe).toHaveBeenCalledTimes(3) }) - }) - it('refreshes loaded document availability on reconnect without reading it eagerly', async () => { const b = await bench() declare(b.slots) @@ -192,19 +163,13 @@ describe('ui-settings-general apply', () => { await vi.waitFor(() => { expect(b.settingsDescribe).toHaveBeenCalledTimes(2) }) }) - it('keeps remote welcome acknowledgement process-local', async () => { + it('withholds the loopback-only document action off-loopback', async () => { const b = await bench(false) declare(b.slots) const fiber = b.ctx.plugin({ inject: [...inject], apply }) await fiber.await() - const entry = b.slots.entries('settings.onboarding')[0]! - const { controller } = (entry.inject as unknown as () => WelcomeNoticeInjected)() - - await controller.load() - await expect(controller.acknowledge()).resolves.toBe(true) - expect(controller.store.getSnapshot()).toMatchObject({ status: 'ready', acknowledged: true }) - expect(b.settingsDescribe).not.toHaveBeenCalled() expect(b.slots.entries('settings.action')).toEqual([]) + expect(b.settingsDescribe).not.toHaveBeenCalled() await fiber.dispose() for (const [name] of SEATS) expect(b.slots.entries(name)).toEqual([]) }) diff --git a/packages/client/ui-settings-general/tests/host.client.spec.ts b/packages/client/ui-settings-general/tests/host.client.spec.ts index 5e82764f47..f5bc43b3e1 100644 --- a/packages/client/ui-settings-general/tests/host.client.spec.ts +++ b/packages/client/ui-settings-general/tests/host.client.spec.ts @@ -2,7 +2,9 @@ import { Context } from '@deepseek-ai/cordis' import { describe, expect, it } from 'vitest' import { SettingsProvider, settingsNamespace, type SettingsNamespace } from '@deepseek-ai/dsh-settings' import { apply } from '../src/index.ts' -import { WELCOME_NOTICE_SETTINGS_NAMESPACE } from '../src/onboarding-copy.ts' + +/** Mirrors the module-local namespace id in src/index.ts. */ +const ONBOARDING_SETTINGS_NAMESPACE = 'ui-onboarding' class MemorySettings extends SettingsProvider { readonly writable = true @@ -19,11 +21,11 @@ describe('ui-settings-general host', () => { const fiber = ctx.plugin({ apply }) await fiber.await() expect(ctx.settings.describe().map(row => row.ns)).toContain( - settingsNamespace(WELCOME_NOTICE_SETTINGS_NAMESPACE), + settingsNamespace(ONBOARDING_SETTINGS_NAMESPACE), ) await fiber.dispose() expect(ctx.settings.describe().map(row => row.ns)).not.toContain( - settingsNamespace(WELCOME_NOTICE_SETTINGS_NAMESPACE), + settingsNamespace(ONBOARDING_SETTINGS_NAMESPACE), ) }) }) diff --git a/packages/client/ui-settings-general/tests/shell.client.spec.ts b/packages/client/ui-settings-general/tests/shell.client.spec.ts index 0c95e8b8d3..32c354deda 100644 --- a/packages/client/ui-settings-general/tests/shell.client.spec.ts +++ b/packages/client/ui-settings-general/tests/shell.client.spec.ts @@ -49,7 +49,7 @@ const CHILD_SPECS = { describe('ui-settings apply', () => { it('declares only the slot registry (a pure composition face, no locale)', () => { - expect(inject).toEqual(['slots', 'locale', 'connection', 'remote']) + expect(inject).toEqual(['slots', 'locale', 'connection']) }) it('registers the shell and declares every child slot, before or after the declaration', async () => { @@ -110,8 +110,6 @@ describe('ui-settings apply', () => { b.slots.register({ name: 'settings.onboarding', id: 'default-order' } as never, () => null) const steps = onboardingSteps.getSnapshot() expect(steps).toEqual([ - // This package's own onboarding page, registered by the same apply. - { id: 'welcome-notice', order: -100 }, { id: 'welcome', order: -100 }, { id: 'credential', order: 0 }, { id: 'default-order', order: 0 }, diff --git a/packages/client/ui-settings-general/tests/welcome-notice.client.spec.tsx b/packages/client/ui-settings-general/tests/welcome-notice.client.spec.tsx deleted file mode 100644 index 74feea89a5..0000000000 --- a/packages/client/ui-settings-general/tests/welcome-notice.client.spec.tsx +++ /dev/null @@ -1,106 +0,0 @@ -// @vitest-environment jsdom -import { act, cleanup, fireEvent, render, screen } from '@testing-library/react' -import { afterEach, describe, expect, it, vi } from 'vitest' -import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-web-react' -import { WelcomeNotice } from '../src/client/WelcomeNotice.tsx' -import type { WelcomeNoticeProps } from '../src/client/WelcomeNotice.tsx' -import { WelcomeNoticeStore } from '../src/client/welcome-store.ts' -import { zh } from '../src/client/locales.ts' -import { - WELCOME_NOTICE_ACK_FIELD, WELCOME_NOTICE_COPY, WELCOME_NOTICE_SETTINGS_NAMESPACE, - WELCOME_NOTICE_VERSION, -} from '../src/onboarding-copy.ts' - -afterEach(cleanup) - -function response(value: T) { - return { rpcId: 'welcome-rpc' as never, result: { ok: true as const, value } } -} - -function mount(version?: string, mutateImpl: () => Promise = () => Promise.resolve(response({}))) { - const mutate = vi.fn(mutateImpl) - const api = { - settings: { - describe: () => Promise.resolve(response({ - writable: true, - hasDocument: false, - namespaces: [{ - ns: WELCOME_NOTICE_SETTINGS_NAMESPACE, - schema: {}, - value: version === undefined ? {} : { [WELCOME_NOTICE_ACK_FIELD]: version }, - applies: 'live' as const, - secrets: [], - revision: 0, - }], - })), - mutate, - }, - } - const controller = new WelcomeNoticeStore(api as never) - const complete = vi.fn() - const unusedHook = (() => { throw new Error('unused standard hook') }) as never - const props: WelcomeNoticeProps = { - stepId: 'welcome-notice', - complete, - openSection: vi.fn(), - useSessions: unusedHook, - useWorkspaces: unusedHook, - controller, - useSnapshot: bindSnapshotSelector(controller.store), - t: key => key in zh ? zh[key as keyof typeof zh] : key, - } - return { ...render(), complete, controller, mutate } -} - -describe('WelcomeNotice', () => { - it('uses the same Chinese owner copy in both GUI locales', () => { - expect(WELCOME_NOTICE_COPY.en).toEqual(WELCOME_NOTICE_COPY.zh) - }) - - it('renders the owner copy with one primary action and no dismissal control', async () => { - const h = mount() - const page = await screen.findByRole('region', { name: WELCOME_NOTICE_COPY.zh.title }) - expect(screen.getByText(WELCOME_NOTICE_COPY.zh.title)).toBeTruthy() - for (const text of WELCOME_NOTICE_COPY.zh.paragraphs) expect(page.textContent).toContain(text) - expect(page.textContent?.match(/感谢您愿意拨冗试用 DeepSeek Harness/g) ?? []).toHaveLength(1) - const buttons = page.querySelectorAll('button') - expect(buttons).toHaveLength(1) - expect(screen.getByRole('button', { name: WELCOME_NOTICE_COPY.zh.continueLabel })).toBeTruthy() - expect(document.activeElement).toBe(screen.getByRole('heading', { name: WELCOME_NOTICE_COPY.zh.title })) - fireEvent.keyDown(document, { key: 'Escape' }) - expect(h.complete).not.toHaveBeenCalled() - expect(screen.getByRole('region')).toBeTruthy() - }) - - it('completes only after the acknowledgement write commits', async () => { - const h = mount() - await screen.findByRole('region') - fireEvent.click(screen.getByRole('button', { name: WELCOME_NOTICE_COPY.zh.continueLabel })) - await act(async () => { await Promise.resolve() }) - expect(h.mutate).toHaveBeenCalledOnce() - expect(h.complete).toHaveBeenCalledOnce() - }) - - it('skips itself when this exact version was already acknowledged', async () => { - const h = mount(WELCOME_NOTICE_VERSION) - await act(async () => { await h.controller.load() }) - expect(screen.queryByRole('region')).toBeNull() - expect(h.complete).toHaveBeenCalledOnce() - }) - - it('keeps the sole action disabled while saving and reports a refused write', async () => { - let resolveWrite!: (value: unknown) => void - const write = new Promise((resolve) => { resolveWrite = resolve }) - const h = mount(undefined, () => write) - await screen.findByRole('region') - const action = screen.getByRole('button', { name: WELCOME_NOTICE_COPY.zh.continueLabel }) - fireEvent.click(action) - expect(action.disabled).toBe(true) - resolveWrite({ - rpcId: 'welcome-refused' as never, - result: { ok: false, error: { code: 'settings-rejected', message: 'read only', details: { ns: WELCOME_NOTICE_SETTINGS_NAMESPACE } } }, - }) - expect((await screen.findByRole('alert')).textContent).toBe('暂时无法保存确认状态,请重试。') - expect(h.complete).not.toHaveBeenCalled() - }) -}) diff --git a/packages/client/ui-settings-general/tests/welcome-store.client.spec.ts b/packages/client/ui-settings-general/tests/welcome-store.client.spec.ts deleted file mode 100644 index bfa5e16999..0000000000 --- a/packages/client/ui-settings-general/tests/welcome-store.client.spec.ts +++ /dev/null @@ -1,184 +0,0 @@ -import { describe, expect, it, vi } from 'vitest' -import type { RpcResponse } from '@deepseek-ai/dsh-api-remotes/client' -import { WelcomeNoticeStore } from '../src/client/welcome-store.ts' -import { refreshWelcomeIfLoaded } from '../src/client/welcome-store.ts' -import { - WELCOME_NOTICE_ACK_FIELD, WELCOME_NOTICE_SETTINGS_NAMESPACE, WELCOME_NOTICE_VERSION, -} from '../src/onboarding-copy.ts' - -let rpc = 0 -function ok(value: T): RpcResponse { - return { rpcId: `welcome-${rpc++}` as never, result: { ok: true, value } } -} - -function namespace(version?: string) { - return { - ns: WELCOME_NOTICE_SETTINGS_NAMESPACE, - schema: {}, - value: version === undefined ? {} : { [WELCOME_NOTICE_ACK_FIELD]: version }, - applies: 'live' as const, - secrets: [], - revision: 0, - } -} - -function deferred() { - let resolve!: (value: T) => void - let reject!: (reason: unknown) => void - const promise = new Promise((res, rej) => { resolve = res; reject = rej }) - return { promise, resolve, reject } -} - -describe('WelcomeNoticeStore', () => { - it('acknowledges in memory without calling loopback-only settings APIs', async () => { - const describe = vi.fn() - const mutate = vi.fn() - const controller = new WelcomeNoticeStore({ settings: { describe, mutate } } as never, 'memory') - - await controller.load() - expect(controller.store.getSnapshot()).toEqual({ status: 'ready', acknowledged: false, error: null }) - await expect(controller.acknowledge()).resolves.toBe(true) - expect(controller.store.getSnapshot()).toEqual({ status: 'ready', acknowledged: true, error: null }) - await controller.load() - expect(controller.store.getSnapshot()).toEqual({ status: 'ready', acknowledged: true, error: null }) - expect(describe).not.toHaveBeenCalled() - expect(mutate).not.toHaveBeenCalled() - }) - - it('acknowledges only the exact current copy version', async () => { - for (const [version, acknowledged] of [ - [undefined, false], - ['older-copy', false], - [WELCOME_NOTICE_VERSION, true], - ] as const) { - const api = { - settings: { - describe: vi.fn(() => Promise.resolve(ok({ writable: true, hasDocument: false, namespaces: [namespace(version)] }))), - }, - } - const controller = new WelcomeNoticeStore(api as never) - await controller.load() - expect(controller.store.getSnapshot()).toMatchObject({ status: 'ready', acknowledged }) - } - }) - - it('persists the owner version through one idempotent path mutation', async () => { - const mutate = vi.fn(() => Promise.resolve(ok(namespace(WELCOME_NOTICE_VERSION)))) - const controller = new WelcomeNoticeStore({ settings: { mutate } } as never) - await expect(controller.acknowledge()).resolves.toBe(true) - expect(mutate).toHaveBeenCalledWith({ - ns: WELCOME_NOTICE_SETTINGS_NAMESPACE, - ops: [{ op: 'set', path: [WELCOME_NOTICE_ACK_FIELD], value: WELCOME_NOTICE_VERSION }], - }) - expect(controller.store.getSnapshot()).toMatchObject({ status: 'ready', acknowledged: true }) - }) - - it('keeps the notice pending when loading or persistence fails', async () => { - const load = new WelcomeNoticeStore({ - settings: { describe: () => Promise.reject(new Error('offline')) }, - } as never) - await load.load() - expect(load.store.getSnapshot()).toEqual({ status: 'error', acknowledged: false, error: 'offline' }) - - const save = new WelcomeNoticeStore({ - settings: { mutate: () => Promise.reject(new Error('disk full')) }, - } as never) - await expect(save.acknowledge()).resolves.toBe(false) - expect(save.store.getSnapshot()).toEqual({ status: 'error', acknowledged: false, error: 'disk full' }) - - const nonError = new WelcomeNoticeStore({ - // Durable/wire failures are unknown; exercise containment of a non-Error rejection. - // oxlint-disable-next-line typescript/prefer-promise-reject-errors - settings: { describe: () => Promise.reject('offline string') }, - } as never) - await nonError.load() - expect(nonError.store.getSnapshot().error).toBe('offline string') - }) - - it('reports business failures, missing namespaces, and malformed durable values', async () => { - for (const describe of [ - () => Promise.resolve({ - rpcId: 'failed' as never, - result: { ok: false as const, error: { code: 'internal' as const, message: 'denied', details: {} } }, - }), - () => Promise.resolve(ok({ writable: true, hasDocument: false, namespaces: [] })), - ]) { - const controller = new WelcomeNoticeStore({ settings: { describe } } as never) - await controller.load() - expect(controller.store.getSnapshot().status).toBe('error') - } - - for (const value of [null, 42, { [WELCOME_NOTICE_ACK_FIELD]: 42 }]) { - const controller = new WelcomeNoticeStore({ - settings: { describe: () => Promise.resolve(ok({ - writable: true, - hasDocument: false, - namespaces: [{ ...namespace(), value }], - })) }, - } as never) - await controller.load() - expect(controller.store.getSnapshot()).toMatchObject({ status: 'ready', acknowledged: false }) - } - - const save = new WelcomeNoticeStore({ - settings: { mutate: () => Promise.resolve({ - rpcId: 'failed-save' as never, - result: { ok: false, error: { code: 'settings-rejected', message: 'denied', details: { ns: WELCOME_NOTICE_SETTINGS_NAMESPACE } } }, - }) }, - } as never) - await expect(save.acknowledge()).resolves.toBe(false) - expect(save.store.getSnapshot().error).toBe('denied') - }) - - it('lets the latest load win over stale success and failure', async () => { - const first = deferred>() - const describe = vi.fn() - .mockImplementationOnce(() => first.promise) - .mockImplementationOnce(() => Promise.resolve(ok({ writable: true, hasDocument: false, namespaces: [namespace()] }))) - const controller = new WelcomeNoticeStore({ settings: { describe } } as never) - const stale = controller.load() - await controller.load() - first.resolve(ok({ writable: true, hasDocument: false, namespaces: [namespace(WELCOME_NOTICE_VERSION)] })) - await stale - expect(controller.store.getSnapshot().acknowledged).toBe(false) - - const failed = deferred>() - describe - .mockImplementationOnce(() => failed.promise) - .mockImplementationOnce(() => Promise.resolve(ok({ - writable: true, hasDocument: false, namespaces: [namespace(WELCOME_NOTICE_VERSION)], - }))) - const staleFailure = controller.load() - await controller.load() - failed.reject('stale failure') - await staleFailure - expect(controller.store.getSnapshot()).toMatchObject({ status: 'ready', acknowledged: true, error: null }) - }) - - it('contains stale acknowledgement settlements and refreshes only a loaded store', async () => { - const write = deferred>() - const describe = vi.fn(() => Promise.resolve(ok({ writable: true, hasDocument: false, namespaces: [namespace()] }))) - const controller = new WelcomeNoticeStore({ - settings: { mutate: () => write.promise, describe }, - } as never) - refreshWelcomeIfLoaded(controller) - expect(describe).not.toHaveBeenCalled() - const staleWrite = controller.acknowledge() - await controller.load() - write.resolve(ok(namespace(WELCOME_NOTICE_VERSION))) - await expect(staleWrite).resolves.toBe(true) - expect(controller.store.getSnapshot().acknowledged).toBe(false) - refreshWelcomeIfLoaded(controller) - await vi.waitFor(() => { expect(describe).toHaveBeenCalledTimes(2) }) - - const failedWrite = deferred>() - const staleFailure = new WelcomeNoticeStore({ - settings: { mutate: () => failedWrite.promise, describe }, - } as never) - const pending = staleFailure.acknowledge() - await staleFailure.load() - failedWrite.reject('late failure') - await expect(pending).resolves.toBe(false) - expect(staleFailure.store.getSnapshot().status).toBe('ready') - }) -}) 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 2143493488..015d36e896 100644 --- a/packages/extensions/cordis-client-runner/src/client/slot-catalog.ts +++ b/packages/extensions/cordis-client-runner/src/client/slot-catalog.ts @@ -1259,7 +1259,6 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ slotInject: '', declaredBy: 'an entry in \'sidebar.settings\' (client-ui-settings-general), so it exists while that entry is mounted', occupants: [ - 'client-ui-settings-general WelcomeNotice id \'welcome-notice\'', 'client-ui-settings-models DeepSeekOnboardingDialog id \'deepseek-official\'', ], replaceRisk: 'none', From 3c06c66ac46df8003fb547774d7f788b6e4cde7f Mon Sep 17 00:00:00 2001 From: _Kerman Date: Thu, 13 Aug 2026 14:04:16 +0800 Subject: [PATCH 02/24] fix(web): hide verified cold blank sessions --- ...-bounded-cold-blank-verification.i18n.yaml | 6 + ...6-08-13-bounded-cold-blank-verification.md | 37 +++++ ...8-13-bounded-cold-blank-verification.zh.md | 37 +++++ ...7-29-durable-last-activity-index.i18n.yaml | 4 +- .../2026-07-29-durable-last-activity-index.md | 23 +-- ...26-07-29-durable-last-activity-index.zh.md | 23 +-- apps/web/tests/cold-blank-session.e2e.ts | 60 +++++++ apps/web/tests/scaffold.ts | 41 ++++- .../cold-blank-session/sidebar.expected.md | 1 + apps/web/tsconfig.json | 1 + docs/config-catalog.i18n.yaml | 4 +- docs/config-catalog.md | 6 + docs/config-catalog.zh.md | 6 + packages/host/apiproxy/README.i18n.yaml | 4 +- packages/host/apiproxy/README.md | 8 +- packages/host/apiproxy/README.zh.md | 8 +- packages/host/apiproxy/src/api-proxy.ts | 135 ++++++++++++---- packages/host/apiproxy/src/api/index.ts | 2 +- .../host/apiproxy/src/api/sessions.schema.ts | 8 +- packages/host/apiproxy/src/api/sessions.ts | 15 ++ packages/host/apiproxy/src/index.ts | 12 +- .../apiproxy/tests/api-proxy-cold.spec.ts | 149 ++++++++++++++---- .../tests/api-proxy-projections.spec.ts | 54 ++++++- .../tests/api-proxy-subagents.spec.ts | 7 +- .../apiproxy/tests/session-export.spec.ts | 21 ++- tsconfig.host.json | 1 + 26 files changed, 548 insertions(+), 125 deletions(-) create mode 100644 .agents/notes/implemented/bug-fix/2026-08-13-bounded-cold-blank-verification.i18n.yaml create mode 100644 .agents/notes/implemented/bug-fix/2026-08-13-bounded-cold-blank-verification.md create mode 100644 .agents/notes/implemented/bug-fix/2026-08-13-bounded-cold-blank-verification.zh.md create mode 100644 apps/web/tests/cold-blank-session.e2e.ts create mode 100644 apps/web/tests/snapshots/cold-blank-session/sidebar.expected.md diff --git a/.agents/notes/implemented/bug-fix/2026-08-13-bounded-cold-blank-verification.i18n.yaml b/.agents/notes/implemented/bug-fix/2026-08-13-bounded-cold-blank-verification.i18n.yaml new file mode 100644 index 0000000000..d57e57211b --- /dev/null +++ b/.agents/notes/implemented/bug-fix/2026-08-13-bounded-cold-blank-verification.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 .agents/notes/implemented/bug-fix/2026-08-13-bounded-cold-blank-verification.md +2026-08-13-bounded-cold-blank-verification.md: 330f5acbd520487732bc72fe75f09496aaa0c028 +2026-08-13-bounded-cold-blank-verification.zh.md: 15c0d28be8c15f7076ac90a50bb023cc42fbbdd6 diff --git a/.agents/notes/implemented/bug-fix/2026-08-13-bounded-cold-blank-verification.md b/.agents/notes/implemented/bug-fix/2026-08-13-bounded-cold-blank-verification.md new file mode 100644 index 0000000000..330f5acbd5 --- /dev/null +++ b/.agents/notes/implemented/bug-fix/2026-08-13-bounded-cold-blank-verification.md @@ -0,0 +1,37 @@ +# Agent Note: Bound cold blank-session verification + +Status: implemented + +English | [中文](2026-08-13-bounded-cold-blank-verification.zh.md) + +## Problem + +The Web session tree hides blank Sessions and reuses the selected blank entry as New Session. Attached Sessions can derive blankness from their in-memory event log, but `session.list` normally avoids loading every cold log. Treating every materialized cold Session as non-blank exposes empty Sessions left by older versions. Treating a projection-cache `blank: true` as current can instead hide a real conversation after the log advances and the fail-soft cache remains stale. + +The same cold list used the JSONL artifact mtime for `updatedAt`. Opening a Session appends `session/end-seed`, so a pickup with no human prompt refreshed mtime and promoted that Session above recently used conversations. + +## Decision + +`dsh-host-apiproxy` registers `sessionListMetadata`, a projection containing `blank` and `lastPromptAt`. The attached summary folds the same functions directly over the live log. `blank` changes only from true to false on `turn/start`; `lastPromptAt` changes only on a `user/message` whose source kind is `user`. + +A cold summary trusts cached `blank: false`, because a checkpoint prefix containing `turn/start` remains non-blank. Cached `blank: true` and a cache miss do not prove the current log is blank. When persistence exposes a physical artifact through `locate()` and its size is at most `coldBlankProbeMaxBytes` (default 1 KiB per Session), the gateway calls `readFrom(id, 0)` and verifies whether the stored prefix contains `turn/start`. Files above the bound, backends without a location, vanished artifacts, and failed reads all produce `blank: false`, keeping the Session visible. + +`updatedAt` is the later of `createdAt` and `lastPromptAt`. A cold cache miss or stale checkpoint therefore orders the Session too old rather than promoting it from an unrelated file write. The bounded blank read does not replace missing recency metadata. + +## Alternatives considered + +**Trust cached `blank: true`.** Rejected because the projection cache deliberately permits a persisted log to advance beyond its checkpoint. A crash or fail-soft write failure after the first `turn/start` would hide a real conversation and could make the client reuse it as New Session. + +**Read every cold log.** Rejected because list latency and I/O would scale with total stored conversation bytes. The physical-size bound targets the small historical artifacts that can be checked cheaply and degrades larger unknowns toward visibility. + +**Store blankness and recency in an authoritative persistence index.** Deferred because JSONL has an immutable first line and would require a second durable artifact with ordered updates, while SQLite would require a schema field. The broader exact-index design remains in the [last-activity proposal](../../proposed/architecture/2026-07-29-durable-last-activity-index.md). + +**Continue ordering JSONL by mtime.** Rejected because mtime records every artifact write, including pickup boundaries, rather than the latest human prompt. Its error direction promotes untouched Sessions to the front. + +## Consequences + +Existing small blank JSONL artifacts are hidden without depending on projection-cache availability, and a stale cache cannot hide a stored `turn/start`. A cold list may read each artifact whose physical size is within the configured bound when its cache does not already prove non-blank. The default bound applies to compressed bytes for the shipped Zstandard JSONL backend. + +Blank artifacts above the bound and blank Sessions on location-less backends remain visible. Missing or delayed recency cache entries fall back to `createdAt`. These are conservative degradations: the UI may show an extra empty row or order a Session too low, but it does not hide a conversation or promote one because it was merely opened. + +The gateway-owned projection is an effect of the gateway fiber; unloading the gateway removes the key. Unit coverage pins exact-threshold probing, stale-true rejection, monotonic false reuse, fallback direction, human-prompt recency, and fiber disposal. A keyless Web snapshot boots the shipped compressed JSONL composition, seeds a small cold blank artifact without a cache row, and verifies that the sidebar omits it. diff --git a/.agents/notes/implemented/bug-fix/2026-08-13-bounded-cold-blank-verification.zh.md b/.agents/notes/implemented/bug-fix/2026-08-13-bounded-cold-blank-verification.zh.md new file mode 100644 index 0000000000..15c0d28be8 --- /dev/null +++ b/.agents/notes/implemented/bug-fix/2026-08-13-bounded-cold-blank-verification.zh.md @@ -0,0 +1,37 @@ +# Agent Note: 有界验证冷空白会话 + +Status: implemented + +[English](2026-08-13-bounded-cold-blank-verification.md) | 中文 + +## Problem + +Web 会话树会隐藏空白 Session,并把当前选中的空白项复用为 New Session。已附加 Session 可以从内存事件日志派生空白状态,但 `session.list` 通常不会加载每一份冷日志。把所有已物化的冷 Session 都视为非空,会暴露旧版本留下的空 Session;反过来,把 projection cache 中的 `blank: true` 当成当前事实,则可能在日志已经前进而 fail-soft cache 仍然陈旧时隐藏真实对话。 + +同一份冷列表还曾用 JSONL 工件的 mtime 作为 `updatedAt`。打开 Session 会追加 `session/end-seed`,因此即使没有真人 prompt,单纯拾起也会刷新 mtime,并把该 Session 提升到最近使用的对话之前。 + +## Decision + +`dsh-host-apiproxy` 注册 `sessionListMetadata` 投影,其中包含 `blank` 与 `lastPromptAt`。已附加摘要直接用同一组函数折叠实时日志。`blank` 只在 `turn/start` 时从 true 单调变为 false;`lastPromptAt` 只在来源 kind 为 `user` 的 `user/message` 上更新。 + +冷摘要信任缓存的 `blank: false`,因为已包含 `turn/start` 的 checkpoint 前缀会始终保持非空。缓存的 `blank: true` 和 cache miss 都无法证明当前日志为空。当 persistence 通过 `locate()` 暴露物理工件,且其大小不超过 `coldBlankProbeMaxBytes`(默认每个 Session 1 KiB)时,网关调用 `readFrom(id, 0)`,验证已存前缀是否含有 `turn/start`。超过上限的文件、不提供位置的后端、已消失的工件和读取失败都产生 `blank: false`,让 Session 保持可见。 + +`updatedAt` 取 `createdAt` 与 `lastPromptAt` 中较晚者。因此冷 cache miss 或陈旧 checkpoint 只会让 Session 排得偏旧,而不会因无关的文件写入被提升。有界 blank 读取不用于补齐缺失的最近时间元数据。 + +## Alternatives considered + +**信任缓存的 `blank: true`。** 拒绝,因为 projection cache 有意允许持久日志前进到 checkpoint 之后。首个 `turn/start` 之后若发生崩溃或 fail-soft 写入失败,真实对话就会被隐藏,客户端还可能把它复用为 New Session。 + +**读取每一份冷日志。** 拒绝,因为列表延迟与 I/O 会随所有已存对话的总字节数增长。物理大小上限只针对能够低成本核验的小型历史工件,更大的未知项则向保持可见降级。 + +**把空白状态与最近时间存入权威 persistence index。** 暂缓,因为 JSONL 的首行不可变,需要增加带有顺序写入要求的第二份持久工件;SQLite 则需要 schema 字段。更广泛的精确索引设计仍由[最后活动提案](../../proposed/architecture/2026-07-29-durable-last-activity-index.md)负责。 + +**继续按 mtime 排序 JSONL。** 拒绝,因为 mtime 记录包括拾起边界在内的每一次工件写入,而非最近真人 prompt;其错误方向会把未经操作的 Session 提升到列表开头。 + +## Consequences + +既有的小型空白 JSONL 工件无需依赖 projection cache 是否存在即可被隐藏,陈旧 cache 也无法隐藏已存的 `turn/start`。对于 cache 尚不能证明非空,且物理大小在配置上限内的每个 Session,冷列表可能读取其工件。对默认交付的 Zstandard JSONL 后端,该上限作用于压缩后的字节数。 + +超过上限的空白工件,以及来自不提供位置的后端的空白 Session 会保持可见。缺失或延迟的最近时间 cache 会回退到 `createdAt`。这些都是保守降级:UI 可能多显示一条空记录,或把 Session 排得偏低,但不会隐藏真实对话,也不会因为单纯打开而把会话提升到前面。 + +网关自有投影是网关 fiber 的 effect;卸载网关会移除该 key。单元覆盖固定了临界大小探测、拒绝陈旧 true、复用单调 false、回退方向、真人 prompt 最近时间和 fiber 销毁。无密钥 Web snapshot 会启动发行版的压缩 JSONL 组合,在没有 cache row 的情况下播种一份小型冷空白工件,并验证侧栏不展示它。 diff --git a/.agents/notes/proposed/architecture/2026-07-29-durable-last-activity-index.i18n.yaml b/.agents/notes/proposed/architecture/2026-07-29-durable-last-activity-index.i18n.yaml index af319b51a7..d9e26b561d 100644 --- a/.agents/notes/proposed/architecture/2026-07-29-durable-last-activity-index.i18n.yaml +++ b/.agents/notes/proposed/architecture/2026-07-29-durable-last-activity-index.i18n.yaml @@ -2,5 +2,5 @@ # 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 .agents/notes/proposed/architecture/2026-07-29-durable-last-activity-index.md -2026-07-29-durable-last-activity-index.md: 0e441f54a719b29a1a450c133e08cdf7d2c82e9e -2026-07-29-durable-last-activity-index.zh.md: ebc2e2167d7743eafc5a4600fe9a0f687349e1a3 +2026-07-29-durable-last-activity-index.md: b530508877adf3a66b158ff659441c4179878d00 +2026-07-29-durable-last-activity-index.zh.md: 4c099bc835c4708fda09811fafd6dabb65dbb575 diff --git a/.agents/notes/proposed/architecture/2026-07-29-durable-last-activity-index.md b/.agents/notes/proposed/architecture/2026-07-29-durable-last-activity-index.md index 0e441f54a7..b530508877 100644 --- a/.agents/notes/proposed/architecture/2026-07-29-durable-last-activity-index.md +++ b/.agents/notes/proposed/architecture/2026-07-29-durable-last-activity-index.md @@ -6,17 +6,17 @@ English | [中文](2026-07-29-durable-last-activity-index.zh.md) ## Problem -A cold (persisted, unattached) session has no stored answer to "when was this last worked in". `dsh-host-apiproxy`'s `summarizeCold()` therefore approximates it with the log file's mtime where one exists — `locate()` resolves a per-session artifact for JSONL and `undefined` for SQLite, whose cold sessions fall back to `createdAt` — and the web client sorts its session tree by the resulting `updatedAt`. The two backends are wrong in opposite directions: JSONL reads too new, SQLite too old. +A cold (persisted, unattached) session has no authoritative stored answer to "when did the user last prompt here". `dsh-host-apiproxy` serves `updatedAt` from the optional projection cache's `lastPromptAt`, falling back to `createdAt`, and the Web client sorts its Session tree by that value. The cache is fail-soft and checkpointed asynchronously, so a missing or delayed row makes a recently prompted Session sort too old. -mtime answers a different question: when the artifact was last written. Every durable write refreshes it, including writes that are not activity — a truncate-repair of a torn tail, the synthetic closers that balance an interrupted turn, and the [`session/end-seed` boundary](../../implemented/architecture/2026-07-30-session-end-seed-log-boundary.md) a seeded session appends. (A `flush` with nothing pending is not among them: the coordinator returns without reaching the backend.) The visible consequence is stable and wrong in one direction: a session touched without being worked in promotes itself above sessions the user actually worked in afterwards, and each touch re-promotes it. `dsh-host-apiproxy` keeps `session.history` inspection-only, but any Agent-bound ordinary-session control resumes through `agentFor()` and is enough to promote the cold artifact. +The gateway previously used JSONL artifact mtime when available. mtime answers a different question: when the artifact was last written. Every durable write refreshes it, including a truncate-repair of a torn tail, synthetic closers that balance an interrupted turn, and the [`session/end-seed` boundary](../../implemented/architecture/2026-07-30-session-end-seed-log-boundary.md) appended during pickup. That approximation promoted a Session merely because it was opened. The [bounded cold blank verification](../../implemented/bug-fix/2026-08-13-bounded-cold-blank-verification.md) removed mtime ordering and accepted the cache's conservative "too old" failure direction as an interim tradeoff. -The attached projection has a real fix — `lastActivityTime()` skips boundaries — but it needs the event log, and the cold path deliberately does not read one. Reading the log to compute `updatedAt` would defeat the header-only listing that keeps `list()` scaling with session count rather than log size. +An attached summary can fold the live event log and select the latest human-authored `user/message`, but the cold path deliberately does not read large logs. Reading every log to compute `updatedAt` would make `list()` scale with total conversation bytes rather than Session count. The 1 KiB cold read introduced for blank verification does not solve recency: it is conditional, targets only small artifacts, and does not make large-log ordering exact. -The [boundary change](../../implemented/architecture/2026-07-30-session-end-seed-log-boundary.md) raised the frequency of this defect, because a pickup now writes where nothing was written before; `dsh-host-apiproxy`'s README records it under Known Limitations. It did not introduce the approximation, and removing the approximation is a durable-format decision, which is why it is scoped here rather than there. +Making cold ordering exact remains a durable-format decision, which is why it is scoped here rather than in the gateway workaround. ## Proposal -Store last-activity time where a listing already reads — the session index — so `summarizeCold()` can serve it without opening the log. The coordinator computes the value, because it sees every append and already owns per-id state; backends persist it. That makes it a new `PersistenceBackend` contract element rather than backend-local bookkeeping, and keeps one definition of "activity" shared with the in-log `lastActivityTime()`. +Store the latest human-prompt time where a listing already reads — the Session index — so `summarizeCold()` can serve it without opening the log or depending on a cache checkpoint. The coordinator computes the value because it sees every append and already owns per-id state; backends persist it. That makes it a new `PersistenceBackend` contract element rather than backend-local bookkeeping, with the same event predicate as the attached projection: `user/message` whose `source.kind` is `user`. The two shipped backends have opposite constraints, and the proposal is deliberately asymmetric about them: @@ -25,7 +25,7 @@ The two shipped backends have opposite constraints, and the proposal is delibera Three questions must be answered before implementation, and none of them is settled here: -**Which events count as activity?** `lastActivityTime()` answers this for the log by excluding `session/end-seed`. A stored field encodes the rule at write time, where the writer sees one batch rather than the whole log. The two must not drift, or the attached and cold surfaces will disagree about the same session. +**How is the shared predicate owned?** A stored field encodes the rule at write time, where the writer sees one batch, while the attached summary folds a whole log. Both must use one exported event predicate or reducer so new message-source variants cannot make attached and cold ordering disagree. **How do pre-field logs behave?** Existing artifacts have no value. Falling back to mtime keeps them at today's accuracy; falling back to `createdAt` is honest but reorders every existing session in the picker and the tree. @@ -39,28 +39,29 @@ Three questions must be answered before implementation, and none of them is sett **Write the boundary only when repair occurred.** Would reduce the frequency, and the [boundary note](../../implemented/architecture/2026-07-30-session-end-seed-log-boundary.md) already rejected it: the predicate must hold for an orderly restart too. Trading a correctness invariant for timestamp accuracy is the wrong direction. -**Derive activity from a projection cache.** `session-projection-cache` already folds tails past a watermark, so a last-activity unit would ride existing machinery. Rejected as the primary shape because the cache is an optional composition entry; a listing served only when a cache plugin is mounted makes ordering depend on composition. +**Derive activity from a projection cache.** This is the current interim implementation. `session-projection-cache` folds tails past a watermark without changing the persistence format, but it is optional and fail-soft. Its absence or checkpoint delay makes ordering depend on cache availability and freshness, so it cannot provide the authoritative value proposed here. ## Acceptance criteria - `SessionSummary.updatedAt` for a cold session equals the same value the attached projection reports for that session, verified by resuming, quitting without a turn, and asserting the order is unchanged across both paths. - A resumed-then-abandoned session does not sort above a session worked in afterwards, in the web session tree and the TUI resume picker, pinned by an assembled snapshot rather than unit tests alone. -- The activity rule has one definition: a test proves the stored field and `lastActivityTime()` agree over a log containing boundaries, closers, and a plain turn. +- The prompt-time rule has one definition: a test proves the stored field and attached fold agree over a log containing human prompts, injected user messages, boundaries, and closers. - Pre-field artifacts load and list without error under the chosen fallback, with the fallback's ordering consequence asserted. - SQLite's `SCHEMA_VERSION` bump rejects the old on-disk version per the repo's no-migration stance. ## Risks -**Two definitions of activity drift.** The stored field is computed per batch, the projection over a whole log. A new event type classified one way at write time and the other at read time yields a session whose cold and attached orderings disagree — a bug that only appears after a restart, which is where it is hardest to notice. +**Two definitions of prompt time drift.** The stored field is computed per batch, the projection over a whole log. A new message source classified one way at write time and the other at read time yields a Session whose cold and attached orderings disagree — a bug that only appears after restart. **A JSONL sidecar can disagree with its log.** A crash between the log append and the sidecar write leaves a stale value with no torn-tail marker to repair it. Every consumer would need to treat the sidecar as a hint, which is close to what mtime already is. **The fallback reorders existing sessions.** Whichever fallback is chosen, users with existing logs see their picker and tree reorder once on upgrade. `createdAt` makes that reordering large. -**Cost may exceed the defect.** The defect is a misordering of abandoned sessions. If the honest answer for JSONL is "keep the approximation", this note's outcome may be documenting that decision rather than implementing a field — and that is an acceptable outcome. +**Cost may exceed the defect.** The remaining defect is conservative misordering when projection metadata is missing or delayed. If the honest answer for JSONL is "keep the cache fallback", this note's outcome may be documenting that decision rather than implementing a field. ## Related -- [The end-seed log boundary](../../implemented/architecture/2026-07-30-session-end-seed-log-boundary.md) — one of the non-activity writes mtime counts; `dsh-session` owns `lastActivityTime()`, the in-log projection a stored field must agree with. +- [Bounded cold blank verification](../../implemented/bug-fix/2026-08-13-bounded-cold-blank-verification.md) — removes mtime ordering, defines the interim projection-cache fallback, and limits direct cold reads to blankness checks. +- [The end-seed log boundary](../../implemented/architecture/2026-07-30-session-end-seed-log-boundary.md) — one of the non-prompt writes that made mtime unsuitable. - [Session persistence](../../implemented/architecture/2026-06-14-session-persistence.md) — the append-only and never-rewrite invariants that rule out a mutable JSONL header field. - [Shared persistence write coordinator](../../implemented/architecture/2026-06-18-shared-persistence-write-coordinator.md) — the append path a stored field would hook into. diff --git a/.agents/notes/proposed/architecture/2026-07-29-durable-last-activity-index.zh.md b/.agents/notes/proposed/architecture/2026-07-29-durable-last-activity-index.zh.md index ebc2e2167d..4c099bc835 100644 --- a/.agents/notes/proposed/architecture/2026-07-29-durable-last-activity-index.zh.md +++ b/.agents/notes/proposed/architecture/2026-07-29-durable-last-activity-index.zh.md @@ -6,17 +6,17 @@ Status: proposed ## 问题 -一个冷会话(已持久化、未附加)对「上次是什么时候在这里面工作过」没有任何已存储的答案。因此 `dsh-host-apiproxy` 的 `summarizeCold()` 在存在日志文件时用它的 mtime 来近似它——`locate()` 为 JSONL 解析出一个逐会话产物,为 SQLite 解析出 `undefined`,而 SQLite 的冷会话会回退到 `createdAt`——而 web 客户端就按由此得到的 `updatedAt` 为自己的会话树排序。这两个后端错的方向正好相反:JSONL 读出来偏新,SQLite 偏旧。 +一个冷会话(已持久化、未附加)对「用户上次是什么时候在这里发出 prompt」没有权威的已存储答案。`dsh-host-apiproxy` 从可选 projection cache 的 `lastPromptAt` 提供 `updatedAt`,缺失时回退到 `createdAt`,Web 客户端按该值为 Session 树排序。cache 采用 fail-soft 并异步写入 checkpoint,因此缺失或延迟的记录会让最近收到 prompt 的 Session 排得过旧。 -mtime 回答的是另一个问题:这份产物上次是什么时候被写入的。每一次持久写入都会刷新它,包括那些并不是活动的写入:一次对撕裂尾部的截断修复、用来平衡被中断的轮次的那些合成 closer,以及带种子的会话会追加的 [`session/end-seed` 边界](../../implemented/architecture/2026-07-30-session-end-seed-log-boundary.md)。(没有待处理内容的 `flush` 不在其中:协调器在到达后端之前就返回了。)用户可见的后果是稳定的,而且只朝一个方向错:一个被触碰过却没有在里面工作过的会话,会把自己排到用户此后真正工作过的那些会话之前,而且每次触碰都会重新把它排上去一次。`dsh-host-apiproxy` 让 `session.history` 保持只执行检查,但任何绑定到 Agent 的普通会话控件都会通过 `agentFor()` 恢复会话,足以把冷态产物排到前面。 +网关以前会在可用时采用 JSONL 产物的 mtime。mtime 回答的是另一件事:这份产物上次是什么时候被写入。每一次持久写入都会刷新它,包括对撕裂尾部的截断修复、平衡中断轮次的合成 closer,以及拾起时追加的 [`session/end-seed` 边界](../../implemented/architecture/2026-07-30-session-end-seed-log-boundary.md)。这套近似会让 Session 仅仅因为被打开就提升排序。[有界冷空白验证](../../implemented/bug-fix/2026-08-13-bounded-cold-blank-verification.md)移除了 mtime 排序,并把 cache 保守的「过旧」错误方向作为现阶段取舍。 -已附加会话的那个投影有真正的修复办法(`lastActivityTime()` 会跳过边界),但它需要事件日志,而冷路径有意不去读日志。为计算 `updatedAt` 而读取日志,会让只读 header 的列举失去意义,而正是它让 `list()` 的开销随会话数量而非日志体量增长。 +已附加摘要可以折叠实时事件日志并选择最新的真人 `user/message`,但冷路径有意不读取大日志。为计算 `updatedAt` 而读取每一份日志,会让 `list()` 的开销随对话总字节数而非 Session 数量增长。为空白验证引入的 1 KiB 冷读取并不能解决最近时间:它是条件式的,只针对小产物,也不能让大日志的排序精确。 -[边界那次变更](../../implemented/architecture/2026-07-30-session-end-seed-log-boundary.md)提高了这个缺陷的出现频率,因为一次拾起如今会在此前完全无写入的路径上产生写入;`dsh-host-apiproxy` 的 README 已在 Known Limitations 中记录该项。它并没有引入这套近似做法,而移除这套近似是一项持久格式决策,因此它的范围划在本文,而不是那里。 +让冷排序变得精确仍是一项持久格式决策,因此其范围留在本文,而不是网关 workaround 中。 ## 提案 -把最后活动时间存到列举本就会读取的地方,也就是会话索引,这样 `summarizeCold()` 无需打开日志就能给出答案。该值由协调器计算,因为它看得到每一次追加,而且本就拥有每 id 状态;由后端负责持久化。这样它就成为 `PersistenceBackend` 约定中新增的一个要素,而不是各后端本地的账目,同时让「活动」只保留一个定义,与日志内的 `lastActivityTime()` 共用。 +把最新真人 prompt 时间存到列举本就会读取的 Session 索引,这样 `summarizeCold()` 无需打开日志或依赖 cache checkpoint 就能给出答案。该值由协调器计算,因为它看得到每一次追加,而且本就拥有每 id 状态;由后端负责持久化。这样它就成为 `PersistenceBackend` 约定中新增的一个要素,而不是各后端本地账目,并与已附加投影使用同一个事件谓词:`source.kind` 为 `user` 的 `user/message`。 两个已交付的后端受到的约束正好相反,本提案对它们有意采取不对称的处理: @@ -25,7 +25,7 @@ mtime 回答的是另一个问题:这份产物上次是什么时候被写入 实现之前必须回答三个问题,本文对它们都没有定论: -**哪些事件算作活动?** 对日志而言,`lastActivityTime()` 通过排除 `session/end-seed` 回答了这个问题。一个已存储字段是在写入时编码这条规则的,而写入方在那里只看到一个批次,不是整份日志。两者不得发生漂移,否则已附加表层与冷表层会对同一个会话给出彼此矛盾的答案。 +**共享谓词由谁拥有?** 已存储字段在写入时编码规则,写入方只看到一个批次,而已附加摘要折叠整份日志。两者必须使用同一个导出的事件谓词或 reducer,避免新的消息来源变体让已附加排序与冷排序发生分歧。 **该字段引入之前的日志表现如何?** 既有产物里没有这个值。回退到 mtime 能让它们保持今天的准确度;回退到 `createdAt` 是诚实的,但会把选择器和会话树里每一个既有会话都重新排一次序。 @@ -39,28 +39,29 @@ mtime 回答的是另一个问题:这份产物上次是什么时候被写入 **仅在确实发生了修复时才写入边界。** 这能降低出现频率,而[边界 Agent Note](../../implemented/architecture/2026-07-30-session-end-seed-log-boundary.md)已经否决过它:谓词对有序重启同样必须成立。用一条正确性不变式去换时间戳的准确度,方向是错的。 -**从投影缓存派生活动时间。** `session-projection-cache` 本就会折叠水位线之后的尾部,因此一个最后活动单元可以搭乘既有机制。它作为主形态被否决,因为该缓存是一个可选的组合项;只有挂载了缓存插件才提供的列举,会让排序取决于如何组合。 +**从投影缓存派生活动时间。** 这是当前的过渡实现。`session-projection-cache` 会折叠水位线之后的尾部,无需改变持久格式,但它是可选且 fail-soft 的。缺失或 checkpoint 延迟会让排序取决于 cache 是否存在以及是否新鲜,因此无法提供本文所提议的权威值。 ## 验收标准 - 冷会话的 `SessionSummary.updatedAt` 等于已附加会话的投影为同一个会话报告的那个值;验证方式是恢复、不跑轮次就退出,并断言两条路径上的顺序都没有变化。 - 在 web 会话树和 TUI 恢复选择器中,一个恢复后即被弃置的会话不会排到此后工作过的会话之前;由一份组装后的快照钉住,而不是只靠单元测试。 -- 活动规则只有一个定义:一个测试证明,在一份同时包含边界、closer 和一个普通轮次的日志上,已存储字段与 `lastActivityTime()` 的结果一致。 +- prompt 时间规则只有一个定义:一个测试证明,在包含真人 prompt、注入式 user message、边界和 closer 的日志上,已存储字段与已附加折叠结果一致。 - 在选定的回退方案下,该字段引入之前的产物能够无错误地加载和列举,并且该回退在排序上的后果有断言覆盖。 - 按本仓库不做迁移的立场,SQLite 的 `SCHEMA_VERSION` 递增会拒绝旧的磁盘版本。 ## 风险 -**「活动」的两个定义发生漂移。** 已存储字段按批次计算,而投影在整份日志上计算。一种新事件类型若在写入时按一种方式归类、在读取时按另一种方式归类,就会产生一个冷排序与已附加排序彼此矛盾的会话;这个缺陷只在重启之后才显现,而那正是最难被注意到的地方。 +**prompt 时间的两个定义发生漂移。** 已存储字段按批次计算,而投影在整份日志上计算。一种新消息来源若在写入时按一种方式归类、在读取时按另一种方式归类,就会产生冷排序与已附加排序彼此矛盾的 Session;该缺陷只会在重启后显现。 **JSONL 的伴随文件可能与它的日志不一致。** 在日志追加与伴随文件写入之间发生崩溃,会留下一个陈旧的值,而且没有撕裂尾部标记可用来修复它。每个消费方都得把伴随文件当作一条提示来对待,而这与 mtime 今天的地位已经很接近了。 **回退方案会让既有会话重新排序。** 无论选定哪种回退,持有既有日志的用户都会在升级时看到自己的选择器和会话树重新排一次序。选 `createdAt` 会让这次重排的幅度很大。 -**代价可能超过这个缺陷本身。** 该缺陷是被弃置会话的排序出错。如果对 JSONL 来说诚实的答案是「保留这套近似」,那么本文的结局可能是记录下这个决定,而不是实现一个字段,而这也是一个可以接受的结局。 +**代价可能超过这个缺陷本身。** 剩余缺陷是 projection metadata 缺失或延迟时的保守错序。如果对 JSONL 来说诚实的答案是「保留 cache 回退」,那么本文的结局可能是记录该决定,而不是实现一个字段。 ## 相关 -- [种子结束日志边界](../../implemented/architecture/2026-07-30-session-end-seed-log-boundary.md)——mtime 会计入的非活动写入之一;`dsh-session` 拥有 `lastActivityTime()`,也就是一个已存储字段必须与之保持一致的那个日志内投影。 +- [有界冷空白验证](../../implemented/bug-fix/2026-08-13-bounded-cold-blank-verification.md)——移除 mtime 排序,定义 projection cache 的过渡回退,并把直接冷读取限制为空白检查。 +- [种子结束日志边界](../../implemented/architecture/2026-07-30-session-end-seed-log-boundary.md)——让 mtime 不适用的非 prompt 写入之一。 - [会话持久化](../../implemented/architecture/2026-06-14-session-persistence.md)——仅追加与绝不重写这两条不变式,正是它们排除了可变的 JSONL header 字段。 - [共享持久化写入协调器](../../implemented/architecture/2026-06-18-shared-persistence-write-coordinator.md)——一个已存储字段将挂入的那条追加路径。 diff --git a/apps/web/tests/cold-blank-session.e2e.ts b/apps/web/tests/cold-blank-session.e2e.ts new file mode 100644 index 0000000000..dd79c4e8b1 --- /dev/null +++ b/apps/web/tests/cold-blank-session.e2e.ts @@ -0,0 +1,60 @@ +/** Cold Session list visibility through the shipped compressed JSONL backend. */ + +import { mkdir, stat } from 'node:fs/promises' +import { fileURLToPath } from 'node:url' +import { join } from 'node:path' +import type { Browser, Page } from 'playwright' +import { chromium } from 'playwright' +import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest' +import { + captureStableAria, compareOrRefreshGolden, launchWebScaffold, seedBlankSession, + watchConsole, webSnapshotMode, type WebScaffold, +} from './scaffold.ts' +import { newEnglishPage, saveFailureShot } from './support.ts' + +const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/cold-blank-session', import.meta.url)) +const SIDEBAR_EXPECTED = join(SNAPSHOT_DIR, 'sidebar.expected.md') +const MODE = webSnapshotMode() +const SESSION_ID = 'cold-blank-session-web-e2e' +const WORKSPACE_NAME = 'cold-blank-workspace' + +describe('web e2e: cold blank Session visibility', () => { + let scaffold: WebScaffold + let browser: Browser + let page: Page + let tripwire: ReturnType + + beforeAll(async () => { + scaffold = await launchWebScaffold({}) + const cwd = join(scaffold.workspaceCwd, WORKSPACE_NAME) + await mkdir(cwd, { recursive: true }) + await seedBlankSession(scaffold, SESSION_ID, cwd) + const header = (await scaffold.ctx.sessionPersistence.list()) + .find(candidate => candidate.id === SESSION_ID) + if (header === undefined) throw new Error('blank Session fixture did not materialize') + const location = scaffold.ctx.sessionPersistence.locate(header) + if (location === undefined) throw new Error('JSONL fixture has no physical artifact') + expect((await stat(location.path)).size).toBeLessThanOrEqual(1024) + + browser = await chromium.launch() + page = await newEnglishPage(browser) + tripwire = watchConsole(page) + await page.goto(scaffold.baseUrl, { waitUntil: 'load' }) + await page.waitForSelector('[class*="frame"]', { timeout: 30_000 }) + }, 120_000) + + afterAll(async () => { + await browser?.close() + await scaffold?.close() + }) + + it('keeps the verified cold blank Session out of the sidebar', async () => { + onTestFailed(() => saveFailureShot(page, 'web-e2e-cold-blank-session')) + const tree = page.getByRole('tree', { name: 'Sessions' }) + await tree.waitFor({ timeout: 30_000 }) + expect(await tree.getByText(WORKSPACE_NAME, { exact: true }).count()).toBe(0) + const sidebar = await captureStableAria(page, '[role="tree"][aria-label="Sessions"]', scaffold.workspaceCwd) + await compareOrRefreshGolden(SIDEBAR_EXPECTED, sidebar, MODE) + expect(tripwire.pageErrors).toEqual([]) + }) +}) diff --git a/apps/web/tests/scaffold.ts b/apps/web/tests/scaffold.ts index 970ff7b139..97e8708547 100644 --- a/apps/web/tests/scaffold.ts +++ b/apps/web/tests/scaffold.ts @@ -23,7 +23,7 @@ // (the plugin-row path discards the ReplayHandle; the direct install keeps // assertConsumed for the teardown fixture-consumption check). import { existsSync } from 'node:fs' -import { mkdir, mkdtemp, readFile, readdir, realpath, rm, utimes, writeFile } from 'node:fs/promises' +import { mkdir, mkdtemp, readFile, readdir, realpath, rm, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' import { pathToFileURL } from 'node:url' @@ -724,6 +724,38 @@ export async function seedSession( delegationDepth: 0, ...agentPreset === undefined ? {} : { agentPreset }, } + await persistSeedSession(scaffold, meta, events) + return meta.id +} + +/** Seed one materialized cold Session whose log has no turn/start event. */ +export async function seedBlankSession( + scaffold: WebScaffold, + id: string, + cwd: string, +): Promise { + const meta: SessionHeader = { + version: SESSION_FORMAT_VERSION, + id: SessionId(id), + createdAt: Date.now() - 60_000, + cwd, + delegationDepth: 0, + } + await persistSeedSession(scaffold, meta, [{ + type: 'session/end-seed', + seq: 0, + time: meta.createdAt, + data: {}, + }]) + return meta.id +} + +/** Materialize one detached Session fixture through the shipped JSONL provider. */ +async function persistSeedSession( + scaffold: WebScaffold, + meta: SessionHeader, + events: readonly SessionEvent[], +): Promise { const seeder = new Context() try { await seeder.plugin(SessionStore) @@ -732,16 +764,9 @@ export async function seedSession( await seeder.plugin(JsonlSessionPersistence, { root: scaffold.persistenceRoot }) await seeder.sessionPersistence.create(meta) await seeder.sessionPersistence.append(meta.id, events) - // Deterministic sidebar order: cold summaries take updatedAt from mtime. - const located = seeder.sessionPersistence.locate(meta) - if (located !== undefined) { - const backdated = new Date(meta.createdAt) - await utimes(located.path, backdated, backdated) - } } finally { await seeder.fiber.dispose() } - return meta.id } /** diff --git a/apps/web/tests/snapshots/cold-blank-session/sidebar.expected.md b/apps/web/tests/snapshots/cold-blank-session/sidebar.expected.md new file mode 100644 index 0000000000..d4d370892e --- /dev/null +++ b/apps/web/tests/snapshots/cold-blank-session/sidebar.expected.md @@ -0,0 +1 @@ +- tree "Sessions": No sessions yet diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json index e087543cf9..6e706c7123 100644 --- a/apps/web/tsconfig.json +++ b/apps/web/tsconfig.json @@ -48,6 +48,7 @@ "tests/replay-round-trip.e2e.ts", "tests/hmr-live.e2e.ts", "tests/seeded-history.e2e.ts", + "tests/cold-blank-session.e2e.ts", "tests/stats-paged-history.e2e.ts", "tests/sidebar-scrollbar.e2e.ts", "tests/conversation-column-overflow.e2e.ts", diff --git a/docs/config-catalog.i18n.yaml b/docs/config-catalog.i18n.yaml index b1bec33ca0..6d64bdd87c 100644 --- a/docs/config-catalog.i18n.yaml +++ b/docs/config-catalog.i18n.yaml @@ -2,5 +2,5 @@ # 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 docs/config-catalog.md -config-catalog.md: 19bfa6d1fb847de4a7207f42dabd67d43f288361 -config-catalog.zh.md: fda208e8fcd2ff6dd697efed84a4073ecbd5a912 +config-catalog.md: 4e7039968dba5409d07a7f1fbbee2d77c9a5d7f7 +config-catalog.zh.md: 3c43a09c359e2bd3f3985c47ba6ad402d93dd2a9 diff --git a/docs/config-catalog.md b/docs/config-catalog.md index 19bfa6d1fb..4e7039968d 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -692,6 +692,12 @@ export interface Config { * @default 6 */ sessionExportCompressionLevel?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 + /** + * Maximum physical size of a cold Session artifact eligible for blankness + * verification. Zero disables probes. + * @default 1024 + */ + coldBlankProbeMaxBytes?: number } ``` diff --git a/docs/config-catalog.zh.md b/docs/config-catalog.zh.md index fda208e8fc..3c43a09c35 100644 --- a/docs/config-catalog.zh.md +++ b/docs/config-catalog.zh.md @@ -694,6 +694,12 @@ export interface Config { * @default 6 */ sessionExportCompressionLevel?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 + /** + * Maximum physical size of a cold Session artifact eligible for blankness + * verification. Zero disables probes. + * @default 1024 + */ + coldBlankProbeMaxBytes?: number } ``` diff --git a/packages/host/apiproxy/README.i18n.yaml b/packages/host/apiproxy/README.i18n.yaml index 037a81519a..ea8d70c9e4 100644 --- a/packages/host/apiproxy/README.i18n.yaml +++ b/packages/host/apiproxy/README.i18n.yaml @@ -2,5 +2,5 @@ # 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 packages/host/apiproxy/README.md -README.md: 9467ec288ae597a43eaf954393005ef81ec02c66 -README.zh.md: 8194bf0a72f52cf9824a067f12040167eaf005da +README.md: 518a7e5640bc62b493244a3d863cfea643386f7f +README.zh.md: 5692e9441d20d9dbec5d8a69263c875eb3f3f907 diff --git a/packages/host/apiproxy/README.md b/packages/host/apiproxy/README.md index 9467ec288a..518a7e5640 100644 --- a/packages/host/apiproxy/README.md +++ b/packages/host/apiproxy/README.md @@ -2,7 +2,7 @@ English | [中文](README.zh.md) -The API gateway shared by every client consists of the TypeScript API contract (`src/api/`, zero Node dependencies, importable from the browser), the fetch carrier pair (`src/fetch/`: `toFetchHandler` on the host side, `AbstractApiClient` plus platform subclasses on the client side), and the host-side implementation (`src/api-proxy.ts`: `createApiProxy` plus the default-exported `ApiProxyService` gateway plugin — config `{nativeOpen?, sessionExportCompressionLevel?}`, provides `ctx.apiProxy`). This package registers no routes; carriers such as HTTP wrap `ctx.apiProxy` themselves. The shipped Web composition lives in [`packages/bundle/web-app/cordis.patch.yml`](../../bundle/web-app/cordis.patch.yml), while its default Agent model selection belongs to [`@deepseek-ai/dsh-agent-default-model`](../../core/agent-default-model/README.md) in the base bundle. +The API gateway shared by every client consists of the TypeScript API contract (`src/api/`, zero Node dependencies, importable from the browser), the fetch carrier pair (`src/fetch/`: `toFetchHandler` on the host side, `AbstractApiClient` plus platform subclasses on the client side), and the host-side implementation (`src/api-proxy.ts`: `createApiProxy` plus the default-exported `ApiProxyService` gateway plugin — config `{nativeOpen?, sessionExportCompressionLevel?, coldBlankProbeMaxBytes?}`, provides `ctx.apiProxy`). This package registers no routes; carriers such as HTTP wrap `ctx.apiProxy` themselves. The shipped Web composition lives in [`packages/bundle/web-app/cordis.patch.yml`](../../bundle/web-app/cordis.patch.yml), while its default Agent model selection belongs to [`@deepseek-ai/dsh-agent-default-model`](../../core/agent-default-model/README.md) in the base bundle. ## The shared Agent default (`agent-default-model` Settings section) @@ -26,7 +26,7 @@ Question responses are validated against their pending request before the first `session.history` reads an attached Session in memory or inspects a cold log through persistence without resuming or publishing an Agent, then pages on append-origin message boundaries. `maxMessages` counts `user/message` and `assistant/message` events that entered the surface by appending, so a model-only replacement copy consumes no quota. Each page stays one contiguous raw event range, which keeps a compaction's log-only `compaction/summary` record on the same page as the replacement that cites it. -`session.history`'s tail page (`beforeSeq` absent) additionally carries an optional `projections` block — the watermark snapshot of every unit registered on `ctx.sessionProjections` (`@deepseek-ai/dsh-session-projection`), with `asOfSeq` = the last event seq the values reflect (`-1` on an empty log). The gateway also subscribes to the registry's change feed and mints a `session/projection` mux frame per changed unit (`{sessionId, key, value, seq}` — live push state, never logged; clients hold one generic per-session value store under higher-seq-wins). The carrier holds no other domain's knowledge (each value passed its unit's own schema inside the registry; the wire schemas keep `values`/`value` wide); loadOlder pages never carry the block, and a composition without the registry serves histories without either surface. The gateway registers exactly one unit of its own: `imageLimits`, the attachments config it enforces at prompt admission, published as a per-boot constant (`apply` keeps the state reference, so baselines alone carry it — no change frames) so clients can refuse an over-limit intake before submit and label upload affordances; the unit activates only while both the registry and the attachments service are composed. +`session.history`'s tail page (`beforeSeq` absent) additionally carries an optional `projections` block — the watermark snapshot of every unit registered on `ctx.sessionProjections` (`@deepseek-ai/dsh-session-projection`), with `asOfSeq` = the last event seq the values reflect (`-1` on an empty log). The gateway also subscribes to the registry's change feed and mints a `session/projection` mux frame per changed unit (`{sessionId, key, value, seq}` — live push state, never logged; clients hold one generic per-session value store under higher-seq-wins). The carrier holds no other domain's knowledge (each value passed its unit's own schema inside the registry; the wire schemas keep `values`/`value` wide); loadOlder pages never carry the block, and a composition without the registry serves histories without either surface. The gateway owns two units: `sessionListMetadata` caches the monotonic blank-to-nonblank transition and latest human prompt time used by `session.list`, while `imageLimits` publishes the attachments config enforced at prompt admission as a per-boot constant (`apply` keeps the state reference, so baselines alone carry it — no change frames) so clients can refuse an over-limit intake before submit and label upload affordances; the latter activates only while both the registry and the attachments service are composed. Session-log export is a host-only download surface, not an RPC: `GET /api/session.export?sessionId=…&includeDescendants=true` streams a ZIP whose files are each session's stored artifact text verbatim (the persistence backend's `readRaw` — exact durable bytes decoded from the physical encoding, never a reconstruction from parsed events), root under its original base name plus each subagent descendant under `subagents//`, and every image any included log references under `media/.` (read and verified from the attachment store; a shared image appears once). `HEAD` runs the same root preparation and returns its status and headers without a response body, so browser clients can detect pre-stream failures before handing the GET to the native download manager. Each live root or descendant crosses the authoritative `SessionStore.flush` durability barrier immediately before its raw artifact read; cold sessions have no in-memory work to flush. Compression runs on the host with fflate's streaming Zip API at validated `sessionExportCompressionLevel` 0–9 (default 6), so deployments can trade CPU and latency against archive size; the response is chunked as it is produced and the host never holds the whole archive in one buffer. Once the response queue reaches its 64 KiB byte high-water mark, production waits until consumer pull restores positive capacity; fflate's synchronous callback can overshoot that bound only by the output of one bounded input push. Request abort and response-body cancellation stop lineage and artifact work, terminate the active compressor, and propagate as cancellation rather than an HTTP 500. It requires the persistence, session-query, and attachment services: a deployment without any answers 500, a persistence backend without per-session raw artifacts answers 501, a missing root session answers 404, and a descendant without a stored artifact or a referenced image that cannot be read fails the stream (fail-loud, never silent under-export). The carrier mounts the endpoint; `ApiProxy.downloads.sessionLog` implements it. @@ -42,7 +42,7 @@ Pending queued input is a live control-plane contract, not conversation history. Background jobs ride the same live-push posture. When `ctx.jobs` is composed, the gateway subscribes to its change feed and broadcasts a whole `session/jobs` snapshot after every registry commit that alters what a session can see — registration, the stopping transition, settlement, and owner-disposal removal — plus a subscription baseline for each session that already has tasks (an absent baseline is the empty set; a change that empties a set still sends `[]`). A change carrying an owner reads through that exact `Agent`, so a push stays correct while its scope tears down; the baseline reads `ctx.agents.get(sessionId)`, which yields only unowned tasks for a session with no live Agent and never resumes a cold one. An unowned change fans out to every subscribed session, because unowned tasks are visible to every caller. The wire `JobView` drops `ownerSession`, `reported`, and `outputLimitBytes`: the frame's own `sessionId` carries the first, and the other two are internal notice and model-presentation policy. A composition without the registry emits no such frames. -Workspace and Session lists are separate reconnect baselines. `workspace.create({ path })` adopts an existing canonical directory and permits basename-derived titles to repeat. `workspace.insertBefore({ workspaceId, beforeWorkspaceId? })` commits one registry-order move and answers the complete order; a pure reorder emits `host/workspace-order-changed` with that complete order, while unknown sources or anchors return `workspace-not-found`. `workspace.delete` removes only the Workspace registration, `session.create` accepts an optional preallocated Session id, and `host/workspace-changed`, `host/workspace-removed`, plus `host/session-added` carry committed increments in either arrival order. `workspace.archiveSession` adds one session to the registry-global archive set and answers the full updated set; `workspace.list` carries that set as the reconnect baseline and `host/archived-sessions-changed` pushes the full snapshot after every durable change. Archiving hides the session from grouping surfaces without touching its log or its workspace account; a session neither live nor persisted fails with `session-not-found`. Registration deletion preserves the directory and session logs; its Sessions remain in `session.list` and become Ungrouped. `SessionSummary.blank` and the `host/session-added` frame carry the derived zero-events bit: clients hide blank sessions and reuse them per workspace, flip blank on the first `host/session-status(running:true)`, and treat `session.list` as the reconnect authority; cold summaries are never blank because lazy persistence keeps never-appended sessions out of `list()`. +Workspace and Session lists are separate reconnect baselines. `workspace.create({ path })` adopts an existing canonical directory and permits basename-derived titles to repeat. `workspace.insertBefore({ workspaceId, beforeWorkspaceId? })` commits one registry-order move and answers the complete order; a pure reorder emits `host/workspace-order-changed` with that complete order, while unknown sources or anchors return `workspace-not-found`. `workspace.delete` removes only the Workspace registration, `session.create` accepts an optional preallocated Session id, and `host/workspace-changed`, `host/workspace-removed`, plus `host/session-added` carry committed increments in either arrival order. `workspace.archiveSession` adds one session to the registry-global archive set and answers the full updated set; `workspace.list` carries that set as the reconnect baseline and `host/archived-sessions-changed` pushes the full snapshot after every durable change. Archiving hides the session from grouping surfaces without touching its log or its workspace account; a session neither live nor persisted fails with `session-not-found`. Registration deletion preserves the directory and session logs; its Sessions remain in `session.list` and become Ungrouped. `SessionSummary.blank` and the `host/session-added` frame carry whether a turn has started: clients hide blank sessions and reuse them per workspace, flip blank on the first `host/session-status(running:true)`, and treat `session.list` as the reconnect authority. Attached summaries fold the live log. A cold summary trusts cached `blank: false`, but treats cached `true` and a cache miss as unverified; when `locate()` resolves an artifact no larger than `coldBlankProbeMaxBytes` (default 1 KiB), the gateway reads that Session with `readFrom()` and checks for `turn/start`. A larger, location-less, vanished, or unreadable artifact remains visible. `updatedAt` is the latest human `user/message` time from the live fold or projection cache, falling back to `createdAt`; pickup boundaries and other writes never promote a Session. `session.search` is a bounded content-search projection over the sessions visible through `session.list`. The gateway asks the optional `ctx.sessionQuery` service for globally ranked current-surface user, assistant, and steering matches, consumes that stream until it has at most 20 visible session/snippet pairs plus one lookahead, and revalidates every hit against the list-derived authorization set before returning it. Provider pages start at 20 hits; when a first-page request rejects that limit, the gateway probes 10, 5, 2, then 1 and retains the learned size for continuation and stale-generation restarts. Returned snippets contain at most 240 Unicode code points, and the response schema independently enforces that bound at each client boundary. Keeping the authorization set in Host memory avoids SQLite's variable ceiling for large valid corpora without weakening visibility or ranking. @@ -80,4 +80,4 @@ None; this package neither assembles nor sends a provider request. - **No protocol version field** — client and host ship together; `host.describe` gains a version negotiation field only when an independently released client exists. - **Search failures include provider diagnostics** — the gateway is a single-user local service. A carrier that exposes it to multiple users must replace internal search details with a public-safe diagnostic. - **Linux native picker requires desktop tooling** — under the `native` capability, `host.pickDirectory` reports an actionable error when neither Zenity nor KDialog is installed; the browse backend is the composition-level fallback (see the [native backend README](../directory-picker-native/README.md)). -- **A cold session's `updatedAt` counts a mere pickup as a write (per-file backends only)** — the attached projection excludes the `session/end-seed` boundary, because picking a session up is not activity, but a cold session's `updatedAt` is its log file's mtime and every durable write refreshes that, the boundary included. `agentFor()` resumes a cold session on first touch, so merely opening one in a client writes it. This applies only where `locate()` resolves a per-session artifact, i.e. JSONL; SQLite returns `undefined`, so its cold sessions fall back to `createdAt` and are skewed the other way — too old rather than too new — independently of this boundary. A session touched without being worked in therefore sorts newer than its last real activity until it attaches. Separating the two needs a log read, which is exactly what the mtime path exists to avoid; a stored last-activity field in the index would fix it at the source, scoped in the [last-activity-index Agent Note](../../../.agents/notes/proposed/architecture/2026-07-29-durable-last-activity-index.md). +- **Cold-list hints degrade only toward visibility and older ordering** — a projection-cache miss or stale `lastPromptAt` falls back to `createdAt`, so a recently worked Session may sort too low until the next checkpoint. A blank artifact larger than `coldBlankProbeMaxBytes`, or one from a backend without `locate()`, remains visible because the gateway cannot verify the absence of `turn/start` within the read bound. The [bounded blank-verification decision](../../../.agents/notes/implemented/bug-fix/2026-08-13-bounded-cold-blank-verification.md) owns this safety direction; an authoritative exact recency index remains scoped in the [last-activity-index proposal](../../../.agents/notes/proposed/architecture/2026-07-29-durable-last-activity-index.md). diff --git a/packages/host/apiproxy/README.zh.md b/packages/host/apiproxy/README.zh.md index 8194bf0a72..5692e9441d 100644 --- a/packages/host/apiproxy/README.zh.md +++ b/packages/host/apiproxy/README.zh.md @@ -2,7 +2,7 @@ [English](README.md) | 中文 -所有客户端共用的 API 网关由三部分组成:TypeScript API 约定(`src/api/`,不依赖 Node,可从浏览器导入)、fetch 载体对(`src/fetch/`:宿主侧的 `toFetchHandler`,以及客户端侧的 `AbstractApiClient` 与平台子类)和宿主侧实现(`src/api-proxy.ts`:`createApiProxy` 加上默认导出的 `ApiProxyService` 网关插件,其配置为 `{nativeOpen?, sessionExportCompressionLevel?}`,提供 `ctx.apiProxy`)。该包不注册任何路由;HTTP 等载体自行包装 `ctx.apiProxy`。随发行版交付的 Web 组合位于 [`packages/bundle/web-app/cordis.patch.yml`](../../bundle/web-app/cordis.patch.yml),其默认 Agent(智能体)模型选择属于 base 组合包中的 [`@deepseek-ai/dsh-agent-default-model`](../../core/agent-default-model/README.md)。 +所有客户端共用的 API 网关由三部分组成:TypeScript API 约定(`src/api/`,不依赖 Node,可从浏览器导入)、fetch 载体对(`src/fetch/`:宿主侧的 `toFetchHandler`,以及客户端侧的 `AbstractApiClient` 与平台子类)和宿主侧实现(`src/api-proxy.ts`:`createApiProxy` 加上默认导出的 `ApiProxyService` 网关插件,其配置为 `{nativeOpen?, sessionExportCompressionLevel?, coldBlankProbeMaxBytes?}`,提供 `ctx.apiProxy`)。该包不注册任何路由;HTTP 等载体自行包装 `ctx.apiProxy`。随发行版交付的 Web 组合位于 [`packages/bundle/web-app/cordis.patch.yml`](../../bundle/web-app/cordis.patch.yml),其默认 Agent(智能体)模型选择属于 base 组合包中的 [`@deepseek-ai/dsh-agent-default-model`](../../core/agent-default-model/README.md)。 ## 共享 Agent 默认值(`agent-default-model` Settings 分节) @@ -26,7 +26,7 @@ Settings 分节中的 `reasoningEffort` 在 agent-default-model 插件配置中 `session.history` 会读取已附加 Session 的内存状态,或通过持久化检查冷日志,而不会恢复或发布 agent,然后按追加来源的消息边界分页:`maxMessages` 统计以追加方式进入 surface 的 `user/message` 和 `assistant/message` 事件,因此仅供模型使用的替换副本不占用配额。每一页仍是一段连续的原始事件区间,从而让压缩(compaction)的仅日志 `compaction/summary` 记录与引用它的替换留在同一页。 -`session.history` 的尾页(不带 `beforeSeq`)额外携带一个可选的 `projections` 块——`ctx.sessionProjections`(`@deepseek-ai/dsh-session-projection`)上每个已注册单元的水位线快照,`asOfSeq` = 这些值共同反映到的最后一个事件 seq(空日志为 `-1`)。网关还订阅注册表的变更流,为每个状态发生变化的单元生成一个 `session/projection` mux 帧(`{sessionId, key, value, seq}`——实时推送状态,绝不入日志;客户端按 seq 高者胜维护一个按会话的通用值仓)。载体不持有其他领域的知识(每个值在注册表内部已过其单元自己的 schema;协议 schema 对 `values`/`value` 保持宽松);loadOlder 页永不携带该块,未装注册表的组合则两个面都不提供。网关唯一自己注册的单元是 `imageLimits`:它在 prompt 准入时执行的 attachments 配置,以每次启动恒定的值发布(`apply` 保持状态引用不变,因此只靠基线携带、绝不产生变更帧),供客户端在提交前拒绝超限的加入并给上传入口标注上限;该单元仅在注册表与 attachments 服务同时组合时激活。 +`session.history` 的尾页(不带 `beforeSeq`)额外携带一个可选的 `projections` 块——`ctx.sessionProjections`(`@deepseek-ai/dsh-session-projection`)上每个已注册单元的水位线快照,`asOfSeq` = 这些值共同反映到的最后一个事件 seq(空日志为 `-1`)。网关还订阅注册表的变更流,为每个状态发生变化的单元生成一个 `session/projection` mux 帧(`{sessionId, key, value, seq}`——实时推送状态,绝不入日志;客户端按 seq 高者胜维护一个按会话的通用值仓)。载体不持有其他领域的知识(每个值在注册表内部已过其单元自己的 schema;协议 schema 对 `values`/`value` 保持宽松);loadOlder 页永不携带该块,未装注册表的组合则两个面都不提供。网关拥有两个单元:`sessionListMetadata` 缓存用于 `session.list` 的单调 blank→nonblank 转换与最新真人 prompt 时间;`imageLimits` 则把 prompt 准入时执行的 attachments 配置作为每次启动恒定的值发布(`apply` 保持状态引用不变,因此只靠基线携带、绝不产生变更帧),供客户端在提交前拒绝超限的加入并给上传入口标注上限,后者仅在注册表与 attachments 服务同时组合时激活。 会话日志导出是宿主侧的下载面,不是 RPC:`GET /api/session.export?sessionId=…&includeDescendants=true` 流式返回一个 ZIP,其中每个文件都是会话存储工件的逐字原文(持久化后端的 `readRaw`——按物理编码解码的确切持久化字节,绝非从解析后事件重建),根会话放在其原始基础文件名下,每个子代理后代放在 `subagents//` 下,每个被任何包含的日志引用的图片放在 `media/.` 下(从附件存储读取并校验;共享图片只出现一次)。`HEAD` 会执行相同的根工件准备,并在没有响应 body 的情况下返回状态与响应头,使浏览器 Client 可以在把 GET 交给原生下载管理器前发现流式传输前的失败。每个实时根会话或后代都会在读取原始工件前立即通过权威的 `SessionStore.flush` 持久性屏障;冷会话没有需要 flush 的内存工作。压缩在宿主侧使用 fflate 流式 Zip API 和已验证的 `sessionExportCompressionLevel` 0–9(默认 6),使部署可以在 CPU/延迟与归档大小之间取舍;响应边生成边分块写出,宿主从不把整个归档放进单个缓冲区。响应队列达到 64 KiB 字节高水位后,生产会等待 Consumer pull 恢复正容量;fflate 的同步回调最多只会让该界限多出一次有界输入 push 的输出。请求中止或响应 body 取消会停止血缘与工件工作、终止活跃压缩器,并继续按取消传播,而不会变成 HTTP 500。它要求同时挂载持久化、session-query 与附件服务:任一缺失应答 500,持久化后端不提供每会话原始工件时应答 501,根会话缺失时应答 404,后代缺少存储工件或引用的图片无法读取则整个流失败(fail-loud,绝不静默少导出)。端点由传输层挂载,`ApiProxy.downloads.sessionLog` 实现它。 @@ -42,7 +42,7 @@ Settings 分节中的 `reasoningEffort` 在 agent-default-model 插件配置中 后台任务沿用同一种实时推送姿态。当组合中有 `ctx.jobs` 时,网关订阅它的变更订阅,并在注册表每一次改变某个会话可见内容的提交后——注册、转入 stopping、结算,以及 owner 销毁时的移除——广播一份完整的 `session/jobs` 快照,另外为每个已经有任务的会话发送订阅 baseline(没有 baseline 即表示空集;把集合清空的那次变更仍然发送 `[]`)。带 owner 的变更通过那个确切的 `Agent` 读取,因此推送在其 scope 拆除期间依然正确;baseline 读 `ctx.agents.get(sessionId)`,对没有活体 Agent 的会话只得到无主任务,且绝不恢复冷会话。无主变更向每一个已订阅会话扇出,因为无主任务对所有调用方可见。线路上的 `JobView` 丢弃 `ownerSession`、`reported` 和 `outputLimitBytes`:第一个由帧自身的 `sessionId` 携带,另外两个分别是内部通知位和模型呈现策略。没有该注册表的组合不发出这类帧。 -Workspace 列表与 Session 列表是相互独立的重连基线。`workspace.create({ path })` 会接纳已有的规范目录,并允许由 basename 派生的标题重复。`workspace.insertBefore({ workspaceId, beforeWorkspaceId? })` 提交一次注册表顺序移动并应答完整顺序;单纯重排序会通过 `host/workspace-order-changed` 推送同一份完整顺序,而未知来源或锚点返回 `workspace-not-found`。`workspace.delete` 只移除 Workspace 注册记录,`session.create` 接受可选的预分配 Session id,`host/workspace-changed`、`host/workspace-removed` 与 `host/session-added` 则以任意到达顺序携带已提交的增量。`workspace.archiveSession` 向注册表级全局归档集合添加一个会话,并应答完整的更新后集合;`workspace.list` 携带该集合作为重连基线,`host/archived-sessions-changed` 在每次持久变更后推送完整快照。归档只把会话从各分组视图中隐藏,不触碰其日志和 workspace 记账;既非活动会话也未持久化的会话以 `session-not-found` 失败。删除注册记录会保留目录和会话日志;相关 Session 仍留在 `session.list` 中,并进入 Ungrouped。`SessionSummary.blank` 与 `host/session-added` 帧携带派生的零事件位:客户端隐藏空白会话并按 workspace 复用它们,在首个 `host/session-status(running:true)` 时翻转 blank,并以 `session.list` 作为重连权威;冷会话摘要永远不是空白:惰性持久化让从未追加过事件的会话根本不出现在 `list()` 中。 +Workspace 列表与 Session 列表是相互独立的重连基线。`workspace.create({ path })` 会接纳已有的规范目录,并允许由 basename 派生的标题重复。`workspace.insertBefore({ workspaceId, beforeWorkspaceId? })` 提交一次注册表顺序移动并应答完整顺序;单纯重排序会通过 `host/workspace-order-changed` 推送同一份完整顺序,而未知来源或锚点返回 `workspace-not-found`。`workspace.delete` 只移除 Workspace 注册记录,`session.create` 接受可选的预分配 Session id,`host/workspace-changed`、`host/workspace-removed` 与 `host/session-added` 则以任意到达顺序携带已提交的增量。`workspace.archiveSession` 向注册表级全局归档集合添加一个会话,并应答完整的更新后集合;`workspace.list` 携带该集合作为重连基线,`host/archived-sessions-changed` 在每次持久变更后推送完整快照。归档只把会话从各分组视图中隐藏,不触碰其日志和 workspace 记账;既非活动会话也未持久化的会话以 `session-not-found` 失败。删除注册记录会保留目录和会话日志;相关 Session 仍留在 `session.list` 中,并进入 Ungrouped。`SessionSummary.blank` 与 `host/session-added` 帧携带是否已开始过轮次:客户端隐藏空白会话并按 workspace 复用它们,在首个 `host/session-status(running:true)` 时翻转 blank,并以 `session.list` 作为重连权威。已附加摘要折叠实时日志。冷摘要信任缓存的 `blank: false`,但把缓存的 `true` 与 cache miss 都视为未经验证;当 `locate()` 解析出的工件不大于 `coldBlankProbeMaxBytes`(默认 1 KiB)时,网关通过 `readFrom()` 读取该 Session 并检查 `turn/start`。更大、无位置、已消失或不可读的工件保持可见。`updatedAt` 取实时折叠或投影缓存中的最新真人 `user/message` 时间,缺失时回退到 `createdAt`;拾起边界及其他写入都不会提升 Session 排序。 `session.search` 是以 `session.list` 所列会话为范围的有界内容搜索投影。网关向可选的 `ctx.sessionQuery` 服务请求全局排序后的当前内容视图中的 user、assistant 和 steering 匹配项,并持续消费该结果流,直到获得至多 20 个可见会话/snippet 对及一个前瞻项;返回前仍会依据从列表推导的授权集合重新校验每个命中。提供方分页初始请求 20 个命中;如果第一页请求因这一上限被拒绝,网关会依次探测 10、5、2、1,并在续传和陈旧世代重启中沿用探测所得的页面大小。返回的 snippet 最多包含 240 个 Unicode 码点,响应 schema 则会在每个客户端边界独立强制执行该上限。将授权集合保留在宿主内存中,可在不削弱可见性或排序的前提下避开有效大型语料库的 SQLite 变量上限。 @@ -80,4 +80,4 @@ Workspace 列表与 Session 列表是相互独立的重连基线。`workspace.cr - **没有协议版本字段**:客户端与宿主一同发布;只有出现独立发布的客户端后,`host.describe` 才会增加版本协商字段。 - **搜索失败会包含提供方诊断信息**:网关是单用户本地服务。将其暴露给多名用户的载体必须用可安全公开的诊断信息替代内部搜索细节。 - **Linux 原生选择器依赖桌面工具**:在 `native` 能力下,Zenity 和 KDialog 均未安装时,`host.pickDirectory` 会给出包含解决建议的错误提示;组合层面的回退是 browse 后端(见 [native 后端 README](../directory-picker-native/README.md))。 -- **冷会话的 `updatedAt` 会把一次单纯的拾起算作写入(仅逐文件后端)**:已附加投影排除了 `session/end-seed` 边界,因为接手一个会话不算活动;但冷会话的 `updatedAt` 取自其日志文件的 mtime,而每一次持久写入都会刷新它,包括这条边界。`agentFor()` 会在首次触碰时恢复一个冷会话,因此在客户端里仅仅打开一个会话就会写入它。这只适用于 `locate()` 能解析出逐会话产物的场景,即 JSONL;SQLite 返回 `undefined`,因此它的冷会话回退到 `createdAt`,偏差方向相反——偏旧而不是偏新——且与这条边界无关。于是一个被触碰过却没有在里面工作过的会话,在重新附加之前会按晚于其最后一次真实活动的时间排序。要把两者区分开需要读取日志,而这恰恰是 mtime 路径存在的目的;在索引中存储一个最后活动字段可以从源头修好它,范围见 [最后活动索引 Agent Note](../../../.agents/notes/proposed/architecture/2026-07-29-durable-last-activity-index.md)。 +- **冷列表提示只向“保持可见、排序偏旧”降级**:projection cache miss 或陈旧的 `lastPromptAt` 会回退到 `createdAt`,因此最近工作过的 Session 可能在下一个 checkpoint 前排得偏低。大于 `coldBlankProbeMaxBytes` 的空白工件,或来自不提供 `locate()` 的后端的空白工件会保持可见,因为网关无法在读取上限内验证其中不存在 `turn/start`。[有界空白验证决策](../../../.agents/notes/implemented/bug-fix/2026-08-13-bounded-cold-blank-verification.md)规定了这个安全方向;权威且精确的最近时间索引仍属于[最后活动索引提案](../../../.agents/notes/proposed/architecture/2026-07-29-durable-last-activity-index.md)的范围。 diff --git a/packages/host/apiproxy/src/api-proxy.ts b/packages/host/apiproxy/src/api-proxy.ts index 9ee0b35ef1..fc986583a0 100644 --- a/packages/host/apiproxy/src/api-proxy.ts +++ b/packages/host/apiproxy/src/api-proxy.ts @@ -15,7 +15,7 @@ import type { ImageAttachmentRef } from '@deepseek-ai/dsh-attachment' import { contentHasImage, createUserMessage, freezeMessage, ReasoningEffortId } from '@deepseek-ai/dsh-llm' import { errorChain } from '@deepseek-ai/dsh-llm' import type { ContentBlock, MessageSource } from '@deepseek-ai/dsh-llm' -import { isAppendSurfaceEvent, isJsonValue, lastActivityTime } from '@deepseek-ai/dsh-session' +import { isAppendSurfaceEvent, isJsonValue } from '@deepseek-ai/dsh-session' import type { JsonValue, Session, SessionEvent, SessionEventMap, SessionHeader, SessionId, UserMessage } from '@deepseek-ai/dsh-session' import type { SessionPersistence } from '@deepseek-ai/dsh-session-persistence' import { SessionQueryError, type SessionSearchCursor } from '@deepseek-ai/dsh-session-query' @@ -38,7 +38,7 @@ import type {} from '@deepseek-ai/dsh-tools' import type { ApiProxy, ConfigurableProviderView, CredentialView, GoalRef, HistoryEntry, HostFrame, ModelCatalogFailure, ModelProviderGroup, - ModelReasoning, MuxFrame, PromptContentPart, QuestionResponsePayload, SessionProjectionsBlock, SessionSearchItem, + ModelReasoning, MuxFrame, PromptContentPart, QuestionResponsePayload, SessionListMetadata, SessionProjectionsBlock, SessionSearchItem, QueuedInboxItem, SessionSummary, SettingsNamespaceView, SubagentAddress, JobView, ToolEventView, WorkspaceId, WorkspaceView, } from './api/index.ts' @@ -90,7 +90,7 @@ import type { ApprovalOutcome, ApprovalRequestId } from '@deepseek-ai/dsh-user-a // `ctx.get('approval')` without a value dependency on the seam (optional composition). import type {} from '@deepseek-ai/dsh-user-approval' import { approvalResponsePayloadSchema } from './api/approvals.schema.ts' -import { imageLimitsProjectionSchema } from './api/sessions.schema.ts' +import { imageLimitsProjectionSchema, sessionListMetadataProjectionSchema } from './api/sessions.schema.ts' import { questionResponsePayloadSchema } from './api/questions.schema.ts' import type { ClientResponse, RpcError, RpcReceipt, RpcRequest, RpcResponse } from './api/rpc.ts' import { RpcId } from './api/rpc.ts' @@ -132,6 +132,8 @@ const SESSION_SEARCH_PROVIDER_CALL_LIMIT = 100 /** Bound cold-log stat fan-out and settle each started batch before cancellation returns. */ const COLD_SUMMARY_BATCH_SIZE = 16 +/** Default maximum artifact size eligible for one cold blankness read. */ +export const DEFAULT_COLD_BLANK_PROBE_MAX_BYTES = 1024 /** Conversation message event types (the pagination counting unit). */ const MESSAGE_TYPES = new Set(['user/message', 'assistant/message']) @@ -506,6 +508,29 @@ function sessionBlank(session: Session): boolean { return !session.events.some(event => event.type === 'turn/start') } +/** Advance the Session-list hint projection by one committed event. */ +function applySessionListMetadata(state: SessionListMetadata, event: SessionEvent): SessionListMetadata { + const blank = state.blank && event.type !== 'turn/start' + const lastPromptAt = event.type === 'user/message' && event.data.source.kind === 'user' + ? event.time + : state.lastPromptAt + return blank === state.blank && lastPromptAt === state.lastPromptAt + ? state + : { blank, lastPromptAt } +} + +/** Fold exact list metadata for an attached Session. */ +function sessionListMetadata(events: readonly SessionEvent[]): SessionListMetadata { + let state: SessionListMetadata = { blank: true, lastPromptAt: null } + for (const event of events) state = applySessionListMetadata(state, event) + return state +} + +/** Sort by creation or latest human prompt, whichever is newer. */ +function sessionListUpdatedAt(header: SessionHeader, metadata: SessionListMetadata | undefined): number { + return Math.max(header.createdAt, metadata?.lastPromptAt ?? 0) +} + /** Shared Session-header projection for list baselines and creation frames. */ function sessionListFields(header: SessionHeader, events: readonly SessionEvent[] = []): { parentSessionId?: SessionId @@ -527,47 +552,71 @@ function sessionListFields(header: SessionHeader, events: readonly SessionEvent[ /** SessionSummary projection for attached (in-memory) sessions. */ function summarize(session: Session, running: boolean): SessionSummary { + const metadata = sessionListMetadata(session.events) return { sessionId: session.id, - // Excludes end-seed: a resumed-but-untouched session - // must not sort as freshly worked in. - updatedAt: lastActivityTime(session.events) ?? session.header.createdAt, + updatedAt: sessionListUpdatedAt(session.header, metadata), running, - blank: sessionBlank(session), + blank: metadata.blank, ...sessionListFields(session.header, session.events), } } /** - * SessionSummary projection for cold (persisted, unattached) sessions. - * updatedAt is the log file's mtime; backends without a per-session file - * (locate() undefined) fall back to the header's createdAt. + * Verify a possibly blank cold Session only when its physical artifact is + * within the configured per-Session read bound. A stale `blank: true`, an + * absent cache row, a large or location-less artifact, and read failures all + * resolve to visible (`false`); listing must never hide a conversation on a + * cache hint or an unavailable optimization. */ -async function summarizeCold( +async function probeColdSessionBlank( + ctx: Context, persistence: SessionPersistence, meta: SessionHeader, + maxBytes: number, + signal?: AbortSignal, +): Promise { + if (maxBytes === 0) return false + signal?.throwIfAborted() + const location = persistence.locate(meta) + if (location === undefined) return false + signal?.throwIfAborted() + let size: number + try { + size = (await stat(location.path)).size + } catch { + signal?.throwIfAborted() + return false + } + if (size > maxBytes) return false + try { + const { events } = await persistence.readFrom(meta.id, 0, signal) + signal?.throwIfAborted() + return !events.some(event => event.type === 'turn/start') + } catch (error) { + signal?.throwIfAborted() + ctx.logger.warn(`session.list: blank probe for "${meta.id}" failed (serving it as visible): ${String(error)}`) + return false + } +} + +/** SessionSummary projection for a cold persisted Session. */ +async function summarizeCold( + ctx: Context, + persistence: SessionPersistence, + meta: SessionHeader, + metadata: SessionListMetadata | undefined, + blankProbeMaxBytes: number, signal?: AbortSignal, ): Promise { - signal?.throwIfAborted() - let updatedAt = meta.createdAt - const location = persistence.locate(meta) - signal?.throwIfAborted() - if (location !== undefined) { - try { - updatedAt = (await stat(location.path)).mtimeMs - } catch { - // The log vanished between list() and stat() (concurrent cleanup); createdAt stands in. - } - signal?.throwIfAborted() - } + const blank = metadata?.blank === false + ? false + : await probeColdSessionBlank(ctx, persistence, meta, blankProbeMaxBytes, signal) return { sessionId: meta.id, - updatedAt, + updatedAt: sessionListUpdatedAt(meta, metadata), running: false, - // Lazy persistence keeps never-appended sessions out of list(); reading - // a cold log to check for turns would defeat the index read, so a listed - // cold session is served as not-blank (its log holds its conversation). - blank: false, + blank, // Header-only: reading the log for a blank-window preset switch would // defeat the same index read, and attaching the session replaces this row // with `summarize()`, which resolves the switch from the events. @@ -608,6 +657,8 @@ export interface ApiProxyDefaults { openTextFile?: (path: string, signal: AbortSignal) => Promise /** Validated DEFLATE level for session-log ZIP entries; defaults to 6. */ sessionExportCompressionLevel?: SessionLogCompressionLevel + /** Maximum artifact size eligible for one cold blankness read. */ + coldBlankProbeMaxBytes?: number /** * Whether handing a path to the native opener can work at all — the * `hasDocument` capability the preset roster reports, and the switch @@ -1055,6 +1106,8 @@ function changedWorkspaceView(workspaceId: string, value: unknown): WorkspaceVie export function createApiProxy(ctx: Context, defaults: ApiProxyDefaults): ApiProxy { const sessionExportCompressionLevel = defaults.sessionExportCompressionLevel ?? DEFAULT_SESSION_LOG_COMPRESSION_LEVEL + const coldBlankProbeMaxBytes = defaults.coldBlankProbeMaxBytes + ?? DEFAULT_COLD_BLANK_PROBE_MAX_BYTES /** The seed model each create/resume declares; re-read so it never goes stale. */ const agentOptions = (): AgentOptions => { const { provider, model } = defaults.defaultModelSelection() @@ -1233,6 +1286,19 @@ export function createApiProxy(ctx: Context, defaults: ApiProxyDefaults): ApiPro }) }) + // The cache supplies recency and a monotonic non-blank hint. A cached + // `blank: true` remains only a prefix fact and is verified on the cold path. + ctx.inject(['sessionProjections'], (projectionCtx) => { + projectionCtx.sessionProjections.register<'sessionListMetadata', SessionListMetadata>({ + key: 'sessionListMetadata', + schema: sessionListMetadataProjectionSchema, + init: () => ({ blank: true, lastPromptAt: null }), + apply: applySessionListMetadata, + view: state => state, + stateVersion: 1, + }) + }) + // The imageLimits projection unit: the attachments config this proxy // enforces at prompt admission, constant per host boot. `apply` keeps the // same state reference for every event, so no change frames are ever @@ -1678,11 +1744,18 @@ export function createApiProxy(ctx: Context, defaults: ApiProxyDefaults): ApiPro const batch = cold.slice(offset, offset + COLD_SUMMARY_BATCH_SIZE) const settled = await Promise.allSettled( batch.map(async (meta) => { - // Cold rows read the persisted projection cache only — never a - // log load; a session without a cache row simply has no column. + // Projection hints remain optional. Blank verification may read + // this Session's artifact only when it fits the configured bound. const projections = listProjectionsFor(ctx, meta, undefined) return { - ...await summarizeCold(persistence, meta, signal), + ...await summarizeCold( + ctx, + persistence, + meta, + projections?.values.sessionListMetadata, + coldBlankProbeMaxBytes, + signal, + ), ...projections === undefined ? {} : { projections }, } }), diff --git a/packages/host/apiproxy/src/api/index.ts b/packages/host/apiproxy/src/api/index.ts index ea632e89ee..b5e1d1ffd9 100644 --- a/packages/host/apiproxy/src/api/index.ts +++ b/packages/host/apiproxy/src/api/index.ts @@ -45,7 +45,7 @@ export interface ApiProxy { export type { HistoryEntry, ModelCatalogFailure, ModelCatalogModel, ModelProviderGroup, ModelReasoning, ModelReasoningEffort, ModelSelection, PromptContentPart, QueueAction, SessionModels, - SessionProjectionsBlock, SessionSearchItem, SessionsApi, SessionSummary, + SessionListMetadata, SessionProjectionsBlock, SessionSearchItem, SessionsApi, SessionSummary, } from './sessions.ts' export type { DirectoryEntry, DirectoryListing, HostApi } from './host.ts' export type { diff --git a/packages/host/apiproxy/src/api/sessions.schema.ts b/packages/host/apiproxy/src/api/sessions.schema.ts index e62087c63a..0fbe06bf11 100644 --- a/packages/host/apiproxy/src/api/sessions.schema.ts +++ b/packages/host/apiproxy/src/api/sessions.schema.ts @@ -12,7 +12,7 @@ import type { RequestPayload, ResponseValue } from './rpc-map.ts' import type { Wire } from './rpc.schema.ts' import type { HistoryEntry, ModelCatalogFailure, ModelCatalogModel, ModelProviderGroup, ModelReasoning, - ModelReasoningEffort, ModelSelection, SessionProjectionsBlock, SessionSearchItem, SessionSummary, + ModelReasoningEffort, ModelSelection, SessionListMetadata, SessionProjectionsBlock, SessionSearchItem, SessionSummary, } from './sessions.ts' import type { ToolEventView } from './events.ts' import type { AttachmentIdType, ImageAttachmentLimits, ImageAttachmentRef } from '@deepseek-ai/dsh-attachment' @@ -215,6 +215,12 @@ export const sessionProjectionsBlockSchema = z.object({ values: z.record(z.string(), z.unknown()), }) as unknown as z.ZodType> +/** Host-side validation for the persisted Session-list projection. */ +export const sessionListMetadataProjectionSchema: z.ZodType = z.object({ + blank: z.boolean(), + lastPromptAt: z.number().nullable(), +}) + /** * imageLimits projection unit schema (host-side view validation). zod widens * `readonly ImageMediaType[]` to `string[]`; on the JSON wire the two diff --git a/packages/host/apiproxy/src/api/sessions.ts b/packages/host/apiproxy/src/api/sessions.ts index 2635668fb7..c834718cf0 100644 --- a/packages/host/apiproxy/src/api/sessions.ts +++ b/packages/host/apiproxy/src/api/sessions.ts @@ -17,6 +17,13 @@ import type { WorkspaceId } from './workspace.ts' declare module '@deepseek-ai/dsh-session-projection/types' { interface SessionProjectionMap { + /** + * Session-list hints persisted by the projection cache. `blank: false` + * is monotonic and may suppress a cold-log probe; `blank: true` is only a + * checkpoint-prefix fact and must not hide a cold Session without direct + * verification. `lastPromptAt` is the latest human-authored prompt time. + */ + sessionListMetadata: SessionListMetadata /** * The deployment's image-intake limits: the attachments service's config * as this proxy enforces it at prompt admission, constant per host boot. @@ -28,6 +35,14 @@ declare module '@deepseek-ai/dsh-session-projection/types' { } } +/** Persisted hints used to summarize a cold Session without reading a large log. */ +export interface SessionListMetadata { + /** Whether the checkpoint prefix contains no turn/start event. */ + blank: boolean + /** Latest source.kind=user message time in the checkpoint prefix. */ + lastPromptAt: number | null +} + declare module '@deepseek-ai/dsh-llm' { interface MessageSourceMap { /** diff --git a/packages/host/apiproxy/src/index.ts b/packages/host/apiproxy/src/index.ts index f7824d1d14..ac6c770801 100644 --- a/packages/host/apiproxy/src/index.ts +++ b/packages/host/apiproxy/src/index.ts @@ -16,7 +16,7 @@ import { Context, Service } from '@deepseek-ai/cordis' import z from '@deepseek-ai/schemastery' import type {} from '@deepseek-ai/dsh-agent-default-model' import type { ApiProxy } from './api/index.ts' -import { createApiProxy } from './api-proxy.ts' +import { createApiProxy, DEFAULT_COLD_BLANK_PROBE_MAX_BYTES } from './api-proxy.ts' import { DEFAULT_SESSION_LOG_COMPRESSION_LEVEL, type SessionLogCompressionLevel, @@ -53,6 +53,12 @@ export interface Config { * @default 6 */ sessionExportCompressionLevel?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 + /** + * Maximum physical size of a cold Session artifact eligible for blankness + * verification. Zero disables probes. + * @default 1024 + */ + coldBlankProbeMaxBytes?: number } /** @@ -70,6 +76,7 @@ export class ApiProxyService extends Service implements ApiProxy { nativeOpen: z.boolean(), sessionExportCompressionLevel: z.number().step(1).min(0).max(9) .default(DEFAULT_SESSION_LOG_COMPRESSION_LEVEL) as z, + coldBlankProbeMaxBytes: z.natural().default(DEFAULT_COLD_BLANK_PROBE_MAX_BYTES), }) readonly sessions: ApiProxy['sessions'] @@ -96,6 +103,9 @@ export class ApiProxyService extends Service implements ApiProxy { ...(config.sessionExportCompressionLevel === undefined ? {} : { sessionExportCompressionLevel: config.sessionExportCompressionLevel }), + ...(config.coldBlankProbeMaxBytes === undefined + ? {} + : { coldBlankProbeMaxBytes: config.coldBlankProbeMaxBytes }), }) this.sessions = api.sessions this.subagents = api.subagents diff --git a/packages/host/apiproxy/tests/api-proxy-cold.spec.ts b/packages/host/apiproxy/tests/api-proxy-cold.spec.ts index 796b1e0fe7..20a891d420 100644 --- a/packages/host/apiproxy/tests/api-proxy-cold.spec.ts +++ b/packages/host/apiproxy/tests/api-proxy-cold.spec.ts @@ -4,7 +4,7 @@ * isolation, and prompt failure mapping. */ -import { mkdtempSync, writeFileSync, utimesSync } from 'node:fs' +import { mkdtempSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' import { describe, expect, it, vi } from 'vitest' @@ -13,7 +13,7 @@ import SessionStore from '@deepseek-ai/dsh-session' import AgentRegistry from '@deepseek-ai/dsh-agent' import { TypertLookupFailure } from '@deepseek-ai/dsh-typert-protocol' import TypertRegistry from '@deepseek-ai/dsh-typert-registry' -import { MessageId } from '@deepseek-ai/dsh-llm' +import { createUserMessage, MessageId } from '@deepseek-ai/dsh-llm' import type { Agent } from '@deepseek-ai/dsh-agent' import UserQuestionService from '@deepseek-ai/dsh-user-questions' import type { SessionEvent, SessionHeader, SessionId } from '@deepseek-ai/dsh-session' @@ -39,55 +39,120 @@ function header(id: string, createdAt: number, extra: Partial = { } describe('sessions.list cold merge', () => { - it('summarizes unattached sessions: log mtime, locate-less and vanished-log createdAt fallbacks, lineage', async () => { + it('verifies only small possibly-blank artifacts and treats every unavailable probe as visible', async () => { const ctx = new Context() await ctx.plugin(SessionStore) await ctx.plugin(UserQuestionService) const root = mkdtempSync(join(tmpdir(), 'dsh-cold-')) - const logPath = join(root, 'a.log') - writeFileSync(logPath, 'log-bytes') - utimesSync(logPath, 5000, 5000) // mtime 5_000_000 ms — newer than every createdAt below + const smallPath = join(root, 'small.log') + const largePath = join(root, 'large.log') + writeFileSync(smallPath, 'x'.repeat(1024)) + writeFileSync(largePath, 'x'.repeat(1025)) const metas = [ - header('session-a', 1000), - header('session-b', 2000, { parentSession: sid('session-parent'), origin: 'subagent' }), - header('session-c', 1500), + header('small-blank', 100), + header('small-conversation', 200), + header('large-unknown', 300), + header('cached-nonblank', 400), + header('locationless', 500, { parentSession: sid('session-parent'), origin: 'subagent' }), + header('vanished', 600), + header('read-failure', 700), ] - // Structural fake of the persistence face list() consumes: list + locate. - // locate: a real per-session file (mtime wins), a backend without one - // (SQLite shape → createdAt), and a path whose file vanished (stat ENOENT - // → createdAt). + const readFrom = vi.fn(async (id: SessionId) => { + if (id === sid('small-blank')) { + return { + meta: metas[0]!, + events: [{ type: 'session/end-seed', seq: 0, time: 700, data: {} }] as SessionEvent[], + } + } + if (id === sid('small-conversation')) { + return { + meta: metas[1]!, + events: [{ type: 'turn/start', seq: 0, time: 800, data: { turn: 1 } }] as SessionEvent[], + } + } + if (id === sid('read-failure')) throw new Error('simulated read failure') + throw new Error(`unexpected cold read: ${id}`) + }) ctx.provide('sessionPersistence', { list: () => Promise.resolve(metas), locate: (meta: SessionHeader) => { - if (meta.id === sid('session-a')) return { kind: 'jsonl', path: logPath } - if (meta.id === sid('session-c')) return { kind: 'jsonl', path: join(root, 'vanished.log') } + if (meta.id === sid('large-unknown')) return { kind: 'jsonl', path: largePath } + if (meta.id === sid('locationless')) return undefined + if (meta.id === sid('vanished')) return { kind: 'jsonl', path: join(root, 'vanished.log') } + return { kind: 'jsonl', path: smallPath } + }, + readFrom, + } as never) + ctx.provide('sessionProjectionCache', { + cachedSnapshot: (meta: SessionHeader) => { + if (meta.id === sid('small-blank')) { + return { asOfSeq: 0, values: { sessionListMetadata: { blank: true, lastPromptAt: null } } } + } + if (meta.id === sid('small-conversation')) { + return { asOfSeq: 0, values: { sessionListMetadata: { blank: true, lastPromptAt: 900 } } } + } + if (meta.id === sid('cached-nonblank')) { + return { asOfSeq: 1, values: { sessionListMetadata: { blank: false, lastPromptAt: 1000 } } } + } return undefined }, - }) + } as never) const api = createApiProxy(ctx, { defaultModelSelection: () => ({ provider: 'p', model: 'm' }), cwd: '/tmp' }) const response = await api.sessions.list(request({})) expect(response.result.ok).toBe(true) if (!response.result.ok) throw new Error('unreachable') - const items = response.result.value.items - expect(items.map(item => item.sessionId)).toEqual(['session-a', 'session-b', 'session-c']) - const [a, b, c] = items - expect(a?.updatedAt).toBeCloseTo(5_000_000, -3) - expect(a?.running).toBe(false) - // Cold summaries are never blank: lazy persistence keeps never-appended - // sessions out of list(), so a listed session necessarily has events. - expect(items.every(item => !item.blank)).toBe(true) - expect(a?.cwd).toBe('/proj') - expect(a?.parentSessionId).toBeUndefined() - expect(b?.updatedAt).toBe(2000) - expect(b?.parentSessionId).toBe('session-parent') - expect(b?.origin).toBe('subagent') - expect(c?.updatedAt).toBe(1500) + const byId = Object.fromEntries(response.result.value.items.map(item => [item.sessionId, item])) + expect(byId['small-blank']).toMatchObject({ blank: true, updatedAt: 100, running: false }) + // A stale true hint cannot hide the turn found in the bounded read. + expect(byId['small-conversation']).toMatchObject({ blank: false, updatedAt: 900 }) + expect(byId['large-unknown']).toMatchObject({ blank: false, updatedAt: 300 }) + // false is monotonic, so this row skips stat/read and keeps cached recency. + expect(byId['cached-nonblank']).toMatchObject({ blank: false, updatedAt: 1000 }) + expect(byId['locationless']).toMatchObject({ + blank: false, + updatedAt: 500, + parentSessionId: 'session-parent', + origin: 'subagent', + }) + expect(byId['vanished']).toMatchObject({ blank: false, updatedAt: 600 }) + expect(byId['read-failure']).toMatchObject({ blank: false, updatedAt: 700 }) + expect(readFrom).toHaveBeenCalledTimes(3) + expect(readFrom.mock.calls.map(([id]) => id)).toEqual(expect.arrayContaining([ + sid('small-blank'), + sid('small-conversation'), + sid('read-failure'), + ])) + }) + + it('can disable bounded blank probes without hiding cold Sessions', async () => { + const ctx = new Context() + await ctx.plugin(SessionStore) + await ctx.plugin(UserQuestionService) + const meta = header('probe-disabled', 100) + const readFrom = vi.fn() + ctx.provide('sessionPersistence', { + list: () => Promise.resolve([meta]), + locate: () => ({ kind: 'jsonl', path: '/not-read' }), + readFrom, + } as never) + const api = createApiProxy(ctx, { + defaultModelSelection: () => ({ provider: 'p', model: 'm' }), + cwd: '/tmp', + coldBlankProbeMaxBytes: 0, + }) + + const response = await api.sessions.list(request({})) + if (!response.result.ok) throw new Error('unreachable') + expect(response.result.value.items).toEqual([ + expect.objectContaining({ sessionId: meta.id, blank: false, updatedAt: meta.createdAt }), + ]) + expect(readFrom).not.toHaveBeenCalled() }) }) -describe('attached updatedAt excludes end-seed', () => { - it('reports the last real work, not the pickup, so a resumed-untouched session does not float', async () => { +describe('attached updatedAt tracks human prompts', () => { + it('ignores pickup and non-prompt work after the latest human message', async () => { const ctx = new Context() await ctx.plugin(SessionStore) await ctx.plugin(UserQuestionService) @@ -99,7 +164,12 @@ describe('attached updatedAt excludes end-seed', () => { const resumed = ctx.sessions.create(sid('resumed-untouched'), { seed: [ { type: 'turn/start', seq: 0, time: worked, data: { turn: 1 } }, - { type: 'turn/end', seq: 1, time: worked, data: { turn: 1, reason: { kind: 'completed' } } }, + { + type: 'user/message', seq: 1, time: worked, + data: createUserMessage({ content: [{ type: 'text', text: 'worked' }], source: { kind: 'user' } }), + surfaceOp: 'append', + }, + { type: 'turn/end', seq: 2, time: worked + 1, data: { turn: 1, reason: { kind: 'completed' } } }, ], meta: { cwd: '/proj', createdAt: 500 }, }) @@ -113,12 +183,21 @@ describe('attached updatedAt excludes end-seed', () => { const summary = listed.result.value.items.find(item => item.sessionId === 'resumed-untouched') expect(summary?.updatedAt).toBe(worked) - // Real work appended after end-seed does move it. + // A lifecycle boundary is not a human update. resumed.append('turn/start', { turn: 2 }) + const afterBoundary = await api.sessions.list(request({})) + if (!afterBoundary.result.ok) throw new Error('list failed') + expect(afterBoundary.result.value.items.find(item => item.sessionId === 'resumed-untouched')?.updatedAt) + .toBe(worked) + + const prompt = resumed.append('user/message', createUserMessage({ + content: [{ type: 'text', text: 'new prompt' }], + source: { kind: 'user' }, + }), { surfaceOp: 'append' }) const after = await api.sessions.list(request({})) if (!after.result.ok) throw new Error('list failed') const moved = after.result.value.items.find(item => item.sessionId === 'resumed-untouched') - expect(moved?.updatedAt).toBeGreaterThan(worked) + expect(moved?.updatedAt).toBe(prompt.time) }) }) diff --git a/packages/host/apiproxy/tests/api-proxy-projections.spec.ts b/packages/host/apiproxy/tests/api-proxy-projections.spec.ts index 09c7811696..bef2dba6a2 100644 --- a/packages/host/apiproxy/tests/api-proxy-projections.spec.ts +++ b/packages/host/apiproxy/tests/api-proxy-projections.spec.ts @@ -7,7 +7,7 @@ * pushed to mux consumers as a session/projection frame minted here. */ -import { describe, expect, it } from 'vitest' +import { describe, expect, it, vi } from 'vitest' import { Context } from '@deepseek-ai/cordis' import { z } from 'zod' import AgentRegistry, { Inbox } from '@deepseek-ai/dsh-agent' @@ -163,10 +163,29 @@ describe('session.history projections block', () => { dispose() const after = await proxy.sessions.history(request({ sessionId: session.id })) if (!after.result.ok) throw new Error('unreachable') - // The registry is still mounted, so the block itself stays (asOfSeq cut - // with zero keys); the disposed key reads as capability absence. + // The registry stays mounted; only the disposed key leaves while the + // gateway-owned Session-list unit remains. expect(after.result.value.projections?.asOfSeq).toBe(session.seq - 1) - expect(after.result.value.projections?.values).toEqual({}) + expect('test/last-user' in (after.result.value.projections?.values ?? {})).toBe(false) + expect(after.result.value.projections?.values.sessionListMetadata).toEqual({ + blank: true, + lastPromptAt: session.events.at(-1)?.time, + }) + }) + + it('removes the gateway-owned Session-list unit when the gateway fiber unloads', async () => { + const { ctx, session } = await harness(true) + expect('sessionListMetadata' in ctx.sessionProjections.snapshot(session).values).toBe(false) + const fiber = ctx.plugin(Object.assign((gatewayCtx: Context) => { + createApiProxy(gatewayCtx, { defaultModelSelection: () => ({ provider: 'p', model: 'm' }), cwd: '/tmp' }) + }, { inject: ['sessions', 'agents', 'userQuestions', 'sessionProjections'] })) + await fiber.await() + await vi.waitFor(() => { + expect(ctx.sessionProjections.snapshot(session).values.sessionListMetadata) + .toEqual({ blank: true, lastPromptAt: null }) + }) + await fiber.dispose() + expect('sessionListMetadata' in ctx.sessionProjections.snapshot(session).values).toBe(false) }) }) @@ -174,11 +193,18 @@ describe('session.list projections column', () => { it('serves attached rows from the live registry cut, watermarked for client seeding', async () => { const { ctx, session } = await harness(true) ctx.sessionProjections.register(lastUserUnit()) + const gateway = api(ctx) + await new Promise(resolve => setTimeout(resolve, 0)) + session.append('turn/start', { turn: 1 }) seedMessages(session, 1) - const response = await api(ctx).sessions.list(request({})) + const response = await gateway.sessions.list(request({})) if (!response.result.ok) throw new Error('unreachable') const row = response.result.value.items.find(item => item.sessionId === session.id) expect(row?.projections?.values['test/last-user']).toEqual({ text: 'm0' }) + expect(row?.projections?.values.sessionListMetadata).toEqual({ + blank: false, + lastPromptAt: session.events.at(-1)?.time, + }) expect(row?.projections?.asOfSeq).toBe(session.seq - 1) }) @@ -266,21 +292,33 @@ describe('session/projection push frame', () => { await new Promise(resolve => setTimeout(resolve, 0)) const abort = new AbortController() const stream = proxy.events.mux({ rpcId: RpcId('t-proj-mux'), payload: {} }, abort.signal) - const collected = collect(stream, 2, abort) + const collected = collect(stream, 5, abort) + const now = vi.spyOn(Date, 'now').mockReturnValue(100) seedMessages(session, 1) - // Same-reference apply: turn/start does not concern the unit — no frame. + now.mockReturnValue(200) session.append('turn/start', { turn: 1 }) + now.mockReturnValue(300) seedMessages(session, 1) + now.mockRestore() const frames = await collected const pushes = frames.filter( - (f): f is Extract => f.type === 'session/projection', + (f): f is Extract => + f.type === 'session/projection' && f.key === 'test/last-user', ) expect(pushes).toEqual([ { type: 'session/projection', sessionId: session.id, key: 'test/last-user', value: { text: 'm0' }, seq: 0 }, { type: 'session/projection', sessionId: session.id, key: 'test/last-user', value: { text: 'm0' }, seq: 2 }, ]) + expect(frames.filter( + (f): f is Extract => + f.type === 'session/projection' && f.key === 'sessionListMetadata', + )).toEqual([ + { type: 'session/projection', sessionId: session.id, key: 'sessionListMetadata', value: { blank: true, lastPromptAt: 100 }, seq: 0 }, + { type: 'session/projection', sessionId: session.id, key: 'sessionListMetadata', value: { blank: false, lastPromptAt: 100 }, seq: 1 }, + { type: 'session/projection', sessionId: session.id, key: 'sessionListMetadata', value: { blank: false, lastPromptAt: 300 }, seq: 2 }, + ]) // Frame seq aligns with the tail block's asOfSeq vocabulary (higher-seq-wins compatible). const tail = await proxy.sessions.history(request({ sessionId: session.id })) if (!tail.result.ok) throw new Error('unreachable') diff --git a/packages/host/apiproxy/tests/api-proxy-subagents.spec.ts b/packages/host/apiproxy/tests/api-proxy-subagents.spec.ts index d14ba4321a..a11e2a0416 100644 --- a/packages/host/apiproxy/tests/api-proxy-subagents.spec.ts +++ b/packages/host/apiproxy/tests/api-proxy-subagents.spec.ts @@ -95,7 +95,12 @@ function bench(options: { }) // The gateway's own projection push feed subscribes at construction; the // no-op disposer keeps that feed quiet while these tests pin history reads. - ctx.provide('sessionProjections', { snapshot, restore, onChanged: () => () => {} }) + ctx.provide('sessionProjections', { + snapshot, + restore, + onChanged: () => () => {}, + register: () => () => {}, + }) ctx.provide('userQuestions', { registerProvider: () => () => {} }) const api = createApiProxy(ctx, { defaultModelSelection: () => ({ provider: 'p', model: 'm' }), cwd: '/tmp', diff --git a/packages/host/apiproxy/tests/session-export.spec.ts b/packages/host/apiproxy/tests/session-export.spec.ts index 40b0c0f4b7..5b5fcaa216 100644 --- a/packages/host/apiproxy/tests/session-export.spec.ts +++ b/packages/host/apiproxy/tests/session-export.spec.ts @@ -127,17 +127,32 @@ async function responseBytes(response: Response): Promise { describe('session export compression config', () => { it('defaults to level 6 and rejects values outside the integer 0-9 range', () => { - expect(ApiProxyService.Config({})).toEqual({ sessionExportCompressionLevel: 6 }) + expect(ApiProxyService.Config({})).toEqual({ + sessionExportCompressionLevel: 6, + coldBlankProbeMaxBytes: 1024, + }) expect(ApiProxyService.Config({ sessionExportCompressionLevel: 0 })) - .toEqual({ sessionExportCompressionLevel: 0 }) + .toEqual({ sessionExportCompressionLevel: 0, coldBlankProbeMaxBytes: 1024 }) expect(ApiProxyService.Config({ sessionExportCompressionLevel: 9 })) - .toEqual({ sessionExportCompressionLevel: 9 }) + .toEqual({ sessionExportCompressionLevel: 9, coldBlankProbeMaxBytes: 1024 }) for (const value of [-1, 10, 1.5]) { expect(() => ApiProxyService.Config({ sessionExportCompressionLevel: value } as never)).toThrow() } }) }) +describe('cold blank probe config', () => { + it('accepts a per-Session byte bound including zero and rejects invalid bounds', () => { + expect(ApiProxyService.Config({ coldBlankProbeMaxBytes: 0 })) + .toEqual({ sessionExportCompressionLevel: 6, coldBlankProbeMaxBytes: 0 }) + expect(ApiProxyService.Config({ coldBlankProbeMaxBytes: 2048 })) + .toEqual({ sessionExportCompressionLevel: 6, coldBlankProbeMaxBytes: 2048 }) + for (const value of [-1, 1.5]) { + expect(() => ApiProxyService.Config({ coldBlankProbeMaxBytes: value })).toThrow() + } + }) +}) + describe('session.export download endpoint', () => { it('streams a ZIP with the root artifact verbatim under its original filename', async () => { const api = await buildApi({ 'session-root': artifact('session-root') }) diff --git a/tsconfig.host.json b/tsconfig.host.json index a46534f0fe..459036247a 100644 --- a/tsconfig.host.json +++ b/tsconfig.host.json @@ -35,6 +35,7 @@ "apps/web/tests/replay-round-trip.e2e.ts", "apps/web/tests/hmr-live.e2e.ts", "apps/web/tests/seeded-history.e2e.ts", + "apps/web/tests/cold-blank-session.e2e.ts", "apps/web/tests/stats-paged-history.e2e.ts", "apps/web/tests/sidebar-scrollbar.e2e.ts", "apps/web/tests/conversation-column-overflow.e2e.ts", From b70a5497140e89cfd9cc1cb116cdc29bdb1b5599 Mon Sep 17 00:00:00 2001 From: creatixchu Date: Thu, 13 Aug 2026 14:10:42 +0800 Subject: [PATCH 03/24] fix(review): update smoke-real onboarding path and telemetry disclosure facts --- .../2026-08-10-telemetry-default-off.i18n.yaml | 4 ++-- .../feature/2026-08-10-telemetry-default-off.md | 2 +- .../feature/2026-08-10-telemetry-default-off.zh.md | 2 +- ...026-08-13-remove-first-run-beta-notice.i18n.yaml | 4 ++-- .../2026-08-13-remove-first-run-beta-notice.md | 2 +- .../2026-08-13-remove-first-run-beta-notice.zh.md | 2 +- apps/web/tests/smoke-real.e2e.ts | 13 +++---------- 7 files changed, 11 insertions(+), 18 deletions(-) diff --git a/.agents/notes/implemented/feature/2026-08-10-telemetry-default-off.i18n.yaml b/.agents/notes/implemented/feature/2026-08-10-telemetry-default-off.i18n.yaml index 7a3b82d3a6..434bd14018 100644 --- a/.agents/notes/implemented/feature/2026-08-10-telemetry-default-off.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-08-10-telemetry-default-off.i18n.yaml @@ -2,5 +2,5 @@ # 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 .agents/notes/implemented/feature/2026-08-10-telemetry-default-off.md -2026-08-10-telemetry-default-off.md: 1979597eb81ecc7413996b7eb4fbe0e270abc629 -2026-08-10-telemetry-default-off.zh.md: 25d675c69568a30ad754b301139b30ef0e8ffe3d +2026-08-10-telemetry-default-off.md: b70fdbc64074936ae910cba305a59111d6f9b572 +2026-08-10-telemetry-default-off.zh.md: abb5cafa9f877659dca1d53c0be9df9414799cf9 diff --git a/.agents/notes/implemented/feature/2026-08-10-telemetry-default-off.md b/.agents/notes/implemented/feature/2026-08-10-telemetry-default-off.md index 1979597eb8..b70fdbc640 100644 --- a/.agents/notes/implemented/feature/2026-08-10-telemetry-default-off.md +++ b/.agents/notes/implemented/feature/2026-08-10-telemetry-default-off.md @@ -14,7 +14,7 @@ Both feeds use `DSH_TELEMETRY_MODE` as their positive consent setting. Unset and The dsh-sdk launcher reads the same variable without parsing `cordis.yml` or booting Cordis. `FULL` permits reporting; `FEEDBACK_ONLY`, `DISABLED`, unset, and empty values deny it. Consent is frozen from the launching environment before the command runs, because `dsh-sdk start` loads a project `.env` and project code can mutate `process.env`: resolving afterwards would let a project grant reporting of its own configuration, which the [configuration source ownership decision](../architecture/2026-08-04-configuration-source-ownership.md) denies for the whole `DSH_*` namespace. An unsupported mode denies rather than throwing at that boundary, since telemetry may never change a command's result. This rule superseded the default-on launcher consent before the launcher and its proposal were deleted by the [SDK project toolchain removal](../simplification/2026-08-11-remove-sdk-project-toolchain.md). -The repository README states that Session Log upload is off by default, names `DSH_TELEMETRY_MODE=FEEDBACK_ONLY` and `DSH_TELEMETRY_MODE=FULL` as the two opt-in choices, and discloses that `FULL` also enables dsh-sdk command telemetry. The product itself presents no prompt about enabling telemetry since the [first-run beta notice removal](../simplification/2026-08-13-remove-first-run-beta-notice.md). +The [CLI reference README](../../../../apps/cli/reference/README.md) documents the deployment stance: Session Log upload is off by default, `DSH_TELEMETRY_MODE=FEEDBACK_ONLY` and `DSH_TELEMETRY_MODE=FULL` are the two opt-in choices, and explicitly enabled exports can contain complete session content. The product itself presents no prompt about enabling telemetry since the [first-run beta notice removal](../simplification/2026-08-13-remove-first-run-beta-notice.md). ## Alternatives considered diff --git a/.agents/notes/implemented/feature/2026-08-10-telemetry-default-off.zh.md b/.agents/notes/implemented/feature/2026-08-10-telemetry-default-off.zh.md index 25d675c695..abb5cafa9f 100644 --- a/.agents/notes/implemented/feature/2026-08-10-telemetry-default-off.zh.md +++ b/.agents/notes/implemented/feature/2026-08-10-telemetry-default-off.zh.md @@ -14,7 +14,7 @@ DeepSeek Harness 有两路出站遥测数据流。在内测阶段,共享基础 dsh-sdk 启动器读取同一变量,不解析 `cordis.yml`,也不启动 Cordis。`FULL` 允许上报;`FEEDBACK_ONLY`、`DISABLED`、未设置和空值都会拒绝。授权在命令执行前从启动环境冻结:`dsh-sdk start` 会加载项目 `.env`,项目代码也能修改 `process.env`,若在执行后解析,项目便能自行授权上报其自身配置,而[配置来源所有权决策](../architecture/2026-08-04-configuration-source-ownership.md)对整个 `DSH_*` 命名空间禁止这种行为。在该边界上,不受支持的模式按拒绝处理而非抛出,因为遥测不得改变命令结果。此规则在启动器及其提案被[SDK 项目工具链移除决策](../simplification/2026-08-11-remove-sdk-project-toolchain.md)删除之前,仅取代了启动器默认允许上报的规则。 -仓库 README 说明会话日志上传默认关闭,将 `DSH_TELEMETRY_MODE=FEEDBACK_ONLY` 和 `DSH_TELEMETRY_MODE=FULL` 列为两种显式启用选项,并披露 `FULL` 同时会启用 dsh-sdk 命令遥测。自[移除首次启动内测声明](../simplification/2026-08-13-remove-first-run-beta-notice.md)起,产品本身不提供任何关于开启遥测的提示。 +[CLI reference README](../../../../apps/cli/reference/README.md) 记录了这一部署口径:会话日志上传默认关闭,`DSH_TELEMETRY_MODE=FEEDBACK_ONLY` 和 `DSH_TELEMETRY_MODE=FULL` 是两种显式启用选项,显式开启后的导出可能包含完整会话内容。自[移除首次启动内测声明](../simplification/2026-08-13-remove-first-run-beta-notice.md)起,产品本身不提供任何关于开启遥测的提示。 ## 考虑过的替代方案 diff --git a/.agents/notes/implemented/simplification/2026-08-13-remove-first-run-beta-notice.i18n.yaml b/.agents/notes/implemented/simplification/2026-08-13-remove-first-run-beta-notice.i18n.yaml index a040b192f2..7a48a069c5 100644 --- a/.agents/notes/implemented/simplification/2026-08-13-remove-first-run-beta-notice.i18n.yaml +++ b/.agents/notes/implemented/simplification/2026-08-13-remove-first-run-beta-notice.i18n.yaml @@ -2,5 +2,5 @@ # 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 .agents/notes/implemented/simplification/2026-08-13-remove-first-run-beta-notice.md -2026-08-13-remove-first-run-beta-notice.md: e0876e5e96e04fed5e47adc0f83b1c7bfa4f2519 -2026-08-13-remove-first-run-beta-notice.zh.md: 8598fc28dda1ff5404e3c06fc8e2aaadca4cd471 +2026-08-13-remove-first-run-beta-notice.md: 69f47be445ebfa9f10e6ba1217e4674a98c747f1 +2026-08-13-remove-first-run-beta-notice.zh.md: 51c91918baaf1ae3db28434d7d86093fbd5ffb4e diff --git a/.agents/notes/implemented/simplification/2026-08-13-remove-first-run-beta-notice.md b/.agents/notes/implemented/simplification/2026-08-13-remove-first-run-beta-notice.md index e0876e5e96..69f47be445 100644 --- a/.agents/notes/implemented/simplification/2026-08-13-remove-first-run-beta-notice.md +++ b/.agents/notes/implemented/simplification/2026-08-13-remove-first-run-beta-notice.md @@ -10,7 +10,7 @@ Every GUI first launch opened with a full-viewport internal-test statement (内 ## Decision -The first-run notice is removed from the assembled product rather than reworded. `ui-settings-general` seats no `settings.onboarding` step; the notice component, its durable acknowledgement store, its copy owner, and its locale keys are deleted. The `settings.onboarding` coordinator and its takeover stage stay ([ordered onboarding](../feature/2026-07-30-versioned-gui-welcome-onboarding.md)), and the conditional DeepSeek credential step is the only shipped occupant. The Host half still registers the `ui-onboarding` settings namespace: its `welcomeNoticeVersion` field keeps acknowledgements already stored in `$DSH_HOME/settings.yaml` valid, and nothing reads or writes it. Telemetry opt-in remains an explicit deployment environment choice documented in the repository README; the product presents no prompt about enabling it. +The first-run notice is removed from the assembled product rather than reworded. `ui-settings-general` seats no `settings.onboarding` step; the notice component, its durable acknowledgement store, its copy owner, and its locale keys are deleted. The `settings.onboarding` coordinator and its takeover stage stay ([ordered onboarding](../feature/2026-07-30-versioned-gui-welcome-onboarding.md)), and the conditional DeepSeek credential step is the only shipped occupant. The Host half still registers the `ui-onboarding` settings namespace: its `welcomeNoticeVersion` field keeps acknowledgements already stored in `$DSH_HOME/settings.yaml` valid, and nothing reads or writes it. Telemetry opt-in remains an explicit deployment environment choice documented in the [CLI reference README](../../../../apps/cli/reference/README.md); the product presents no prompt about enabling it. ## Alternatives considered diff --git a/.agents/notes/implemented/simplification/2026-08-13-remove-first-run-beta-notice.zh.md b/.agents/notes/implemented/simplification/2026-08-13-remove-first-run-beta-notice.zh.md index 8598fc28dd..51c91918ba 100644 --- a/.agents/notes/implemented/simplification/2026-08-13-remove-first-run-beta-notice.zh.md +++ b/.agents/notes/implemented/simplification/2026-08-13-remove-first-run-beta-notice.zh.md @@ -10,7 +10,7 @@ GUI 每次首启都会先显示占满视口的内测声明:内部测试的定 ## 决策 -首启声明从组装后的产品中整体移除,而不是改写。`ui-settings-general` 不再注册任何 `settings.onboarding` 步骤;声明组件、其持久化确认 store、文案所有者文件和 locale 键全部删除。`settings.onboarding` 协调器及其接管式展示阶段保留([有序引导](../feature/2026-07-30-versioned-gui-welcome-onboarding.md)),按条件显示的 DeepSeek 凭据步骤是当前唯一的注册方。宿主端仍注册 `ui-onboarding` 设置 namespace:其中的 `welcomeNoticeVersion` 字段让 `$DSH_HOME/settings.yaml` 中已写入的确认记录保持有效,没有任何代码读取或写入它。遥测的开启仍是显式的部署环境变量选择,记录在仓库 README 中;产品界面不出现任何关于开启遥测的提示。 +首启声明从组装后的产品中整体移除,而不是改写。`ui-settings-general` 不再注册任何 `settings.onboarding` 步骤;声明组件、其持久化确认 store、文案所有者文件和 locale 键全部删除。`settings.onboarding` 协调器及其接管式展示阶段保留([有序引导](../feature/2026-07-30-versioned-gui-welcome-onboarding.md)),按条件显示的 DeepSeek 凭据步骤是当前唯一的注册方。宿主端仍注册 `ui-onboarding` 设置 namespace:其中的 `welcomeNoticeVersion` 字段让 `$DSH_HOME/settings.yaml` 中已写入的确认记录保持有效,没有任何代码读取或写入它。遥测的开启仍是显式的部署环境变量选择,记录在 [CLI reference README](../../../../apps/cli/reference/README.md) 中;产品界面不出现任何关于开启遥测的提示。 ## 曾考虑的替代方案 diff --git a/apps/web/tests/smoke-real.e2e.ts b/apps/web/tests/smoke-real.e2e.ts index 2784b66d07..5d479e9552 100644 --- a/apps/web/tests/smoke-real.e2e.ts +++ b/apps/web/tests/smoke-real.e2e.ts @@ -533,16 +533,9 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY || notReady.length > 0)('web smoke it('empty-state first send completes a real model round', async () => { onTestFailed(() => saveFailureShot(page, 'w5-first-round')) - // This scenario spawns its own server against a fresh $DSH_HOME, so the - // first-run welcome notice is unacknowledged and its overlay owns pointer - // events (the shared scaffold acknowledges it before boot instead). The - // notice is anchored structurally, not by its copy: this spec sits in the - // client TypeScript program, which does not reference the package that - // owns the strings. - const welcome = page.locator('[class*="onboardingOverlay"]') - await welcome.waitFor({ timeout: 15_000 }) - await welcome.getByRole('button').click() - await welcome.waitFor({ state: 'detached', timeout: 15_000 }) + // This scenario spawns its own server against a fresh $DSH_HOME with the + // DeepSeek credential inherited from the environment, so no onboarding + // step mounts and the page is immediately interactive. // Fresh world: connect a Workspace so the composer starts live. await connectFreshWorkspace(page, sessionsDir) const input = page.locator('textarea').first() From c4226840f4b2d493f3c812b3ade21d902c571dc6 Mon Sep 17 00:00:00 2001 From: _Kerman Date: Thu, 13 Aug 2026 14:11:21 +0800 Subject: [PATCH 04/24] test(todo): isolate projection assertions --- packages/todo/tool-todo/tests/projection.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/todo/tool-todo/tests/projection.spec.ts b/packages/todo/tool-todo/tests/projection.spec.ts index 03bb3ce4da..ff374e641e 100644 --- a/packages/todo/tool-todo/tests/projection.spec.ts +++ b/packages/todo/tool-todo/tests/projection.spec.ts @@ -70,7 +70,7 @@ describe('todos projection provider', () => { const bench = await harness(true) seedMessage(bench.session) const projections = await bench.tailProjections() - expect(projections?.values).toEqual({ todos: null }) + expect(projections?.values.todos).toBeNull() expect(projections?.asOfSeq).toBe(bench.session.seq - 1) }) @@ -117,7 +117,7 @@ describe('todos projection provider', () => { const bench = await harness(false) seedMessage(bench.session) const fiber = await bench.ctx.plugin(ToolTodo, { allowParallelInProgress: true }) - expect((await bench.tailProjections())?.values).toEqual({ todos: null }) + expect((await bench.tailProjections())?.values.todos).toBeNull() await fiber.dispose() expect('todos' in ((await bench.tailProjections())?.values ?? {})).toBe(false) }) From 62080d49c265d3a7a5bf8efcae524a29e3320b8e Mon Sep 17 00:00:00 2001 From: Turtle Date: Thu, 13 Aug 2026 13:43:43 +0800 Subject: [PATCH 05/24] docs: fix release smoke test failures --- ...-13-published-document-fragments.i18n.yaml | 6 + ...2026-08-13-published-document-fragments.md | 27 +++ ...6-08-13-published-document-fragments.zh.md | 27 +++ docs/architecture.i18n.yaml | 2 +- docs/architecture.zh.md | 4 + docs/config-catalog.i18n.yaml | 4 +- docs/config-catalog.md | 210 ++++++++++++++++++ docs/config-catalog.zh.md | 210 ++++++++++++++++++ docs/cookbook/adding-a-tool.i18n.yaml | 2 +- docs/cookbook/adding-a-tool.zh.md | 2 + docs/cookbook/extension-cookbook.i18n.yaml | 2 +- docs/cookbook/extension-cookbook.zh.md | 2 + docs/cordis-primer.i18n.yaml | 2 +- docs/cordis-primer.zh.md | 2 + docs/cordis-tutorial/03-services.i18n.yaml | 4 +- docs/cordis-tutorial/03-services.md | 2 +- docs/cordis-tutorial/03-services.zh.md | 2 +- .../06-composition-and-hmr.i18n.yaml | 4 +- .../cordis-tutorial/06-composition-and-hmr.md | 2 +- .../06-composition-and-hmr.zh.md | 2 +- docs/cordis-tutorial/index.i18n.yaml | 2 +- docs/cordis-tutorial/index.zh.md | 2 + docs/persistence-catalog.i18n.yaml | 4 +- docs/persistence-catalog.md | 88 ++++++++ docs/persistence-catalog.zh.md | 88 ++++++++ docs/subsystems/core.i18n.yaml | 4 +- docs/subsystems/core.md | 2 + docs/subsystems/core.zh.md | 2 + docs/subsystems/llm-streaming.i18n.yaml | 2 +- docs/subsystems/llm-streaming.md | 2 + docs/subsystems/persistence.i18n.yaml | 4 +- docs/subsystems/persistence.md | 2 + docs/subsystems/persistence.zh.md | 2 + docs/subsystems/session.i18n.yaml | 2 +- docs/subsystems/session.md | 2 + docs/tool-catalog.i18n.yaml | 4 +- docs/tool-catalog.md | 48 ++++ docs/tool-catalog.zh.md | 48 ++++ docs/user/develop/basic/index.i18n.yaml | 4 +- docs/user/develop/basic/index.md | 6 +- docs/user/develop/basic/index.zh.md | 6 +- docs/user/develop/basic/publish.i18n.yaml | 4 +- docs/user/develop/basic/publish.md | 45 ++-- docs/user/develop/basic/publish.zh.md | 45 ++-- docs/user/develop/framework/events.i18n.yaml | 4 +- docs/user/develop/framework/events.md | 6 +- docs/user/develop/framework/events.zh.md | 6 +- docs/user/develop/framework/service.i18n.yaml | 4 +- docs/user/develop/framework/service.md | 4 +- docs/user/develop/framework/service.zh.md | 6 +- docs/user/develop/practice/index.i18n.yaml | 4 +- docs/user/develop/practice/index.md | 2 +- docs/user/develop/practice/index.zh.md | 2 +- .../develop/practice/llm-adapter.i18n.yaml | 4 +- docs/user/develop/practice/llm-adapter.md | 20 +- docs/user/develop/practice/llm-adapter.zh.md | 20 +- docs/user/guide/index.i18n.yaml | 4 +- docs/user/guide/index.md | 8 +- docs/user/guide/index.zh.md | 8 +- package.json | 7 +- scripts/gen-config-catalog.ts | 10 +- scripts/gen-persistence-catalog.ts | 4 +- scripts/gen-tool-catalog.ts | 3 +- scripts/run-gates.ts | 2 +- scripts/verify-doc-site-fragments.spec.ts | 67 ++++++ scripts/verify-doc-site-fragments.ts | 143 ++++++++++++ 66 files changed, 1150 insertions(+), 124 deletions(-) create mode 100644 .agents/notes/implemented/process/2026-08-13-published-document-fragments.i18n.yaml create mode 100644 .agents/notes/implemented/process/2026-08-13-published-document-fragments.md create mode 100644 .agents/notes/implemented/process/2026-08-13-published-document-fragments.zh.md create mode 100644 scripts/verify-doc-site-fragments.spec.ts create mode 100644 scripts/verify-doc-site-fragments.ts diff --git a/.agents/notes/implemented/process/2026-08-13-published-document-fragments.i18n.yaml b/.agents/notes/implemented/process/2026-08-13-published-document-fragments.i18n.yaml new file mode 100644 index 0000000000..1051ae0b32 --- /dev/null +++ b/.agents/notes/implemented/process/2026-08-13-published-document-fragments.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 .agents/notes/implemented/process/2026-08-13-published-document-fragments.md +2026-08-13-published-document-fragments.md: 9837d4ec1335f8da9910b3d2c2167f4c6d3a1aab +2026-08-13-published-document-fragments.zh.md: f4c68ae01082ab53a251c9951906538f0f4ae770 diff --git a/.agents/notes/implemented/process/2026-08-13-published-document-fragments.md b/.agents/notes/implemented/process/2026-08-13-published-document-fragments.md new file mode 100644 index 0000000000..9837d4ec13 --- /dev/null +++ b/.agents/notes/implemented/process/2026-08-13-published-document-fragments.md @@ -0,0 +1,27 @@ +# Agent Note: Validate published document fragments + +Status: implemented + +English | [中文](2026-08-13-published-document-fragments.zh.md) + +## Problem + +`verify-md-links` validates fragments with GitHub's Markdown heading ids, while the documentation website renders headings with VitePress. Punctuation-heavy headings and translated headings can therefore pass source validation but produce links to ids absent from the published HTML. A successful VitePress build validates target pages, not fragment ids. + +## Decision + +`docs:build` and its MPA variant run `verify-doc-site-fragments` after VitePress emits `website/.dist`. The verifier parses every emitted HTML page, resolves each internal fragment link against VitePress clean URLs, and fails when the output is absent or either the target page or requested id is missing. Unit tests cover absent output, clean URLs, `.html` aliases, same-page links, encoded ids, missing ids, missing routes, and external-link exclusion. + +Generated config, tool, and persistence catalogs emit explicit GitHub-compatible id aliases before punctuation-heavy headings. Authored translated pages add explicit language-neutral aliases when their localized VitePress heading id differs from the shared fragment used by the bilingual pair. Source Markdown validation remains independent and continues to reject links that do not resolve under repository rendering. + +## Alternatives considered + +**Use locale-specific fragments.** Bilingual pairs intentionally preserve identical link targets. Locale-specific fragments would make the two sources disagree and would require every link producer to know the target locale's translated heading. + +**Rely on VitePress heading ids.** Those ids depend on rendered punctuation and localized heading text. They do not preserve the GitHub ids already used by repository links and generated references. + +**Check source Markdown only.** This leaves the published artifact unverified and cannot detect differences between the GitHub and VitePress slug algorithms. + +## Consequences + +Every production documentation build reads its emitted HTML once, adding a bounded post-build check to the existing site build. Cross-page fragment links now require an id that survives publication. Explicit aliases become part of the published reference and let headings change language or punctuation without invalidating established fragments. diff --git a/.agents/notes/implemented/process/2026-08-13-published-document-fragments.zh.md b/.agents/notes/implemented/process/2026-08-13-published-document-fragments.zh.md new file mode 100644 index 0000000000..f4c68ae010 --- /dev/null +++ b/.agents/notes/implemented/process/2026-08-13-published-document-fragments.zh.md @@ -0,0 +1,27 @@ +# Agent Note: 校验已发布文档的 fragment + +Status: implemented + +[English](2026-08-13-published-document-fragments.md) | 中文 + +## Problem + +`verify-md-links` 使用 GitHub 的 Markdown 标题 id 校验 fragment,而文档网站使用 VitePress 渲染标题。包含较多标点的标题与翻译后的标题可能通过源码校验,却在已发布 HTML 中没有对应 id。VitePress 构建成功只会校验目标页面,不会校验 fragment id。 + +## Decision + +`docs:build` 及其 MPA 变体会在 VitePress 生成 `website/.dist` 后运行 `verify-doc-site-fragments`。该校验器解析每个生成的 HTML 页面,按照 VitePress clean URL 解析每个内部 fragment 链接,并在构建产物不存在、目标页面不存在或请求的 id 缺失时失败。单元测试覆盖缺失构建产物、clean URL、`.html` 别名、同页链接、编码 id、缺失 id、缺失路由和外部链接排除。 + +生成的配置、工具和持久化目录会在包含较多标点的标题前生成与 GitHub 兼容的显式 id 别名。如果翻译页面的 VitePress 本地化标题 id 与双语对侧文件共享的 fragment 不同,编写者会添加语言无关的显式别名。源码 Markdown 校验保持独立,仍会拒绝在仓库渲染规则下无法解析的链接。 + +## Alternatives considered + +**使用各语言专属的 fragment。** 双语对侧文件会刻意保留相同的链接目标。语言专属 fragment 会使两侧源码不一致,还会要求每个链接生成方都了解目标语言翻译后的标题。 + +**依赖 VitePress 标题 id。** 这些 id 取决于渲染后的标点与本地化标题文本,无法保留仓库链接和生成引用已经使用的 GitHub id。 + +**只检查 Markdown 源码。** 这种做法不会校验发布产物,也无法发现 GitHub 与 VitePress slug 算法之间的差异。 + +## Consequences + +每次生产文档构建都会读取一次生成的 HTML,在现有网站构建后增加一个有界检查。跨页面 fragment 链接必须指向发布后仍存在的 id。显式别名成为已发布参考的一部分,使标题更换语言或标点后仍能保留既有 fragment。 diff --git a/docs/architecture.i18n.yaml b/docs/architecture.i18n.yaml index f077b66226..d1556bb542 100644 --- a/docs/architecture.i18n.yaml +++ b/docs/architecture.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/architecture.md architecture.md: 77000ce9d4608d440e1d903eb80a42f2ed6435ef -architecture.zh.md: 268724d52c82e31c4fc2b51db59823f3720f9b91 +architecture.zh.md: f2f5310f665b86b86587307e7ce31c5841b96317 diff --git a/docs/architecture.zh.md b/docs/architecture.zh.md index 268724d52c..f2f5310f66 100644 --- a/docs/architecture.zh.md +++ b/docs/architecture.zh.md @@ -50,6 +50,8 @@ dsh --profile web --dump-config | [`core/scope`](subsystems/scope.md) | 按 agent 划分作用域的注册原语 | 库,无 ctx 键 | | [`llm/llm`](subsystems/llm-streaming.md) | 消息与流式词汇表,以及适配器 seam | `ctx.llm` | + + ## 事件 事件就是扩展点,而选对事件域是大多数改动的第一个决定。 @@ -60,6 +62,8 @@ dsh --profile web --dump-config [事件映射](event-producer-consumer.md)列出每个事件的生产方与消费方。 + + ## 轮次流程 一个**步骤**是一次模型请求加上它调用的工具。一个**轮次**包含零个或多个步骤:它在领取首条输入之前打开,并在不再欠下任何工作时关闭。 diff --git a/docs/config-catalog.i18n.yaml b/docs/config-catalog.i18n.yaml index b1bec33ca0..b3aa609638 100644 --- a/docs/config-catalog.i18n.yaml +++ b/docs/config-catalog.i18n.yaml @@ -2,5 +2,5 @@ # 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 docs/config-catalog.md -config-catalog.md: 19bfa6d1fb847de4a7207f42dabd67d43f288361 -config-catalog.zh.md: fda208e8fcd2ff6dd697efed84a4073ecbd5a912 +config-catalog.md: dfa03e7109fbfeee425469e457186a9ad7e3cb28 +config-catalog.zh.md: dc20cd90e5839be67a086b8e64a814f1013ce3af diff --git a/docs/config-catalog.md b/docs/config-catalog.md index 19bfa6d1fb..dfa03e7109 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -9,6 +9,8 @@ This file is GENERATED from source (`scripts/gen-config-catalog.ts`) and verifie A `Requires:` line lists the service keys the plugin `inject`s: its `cordis.yml` tree must also load providers for those services. Scope is the harness tier (`packages/`); the vendored cordis plugins a config tree may also load (`hmr`, the console logger, …) are pinned upstream source ([vendoring policy](../vendor/README.md)) and not catalogued here. + + ## `@deepseek-ai/dsh-acp` Requires: `agents` @@ -29,6 +31,8 @@ Depends on: `Stream` (`@agentclientprotocol/sdk`) Source: [`packages/acp/acp/src/index.ts:70`](../packages/acp/acp/src/index.ts) + + ## `@deepseek-ai/dsh-acp-demo` ```ts config-catalog @@ -82,6 +86,8 @@ Depends on: [`agentCore`](../packages/examples/agent-spine-demo/src/index.ts) · Source: [`packages/examples/acp-demo/src/index.ts:39`](../packages/examples/acp-demo/src/index.ts) + + ## `@deepseek-ai/dsh-agent-default-model` ```ts config-catalog @@ -96,6 +102,8 @@ export interface Config { Source: [`packages/core/agent-default-model/src/index.ts:41`](../packages/core/agent-default-model/src/index.ts) + + ## `@deepseek-ai/dsh-agent-instructions` ```ts config-catalog @@ -124,6 +132,8 @@ export interface Config { Source: [`packages/context/agent-instructions/src/config.ts:18`](../packages/context/agent-instructions/src/config.ts) + + ## `@deepseek-ai/dsh-agent-loop` Requires: `agents` · `sessions` · `llm` · `tools` · `systemPrompt` @@ -154,6 +164,8 @@ Depends on: [`AgentOptions`](subsystems/core.md) · [`SessionId`](subsystems/cor Source: [`packages/core/agent-loop/src/index.ts:255`](../packages/core/agent-loop/src/index.ts) + + ## `@deepseek-ai/dsh-agent-presets` Requires: `loader` @@ -190,6 +202,8 @@ export type PresetTrust = 'system' | 'user' Source: [`packages/preset/agent-presets/src/preset.ts:52`](../packages/preset/agent-presets/src/preset.ts) + + ## `@deepseek-ai/dsh-agent-spine-demo` ```ts config-catalog @@ -280,6 +294,8 @@ Depends on: [`AgentLoopConfig`](#deepseek-aidsh-agent-loop) · [`GoalDomainConfi Source: [`packages/examples/agent-spine-demo/src/index.ts:92`](../packages/examples/agent-spine-demo/src/index.ts) + + ## `@deepseek-ai/dsh-agent-tool-presentation` Requires: `tools` @@ -302,6 +318,8 @@ Depends on: [`ToolPresentationMode`](subsystems/tools.md) Source: [`packages/core/agent-tool-presentation/src/index.ts:38`](../packages/core/agent-tool-presentation/src/index.ts) + + ## `@deepseek-ai/dsh-attachment-local` ```ts config-catalog @@ -322,6 +340,8 @@ export interface Config { Source: [`packages/attachment/attachment-local/src/index.ts:24`](../packages/attachment/attachment-local/src/index.ts) + + ## `@deepseek-ai/dsh-bash-local` Requires: `subprocess` @@ -346,6 +366,8 @@ export interface Config { Source: [`packages/shell/bash-local/src/index.ts:41`](../packages/shell/bash-local/src/index.ts) + + ## `@deepseek-ai/dsh-bash-sandbox` Requires: `subprocess` · `sandbox` · `sandboxPolicy` @@ -365,6 +387,8 @@ Depends on: [`LocalConfig`](#deepseek-aidsh-bash-local) Source: [`packages/shell/bash-sandbox/src/index.ts:35`](../packages/shell/bash-sandbox/src/index.ts) + + ## `@deepseek-ai/dsh-client-connection` Requires: `webServer` @@ -388,6 +412,8 @@ export interface ConnectionConfig { Source: [`packages/client/connection/src/index.ts:50`](../packages/client/connection/src/index.ts) + + ## `@deepseek-ai/dsh-client-hmr` Requires: `clientModules` · `webServer` @@ -402,6 +428,8 @@ export interface Config { Source: [`packages/client/hmr/src/index.ts:31`](../packages/client/hmr/src/index.ts) + + ## `@deepseek-ai/dsh-code-runtime-worker-thread` ```ts config-catalog @@ -437,6 +465,8 @@ export interface Config { Source: [`packages/code-runtime/code-runtime-worker-thread/src/index.ts:25`](../packages/code-runtime/code-runtime-worker-thread/src/index.ts) + + ## `@deepseek-ai/dsh-compaction-basic` Requires: `llm` · `tokenMeter` · `sessions` @@ -481,6 +511,8 @@ export interface ModelCompactPolicyConfig extends CompactionPolicyConfig { Source: [`packages/compaction/compaction-basic/src/types.ts:38`](../packages/compaction/compaction-basic/src/types.ts) + + ## `@deepseek-ai/dsh-compaction-tool-result-pruner` Requires: `tokenMeter` @@ -499,6 +531,8 @@ export interface ToolResultPruneConfig { Source: [`packages/compaction/compaction-tool-result-pruner/src/types.ts:4`](../packages/compaction/compaction-tool-result-pruner/src/types.ts) + + ## `@deepseek-ai/dsh-cordis-host-runner` Requires: `tools` @@ -513,6 +547,8 @@ export interface Config { Source: [`packages/extensions/cordis-host-runner/src/index.ts:88`](../packages/extensions/cordis-host-runner/src/index.ts) + + ## `@deepseek-ai/dsh-credentials-local` ```ts config-catalog @@ -531,6 +567,8 @@ export interface Config { Source: [`packages/credentials/credentials-local/src/index.ts:55`](../packages/credentials/credentials-local/src/index.ts) + + ## `@deepseek-ai/dsh-e2b` ```ts config-catalog @@ -547,6 +585,8 @@ export interface Config { Source: [`packages/e2b/e2b/src/index.ts:43`](../packages/e2b/e2b/src/index.ts) + + ## `@deepseek-ai/dsh-fs-local` ```ts config-catalog @@ -564,6 +604,8 @@ export interface Config { Source: [`packages/fs/fs-local/src/index.ts:41`](../packages/fs/fs-local/src/index.ts) + + ## `@deepseek-ai/dsh-fs-sandbox` Requires: `sandboxPolicy` @@ -582,6 +624,8 @@ Depends on: [`LocalConfig`](#deepseek-aidsh-fs-local) Source: [`packages/fs/fs-sandbox/src/index.ts:49`](../packages/fs/fs-sandbox/src/index.ts) + + ## `@deepseek-ai/dsh-goal` Requires: `agents` @@ -596,6 +640,8 @@ export interface Config { Source: [`packages/goal/goal/src/index.ts:116`](../packages/goal/goal/src/index.ts) + + ## `@deepseek-ai/dsh-headless` Requires: `agentDefaultModel` · `agents` · `sessions` @@ -610,6 +656,8 @@ export interface Config { Source: [`packages/bundle/headless/src/index.ts:31`](../packages/bundle/headless/src/index.ts) + + ## `@deepseek-ai/dsh-hooks-claude-code` Requires: `shell` @@ -646,6 +694,8 @@ export interface Config { Source: [`packages/hooks/hooks-claude-code/src/index.ts:45`](../packages/hooks/hooks-claude-code/src/index.ts) + + ## `@deepseek-ai/dsh-hooks-codex` Requires: `shell` @@ -671,6 +721,8 @@ export interface Config { Source: [`packages/hooks/hooks-codex/src/index.ts:44`](../packages/hooks/hooks-codex/src/index.ts) + + ## `@deepseek-ai/dsh-host-apiproxy` Requires: `agentDefaultModel` · `agents` · `attachments` · `directoryPicker` · `llm` · `sessions` · `subagents` · `sessionQuery` · `tools` · `userQuestions` · `workspaceRegistry` @@ -697,6 +749,8 @@ export interface Config { Source: [`packages/host/apiproxy/src/index.ts:41`](../packages/host/apiproxy/src/index.ts) + + ## `@deepseek-ai/dsh-host-directory-picker-browse` ```ts config-catalog @@ -709,6 +763,8 @@ export interface Config { Source: [`packages/host/directory-picker-browse/src/index.ts:181`](../packages/host/directory-picker-browse/src/index.ts) + + ## `@deepseek-ai/dsh-host-frontend-static` Requires: `webServer` @@ -723,6 +779,8 @@ export interface Config { Source: [`packages/host/frontend-static/src/index.ts:28`](../packages/host/frontend-static/src/index.ts) + + ## `@deepseek-ai/dsh-host-webserver` ```ts config-catalog @@ -737,6 +795,8 @@ export interface Config { Source: [`packages/host/webserver/src/index.ts:45`](../packages/host/webserver/src/index.ts) + + ## `@deepseek-ai/dsh-invariants` ```ts config-catalog @@ -753,6 +813,8 @@ export interface Config { Source: [`packages/runtime-diagnostics/invariants/src/index.ts:15`](../packages/runtime-diagnostics/invariants/src/index.ts) + + ## `@deepseek-ai/dsh-jobs-local` ```ts config-catalog @@ -768,6 +830,8 @@ export interface Config { Source: [`packages/jobs/jobs-local/src/index.ts:31`](../packages/jobs/jobs-local/src/index.ts) + + ## `@deepseek-ai/dsh-llm-deepseek` Requires: `llm` @@ -821,6 +885,8 @@ Depends on: [`RetryPolicyConfig`](../packages/llm/llm/src/index.ts) Source: [`packages/llm/llm-deepseek/src/index.ts:62`](../packages/llm/llm-deepseek/src/index.ts) + + ## `@deepseek-ai/dsh-llm-pi-ai` Requires: `llm` @@ -1009,6 +1075,8 @@ Depends on: `Api` (`@earendil-works/pi-ai`) · `CacheRetention` (`@earendil-work Source: [`packages/llm/llm-pi-ai/src/config.ts:172`](../packages/llm/llm-pi-ai/src/config.ts) + + ## `@deepseek-ai/dsh-llm-replay` Requires: `llm` @@ -1075,6 +1143,8 @@ Depends on: [`ModelModality`](../packages/llm/llm/src/index.ts) · [`RetryPolicy Source: [`packages/test-support/llm-replay/src/index.ts:776`](../packages/test-support/llm-replay/src/index.ts) + + ## `@deepseek-ai/dsh-llm-retry` Requires: `agents` @@ -1086,6 +1156,8 @@ export type Config = Readonly> Source: [`packages/llm/llm-retry/src/index.ts:24`](../packages/llm/llm-retry/src/index.ts) + + ## `@deepseek-ai/dsh-lsp-stdio` Requires: `fs` · `lsp` · `subprocess` @@ -1126,6 +1198,8 @@ export interface LspLocalServerConfig { Source: [`packages/lsp/lsp-stdio/src/index.ts:82`](../packages/lsp/lsp-stdio/src/index.ts) + + ## `@deepseek-ai/dsh-mcp-client` Requires: `tools` @@ -1197,6 +1271,8 @@ export interface ReconnectConfig { Source: [`packages/mcp/mcp-client/src/index.ts:98`](../packages/mcp/mcp-client/src/index.ts) + + ## `@deepseek-ai/dsh-message-feedback` Requires: `storageDomain` · `sessionPersistence` · `sessions` @@ -1211,6 +1287,8 @@ export interface Config { Source: [`packages/feedback/message-feedback/src/index.ts:49`](../packages/feedback/message-feedback/src/index.ts) + + ## `@deepseek-ai/dsh-permission-presets` Requires: `shell` · `approval` · `sessions` @@ -1248,6 +1326,8 @@ Depends on: [`ApprovalPolicy`](subsystems/approval.md) · [`SandboxMode`](subsys Source: [`packages/interaction/permission-presets/src/index.ts:140`](../packages/interaction/permission-presets/src/index.ts) + + ## `@deepseek-ai/dsh-persona` Requires: `systemPrompt` @@ -1270,6 +1350,8 @@ export interface Config { Source: [`packages/preset/persona/src/index.ts:34`](../packages/preset/persona/src/index.ts) + + ## `@deepseek-ai/dsh-plan-mode` Requires: `tools` · `systemPrompt` @@ -1284,6 +1366,8 @@ export interface PlanModeConfig { Source: [`packages/plan/plan-mode/src/index.ts:70`](../packages/plan/plan-mode/src/index.ts) + + ## `@deepseek-ai/dsh-pwsh-local` Requires: `subprocess` @@ -1315,6 +1399,8 @@ export interface Config { Source: [`packages/shell/pwsh-local/src/index.ts:58`](../packages/shell/pwsh-local/src/index.ts) + + ## `@deepseek-ai/dsh-pwsh-sandbox` Requires: `subprocess` · `sandbox` · `sandboxPolicy` @@ -1335,6 +1421,8 @@ Depends on: [`LocalConfig`](#deepseek-aidsh-pwsh-local) Source: [`packages/shell/pwsh-sandbox/src/index.ts:40`](../packages/shell/pwsh-sandbox/src/index.ts) + + ## `@deepseek-ai/dsh-repeat-tool-reminder` ```ts config-catalog @@ -1367,6 +1455,8 @@ export interface Config { Source: [`packages/guard/repeat-tool-reminder/src/index.ts:28`](../packages/guard/repeat-tool-reminder/src/index.ts) + + ## `@deepseek-ai/dsh-sandbox-local` ```ts config-catalog @@ -1397,6 +1487,8 @@ export interface Config { Source: [`packages/sandbox/sandbox-local/src/index.ts:44`](../packages/sandbox/sandbox-local/src/index.ts) + + ## `@deepseek-ai/dsh-sandbox-policy` ```ts config-catalog @@ -1422,6 +1514,8 @@ Depends on: [`SandboxMode`](subsystems/sandbox.md) Source: [`packages/sandbox/sandbox-policy/src/index.ts:67`](../packages/sandbox/sandbox-policy/src/index.ts) + + ## `@deepseek-ai/dsh-sdk-jsonrpc-server` Requires: `agents` @@ -1444,6 +1538,8 @@ Depends on: `Readable` (`node:stream`) · `Writable` (`node:stream`) Source: [`packages/sdk/server/src/index.ts:25`](../packages/sdk/server/src/index.ts) + + ## `@deepseek-ai/dsh-session-persistence-jsonl` Requires: `sessions` @@ -1481,6 +1577,8 @@ export type JsonlCompression = 'zstd' | 'none' Source: [`packages/session/session-persistence-jsonl/src/index.ts:60`](../packages/session/session-persistence-jsonl/src/index.ts) + + ## `@deepseek-ai/dsh-session-persistence-sqlite` Requires: `sessions` @@ -1524,6 +1622,8 @@ export type JournalMode = 'wal' | 'delete' | 'truncate' | 'persist' Source: [`packages/session/session-persistence-sqlite/src/index.ts:70`](../packages/session/session-persistence-sqlite/src/index.ts) + + ## `@deepseek-ai/dsh-session-projection-cache` Requires: `storageDomain` · `sessionProjections` · `sessionPersistence` · `sessions` @@ -1545,6 +1645,8 @@ export interface Config { Source: [`packages/session/session-projection-cache/src/index.ts:42`](../packages/session/session-projection-cache/src/index.ts) + + ## `@deepseek-ai/dsh-session-query-sqlite` Requires: `sessions` @@ -1589,6 +1691,8 @@ Depends on: [`SessionQueryConfig`](../packages/session-query/session-query/src/i Source: [`packages/session-query/session-query-sqlite/src/index.ts:89`](../packages/session-query/session-query-sqlite/src/index.ts) + + ## `@deepseek-ai/dsh-session-reference` Requires: `sessionQuery` @@ -1607,6 +1711,8 @@ export interface Config { Source: [`packages/context/session-reference/src/config.ts:11`](../packages/context/session-reference/src/config.ts) + + ## `@deepseek-ai/dsh-session-telemetry-otel` Requires: `sessions` @@ -1651,6 +1757,8 @@ Depends on: `BatchLogRecordProcessorOptions` (`@opentelemetry/sdk-logs`) · `OTL Source: [`packages/session/session-telemetry-otel/src/index.ts:91`](../packages/session/session-telemetry-otel/src/index.ts) + + ## `@deepseek-ai/dsh-session-title` Requires: `sessions` @@ -1669,6 +1777,8 @@ export interface Config { Source: [`packages/session/session-title/src/index.ts:79`](../packages/session/session-title/src/index.ts) + + ## `@deepseek-ai/dsh-session-title-all-prompts-llm` Requires: `sessionTitle` · `llm` · `sessions` @@ -1682,6 +1792,8 @@ Depends on: [`SessionTitleLlmConfig`](../packages/session/session-title-llm/src/ Source: [`packages/session/session-title-all-prompts-llm/src/index.ts:15`](../packages/session/session-title-all-prompts-llm/src/index.ts) + + ## `@deepseek-ai/dsh-session-title-first-prompt-llm` Requires: `sessionTitle` · `llm` · `sessions` @@ -1695,6 +1807,8 @@ Depends on: [`SessionTitleLlmConfig`](../packages/session/session-title-llm/src/ Source: [`packages/session/session-title-first-prompt-llm/src/index.ts:15`](../packages/session/session-title-first-prompt-llm/src/index.ts) + + ## `@deepseek-ai/dsh-settings-file` ```ts config-catalog @@ -1713,6 +1827,8 @@ export interface Config { Source: [`packages/settings/settings-file/src/index.ts:21`](../packages/settings/settings-file/src/index.ts) + + ## `@deepseek-ai/dsh-shell-env` ```ts config-catalog @@ -1725,6 +1841,8 @@ export interface Config { Source: [`packages/shell/shell-env/src/index.ts:29`](../packages/shell/shell-env/src/index.ts) + + ## `@deepseek-ai/dsh-skill` ```ts config-catalog @@ -1737,6 +1855,8 @@ export interface Config { Source: [`packages/skill/skill/src/index.ts:279`](../packages/skill/skill/src/index.ts) + + ## `@deepseek-ai/dsh-skill-filesystem` Requires: `skills` @@ -1773,6 +1893,8 @@ export interface Config { Source: [`packages/skill/skill-filesystem/src/index.ts:49`](../packages/skill/skill-filesystem/src/index.ts) + + ## `@deepseek-ai/dsh-spill-local` ```ts config-catalog @@ -1789,6 +1911,8 @@ export interface Config { Source: [`packages/spill/spill-local/src/index.ts:22`](../packages/spill/spill-local/src/index.ts) + + ## `@deepseek-ai/dsh-spill-policy` Requires: `tools` @@ -1807,6 +1931,8 @@ export interface Config { Source: [`packages/spill/spill-policy/src/index.ts:60`](../packages/spill/spill-policy/src/index.ts) + + ## `@deepseek-ai/dsh-storage-domain` Requires: `storage` @@ -1828,6 +1954,8 @@ export interface Config { Source: [`packages/storage/storage-domain/src/index.ts:52`](../packages/storage/storage-domain/src/index.ts) + + ## `@deepseek-ai/dsh-storage-json` Requires: `storage` @@ -1847,6 +1975,8 @@ export interface Config { Source: [`packages/storage/storage-json/src/index.ts:27`](../packages/storage/storage-json/src/index.ts) + + ## `@deepseek-ai/dsh-storage-sqlite` Requires: `storage` @@ -1885,6 +2015,8 @@ export type JournalMode = 'wal' | 'delete' | 'truncate' | 'persist' Source: [`packages/storage/storage-sqlite/src/index.ts:24`](../packages/storage/storage-sqlite/src/index.ts) + + ## `@deepseek-ai/dsh-subagent-acp` Requires: `subagents` · `subprocess` @@ -1936,6 +2068,8 @@ export type PermissionPolicy = 'allow' | 'reject' Source: [`packages/subagent/subagent-acp/src/index.ts:27`](../packages/subagent/subagent-acp/src/index.ts) + + ## `@deepseek-ai/dsh-subagent-claude-code` Requires: `subagents` · `subprocess` @@ -1955,6 +2089,8 @@ export interface Config { Source: [`packages/subagent/subagent-claude-code/src/index.ts:32`](../packages/subagent/subagent-claude-code/src/index.ts) + + ## `@deepseek-ai/dsh-subagent-codex` Requires: `subagents` · `subprocess` @@ -1974,6 +2110,8 @@ export interface Config { Source: [`packages/subagent/subagent-codex/src/index.ts:30`](../packages/subagent/subagent-codex/src/index.ts) + + ## `@deepseek-ai/dsh-subagent-dsh-sdk` Requires: `subagents` @@ -2025,6 +2163,8 @@ export interface Config { Source: [`packages/subagent/subagent-dsh-sdk/src/index.ts:29`](../packages/subagent/subagent-dsh-sdk/src/index.ts) + + ## `@deepseek-ai/dsh-subagent-fork-in-process` Requires: `subagents` @@ -2039,6 +2179,8 @@ export interface Config { Source: [`packages/subagent/subagent-fork-in-process/src/index.ts:31`](../packages/subagent/subagent-fork-in-process/src/index.ts) + + ## `@deepseek-ai/dsh-subagent-spawn-in-process` Requires: `subagents` @@ -2053,6 +2195,8 @@ export interface Config { Source: [`packages/subagent/subagent-spawn-in-process/src/index.ts:25`](../packages/subagent/subagent-spawn-in-process/src/index.ts) + + ## `@deepseek-ai/dsh-subprocess-e2b` Requires: `e2b` @@ -2067,6 +2211,8 @@ export interface Config { Source: [`packages/e2b/subprocess-e2b/src/index.ts:25`](../packages/e2b/subprocess-e2b/src/index.ts) + + ## `@deepseek-ai/dsh-system-prompt` ```ts config-catalog @@ -2092,6 +2238,8 @@ export interface Config { Source: [`packages/core/system-prompt/src/index.ts:186`](../packages/core/system-prompt/src/index.ts) + + ## `@deepseek-ai/dsh-terminal-bash` Requires: `terminals` · `sandboxPolicy` · `subprocess` @@ -2135,6 +2283,8 @@ export interface Config { Source: [`packages/terminal/terminal-bash/src/config.ts:6`](../packages/terminal/terminal-bash/src/config.ts) + + ## `@deepseek-ai/dsh-time-context` Requires: `agents` @@ -2151,6 +2301,8 @@ export interface Config { Source: [`packages/context/time-context/src/index.ts:27`](../packages/context/time-context/src/index.ts) + + ## `@deepseek-ai/dsh-tmux-context` Requires: `agents` @@ -2165,6 +2317,8 @@ export interface Config { Source: [`packages/context/tmux-context/src/index.ts:34`](../packages/context/tmux-context/src/index.ts) + + ## `@deepseek-ai/dsh-token-meter` ```ts config-catalog @@ -2174,6 +2328,8 @@ export type TokenMeterConfig = Record Source: [`packages/llm/token-meter/src/types.ts:12`](../packages/llm/token-meter/src/types.ts) + + ## `@deepseek-ai/dsh-tool-bash` Requires: `tools` · `shell` · `systemPrompt` · `shellEnv` @@ -2188,6 +2344,8 @@ export interface Config { Source: [`packages/shell/tool-bash/src/index.ts:34`](../packages/shell/tool-bash/src/index.ts) + + ## `@deepseek-ai/dsh-tool-bash-persistent` Requires: `tools` · `terminals` @@ -2208,6 +2366,8 @@ export interface Config { Source: [`packages/shell/tool-bash-persistent/src/index.ts:405`](../packages/shell/tool-bash-persistent/src/index.ts) + + ## `@deepseek-ai/dsh-tool-fs` Requires: `tools` · `fs` · `systemPrompt` @@ -2228,6 +2388,8 @@ export interface Config { Source: [`packages/fs/tool-fs/src/index.ts:25`](../packages/fs/tool-fs/src/index.ts) + + ## `@deepseek-ai/dsh-tool-fs-search` Requires: `tools` · `systemPrompt` · `subprocess` @@ -2261,6 +2423,8 @@ export interface Config { Source: [`packages/fs/tool-fs-search/src/index.ts:73`](../packages/fs/tool-fs-search/src/index.ts) + + ## `@deepseek-ai/dsh-tool-goal` Requires: `agents` · `goals` · `tools` · `systemPrompt` @@ -2275,6 +2439,8 @@ export interface Config { Source: [`packages/goal/tool-goal/src/index.ts:26`](../packages/goal/tool-goal/src/index.ts) + + ## `@deepseek-ai/dsh-tool-jobs` Requires: `tools` · `jobs` · `systemPrompt` @@ -2307,6 +2473,8 @@ export type CompletionDelivery = 'quiet' | 'wakeup' Source: [`packages/jobs/tool-jobs/src/index.ts:32`](../packages/jobs/tool-jobs/src/index.ts) + + ## `@deepseek-ai/dsh-tool-lsp` Requires: `tools` · `lsp` · `systemPrompt` @@ -2325,6 +2493,8 @@ export interface Config { Source: [`packages/lsp/tool-lsp/src/index.ts:58`](../packages/lsp/tool-lsp/src/index.ts) + + ## `@deepseek-ai/dsh-tool-pwsh` Requires: `tools` · `shell` · `systemPrompt` · `shellEnv` @@ -2339,6 +2509,8 @@ export interface Config { Source: [`packages/shell/tool-pwsh/src/index.ts:52`](../packages/shell/tool-pwsh/src/index.ts) + + ## `@deepseek-ai/dsh-tool-ralph` Requires: `tools` · `workflowEngine` · `subagents` · `systemPrompt` @@ -2359,6 +2531,8 @@ export interface Config { Source: [`packages/workflow/tool-ralph/src/index.ts:23`](../packages/workflow/tool-ralph/src/index.ts) + + ## `@deepseek-ai/dsh-tool-session-query` Requires: `tools` · `systemPrompt` · `sessionQuery` @@ -2375,6 +2549,8 @@ export interface Config { Source: [`packages/session-query/tool-session-query/src/index.ts:29`](../packages/session-query/tool-session-query/src/index.ts) + + ## `@deepseek-ai/dsh-tool-skill` Requires: `agents` · `tools` · `skills` @@ -2389,6 +2565,8 @@ export interface Config { Source: [`packages/skill/tool-skill/src/index.ts:61`](../packages/skill/tool-skill/src/index.ts) + + ## `@deepseek-ai/dsh-tool-str-replace-editor` Requires: `tools` · `fs` @@ -2405,6 +2583,8 @@ export interface Config { Source: [`packages/fs/tool-str-replace-editor/src/index.ts:497`](../packages/fs/tool-str-replace-editor/src/index.ts) + + ## `@deepseek-ai/dsh-tool-subagent` Requires: `tools` · `subagents` · `systemPrompt` @@ -2468,6 +2648,8 @@ Depends on: [`AgentOptions`](subsystems/core.md) Source: [`packages/subagent/tool-subagent/src/index.ts:29`](../packages/subagent/tool-subagent/src/index.ts) + + ## `@deepseek-ai/dsh-tool-subagent-report` Requires: `subagents` · `tools` · `systemPrompt` @@ -2488,6 +2670,8 @@ Depends on: [`SubagentReportDelivery`](subsystems/subagent.md) Source: [`packages/subagent/tool-subagent-report/src/index.ts:27`](../packages/subagent/tool-subagent-report/src/index.ts) + + ## `@deepseek-ai/dsh-tool-terminal` Requires: `terminals` · `tools` · `systemPrompt` @@ -2504,6 +2688,8 @@ export interface Config { Source: [`packages/terminal/tool-terminal/src/index.ts:35`](../packages/terminal/tool-terminal/src/index.ts) + + ## `@deepseek-ai/dsh-tool-todo` Requires: `tools` @@ -2524,6 +2710,8 @@ export interface Config { Source: [`packages/todo/tool-todo/src/index.ts:29`](../packages/todo/tool-todo/src/index.ts) + + ## `@deepseek-ai/dsh-tool-web` Requires: `tools` · `web` · `systemPrompt` @@ -2548,6 +2736,8 @@ export interface Config { Source: [`packages/web/tool-web/src/index.ts:37`](../packages/web/tool-web/src/index.ts) + + ## `@deepseek-ai/dsh-tool-workflow` Requires: `tools` · `workflowEngine` · `systemPrompt` @@ -2564,6 +2754,8 @@ export interface Config { Source: [`packages/workflow/tool-workflow/src/index.ts:33`](../packages/workflow/tool-workflow/src/index.ts) + + ## `@deepseek-ai/dsh-tools` Requires: `systemPrompt` @@ -2598,6 +2790,8 @@ export type ToolPresentationMode = 'native' | 'code' | 'both' Source: [`packages/core/tools/src/index.ts:654`](../packages/core/tools/src/index.ts) + + ## `@deepseek-ai/dsh-typert-loader` Requires: `typert` · `loader` @@ -2612,6 +2806,8 @@ export interface Config { Source: [`packages/typert/loader/src/index.ts:47`](../packages/typert/loader/src/index.ts) + + ## `@deepseek-ai/dsh-user-approval` ```ts config-catalog @@ -2641,6 +2837,8 @@ export type ApprovalPolicy = 'ask' | 'never' Source: [`packages/interaction/user-approval/src/index.ts:177`](../packages/interaction/user-approval/src/index.ts) + + ## `@deepseek-ai/dsh-web` ```ts config-catalog @@ -2660,6 +2858,8 @@ export interface WebRuntimeConfig { Source: [`packages/web/web/src/index.ts:55`](../packages/web/web/src/index.ts) + + ## `@deepseek-ai/dsh-web-app` Requires: `webServer` @@ -2683,6 +2883,8 @@ export interface Config { Source: [`packages/bundle/web-app/src/index.ts:38`](../packages/bundle/web-app/src/index.ts) + + ## `@deepseek-ai/dsh-web-fetch-http` Requires: `web` @@ -2707,6 +2909,8 @@ export interface Config { Source: [`packages/web/web-fetch-http/src/index.ts:34`](../packages/web/web-fetch-http/src/index.ts) + + ## `@deepseek-ai/dsh-web-search-deepseek` Requires: `web` @@ -2733,6 +2937,8 @@ export interface Config { Source: [`packages/web/web-search-deepseek/src/index.ts:46`](../packages/web/web-search-deepseek/src/index.ts) + + ## `@deepseek-ai/dsh-web-search-exa` Requires: `web` @@ -2755,6 +2961,8 @@ export interface Config { Source: [`packages/web/web-search-exa/src/index.ts:38`](../packages/web/web-search-exa/src/index.ts) + + ## `@deepseek-ai/dsh-web-search-perplexity` Requires: `web` @@ -2777,6 +2985,8 @@ export interface Config { Source: [`packages/web/web-search-perplexity/src/index.ts:32`](../packages/web/web-search-perplexity/src/index.ts) + + ## `@deepseek-ai/dsh-workflow-worker-thread` Requires: `subagents` diff --git a/docs/config-catalog.zh.md b/docs/config-catalog.zh.md index fda208e8fc..dc20cd90e5 100644 --- a/docs/config-catalog.zh.md +++ b/docs/config-catalog.zh.md @@ -11,6 +11,8 @@ `Requires:` 行列出插件通过 `inject` 注入的服务键:其 `cordis.yml` 树还必须加载这些服务的提供者。范围限定为 harness 层级(`packages/`);配置树还可能加载的 vendored cordis 插件(`hmr`、控制台日志记录器等)固定为上游源代码(参见 [vendoring policy](../vendor/README.md)),未收录于此目录。 + + ## `@deepseek-ai/dsh-acp` 需要:`agents` @@ -31,6 +33,8 @@ export interface AcpConfig { 来源:[`packages/acp/acp/src/index.ts:70`](../packages/acp/acp/src/index.ts) + + ## `@deepseek-ai/dsh-acp-demo` ```ts config-catalog @@ -84,6 +88,8 @@ export interface Config { 来源:[`packages/examples/acp-demo/src/index.ts:39`](../packages/examples/acp-demo/src/index.ts) + + ## `@deepseek-ai/dsh-agent-default-model` ```ts config-catalog @@ -98,6 +104,8 @@ export interface Config { 来源:[`packages/core/agent-default-model/src/index.ts:41`](../packages/core/agent-default-model/src/index.ts) + + ## `@deepseek-ai/dsh-agent-instructions` ```ts config-catalog @@ -126,6 +134,8 @@ export interface Config { 来源:[`packages/context/agent-instructions/src/config.ts:18`](../packages/context/agent-instructions/src/config.ts) + + ## `@deepseek-ai/dsh-agent-loop` 需要:`agents` · `sessions` · `llm` · `tools` · `systemPrompt` @@ -156,6 +166,8 @@ export interface Config { 来源:[`packages/core/agent-loop/src/index.ts:255`](../packages/core/agent-loop/src/index.ts) + + ## `@deepseek-ai/dsh-agent-presets` 需要:`loader` @@ -192,6 +204,8 @@ export type PresetTrust = 'system' | 'user' 来源:[`packages/preset/agent-presets/src/preset.ts:52`](../packages/preset/agent-presets/src/preset.ts) + + ## `@deepseek-ai/dsh-agent-spine-demo` ```ts config-catalog @@ -282,6 +296,8 @@ export interface GoalConfig { 来源:[`packages/examples/agent-spine-demo/src/index.ts:92`](../packages/examples/agent-spine-demo/src/index.ts) + + ## `@deepseek-ai/dsh-agent-tool-presentation` 需要:`tools` @@ -304,6 +320,8 @@ export interface Config { 来源:[`packages/core/agent-tool-presentation/src/index.ts:38`](../packages/core/agent-tool-presentation/src/index.ts) + + ## `@deepseek-ai/dsh-attachment-local` ```ts config-catalog @@ -324,6 +342,8 @@ export interface Config { 来源:[`packages/attachment/attachment-local/src/index.ts:24`](../packages/attachment/attachment-local/src/index.ts) + + ## `@deepseek-ai/dsh-bash-local` 需要:`subprocess` @@ -348,6 +368,8 @@ export interface Config { 来源:[`packages/shell/bash-local/src/index.ts:41`](../packages/shell/bash-local/src/index.ts) + + ## `@deepseek-ai/dsh-bash-sandbox` 需要:`subprocess` · `sandbox` · `sandboxPolicy` @@ -367,6 +389,8 @@ export type Config = LocalConfig 来源:[`packages/shell/bash-sandbox/src/index.ts:35`](../packages/shell/bash-sandbox/src/index.ts) + + ## `@deepseek-ai/dsh-client-connection` 需要:`webServer` @@ -390,6 +414,8 @@ export interface ConnectionConfig { 来源:[`packages/client/connection/src/index.ts:50`](../packages/client/connection/src/index.ts) + + ## `@deepseek-ai/dsh-client-hmr` 需要:`clientModuleHost` · `webServer` @@ -404,6 +430,8 @@ export interface Config { 来源:[`packages/client/hmr/src/index.ts:31`](../packages/client/hmr/src/index.ts) + + ## `@deepseek-ai/dsh-code-runtime-worker-thread` ```ts config-catalog @@ -439,6 +467,8 @@ export interface Config { 来源:[`packages/code-runtime/code-runtime-worker-thread/src/index.ts:25`](../packages/code-runtime/code-runtime-worker-thread/src/index.ts) + + ## `@deepseek-ai/dsh-compaction-basic` 需要:`llm` · `tokenMeter` · `sessions` @@ -483,6 +513,8 @@ export interface ModelCompactPolicyConfig extends CompactionPolicyConfig { 来源:[`packages/compaction/compaction-basic/src/types.ts:38`](../packages/compaction/compaction-basic/src/types.ts) + + ## `@deepseek-ai/dsh-compaction-tool-result-pruner` 需要:`tokenMeter` @@ -501,6 +533,8 @@ export interface ToolResultPruneConfig { 来源:[`packages/compaction/compaction-tool-result-pruner/src/types.ts:4`](../packages/compaction/compaction-tool-result-pruner/src/types.ts) + + ## `@deepseek-ai/dsh-cordis-host-runner` 需要:`tools` @@ -515,6 +549,8 @@ export interface Config { 来源:[`packages/extensions/cordis-host-runner/src/index.ts:88`](../packages/extensions/cordis-host-runner/src/index.ts) + + ## `@deepseek-ai/dsh-credentials-local` ```ts config-catalog @@ -533,6 +569,8 @@ export interface Config { 来源:[`packages/credentials/credentials-local/src/index.ts:55`](../packages/credentials/credentials-local/src/index.ts) + + ## `@deepseek-ai/dsh-e2b` ```ts config-catalog @@ -549,6 +587,8 @@ export interface Config { 来源:[`packages/e2b/e2b/src/index.ts:43`](../packages/e2b/e2b/src/index.ts) + + ## `@deepseek-ai/dsh-fs-local` ```ts config-catalog @@ -566,6 +606,8 @@ export interface Config { 来源:[`packages/fs/fs-local/src/index.ts:41`](../packages/fs/fs-local/src/index.ts) + + ## `@deepseek-ai/dsh-fs-sandbox` 需要:`sandboxPolicy` @@ -584,6 +626,8 @@ export type Config = LocalConfig 来源:[`packages/fs/fs-sandbox/src/index.ts:49`](../packages/fs/fs-sandbox/src/index.ts) + + ## `@deepseek-ai/dsh-goal` 需要:`agents` @@ -598,6 +642,8 @@ export interface Config { 来源:[`packages/goal/goal/src/index.ts:116`](../packages/goal/goal/src/index.ts) + + ## `@deepseek-ai/dsh-headless` 需要:`agentDefaultModel` · `agents` · `sessions` @@ -612,6 +658,8 @@ export interface Config { 来源:[`packages/bundle/headless/src/index.ts:31`](../packages/bundle/headless/src/index.ts) + + ## `@deepseek-ai/dsh-hooks-claude-code` 需要:`bash` @@ -648,6 +696,8 @@ export interface Config { 来源:[`packages/hooks/hooks-claude-code/src/index.ts:45`](../packages/hooks/hooks-claude-code/src/index.ts) + + ## `@deepseek-ai/dsh-hooks-codex` 需要:`bash` @@ -673,6 +723,8 @@ export interface Config { 来源:[`packages/hooks/hooks-codex/src/index.ts:44`](../packages/hooks/hooks-codex/src/index.ts) + + ## `@deepseek-ai/dsh-host-apiproxy` 需要:`agentDefaultModel` · `agents` · `attachments` · `directoryPicker` · `llm` · `sessions` · `subagents` · `sessionQuery` · `tools` · `userInteraction` · `workspace` @@ -699,6 +751,8 @@ export interface Config { 来源:[`packages/host/apiproxy/src/index.ts:41`](../packages/host/apiproxy/src/index.ts) + + ## `@deepseek-ai/dsh-host-directory-picker-browse` ```ts config-catalog @@ -711,6 +765,8 @@ export interface Config { 来源:[`packages/host/directory-picker-browse/src/index.ts:181`](../packages/host/directory-picker-browse/src/index.ts) + + ## `@deepseek-ai/dsh-host-frontend-static` 需要:`webServer` @@ -725,6 +781,8 @@ export interface Config { 来源:[`packages/host/frontend-static/src/index.ts:28`](../packages/host/frontend-static/src/index.ts) + + ## `@deepseek-ai/dsh-host-webserver` ```ts config-catalog @@ -739,6 +797,8 @@ export interface Config { 来源:[`packages/host/webserver/src/index.ts:45`](../packages/host/webserver/src/index.ts) + + ## `@deepseek-ai/dsh-invariants` ```ts config-catalog @@ -755,6 +815,8 @@ export interface Config { 来源:[`packages/runtime-diagnostics/invariants/src/index.ts:15`](../packages/runtime-diagnostics/invariants/src/index.ts) + + ## `@deepseek-ai/dsh-jobs-local` ```ts config-catalog @@ -770,6 +832,8 @@ export interface Config { 来源:[`packages/jobs/jobs-local/src/index.ts:31`](../packages/jobs/jobs-local/src/index.ts) + + ## `@deepseek-ai/dsh-llm-deepseek` 需要:`llm` @@ -823,6 +887,8 @@ export interface DeepSeekCatalogModel { 来源:[`packages/llm/llm-deepseek/src/index.ts:62`](../packages/llm/llm-deepseek/src/index.ts) + + ## `@deepseek-ai/dsh-llm-pi-ai` 需要:`llm` @@ -1011,6 +1077,8 @@ type WithheldThinkingFormat = 'chat-template' | 'qwen-chat-template' 来源:[`packages/llm/llm-pi-ai/src/config.ts:172`](../packages/llm/llm-pi-ai/src/config.ts) + + ## `@deepseek-ai/dsh-llm-replay` 需要:`llm` @@ -1077,6 +1145,8 @@ export interface ReplayModelConfig { 来源:[`packages/test-support/llm-replay/src/index.ts:776`](../packages/test-support/llm-replay/src/index.ts) + + ## `@deepseek-ai/dsh-llm-retry` 需要:`agents` @@ -1088,6 +1158,8 @@ export type Config = Readonly> 来源:[`packages/llm/llm-retry/src/index.ts:24`](../packages/llm/llm-retry/src/index.ts) + + ## `@deepseek-ai/dsh-lsp-stdio` 需要:`fs` · `lsp` · `subprocess` @@ -1128,6 +1200,8 @@ export interface LspLocalServerConfig { 来源:[`packages/lsp/lsp-stdio/src/index.ts:82`](../packages/lsp/lsp-stdio/src/index.ts) + + ## `@deepseek-ai/dsh-mcp-client` 需要:`tools` @@ -1199,6 +1273,8 @@ export interface ReconnectConfig { 来源:[`packages/mcp/mcp-client/src/index.ts:98`](../packages/mcp/mcp-client/src/index.ts) + + ## `@deepseek-ai/dsh-message-feedback` 需要:`storageDomain` · `sessionPersistence` · `sessions` @@ -1213,6 +1289,8 @@ export interface Config { 来源:[`packages/feedback/message-feedback/src/index.ts:49`](../packages/feedback/message-feedback/src/index.ts) + + ## `@deepseek-ai/dsh-permission-presets` 需要:`bash` · `approval` · `sessions` @@ -1250,6 +1328,8 @@ export interface PresetSpec { 来源:[`packages/interaction/permission-presets/src/index.ts:140`](../packages/interaction/permission-presets/src/index.ts) + + ## `@deepseek-ai/dsh-persona` 需要:`systemPrompt` @@ -1272,6 +1352,8 @@ export interface Config { 来源:[`packages/preset/persona/src/index.ts:34`](../packages/preset/persona/src/index.ts) + + ## `@deepseek-ai/dsh-plan-mode` 需要:`tools` · `systemPrompt` @@ -1286,6 +1368,8 @@ export interface PlanModeConfig { 来源:[`packages/plan/plan-mode/src/index.ts:70`](../packages/plan/plan-mode/src/index.ts) + + ## `@deepseek-ai/dsh-pwsh-local` 需要:`subprocess` @@ -1317,6 +1401,8 @@ export interface Config { 来源:[`packages/shell/pwsh-local/src/index.ts:58`](../packages/shell/pwsh-local/src/index.ts) + + ## `@deepseek-ai/dsh-pwsh-sandbox` 需要:`subprocess` · `sandbox` · `sandboxPolicy` @@ -1337,6 +1423,8 @@ export type Config = LocalConfig 来源:[`packages/shell/pwsh-sandbox/src/index.ts:40`](../packages/shell/pwsh-sandbox/src/index.ts) + + ## `@deepseek-ai/dsh-repeat-tool-reminder` ```ts config-catalog @@ -1369,6 +1457,8 @@ export interface Config { 来源:[`packages/guard/repeat-tool-reminder/src/index.ts:28`](../packages/guard/repeat-tool-reminder/src/index.ts) + + ## `@deepseek-ai/dsh-sandbox-local` ```ts config-catalog @@ -1399,6 +1489,8 @@ export interface Config { 来源:[`packages/sandbox/sandbox-local/src/index.ts:44`](../packages/sandbox/sandbox-local/src/index.ts) + + ## `@deepseek-ai/dsh-sandbox-policy` ```ts config-catalog @@ -1424,6 +1516,8 @@ export interface Config { 来源:[`packages/sandbox/sandbox-policy/src/index.ts:67`](../packages/sandbox/sandbox-policy/src/index.ts) + + ## `@deepseek-ai/dsh-sdk-jsonrpc-server` 需要:`agents` @@ -1446,6 +1540,8 @@ export interface JsonRpcConfig { 来源:[`packages/sdk/server/src/index.ts:29`](../packages/sdk/server/src/index.ts) + + ## `@deepseek-ai/dsh-session-persistence-jsonl` 需要:`sessions` @@ -1483,6 +1579,8 @@ export type JsonlCompression = 'zstd' | 'none' 来源:[`packages/session/session-persistence-jsonl/src/index.ts:60`](../packages/session/session-persistence-jsonl/src/index.ts) + + ## `@deepseek-ai/dsh-session-persistence-sqlite` 需要:`sessions` @@ -1526,6 +1624,8 @@ export type JournalMode = 'wal' | 'delete' | 'truncate' | 'persist' 来源:[`packages/session/session-persistence-sqlite/src/index.ts:70`](../packages/session/session-persistence-sqlite/src/index.ts) + + ## `@deepseek-ai/dsh-session-projection-cache` 需要:`storageDomain` · `sessionProjections` · `sessionPersistence` · `sessions` @@ -1547,6 +1647,8 @@ export interface Config { 来源:[`packages/session/session-projection-cache/src/index.ts:42`](../packages/session/session-projection-cache/src/index.ts) + + ## `@deepseek-ai/dsh-session-query-sqlite` 需要:`sessions` @@ -1591,6 +1693,8 @@ export type JournalMode = 'wal' | 'delete' | 'truncate' | 'persist' 来源:[`packages/session-query/session-query-sqlite/src/index.ts:89`](../packages/session-query/session-query-sqlite/src/index.ts) + + ## `@deepseek-ai/dsh-session-reference` 需要:`sessionQuery` @@ -1609,6 +1713,8 @@ export interface Config { 来源:[`packages/context/session-reference/src/config.ts:11`](../packages/context/session-reference/src/config.ts) + + ## `@deepseek-ai/dsh-session-telemetry-otel` 需要:`sessions` @@ -1653,6 +1759,8 @@ export enum SessionTelemetryMode { 来源:[`packages/session/session-telemetry-otel/src/index.ts:91`](../packages/session/session-telemetry-otel/src/index.ts) + + ## `@deepseek-ai/dsh-session-title` 需要:`sessions` @@ -1671,6 +1779,8 @@ export interface Config { 来源:[`packages/session/session-title/src/index.ts:79`](../packages/session/session-title/src/index.ts) + + ## `@deepseek-ai/dsh-session-title-all-prompts-llm` 需要:`sessionTitle` · `llm` · `sessions` @@ -1684,6 +1794,8 @@ export type Config = SessionTitleLlmConfig 来源:[`packages/session/session-title-all-prompts-llm/src/index.ts:15`](../packages/session/session-title-all-prompts-llm/src/index.ts) + + ## `@deepseek-ai/dsh-session-title-first-prompt-llm` 需要:`sessionTitle` · `llm` · `sessions` @@ -1697,6 +1809,8 @@ export type Config = SessionTitleLlmConfig 来源:[`packages/session/session-title-first-prompt-llm/src/index.ts:15`](../packages/session/session-title-first-prompt-llm/src/index.ts) + + ## `@deepseek-ai/dsh-settings-file` ```ts config-catalog @@ -1715,6 +1829,8 @@ export interface Config { 来源:[`packages/settings/settings-file/src/index.ts:21`](../packages/settings/settings-file/src/index.ts) + + ## `@deepseek-ai/dsh-shell-env` ```ts config-catalog @@ -1727,6 +1843,8 @@ export interface Config { 来源:[`packages/shell/shell-env/src/index.ts:29`](../packages/shell/shell-env/src/index.ts) + + ## `@deepseek-ai/dsh-skill` ```ts config-catalog @@ -1739,6 +1857,8 @@ export interface Config { 来源:[`packages/skill/skill/src/index.ts:279`](../packages/skill/skill/src/index.ts) + + ## `@deepseek-ai/dsh-skill-filesystem` 需要:`skills` @@ -1775,6 +1895,8 @@ export interface Config { 来源:[`packages/skill/skill-filesystem/src/index.ts:49`](../packages/skill/skill-filesystem/src/index.ts) + + ## `@deepseek-ai/dsh-spill-local` ```ts config-catalog @@ -1791,6 +1913,8 @@ export interface Config { 来源:[`packages/spill/spill-local/src/index.ts:22`](../packages/spill/spill-local/src/index.ts) + + ## `@deepseek-ai/dsh-spill-policy` 需要:`tools` @@ -1809,6 +1933,8 @@ export interface Config { 来源:[`packages/spill/spill-policy/src/index.ts:60`](../packages/spill/spill-policy/src/index.ts) + + ## `@deepseek-ai/dsh-storage-domain` 需要:`storage` @@ -1830,6 +1956,8 @@ export interface Config { 来源:[`packages/storage/storage-domain/src/index.ts:52`](../packages/storage/storage-domain/src/index.ts) + + ## `@deepseek-ai/dsh-storage-json` 需要:`storage` @@ -1849,6 +1977,8 @@ export interface Config { 来源:[`packages/storage/storage-json/src/index.ts:27`](../packages/storage/storage-json/src/index.ts) + + ## `@deepseek-ai/dsh-storage-sqlite` 需要:`storage` @@ -1887,6 +2017,8 @@ export type JournalMode = 'wal' | 'delete' | 'truncate' | 'persist' 来源:[`packages/storage/storage-sqlite/src/index.ts:24`](../packages/storage/storage-sqlite/src/index.ts) + + ## `@deepseek-ai/dsh-subagent-acp` 需要:`subagents` · `subprocess` @@ -1938,6 +2070,8 @@ export type PermissionPolicy = 'allow' | 'reject' 来源:[`packages/subagent/subagent-acp/src/index.ts:27`](../packages/subagent/subagent-acp/src/index.ts) + + ## `@deepseek-ai/dsh-subagent-claude-code` 需要:`subagents` · `subprocess` @@ -1957,6 +2091,8 @@ export interface Config { 来源:[`packages/subagent/subagent-claude-code/src/index.ts:32`](../packages/subagent/subagent-claude-code/src/index.ts) + + ## `@deepseek-ai/dsh-subagent-codex` 需要:`subagents` · `subprocess` @@ -1976,6 +2112,8 @@ export interface Config { 来源:[`packages/subagent/subagent-codex/src/index.ts:30`](../packages/subagent/subagent-codex/src/index.ts) + + ## `@deepseek-ai/dsh-subagent-dsh-sdk` 需要:`subagents` @@ -2027,6 +2165,8 @@ export interface Config { 来源:[`packages/subagent/subagent-dsh-sdk/src/index.ts:29`](../packages/subagent/subagent-dsh-sdk/src/index.ts) + + ## `@deepseek-ai/dsh-subagent-fork-in-process` 需要:`subagents` @@ -2041,6 +2181,8 @@ export interface Config { 来源:[`packages/subagent/subagent-fork-in-process/src/index.ts:31`](../packages/subagent/subagent-fork-in-process/src/index.ts) + + ## `@deepseek-ai/dsh-subagent-spawn-in-process` 需要:`subagents` @@ -2055,6 +2197,8 @@ export interface Config { 来源:[`packages/subagent/subagent-spawn-in-process/src/index.ts:25`](../packages/subagent/subagent-spawn-in-process/src/index.ts) + + ## `@deepseek-ai/dsh-subprocess-e2b` 需要:`e2b` @@ -2069,6 +2213,8 @@ export interface Config { 来源:[`packages/e2b/subprocess-e2b/src/index.ts:25`](../packages/e2b/subprocess-e2b/src/index.ts) + + ## `@deepseek-ai/dsh-system-prompt` ```ts config-catalog @@ -2094,6 +2240,8 @@ export interface Config { 来源:[`packages/core/system-prompt/src/index.ts:186`](../packages/core/system-prompt/src/index.ts) + + ## `@deepseek-ai/dsh-terminal-bash` 需要:`pty` · `sandboxPolicy` · `subprocess` @@ -2137,6 +2285,8 @@ export interface Config { 来源:[`packages/terminal/terminal-bash/src/config.ts:6`](../packages/terminal/terminal-bash/src/config.ts) + + ## `@deepseek-ai/dsh-time-context` 需要:`agents` @@ -2153,6 +2303,8 @@ export interface Config { 来源:[`packages/context/time-context/src/index.ts:27`](../packages/context/time-context/src/index.ts) + + ## `@deepseek-ai/dsh-tmux-context` 需要:`agents` @@ -2167,6 +2319,8 @@ export interface Config { 来源:[`packages/context/tmux-context/src/index.ts:34`](../packages/context/tmux-context/src/index.ts) + + ## `@deepseek-ai/dsh-token-meter` ```ts config-catalog @@ -2176,6 +2330,8 @@ export type TokenMeterConfig = Record 来源:[`packages/llm/token-meter/src/types.ts:12`](../packages/llm/token-meter/src/types.ts) + + ## `@deepseek-ai/dsh-tool-bash` 需要:`tools` · `bash` · `systemPrompt` · `bashEnv` @@ -2190,6 +2346,8 @@ export interface Config { 来源:[`packages/shell/tool-bash/src/index.ts:34`](../packages/shell/tool-bash/src/index.ts) + + ## `@deepseek-ai/dsh-tool-bash-persistent` 需要:`tools` · `pty` @@ -2210,6 +2368,8 @@ export interface Config { 来源:[`packages/shell/tool-bash-persistent/src/index.ts:405`](../packages/shell/tool-bash-persistent/src/index.ts) + + ## `@deepseek-ai/dsh-tool-fs` 需要:`tools` · `fs` · `systemPrompt` @@ -2230,6 +2390,8 @@ export interface Config { 来源:[`packages/fs/tool-fs/src/index.ts:25`](../packages/fs/tool-fs/src/index.ts) + + ## `@deepseek-ai/dsh-tool-fs-search` 需要:`tools` · `systemPrompt` · `subprocess` @@ -2263,6 +2425,8 @@ export interface Config { 来源:[`packages/fs/tool-fs-search/src/index.ts:73`](../packages/fs/tool-fs-search/src/index.ts) + + ## `@deepseek-ai/dsh-tool-goal` 需要:`agents` · `goals` · `tools` · `systemPrompt` @@ -2277,6 +2441,8 @@ export interface Config { 来源:[`packages/goal/tool-goal/src/index.ts:26`](../packages/goal/tool-goal/src/index.ts) + + ## `@deepseek-ai/dsh-tool-jobs` 需要:`tools` · `tasks` · `systemPrompt` @@ -2309,6 +2475,8 @@ export type CompletionDelivery = 'quiet' | 'wakeup' 来源:[`packages/jobs/tool-jobs/src/index.ts:32`](../packages/jobs/tool-jobs/src/index.ts) + + ## `@deepseek-ai/dsh-tool-lsp` 需要:`tools` · `lsp` · `systemPrompt` @@ -2327,6 +2495,8 @@ export interface Config { 来源:[`packages/lsp/tool-lsp/src/index.ts:58`](../packages/lsp/tool-lsp/src/index.ts) + + ## `@deepseek-ai/dsh-tool-pwsh` 需要:`tools` · `bash` · `systemPrompt` · `bashEnv` @@ -2341,6 +2511,8 @@ export interface Config { 来源:[`packages/shell/tool-pwsh/src/index.ts:52`](../packages/shell/tool-pwsh/src/index.ts) + + ## `@deepseek-ai/dsh-tool-ralph` 需要:`tools` · `workflows` · `subagents` · `systemPrompt` @@ -2361,6 +2533,8 @@ export interface Config { 来源:[`packages/workflow/tool-ralph/src/index.ts:23`](../packages/workflow/tool-ralph/src/index.ts) + + ## `@deepseek-ai/dsh-tool-session-query` 需要:`tools` · `systemPrompt` · `sessionQuery` @@ -2377,6 +2551,8 @@ export interface Config { 来源:[`packages/session-query/tool-session-query/src/index.ts:29`](../packages/session-query/tool-session-query/src/index.ts) + + ## `@deepseek-ai/dsh-tool-skill` 需要:`agents` · `tools` · `skills` @@ -2391,6 +2567,8 @@ export interface Config { 来源:[`packages/skill/tool-skill/src/index.ts:61`](../packages/skill/tool-skill/src/index.ts) + + ## `@deepseek-ai/dsh-tool-str-replace-editor` 需要:`tools` · `fs` @@ -2407,6 +2585,8 @@ export interface Config { 来源:[`packages/fs/tool-str-replace-editor/src/index.ts:497`](../packages/fs/tool-str-replace-editor/src/index.ts) + + ## `@deepseek-ai/dsh-tool-subagent` 需要:`tools` · `subagents` · `systemPrompt` @@ -2470,6 +2650,8 @@ export interface Config { 来源:[`packages/subagent/tool-subagent/src/index.ts:29`](../packages/subagent/tool-subagent/src/index.ts) + + ## `@deepseek-ai/dsh-tool-subagent-report` 需要:`subagents` · `tools` · `systemPrompt` @@ -2490,6 +2672,8 @@ export interface Config { 来源:[`packages/subagent/tool-subagent-report/src/index.ts:27`](../packages/subagent/tool-subagent-report/src/index.ts) + + ## `@deepseek-ai/dsh-tool-terminal` 需要:`pty` · `tools` · `systemPrompt` @@ -2506,6 +2690,8 @@ export interface Config { 来源:[`packages/terminal/tool-terminal/src/index.ts:35`](../packages/terminal/tool-terminal/src/index.ts) + + ## `@deepseek-ai/dsh-tool-todo` 需要:`tools` @@ -2526,6 +2712,8 @@ export interface Config { 来源:[`packages/todo/tool-todo/src/index.ts:29`](../packages/todo/tool-todo/src/index.ts) + + ## `@deepseek-ai/dsh-tool-web` 需要:`tools` · `web` · `systemPrompt` @@ -2550,6 +2738,8 @@ export interface Config { 来源:[`packages/web/tool-web/src/index.ts:37`](../packages/web/tool-web/src/index.ts) + + ## `@deepseek-ai/dsh-tool-workflow` 需要:`tools` · `workflows` · `systemPrompt` @@ -2566,6 +2756,8 @@ export interface Config { 来源:[`packages/workflow/tool-workflow/src/index.ts:33`](../packages/workflow/tool-workflow/src/index.ts) + + ## `@deepseek-ai/dsh-tools` 需要:`systemPrompt` @@ -2600,6 +2792,8 @@ export type ToolPresentationMode = 'native' | 'code' | 'both' 来源:[`packages/core/tools/src/index.ts:654`](../packages/core/tools/src/index.ts) + + ## `@deepseek-ai/dsh-typert-loader` 需要:`typert` · `loader` @@ -2614,6 +2808,8 @@ export interface Config { 来源:[`packages/typert/loader/src/index.ts:47`](../packages/typert/loader/src/index.ts) + + ## `@deepseek-ai/dsh-user-approval` ```ts config-catalog @@ -2643,6 +2839,8 @@ export type ApprovalPolicy = 'ask' | 'never' 来源:[`packages/interaction/user-approval/src/index.ts:177`](../packages/interaction/user-approval/src/index.ts) + + ## `@deepseek-ai/dsh-web` ```ts config-catalog @@ -2662,6 +2860,8 @@ export interface WebRuntimeConfig { 来源:[`packages/web/web/src/index.ts:55`](../packages/web/web/src/index.ts) + + ## `@deepseek-ai/dsh-web-app` 需要:`webServer` @@ -2685,6 +2885,8 @@ export interface Config { 来源:[`packages/bundle/web-app/src/index.ts:38`](../packages/bundle/web-app/src/index.ts) + + ## `@deepseek-ai/dsh-web-fetch-http` 需要:`web` @@ -2709,6 +2911,8 @@ export interface Config { 来源:[`packages/web/web-fetch-http/src/index.ts:34`](../packages/web/web-fetch-http/src/index.ts) + + ## `@deepseek-ai/dsh-web-search-deepseek` 需要:`web` @@ -2735,6 +2939,8 @@ export interface Config { 来源:[`packages/web/web-search-deepseek/src/index.ts:46`](../packages/web/web-search-deepseek/src/index.ts) + + ## `@deepseek-ai/dsh-web-search-exa` 需要:`web` @@ -2757,6 +2963,8 @@ export interface Config { 来源:[`packages/web/web-search-exa/src/index.ts:38`](../packages/web/web-search-exa/src/index.ts) + + ## `@deepseek-ai/dsh-web-search-perplexity` 需要:`web` @@ -2779,6 +2987,8 @@ export interface Config { 来源:[`packages/web/web-search-perplexity/src/index.ts:32`](../packages/web/web-search-perplexity/src/index.ts) + + ## `@deepseek-ai/dsh-workflow-worker-thread` 需要:`subagents` diff --git a/docs/cookbook/adding-a-tool.i18n.yaml b/docs/cookbook/adding-a-tool.i18n.yaml index d7f08cc5af..b39b907128 100644 --- a/docs/cookbook/adding-a-tool.i18n.yaml +++ b/docs/cookbook/adding-a-tool.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/cookbook/adding-a-tool.md adding-a-tool.md: 37516521de4d00de964003fd6f877831774fdcd3 -adding-a-tool.zh.md: 9455fa3b8d8b87632724ad4b035f184cb0c17993 +adding-a-tool.zh.md: 27a90ce19653333a0a6afd989115e614a1c8f9f4 diff --git a/docs/cookbook/adding-a-tool.zh.md b/docs/cookbook/adding-a-tool.zh.md index 9455fa3b8d..27a90ce196 100644 --- a/docs/cookbook/adding-a-tool.zh.md +++ b/docs/cookbook/adding-a-tool.zh.md @@ -54,6 +54,8 @@ export function apply(ctx: Context) { producer 提供同步的 `cancel`、在资源清理后 settle 且不 reject 的 `done`,以及可选的消费式 `readOutput`(负责有界输出的格式化)。预先中止的调用属于失败,因为此时没有任务,其 id 无法满足成功输出 schema。`ctx.jobs.start()` 发布 id 后,应使用任务自有的取消信号,而不是 `exec.signal`:之后取消外层调用只会停止等待本次调用,不会终止已经发布的工作;该生命周期归 `job_kill`、owner dispose 和服务 teardown 所有。前台工作仍与 `exec.signal` 耦合。流式 producer 的示例和完整约定见[后台任务运行时 Agent Note](../../.agents/notes/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.md)与 `dsh-tool-bash`。 + + ## 执行策略与观测 尽量不要把部署策略内建到工具中。使用 `tools/pre-execute` 实现可扩展的允许/拒绝/询问策略(见[权限门禁示例](extension-cookbook.md#a-hook-plugin-permission-gate-example));使用 `ctx.tools.guard()` 设置最终的单调拒绝,后续监听器无法撤销;使用 `tools/execute` 为分发添加截止时间、重试或指标收集;使用 `tools/post-execute` 替换展示内容或返回值、阻止结果,或附加模型可见上下文;使用 `tools/result` 观测不可变的归一化结果而不改变它。替换内容不会阻止程序化访问 `value`;保密策略会屏蔽或替换该值。沙箱实现也可以在工具的执行器实现中运行;[`dsh-tools` README](../../packages/core/tools/README.md#extension-points) 定义每个扩展点的输入、顺序、返回值和失败行为。 diff --git a/docs/cookbook/extension-cookbook.i18n.yaml b/docs/cookbook/extension-cookbook.i18n.yaml index a496c31981..d55871fa50 100644 --- a/docs/cookbook/extension-cookbook.i18n.yaml +++ b/docs/cookbook/extension-cookbook.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/cookbook/extension-cookbook.md extension-cookbook.md: 9618a3522c5566636fe3e49f7eca93d1e113d51a -extension-cookbook.zh.md: 7b82d7d1ff239cc2139ee424613e818669f3e2e8 +extension-cookbook.zh.md: 540bc6867016d095ccae0fb0fdc79bc1b1c26290 diff --git a/docs/cookbook/extension-cookbook.zh.md b/docs/cookbook/extension-cookbook.zh.md index 7b82d7d1ff..540bc68670 100644 --- a/docs/cookbook/extension-cookbook.zh.md +++ b/docs/cookbook/extension-cookbook.zh.md @@ -8,6 +8,8 @@ harness 扩展的参考模式。代码片段省略了 import 和辅助实现, 工具在 `ctx.tools` 上注册。带注解的 `defineTool` 示例(类型化的 `execute` 参数、结果构造、`run_in_background` 模式)见 [adding-a-tool.md](adding-a-tool.md)——该指南是工具定义的真源。`ctx.tools.register()` 也直接接受原始 JSON Schema `ToolDefinition`(MCP 来源的工具就是这样到达的);`defineTool` 是第一方工具使用的类型化辅助函数。 + + ## 钩子插件(以权限门禁为例) 这个权限门禁是钩子插件的一个示例。它从 `tools/pre-execute` 门禁返回一个类型化的决策,用于允许或拒绝一次调用;沙箱、权限和 plan-mode 插件都可以使用该扩展点。钩子插件也可以拦截其他扩展点,本身并不等同于权限门禁。「原生钩子」是在拦截点上运行的普通 Cordis 插件,不需要外部协议。 diff --git a/docs/cordis-primer.i18n.yaml b/docs/cordis-primer.i18n.yaml index cec0660aae..949846bee5 100644 --- a/docs/cordis-primer.i18n.yaml +++ b/docs/cordis-primer.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/cordis-primer.md cordis-primer.md: 2a3afe180623d89b006dfa3e73aba5567c15bbe9 -cordis-primer.zh.md: bdce14cf9f157959d6419f88c9c69570102b9c0c +cordis-primer.zh.md: d4d60f60717ffdc01499fdffadba2808557b285f diff --git a/docs/cordis-primer.zh.md b/docs/cordis-primer.zh.md index bdce14cf9f..d4d60f6071 100644 --- a/docs/cordis-primer.zh.md +++ b/docs/cordis-primer.zh.md @@ -12,6 +12,8 @@ Cordis 是 DeepSeek Harness 底层以 vendor 方式引入的插件框架。本 - **类型化事件用于通信。** 服务通过 TypeScript 声明合并注册事件名,然后以 `emit`、`waterfall`(瀑布式事件)、`parallel` 或 `serial` 方式分发,分别对应监听者观察、包装、并行扇出或按序执行。 - **注册是可逆的副作用。** 提示词片段、工具 schema、适配器、提供方和监听器通过 `ctx.effect()` 或 `ctx.on()` 安装,reload 和 teardown 时会按预期撤销。 + + ## 分发模式 每个事件具有以下分发模式之一,且只能通过对应方法分发。 diff --git a/docs/cordis-tutorial/03-services.i18n.yaml b/docs/cordis-tutorial/03-services.i18n.yaml index 372e028a47..80ea0f275f 100644 --- a/docs/cordis-tutorial/03-services.i18n.yaml +++ b/docs/cordis-tutorial/03-services.i18n.yaml @@ -2,5 +2,5 @@ # 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 docs/cordis-tutorial/03-services.md -03-services.md: 32007284be99ef46b4621089c9b3a80317e77189 -03-services.zh.md: d82be29aa69686b8dc10cc6a45a658683c017cbd +03-services.md: ebfc400dbbc701a3c164c7d30c371dec879d7d73 +03-services.zh.md: fcfd8be7f7fe654a4f4943cf591b5ab7bfc27fc6 diff --git a/docs/cordis-tutorial/03-services.md b/docs/cordis-tutorial/03-services.md index 32007284be..ebfc400dbb 100644 --- a/docs/cordis-tutorial/03-services.md +++ b/docs/cordis-tutorial/03-services.md @@ -75,7 +75,7 @@ Swap the two lines in `cordis.yml` and rerun: same output. Try removing `./greet `inject` is not a one-shot boot check. If a required service disappears while the app runs — its provider was unloaded or hot-replaced — every dependent plugin is unloaded too, and loads again when the service returns. Combined with effects ([chapter 2](02-lifecycle-and-effects.md)), this prevents a running consumer from retaining a reference to an unavailable service: its own registrations are unwound when the dependency disappears. -This is also why service replacement works in config: unload the `dsh-bash-local` entry, mount a different `bash` provider, and every plugin injecting `'bash'` cleanly restarts against the new implementation. +This is also why service replacement works in config: unload the `dsh-bash-local` entry, mount a different `shell` provider, and every plugin injecting `'shell'` cleanly restarts against the new implementation. ## Optional dependencies diff --git a/docs/cordis-tutorial/03-services.zh.md b/docs/cordis-tutorial/03-services.zh.md index d82be29aa6..fcfd8be7f7 100644 --- a/docs/cordis-tutorial/03-services.zh.md +++ b/docs/cordis-tutorial/03-services.zh.md @@ -75,7 +75,7 @@ Hello, world! `inject` 并非一次性的启动检查。如果应用运行期间所需服务消失,例如提供方被卸载或热替换,每个依赖插件也会随之卸载,并在服务恢复后再次加载。结合 effect([第 2 章](02-lifecycle-and-effects.md)),这能防止运行中的消费方保留对不可用服务的引用:依赖消失时,它自己的注册也会撤销。 -这也是配置中可以替换服务的原因:卸载 Cordis 配置项 `dsh-bash-local`,挂载另一个 `bash` 提供方,所有注入 `'bash'` 的插件都会重新启动并使用新实现。 +这也是配置中可以替换服务的原因:卸载 Cordis 配置项 `dsh-bash-local`,挂载另一个 `shell` 提供方,所有注入 `'shell'` 的插件都会重新启动并使用新实现。 ## 可选依赖 diff --git a/docs/cordis-tutorial/06-composition-and-hmr.i18n.yaml b/docs/cordis-tutorial/06-composition-and-hmr.i18n.yaml index 6a79850803..c61ee1a5ea 100644 --- a/docs/cordis-tutorial/06-composition-and-hmr.i18n.yaml +++ b/docs/cordis-tutorial/06-composition-and-hmr.i18n.yaml @@ -2,5 +2,5 @@ # 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 docs/cordis-tutorial/06-composition-and-hmr.md -06-composition-and-hmr.md: 87ea26014657ae8c8199e1ebb486556c827d96ca -06-composition-and-hmr.zh.md: 830f55de7c1be351fe701cb068197543602619a7 +06-composition-and-hmr.md: 2b53aa28be99851e77a71de76337f2beb4003e8d +06-composition-and-hmr.zh.md: cd4afa1d5465a5442bfd6b771ebcd61c46fe2983 diff --git a/docs/cordis-tutorial/06-composition-and-hmr.md b/docs/cordis-tutorial/06-composition-and-hmr.md index 87ea260146..2b53aa28be 100644 --- a/docs/cordis-tutorial/06-composition-and-hmr.md +++ b/docs/cordis-tutorial/06-composition-and-hmr.md @@ -18,7 +18,7 @@ A config entry accepts metadata beyond `name` and `config`: `id` gives the entry a stable identity so the loader can tell an edit to an existing entry apart from a removal plus an addition. `disabled: true` unmounts a plugin without deleting its entry — flip it back and the plugin (and everything PENDING on its services) loads again. -Groups nest a sub-list of entries that load and unload as one unit, and `isolate` gives a group its own instance of a service name — two groups can each see a differently-configured `bash` without affecting each other. The [Cordis primer](../cordis-primer.md) and the [service isolation example](../user/develop/framework/service.md#service-isolation) cover the details. +Groups nest a sub-list of entries that load and unload as one unit, and `isolate` gives a group its own instance of a service name — two groups can each see a differently configured `shell` provider without affecting each other. The [Cordis primer](../cordis-primer.md) and the [service isolation example](../user/develop/framework/service.md#service-isolation) cover the details. ## Hot module replacement diff --git a/docs/cordis-tutorial/06-composition-and-hmr.zh.md b/docs/cordis-tutorial/06-composition-and-hmr.zh.md index 830f55de7c..cd4afa1d54 100644 --- a/docs/cordis-tutorial/06-composition-and-hmr.zh.md +++ b/docs/cordis-tutorial/06-composition-and-hmr.zh.md @@ -18,7 +18,7 @@ Cordis 配置项除了 `name` 和 `config`,还接受其他元数据: `id` 为 Cordis 配置项提供稳定标识,使 loader 能区分修改现有 Cordis 配置项与先删除再添加。`disabled: true` 会卸载插件而不删除其 Cordis 配置项;改回原值后,插件以及所有因依赖其服务而处于 PENDING 的插件都会再次加载。 -组可以嵌套一份 Cordis 配置项子列表,并将其作为一个单元加载和卸载;`isolate` 则为一个组提供某项服务名称的独立实例,因此两个组可以各自看到配置不同的 `bash`,互不影响。[Cordis 入门](../cordis-primer.md)和[服务隔离示例](../user/develop/framework/service.md#service-isolation)介绍了详细内容。 +组可以嵌套一份 Cordis 配置项子列表,并将其作为一个单元加载和卸载;`isolate` 则为一个组提供某项服务名称的独立实例,因此两个组可以各自看到配置不同的 `shell` 提供方,互不影响。[Cordis 入门](../cordis-primer.md)和[服务隔离示例](../user/develop/framework/service.md#service-isolation)介绍了详细内容。 ## 热模块替换 diff --git a/docs/cordis-tutorial/index.i18n.yaml b/docs/cordis-tutorial/index.i18n.yaml index 9e18b6899e..68591da3e4 100644 --- a/docs/cordis-tutorial/index.i18n.yaml +++ b/docs/cordis-tutorial/index.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/cordis-tutorial/index.md index.md: c51965e186ce8d78b577c1005c1c3e1831b91be1 -index.zh.md: 8811930eba0f3e24ecbc47521e65582502257473 +index.zh.md: 22e1918672a34219ca0cf5efaa43a8eb693e6996 diff --git a/docs/cordis-tutorial/index.zh.md b/docs/cordis-tutorial/index.zh.md index 8811930eba..22e1918672 100644 --- a/docs/cordis-tutorial/index.zh.md +++ b/docs/cordis-tutorial/index.zh.md @@ -10,6 +10,8 @@ Cordis 是 DeepSeek Harness 底层的插件框架:它是一个小型运行时 如果你要为 harness 本身编写插件——由 `cordis.yml` 加载、在 Web UI 中驱动,而不是下面这个启动器——请从[第一个 Harness 插件](../user/develop/basic/index.md)开始。 + + ## 准备工作 你需要克隆本仓库并安装依赖;[开发指南](../development.md#setup-tutorial)列出了前置条件。本教程不需要 API 密钥;所有示例均可在无密钥环境中运行。 diff --git a/docs/persistence-catalog.i18n.yaml b/docs/persistence-catalog.i18n.yaml index 37859f5f2e..bc6e3dde66 100644 --- a/docs/persistence-catalog.i18n.yaml +++ b/docs/persistence-catalog.i18n.yaml @@ -2,5 +2,5 @@ # 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 docs/persistence-catalog.md -persistence-catalog.md: 032f7ae45d05b688fd317e9aa46363f658f19b76 -persistence-catalog.zh.md: 4f3ace46f7abb2d87be1cd66cbead0ac6b0cccda +persistence-catalog.md: c400298f7d37c590918820bcbda10e6550f197e8 +persistence-catalog.zh.md: 65ec0e3fbdd226c51a371dc9a90f10db5c929c7a diff --git a/docs/persistence-catalog.md b/docs/persistence-catalog.md index 032f7ae45d..c400298f7d 100644 --- a/docs/persistence-catalog.md +++ b/docs/persistence-catalog.md @@ -96,6 +96,8 @@ Sources: [`packages/core/session/src/types.ts:336`](../packages/core/session/src ### `agent/*` + + #### `agent/inbox/spliced` — log-only ```ts persistence-catalog @@ -117,6 +119,8 @@ Source: [`packages/core/agent/src/types.ts:19`](../packages/core/agent/src/types ### `agent-preset/*` + + #### `agent-preset/selected` — log-only ```ts persistence-catalog @@ -133,6 +137,8 @@ Source: [`packages/preset/agent-presets/src/session.ts:26`](../packages/preset/a ### `approval/*` + + #### `approval/asked` — log-only ```ts persistence-catalog @@ -156,6 +162,8 @@ Types: [CallId](subsystems/core.md) Source: [`packages/interaction/user-approval/src/index.ts:44`](../packages/interaction/user-approval/src/index.ts) + + #### `approval/decided` — log-only ```ts persistence-catalog @@ -172,6 +180,8 @@ Source: [`packages/interaction/user-approval/src/index.ts:44`](../packages/inter Source: [`packages/interaction/user-approval/src/index.ts:55`](../packages/interaction/user-approval/src/index.ts) + + #### `approval/policy` — log-only ```ts persistence-catalog @@ -194,6 +204,8 @@ Source: [`packages/interaction/user-approval/src/index.ts:67`](../packages/inter ### `assistant/*` + + #### `assistant/chunk` — log-only ```ts persistence-catalog @@ -205,6 +217,8 @@ Types: [StreamChunk](subsystems/llm-streaming.md) Source: [`packages/core/session/src/types.ts:266`](../packages/core/session/src/types.ts) + + #### `assistant/message` — surface ```ts persistence-catalog @@ -223,6 +237,8 @@ Source: [`packages/core/session/src/types.ts:273`](../packages/core/session/src/ ### `command/*` + + #### `command/done` — log-only ```ts persistence-catalog @@ -242,6 +258,8 @@ Source: [`packages/core/session/src/types.ts:273`](../packages/core/session/src/ Source: [`packages/interaction/commands/src/types.ts:95`](../packages/interaction/commands/src/types.ts) + + #### `command/run` — log-only ```ts persistence-catalog @@ -262,6 +280,8 @@ Source: [`packages/interaction/commands/src/types.ts:88`](../packages/interactio ### `compaction/*` + + #### `compaction/end` — log-only ```ts persistence-catalog @@ -274,6 +294,8 @@ Source: [`packages/interaction/commands/src/types.ts:88`](../packages/interactio Source: [`packages/compaction/compaction/src/types.ts:71`](../packages/compaction/compaction/src/types.ts) + + #### `compaction/prune` — log-only ```ts persistence-catalog @@ -298,6 +320,8 @@ Source: [`packages/compaction/compaction/src/types.ts:71`](../packages/compactio Source: [`packages/compaction/compaction/src/types.ts:81`](../packages/compaction/compaction/src/types.ts) + + #### `compaction/start` — log-only ```ts persistence-catalog @@ -311,6 +335,8 @@ Source: [`packages/compaction/compaction/src/types.ts:81`](../packages/compactio Source: [`packages/compaction/compaction/src/types.ts:23`](../packages/compaction/compaction/src/types.ts) + + #### `compaction/summary` — log-only ```ts persistence-catalog @@ -365,6 +391,8 @@ Source: [`packages/compaction/compaction/src/types.ts:33`](../packages/compactio ### `feedback/*` + + #### `feedback/record` — log-only ```ts persistence-catalog @@ -379,6 +407,8 @@ Source: [`packages/feedback/command-feedback/src/index.ts:62`](../packages/feedb ### `goal/*` + + #### `goal/change` — log-only ```ts persistence-catalog @@ -392,6 +422,8 @@ Source: [`packages/goal/goal/src/domain.ts:66`](../packages/goal/goal/src/domain ### `hook/*` + + #### `hook/invoked` — log-only ```ts persistence-catalog @@ -415,6 +447,8 @@ Source: [`packages/goal/goal/src/domain.ts:66`](../packages/goal/goal/src/domain Source: [`packages/hooks/hook-protocol/src/types.ts:19`](../packages/hooks/hook-protocol/src/types.ts) + + #### `hook/result` — log-only ```ts persistence-catalog @@ -438,6 +472,8 @@ Source: [`packages/hooks/hook-protocol/src/types.ts:31`](../packages/hooks/hook- ### `llm/*` + + #### `llm/retry` — log-only ```ts persistence-catalog @@ -447,6 +483,8 @@ Source: [`packages/hooks/hook-protocol/src/types.ts:31`](../packages/hooks/hook- Source: [`packages/llm/llm-retry/src/types.ts:9`](../packages/llm/llm-retry/src/types.ts) + + #### `llm/retry-started` — log-only ```ts persistence-catalog @@ -458,6 +496,8 @@ Source: [`packages/llm/llm-retry/src/types.ts:11`](../packages/llm/llm-retry/src ### `permission/*` + + #### `permission/preset` — log-only ```ts persistence-catalog @@ -474,6 +514,8 @@ Source: [`packages/interaction/permission-presets/src/index.ts:50`](../packages/ ### `plan/*` + + #### `plan/mode` — log-only ```ts persistence-catalog @@ -489,6 +531,8 @@ Source: [`packages/plan/plan-mode/src/index.ts:53`](../packages/plan/plan-mode/s ### `request/*` + + #### `request/context` — log-only ```ts persistence-catalog @@ -501,6 +545,8 @@ Source: [`packages/plan/plan-mode/src/index.ts:53`](../packages/plan/plan-mode/s Source: [`packages/core/session/src/types.ts:309`](../packages/core/session/src/types.ts) + + #### `request/header` — log-only ```ts persistence-catalog @@ -515,6 +561,8 @@ Source: [`packages/core/session/src/types.ts:304`](../packages/core/session/src/ ### `sandbox/*` + + #### `sandbox/mode` — log-only ```ts persistence-catalog @@ -536,6 +584,8 @@ Source: [`packages/sandbox/sandbox-policy/src/session-mode.ts:33`](../packages/s ### `schedule/*` + + #### `schedule/change` — log-only ```ts persistence-catalog @@ -552,6 +602,8 @@ Source: [`packages/schedule/schedule/src/types.ts:219`](../packages/schedule/sch ### `session/*` + + #### `session/end-seed` — log-only ```ts persistence-catalog @@ -582,6 +634,8 @@ Source: [`packages/schedule/schedule/src/types.ts:219`](../packages/schedule/sch Source: [`packages/core/session/src/types.ts:332`](../packages/core/session/src/types.ts) + + #### `session/title` — log-only ```ts persistence-catalog @@ -596,6 +650,8 @@ Types: [SessionTitleEventData](subsystems/session-title.md) Source: [`packages/session/session-title/src/index.ts:100`](../packages/session/session-title/src/index.ts) + + #### `session/title-llm-request` — log-only ```ts persistence-catalog @@ -609,6 +665,8 @@ Source: [`packages/session/session-title-llm/src/index.ts:43`](../packages/sessi ### `step/*` + + #### `step/end` — log-only ```ts persistence-catalog @@ -618,6 +676,8 @@ Source: [`packages/session/session-title-llm/src/index.ts:43`](../packages/sessi Source: [`packages/core/session/src/types.ts:256`](../packages/core/session/src/types.ts) + + #### `step/start` — log-only ```ts persistence-catalog @@ -629,6 +689,8 @@ Source: [`packages/core/session/src/types.ts:254`](../packages/core/session/src/ ### `subagent/*` + + #### `subagent/descriptor` — log-only ```ts persistence-catalog @@ -646,6 +708,8 @@ Source: [`packages/subagent/subagent/src/descriptor.ts:37`](../packages/subagent ### `todo/*` + + #### `todo/write` — log-only ```ts persistence-catalog @@ -659,6 +723,8 @@ Source: [`packages/core/session/src/types.ts:299`](../packages/core/session/src/ ### `tool/*` + + #### `tool/call` — log-only ```ts persistence-catalog @@ -674,6 +740,8 @@ Types: [CallId](subsystems/core.md) Source: [`packages/core/session/src/types.ts:279`](../packages/core/session/src/types.ts) + + #### `tool/code-dispatch` — log-only ```ts persistence-catalog @@ -697,6 +765,8 @@ Source: [`packages/core/session/src/types.ts:279`](../packages/core/session/src/ Source: [`packages/core/tools/src/types.ts:56`](../packages/core/tools/src/types.ts) + + #### `tool/code-dispatch-start` — log-only ```ts persistence-catalog @@ -718,6 +788,8 @@ Source: [`packages/core/tools/src/types.ts:56`](../packages/core/tools/src/types Source: [`packages/core/tools/src/types.ts:40`](../packages/core/tools/src/types.ts) + + #### `tool/result` — surface ```ts persistence-catalog @@ -745,6 +817,8 @@ Source: [`packages/core/session/src/types.ts:291`](../packages/core/session/src/ ### `tool-workflow/*` + + #### `tool-workflow/agent-end` — log-only ```ts persistence-catalog @@ -757,6 +831,8 @@ Source: [`packages/core/session/src/types.ts:291`](../packages/core/session/src/ Source: [`packages/workflow/tool-workflow/src/types.ts:57`](../packages/workflow/tool-workflow/src/types.ts) + + #### `tool-workflow/agent-start` — log-only ```ts persistence-catalog @@ -769,6 +845,8 @@ Source: [`packages/workflow/tool-workflow/src/types.ts:57`](../packages/workflow Source: [`packages/workflow/tool-workflow/src/types.ts:52`](../packages/workflow/tool-workflow/src/types.ts) + + #### `tool-workflow/run-end` — log-only ```ts persistence-catalog @@ -781,6 +859,8 @@ Source: [`packages/workflow/tool-workflow/src/types.ts:52`](../packages/workflow Source: [`packages/workflow/tool-workflow/src/types.ts:62`](../packages/workflow/tool-workflow/src/types.ts) + + #### `tool-workflow/run-start` — log-only ```ts persistence-catalog @@ -795,6 +875,8 @@ Source: [`packages/workflow/tool-workflow/src/types.ts:47`](../packages/workflow ### `turn/*` + + #### `turn/end` — log-only ```ts persistence-catalog @@ -813,6 +895,8 @@ Types: [TurnEndReason](subsystems/session.md) Source: [`packages/core/session/src/types.ts:252`](../packages/core/session/src/types.ts) + + #### `turn/start` — log-only ```ts persistence-catalog @@ -829,6 +913,8 @@ Source: [`packages/core/session/src/types.ts:243`](../packages/core/session/src/ ### `user/*` + + #### `user/message` — surface ```ts persistence-catalog @@ -846,6 +932,8 @@ Source: [`packages/core/session/src/types.ts:264`](../packages/core/session/src/ ### `web/*` + + #### `web/deepseek-search-llm-request` — log-only ```ts persistence-catalog diff --git a/docs/persistence-catalog.zh.md b/docs/persistence-catalog.zh.md index 4f3ace46f7..65ec0e3fbd 100644 --- a/docs/persistence-catalog.zh.md +++ b/docs/persistence-catalog.zh.md @@ -98,6 +98,8 @@ export type SessionEvent = { ### `agent/*` + + #### `agent/inbox/spliced` — log-only ```ts persistence-catalog @@ -119,6 +121,8 @@ export type SessionEvent = { ### `agent-preset/*` + + #### `agent-preset/selected` — log-only ```ts persistence-catalog @@ -135,6 +139,8 @@ export type SessionEvent = { ### `approval/*` + + #### `approval/asked` — log-only ```ts persistence-catalog @@ -158,6 +164,8 @@ export type SessionEvent = { 来源:[`packages/interaction/user-approval/src/index.ts:44`](../packages/interaction/user-approval/src/index.ts) + + #### `approval/decided` — log-only ```ts persistence-catalog @@ -174,6 +182,8 @@ export type SessionEvent = { 来源:[`packages/interaction/user-approval/src/index.ts:55`](../packages/interaction/user-approval/src/index.ts) + + #### `approval/policy` — log-only ```ts persistence-catalog @@ -196,6 +206,8 @@ export type SessionEvent = { ### `assistant/*` + + #### `assistant/chunk` — log-only ```ts persistence-catalog @@ -207,6 +219,8 @@ export type SessionEvent = { 来源:[`packages/core/session/src/types.ts:266`](../packages/core/session/src/types.ts) + + #### `assistant/message` — surface ```ts persistence-catalog @@ -225,6 +239,8 @@ export type SessionEvent = { ### `command/*` + + #### `command/done` — log-only ```ts persistence-catalog @@ -244,6 +260,8 @@ export type SessionEvent = { 来源:[`packages/interaction/commands/src/types.ts:95`](../packages/interaction/commands/src/types.ts) + + #### `command/run` — log-only ```ts persistence-catalog @@ -264,6 +282,8 @@ export type SessionEvent = { ### `compaction/*` + + #### `compaction/end` — log-only ```ts persistence-catalog @@ -276,6 +296,8 @@ export type SessionEvent = { 来源:[`packages/compaction/compaction/src/types.ts:71`](../packages/compaction/compaction/src/types.ts) + + #### `compaction/prune` — log-only ```ts persistence-catalog @@ -300,6 +322,8 @@ export type SessionEvent = { 来源:[`packages/compaction/compaction/src/types.ts:81`](../packages/compaction/compaction/src/types.ts) + + #### `compaction/start` — log-only ```ts persistence-catalog @@ -313,6 +337,8 @@ export type SessionEvent = { 来源:[`packages/compaction/compaction/src/types.ts:23`](../packages/compaction/compaction/src/types.ts) + + #### `compaction/summary` — log-only ```ts persistence-catalog @@ -367,6 +393,8 @@ export type SessionEvent = { ### `feedback/*` + + #### `feedback/record` — log-only ```ts persistence-catalog @@ -381,6 +409,8 @@ export type SessionEvent = { ### `goal/*` + + #### `goal/change` — log-only ```ts persistence-catalog @@ -394,6 +424,8 @@ export type SessionEvent = { ### `hook/*` + + #### `hook/invoked` — log-only ```ts persistence-catalog @@ -417,6 +449,8 @@ export type SessionEvent = { 来源:[`packages/hooks/hook-protocol/src/types.ts:19`](../packages/hooks/hook-protocol/src/types.ts) + + #### `hook/result` — log-only ```ts persistence-catalog @@ -440,6 +474,8 @@ export type SessionEvent = { ### `llm/*` + + #### `llm/retry` — log-only ```ts persistence-catalog @@ -449,6 +485,8 @@ export type SessionEvent = { 来源:[`packages/llm/llm-retry/src/types.ts:9`](../packages/llm/llm-retry/src/types.ts) + + #### `llm/retry-started` — log-only ```ts persistence-catalog @@ -460,6 +498,8 @@ export type SessionEvent = { ### `permission/*` + + #### `permission/preset` — log-only ```ts persistence-catalog @@ -476,6 +516,8 @@ export type SessionEvent = { ### `plan/*` + + #### `plan/mode` — log-only ```ts persistence-catalog @@ -491,6 +533,8 @@ export type SessionEvent = { ### `request/*` + + #### `request/context` — log-only ```ts persistence-catalog @@ -503,6 +547,8 @@ export type SessionEvent = { 来源:[`packages/core/session/src/types.ts:309`](../packages/core/session/src/types.ts) + + #### `request/header` — log-only ```ts persistence-catalog @@ -517,6 +563,8 @@ export type SessionEvent = { ### `sandbox/*` + + #### `sandbox/mode` — log-only ```ts persistence-catalog @@ -538,6 +586,8 @@ export type SessionEvent = { ### `schedule/*` + + #### `schedule/change` — log-only ```ts persistence-catalog @@ -554,6 +604,8 @@ export type SessionEvent = { ### `session/*` + + #### `session/end-seed` — log-only ```ts persistence-catalog @@ -584,6 +636,8 @@ export type SessionEvent = { 来源:[`packages/core/session/src/types.ts:332`](../packages/core/session/src/types.ts) + + #### `session/title` — log-only ```ts persistence-catalog @@ -598,6 +652,8 @@ export type SessionEvent = { 来源:[`packages/session/session-title/src/index.ts:100`](../packages/session/session-title/src/index.ts) + + #### `session/title-llm-request` — log-only ```ts persistence-catalog @@ -611,6 +667,8 @@ export type SessionEvent = { ### `step/*` + + #### `step/end` — log-only ```ts persistence-catalog @@ -620,6 +678,8 @@ export type SessionEvent = { 来源:[`packages/core/session/src/types.ts:256`](../packages/core/session/src/types.ts) + + #### `step/start` — log-only ```ts persistence-catalog @@ -631,6 +691,8 @@ export type SessionEvent = { ### `subagent/*` + + #### `subagent/descriptor` — log-only ```ts persistence-catalog @@ -648,6 +710,8 @@ export type SessionEvent = { ### `todo/*` + + #### `todo/write` — log-only ```ts persistence-catalog @@ -661,6 +725,8 @@ export type SessionEvent = { ### `tool/*` + + #### `tool/call` — log-only ```ts persistence-catalog @@ -676,6 +742,8 @@ export type SessionEvent = { 来源:[`packages/core/session/src/types.ts:279`](../packages/core/session/src/types.ts) + + #### `tool/code-dispatch` — log-only ```ts persistence-catalog @@ -699,6 +767,8 @@ export type SessionEvent = { 来源:[`packages/core/tools/src/types.ts:56`](../packages/core/tools/src/types.ts) + + #### `tool/code-dispatch-start` — log-only ```ts persistence-catalog @@ -720,6 +790,8 @@ export type SessionEvent = { 来源:[`packages/core/tools/src/types.ts:40`](../packages/core/tools/src/types.ts) + + #### `tool/result` — surface ```ts persistence-catalog @@ -747,6 +819,8 @@ export type SessionEvent = { ### `tool-workflow/*` + + #### `tool-workflow/agent-end` — log-only ```ts persistence-catalog @@ -759,6 +833,8 @@ export type SessionEvent = { 来源:[`packages/workflow/tool-workflow/src/types.ts:57`](../packages/workflow/tool-workflow/src/types.ts) + + #### `tool-workflow/agent-start` — log-only ```ts persistence-catalog @@ -771,6 +847,8 @@ export type SessionEvent = { 来源:[`packages/workflow/tool-workflow/src/types.ts:52`](../packages/workflow/tool-workflow/src/types.ts) + + #### `tool-workflow/run-end` — log-only ```ts persistence-catalog @@ -783,6 +861,8 @@ export type SessionEvent = { 来源:[`packages/workflow/tool-workflow/src/types.ts:62`](../packages/workflow/tool-workflow/src/types.ts) + + #### `tool-workflow/run-start` — log-only ```ts persistence-catalog @@ -797,6 +877,8 @@ export type SessionEvent = { ### `turn/*` + + #### `turn/end` — log-only ```ts persistence-catalog @@ -815,6 +897,8 @@ export type SessionEvent = { 来源:[`packages/core/session/src/types.ts:252`](../packages/core/session/src/types.ts) + + #### `turn/start` — log-only ```ts persistence-catalog @@ -831,6 +915,8 @@ export type SessionEvent = { ### `user/*` + + #### `user/message` — surface ```ts persistence-catalog @@ -848,6 +934,8 @@ export type SessionEvent = { ### `web/*` + + #### `web/deepseek-search-llm-request` — log-only ```ts persistence-catalog diff --git a/docs/subsystems/core.i18n.yaml b/docs/subsystems/core.i18n.yaml index 041f874d47..1265b726fe 100644 --- a/docs/subsystems/core.i18n.yaml +++ b/docs/subsystems/core.i18n.yaml @@ -2,5 +2,5 @@ # 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 docs/subsystems/core.md -core.md: 6eda80cbd164168c3c4a846c9bc50a8fc12b0c92 -core.zh.md: 14c2e78bc3fff959fb0c0040eae0f382985ecdf4 +core.md: d14ad52e57572d5b0b110a0b16ff734e3499bdf5 +core.zh.md: 9ace28731f2532f571b66f2e7f6a3ea4a2c4e345 diff --git a/docs/subsystems/core.md b/docs/subsystems/core.md index 6eda80cbd1..d14ad52e57 100644 --- a/docs/subsystems/core.md +++ b/docs/subsystems/core.md @@ -257,6 +257,8 @@ Its full fields, the `defineTool`/`ValueSchemaSpec`/`ParameterSchemaSpec` typed Two patterns recur across every subsystem and are documented once, here. + + ### The `…Map → derived-union` pattern Almost every extensible sum type in the harness follows one pattern: an interface keyed by a discriminant tag (the `…Map`), from which the union is derived with `keyof`. Plugins add variants by **declaration merging** — no edit to the owning package. diff --git a/docs/subsystems/core.zh.md b/docs/subsystems/core.zh.md index 14c2e78bc3..9ace28731f 100644 --- a/docs/subsystems/core.zh.md +++ b/docs/subsystems/core.zh.md @@ -263,6 +263,8 @@ type SessionStartSource = 'startup' | 'resume' | 'clear' | 'compact' 两个模式在每个子系统中反复出现,只在此处记录一次。 + + ### `…Map → derived-union` 模式 harness 中几乎所有可扩展的和类型都遵循同一模式:一个以判别标签为键的接口(`…Map`),联合类型由 `keyof` 派生。插件通过**声明合并**添加变体——无需修改拥有该类型的包。 diff --git a/docs/subsystems/llm-streaming.i18n.yaml b/docs/subsystems/llm-streaming.i18n.yaml index d1d2810cfe..5708a7b6d5 100644 --- a/docs/subsystems/llm-streaming.i18n.yaml +++ b/docs/subsystems/llm-streaming.i18n.yaml @@ -2,5 +2,5 @@ # 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 docs/subsystems/llm-streaming.md -llm-streaming.md: 0f395245332e735c04997bd1ca82f66fa9286104 +llm-streaming.md: 0d3a0d53c875c9d943146ba44b775d81fc9cae01 llm-streaming.zh.md: fbaa47d14d57e7377be4db6ecaa04f11997572a6 diff --git a/docs/subsystems/llm-streaming.md b/docs/subsystems/llm-streaming.md index 0f39524533..0d3a0d53c8 100644 --- a/docs/subsystems/llm-streaming.md +++ b/docs/subsystems/llm-streaming.md @@ -151,6 +151,8 @@ type ContextFormed = | { readonly form: 'recall' } ``` + + ## `StreamChunk` — the raw protocol A streaming response interleaves several typed blocks (text, reasoning, multiple tool calls). `index` ties each delta to its block; `block-end` carries the fully-assembled `ContentBlock` so consumers don't have to re-assemble deltas themselves. It is a **closed** discriminated union — a `switch` over `type` ends with `assertNever`, so adding a variant breaks compilation at every consumer that must handle it. diff --git a/docs/subsystems/persistence.i18n.yaml b/docs/subsystems/persistence.i18n.yaml index 6d0ca447a2..2950610348 100644 --- a/docs/subsystems/persistence.i18n.yaml +++ b/docs/subsystems/persistence.i18n.yaml @@ -2,5 +2,5 @@ # 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 docs/subsystems/persistence.md -persistence.md: fde8348d64a200eda5133abf66deedee6be09857 -persistence.zh.md: a2a836d81d153a2697c1d739345501e7e6770eef +persistence.md: 5b1b224e419aca205baba69894ed64467b8fb4e1 +persistence.zh.md: a91e7d66b92270c82287d054e619b665e96ea206 diff --git a/docs/subsystems/persistence.md b/docs/subsystems/persistence.md index fde8348d64..5b1b224e41 100644 --- a/docs/subsystems/persistence.md +++ b/docs/subsystems/persistence.md @@ -36,6 +36,8 @@ interface SessionLocation { } ``` + + ## `SessionHeader` — metadata beside the log Per-session metadata travels **separately** from the event log: format version, cwd, lineage, and the seed boundary are storage concerns, not conversation events, so they stay out of `SessionEventMap` and never reach `deriveMessages()`. The header is attached to a `Session` via `session.header`. diff --git a/docs/subsystems/persistence.zh.md b/docs/subsystems/persistence.zh.md index a2a836d81d..a91e7d66b9 100644 --- a/docs/subsystems/persistence.zh.md +++ b/docs/subsystems/persistence.zh.md @@ -36,6 +36,8 @@ interface SessionLocation { } ``` + + ## `SessionHeader`:日志旁的元数据 每个会话的元数据与事件日志**分开**存储:格式版本、cwd、血统与 seed 边界是存储层关注点而非对话事件,因此不进入 `SessionEventMap`,也不会到达 `deriveMessages()`。header 通过 `session.header` 附加到 `Session` 上。 diff --git a/docs/subsystems/session.i18n.yaml b/docs/subsystems/session.i18n.yaml index 8ee4185e54..5eacc07962 100644 --- a/docs/subsystems/session.i18n.yaml +++ b/docs/subsystems/session.i18n.yaml @@ -2,5 +2,5 @@ # 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 docs/subsystems/session.md -session.md: 4c40971fe58952b32635aa5eb767a42f73108b00 +session.md: 1741b0dbf8b79187e2777cba17dbfb7c5a698b94 session.zh.md: d958b03fcdad91b58cc277636e599cce46a2c7fb diff --git a/docs/subsystems/session.md b/docs/subsystems/session.md index 4c40971fe5..1741b0dbf8 100644 --- a/docs/subsystems/session.md +++ b/docs/subsystems/session.md @@ -149,6 +149,8 @@ interface TodoItem { } ``` + + ### The request header event: `request/header` The request envelope — the `EpochHeader` (call config + markers for adapter-supplied defaults + rendered system prompt + assembled tool schemas) — is logged session state, so every conversation request is a pure function of the log (the reconstructability Agent Note). A full `request/header` snapshot with reason `'initial'` or `'resume'` records each loop-instance boundary; a later changed request records another full snapshot with reason `'change'`. `foldRequestHeader(events)` reconstructs the header by selecting the latest snapshot. The event is not a `SurfaceEventType`: it produces no LLM message. diff --git a/docs/tool-catalog.i18n.yaml b/docs/tool-catalog.i18n.yaml index a2083a076c..3ed16072ca 100644 --- a/docs/tool-catalog.i18n.yaml +++ b/docs/tool-catalog.i18n.yaml @@ -2,5 +2,5 @@ # 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 docs/tool-catalog.md -tool-catalog.md: 3d73ed1ef5346f620808e9d6b04299291ba1ed4c -tool-catalog.zh.md: 90573928a24630e802d98441bc0e340baf582b23 +tool-catalog.md: 50563c97c6cd5496871ea7fa52c4823a56b088fd +tool-catalog.zh.md: ed0c7e3f70cffbecd3d20a1556bcb0cd4204df00 diff --git a/docs/tool-catalog.md b/docs/tool-catalog.md index 3d73ed1ef5..50563c97c6 100644 --- a/docs/tool-catalog.md +++ b/docs/tool-catalog.md @@ -40,6 +40,8 @@ This table connects model-visible tool names to the plugin package and service s | `@deepseek-ai/dsh-tool-workflow` | `workflow` | `ctx.tools`, `ctx.workflowEngine`, `ctx.systemPrompt`, `a calling Agent (exec.agent parents the script children)` | `tool/call`, `tool/result` | - | - | | `@deepseek-ai/dsh-tool-web` | `web_fetch`, `web_search` | `ctx.tools`, `ctx.web`, `ctx.systemPrompt` | `tool/call`, `tool/result` | - | web_search and web_fetch keep provider selection behind ctx.web so model-visible schemas stay stable across backend swaps. | + + ## `@deepseek-ai/dsh-tool-ask-user` ### `ask_user_question` @@ -112,6 +114,8 @@ Source: [`packages/interaction/tool-ask-user/src/index.ts`](../packages/interact ask_user_question pauses the tool call until the active UI provider returns a human answer. + + ## `@deepseek-ai/dsh-tools` ### `run_code` @@ -142,6 +146,8 @@ Source: [`packages/core/tools/src/code-mode.ts`](../packages/core/tools/src/code Owned by the tool registry as a reserved transport outside filterable capability layers under `mode: code` / `mode: both` (see the Code Mode Agent Note). Under `code` it is the registry's only wire contribution; the other visible capabilities are declared in a generated SDK section in the loaded runtime's language, and a program calls them through bindings scheduled under the native concurrency contract (submission-ordered starts and policy; concurrency-safe bodies overlap up to `maxParallelSubCalls`) that re-enter the complete guarded tool pipeline and link each nested execution to this outer result. + + ## `@deepseek-ai/dsh-plan-mode` ### `exit_plan_mode` @@ -167,6 +173,8 @@ Source: [`packages/plan/plan-mode/src/index.ts`](../packages/plan/plan-mode/src/ exit_plan_mode stays in the model-facing schema while planning is inactive so transitions add no tool-catalog churn on top of the plan-policy change. Its execute path rejects calls outside plan mode; in plan mode it presents the plan over the user-questions seam (approve / keep planning with feedback), and approval logs plan mode inactive at the step boundary. + + ## `@deepseek-ai/dsh-tool-bash` ### `bash` @@ -209,6 +217,8 @@ Source: [`packages/shell/tool-bash/src/index.ts`](../packages/shell/tool-bash/sr The bash tool is the model-facing consumer of the bash executor seam. A `run_in_background` run registers with the generic `ctx.jobs` runtime and is collected/stopped through the `job_*` tools from `@deepseek-ai/dsh-tool-jobs`; the `enableRunInBackground` config (default true) removes the parameter entirely when disabled. + + ## `@deepseek-ai/dsh-tool-pwsh` ### `pwsh` @@ -251,6 +261,8 @@ Source: [`packages/shell/tool-pwsh/src/index.ts`](../packages/shell/tool-pwsh/sr The pwsh tool is the PowerShell-dialect consumer of the bash executor seam for Windows compositions (a PowerShell executor such as `@deepseek-ai/dsh-pwsh-local` backs `ctx.shell`); it mirrors the bash tool call-for-call minus sandbox controls — `run_in_background` runs register with the generic `ctx.jobs` runtime and are collected/stopped through the `job_*` tools, and the managed `DSH_*` environment comes from `@deepseek-ai/dsh-shell-env`. Each call runs in a fresh process (no persistent PTY session), with native `C:\...` paths and `$env:NAME` variables. + + ## `@deepseek-ai/dsh-tool-cordis` ### `cordis_define` @@ -487,6 +499,8 @@ Source: [`packages/extensions/tool-cordis/src/index.ts`](../packages/extensions/ Not in any shipped tree (a deliberate opt-in — dynamic package code reaches the real runtime, see .agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.md). The toolset injects `ctx.dynamicCordisRunner` from `@deepseek-ai/dsh-cordis-host-runner`, which owns the definition registry and the vm sandbox; a composition missing it never activates the tools. A running package may register ADDITIONAL model-visible tools until it is stopped, undefined, or DSH restarts; a full changed request header logs those tool-set changes. + + ## `@deepseek-ai/dsh-tool-bash-persistent` ### `bash` @@ -512,6 +526,8 @@ Source: [`packages/shell/tool-bash-persistent/src/index.ts`](../packages/shell/t One owner-isolated persistent bash tool; deployment composition supplies the PTY backend and may override the model-facing environment description. + + ## `@deepseek-ai/dsh-tool-str-replace-editor` ### `str_replace_editor` @@ -580,6 +596,8 @@ Source: [`packages/fs/tool-str-replace-editor/src/index.ts`](../packages/fs/tool Standalone view/create/unique literal replace/line insert tool over the filesystem seam; it composes with any shell or terminal API. + + ## `@deepseek-ai/dsh-tool-fs` ### `edit` @@ -695,6 +713,8 @@ Source: [`packages/fs/tool-fs/src/index.ts`](../packages/fs/tool-fs/src/index.ts The read-before-write/edit policy is added by `@deepseek-ai/dsh-fs-observation-policy` (an `fs/*` event-gate plugin, no schema change); a deployment that loads these tools is expected to also load it. `read_image` is not registered without `ctx.attachments`; its schema is route-independent, and execution refuses unless the exact routed model declares image input. + + ## `@deepseek-ai/dsh-tool-fs-search` ### `glob` @@ -753,6 +773,8 @@ Source: [`packages/fs/tool-fs-search/src/index.ts`](../packages/fs/tool-fs-searc glob and grep are unconditional discovery tools that spawn the packaged ripgrep binary (`@vscode/ripgrep`) through ctx.subprocess as ordinary foreground calls (never background jobs) — no host `rg` install and no shell layer. The catalog uses `sampleOverCapGlobResults: true`; deployments must choose that behavior explicitly. Capped results save the complete formatted list through the optional ctx.spillStore backend; returned locators are follow-up-readable/searchable when the backend exposes local paths in co-located deployments. + + ## `@deepseek-ai/dsh-tool-terminal` ### `terminal_close` @@ -916,6 +938,8 @@ Source: [`packages/terminal/tool-terminal/src/index.ts`](../packages/terminal/to The six terminal tools are opt-in and complement one-shot shell/filesystem tools. `terminal_send(run_in_background: true)` registers with `ctx.jobs`; TUI, named key sequences, BEL, resize, auto-start, and cross-agent sharing are absent from the schema. + + ## `@deepseek-ai/dsh-tool-goal` ### `create_goal` @@ -1008,6 +1032,8 @@ Source: [`packages/goal/tool-goal/src/index.ts`](../packages/goal/tool-goal/src/ create, edit, pause, and resume require direct-human root authority; complete and blocked also accept the exact current goal round. The default blocked lower bound is three admitted rounds. + + ## `@deepseek-ai/dsh-schedule` ### `schedule_create` @@ -1103,6 +1129,8 @@ Source: [`packages/schedule/schedule/src/tools.ts`](../packages/schedule/schedul Registered only inside live root Agent scopes created after the opt-in Schedule plugin loads. Version 1 accepts after_seconds, explicit absolute at, and bounded fixed-rate every_seconds, and discloses session-local delivery; management reads and mutations require the shared Session persistence barrier. + + ## `@deepseek-ai/dsh-tool-lsp` ### `lsp` @@ -1149,6 +1177,8 @@ Source: [`packages/lsp/tool-lsp/src/index.ts`](../packages/lsp/tool-lsp/src/inde The lsp tool keeps provider selection and language-server subprocesses behind ctx.lsp, so its model-visible schema stays stable across providers. Requires a registered provider (e.g. `@deepseek-ai/dsh-lsp-stdio`) at runtime; without one, a query returns the structured `LSP_UNAVAILABLE` error rather than changing the schema. + + ## `@deepseek-ai/dsh-tool-ralph` ### `ralph` @@ -1178,6 +1208,8 @@ Source: [`packages/workflow/tool-ralph/src/index.ts`](../packages/workflow/tool- A fixed foreground workflow starts one fresh structured child per round; the model selects only the immutable objective and an optional round cap. + + ## `@deepseek-ai/dsh-tool-skill` ### `skill` @@ -1201,6 +1233,8 @@ Load the full instructions for an available skill. Call this with the exact skil Source: [`packages/skill/tool-skill/src/index.ts`](../packages/skill/tool-skill/src/index.ts) + + ## `@deepseek-ai/dsh-tool-session-query` ### `session_event_read` @@ -1434,6 +1468,8 @@ Source: [`packages/session-query/tool-session-query/src/index.ts`](../packages/s The five read-only tools hide provider cursors and authorize every result from the immutable calling agent session. The package is opt-in; compositions that need enforced deadlines or bounded inline output also mount the generic timeout or spill policies. + + ## `@deepseek-ai/dsh-tool-subagent` ### `subagent` @@ -1468,6 +1504,8 @@ Source: [`packages/subagent/tool-subagent/src/index.ts`](../packages/subagent/to The registered tool name is the load-time `toolName` config (default `subagent`); the schema above is that default. The shipped compositions load this package once per subagent backend, so the model additionally sees `subagent_fork` bound to the fork backend. Each instance's description, `run_in_background` parameter, and system-prompt policy follow its own `backgroundMode` and `enableRunInBackground`, so the two shipped schemas are not identical: `subagent` is `continuable` and defaults omitted calls to background with automatic settlement delivery, while `subagent_fork` stays `one-shot` and defaults them to foreground — see `packages/bundle/base/cordis.patch.yml` and `examples/acp-agent/cordis.yml`. + + ## `@deepseek-ai/dsh-tool-subagent-control` ### `interrupt_agent` @@ -1541,6 +1579,8 @@ Source: [`packages/subagent/tool-subagent-control/src/index.ts`](../packages/sub The globally named control tools over continuable background subagents: provider-bound `tool-subagent` instances register distinct delegation tools, while this package registers `send_message` and `interrupt_agent` once, plus `list_agents` from its separately loaded `/list-agents` plugin (whose catalog rows use the sessionProjections and live Agent registries). + + ## `@deepseek-ai/dsh-tool-subagent-report` ### `report` @@ -1566,6 +1606,8 @@ Source: [`packages/subagent/tool-subagent-report/src/index.ts`](../packages/suba Registered per continuable in-process child rather than globally, so this schema is visible only inside such a child and survives its global `toolFilter`. The same contribution installs the child-scoped `tool:report` prompt section, which this catalog does not render. The parent-facing `send_message` tool is installed independently. + + ## `@deepseek-ai/dsh-tool-jobs` ### `job_kill` @@ -1637,6 +1679,8 @@ Source: [`packages/jobs/tool-jobs/src/index.ts`](../packages/jobs/tool-jobs/src/ The kind-agnostic background-job controller: background bash commands, PTY sends, and subagents are read, listed, and killed through the same three tools. Loading the plugin attaches the controller that arms producers' `ctx.jobs.start()`. + + ## `@deepseek-ai/dsh-tool-todo` ### `todo_write` @@ -1685,6 +1729,8 @@ Source: [`packages/todo/tool-todo/src/index.ts`](../packages/todo/tool-todo/src/ todo_write is session-owned state; UIs render the latest todo/write event as a checklist. `allowParallelInProgress` is required with no default, so the catalog states its choice: `true`, whose description invites several `in_progress` items. A deployment choosing `false` receives the same tool with a description asking for exactly one active task. + + ## `@deepseek-ai/dsh-tool-workflow` ### `workflow` @@ -1778,6 +1824,8 @@ Constraints: concurrency and total-agent caps apply; no filesystem, network, tim Source: [`packages/workflow/tool-workflow/src/index.ts`](../packages/workflow/tool-workflow/src/index.ts) + + ## `@deepseek-ai/dsh-tool-web` ### `web_fetch` diff --git a/docs/tool-catalog.zh.md b/docs/tool-catalog.zh.md index 90573928a2..ed0c7e3f70 100644 --- a/docs/tool-catalog.zh.md +++ b/docs/tool-catalog.zh.md @@ -42,6 +42,8 @@ | `@deepseek-ai/dsh-tool-workflow` | `workflow` | `ctx.tools`、`ctx.workflowEngine`、`ctx.systemPrompt`、`a calling Agent (exec.agent parents the script children)` | `tool/call`、`tool/result` | - | - | | `@deepseek-ai/dsh-tool-web` | `web_fetch`、`web_search` | `ctx.tools`、`ctx.web`、`ctx.systemPrompt` | `tool/call`、`tool/result` | - | web_search 和 web_fetch 将提供方选择置于 ctx.web 之后,使模型可见 schema 在更换后端时保持稳定。 | + + ## `@deepseek-ai/dsh-tool-ask-user` ### `ask_user_question` @@ -114,6 +116,8 @@ ask_user_question 会暂停工具调用,直到当前 UI 提供方返回人类答案。 + + ## `@deepseek-ai/dsh-tools` ### `run_code` @@ -144,6 +148,8 @@ ask_user_question 会暂停工具调用,直到当前 UI 提供方返回人类 在 `mode: code`/`mode: both` 下,它由工具注册表所有,作为可过滤能力层之外的保留传输机制(参见 Code Mode Agent Note)。在 `code` 下,它是注册表对协议格式的唯一贡献;其他可见能力在使用已加载运行时语言生成的 SDK 章节中声明。程序通过 binding 调用这些能力,调用按照原生并发约定调度:启动顺序和策略遵循提交顺序,并发安全的函数体最多重叠执行 `maxParallelSubCalls` 个。调用会重新进入完整且受守卫保护的工具流水线,并将每个嵌套执行关联到此外层结果。 + + ## `@deepseek-ai/dsh-plan-mode` ### `exit_plan_mode` @@ -169,6 +175,8 @@ ask_user_question 会暂停工具调用,直到当前 UI 提供方返回人类 规划未激活时,exit_plan_mode 仍保留在面向模型的 schema 中,这样状态转换不会在规划策略变更之外额外造成工具目录变动。其执行路径会拒绝规划模式之外的调用;在规划模式下,它通过用户交互 seam 提交计划(批准/根据反馈继续规划),批准后会在步骤边界记录规划模式已停用。 + + ## `@deepseek-ai/dsh-tool-bash` ### `bash` @@ -211,6 +219,8 @@ ask_user_question 会暂停工具调用,直到当前 UI 提供方返回人类 bash 工具是 bash 执行器 seam 面向模型的消费方。使用 `run_in_background` 的运行会注册到通用 `ctx.jobs` 运行时,并通过 `job_*` 工具(来自 `@deepseek-ai/dsh-tool-jobs`)收集/停止;禁用 `enableRunInBackground` 配置(默认为 true)后,该参数会被完全移除。 + + ## `@deepseek-ai/dsh-tool-pwsh` ### `pwsh` @@ -253,6 +263,8 @@ bash 工具是 bash 执行器 seam 面向模型的消费方。使用 `run_in_bac pwsh 工具是 Windows 组合中 bash 执行器 seam 的 PowerShell 方言消费方(由 `@deepseek-ai/dsh-pwsh-local` 等 PowerShell 执行器为 `ctx.shell` 提供后端);除沙箱接口外,它逐项对应 bash 工具调用。使用 `run_in_background` 的运行会注册到通用 `ctx.jobs` 运行时,并通过 `job_*` 工具收集/停止;托管的 `DSH_*` 环境来自 `@deepseek-ai/dsh-shell-env`。每次调用都在新进程中运行,不使用持久 PTY 会话。路径采用原生 `C:\...` 形式,变量采用 `$env:NAME`。 + + ## `@deepseek-ai/dsh-tool-cordis` ### `cordis_define` @@ -489,6 +501,8 @@ pwsh 工具是 Windows 组合中 bash 执行器 seam 的 PowerShell 方言消费 不在任何随产品发布的树中,需要显式选择启用;动态 Package 代码可以访问真实运行时,见 .agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.md。该工具集注入 `@deepseek-ai/dsh-cordis-host-runner` 提供的 `ctx.dynamicCordisRunner`,后者拥有定义注册表和 vm 沙箱;组合缺少它时这些工具不会激活。运行中的 Package 在停止、undefine 或 DSH 重启前可以注册**额外的**模型可见工具;发生这类工具集变化时,系统会记录完整且有变动的请求头。 + + ## `@deepseek-ai/dsh-tool-bash-persistent` ### `bash` @@ -514,6 +528,8 @@ pwsh 工具是 Windows 组合中 bash 执行器 seam 的 PowerShell 方言消费 一个按所有者隔离的持久 bash 工具;部署组合提供 PTY 后端,并可覆盖面向模型的环境描述。 + + ## `@deepseek-ai/dsh-tool-str-replace-editor` ### `str_replace_editor` @@ -584,6 +600,8 @@ pwsh 工具是 Windows 组合中 bash 执行器 seam 的 PowerShell 方言消费 基于文件系统 seam 的独立查看/创建/唯一字面量替换/按行插入工具;可与任何 shell 或终端接口组合。 + + ## `@deepseek-ai/dsh-tool-fs` ### `edit` @@ -699,6 +717,8 @@ pwsh 工具是 Windows 组合中 bash 执行器 seam 的 PowerShell 方言消费 先读后写/编辑策略由 `@deepseek-ai/dsh-fs-observation-policy` 添加;它是一个 `fs/*` 事件门禁插件,不会改变 schema。加载这些工具的部署按预期也应加载该插件。没有 `ctx.attachments` 时 `read_image` 不会注册;其 schema 与路由无关,执行时除非确切路由的模型声明图像输入,否则拒绝。 + + ## `@deepseek-ai/dsh-tool-fs-search` ### `glob` @@ -757,6 +777,8 @@ pwsh 工具是 Windows 组合中 bash 执行器 seam 的 PowerShell 方言消费 glob 和 grep 是无条件可用的发现工具,通过 ctx.subprocess spawn 随包提供的 ripgrep 二进制文件(`@vscode/ripgrep`),并作为普通前台调用运行,绝不作为后台任务;无需在宿主机安装 `rg`,也不经过 shell 层。本目录使用 `sampleOverCapGlobResults: true`;部署必须显式选择该行为。结果超过上限时,会通过可选的 ctx.spillStore 后端保存完整的格式化列表;在共置部署中,如果后端公开本地路径,返回的定位信息可供后续读取/搜索。 + + ## `@deepseek-ai/dsh-tool-terminal` ### `terminal_close` @@ -920,6 +942,8 @@ glob 和 grep 是无条件可用的发现工具,通过 ctx.subprocess spawn 这 6 个终端工具需要选择启用,用于补充一次性 bash/文件系统工具。`terminal_send(run_in_background: true)` 会注册到 `ctx.jobs`;schema 不包含 TUI、具名按键序列、BEL、调整尺寸、自动启动和跨 agent 共享。 + + ## `@deepseek-ai/dsh-tool-goal` ### `create_goal` @@ -1012,6 +1036,8 @@ glob 和 grep 是无条件可用的发现工具,通过 ctx.subprocess spawn create、edit、pause 和 resume 要求直接来自人类的根权限;complete 和 blocked 也接受确切的当前 Goal Round。blocked 的默认下限是 3 个获准的 Round。 + + ## `@deepseek-ai/dsh-schedule` ### `schedule_create` @@ -1107,6 +1133,8 @@ create、edit、pause 和 resume 要求直接来自人类的根权限;complete 仅在选择启用的 Schedule 插件加载后创建的 live 根 Agent scope 内注册。版本 1 接受 after_seconds、显式绝对 at 和有界固定速率 every_seconds,并披露 session-local 交付;管理读取与变更必须通过共享的 Session 持久化 barrier。 + + ## `@deepseek-ai/dsh-tool-lsp` ### `lsp` @@ -1153,6 +1181,8 @@ create、edit、pause 和 resume 要求直接来自人类的根权限;complete lsp 工具将提供方选择和语言服务器子进程置于 ctx.lsp 之后,因此其模型可见 schema 在更换提供方时保持稳定。运行时要求已注册提供方,例如 `@deepseek-ai/dsh-lsp-stdio`;如果没有提供方,查询会返回结构化 `LSP_UNAVAILABLE` 错误,而不会改变 schema。 + + ## `@deepseek-ai/dsh-tool-ralph` ### `ralph` @@ -1182,6 +1212,8 @@ lsp 工具将提供方选择和语言服务器子进程置于 ctx.lsp 之后, 固定的前台工作流会在每个 Round 启动一个全新的结构化子级;模型只能选择不可变目标和可选的 Round 上限。 + + ## `@deepseek-ai/dsh-tool-skill` ### `skill` @@ -1205,6 +1237,8 @@ lsp 工具将提供方选择和语言服务器子进程置于 ctx.lsp 之后, 来源:[`packages/skill/tool-skill/src/index.ts`](../packages/skill/tool-skill/src/index.ts) + + ## `@deepseek-ai/dsh-tool-session-query` ### `session_event_read` @@ -1438,6 +1472,8 @@ lsp 工具将提供方选择和语言服务器子进程置于 ctx.lsp 之后, 这 5 个只读工具会隐藏提供方游标,并根据不可变的调用 agent 会话为每个结果授权。该包需要选择启用;需要强制截止时间或限制行内输出的组合还会挂载通用超时或 spill 策略。 + + ## `@deepseek-ai/dsh-tool-subagent` ### `subagent` @@ -1472,6 +1508,8 @@ lsp 工具将提供方选择和语言服务器子进程置于 ctx.lsp 之后, 注册的工具名称取决于加载时 `toolName` 配置(默认为 `subagent`);上述 schema 对应默认值。随产品发布的组合会为每个 subagent 后端加载一次该包,因此模型还会看到绑定到 fork 后端的 `subagent_fork`。每个实例的描述、`run_in_background` 参数与 system prompt 策略取决于它自己的 `backgroundMode` 和 `enableRunInBackground`,因此两个随附 schema 并不相同:`subagent` 为 `continuable`,省略参数时默认后台运行,并由 runtime 自动投递结束结果;`subagent_fork` 保持 `one-shot`,省略参数时默认前台运行。详见 `packages/bundle/base/cordis.patch.yml` 和 `examples/acp-agent/cordis.yml`。 + + ## `@deepseek-ai/dsh-tool-subagent-control` ### `interrupt_agent` @@ -1545,6 +1583,8 @@ lsp 工具将提供方选择和语言服务器子进程置于 ctx.lsp 之后, 这些是控制可继续后台 subagent 的全局命名工具:绑定提供方的 `tool-subagent` 实例注册不同的委派工具;本包注册一次 `send_message` 和 `interrupt_agent`,另由 `list_agents` 通过单独加载的 `/list-agents` 插件提供,其目录行使用 sessionProjections 和实时 Agent 注册表。 + + ## `@deepseek-ai/dsh-tool-subagent-report` ### `report` @@ -1570,6 +1610,8 @@ lsp 工具将提供方选择和语言服务器子进程置于 ctx.lsp 之后, 按可继续的进程内子级注册,而非全局注册,因此该 schema 仅在这种子级内部可见,并且不受其全局 `toolFilter` 影响。同一份贡献还会安装子级作用域的 `tool:report` 系统提示词 section,本目录不渲染该 section。面向父级的 `send_message` 工具单独安装。 + + ## `@deepseek-ai/dsh-tool-jobs` ### `job_kill` @@ -1641,6 +1683,8 @@ lsp 工具将提供方选择和语言服务器子进程置于 ctx.lsp 之后, 与任务种类无关的后台任务控制器:后台 bash 命令、PTY 发送和 subagent 都通过相同的 3 个工具读取、列出和终止。加载该插件会挂接控制器,从而启用生产方的 `ctx.jobs.start()`。 + + ## `@deepseek-ai/dsh-tool-todo` ### `todo_write` @@ -1689,6 +1733,8 @@ lsp 工具将提供方选择和语言服务器子进程置于 ctx.lsp 之后, todo_write 是会话所有的状态;UI 将最新的 todo/write 事件渲染为检查清单。`allowParallelInProgress` 是没有默认值的必填项,因此本目录明确选择 `true`,对应描述允许同时存在多个 `in_progress` 项。选择 `false` 的部署会获得同一工具,但描述会要求只能有 1 个活动任务。 + + ## `@deepseek-ai/dsh-tool-workflow` ### `workflow` @@ -1783,6 +1829,8 @@ todo_write 是会话所有的状态;UI 将最新的 todo/write 事件渲染为 来源:[`packages/workflow/tool-workflow/src/index.ts`](../packages/workflow/tool-workflow/src/index.ts) + + ## `@deepseek-ai/dsh-tool-web` ### `web_fetch` diff --git a/docs/user/develop/basic/index.i18n.yaml b/docs/user/develop/basic/index.i18n.yaml index 0dd8723e1b..96e27f6c6d 100644 --- a/docs/user/develop/basic/index.i18n.yaml +++ b/docs/user/develop/basic/index.i18n.yaml @@ -2,5 +2,5 @@ # 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 docs/user/develop/basic/index.md -index.md: 494b7869be6ffdf5767fac260b36b2585305b516 -index.zh.md: a55b8e31151c5cfa5445069974be9fa022fea1eb +index.md: 08199624e638aaf4a36b04446c39c228b2af6025 +index.zh.md: c45a30d0bfffaf4a6c78303f9ca043c3397c8a08 diff --git a/docs/user/develop/basic/index.md b/docs/user/develop/basic/index.md index 494b7869be..08199624e6 100644 --- a/docs/user/develop/basic/index.md +++ b/docs/user/develop/basic/index.md @@ -45,14 +45,16 @@ export function apply(ctx: Context) { ## Register it in cordis.yml -Create `scratch-plugin/cordis.yml` as a Web overlay that inserts the local plugin: +Run `pwd` from the repository root, then create `scratch-plugin/cordis.yml` as a Web overlay that inserts the local plugin. Replace `/absolute/path/to/deepseek-harness` below with the printed path: ```yaml - insert: - id: hello - name: './src/my-plugin.ts' + name: '/absolute/path/to/deepseek-harness/scratch-plugin/src/my-plugin.ts' ``` +The plugin path must be absolute. A patch file contributes configuration but does not change the profile directory from which the loader resolves module paths. + Start the Web UI with that overlay: ```sh diff --git a/docs/user/develop/basic/index.zh.md b/docs/user/develop/basic/index.zh.md index a55b8e3115..c45a30d0bf 100644 --- a/docs/user/develop/basic/index.zh.md +++ b/docs/user/develop/basic/index.zh.md @@ -45,14 +45,16 @@ export function apply(ctx: Context) { ## 注册到 cordis.yml -创建 `scratch-plugin/cordis.yml`,作为插入本地插件的 Web 覆盖层: +在仓库根目录运行 `pwd`,然后创建 `scratch-plugin/cordis.yml`,作为插入本地插件的 Web 覆盖层。请将下文的 `/absolute/path/to/deepseek-harness` 替换为命令打印的路径: ```yaml - insert: - id: hello - name: './src/my-plugin.ts' + name: '/absolute/path/to/deepseek-harness/scratch-plugin/src/my-plugin.ts' ``` +插件路径必须是绝对路径。patch 文件只贡献配置,不会改变 loader 解析模块路径时使用的 profile 目录。 + 使用该覆盖层启动 Web UI: ```sh diff --git a/docs/user/develop/basic/publish.i18n.yaml b/docs/user/develop/basic/publish.i18n.yaml index 649408dead..694a7ff663 100644 --- a/docs/user/develop/basic/publish.i18n.yaml +++ b/docs/user/develop/basic/publish.i18n.yaml @@ -2,5 +2,5 @@ # 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 docs/user/develop/basic/publish.md -publish.md: 588531a28020ebe620643cd1aaaa43de000e658a -publish.zh.md: b86bd43369c027972705394fded43ae053248c0f +publish.md: edc85b74049c5b993efb02663195fcc0001b4620 +publish.zh.md: cb824ad12eec0d706efa17b28f3bdc874ba59131 diff --git a/docs/user/develop/basic/publish.md b/docs/user/develop/basic/publish.md index 588531a280..edc85b7404 100644 --- a/docs/user/develop/basic/publish.md +++ b/docs/user/develop/basic/publish.md @@ -2,7 +2,7 @@ English | [中文](publish.zh.md) -The previous tutorials loaded a local plugin through a `--patch` overlay. This tutorial packages it as an installable **bundle**, installs it into a **profile** with `dsh plugin add`, and explains the layer order that determines the composed configuration. Complete [plugin configuration](./config.md) first. +The previous tutorials loaded a local plugin through a `--patch` overlay. This tutorial packages it as an installable **bundle**, installs it into a **profile** with `pnpm dsh plugin add`, and explains the layer order that determines the composed configuration. Complete [plugin configuration](./config.md) first. ## Two concepts, two manifests @@ -11,10 +11,16 @@ Installation is built on two concepts. Both are described by a `package.json`, b - A **bundle** is an npm package that ships a configuration layer. Its manifest declares `dsh.bundle`, answering "what does this package contribute?": a patch file that inserts or overrides plugin rows. - A **profile** is a directory under `$DSH_HOME/profiles/` describing one runnable composition. Its manifest declares `dsh.profile`, answering "which bundles compose this setup, in what order?". -A bundle is what you author and distribute; a profile is what a user boots with `dsh --profile `. Nothing is both. +A bundle is what you author and distribute; a profile is what a user boots from this source checkout with `pnpm dsh --profile `. Nothing is both. ### The bundle manifest +From the repository root, create the package directory: + +```sh +mkdir -p hello-plugin +``` + ``` hello-plugin/ ├── package.json # declares dsh.bundle @@ -22,6 +28,8 @@ hello-plugin/ └── index.js # plugin modules the patch rows reference ``` +Create `hello-plugin/package.json`: + ```json { "name": "dsh-hello-plugin", @@ -33,7 +41,17 @@ hello-plugin/ } ``` -The patch file is a YAML array of patch entries, like the `--patch` overlays you have been writing, except plugin rows reference the package by name instead of a relative source path so Node resolution finds the installed code: +Create `hello-plugin/index.js` with the plugin entry point: + +```js +export const name = 'hello-plugin' + +export function apply() { + console.log('[hello-plugin] plugin loaded!') +} +``` + +Create `hello-plugin/cordis.patch.yml`. The patch is a YAML array like the `--patch` overlays you have been writing, except plugin rows reference the package by name instead of a relative source path so Node resolution finds the installed code: ```yaml - insert: @@ -41,7 +59,7 @@ The patch file is a YAML array of patch entries, like the `--patch` overlays you name: dsh-hello-plugin ``` -A package without the `dsh.bundle` declaration still installs, but only as a plain dependency: `dsh plugin` prints a warning and activates no layer. Use that package format for a library that plugin packages import rather than a plugin users enable. +A package without the `dsh.bundle` declaration still installs, but only as a plain dependency: `pnpm dsh plugin` prints a warning and activates no layer. Use that package format for a library that plugin packages import rather than a plugin users enable. ### The profile manifest @@ -50,15 +68,14 @@ A profile directory holds two files: - `package.json` — the profile's out-of-tree plugin dependencies (managed by pnpm) plus the `dsh.profile` manifest with its ordered `bundles` list. - `cordis.patch.yml` — the user's own patch layer, applied after every bundle layer. -You never write a profile manifest by hand: `dsh plugin` creates and maintains it. The next section shows the result. +You never write a profile manifest by hand: `pnpm dsh plugin` creates and maintains it. The next section shows the result. ## Install into a profile -`dsh plugin --profile ` forwards to pnpm in the profile directory, so every pnpm verb works. Install your package from its checkout: +`pnpm dsh plugin --profile ` forwards to pnpm in the profile directory, so every pnpm verb works. From the repository root, install the package checkout: ```sh -cd hello-plugin -dsh plugin --profile demo add . +pnpm dsh plugin --profile demo add ./hello-plugin ``` The first use initializes the profile (with `@deepseek-ai/dsh-base` as its first bundle), pnpm links the checkout, and `dsh` appends the bundle to `dsh.profile.bundles` because the package declares `dsh.bundle`: @@ -84,11 +101,11 @@ The first use initializes the profile (with `@deepseek-ai/dsh-base` as its first Verify the layer without booting, then boot: ```sh -dsh --profile demo --dump-config # shows a "# == dsh-hello-plugin" layer -dsh --profile demo +pnpm dsh --profile demo --dump-config # shows a "# == dsh-hello-plugin" layer +pnpm dsh --profile demo ``` -`dsh plugin --profile demo remove dsh-hello-plugin` removes both the dependency and the layer. +`pnpm dsh plugin --profile demo remove dsh-hello-plugin` removes both the dependency and the layer. ## The loading order @@ -136,7 +153,7 @@ On `--help`, the provider publishes no service, so those rows never activate. Lo Publishing to a registry is not required — users can install straight from a git host: ```sh -dsh plugin --profile demo add github:you/hello-plugin +pnpm dsh plugin --profile demo add github:you/hello-plugin ``` But a git install fetches **sources, not built artifacts**: nothing runs your `build` script, so a TypeScript package arrives without its `lib/` output and fails to load. Two things must happen, one on each side: @@ -155,8 +172,8 @@ Treat that allowance as what it is: **permission to execute the package's code o If you would rather not ask users for the allowance, distribute built artifacts instead — neither form needs any build permission: -- **Publish to npm** with `lib/` built at `pnpm publish` time; `dsh plugin add your-package` then installs prebuilt code. -- **Ship a tarball** from `pnpm pack`; users run `dsh plugin add ./hello-plugin-0.1.0.tgz`. +- **Publish to npm** with `lib/` built at `pnpm publish` time; `pnpm dsh plugin add your-package` then installs prebuilt code. +- **Ship a tarball** from `pnpm pack`; users run `pnpm dsh plugin add ./hello-plugin-0.1.0.tgz`. ## Next steps diff --git a/docs/user/develop/basic/publish.zh.md b/docs/user/develop/basic/publish.zh.md index b86bd43369..cb824ad12e 100644 --- a/docs/user/develop/basic/publish.zh.md +++ b/docs/user/develop/basic/publish.zh.md @@ -2,7 +2,7 @@ [English](publish.md) | 中文 -前几篇教程通过 `--patch` overlay 加载本地插件。本教程把它打包成可安装的**组合包**(bundle),用 `dsh plugin add` 安装进一个 **profile**,并解释决定组合后配置的层顺序。请先完成[插件配置](./config.md)。 +前几篇教程通过 `--patch` overlay 加载本地插件。本教程把它打包成可安装的**组合包**(bundle),用 `pnpm dsh plugin add` 安装进一个 **profile**,并解释决定组合后配置的层顺序。请先完成[插件配置](./config.md)。 ## 两个概念,两种 manifest @@ -11,10 +11,16 @@ - **组合包**是附带一个配置层的 npm 包。它的 manifest 声明 `dsh.bundle`,回答的是"这个包贡献什么?":一个插入或覆盖插件行的 patch 文件。 - **profile** 是位于 `$DSH_HOME/profiles/` 下、描述一份可启动组合的目录。它的 manifest 声明 `dsh.profile`,回答的是"这套配置由哪些组合包按什么顺序组成?"。 -组合包是你编写并分发的东西;profile 是用户用 `dsh --profile ` 启动的东西。没有东西同时是两者。 +组合包是你编写并分发的东西;profile 是用户在当前源码 checkout 中用 `pnpm dsh --profile ` 启动的东西。没有东西同时是两者。 ### 组合包 manifest +在仓库根目录创建包目录: + +```sh +mkdir -p hello-plugin +``` + ``` hello-plugin/ ├── package.json # declares dsh.bundle @@ -22,6 +28,8 @@ hello-plugin/ └── index.js # plugin modules the patch rows reference ``` +创建 `hello-plugin/package.json`: + ```json { "name": "dsh-hello-plugin", @@ -33,7 +41,17 @@ hello-plugin/ } ``` -patch 文件与一直在写的 `--patch` overlay 一样,是一个 patch 条目的 YAML 数组;区别是插件行按包名而不是相对源码路径引用这个包,这样 Node 的模块解析才能找到已安装的代码: +创建 `hello-plugin/index.js`,写入插件入口: + +```js +export const name = 'hello-plugin' + +export function apply() { + console.log('[hello-plugin] plugin loaded!') +} +``` + +创建 `hello-plugin/cordis.patch.yml`。这个 patch 与一直在写的 `--patch` overlay 一样,是一个 patch 条目的 YAML 数组;区别是插件行按包名而不是相对源码路径引用这个包,这样 Node 的模块解析才能找到已安装的代码: ```yaml - insert: @@ -41,7 +59,7 @@ patch 文件与一直在写的 `--patch` overlay 一样,是一个 patch 条目 name: dsh-hello-plugin ``` -没有 `dsh.bundle` 声明的包仍然可以安装,但只作为普通依赖:`dsh plugin` 会打印警告,且不激活任何层。如果一个库供插件包 import,而不是供用户启用,就使用这种包格式。 +没有 `dsh.bundle` 声明的包仍然可以安装,但只作为普通依赖:`pnpm dsh plugin` 会打印警告,且不激活任何层。如果一个库供插件包 import,而不是供用户启用,就使用这种包格式。 ### profile manifest @@ -50,15 +68,14 @@ profile 目录包含两个文件: - `package.json` — profile 的树外插件依赖(由 pnpm 管理),加上 `dsh.profile` manifest 及其有序的 `bundles` 列表。 - `cordis.patch.yml` — 用户自己的 patch 层,在每个组合包层之后应用。 -profile manifest 从不需要手写:`dsh plugin` 负责创建和维护它。下一节展示其结果。 +profile manifest 从不需要手写:`pnpm dsh plugin` 负责创建和维护它。下一节展示其结果。 ## 安装进 profile -`dsh plugin --profile ` 在 profile 目录内转发给 pnpm,因此所有 pnpm 子命令都可用。从 checkout 安装你的包: +`pnpm dsh plugin --profile ` 在 profile 目录内转发给 pnpm,因此所有 pnpm 子命令都可用。在仓库根目录安装该包的 checkout: ```sh -cd hello-plugin -dsh plugin --profile demo add . +pnpm dsh plugin --profile demo add ./hello-plugin ``` 首次使用会初始化 profile(`@deepseek-ai/dsh-base` 作为它的第一个组合包),pnpm 链接该 checkout,而 `dsh` 因为这个包声明了 `dsh.bundle`,把它追加进 `dsh.profile.bundles`: @@ -84,11 +101,11 @@ dsh plugin --profile demo add . 先不启动、只验证该层,再启动: ```sh -dsh --profile demo --dump-config # shows a "# == dsh-hello-plugin" layer -dsh --profile demo +pnpm dsh --profile demo --dump-config # shows a "# == dsh-hello-plugin" layer +pnpm dsh --profile demo ``` -`dsh plugin --profile demo remove dsh-hello-plugin` 会同时移除依赖和对应的层。 +`pnpm dsh plugin --profile demo remove dsh-hello-plugin` 会同时移除依赖和对应的层。 ## 加载顺序 @@ -136,7 +153,7 @@ dsh --profile demo 发布到注册表不是必须的——用户可以直接从 git 托管安装: ```sh -dsh plugin --profile demo add github:you/hello-plugin +pnpm dsh plugin --profile demo add github:you/hello-plugin ``` 但 git 安装拉取的是**源码,不是构建产物**:没有任何环节运行你的 `build` 脚本,因此 TypeScript 包到手时没有 `lib/` 输出,加载会失败。必须两边各做一件事: @@ -155,8 +172,8 @@ dsh plugin --profile demo add github:you/hello-plugin 如果不想让用户做这项授权,就改为分发构建产物——以下两种形式都不需要任何构建权限: -- **发布到 npm**,在 `pnpm publish` 时构建好 `lib/`;`dsh plugin add your-package` 安装的就是预构建代码。 -- **交付 tarball**:用 `pnpm pack` 打包;用户执行 `dsh plugin add ./hello-plugin-0.1.0.tgz`。 +- **发布到 npm**,在 `pnpm publish` 时构建好 `lib/`;`pnpm dsh plugin add your-package` 安装的就是预构建代码。 +- **交付 tarball**:用 `pnpm pack` 打包;用户执行 `pnpm dsh plugin add ./hello-plugin-0.1.0.tgz`。 ## 下一步 diff --git a/docs/user/develop/framework/events.i18n.yaml b/docs/user/develop/framework/events.i18n.yaml index b5f1cc7745..7b62e40d71 100644 --- a/docs/user/develop/framework/events.i18n.yaml +++ b/docs/user/develop/framework/events.i18n.yaml @@ -2,5 +2,5 @@ # 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 docs/user/develop/framework/events.md -events.md: 4a6ecbad614cf2debccaee9ace1086273d8fb95b -events.zh.md: c77747bf6ac767c69e4f2d9cfe7375b71bd388e6 +events.md: 1d9fe5c8f5068de6ad8b2abaa85cf67be35c8459 +events.zh.md: 8bb9447a270cc2db966b1e01298a831a60a1b9c1 diff --git a/docs/user/develop/framework/events.md b/docs/user/develop/framework/events.md index 4a6ecbad61..1d9fe5c8f5 100644 --- a/docs/user/develop/framework/events.md +++ b/docs/user/develop/framework/events.md @@ -40,7 +40,7 @@ ctx.on('my-plugin/ready', ({ id }) => { ### bail — short circuit -Listeners run in order; the first non-`undefined` result becomes the final result: +Listeners run in order; the first result other than `null`, `false`, or `undefined` becomes the final result: ```ts ignore-check // Dispatch @@ -49,13 +49,13 @@ const result = ctx.bail('some-check', input) // Listen: a returned value stops later listeners. ctx.on('some-check', (input) => { if (shouldBlock(input)) return 'blocked' - // Return undefined to continue to the next listener. + // Return null, false, or undefined to continue to the next listener. }) ``` ### serial — ordered execution -Listeners run in registration order and asynchronous results are awaited. The first listener to return a non-empty value stops further execution: +Listeners run in registration order and asynchronous results are awaited. The first result other than `null`, `false`, or `undefined` stops further execution: ```ts ignore-check await ctx.serial('setup-phase', context) diff --git a/docs/user/develop/framework/events.zh.md b/docs/user/develop/framework/events.zh.md index c77747bf6a..8bb9447a27 100644 --- a/docs/user/develop/framework/events.zh.md +++ b/docs/user/develop/framework/events.zh.md @@ -40,7 +40,7 @@ ctx.on('my-plugin/ready', ({ id }) => { ### bail — 短路 -依次调用监听器,第一个非 `undefined` 的返回值将作为最终结果: +监听器按顺序运行,第一个不是 `null`、`false` 或 `undefined` 的返回值会成为最终结果: ```ts ignore-check // Dispatch @@ -49,13 +49,13 @@ const result = ctx.bail('some-check', input) // Listen: a returned value stops later listeners. ctx.on('some-check', (input) => { if (shouldBlock(input)) return 'blocked' - // Return undefined to continue to the next listener. + // Return null, false, or undefined to continue to the next listener. }) ``` ### serial — 顺序执行 -监听器按注册顺序依次执行,并等待异步结果;第一个返回非空值的监听器会终止后续执行: +监听器按注册顺序依次执行,并等待异步结果;第一个不是 `null`、`false` 或 `undefined` 的返回值会终止后续执行: ```ts ignore-check await ctx.serial('setup-phase', context) diff --git a/docs/user/develop/framework/service.i18n.yaml b/docs/user/develop/framework/service.i18n.yaml index 69b91821ba..507fc26f86 100644 --- a/docs/user/develop/framework/service.i18n.yaml +++ b/docs/user/develop/framework/service.i18n.yaml @@ -2,5 +2,5 @@ # 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 docs/user/develop/framework/service.md -service.md: a9e873f1ba969b1f2d22266f8ec03207f84eeddc -service.zh.md: a201280bc38176f57f5354ab205ae700c7b04a89 +service.md: 03f4e7dc4df934495a4b203066183753b621339e +service.zh.md: 2f4c01e0ea6a87a87ca694b265b07e07098f3e59 diff --git a/docs/user/develop/framework/service.md b/docs/user/develop/framework/service.md index a9e873f1ba..03f4e7dc4d 100644 --- a/docs/user/develop/framework/service.md +++ b/docs/user/develop/framework/service.md @@ -117,7 +117,7 @@ This prevents a plugin from calling a service that no longer exists. name: '@deepseek-ai/cordis-plugin-group' group: true isolate: - bash: true + shell: true config: - name: '@deepseek-ai/dsh-bash-local' config: @@ -128,7 +128,7 @@ This prevents a plugin from calling a service that no longer exists. name: '@deepseek-ai/cordis-plugin-group' group: true isolate: - bash: true + shell: true config: - name: '@deepseek-ai/dsh-bash-local' config: diff --git a/docs/user/develop/framework/service.zh.md b/docs/user/develop/framework/service.zh.md index a201280bc3..2f4c01e0ea 100644 --- a/docs/user/develop/framework/service.zh.md +++ b/docs/user/develop/framework/service.zh.md @@ -108,6 +108,8 @@ export function apply(ctx: Context) { 这可以防止插件调用已不存在的服务。 + + ## 服务隔离 `cordis.yml` 支持服务隔离——同一个服务可以有多个实例,不同插件组看到不同实例: @@ -117,7 +119,7 @@ export function apply(ctx: Context) { name: '@deepseek-ai/cordis-plugin-group' group: true isolate: - bash: true + shell: true config: - name: '@deepseek-ai/dsh-bash-local' config: @@ -128,7 +130,7 @@ export function apply(ctx: Context) { name: '@deepseek-ai/cordis-plugin-group' group: true isolate: - bash: true + shell: true config: - name: '@deepseek-ai/dsh-bash-local' config: diff --git a/docs/user/develop/practice/index.i18n.yaml b/docs/user/develop/practice/index.i18n.yaml index 166269706c..2db0900779 100644 --- a/docs/user/develop/practice/index.i18n.yaml +++ b/docs/user/develop/practice/index.i18n.yaml @@ -2,5 +2,5 @@ # 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 docs/user/develop/practice/index.md -index.md: 56ca7249b8b156ceb78327b547413dbf65726dcc -index.zh.md: aec5980ea45cfd5f8f7408877743920e68e15cbd +index.md: cc6bd7a234305f6fa193341f15354b40855f72e9 +index.zh.md: aed13b00f9bc74946aace614953bed30705c8281 diff --git a/docs/user/develop/practice/index.md b/docs/user/develop/practice/index.md index 56ca7249b8..cc6bd7a234 100644 --- a/docs/user/develop/practice/index.md +++ b/docs/user/develop/practice/index.md @@ -23,7 +23,7 @@ The Bash execution capability consists of: └─────────────┘ └──────────────────┘ └──────────────┘ ▲ │ └────────────────────────────────────────────┘ - inject: ['bash'] + inject: ['shell'] ``` ## Benefits of the split diff --git a/docs/user/develop/practice/index.zh.md b/docs/user/develop/practice/index.zh.md index aec5980ea4..aed13b00f9 100644 --- a/docs/user/develop/practice/index.zh.md +++ b/docs/user/develop/practice/index.zh.md @@ -23,7 +23,7 @@ └─────────────┘ └──────────────────┘ └──────────────┘ ▲ │ └────────────────────────────────────────────┘ - inject: ['bash'] + inject: ['shell'] ``` ## 拆分的好处 diff --git a/docs/user/develop/practice/llm-adapter.i18n.yaml b/docs/user/develop/practice/llm-adapter.i18n.yaml index 6c91f15cb1..d7d431bd5e 100644 --- a/docs/user/develop/practice/llm-adapter.i18n.yaml +++ b/docs/user/develop/practice/llm-adapter.i18n.yaml @@ -2,5 +2,5 @@ # 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 docs/user/develop/practice/llm-adapter.md -llm-adapter.md: aba4a6d0c8ee42e78ca5a804d9a0dd9b31c1e240 -llm-adapter.zh.md: 5c7a6483e29a28871257df6a31e13d859a245097 +llm-adapter.md: 882e82d880ac622cf886c6c24e75a1987e1c6702 +llm-adapter.zh.md: 27c480af2a19a68ac35900a2ce1b5e9dbdc85bf8 diff --git a/docs/user/develop/practice/llm-adapter.md b/docs/user/develop/practice/llm-adapter.md index aba4a6d0c8..882e82d880 100644 --- a/docs/user/develop/practice/llm-adapter.md +++ b/docs/user/develop/practice/llm-adapter.md @@ -32,12 +32,12 @@ class MyAdapter extends LlmAdapter { export interface Config { apiKey: string - models: string[] + providers: string[] } export const Config: Schema = Schema.object({ apiKey: Schema.string().required(), - models: Schema.array(Schema.string()).required(), + providers: Schema.array(Schema.string()).required(), }) export const name = 'my-llm-adapter' @@ -45,7 +45,7 @@ export const inject = ['llm'] export function apply(ctx: Context, config: Config) { const adapter = new MyAdapter(config.apiKey) - ctx.llm.registerAdapter(config.models, adapter) + ctx.llm.registerAdapter(config.providers, adapter) } ``` @@ -117,10 +117,10 @@ Override `resolveModel(provider, model, signal?)` to return exact provider/model ## Register an adapter ```ts ignore-check -ctx.llm.registerAdapter(['model-name-1', 'model-name-2'], adapter) +ctx.llm.registerAdapter(['my-provider'], adapter) ``` -The first argument lists the model names handled by the adapter. If `cordis.yml` selects `model: model-name-1`, the service routes that request to this adapter. +The first argument lists provider routes handled by the adapter. `GenerateOptions.provider` selects the registered adapter, while `GenerateOptions.model` passes an adapter-owned model id without lifecycle registration. Override `listModels()` when the adapter can advertise model choices to selectors. ## Use it from cordis.yml @@ -129,18 +129,16 @@ The first argument lists the model names handled by the adapter. If `cordis.yml` name: './src/my-llm-adapter.ts' config: apiKey: !!js process.env.MY_API_KEY - models: - - my-model-v1 - - my-model-v2 + providers: + - my-provider - id: agent-loop name: '@deepseek-ai/dsh-agent-loop' config: agents: - id: main - provider: my-llm - model: my-model-v1 # References the model registered above. - workspaceContext: false + provider: my-provider + model: my-model-v1 ``` ## Reference implementations diff --git a/docs/user/develop/practice/llm-adapter.zh.md b/docs/user/develop/practice/llm-adapter.zh.md index 5c7a6483e2..27c480af2a 100644 --- a/docs/user/develop/practice/llm-adapter.zh.md +++ b/docs/user/develop/practice/llm-adapter.zh.md @@ -32,12 +32,12 @@ class MyAdapter extends LlmAdapter { export interface Config { apiKey: string - models: string[] + providers: string[] } export const Config: Schema = Schema.object({ apiKey: Schema.string().required(), - models: Schema.array(Schema.string()).required(), + providers: Schema.array(Schema.string()).required(), }) export const name = 'my-llm-adapter' @@ -45,7 +45,7 @@ export const inject = ['llm'] export function apply(ctx: Context, config: Config) { const adapter = new MyAdapter(config.apiKey) - ctx.llm.registerAdapter(config.models, adapter) + ctx.llm.registerAdapter(config.providers, adapter) } ``` @@ -117,10 +117,10 @@ async function* exampleChunks(): AsyncIterable { ## 注册适配器 ```ts ignore-check -ctx.llm.registerAdapter(['model-name-1', 'model-name-2'], adapter) +ctx.llm.registerAdapter(['my-provider'], adapter) ``` -第一个参数是该适配器支持的模型名列表。当用户在 `cordis.yml` 中配置 `model: model-name-1` 时,框架会将请求路由到该适配器。 +第一个参数是该适配器处理的提供方路由列表。`GenerateOptions.provider` 选择已注册的适配器,`GenerateOptions.model` 则传入由适配器拥有、无需在生命周期启动时注册的模型 id。适配器能够向选择器公布模型选项时,请覆写 `listModels()`。 ## 在 cordis.yml 中使用 @@ -129,18 +129,16 @@ ctx.llm.registerAdapter(['model-name-1', 'model-name-2'], adapter) name: './src/my-llm-adapter.ts' config: apiKey: !!js process.env.MY_API_KEY - models: - - my-model-v1 - - my-model-v2 + providers: + - my-provider - id: agent-loop name: '@deepseek-ai/dsh-agent-loop' config: agents: - id: main - provider: my-llm - model: my-model-v1 # References the model registered above. - workspaceContext: false + provider: my-provider + model: my-model-v1 ``` ## 实战参考 diff --git a/docs/user/guide/index.i18n.yaml b/docs/user/guide/index.i18n.yaml index 51f8a8802d..f6727b6bc4 100644 --- a/docs/user/guide/index.i18n.yaml +++ b/docs/user/guide/index.i18n.yaml @@ -2,5 +2,5 @@ # 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 docs/user/guide/index.md -index.md: 80d288b1aba37e7f0863fe5fc8237cbd2a6ab9b5 -index.zh.md: addfbc94ff93ed015e52f509a23a3f981e36770b +index.md: 282a5c11b317a8fb8706bb03f41cf03fb2aca49d +index.zh.md: 4ec53b16fc8987b5eb40ef4854cb41d1436a9015 diff --git a/docs/user/guide/index.md b/docs/user/guide/index.md index 80d288b1ab..282a5c11b3 100644 --- a/docs/user/guide/index.md +++ b/docs/user/guide/index.md @@ -2,9 +2,7 @@ English | [中文](index.zh.md) -Start the Web UI through the [root README](../../../README.md#run); the command prints its URL. This guide begins after that server is running. - -The invoking directory is the default workspace, so the agent can inspect and modify the project where you started `dsh`. +Start the Web UI through the [root README](../../../README.md#run); the command prints its URL. This guide begins after that server is running. The `dsh` process uses its invoking directory as the default filesystem location, but a fresh Web UI has no selected workspace until you add one. ## Configure a model @@ -12,6 +10,10 @@ Open **Settings → Models**, enter a DeepSeek API key, and save it. The model r The [model configuration guide](./providers.md) covers other providers and custom OpenAI-compatible endpoints. +## Choose a workspace + +Click **Choose workspace**, add the project directory where you started `dsh`, and select it. The session composer remains unavailable until a workspace is selected. + ## Run a task Start a session and send: diff --git a/docs/user/guide/index.zh.md b/docs/user/guide/index.zh.md index addfbc94ff..4ec53b16fc 100644 --- a/docs/user/guide/index.zh.md +++ b/docs/user/guide/index.zh.md @@ -2,9 +2,7 @@ [English](index.md) | 中文 -先按照[根 README](../../../README.md#run)启动 Web UI;命令会打印其访问地址。本指南从服务器已经运行的状态开始。 - -调用目录是默认工作区,因此 agent(智能体)可以检查并修改启动 `dsh` 时所在的项目。 +先按照[根 README](../../../README.md#run)启动 Web UI;命令会打印其访问地址。本指南从服务器已经运行的状态开始。`dsh` 进程会把调用目录作为默认文件系统位置,但新的 Web UI 在添加工作区前不会选中任何工作区。 ## 配置模型 @@ -12,6 +10,10 @@ [模型配置指南](./providers.md)介绍其他提供方和自定义 OpenAI 兼容端点。 +## 选择工作区 + +点击**选择工作区**,添加启动 `dsh` 时所在的项目目录,然后选中它。选中工作区前,会话输入框不可用。 + ## 运行任务 启动一个会话并发送: diff --git a/package.json b/package.json index cf0f8b917f..9adfdf5cb4 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,7 @@ "doc-typecheck:contracts-ready": "tsx scripts/doc-typecheck.ts", "verify-md-wrap": "tsx scripts/verify-md-wrap.ts", "verify-md-links": "tsx scripts/verify-md-links.ts", + "verify-doc-site-fragments": "tsx scripts/verify-doc-site-fragments.ts", "verify-public-repository-links": "tsx scripts/verify-public-repository-links.ts", "verify-doc-refs": "tsx scripts/verify-doc-refs.ts", "verify-package-paths": "tsx scripts/verify-package-paths.ts", @@ -86,10 +87,10 @@ "gen-translation-brief": "tsx scripts/gen-translation-brief.ts", "verify-doc-budgets": "tsx scripts/verify-doc-budgets.ts", "docs:dev": "pnpm --filter @deepseek-ai/website run dev", - "docs:build": "pnpm --filter @deepseek-ai/website run build", - "docs:build:mpa": "pnpm --filter @deepseek-ai/website exec vitepress build . --mpa", + "docs:build": "pnpm --filter @deepseek-ai/website run build && pnpm run verify-doc-site-fragments", + "docs:build:mpa": "pnpm --filter @deepseek-ai/website exec vitepress build . --mpa && pnpm run verify-doc-site-fragments", "docs:preview": "pnpm --filter @deepseek-ai/website run preview", - "docs:check": "pnpm exec vitest run scripts/project-doc-site.spec.ts && pnpm run docs:build", + "docs:check": "pnpm exec vitest run scripts/project-doc-site.spec.ts scripts/verify-doc-site-fragments.spec.ts && pnpm run docs:build", "website:dev": "pnpm run docs:dev", "website:build": "pnpm run docs:build", "verify-package-readme-limitations": "tsx scripts/verify-package-readme-limitations.ts", diff --git a/scripts/gen-config-catalog.ts b/scripts/gen-config-catalog.ts index 5abc85c896..83fe8e7809 100644 --- a/scripts/gen-config-catalog.ts +++ b/scripts/gen-config-catalog.ts @@ -12,6 +12,7 @@ import { dirname, resolve, sep } from 'node:path' import ts from 'typescript' import { LINK_MAP } from './gen-cordis-catalog.ts' import { parseJsDoc, pointer, rawJsDoc } from './jsdoc.ts' +import { githubSlug } from './verify-md-links.ts' const root = resolve(import.meta.dirname, '..') const OUT = 'docs/config-catalog.md' @@ -766,11 +767,6 @@ export function collectConfigCatalog(scanRoot: string = root): CatalogEntry[] { return entries.sort((a, b) => a.pkg.localeCompare(b.pkg)) } -/** GitHub-style anchor slug for a `## \`pkg\`` heading. */ -function slug(heading: string): string { - return heading.toLowerCase().replace(/[^a-z0-9 -]/g, '').replace(/ /g, '-') -} - /** Render the `Requires:` service-key line, or '' when the plugin injects nothing. */ function requiresLine(inject: string[]): string { return inject.length ? `Requires: ${inject.map(k => `\`${k}\``).join(' · ')}` : '' @@ -782,7 +778,7 @@ function requiresLine(inject: string[]): string { function refLink(ref: TypeRef, byName: Map): string { const target = byName.get(ref.specifier) if (target?.kind === 'config' && ref.imported === target.configTypeName) { - return `[\`${ref.alias}\`](#${slug(target.pkg)})` + return `[\`${ref.alias}\`](#${githubSlug(target.pkg)})` } const page = LINK_MAP[ref.imported] if (page) return `[\`${ref.alias}\`](subsystems/${page})` @@ -792,7 +788,7 @@ function refLink(ref: TypeRef, byName: Map): string { /** Render one configurable plugin's section. */ function renderConfigEntry(entry: CatalogEntry, byName: Map): string[] { - const out = [`## \`${entry.pkg}\``, ''] + const out = [``, '', `## \`${entry.pkg}\``, ''] const requires = requiresLine(entry.inject) if (requires) out.push(requires, '') out.push('```' + FENCE, ...(entry.pastes ?? []).map(p => p.text).join('\n\n').split('\n'), '```', '') diff --git a/scripts/gen-persistence-catalog.ts b/scripts/gen-persistence-catalog.ts index 5ca27b5260..38100fd531 100644 --- a/scripts/gen-persistence-catalog.ts +++ b/scripts/gen-persistence-catalog.ts @@ -10,6 +10,7 @@ import { globSync, readFileSync, writeFileSync } from 'node:fs' import { resolve, sep } from 'node:path' import ts from 'typescript' import { parseJsDoc, pointer, rawJsDoc, reportViolations } from './jsdoc.ts' +import { githubSlug } from './verify-md-links.ts' const root = resolve(import.meta.dirname, '..') const OUT = 'docs/persistence-catalog.md' @@ -341,7 +342,8 @@ function typeLinks(payload: string): string { /** Render one log event entry. */ function renderEvent(e: AnnotatedLogEventEntry): string[] { - const out = [`#### \`${e.name}\` — ${e.surface ? 'surface' : 'log-only'}`, ''] + const heading = `${e.name} — ${e.surface ? 'surface' : 'log-only'}` + const out = [``, '', `#### \`${e.name}\` — ${e.surface ? 'surface' : 'log-only'}`, ''] out.push('```' + FENCE, e.declaration, '```', '') const links = typeLinks(e.payload) if (links) out.push(links, '') diff --git a/scripts/gen-tool-catalog.ts b/scripts/gen-tool-catalog.ts index c6c7fb117f..005eb4aef9 100644 --- a/scripts/gen-tool-catalog.ts +++ b/scripts/gen-tool-catalog.ts @@ -63,6 +63,7 @@ import * as ToolWeb from '@deepseek-ai/dsh-tool-web' import VmWorkflowEngine from '@deepseek-ai/dsh-workflow-worker-thread' import * as ToolRalph from '@deepseek-ai/dsh-tool-ralph' import * as ToolWorkflow from '@deepseek-ai/dsh-tool-workflow' +import { githubSlug } from './verify-md-links.ts' /** Attachment seam marker that makes the attachments-conditional `read_image` schema harvestable. */ class CatalogAttachmentStore extends AttachmentStore { @@ -704,7 +705,7 @@ export function render(catalog: ToolCatalog): string { '', ] for (const entry of catalog) { - lines.push(`## \`${entry.pkg}\``, '') + lines.push(``, '', `## \`${entry.pkg}\``, '') for (const schema of entry.schemas) { // Collection validated that every harvested schema has a source. const source = entry.sources[schema.name] as string diff --git a/scripts/run-gates.ts b/scripts/run-gates.ts index e0b07041c0..9673439fbc 100644 --- a/scripts/run-gates.ts +++ b/scripts/run-gates.ts @@ -606,7 +606,7 @@ function docSyncLeafGates(options: { pnpmScript('translation-prompt', 'verify-translation-prompt', { label: 'translation prompt' }), pnpmScript('translation-pairing', 'verify-translation-pairing', { label: 'translation pairing' }), pnpmScript('doc-budgets', 'verify-doc-budgets', { label: 'doc budgets' }), - pnpmExec('docs-site-projection', ['vitest', 'run', 'scripts/project-doc-site.spec.ts'], { + pnpmExec('docs-site-projection', ['vitest', 'run', 'scripts/project-doc-site.spec.ts', 'scripts/verify-doc-site-fragments.spec.ts'], { label: 'documentation projection', }), // Keep the VitePress build itself in one gate because projection rewrites website/.generated. diff --git a/scripts/verify-doc-site-fragments.spec.ts b/scripts/verify-doc-site-fragments.spec.ts new file mode 100644 index 0000000000..2d041a33d5 --- /dev/null +++ b/scripts/verify-doc-site-fragments.spec.ts @@ -0,0 +1,67 @@ +/** Tests for built-site fragment validation. */ + +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { afterEach, describe, expect, it } from 'vitest' +import { inspectSiteFragments } from './verify-doc-site-fragments.ts' + +const roots: string[] = [] + +afterEach(() => { + for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true }) +}) + +function fixture(): string { + const root = mkdtempSync(join(tmpdir(), 'dsh-doc-fragments-')) + roots.push(root) + mkdirSync(join(root, 'guide'), { recursive: true }) + writeFileSync(join(root, 'index.html'), 'start') + writeFileSync(join(root, 'guide/start.html'), [ + '

Ready

', + '', + 'same page', + 'html alias', + 'root', + 'external', + ].join('')) + return root +} + +describe('inspectSiteFragments', () => { + it('rejects a directory with no built pages', () => { + const root = mkdtempSync(join(tmpdir(), 'dsh-doc-fragments-empty-')) + roots.push(root) + + expect(() => inspectSiteFragments(root)).toThrow('no HTML files found') + }) + + it('resolves clean, encoded, and same-page routes', () => { + const root = fixture() + writeFileSync(join(root, 'guide/encoded.html'), '

Encoded

self') + + expect(inspectSiteFragments(root)).toEqual({ checked: 5, broken: [] }) + }) + + it('reports missing ids and missing built routes', () => { + const root = fixture() + writeFileSync(join(root, 'guide/broken.html'), [ + 'id', + 'route', + ].join('')) + + expect(inspectSiteFragments(root).broken).toEqual([ + { + source: 'guide/broken.html', + href: './start#missing', + target: 'guide/start.html', + fragment: 'missing', + }, + { + source: 'guide/broken.html', + href: './absent#missing', + fragment: 'missing', + }, + ]) + }) +}) diff --git a/scripts/verify-doc-site-fragments.ts b/scripts/verify-doc-site-fragments.ts new file mode 100644 index 0000000000..d42cc752b0 --- /dev/null +++ b/scripts/verify-doc-site-fragments.ts @@ -0,0 +1,143 @@ +/** + * Verify fragment links against the HTML emitted by VitePress. Markdown and + * VitePress use different heading-slug algorithms, so source-link validation + * alone cannot prove that a published fragment exists. + * + * Run with `tsx scripts/verify-doc-site-fragments.ts` after `docs:build`. + */ + +import { globSync, readFileSync } from 'node:fs' +import { resolve, sep } from 'node:path' +import { JSDOM } from 'jsdom' + +const root = resolve(import.meta.dirname, '..') + +/** One fragment reference that does not resolve in the built site. */ +export interface BrokenSiteFragment { + /** HTML file containing the link. */ + source: string + /** Link value as emitted by VitePress. */ + href: string + /** Built HTML target, or `undefined` when the route was not emitted. */ + target?: string + /** Decoded fragment id requested by the link. */ + fragment: string +} + +/** Result of checking every fragment-bearing anchor in a built site. */ +export interface SiteFragmentReport { + /** Number of internal fragment references inspected. */ + checked: number + /** References whose route or fragment id is absent. */ + broken: BrokenSiteFragment[] +} + +interface BuiltPage { + file: string + route: string + ids: Set + document: Document +} + +function posixPath(path: string): string { + return path.split(sep).join('/') +} + +function routeFor(file: string): string { + if (file === 'index.html') return '/' + if (file.endsWith('/index.html')) return `/${file.slice(0, -'index.html'.length)}` + return `/${file.slice(0, -'.html'.length)}` +} + +function aliasesFor(page: BuiltPage): string[] { + if (page.route === '/') return ['/', '/index', '/index.html'] + if (page.route.endsWith('/')) { + const stem = page.route.slice(0, -1) + return [page.route, stem, `${stem}/index`, `${stem}/index.html`] + } + return [page.route, `${page.route}.html`] +} + +function decodedFragment(hash: string): string { + try { + return decodeURIComponent(hash.slice(1)) + } catch { + return hash.slice(1) + } +} + +/** + * Check fragment-bearing links in a VitePress output directory. + * + * @param distRoot - Directory containing generated HTML files. + * @returns Counted internal links and every unresolved target. + */ +export function inspectSiteFragments(distRoot: string): SiteFragmentReport { + const files = globSync('**/*.html', { cwd: distRoot }).map(posixPath).sort() + if (files.length === 0) { + throw new Error(`verify-doc-site-fragments: no HTML files found under ${distRoot}; run docs:build first.`) + } + const pages: BuiltPage[] = files.map((file) => { + const document = new JSDOM(readFileSync(resolve(distRoot, file), 'utf8')).window.document + const ids = new Set() + for (const element of document.querySelectorAll('[id]')) ids.add(element.id) + for (const element of document.querySelectorAll('a[name]')) { + const name = element.getAttribute('name') + if (name !== null) ids.add(name) + } + return { file, route: routeFor(file), ids, document } + }) + + const byRoute = new Map() + for (const page of pages) { + for (const alias of aliasesFor(page)) byRoute.set(alias, page) + } + + const origin = 'https://dsh-docs.invalid' + const broken: BrokenSiteFragment[] = [] + let checked = 0 + for (const page of pages) { + for (const anchor of page.document.querySelectorAll('a[href]')) { + const href = anchor.getAttribute('href') + if (href === null || !href.includes('#')) continue + let targetUrl: URL + try { + targetUrl = new URL(href, `${origin}${page.route}`) + } catch { + continue + } + if (targetUrl.origin !== origin || targetUrl.hash === '') continue + const fragment = decodedFragment(targetUrl.hash) + if (fragment === '') continue + checked++ + const target = byRoute.get(targetUrl.pathname) + if (target === undefined || !target.ids.has(fragment)) { + broken.push({ + source: page.file, + href, + ...(target === undefined ? {} : { target: target.file }), + fragment, + }) + } + } + } + return { checked, broken } +} + +function main(): number { + const distRoot = resolve(root, 'website/.dist') + const report = inspectSiteFragments(distRoot) + if (report.broken.length === 0) { + console.log(`verify-doc-site-fragments: ${report.checked} internal fragment reference(s) resolve.`) + return 0 + } + + console.error(`verify-doc-site-fragments: ${report.broken.length} broken fragment reference(s):`) + for (const item of report.broken) { + const target = item.target === undefined ? 'target route was not built' : `${item.target} has no id ${JSON.stringify(item.fragment)}` + console.error(` ${item.source}: ${JSON.stringify(item.href)} (${target})`) + } + return 1 +} + +if (import.meta.main) process.exitCode = main() From bca790481c02c710e1c8b339efdf1ec7c2404370 Mon Sep 17 00:00:00 2001 From: Turtle Date: Thu, 13 Aug 2026 13:58:06 +0800 Subject: [PATCH 06/24] docs: document stable fragment aliases --- .agents/skills/dsh-doc-site-sync/SKILL.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.agents/skills/dsh-doc-site-sync/SKILL.md b/.agents/skills/dsh-doc-site-sync/SKILL.md index 5d39470d72..bc5b3f48dd 100644 --- a/.agents/skills/dsh-doc-site-sync/SKILL.md +++ b/.agents/skills/dsh-doc-site-sync/SKILL.md @@ -49,6 +49,7 @@ Write normal repository-relative Markdown links in canonical docs. The projector - An image is the exception: its file is copied into the generated tree and referenced from there, so the site serves it regardless of repository visibility. It must be a regular file inside the repository. - External URLs, site-absolute URLs, email links, and fragment-only links remain unchanged. - A missing repository-relative target fails projection instead of silently producing a broken link. +- Cross-language and generated-document fragments use the English GitHub heading id as their canonical id. If a translated or generated heading emits another id, place an explicit `` immediately before it; add generated aliases in the owning generator. Do not write website-specific routes into canonical Markdown just to satisfy VitePress. Use `sourceAliases` for directory-style repository links that should resolve to a mapped index page. From 4201bbceca8104f6a26013e49c44239b6a257929 Mon Sep 17 00:00:00 2001 From: Turtle Date: Thu, 13 Aug 2026 14:02:07 +0800 Subject: [PATCH 07/24] docs: use installed dsh command in publish tutorial --- docs/user/develop/basic/publish.i18n.yaml | 4 ++-- docs/user/develop/basic/publish.md | 26 +++++++++++------------ docs/user/develop/basic/publish.zh.md | 26 +++++++++++------------ 3 files changed, 28 insertions(+), 28 deletions(-) diff --git a/docs/user/develop/basic/publish.i18n.yaml b/docs/user/develop/basic/publish.i18n.yaml index 694a7ff663..e640e971cb 100644 --- a/docs/user/develop/basic/publish.i18n.yaml +++ b/docs/user/develop/basic/publish.i18n.yaml @@ -2,5 +2,5 @@ # 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 docs/user/develop/basic/publish.md -publish.md: edc85b74049c5b993efb02663195fcc0001b4620 -publish.zh.md: cb824ad12eec0d706efa17b28f3bdc874ba59131 +publish.md: 8283f9e7ff0c28580343975d67c5715d17c53074 +publish.zh.md: 5a87901fe39aa00e94db81dc840e39a52e0cc88c diff --git a/docs/user/develop/basic/publish.md b/docs/user/develop/basic/publish.md index edc85b7404..8283f9e7ff 100644 --- a/docs/user/develop/basic/publish.md +++ b/docs/user/develop/basic/publish.md @@ -2,7 +2,7 @@ English | [中文](publish.zh.md) -The previous tutorials loaded a local plugin through a `--patch` overlay. This tutorial packages it as an installable **bundle**, installs it into a **profile** with `pnpm dsh plugin add`, and explains the layer order that determines the composed configuration. Complete [plugin configuration](./config.md) first. +The previous tutorials loaded a local plugin through a `--patch` overlay. This tutorial packages it as an installable **bundle**, installs it into a **profile** with `dsh plugin add`, and explains the layer order that determines the composed configuration. It assumes the `dsh` CLI is installed. Complete [plugin configuration](./config.md) first. ## Two concepts, two manifests @@ -11,11 +11,11 @@ Installation is built on two concepts. Both are described by a `package.json`, b - A **bundle** is an npm package that ships a configuration layer. Its manifest declares `dsh.bundle`, answering "what does this package contribute?": a patch file that inserts or overrides plugin rows. - A **profile** is a directory under `$DSH_HOME/profiles/` describing one runnable composition. Its manifest declares `dsh.profile`, answering "which bundles compose this setup, in what order?". -A bundle is what you author and distribute; a profile is what a user boots from this source checkout with `pnpm dsh --profile `. Nothing is both. +A bundle is what you author and distribute; a profile is what a user boots with `dsh --profile `. Nothing is both. ### The bundle manifest -From the repository root, create the package directory: +Create the package directory: ```sh mkdir -p hello-plugin @@ -59,7 +59,7 @@ Create `hello-plugin/cordis.patch.yml`. The patch is a YAML array like the `--pa name: dsh-hello-plugin ``` -A package without the `dsh.bundle` declaration still installs, but only as a plain dependency: `pnpm dsh plugin` prints a warning and activates no layer. Use that package format for a library that plugin packages import rather than a plugin users enable. +A package without the `dsh.bundle` declaration still installs, but only as a plain dependency: `dsh plugin` prints a warning and activates no layer. Use that package format for a library that plugin packages import rather than a plugin users enable. ### The profile manifest @@ -68,14 +68,14 @@ A profile directory holds two files: - `package.json` — the profile's out-of-tree plugin dependencies (managed by pnpm) plus the `dsh.profile` manifest with its ordered `bundles` list. - `cordis.patch.yml` — the user's own patch layer, applied after every bundle layer. -You never write a profile manifest by hand: `pnpm dsh plugin` creates and maintains it. The next section shows the result. +You never write a profile manifest by hand: `dsh plugin` creates and maintains it. The next section shows the result. ## Install into a profile -`pnpm dsh plugin --profile ` forwards to pnpm in the profile directory, so every pnpm verb works. From the repository root, install the package checkout: +`dsh plugin --profile ` forwards to pnpm in the profile directory, so every pnpm verb works. From the directory that contains `hello-plugin`, install the package checkout: ```sh -pnpm dsh plugin --profile demo add ./hello-plugin +dsh plugin --profile demo add ./hello-plugin ``` The first use initializes the profile (with `@deepseek-ai/dsh-base` as its first bundle), pnpm links the checkout, and `dsh` appends the bundle to `dsh.profile.bundles` because the package declares `dsh.bundle`: @@ -101,11 +101,11 @@ The first use initializes the profile (with `@deepseek-ai/dsh-base` as its first Verify the layer without booting, then boot: ```sh -pnpm dsh --profile demo --dump-config # shows a "# == dsh-hello-plugin" layer -pnpm dsh --profile demo +dsh --profile demo --dump-config # shows a "# == dsh-hello-plugin" layer +dsh --profile demo ``` -`pnpm dsh plugin --profile demo remove dsh-hello-plugin` removes both the dependency and the layer. +`dsh plugin --profile demo remove dsh-hello-plugin` removes both the dependency and the layer. ## The loading order @@ -153,7 +153,7 @@ On `--help`, the provider publishes no service, so those rows never activate. Lo Publishing to a registry is not required — users can install straight from a git host: ```sh -pnpm dsh plugin --profile demo add github:you/hello-plugin +dsh plugin --profile demo add github:you/hello-plugin ``` But a git install fetches **sources, not built artifacts**: nothing runs your `build` script, so a TypeScript package arrives without its `lib/` output and fails to load. Two things must happen, one on each side: @@ -172,8 +172,8 @@ Treat that allowance as what it is: **permission to execute the package's code o If you would rather not ask users for the allowance, distribute built artifacts instead — neither form needs any build permission: -- **Publish to npm** with `lib/` built at `pnpm publish` time; `pnpm dsh plugin add your-package` then installs prebuilt code. -- **Ship a tarball** from `pnpm pack`; users run `pnpm dsh plugin add ./hello-plugin-0.1.0.tgz`. +- **Publish to npm** with `lib/` built at `pnpm publish` time; `dsh plugin add your-package` then installs prebuilt code. +- **Ship a tarball** from `pnpm pack`; users run `dsh plugin add ./hello-plugin-0.1.0.tgz`. ## Next steps diff --git a/docs/user/develop/basic/publish.zh.md b/docs/user/develop/basic/publish.zh.md index cb824ad12e..5a87901fe3 100644 --- a/docs/user/develop/basic/publish.zh.md +++ b/docs/user/develop/basic/publish.zh.md @@ -2,7 +2,7 @@ [English](publish.md) | 中文 -前几篇教程通过 `--patch` overlay 加载本地插件。本教程把它打包成可安装的**组合包**(bundle),用 `pnpm dsh plugin add` 安装进一个 **profile**,并解释决定组合后配置的层顺序。请先完成[插件配置](./config.md)。 +前几篇教程通过 `--patch` overlay 加载本地插件。本教程把它打包成可安装的**组合包**(bundle),用 `dsh plugin add` 安装进一个 **profile**,并解释决定组合后配置的层顺序。本文假设 `dsh` CLI 已安装。请先完成[插件配置](./config.md)。 ## 两个概念,两种 manifest @@ -11,11 +11,11 @@ - **组合包**是附带一个配置层的 npm 包。它的 manifest 声明 `dsh.bundle`,回答的是"这个包贡献什么?":一个插入或覆盖插件行的 patch 文件。 - **profile** 是位于 `$DSH_HOME/profiles/` 下、描述一份可启动组合的目录。它的 manifest 声明 `dsh.profile`,回答的是"这套配置由哪些组合包按什么顺序组成?"。 -组合包是你编写并分发的东西;profile 是用户在当前源码 checkout 中用 `pnpm dsh --profile ` 启动的东西。没有东西同时是两者。 +组合包是你编写并分发的东西;profile 是用户用 `dsh --profile ` 启动的东西。没有东西同时是两者。 ### 组合包 manifest -在仓库根目录创建包目录: +创建包目录: ```sh mkdir -p hello-plugin @@ -59,7 +59,7 @@ export function apply() { name: dsh-hello-plugin ``` -没有 `dsh.bundle` 声明的包仍然可以安装,但只作为普通依赖:`pnpm dsh plugin` 会打印警告,且不激活任何层。如果一个库供插件包 import,而不是供用户启用,就使用这种包格式。 +没有 `dsh.bundle` 声明的包仍然可以安装,但只作为普通依赖:`dsh plugin` 会打印警告,且不激活任何层。如果一个库供插件包 import,而不是供用户启用,就使用这种包格式。 ### profile manifest @@ -68,14 +68,14 @@ profile 目录包含两个文件: - `package.json` — profile 的树外插件依赖(由 pnpm 管理),加上 `dsh.profile` manifest 及其有序的 `bundles` 列表。 - `cordis.patch.yml` — 用户自己的 patch 层,在每个组合包层之后应用。 -profile manifest 从不需要手写:`pnpm dsh plugin` 负责创建和维护它。下一节展示其结果。 +profile manifest 从不需要手写:`dsh plugin` 负责创建和维护它。下一节展示其结果。 ## 安装进 profile -`pnpm dsh plugin --profile ` 在 profile 目录内转发给 pnpm,因此所有 pnpm 子命令都可用。在仓库根目录安装该包的 checkout: +`dsh plugin --profile ` 在 profile 目录内转发给 pnpm,因此所有 pnpm 子命令都可用。在包含 `hello-plugin` 的目录中安装该包的 checkout: ```sh -pnpm dsh plugin --profile demo add ./hello-plugin +dsh plugin --profile demo add ./hello-plugin ``` 首次使用会初始化 profile(`@deepseek-ai/dsh-base` 作为它的第一个组合包),pnpm 链接该 checkout,而 `dsh` 因为这个包声明了 `dsh.bundle`,把它追加进 `dsh.profile.bundles`: @@ -101,11 +101,11 @@ pnpm dsh plugin --profile demo add ./hello-plugin 先不启动、只验证该层,再启动: ```sh -pnpm dsh --profile demo --dump-config # shows a "# == dsh-hello-plugin" layer -pnpm dsh --profile demo +dsh --profile demo --dump-config # shows a "# == dsh-hello-plugin" layer +dsh --profile demo ``` -`pnpm dsh plugin --profile demo remove dsh-hello-plugin` 会同时移除依赖和对应的层。 +`dsh plugin --profile demo remove dsh-hello-plugin` 会同时移除依赖和对应的层。 ## 加载顺序 @@ -153,7 +153,7 @@ pnpm dsh --profile demo 发布到注册表不是必须的——用户可以直接从 git 托管安装: ```sh -pnpm dsh plugin --profile demo add github:you/hello-plugin +dsh plugin --profile demo add github:you/hello-plugin ``` 但 git 安装拉取的是**源码,不是构建产物**:没有任何环节运行你的 `build` 脚本,因此 TypeScript 包到手时没有 `lib/` 输出,加载会失败。必须两边各做一件事: @@ -172,8 +172,8 @@ pnpm dsh plugin --profile demo add github:you/hello-plugin 如果不想让用户做这项授权,就改为分发构建产物——以下两种形式都不需要任何构建权限: -- **发布到 npm**,在 `pnpm publish` 时构建好 `lib/`;`pnpm dsh plugin add your-package` 安装的就是预构建代码。 -- **交付 tarball**:用 `pnpm pack` 打包;用户执行 `pnpm dsh plugin add ./hello-plugin-0.1.0.tgz`。 +- **发布到 npm**,在 `pnpm publish` 时构建好 `lib/`;`dsh plugin add your-package` 安装的就是预构建代码。 +- **交付 tarball**:用 `pnpm pack` 打包;用户执行 `dsh plugin add ./hello-plugin-0.1.0.tgz`。 ## 下一步 From 3a1a4e2cd864e449cf3cd9de837a29b2bea35f7c Mon Sep 17 00:00:00 2001 From: Turtle Date: Thu, 13 Aug 2026 14:30:17 +0800 Subject: [PATCH 08/24] docs: address fragment review feedback --- ...-13-published-document-fragments.i18n.yaml | 4 ++-- ...2026-08-13-published-document-fragments.md | 4 ++-- ...6-08-13-published-document-fragments.zh.md | 4 ++-- .agents/skills/dsh-doc-site-sync/SKILL.md | 4 +++- scripts/run-gates.ts | 2 +- scripts/verify-doc-site-fragments.spec.ts | 24 +++++++++++++++++-- scripts/verify-doc-site-fragments.ts | 24 +++++++++++++++---- 7 files changed, 51 insertions(+), 15 deletions(-) diff --git a/.agents/notes/implemented/process/2026-08-13-published-document-fragments.i18n.yaml b/.agents/notes/implemented/process/2026-08-13-published-document-fragments.i18n.yaml index 1051ae0b32..d90edf5075 100644 --- a/.agents/notes/implemented/process/2026-08-13-published-document-fragments.i18n.yaml +++ b/.agents/notes/implemented/process/2026-08-13-published-document-fragments.i18n.yaml @@ -2,5 +2,5 @@ # 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 .agents/notes/implemented/process/2026-08-13-published-document-fragments.md -2026-08-13-published-document-fragments.md: 9837d4ec1335f8da9910b3d2c2167f4c6d3a1aab -2026-08-13-published-document-fragments.zh.md: f4c68ae01082ab53a251c9951906538f0f4ae770 +2026-08-13-published-document-fragments.md: 4146a592a97b5d8b4e1d3fbabd0f035074cc8001 +2026-08-13-published-document-fragments.zh.md: 6cf76dfa349bbd61d1dd51cb683b696e4c405435 diff --git a/.agents/notes/implemented/process/2026-08-13-published-document-fragments.md b/.agents/notes/implemented/process/2026-08-13-published-document-fragments.md index 9837d4ec13..4146a592a9 100644 --- a/.agents/notes/implemented/process/2026-08-13-published-document-fragments.md +++ b/.agents/notes/implemented/process/2026-08-13-published-document-fragments.md @@ -10,9 +10,9 @@ English | [中文](2026-08-13-published-document-fragments.zh.md) ## Decision -`docs:build` and its MPA variant run `verify-doc-site-fragments` after VitePress emits `website/.dist`. The verifier parses every emitted HTML page, resolves each internal fragment link against VitePress clean URLs, and fails when the output is absent or either the target page or requested id is missing. Unit tests cover absent output, clean URLs, `.html` aliases, same-page links, encoded ids, missing ids, missing routes, and external-link exclusion. +`docs:build` and its MPA variant run `verify-doc-site-fragments` after VitePress emits `website/.dist`. The verifier parses every emitted HTML page, resolves each internal fragment link against VitePress clean URLs, and fails when the output is absent, routes are ambiguous, an href is malformed, or either the target page or requested id is missing. Unit tests cover those failures plus clean URLs, `.html` aliases, same-page links, encoded and literal ids, and external-link exclusion. -Generated config, tool, and persistence catalogs emit explicit GitHub-compatible id aliases before punctuation-heavy headings. Authored translated pages add explicit language-neutral aliases when their localized VitePress heading id differs from the shared fragment used by the bilingual pair. Source Markdown validation remains independent and continues to reject links that do not resolve under repository rendering. +Any fragment target heading whose GitHub id differs from its VitePress id carries an explicit GitHub-compatible alias. Authored English and translated pages place the alias before the heading; translated pages use the English id shared by the bilingual pair. Generated config, tool, and persistence catalogs emit the alias from their owning generator. Source Markdown validation remains independent and continues to reject links that do not resolve under repository rendering. ## Alternatives considered diff --git a/.agents/notes/implemented/process/2026-08-13-published-document-fragments.zh.md b/.agents/notes/implemented/process/2026-08-13-published-document-fragments.zh.md index f4c68ae010..6cf76dfa34 100644 --- a/.agents/notes/implemented/process/2026-08-13-published-document-fragments.zh.md +++ b/.agents/notes/implemented/process/2026-08-13-published-document-fragments.zh.md @@ -10,9 +10,9 @@ Status: implemented ## Decision -`docs:build` 及其 MPA 变体会在 VitePress 生成 `website/.dist` 后运行 `verify-doc-site-fragments`。该校验器解析每个生成的 HTML 页面,按照 VitePress clean URL 解析每个内部 fragment 链接,并在构建产物不存在、目标页面不存在或请求的 id 缺失时失败。单元测试覆盖缺失构建产物、clean URL、`.html` 别名、同页链接、编码 id、缺失 id、缺失路由和外部链接排除。 +`docs:build` 及其 MPA 变体会在 VitePress 生成 `website/.dist` 后运行 `verify-doc-site-fragments`。该校验器解析每个生成的 HTML 页面,按照 VitePress clean URL 解析每个内部 fragment 链接,并在构建产物不存在、路由有歧义、href 格式错误、目标页面不存在或请求的 id 缺失时失败。单元测试覆盖这些失败,以及 clean URL、`.html` 别名、同页链接、编码和字面 id 与外部链接排除。 -生成的配置、工具和持久化目录会在包含较多标点的标题前生成与 GitHub 兼容的显式 id 别名。如果翻译页面的 VitePress 本地化标题 id 与双语对侧文件共享的 fragment 不同,编写者会添加语言无关的显式别名。源码 Markdown 校验保持独立,仍会拒绝在仓库渲染规则下无法解析的链接。 +任何 GitHub id 与 VitePress id 不同的 fragment 目标标题都会带有与 GitHub 兼容的显式别名。英文手写页面和翻译页面会在标题前添加别名;翻译页面使用双语对侧文件共享的英文 id。生成的配置、工具和持久化目录由所属生成器输出别名。源码 Markdown 校验保持独立,仍会拒绝在仓库渲染规则下无法解析的链接。 ## Alternatives considered diff --git a/.agents/skills/dsh-doc-site-sync/SKILL.md b/.agents/skills/dsh-doc-site-sync/SKILL.md index bc5b3f48dd..91dd42ae8a 100644 --- a/.agents/skills/dsh-doc-site-sync/SKILL.md +++ b/.agents/skills/dsh-doc-site-sync/SKILL.md @@ -49,7 +49,7 @@ Write normal repository-relative Markdown links in canonical docs. The projector - An image is the exception: its file is copied into the generated tree and referenced from there, so the site serves it regardless of repository visibility. It must be a regular file inside the repository. - External URLs, site-absolute URLs, email links, and fragment-only links remain unchanged. - A missing repository-relative target fails projection instead of silently producing a broken link. -- Cross-language and generated-document fragments use the English GitHub heading id as their canonical id. If a translated or generated heading emits another id, place an explicit `` immediately before it; add generated aliases in the owning generator. +- Cross-page fragments use the English GitHub heading id as their canonical id. If an authored heading emits a different VitePress id, place an explicit `` immediately before it; add generated aliases in the owning generator. Do not write website-specific routes into canonical Markdown just to satisfy VitePress. Use `sourceAliases` for directory-style repository links that should resolve to a mapped index page. @@ -69,6 +69,8 @@ Run the focused website gate before treating the mapping as valid: pnpm docs:check ``` +If Markdown link checks pass but the site build reports a missing fragment, follow the `verify-doc-site-fragments` source and target paths. Preserve the English GitHub id with an explicit alias in authored Markdown or in the owning generator. + Before committing a documentation-site change, run: ```sh diff --git a/scripts/run-gates.ts b/scripts/run-gates.ts index 9673439fbc..27664fab5e 100644 --- a/scripts/run-gates.ts +++ b/scripts/run-gates.ts @@ -607,7 +607,7 @@ function docSyncLeafGates(options: { pnpmScript('translation-pairing', 'verify-translation-pairing', { label: 'translation pairing' }), pnpmScript('doc-budgets', 'verify-doc-budgets', { label: 'doc budgets' }), pnpmExec('docs-site-projection', ['vitest', 'run', 'scripts/project-doc-site.spec.ts', 'scripts/verify-doc-site-fragments.spec.ts'], { - label: 'documentation projection', + label: 'documentation site checks', }), // Keep the VitePress build itself in one gate because projection rewrites website/.generated. pnpmScript('docs-site-build', options.docsBuildScript ?? 'docs:build', { label: 'documentation build' }), diff --git a/scripts/verify-doc-site-fragments.spec.ts b/scripts/verify-doc-site-fragments.spec.ts index 2d041a33d5..d06d46f53b 100644 --- a/scripts/verify-doc-site-fragments.spec.ts +++ b/scripts/verify-doc-site-fragments.spec.ts @@ -38,9 +38,29 @@ describe('inspectSiteFragments', () => { it('resolves clean, encoded, and same-page routes', () => { const root = fixture() - writeFileSync(join(root, 'guide/encoded.html'), '

Encoded

self') + writeFileSync( + join(root, 'guide/encoded.html'), + '

Encoded

Literal

encodedliteral', + ) - expect(inspectSiteFragments(root)).toEqual({ checked: 5, broken: [] }) + expect(inspectSiteFragments(root)).toEqual({ checked: 6, broken: [] }) + }) + + it('rejects ambiguous built routes', () => { + const root = fixture() + writeFileSync(join(root, 'guide.html'), '

Flat

') + writeFileSync(join(root, 'guide/index.html'), '

Index

') + + expect(() => inspectSiteFragments(root)).toThrow('share route "/guide"') + }) + + it('rejects malformed fragment hrefs', () => { + const root = fixture() + writeFileSync(join(root, 'guide/invalid.html'), 'invalid') + + expect(() => inspectSiteFragments(root)).toThrow( + 'guide/invalid.html has invalid fragment href "http://[invalid]#fragment"', + ) }) it('reports missing ids and missing built routes', () => { diff --git a/scripts/verify-doc-site-fragments.ts b/scripts/verify-doc-site-fragments.ts index d42cc752b0..c086dedccb 100644 --- a/scripts/verify-doc-site-fragments.ts +++ b/scripts/verify-doc-site-fragments.ts @@ -3,7 +3,8 @@ * VitePress use different heading-slug algorithms, so source-link validation * alone cannot prove that a published fragment exists. * - * Run with `tsx scripts/verify-doc-site-fragments.ts` after `docs:build`. + * This runs as part of `docs:build` and can also run directly after a build + * with `tsx scripts/verify-doc-site-fragments.ts`. */ import { globSync, readFileSync } from 'node:fs' @@ -61,7 +62,9 @@ function aliasesFor(page: BuiltPage): string[] { function decodedFragment(hash: string): string { try { return decodeURIComponent(hash.slice(1)) - } catch { + } catch (error) { + if (!(error instanceof URIError)) throw error + // URIError means malformed percent encoding; preserve the literal id for comparison. return hash.slice(1) } } @@ -90,7 +93,15 @@ export function inspectSiteFragments(distRoot: string): SiteFragmentReport { const byRoute = new Map() for (const page of pages) { - for (const alias of aliasesFor(page)) byRoute.set(alias, page) + for (const alias of aliasesFor(page)) { + const existing = byRoute.get(alias) + if (existing !== undefined && existing !== page) { + throw new Error( + `verify-doc-site-fragments: built pages ${existing.file} and ${page.file} share route ${JSON.stringify(alias)}.`, + ) + } + byRoute.set(alias, page) + } } const origin = 'https://dsh-docs.invalid' @@ -103,8 +114,11 @@ export function inspectSiteFragments(distRoot: string): SiteFragmentReport { let targetUrl: URL try { targetUrl = new URL(href, `${origin}${page.route}`) - } catch { - continue + } catch (error) { + throw new Error( + `verify-doc-site-fragments: ${page.file} has invalid fragment href ${JSON.stringify(href)}.`, + { cause: error }, + ) } if (targetUrl.origin !== origin || targetUrl.hash === '') continue const fragment = decodedFragment(targetUrl.hash) From 8502a38cb6fd5d816b8fe263bf99a62686575d87 Mon Sep 17 00:00:00 2001 From: "jy.ding" Date: Thu, 13 Aug 2026 14:40:28 +0800 Subject: [PATCH 09/24] docs: add localized community channels --- README.i18n.yaml | 4 ++-- README.md | 2 ++ README.zh.md | 17 +++++++++++++++++ assets/community-wechat-official-account.png | Bin 0 -> 156490 bytes assets/community-wecom-assistant.png | Bin 0 -> 26676 bytes 5 files changed, 21 insertions(+), 2 deletions(-) create mode 100644 assets/community-wechat-official-account.png create mode 100644 assets/community-wecom-assistant.png diff --git a/README.i18n.yaml b/README.i18n.yaml index 573a3ffedb..ee4bfdf7f1 100644 --- a/README.i18n.yaml +++ b/README.i18n.yaml @@ -2,5 +2,5 @@ # 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 README.md -README.md: 646262f0b8317a48cab9aa84bab87b7a2b8e85ec -README.zh.md: 892da6f48cd8b60dd61332ec412f604a8062b27d +README.md: b1d84d6ebd20843f6c2e3b7ccd22cccb2df0d104 +README.zh.md: a3960d3d342d005230f8a40e5c43d4bac882cb7a diff --git a/README.md b/README.md index 646262f0b8..b1d84d6ebd 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,8 @@ pnpm dsh web - Feel free to submit feedback or bug reports through [GitHub Discussions](https://github.com/deepseek-ai/deepseek-harness/discussions). - Add the [`dsh-plugin`](https://github.com/topics/dsh-plugin) topic to your plugin repository for discoverability. +

Join DeepSeek Harness Discord community.

+ ## Contributing See [CONTRIBUTING.md](CONTRIBUTING.md). diff --git a/README.zh.md b/README.zh.md index 892da6f48c..a3960d3d34 100644 --- a/README.zh.md +++ b/README.zh.md @@ -39,6 +39,23 @@ pnpm dsh web - 欢迎通过 [GitHub Discussions](https://github.com/deepseek-ai/deepseek-harness/discussions) 提交反馈或 bug 报告。 - 为你的插件仓库添加 [`dsh-plugin`](https://github.com/topics/dsh-plugin) 主题,便于被发现。 + + + + + + + + + + + + + + + +
企微小助手入群问卷微信公众号
DeepSeek Harness 企微小助手二维码DeepSeek Harness 入群问卷二维码DeepSeek Harness 团队微信公众号二维码
+ ## 参与贡献 参见 [CONTRIBUTING.md](CONTRIBUTING.md)。 diff --git a/assets/community-wechat-official-account.png b/assets/community-wechat-official-account.png new file mode 100644 index 0000000000000000000000000000000000000000..32e36e34797a075d1118299bc772bf390c000761 GIT binary patch literal 156490 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 N?;Y$F@vk?-zEvj^J`AhQirHis&0WUh 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 literal 0 HcmV?d00001 diff --git a/assets/community-wecom-assistant.png b/assets/community-wecom-assistant.png new file mode 100644 index 0000000000000000000000000000000000000000..71fc1aba61f49f7183ce0ee292daf81c16b62074 GIT binary patch literal 26676 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 literal 0 HcmV?d00001 From 2be3e129650b65575f2d64c7eede6fa1115d5912 Mon Sep 17 00:00:00 2001 From: _Kerman Date: Thu, 13 Aug 2026 15:09:21 +0800 Subject: [PATCH 10/24] fix(apiproxy): harden cold session metadata probing --- ...-bounded-cold-blank-verification.i18n.yaml | 4 +- ...6-08-13-bounded-cold-blank-verification.md | 12 ++-- ...8-13-bounded-cold-blank-verification.zh.md | 12 ++-- ...7-29-durable-last-activity-index.i18n.yaml | 4 +- .../2026-07-29-durable-last-activity-index.md | 4 +- ...26-07-29-durable-last-activity-index.zh.md | 4 +- docs/subsystems/session.i18n.yaml | 4 +- docs/subsystems/session.md | 2 +- docs/subsystems/session.zh.md | 2 +- packages/core/session/src/index.ts | 2 +- packages/core/session/src/repair.ts | 21 +----- packages/core/session/tests/repair.spec.ts | 43 +----------- packages/host/apiproxy/README.i18n.yaml | 4 +- packages/host/apiproxy/README.md | 4 +- packages/host/apiproxy/README.zh.md | 4 +- packages/host/apiproxy/src/api-proxy.ts | 56 +++++++++------- packages/host/apiproxy/src/api/sessions.ts | 20 +++--- .../apiproxy/tests/api-proxy-cold.spec.ts | 67 ++++++++++++++++++- 18 files changed, 138 insertions(+), 131 deletions(-) diff --git a/.agents/notes/implemented/bug-fix/2026-08-13-bounded-cold-blank-verification.i18n.yaml b/.agents/notes/implemented/bug-fix/2026-08-13-bounded-cold-blank-verification.i18n.yaml index d57e57211b..2cd2577fc5 100644 --- a/.agents/notes/implemented/bug-fix/2026-08-13-bounded-cold-blank-verification.i18n.yaml +++ b/.agents/notes/implemented/bug-fix/2026-08-13-bounded-cold-blank-verification.i18n.yaml @@ -2,5 +2,5 @@ # 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 .agents/notes/implemented/bug-fix/2026-08-13-bounded-cold-blank-verification.md -2026-08-13-bounded-cold-blank-verification.md: 330f5acbd520487732bc72fe75f09496aaa0c028 -2026-08-13-bounded-cold-blank-verification.zh.md: 15c0d28be8c15f7076ac90a50bb023cc42fbbdd6 +2026-08-13-bounded-cold-blank-verification.md: bf8d167d742001ce65b3e96713a9603adb19603e +2026-08-13-bounded-cold-blank-verification.zh.md: 7cfef77a02308a8e75281877f8a774b41bb9559d diff --git a/.agents/notes/implemented/bug-fix/2026-08-13-bounded-cold-blank-verification.md b/.agents/notes/implemented/bug-fix/2026-08-13-bounded-cold-blank-verification.md index 330f5acbd5..bf8d167d74 100644 --- a/.agents/notes/implemented/bug-fix/2026-08-13-bounded-cold-blank-verification.md +++ b/.agents/notes/implemented/bug-fix/2026-08-13-bounded-cold-blank-verification.md @@ -14,15 +14,15 @@ The same cold list used the JSONL artifact mtime for `updatedAt`. Opening a Sess `dsh-host-apiproxy` registers `sessionListMetadata`, a projection containing `blank` and `lastPromptAt`. The attached summary folds the same functions directly over the live log. `blank` changes only from true to false on `turn/start`; `lastPromptAt` changes only on a `user/message` whose source kind is `user`. -A cold summary trusts cached `blank: false`, because a checkpoint prefix containing `turn/start` remains non-blank. Cached `blank: true` and a cache miss do not prove the current log is blank. When persistence exposes a physical artifact through `locate()` and its size is at most `coldBlankProbeMaxBytes` (default 1 KiB per Session), the gateway calls `readFrom(id, 0)` and verifies whether the stored prefix contains `turn/start`. Files above the bound, backends without a location, vanished artifacts, and failed reads all produce `blank: false`, keeping the Session visible. +A cold summary trusts cached `blank: false`, because a checkpoint prefix containing `turn/start` remains non-blank. Cached `blank: true` and a cache miss do not prove the current log is blank. When persistence exposes a physical artifact through `locate()` and its observed size is at most the `coldBlankProbeMaxBytes` eligibility threshold (default 1 KiB per Session), the gateway calls `readFrom(id, 0)` and folds exact list metadata from the stored prefix. Files above the threshold, backends without a location, vanished artifacts, and failed reads all produce `blank: false`, keeping the Session visible. -`updatedAt` is the later of `createdAt` and `lastPromptAt`. A cold cache miss or stale checkpoint therefore orders the Session too old rather than promoting it from an unrelated file write. The bounded blank read does not replace missing recency metadata. +`updatedAt` is the later of `createdAt` and `lastPromptAt`. An eligible artifact read supplies exact `lastPromptAt` at no additional I/O cost; other cache misses or stale checkpoints order the Session too old rather than promoting it from an unrelated file write. After each asynchronous cold read, the gateway checks the live store again and replaces the cold result with an attached summary when another request resumed that Session meanwhile. ## Alternatives considered **Trust cached `blank: true`.** Rejected because the projection cache deliberately permits a persisted log to advance beyond its checkpoint. A crash or fail-soft write failure after the first `turn/start` would hide a real conversation and could make the client reuse it as New Session. -**Read every cold log.** Rejected because list latency and I/O would scale with total stored conversation bytes. The physical-size bound targets the small historical artifacts that can be checked cheaply and degrades larger unknowns toward visibility. +**Read every cold log.** Rejected because list latency and I/O would scale with total stored conversation bytes. The physical-size eligibility check targets small historical artifacts that can be checked cheaply and degrades larger unknowns toward visibility. It intentionally does not add a persistence operation solely to make the threshold atomic with the read: concurrent growth may increase one probe's read cost, but the additional events can only preserve visibility or change a blank result to non-blank. **Store blankness and recency in an authoritative persistence index.** Deferred because JSONL has an immutable first line and would require a second durable artifact with ordered updates, while SQLite would require a schema field. The broader exact-index design remains in the [last-activity proposal](../../proposed/architecture/2026-07-29-durable-last-activity-index.md). @@ -30,8 +30,8 @@ A cold summary trusts cached `blank: false`, because a checkpoint prefix contain ## Consequences -Existing small blank JSONL artifacts are hidden without depending on projection-cache availability, and a stale cache cannot hide a stored `turn/start`. A cold list may read each artifact whose physical size is within the configured bound when its cache does not already prove non-blank. The default bound applies to compressed bytes for the shipped Zstandard JSONL backend. +Existing small blank JSONL artifacts are hidden without depending on projection-cache availability, and a stale cache cannot hide a stored `turn/start`. A cold list may read each artifact whose observed physical size is within the configured threshold when its cache does not already prove non-blank. The default threshold compares compressed bytes for the shipped Zstandard JSONL backend. -Blank artifacts above the bound and blank Sessions on location-less backends remain visible. Missing or delayed recency cache entries fall back to `createdAt`. These are conservative degradations: the UI may show an extra empty row or order a Session too low, but it does not hide a conversation or promote one because it was merely opened. +Blank artifacts above the threshold and blank Sessions on location-less backends remain visible. Missing or delayed recency cache entries for artifacts that are not read fall back to `createdAt`. These are conservative degradations: the UI may show an extra empty row or order a Session too low, but it does not hide a conversation or promote one because it was merely opened. -The gateway-owned projection is an effect of the gateway fiber; unloading the gateway removes the key. Unit coverage pins exact-threshold probing, stale-true rejection, monotonic false reuse, fallback direction, human-prompt recency, and fiber disposal. A keyless Web snapshot boots the shipped compressed JSONL composition, seeds a small cold blank artifact without a cache row, and verifies that the sidebar omits it. +The gateway-owned projection is an effect of the gateway fiber; unloading the gateway removes the key. Unit coverage pins exact-threshold eligibility, stale-true rejection, monotonic false reuse, exact small-log recency, live-attachment races, fallback direction, human-prompt recency, and fiber disposal. A keyless Web snapshot boots the shipped compressed JSONL composition, seeds a small cold blank artifact without a cache row, and verifies that the sidebar omits it. diff --git a/.agents/notes/implemented/bug-fix/2026-08-13-bounded-cold-blank-verification.zh.md b/.agents/notes/implemented/bug-fix/2026-08-13-bounded-cold-blank-verification.zh.md index 15c0d28be8..7cfef77a02 100644 --- a/.agents/notes/implemented/bug-fix/2026-08-13-bounded-cold-blank-verification.zh.md +++ b/.agents/notes/implemented/bug-fix/2026-08-13-bounded-cold-blank-verification.zh.md @@ -14,15 +14,15 @@ Web 会话树会隐藏空白 Session,并把当前选中的空白项复用为 N `dsh-host-apiproxy` 注册 `sessionListMetadata` 投影,其中包含 `blank` 与 `lastPromptAt`。已附加摘要直接用同一组函数折叠实时日志。`blank` 只在 `turn/start` 时从 true 单调变为 false;`lastPromptAt` 只在来源 kind 为 `user` 的 `user/message` 上更新。 -冷摘要信任缓存的 `blank: false`,因为已包含 `turn/start` 的 checkpoint 前缀会始终保持非空。缓存的 `blank: true` 和 cache miss 都无法证明当前日志为空。当 persistence 通过 `locate()` 暴露物理工件,且其大小不超过 `coldBlankProbeMaxBytes`(默认每个 Session 1 KiB)时,网关调用 `readFrom(id, 0)`,验证已存前缀是否含有 `turn/start`。超过上限的文件、不提供位置的后端、已消失的工件和读取失败都产生 `blank: false`,让 Session 保持可见。 +冷摘要信任缓存的 `blank: false`,因为已包含 `turn/start` 的 checkpoint 前缀会始终保持非空。缓存的 `blank: true` 和 cache miss 都无法证明当前日志为空。当 persistence 通过 `locate()` 暴露物理工件,且其观测大小不超过 `coldBlankProbeMaxBytes` 资格阈值(默认每个 Session 1 KiB)时,网关调用 `readFrom(id, 0)`,从已存前缀折叠精确列表元数据。超过阈值的文件、不提供位置的后端、已消失的工件和读取失败都产生 `blank: false`,让 Session 保持可见。 -`updatedAt` 取 `createdAt` 与 `lastPromptAt` 中较晚者。因此冷 cache miss 或陈旧 checkpoint 只会让 Session 排得偏旧,而不会因无关的文件写入被提升。有界 blank 读取不用于补齐缺失的最近时间元数据。 +`updatedAt` 取 `createdAt` 与 `lastPromptAt` 中较晚者。符合资格的工件读取无需额外 I/O 即可提供精确 `lastPromptAt`;其他 cache miss 或陈旧 checkpoint 只会让 Session 排得偏旧,而不会因无关的文件写入被提升。每次异步冷读取后,网关都会再次检查实时 store;若另一请求期间已恢复该 Session,则用已附加摘要替换冷结果。 ## Alternatives considered **信任缓存的 `blank: true`。** 拒绝,因为 projection cache 有意允许持久日志前进到 checkpoint 之后。首个 `turn/start` 之后若发生崩溃或 fail-soft 写入失败,真实对话就会被隐藏,客户端还可能把它复用为 New Session。 -**读取每一份冷日志。** 拒绝,因为列表延迟与 I/O 会随所有已存对话的总字节数增长。物理大小上限只针对能够低成本核验的小型历史工件,更大的未知项则向保持可见降级。 +**读取每一份冷日志。** 拒绝,因为列表延迟与 I/O 会随所有已存对话的总字节数增长。物理大小资格检查只针对能够低成本核验的小型历史工件,更大的未知项则向保持可见降级。该检查有意不为“让阈值与读取原子化”单独新增 persistence 操作:并发增长可能增加一次探测的读取成本,但新增事件只会保持可见,或把空白结果改为非空。 **把空白状态与最近时间存入权威 persistence index。** 暂缓,因为 JSONL 的首行不可变,需要增加带有顺序写入要求的第二份持久工件;SQLite 则需要 schema 字段。更广泛的精确索引设计仍由[最后活动提案](../../proposed/architecture/2026-07-29-durable-last-activity-index.md)负责。 @@ -30,8 +30,8 @@ Web 会话树会隐藏空白 Session,并把当前选中的空白项复用为 N ## Consequences -既有的小型空白 JSONL 工件无需依赖 projection cache 是否存在即可被隐藏,陈旧 cache 也无法隐藏已存的 `turn/start`。对于 cache 尚不能证明非空,且物理大小在配置上限内的每个 Session,冷列表可能读取其工件。对默认交付的 Zstandard JSONL 后端,该上限作用于压缩后的字节数。 +既有的小型空白 JSONL 工件无需依赖 projection cache 是否存在即可被隐藏,陈旧 cache 也无法隐藏已存的 `turn/start`。对于 cache 尚不能证明非空,且观测物理大小在配置阈值内的每个 Session,冷列表可能读取其工件。对默认交付的 Zstandard JSONL 后端,该阈值比较压缩后的字节数。 -超过上限的空白工件,以及来自不提供位置的后端的空白 Session 会保持可见。缺失或延迟的最近时间 cache 会回退到 `createdAt`。这些都是保守降级:UI 可能多显示一条空记录,或把 Session 排得偏低,但不会隐藏真实对话,也不会因为单纯打开而把会话提升到前面。 +超过阈值的空白工件,以及来自不提供位置的后端的空白 Session 会保持可见。对于未被读取的工件,缺失或延迟的最近时间 cache 会回退到 `createdAt`。这些都是保守降级:UI 可能多显示一条空记录,或把 Session 排得偏低,但不会隐藏真实对话,也不会因为单纯打开而把会话提升到前面。 -网关自有投影是网关 fiber 的 effect;卸载网关会移除该 key。单元覆盖固定了临界大小探测、拒绝陈旧 true、复用单调 false、回退方向、真人 prompt 最近时间和 fiber 销毁。无密钥 Web snapshot 会启动发行版的压缩 JSONL 组合,在没有 cache row 的情况下播种一份小型冷空白工件,并验证侧栏不展示它。 +网关自有投影是网关 fiber 的 effect;卸载网关会移除该 key。单元覆盖固定了临界大小资格、拒绝陈旧 true、复用单调 false、小日志精确最近时间、实时附加竞态、回退方向、真人 prompt 最近时间和 fiber 销毁。无密钥 Web snapshot 会启动发行版的压缩 JSONL 组合,在没有 cache row 的情况下播种一份小型冷空白工件,并验证侧栏不展示它。 diff --git a/.agents/notes/proposed/architecture/2026-07-29-durable-last-activity-index.i18n.yaml b/.agents/notes/proposed/architecture/2026-07-29-durable-last-activity-index.i18n.yaml index d9e26b561d..e8355305ed 100644 --- a/.agents/notes/proposed/architecture/2026-07-29-durable-last-activity-index.i18n.yaml +++ b/.agents/notes/proposed/architecture/2026-07-29-durable-last-activity-index.i18n.yaml @@ -2,5 +2,5 @@ # 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 .agents/notes/proposed/architecture/2026-07-29-durable-last-activity-index.md -2026-07-29-durable-last-activity-index.md: b530508877adf3a66b158ff659441c4179878d00 -2026-07-29-durable-last-activity-index.zh.md: 4c099bc835c4708fda09811fafd6dabb65dbb575 +2026-07-29-durable-last-activity-index.md: 99e50dd40b789db5d896cb7f9e25fa8893b02ae2 +2026-07-29-durable-last-activity-index.zh.md: e317fb192d53353295e6b52f631707ba6b400b66 diff --git a/.agents/notes/proposed/architecture/2026-07-29-durable-last-activity-index.md b/.agents/notes/proposed/architecture/2026-07-29-durable-last-activity-index.md index b530508877..99e50dd40b 100644 --- a/.agents/notes/proposed/architecture/2026-07-29-durable-last-activity-index.md +++ b/.agents/notes/proposed/architecture/2026-07-29-durable-last-activity-index.md @@ -10,7 +10,7 @@ A cold (persisted, unattached) session has no authoritative stored answer to "wh The gateway previously used JSONL artifact mtime when available. mtime answers a different question: when the artifact was last written. Every durable write refreshes it, including a truncate-repair of a torn tail, synthetic closers that balance an interrupted turn, and the [`session/end-seed` boundary](../../implemented/architecture/2026-07-30-session-end-seed-log-boundary.md) appended during pickup. That approximation promoted a Session merely because it was opened. The [bounded cold blank verification](../../implemented/bug-fix/2026-08-13-bounded-cold-blank-verification.md) removed mtime ordering and accepted the cache's conservative "too old" failure direction as an interim tradeoff. -An attached summary can fold the live event log and select the latest human-authored `user/message`, but the cold path deliberately does not read large logs. Reading every log to compute `updatedAt` would make `list()` scale with total conversation bytes rather than Session count. The 1 KiB cold read introduced for blank verification does not solve recency: it is conditional, targets only small artifacts, and does not make large-log ordering exact. +An attached summary can fold the live event log and select the latest human-authored `user/message`, but the cold path deliberately does not read large logs. Reading every log to compute `updatedAt` would make `list()` scale with total conversation bytes rather than Session count. The 1 KiB cold read used for metadata verification makes eligible small-artifact recency exact, but it does not make large-log ordering exact. Making cold ordering exact remains a durable-format decision, which is why it is scoped here rather than in the gateway workaround. @@ -61,7 +61,7 @@ Three questions must be answered before implementation, and none of them is sett ## Related -- [Bounded cold blank verification](../../implemented/bug-fix/2026-08-13-bounded-cold-blank-verification.md) — removes mtime ordering, defines the interim projection-cache fallback, and limits direct cold reads to blankness checks. +- [Bounded cold blank verification](../../implemented/bug-fix/2026-08-13-bounded-cold-blank-verification.md) — removes mtime ordering, defines the interim projection-cache fallback, and limits direct cold reads to small-artifact metadata verification. - [The end-seed log boundary](../../implemented/architecture/2026-07-30-session-end-seed-log-boundary.md) — one of the non-prompt writes that made mtime unsuitable. - [Session persistence](../../implemented/architecture/2026-06-14-session-persistence.md) — the append-only and never-rewrite invariants that rule out a mutable JSONL header field. - [Shared persistence write coordinator](../../implemented/architecture/2026-06-18-shared-persistence-write-coordinator.md) — the append path a stored field would hook into. diff --git a/.agents/notes/proposed/architecture/2026-07-29-durable-last-activity-index.zh.md b/.agents/notes/proposed/architecture/2026-07-29-durable-last-activity-index.zh.md index 4c099bc835..e317fb192d 100644 --- a/.agents/notes/proposed/architecture/2026-07-29-durable-last-activity-index.zh.md +++ b/.agents/notes/proposed/architecture/2026-07-29-durable-last-activity-index.zh.md @@ -10,7 +10,7 @@ Status: proposed 网关以前会在可用时采用 JSONL 产物的 mtime。mtime 回答的是另一件事:这份产物上次是什么时候被写入。每一次持久写入都会刷新它,包括对撕裂尾部的截断修复、平衡中断轮次的合成 closer,以及拾起时追加的 [`session/end-seed` 边界](../../implemented/architecture/2026-07-30-session-end-seed-log-boundary.md)。这套近似会让 Session 仅仅因为被打开就提升排序。[有界冷空白验证](../../implemented/bug-fix/2026-08-13-bounded-cold-blank-verification.md)移除了 mtime 排序,并把 cache 保守的「过旧」错误方向作为现阶段取舍。 -已附加摘要可以折叠实时事件日志并选择最新的真人 `user/message`,但冷路径有意不读取大日志。为计算 `updatedAt` 而读取每一份日志,会让 `list()` 的开销随对话总字节数而非 Session 数量增长。为空白验证引入的 1 KiB 冷读取并不能解决最近时间:它是条件式的,只针对小产物,也不能让大日志的排序精确。 +已附加摘要可以折叠实时事件日志并选择最新的真人 `user/message`,但冷路径有意不读取大日志。为计算 `updatedAt` 而读取每一份日志,会让 `list()` 的开销随对话总字节数而非 Session 数量增长。用于 metadata 验证的 1 KiB 冷读取可以让符合条件的小产物得到精确的最近时间,但不能让大日志的排序精确。 让冷排序变得精确仍是一项持久格式决策,因此其范围留在本文,而不是网关 workaround 中。 @@ -61,7 +61,7 @@ Status: proposed ## 相关 -- [有界冷空白验证](../../implemented/bug-fix/2026-08-13-bounded-cold-blank-verification.md)——移除 mtime 排序,定义 projection cache 的过渡回退,并把直接冷读取限制为空白检查。 +- [有界冷空白验证](../../implemented/bug-fix/2026-08-13-bounded-cold-blank-verification.md)——移除 mtime 排序,定义 projection cache 的过渡回退,并把直接冷读取限制为小产物 metadata 验证。 - [种子结束日志边界](../../implemented/architecture/2026-07-30-session-end-seed-log-boundary.md)——让 mtime 不适用的非 prompt 写入之一。 - [会话持久化](../../implemented/architecture/2026-06-14-session-persistence.md)——仅追加与绝不重写这两条不变式,正是它们排除了可变的 JSONL header 字段。 - [共享持久化写入协调器](../../implemented/architecture/2026-06-18-shared-persistence-write-coordinator.md)——一个已存储字段将挂入的那条追加路径。 diff --git a/docs/subsystems/session.i18n.yaml b/docs/subsystems/session.i18n.yaml index 8ee4185e54..d0e0ff2e32 100644 --- a/docs/subsystems/session.i18n.yaml +++ b/docs/subsystems/session.i18n.yaml @@ -2,5 +2,5 @@ # 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 docs/subsystems/session.md -session.md: 4c40971fe58952b32635aa5eb767a42f73108b00 -session.zh.md: d958b03fcdad91b58cc277636e599cce46a2c7fb +session.md: 760a3042724472b5f518896b8ff0e56bcfec2799 +session.zh.md: 8c56029af5144569f1ab6df73a8fe2278f9ef5b4 diff --git a/docs/subsystems/session.md b/docs/subsystems/session.md index 4c40971fe5..760a304272 100644 --- a/docs/subsystems/session.md +++ b/docs/subsystems/session.md @@ -586,7 +586,7 @@ An explicitly supplied empty seed writes `session/end-seed` at seq 0, which dist It exists because seed history and live work are otherwise byte-identical, which defeats any plugin owning a standalone open/close bracket: an unmatched `compaction/start` reads the same whether the writer crashed mid-compaction or is compacting right now. An opening marker before `session/end-seed` came from the constructor seed and belongs to an ended lifecycle, whatever ended it (a crash, a succeeding process, or a fork out of a still-running parent), so its owner may treat it as dead. That covers only brackets *this* session inherited: a concurrently live session holding an open bracket over the same history has its own boundary elsewhere, so tolerating concurrent writers needs a liveness signal beyond the log. Core writes the boundary and reads nothing from it — a bracket's vocabulary stays with its owning plugin, which is why crash repair closes turn/step/tool boundaries and never `compaction/*`. -Activity ordering excludes the boundary through `lastActivityTime(events)`: picking a session up is not work, and lazy resume means browsing writes one, so a resume picker or session list ordering by log tail would float every opened session to the top. +Consumers that order Sessions by human activity exclude this boundary: picking a Session up is not work, so ordering by the log tail would float every opened Session to the top. ## Plugin-contributed log-only events diff --git a/docs/subsystems/session.zh.md b/docs/subsystems/session.zh.md index d958b03fcd..8c56029af5 100644 --- a/docs/subsystems/session.zh.md +++ b/docs/subsystems/session.zh.md @@ -590,7 +590,7 @@ interface TurnEndReasonMap { 它之所以必要,是因为种子历史与实时工作在字节层面完全相同,这会让任何拥有独立开/闭括号的插件失效:一个未配对的 `compaction/start`,无论写入方是在压缩中途崩溃、还是此刻正在压缩,读起来都一样。在 `session/end-seed` 之前的开启标记来自构造种子,并且属于一个已结束的生命周期,无论结束原因为何(崩溃、进程接替,或从仍在运行的父会话 fork 出来),因此其所有方可以视之为已死。这只覆盖*本*会话继承的括号:另一个并发存活的会话可能在同一段历史上持有开放括号,而它自己的边界在别处,因此容忍并发写入方还需要日志之外的存活信号。核心写入该边界但不从中读取任何内容——括号的词汇表仍归其所属插件,这也正是崩溃修复只关闭轮次/步骤/工具边界而从不处理 `compaction/*` 的原因。 -活动排序通过 `lastActivityTime(events)` 排除该边界:接手会话不算工作,而惰性恢复意味着浏览就会写入一个,因此按日志尾部排序的恢复选择器或会话列表会把每个打开过的会话顶到最前。 +按真人活动排序 Session 的消费方会排除该边界:接手 Session 不算工作,因此按日志尾部排序会把每个打开过的 Session 顶到最前。 ## 插件贡献的仅日志事件 diff --git a/packages/core/session/src/index.ts b/packages/core/session/src/index.ts index d2e72b4676..2d82a88623 100644 --- a/packages/core/session/src/index.ts +++ b/packages/core/session/src/index.ts @@ -26,7 +26,7 @@ export type { SessionPreparationOptions } from './preparation.ts' export type { AssistantMessage, ToolResultMessage, UserMessage } from '@deepseek-ai/dsh-llm' export { isJsonValue, snapshotJsonValue } from './json.ts' export type { JsonValue } from './json.ts' -export { interruptedTurnClosers, lastActivityTime, TOOL_NOT_STARTED, TOOL_OUTCOME_UNKNOWN } from './repair.ts' +export { interruptedTurnClosers, TOOL_NOT_STARTED, TOOL_OUTCOME_UNKNOWN } from './repair.ts' export { decodeStorageRecord, packChunkRuns } from './chunk-rows.ts' export type { ChunkRow, StorageRecord } from './chunk-rows.ts' export type { SessionSurface, SurfaceFoldReplacement, SurfaceFoldResult } from './surface.ts' diff --git a/packages/core/session/src/repair.ts b/packages/core/session/src/repair.ts index 1203bed341..d91cb72494 100644 --- a/packages/core/session/src/repair.ts +++ b/packages/core/session/src/repair.ts @@ -1,10 +1,7 @@ /** * Crash-recovery repair for an interrupted session log. It preserves a fully * written final turn and supplies the missing tool, step, and turn boundaries - * needed to resume with a provider-valid transcript, plus the activity-time - * read that must skip the end-seed boundary — which this module does - * not write (`Session`'s constructor does) but whose synthetic closers can - * inherit that boundary's timestamp, the one real coupling between the two. + * needed to resume with a provider-valid transcript. * @module @deepseek-ai/dsh-session/repair */ @@ -12,22 +9,6 @@ import { MessageId, freezeMessage, type CallId } from '@deepseek-ai/dsh-llm' import type { ToolResultMessage } from '@deepseek-ai/dsh-llm' import type { SessionEvent } from './types.ts' -/** - * The `time` of the log's last event representing actual work, skipping the - * `session/end-seed` boundary — picking a session up is not activity, so - * activity ordering must exclude it. - * - * Excluded by type, so a pickup time still leaks when a boundary is the last - * event of an open turn: {@link interruptedTurnClosers} copies it onto the - * synthetic `turn/end`, which this counts as work. Reachable only by seeding an - * unbalanced log directly — `load()` balances first. - * @param events - the log to scan, in seq order. - * @returns the latest non-boundary event's `time`, or undefined when there is none. - */ -export function lastActivityTime(events: readonly SessionEvent[]): number | undefined { - return events.findLast(event => event.type !== 'session/end-seed')?.time -} - /** Recovery code for an assistant tool request that never reached a recorded call start. */ export const TOOL_NOT_STARTED = 'TOOL_NOT_STARTED' diff --git a/packages/core/session/tests/repair.spec.ts b/packages/core/session/tests/repair.spec.ts index b656a7116c..941819c0af 100644 --- a/packages/core/session/tests/repair.spec.ts +++ b/packages/core/session/tests/repair.spec.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest' import { CallId , createMessage, createToolResultMessage } from '@deepseek-ai/dsh-llm' -import { interruptedTurnClosers, lastActivityTime, TOOL_NOT_STARTED, TOOL_OUTCOME_UNKNOWN } from '../src/index.ts' +import { interruptedTurnClosers, TOOL_NOT_STARTED, TOOL_OUTCOME_UNKNOWN } from '../src/index.ts' import type { SessionEvent, SurfaceEvent } from '../src/index.ts' /** @@ -273,44 +273,3 @@ describe('interruptedTurnClosers', () => { expect(closers.map(e => e.type)).toEqual(['step/end', 'turn/end']) }) }) - -describe('lastActivityTime', () => { - const endSeedAt = (seq: number, time: number): SessionEvent => - ({ type: 'session/end-seed', seq, time, data: {} }) - - it('has no answer for an empty log', () => { - expect(lastActivityTime([])).toBeUndefined() - }) - - it('reports the log tail when no boundary is present', () => { - const events: SessionEvent[] = [ - userTurnStart(1, 0), - { type: 'turn/end', seq: 1, time: 500, data: { turn: 1, reason: { kind: 'completed' } } }, - ] - expect(lastActivityTime(events)).toBe(500) - }) - - it('skips a trailing boundary in favour of the last real work', () => { - const events: SessionEvent[] = [ - userTurnStart(1, 0), - { type: 'turn/end', seq: 1, time: 500, data: { turn: 1, reason: { kind: 'completed' } } }, - endSeedAt(2, 9_000), - ] - // Resumed long after the work, but never worked in again. - expect(lastActivityTime(events)).toBe(500) - }) - - it('reports work appended after end-seed', () => { - const events: SessionEvent[] = [ - userTurnStart(1, 0), - endSeedAt(1, 9_000), - { type: 'turn/end', seq: 2, time: 9_500, data: { turn: 1, reason: { kind: 'completed' } } }, - ] - expect(lastActivityTime(events)).toBe(9_500) - }) - - it('has no answer for a log of nothing but boundaries', () => { - // Unreachable via the constructor, but the projection is a pure function. - expect(lastActivityTime([endSeedAt(0, 1), endSeedAt(1, 2)])).toBeUndefined() - }) -}) diff --git a/packages/host/apiproxy/README.i18n.yaml b/packages/host/apiproxy/README.i18n.yaml index ea8d70c9e4..2ae197d230 100644 --- a/packages/host/apiproxy/README.i18n.yaml +++ b/packages/host/apiproxy/README.i18n.yaml @@ -2,5 +2,5 @@ # 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 packages/host/apiproxy/README.md -README.md: 518a7e5640bc62b493244a3d863cfea643386f7f -README.zh.md: 5692e9441d20d9dbec5d8a69263c875eb3f3f907 +README.md: b7dee9488c1b123172c225d4d4235bdb3c76911d +README.zh.md: 46835a84254c4bbc45300a18d29aae5a6ac66f14 diff --git a/packages/host/apiproxy/README.md b/packages/host/apiproxy/README.md index 518a7e5640..b7dee9488c 100644 --- a/packages/host/apiproxy/README.md +++ b/packages/host/apiproxy/README.md @@ -42,7 +42,7 @@ Pending queued input is a live control-plane contract, not conversation history. Background jobs ride the same live-push posture. When `ctx.jobs` is composed, the gateway subscribes to its change feed and broadcasts a whole `session/jobs` snapshot after every registry commit that alters what a session can see — registration, the stopping transition, settlement, and owner-disposal removal — plus a subscription baseline for each session that already has tasks (an absent baseline is the empty set; a change that empties a set still sends `[]`). A change carrying an owner reads through that exact `Agent`, so a push stays correct while its scope tears down; the baseline reads `ctx.agents.get(sessionId)`, which yields only unowned tasks for a session with no live Agent and never resumes a cold one. An unowned change fans out to every subscribed session, because unowned tasks are visible to every caller. The wire `JobView` drops `ownerSession`, `reported`, and `outputLimitBytes`: the frame's own `sessionId` carries the first, and the other two are internal notice and model-presentation policy. A composition without the registry emits no such frames. -Workspace and Session lists are separate reconnect baselines. `workspace.create({ path })` adopts an existing canonical directory and permits basename-derived titles to repeat. `workspace.insertBefore({ workspaceId, beforeWorkspaceId? })` commits one registry-order move and answers the complete order; a pure reorder emits `host/workspace-order-changed` with that complete order, while unknown sources or anchors return `workspace-not-found`. `workspace.delete` removes only the Workspace registration, `session.create` accepts an optional preallocated Session id, and `host/workspace-changed`, `host/workspace-removed`, plus `host/session-added` carry committed increments in either arrival order. `workspace.archiveSession` adds one session to the registry-global archive set and answers the full updated set; `workspace.list` carries that set as the reconnect baseline and `host/archived-sessions-changed` pushes the full snapshot after every durable change. Archiving hides the session from grouping surfaces without touching its log or its workspace account; a session neither live nor persisted fails with `session-not-found`. Registration deletion preserves the directory and session logs; its Sessions remain in `session.list` and become Ungrouped. `SessionSummary.blank` and the `host/session-added` frame carry whether a turn has started: clients hide blank sessions and reuse them per workspace, flip blank on the first `host/session-status(running:true)`, and treat `session.list` as the reconnect authority. Attached summaries fold the live log. A cold summary trusts cached `blank: false`, but treats cached `true` and a cache miss as unverified; when `locate()` resolves an artifact no larger than `coldBlankProbeMaxBytes` (default 1 KiB), the gateway reads that Session with `readFrom()` and checks for `turn/start`. A larger, location-less, vanished, or unreadable artifact remains visible. `updatedAt` is the latest human `user/message` time from the live fold or projection cache, falling back to `createdAt`; pickup boundaries and other writes never promote a Session. +Workspace and Session lists are separate reconnect baselines. `workspace.create({ path })` adopts an existing canonical directory and permits basename-derived titles to repeat. `workspace.insertBefore({ workspaceId, beforeWorkspaceId? })` commits one registry-order move and answers the complete order; a pure reorder emits `host/workspace-order-changed` with that complete order, while unknown sources or anchors return `workspace-not-found`. `workspace.delete` removes only the Workspace registration, `session.create` accepts an optional preallocated Session id, and `host/workspace-changed`, `host/workspace-removed`, plus `host/session-added` carry committed increments in either arrival order. `workspace.archiveSession` adds one session to the registry-global archive set and answers the full updated set; `workspace.list` carries that set as the reconnect baseline and `host/archived-sessions-changed` pushes the full snapshot after every durable change. Archiving hides the session from grouping surfaces without touching its log or its workspace account; a session neither live nor persisted fails with `session-not-found`. Registration deletion preserves the directory and session logs; its Sessions remain in `session.list` and become Ungrouped. `SessionSummary.blank` and the `host/session-added` frame carry whether a turn has started: clients hide blank sessions and reuse them per workspace, flip blank on the first `host/session-status(running:true)`, and treat `session.list` as the reconnect authority. Attached summaries fold the live log. A cold summary trusts cached `blank: false`, but treats cached `true` and a cache miss as unverified; when `locate()` reports an artifact no larger than the `coldBlankProbeMaxBytes` eligibility threshold (default 1 KiB), the gateway reads that Session with `readFrom()` and folds both blankness and the latest human prompt. A larger, location-less, vanished, or unreadable artifact remains visible. After an asynchronous cold read, a Session that attached meanwhile is summarized from its live log instead. `updatedAt` uses the live fold, the exact small-artifact fold, or the projection cache in that order, falling back to `createdAt`; pickup boundaries and other writes never promote a Session. `session.search` is a bounded content-search projection over the sessions visible through `session.list`. The gateway asks the optional `ctx.sessionQuery` service for globally ranked current-surface user, assistant, and steering matches, consumes that stream until it has at most 20 visible session/snippet pairs plus one lookahead, and revalidates every hit against the list-derived authorization set before returning it. Provider pages start at 20 hits; when a first-page request rejects that limit, the gateway probes 10, 5, 2, then 1 and retains the learned size for continuation and stale-generation restarts. Returned snippets contain at most 240 Unicode code points, and the response schema independently enforces that bound at each client boundary. Keeping the authorization set in Host memory avoids SQLite's variable ceiling for large valid corpora without weakening visibility or ranking. @@ -80,4 +80,4 @@ None; this package neither assembles nor sends a provider request. - **No protocol version field** — client and host ship together; `host.describe` gains a version negotiation field only when an independently released client exists. - **Search failures include provider diagnostics** — the gateway is a single-user local service. A carrier that exposes it to multiple users must replace internal search details with a public-safe diagnostic. - **Linux native picker requires desktop tooling** — under the `native` capability, `host.pickDirectory` reports an actionable error when neither Zenity nor KDialog is installed; the browse backend is the composition-level fallback (see the [native backend README](../directory-picker-native/README.md)). -- **Cold-list hints degrade only toward visibility and older ordering** — a projection-cache miss or stale `lastPromptAt` falls back to `createdAt`, so a recently worked Session may sort too low until the next checkpoint. A blank artifact larger than `coldBlankProbeMaxBytes`, or one from a backend without `locate()`, remains visible because the gateway cannot verify the absence of `turn/start` within the read bound. The [bounded blank-verification decision](../../../.agents/notes/implemented/bug-fix/2026-08-13-bounded-cold-blank-verification.md) owns this safety direction; an authoritative exact recency index remains scoped in the [last-activity-index proposal](../../../.agents/notes/proposed/architecture/2026-07-29-durable-last-activity-index.md). +- **Cold-list hints degrade only toward visibility and older ordering** — a projection-cache miss or stale `lastPromptAt` falls back to `createdAt` unless an eligible small artifact supplies an exact fold, so a recently worked large Session may sort too low until the next checkpoint. A blank artifact larger than `coldBlankProbeMaxBytes`, or one from a backend without `locate()`, remains visible. The threshold is checked before `readFrom()` rather than enforced by persistence, so concurrent artifact growth may increase one probe's read cost without changing blankness safety. The [bounded blank-verification decision](../../../.agents/notes/implemented/bug-fix/2026-08-13-bounded-cold-blank-verification.md) owns this safety direction; an authoritative exact recency index remains scoped in the [last-activity-index proposal](../../../.agents/notes/proposed/architecture/2026-07-29-durable-last-activity-index.md). diff --git a/packages/host/apiproxy/README.zh.md b/packages/host/apiproxy/README.zh.md index 5692e9441d..46835a8425 100644 --- a/packages/host/apiproxy/README.zh.md +++ b/packages/host/apiproxy/README.zh.md @@ -42,7 +42,7 @@ Settings 分节中的 `reasoningEffort` 在 agent-default-model 插件配置中 后台任务沿用同一种实时推送姿态。当组合中有 `ctx.jobs` 时,网关订阅它的变更订阅,并在注册表每一次改变某个会话可见内容的提交后——注册、转入 stopping、结算,以及 owner 销毁时的移除——广播一份完整的 `session/jobs` 快照,另外为每个已经有任务的会话发送订阅 baseline(没有 baseline 即表示空集;把集合清空的那次变更仍然发送 `[]`)。带 owner 的变更通过那个确切的 `Agent` 读取,因此推送在其 scope 拆除期间依然正确;baseline 读 `ctx.agents.get(sessionId)`,对没有活体 Agent 的会话只得到无主任务,且绝不恢复冷会话。无主变更向每一个已订阅会话扇出,因为无主任务对所有调用方可见。线路上的 `JobView` 丢弃 `ownerSession`、`reported` 和 `outputLimitBytes`:第一个由帧自身的 `sessionId` 携带,另外两个分别是内部通知位和模型呈现策略。没有该注册表的组合不发出这类帧。 -Workspace 列表与 Session 列表是相互独立的重连基线。`workspace.create({ path })` 会接纳已有的规范目录,并允许由 basename 派生的标题重复。`workspace.insertBefore({ workspaceId, beforeWorkspaceId? })` 提交一次注册表顺序移动并应答完整顺序;单纯重排序会通过 `host/workspace-order-changed` 推送同一份完整顺序,而未知来源或锚点返回 `workspace-not-found`。`workspace.delete` 只移除 Workspace 注册记录,`session.create` 接受可选的预分配 Session id,`host/workspace-changed`、`host/workspace-removed` 与 `host/session-added` 则以任意到达顺序携带已提交的增量。`workspace.archiveSession` 向注册表级全局归档集合添加一个会话,并应答完整的更新后集合;`workspace.list` 携带该集合作为重连基线,`host/archived-sessions-changed` 在每次持久变更后推送完整快照。归档只把会话从各分组视图中隐藏,不触碰其日志和 workspace 记账;既非活动会话也未持久化的会话以 `session-not-found` 失败。删除注册记录会保留目录和会话日志;相关 Session 仍留在 `session.list` 中,并进入 Ungrouped。`SessionSummary.blank` 与 `host/session-added` 帧携带是否已开始过轮次:客户端隐藏空白会话并按 workspace 复用它们,在首个 `host/session-status(running:true)` 时翻转 blank,并以 `session.list` 作为重连权威。已附加摘要折叠实时日志。冷摘要信任缓存的 `blank: false`,但把缓存的 `true` 与 cache miss 都视为未经验证;当 `locate()` 解析出的工件不大于 `coldBlankProbeMaxBytes`(默认 1 KiB)时,网关通过 `readFrom()` 读取该 Session 并检查 `turn/start`。更大、无位置、已消失或不可读的工件保持可见。`updatedAt` 取实时折叠或投影缓存中的最新真人 `user/message` 时间,缺失时回退到 `createdAt`;拾起边界及其他写入都不会提升 Session 排序。 +Workspace 列表与 Session 列表是相互独立的重连基线。`workspace.create({ path })` 会接纳已有的规范目录,并允许由 basename 派生的标题重复。`workspace.insertBefore({ workspaceId, beforeWorkspaceId? })` 提交一次注册表顺序移动并应答完整顺序;单纯重排序会通过 `host/workspace-order-changed` 推送同一份完整顺序,而未知来源或锚点返回 `workspace-not-found`。`workspace.delete` 只移除 Workspace 注册记录,`session.create` 接受可选的预分配 Session id,`host/workspace-changed`、`host/workspace-removed` 与 `host/session-added` 则以任意到达顺序携带已提交的增量。`workspace.archiveSession` 向注册表级全局归档集合添加一个会话,并应答完整的更新后集合;`workspace.list` 携带该集合作为重连基线,`host/archived-sessions-changed` 在每次持久变更后推送完整快照。归档只把会话从各分组视图中隐藏,不触碰其日志和 workspace 记账;既非活动会话也未持久化的会话以 `session-not-found` 失败。删除注册记录会保留目录和会话日志;相关 Session 仍留在 `session.list` 中,并进入 Ungrouped。`SessionSummary.blank` 与 `host/session-added` 帧携带是否已开始过轮次:客户端隐藏空白会话并按 workspace 复用它们,在首个 `host/session-status(running:true)` 时翻转 blank,并以 `session.list` 作为重连权威。已附加摘要折叠实时日志。冷摘要信任缓存的 `blank: false`,但把缓存的 `true` 与 cache miss 都视为未经验证;当 `locate()` 报告的工件不大于 `coldBlankProbeMaxBytes` 资格阈值(默认 1 KiB)时,网关通过 `readFrom()` 读取该 Session,同时折叠空白状态与最新真人 prompt。更大、无位置、已消失或不可读的工件保持可见。异步冷读取结束后,期间已附加的 Session 会改用实时日志生成摘要。`updatedAt` 依次采用实时折叠、小工件精确折叠或 projection cache,缺失时回退到 `createdAt`;拾起边界及其他写入都不会提升 Session 排序。 `session.search` 是以 `session.list` 所列会话为范围的有界内容搜索投影。网关向可选的 `ctx.sessionQuery` 服务请求全局排序后的当前内容视图中的 user、assistant 和 steering 匹配项,并持续消费该结果流,直到获得至多 20 个可见会话/snippet 对及一个前瞻项;返回前仍会依据从列表推导的授权集合重新校验每个命中。提供方分页初始请求 20 个命中;如果第一页请求因这一上限被拒绝,网关会依次探测 10、5、2、1,并在续传和陈旧世代重启中沿用探测所得的页面大小。返回的 snippet 最多包含 240 个 Unicode 码点,响应 schema 则会在每个客户端边界独立强制执行该上限。将授权集合保留在宿主内存中,可在不削弱可见性或排序的前提下避开有效大型语料库的 SQLite 变量上限。 @@ -80,4 +80,4 @@ Workspace 列表与 Session 列表是相互独立的重连基线。`workspace.cr - **没有协议版本字段**:客户端与宿主一同发布;只有出现独立发布的客户端后,`host.describe` 才会增加版本协商字段。 - **搜索失败会包含提供方诊断信息**:网关是单用户本地服务。将其暴露给多名用户的载体必须用可安全公开的诊断信息替代内部搜索细节。 - **Linux 原生选择器依赖桌面工具**:在 `native` 能力下,Zenity 和 KDialog 均未安装时,`host.pickDirectory` 会给出包含解决建议的错误提示;组合层面的回退是 browse 后端(见 [native 后端 README](../directory-picker-native/README.md))。 -- **冷列表提示只向“保持可见、排序偏旧”降级**:projection cache miss 或陈旧的 `lastPromptAt` 会回退到 `createdAt`,因此最近工作过的 Session 可能在下一个 checkpoint 前排得偏低。大于 `coldBlankProbeMaxBytes` 的空白工件,或来自不提供 `locate()` 的后端的空白工件会保持可见,因为网关无法在读取上限内验证其中不存在 `turn/start`。[有界空白验证决策](../../../.agents/notes/implemented/bug-fix/2026-08-13-bounded-cold-blank-verification.md)规定了这个安全方向;权威且精确的最近时间索引仍属于[最后活动索引提案](../../../.agents/notes/proposed/architecture/2026-07-29-durable-last-activity-index.md)的范围。 +- **冷列表提示只向“保持可见、排序偏旧”降级**:projection cache miss 或陈旧的 `lastPromptAt` 会回退到 `createdAt`,除非符合资格的小工件提供精确折叠,因此最近工作过的大 Session 可能在下一个 checkpoint 前排得偏低。大于 `coldBlankProbeMaxBytes` 的空白工件,或来自不提供 `locate()` 的后端的空白工件会保持可见。该阈值在 `readFrom()` 前检查,而非由 persistence 强制,因此工件并发增长可能增加一次探测的读取成本,但不会改变空白状态的安全方向。[有界空白验证决策](../../../.agents/notes/implemented/bug-fix/2026-08-13-bounded-cold-blank-verification.md)规定了这个安全方向;权威且精确的最近时间索引仍属于[最后活动索引提案](../../../.agents/notes/proposed/architecture/2026-07-29-durable-last-activity-index.md)的范围。 diff --git a/packages/host/apiproxy/src/api-proxy.ts b/packages/host/apiproxy/src/api-proxy.ts index fc986583a0..bda99362b6 100644 --- a/packages/host/apiproxy/src/api-proxy.ts +++ b/packages/host/apiproxy/src/api-proxy.ts @@ -563,40 +563,40 @@ function summarize(session: Session, running: boolean): SessionSummary { } /** - * Verify a possibly blank cold Session only when its physical artifact is - * within the configured per-Session read bound. A stale `blank: true`, an + * Verify a possibly blank cold Session only when its physical artifact passes + * the configured per-Session size check. A stale `blank: true`, an * absent cache row, a large or location-less artifact, and read failures all * resolve to visible (`false`); listing must never hide a conversation on a * cache hint or an unavailable optimization. */ -async function probeColdSessionBlank( +async function probeColdSessionMetadata( ctx: Context, persistence: SessionPersistence, meta: SessionHeader, maxBytes: number, signal?: AbortSignal, -): Promise { - if (maxBytes === 0) return false +): Promise { + if (maxBytes === 0) return undefined signal?.throwIfAborted() const location = persistence.locate(meta) - if (location === undefined) return false + if (location === undefined) return undefined signal?.throwIfAborted() let size: number try { size = (await stat(location.path)).size } catch { signal?.throwIfAborted() - return false + return undefined } - if (size > maxBytes) return false + if (size > maxBytes) return undefined try { const { events } = await persistence.readFrom(meta.id, 0, signal) signal?.throwIfAborted() - return !events.some(event => event.type === 'turn/start') + return sessionListMetadata(events) } catch (error) { signal?.throwIfAborted() ctx.logger.warn(`session.list: blank probe for "${meta.id}" failed (serving it as visible): ${String(error)}`) - return false + return undefined } } @@ -609,14 +609,14 @@ async function summarizeCold( blankProbeMaxBytes: number, signal?: AbortSignal, ): Promise { - const blank = metadata?.blank === false - ? false - : await probeColdSessionBlank(ctx, persistence, meta, blankProbeMaxBytes, signal) + const probed = metadata?.blank === false + ? undefined + : await probeColdSessionMetadata(ctx, persistence, meta, blankProbeMaxBytes, signal) return { sessionId: meta.id, - updatedAt: sessionListUpdatedAt(meta, metadata), + updatedAt: sessionListUpdatedAt(meta, probed ?? metadata), running: false, - blank, + blank: metadata?.blank === false ? false : probed?.blank ?? false, // Header-only: reading the log for a blank-window preset switch would // defeat the same index read, and attaching the session replaces this row // with `summarize()`, which resolves the switch from the events. @@ -1724,14 +1724,15 @@ export function createApiProxy(ctx: Context, defaults: ApiProxyDefaults): ApiPro */ async function listVisibleSessionSummaries(signal?: AbortSignal): Promise { signal?.throwIfAborted() - const items = ctx.sessions.list().map((session) => { + const summarizeAttached = (session: Session): SessionSummary => { const agent = ctx.agents.get(session.id) const projections = listProjectionsFor(ctx, session.header, session) return { ...summarize(session, agent?.status === 'running'), ...projections === undefined ? {} : { projections }, } - }) + } + const items = ctx.sessions.list().map(summarizeAttached) signal?.throwIfAborted() const attached = new Set(items.map(item => item.sessionId)) const persistence = ctx.get('sessionPersistence') @@ -1745,17 +1746,20 @@ export function createApiProxy(ctx: Context, defaults: ApiProxyDefaults): ApiPro const settled = await Promise.allSettled( batch.map(async (meta) => { // Projection hints remain optional. Blank verification may read - // this Session's artifact only when it fits the configured bound. + // this Session's artifact only when it passes the configured size check. const projections = listProjectionsFor(ctx, meta, undefined) + const summary = await summarizeCold( + ctx, + persistence, + meta, + projections?.values.sessionListMetadata, + coldBlankProbeMaxBytes, + signal, + ) + const attachedSession = ctx.sessions.get(meta.id) + if (attachedSession !== undefined) return summarizeAttached(attachedSession) return { - ...await summarizeCold( - ctx, - persistence, - meta, - projections?.values.sessionListMetadata, - coldBlankProbeMaxBytes, - signal, - ), + ...summary, ...projections === undefined ? {} : { projections }, } }), diff --git a/packages/host/apiproxy/src/api/sessions.ts b/packages/host/apiproxy/src/api/sessions.ts index c834718cf0..2e7c9b22f4 100644 --- a/packages/host/apiproxy/src/api/sessions.ts +++ b/packages/host/apiproxy/src/api/sessions.ts @@ -173,25 +173,25 @@ export type QueueAction = | { kind: 'remove' } | { kind: 'steer' } -/** Session list entry (v1 builds no index: list does readdir+stat). */ +/** One Session list entry. */ export interface SessionSummary { sessionId: SessionId /** - * Last activity. Attached: the last non-`session/end-seed` event, since a - * pickup is not activity. Cold: the log's mtime, or `createdAt` for a backend - * with no per-session file (README Known Limitations covers the skew). + * The later of creation and the latest human-authored prompt. Attached + * Sessions fold their live log; cold Sessions use a projection-cache hint or + * an exact small-artifact read, falling back to creation time. */ updatedAt: number /** Status of the attached agent; always false for cold (unattached) sessions. */ running: boolean /** - * Derived conversation-not-started bit: true while no turn has run (no - * prompt was accepted yet). Standalone plugin events — command lifecycle + * Derived conversation-not-started bit: true while no turn has run. + * Standalone plugin events — command lifecycle * records, plan/mode, titles, goals — do not open a turn and therefore do - * not clear it. Clients hide blank sessions from lists and reuse them for - * New Session on the same workspace. Always false for cold sessions — - * lazy persistence keeps a never-appended session out of the store, and a - * listed cold session's log holds its turns. + * not clear it. Clients hide blank Sessions from lists and reuse them for + * New Session on the same workspace. A cold Session is true only when a + * small-artifact read verifies that no `turn/start` exists; unavailable + * or oversized artifacts conservatively report false. */ blank: boolean /** fork/spawn lineage (session.header.parentSession passthrough); absent for root sessions. */ diff --git a/packages/host/apiproxy/tests/api-proxy-cold.spec.ts b/packages/host/apiproxy/tests/api-proxy-cold.spec.ts index 20a891d420..ef642082ac 100644 --- a/packages/host/apiproxy/tests/api-proxy-cold.spec.ts +++ b/packages/host/apiproxy/tests/api-proxy-cold.spec.ts @@ -67,7 +67,14 @@ describe('sessions.list cold merge', () => { if (id === sid('small-conversation')) { return { meta: metas[1]!, - events: [{ type: 'turn/start', seq: 0, time: 800, data: { turn: 1 } }] as SessionEvent[], + events: [ + { type: 'turn/start', seq: 0, time: 800, data: { turn: 1 } }, + { + type: 'user/message', seq: 1, time: 1200, + data: createUserMessage({ content: [{ type: 'text', text: 'worked' }], source: { kind: 'user' } }), + surfaceOp: 'append', + }, + ] as SessionEvent[], } } if (id === sid('read-failure')) throw new Error('simulated read failure') @@ -105,7 +112,7 @@ describe('sessions.list cold merge', () => { const byId = Object.fromEntries(response.result.value.items.map(item => [item.sessionId, item])) expect(byId['small-blank']).toMatchObject({ blank: true, updatedAt: 100, running: false }) // A stale true hint cannot hide the turn found in the bounded read. - expect(byId['small-conversation']).toMatchObject({ blank: false, updatedAt: 900 }) + expect(byId['small-conversation']).toMatchObject({ blank: false, updatedAt: 1200 }) expect(byId['large-unknown']).toMatchObject({ blank: false, updatedAt: 300 }) // false is monotonic, so this row skips stat/read and keeps cached recency. expect(byId['cached-nonblank']).toMatchObject({ blank: false, updatedAt: 1000 }) @@ -149,6 +156,62 @@ describe('sessions.list cold merge', () => { ]) expect(readFrom).not.toHaveBeenCalled() }) + + it('replaces a probed cold row with the live Session that attached during the read', async () => { + const ctx = new Context() + await ctx.plugin(SessionStore) + await ctx.plugin(UserQuestionService) + await ctx.plugin(AgentRegistry) + const meta = header('attached-during-probe', 100) + const root = mkdtempSync(join(tmpdir(), 'dsh-cold-race-')) + const path = join(root, 'small.log') + writeFileSync(path, 'x') + const started = Promise.withResolvers() + const release = Promise.withResolvers() + ctx.provide('sessionPersistence', { + list: () => Promise.resolve([meta]), + locate: () => ({ kind: 'jsonl', path }), + readFrom: async () => { + started.resolve(undefined) + await release.promise + return { + meta, + events: [{ type: 'session/end-seed', seq: 0, time: 110, data: {} }] as SessionEvent[], + } + }, + } as never) + const api = createApiProxy(ctx, { defaultModelSelection: () => ({ provider: 'p', model: 'm' }), cwd: '/tmp' }) + + const listing = api.sessions.list(request({})) + await started.promise + const session = ctx.sessions.create(meta.id, { + seed: [ + { type: 'turn/start', seq: 0, time: 200, data: { turn: 1 } }, + { + type: 'user/message', seq: 1, time: 300, + data: createUserMessage({ content: [{ type: 'text', text: 'live' }], source: { kind: 'user' } }), + surfaceOp: 'append', + }, + ], + meta: { + ...meta.cwd === undefined ? {} : { cwd: meta.cwd }, + createdAt: meta.createdAt, + }, + }) + ctx.agents.register({ id: session.id, session, status: 'running', ctx } as Agent) + release.resolve(undefined) + + const response = await listing + if (!response.result.ok) throw new Error('list failed') + expect(response.result.value.items).toEqual([ + expect.objectContaining({ + sessionId: meta.id, + blank: false, + running: true, + updatedAt: 300, + }), + ]) + }) }) describe('attached updatedAt tracks human prompts', () => { From a37e19471e93e36a79685fedace840ea4763f40a Mon Sep 17 00:00:00 2001 From: Ag Date: Thu, 13 Aug 2026 15:14:36 +0800 Subject: [PATCH 11/24] docs: refine localized community links --- README.i18n.yaml | 4 ++-- README.md | 3 +-- README.zh.md | 7 ++++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/README.i18n.yaml b/README.i18n.yaml index ee4bfdf7f1..727a781767 100644 --- a/README.i18n.yaml +++ b/README.i18n.yaml @@ -2,5 +2,5 @@ # 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 README.md -README.md: b1d84d6ebd20843f6c2e3b7ccd22cccb2df0d104 -README.zh.md: a3960d3d342d005230f8a40e5c43d4bac882cb7a +README.md: 098b514f8311de092bab18bdd560722b1e485913 +README.zh.md: f9dacc4e3cce1c83bfa65df9fbc8d6410a37fcc0 diff --git a/README.md b/README.md index b1d84d6ebd..098b514f83 100644 --- a/README.md +++ b/README.md @@ -38,8 +38,7 @@ pnpm dsh web - Feel free to submit feedback or bug reports through [GitHub Discussions](https://github.com/deepseek-ai/deepseek-harness/discussions). - Add the [`dsh-plugin`](https://github.com/topics/dsh-plugin) topic to your plugin repository for discoverability. - -

Join DeepSeek Harness Discord community.

+- Join DeepSeek Harness Discord community. ## Contributing diff --git a/README.zh.md b/README.zh.md index a3960d3d34..f9dacc4e3c 100644 --- a/README.zh.md +++ b/README.zh.md @@ -38,6 +38,7 @@ pnpm dsh web - 欢迎通过 [GitHub Discussions](https://github.com/deepseek-ai/deepseek-harness/discussions) 提交反馈或 bug 报告。 - 为你的插件仓库添加 [`dsh-plugin`](https://github.com/topics/dsh-plugin) 主题,便于被发现。 +- 欢迎加入 DeepSeek Harness 企微群:扫码添加企微小助手并填写入群问卷,完成后小助手会邀请你入群。 @@ -49,9 +50,9 @@ pnpm dsh web - - - + + +
DeepSeek Harness 企微小助手二维码DeepSeek Harness 入群问卷二维码DeepSeek Harness 团队微信公众号二维码DeepSeek Harness 企微小助手二维码DeepSeek Harness 入群问卷二维码DeepSeek Harness 团队微信公众号二维码
From 4ed036da1c2d69052c7b60c9c56e5f72b08c1382 Mon Sep 17 00:00:00 2001 From: Huanqi Cao Date: Wed, 12 Aug 2026 01:11:46 +0800 Subject: [PATCH 12/24] fix: Windows-native CI findings on latest master Local run of check:ci:windows-complete (the windows-native gate) on latest master surfaced five Windows-only failures, all unreachable by current CI because the native windows job is disabled and the wine gate only covers build+site. - install-lefthook/translation-pairing-merge specs junctioned the real scripts/ and tsx package into fixtures; Windows recursive deletion (Node rmSync and git worktree remove) follows MOUNT_POINT junctions and deleted the repository's own directories mid-run. Fixtures now unlink their reparse points before any recursive removal (shared helper in scripts/test-fixture-cleanup.ts). - workflow-workerthread spawned its worker with an empty env; on Windows os.tmpdir() then degrades to the literal relative path undefined\temp, so tsx wrote its transform cache into a cwd-relative undefined/ directory inside the repo. The worker env now injects the host temp path on win32 (workerSpawnEnv, platform-parameterized and unit-tested on both arms). - workspace-context spec did not stub USERPROFILE (win32 homedir) or a set DSH_HOME, leaking the developer machine's real ~/.dsh/AGENTS.md into discovery. - ui-trajectory client-bundle spec mounted the built artifact without the remote/settingsScope provides the locale plugin needs, so the plugin never activated and no view registered. - subagent temp-fixture cleanup lacked the maxRetries Windows handle release needs under load (EPERM); added retries to the three affected specs and the fixture-cleanup helper. --- .../tests/agent-instructions.spec.ts | 9 +++- .../subagent/tests/continuation.spec.ts | 2 +- .../subagent/tests/list-children.spec.ts | 2 +- .../tests/tool-subagent-control.spec.ts | 2 +- .../workflow-worker-thread/src/host.ts | 33 +++++++++++-- .../tests/workflow-worker-thread.spec.ts | 42 ++++++++++++++--- scripts/install-lefthook.spec.ts | 7 ++- scripts/test-fixture-cleanup.ts | 46 +++++++++++++++++++ scripts/translation-pairing-merge.spec.ts | 12 ++++- 9 files changed, 137 insertions(+), 18 deletions(-) create mode 100644 scripts/test-fixture-cleanup.ts diff --git a/packages/context/agent-instructions/tests/agent-instructions.spec.ts b/packages/context/agent-instructions/tests/agent-instructions.spec.ts index 71bd149413..409e4f8afb 100644 --- a/packages/context/agent-instructions/tests/agent-instructions.spec.ts +++ b/packages/context/agent-instructions/tests/agent-instructions.spec.ts @@ -578,10 +578,12 @@ describe('workspace context instruction discovery', () => { const root = await tempRepo() const emptyHome = await tempRepo() // Isolate the default-home fallback: blank DSH_HOME is treated as unset, and - // HOME points at an empty dir so the default ~/.dsh holds no global scope. - // Symlinks are followed, so a real ~/.dsh/AGENTS.md would otherwise leak in. + // the home dirs point at an empty dir so the default ~/.dsh holds no global + // scope. Windows homedir() reads USERPROFILE (not HOME), so both must be + // stubbed or a real ~/.dsh/AGENTS.md would otherwise leak in. vi.stubEnv('DSH_HOME', '') vi.stubEnv('HOME', emptyHome) + if (process.platform === 'win32') vi.stubEnv('USERPROFILE', emptyHome) try { const cwd = join(root, 'child') await mkdir(cwd, { recursive: true }) @@ -622,6 +624,8 @@ describe('workspace context instruction discovery', () => { try { await write(join(home, '.dsh/AGENTS.md'), 'global default rule') + // A set DSH_HOME would override the homedir default and relabel the home. + vi.stubEnv('DSH_HOME', '') vi.resetModules() vi.doMock('node:os', () => ({ homedir: () => home })) const isolated = await import('@deepseek-ai/dsh-agent-instructions') @@ -629,6 +633,7 @@ describe('workspace context instruction discovery', () => { expect(files.map(file => file.displayPath)).toEqual(['~/.dsh/AGENTS.md']) } finally { + vi.unstubAllEnvs() vi.doUnmock('node:os') vi.resetModules() await rm(root, { recursive: true, force: true }) diff --git a/packages/subagent/subagent/tests/continuation.spec.ts b/packages/subagent/subagent/tests/continuation.spec.ts index 7ff021ee33..07f497628d 100644 --- a/packages/subagent/subagent/tests/continuation.spec.ts +++ b/packages/subagent/subagent/tests/continuation.spec.ts @@ -53,7 +53,7 @@ class GatedAdapter extends LlmAdapter { const roots: string[] = [] afterEach(() => { - for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true }) + for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 }) }) /** Boot the full continuable stack: loop, persistence, providers, and subagents. */ diff --git a/packages/subagent/subagent/tests/list-children.spec.ts b/packages/subagent/subagent/tests/list-children.spec.ts index 80b596acc1..3fd303e7f6 100644 --- a/packages/subagent/subagent/tests/list-children.spec.ts +++ b/packages/subagent/subagent/tests/list-children.spec.ts @@ -28,7 +28,7 @@ type Script = ConstructorParameters[0] const roots: string[] = [] afterEach(() => { - for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true }) + for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 }) }) /** Boot the continuable stack with real JSONL session persistence. */ diff --git a/packages/subagent/tool-subagent-control/tests/tool-subagent-control.spec.ts b/packages/subagent/tool-subagent-control/tests/tool-subagent-control.spec.ts index 1721758409..0636254841 100644 --- a/packages/subagent/tool-subagent-control/tests/tool-subagent-control.spec.ts +++ b/packages/subagent/tool-subagent-control/tests/tool-subagent-control.spec.ts @@ -47,7 +47,7 @@ const testToolSignal = new AbortController().signal const roots: string[] = [] afterEach(() => { - for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true }) + for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 }) }) async function setupWith(adapter: MockAdapter | GatedAdapter) { diff --git a/packages/workflow/workflow-worker-thread/src/host.ts b/packages/workflow/workflow-worker-thread/src/host.ts index 209749bb38..9e0c3a514a 100644 --- a/packages/workflow/workflow-worker-thread/src/host.ts +++ b/packages/workflow/workflow-worker-thread/src/host.ts @@ -6,6 +6,7 @@ * @module @deepseek-ai/dsh-workflow-worker-thread/host */ +import { tmpdir } from 'node:os' import { Worker } from 'node:worker_threads' import type { WorkerOptions } from 'node:worker_threads' import { fileURLToPath } from 'node:url' @@ -28,18 +29,42 @@ interface ChildRecord { disposal?: Promise } +/** + * The scrubbed worker environment: no ambient credentials, no loader flags. + * Windows derives `os.tmpdir()` from `TMP`/`TEMP` and falls back to the + * literal relative path `undefined\temp` when the environment is empty, so + * tsx's transform cache would land in a cwd-relative `undefined/temp` + * directory; the host's real temp path (not a credential) is injected there. + * The unbuilt shape additionally forwards `TSX_TSCONFIG_PATH` for path + * resolution. + * @param platform - host platform; overridable so tests exercise both peer arms. + * @returns the scrubbed worker environment object. + */ +export function workerSpawnEnv(platform: NodeJS.Platform = process.platform): NodeJS.ProcessEnv { + const env: NodeJS.ProcessEnv = {} + if (platform === 'win32') { + const tmp = tmpdir() + env.TMP = tmp + env.TEMP = tmp + } + if (process.env.TSX_TSCONFIG_PATH !== undefined) { + env.TSX_TSCONFIG_PATH = process.env.TSX_TSCONFIG_PATH + } + return env +} + /** * Resolve a built worker bundle or an unbuilt bootstrap that installs both tsx * transforms inside the worker. Both shapes clear `execArgv` and the ambient - * environment; the unbuilt shape forwards only `TSX_TSCONFIG_PATH` for path - * resolution. + * environment (the worker only sees the platform temp path and, unbuilt, + * `TSX_TSCONFIG_PATH`). * @param init - the run payload, passed as `workerData`. * @returns the entry path or URL and the Worker options to spawn it with. */ function resolveWorkerSpawn(init: WorkerInit): { entry: string | URL; options: WorkerOptions } { /* v8 ignore next 3 -- the built-output arm: tests always run unbuilt (src/); the built-worker e2e exercises this shape for real */ if (!import.meta.url.endsWith('.ts')) { - return { entry: fileURLToPath(new URL('./worker.cjs', import.meta.url)), options: { workerData: init, env: {}, execArgv: [] } } + return { entry: fileURLToPath(new URL('./worker.cjs', import.meta.url)), options: { workerData: init, env: workerSpawnEnv(), execArgv: [] } } } // Resolve tsx only for unbuilt consumers and install it before importing TS. const workerEntry = new URL('./worker.ts', import.meta.url) @@ -56,7 +81,7 @@ function resolveWorkerSpawn(init: WorkerInit): { entry: string | URL; options: W entry: new URL(`data:text/javascript,${encodeURIComponent(bootstrap)}`), options: { workerData: init, - env: process.env.TSX_TSCONFIG_PATH === undefined ? {} : { TSX_TSCONFIG_PATH: process.env.TSX_TSCONFIG_PATH }, + env: workerSpawnEnv(), execArgv: [], }, } diff --git a/packages/workflow/workflow-worker-thread/tests/workflow-worker-thread.spec.ts b/packages/workflow/workflow-worker-thread/tests/workflow-worker-thread.spec.ts index ced9a632f4..275c92269e 100644 --- a/packages/workflow/workflow-worker-thread/tests/workflow-worker-thread.spec.ts +++ b/packages/workflow/workflow-worker-thread/tests/workflow-worker-thread.spec.ts @@ -1,4 +1,5 @@ import { describe, expect, it, vi } from 'vitest' +import { tmpdir } from 'node:os' import { fileURLToPath } from 'node:url' import type { Worker } from 'node:worker_threads' import { Context } from '@deepseek-ai/cordis' @@ -9,6 +10,7 @@ import type { SubagentCapabilities, SubagentProvider, SubagentResult, SubagentRu import type { WorkflowMeta, WorkflowResult, WorkflowResultInfo, WorkflowRun, WorkflowRunInfo } from '@deepseek-ai/dsh-workflow' import * as workerEngineModule from '../src/index.ts' import WorkerThreadWorkflowEngine, { type Config } from '../src/index.ts' +import { workerSpawnEnv } from '../src/host.ts' import { HostToWorkerType, WorkerToHostType } from '../src/protocol.ts' import { SessionId } from '@deepseek-ai/dsh-session' @@ -559,24 +561,49 @@ describe('dsh-workflow-worker-thread', () => { expect(result.value).toBe('fine') }) - it('the worker spawns with an EMPTY environment: an escaped script finds no ambient credentials', async () => { + it('the worker spawns with a scrubbed environment: an escaped script finds no ambient credentials', async () => { const { ctx, parent } = await setup() // A canary in the HARNESS process's env: with an inherited environment // the escape below would read it back (exactly how DEEPSEEK_API_KEY - // would leak); env: {} in the spawn options is what keeps it out. + // would leak); the worker env keeps every ambient variable out. Windows + // additionally receives the host temp path (TMP/TEMP) so `os.tmpdir()` + // inside the worker resolves instead of degrading to a cwd-relative + // `undefined\temp` (tsx writes its transform cache there). process.env.WORKFLOW_ENV_CANARY = 'leak me' try { const result = await run(ctx, parent, scripted(` const proc = ${ESCAPE} - return { canary: proc.env.WORKFLOW_ENV_CANARY ?? null, keys: Object.keys(proc.env).length } + return { canary: proc.env.WORKFLOW_ENV_CANARY ?? null, keys: Object.keys(proc.env).sort() } `)) expect(result.stopReason).toBe('completed') - expect(result.value).toEqual({ canary: null, keys: 0 }) + const expectedKeys = process.platform === 'win32' ? ['TEMP', 'TMP'] : [] + expect(result.value).toEqual({ canary: null, keys: expectedKeys }) } finally { delete process.env.WORKFLOW_ENV_CANARY } }) + it('workerSpawnEnv injects the host temp path on win32 and leaves the POSIX peer empty', () => { + const tmp = tmpdir() + expect(workerSpawnEnv('win32')).toEqual({ TMP: tmp, TEMP: tmp }) + expect(workerSpawnEnv('linux')).toEqual({}) + }) + + it('workerSpawnEnv forwards TSX_TSCONFIG_PATH when the snapshot harness pins it', () => { + const tsconfig = fileURLToPath(new URL('../../../../tsconfig.json', import.meta.url)) + vi.stubEnv('TSX_TSCONFIG_PATH', tsconfig) + try { + expect(workerSpawnEnv('linux')).toEqual({ TSX_TSCONFIG_PATH: tsconfig }) + expect(workerSpawnEnv('win32')).toEqual({ + TMP: tmpdir(), + TEMP: tmpdir(), + TSX_TSCONFIG_PATH: tsconfig, + }) + } finally { + vi.unstubAllEnvs() + } + }) + it('the unbuilt worker forwards exactly TSX_TSCONFIG_PATH through the scrub: the paths-map pin survives, secrets do not', async () => { const { ctx, parent } = await setup() // The ACP snapshot harness runs the parent with its cwd OUTSIDE the @@ -589,10 +616,13 @@ describe('dsh-workflow-worker-thread', () => { try { const result = await run(ctx, parent, scripted(` const proc = ${ESCAPE} - return { keys: Object.keys(proc.env), tsconfig: proc.env.TSX_TSCONFIG_PATH } + return { keys: Object.keys(proc.env).sort(), tsconfig: proc.env.TSX_TSCONFIG_PATH } `)) expect(result.stopReason).toBe('completed') - expect(result.value).toEqual({ keys: ['TSX_TSCONFIG_PATH'], tsconfig }) + const expectedKeys = process.platform === 'win32' + ? ['TEMP', 'TMP', 'TSX_TSCONFIG_PATH'] + : ['TSX_TSCONFIG_PATH'] + expect(result.value).toEqual({ keys: expectedKeys, tsconfig }) } finally { delete process.env.TSX_TSCONFIG_PATH delete process.env.WORKFLOW_ENV_CANARY diff --git a/scripts/install-lefthook.spec.ts b/scripts/install-lefthook.spec.ts index 2c429bba25..c5a92a675e 100644 --- a/scripts/install-lefthook.spec.ts +++ b/scripts/install-lefthook.spec.ts @@ -16,6 +16,7 @@ import { tmpdir } from 'node:os' import { dirname, isAbsolute, join, resolve } from 'node:path' import { fileURLToPath } from 'node:url' import { afterEach, describe, expect, it } from 'vitest' +import { removeFixtureSafely, unlinkFixtureLinks } from './test-fixture-cleanup.ts' const installer = fileURLToPath(new URL('./install-lefthook.mjs', import.meta.url)) const pairingMergeDriver = 'scripts/merge-translation-pairing-driver.sh %O %A %B %P' @@ -40,7 +41,7 @@ interface CommandResult { } afterEach(() => { - for (const fixture of fixtures.splice(0)) rmSync(fixture, { recursive: true, force: true }) + for (const fixture of fixtures.splice(0)) removeFixtureSafely(fixture) }) function commandResult(command: string, args: string[], cwd: string, env: NodeJS.ProcessEnv): CommandResult { @@ -282,6 +283,10 @@ describe('worktree-local Lefthook installer', { timeout: 15_000 }, () => { expect(gitResult(fixture, fixture.main, ['config', '--file', commonConfig, '--get', 'core.bare']).status).toBe(1) const mainHookBeforeRemoval = readFileSync(join(mainHooks, 'pre-commit'), 'utf8') + // Windows Git follows the fixture's MOUNT_POINT junctions into their real + // targets while removing a worktree; unlink them first so the removal + // cannot delete the repository's scripts/ or tsx package. + unlinkFixtureLinks(fixture.linked) git(fixture, fixture.main, ['worktree', 'remove', '--force', fixture.linked]) expect(readFileSync(join(mainHooks, 'pre-commit'), 'utf8')).toBe(mainHookBeforeRemoval) expect(readFileSync(legacyHook, 'utf8')).toBe('#!/bin/sh\n# legacy hook\n') diff --git a/scripts/test-fixture-cleanup.ts b/scripts/test-fixture-cleanup.ts new file mode 100644 index 0000000000..9c58eea890 --- /dev/null +++ b/scripts/test-fixture-cleanup.ts @@ -0,0 +1,46 @@ +/** + * Junction-safe fixture cleanup for Windows. Test fixtures junction the REAL + * `scripts/`, `node_modules`, and tsx package directories so installer probes + * resolve through them; Windows recursive deletion — both Node's `rmSync` and + * Git's `worktree remove` — follows MOUNT_POINT junctions into their targets + * and would delete the repository's own directories. POSIX `unlink`/`rm` + * already remove symlinks without following them, so the walk is a no-op + * there. + */ + +import { lstatSync, readdirSync, rmSync, unlinkSync } from 'node:fs' +import { join } from 'node:path' + +/** + * Recursively unlink every symbolic link (junction) under `path`. + * @param path - the fixture tree whose reparse points are unlinked. + */ +export function unlinkFixtureLinks(path: string): void { + const visit = (entry: string): void => { + let stat: ReturnType + try { + stat = lstatSync(entry) + } catch (error) { + if ((error as NodeJS.ErrnoException).code === 'ENOENT') return + throw error + } + if (stat.isSymbolicLink() || !stat.isDirectory()) { + if (stat.isSymbolicLink()) unlinkSync(entry) + return + } + for (const child of readdirSync(entry)) visit(join(entry, child)) + } + visit(path) +} + +/** + * Remove one fixture tree after its junctions are unlinked (see + * {@link unlinkFixtureLinks}). Retries the removal: Windows releases child + * process and antivirus file handles asynchronously, and an unretried + * `rmSync` fails immediately with EPERM under load. + * @param path - the fixture tree to remove. + */ +export function removeFixtureSafely(path: string): void { + unlinkFixtureLinks(path) + rmSync(path, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 }) +} diff --git a/scripts/translation-pairing-merge.spec.ts b/scripts/translation-pairing-merge.spec.ts index 0ee78a11b7..32924416a6 100644 --- a/scripts/translation-pairing-merge.spec.ts +++ b/scripts/translation-pairing-merge.spec.ts @@ -1,7 +1,14 @@ /** Integration coverage for automatic and explicit pairing-record conflict resolution. */ import { execFileSync, spawnSync } from 'node:child_process' -import { chmodSync, mkdtempSync, mkdirSync, readFileSync, rmSync, symlinkSync, writeFileSync } from 'node:fs' +import { + chmodSync, + mkdtempSync, + mkdirSync, + readFileSync, + symlinkSync, + writeFileSync, +} from 'node:fs' import { tmpdir } from 'node:os' import { delimiter, dirname, join } from 'node:path' import { fileURLToPath } from 'node:url' @@ -15,6 +22,7 @@ import { renderTranslationPairingRecord, translationPairPaths, } from './translation-pairing-record.ts' +import { removeFixtureSafely } from './test-fixture-cleanup.ts' const driver = fileURLToPath(new URL('./merge-translation-pairing.ts', import.meta.url)) const driverLauncher = fileURLToPath(new URL('./merge-translation-pairing-driver.sh', import.meta.url)) @@ -28,7 +36,7 @@ interface Fixture { } afterEach(() => { - for (const fixture of fixtures.splice(0)) rmSync(fixture, { recursive: true, force: true }) + for (const fixture of fixtures.splice(0)) removeFixtureSafely(fixture) }) function git(fixture: Fixture, args: string[]): string { From 9e9073da2484755f660356d96497a6b7090a6a67 Mon Sep 17 00:00:00 2001 From: Huanqi Cao Date: Wed, 12 Aug 2026 11:18:03 +0800 Subject: [PATCH 13/24] fix(boot): unlink stale profile fallback links instead of rmSync --- ...ink-stale-profile-fallback-links.i18n.yaml | 6 +++++ ...-12-unlink-stale-profile-fallback-links.md | 25 +++++++++++++++++++ ...-unlink-stale-profile-fallback-links.zh.md | 25 +++++++++++++++++++ packages/boot/app-boot/src/profile.ts | 6 +++-- 4 files changed, 60 insertions(+), 2 deletions(-) create mode 100644 .agents/notes/implemented/bug-fix/2026-08-12-unlink-stale-profile-fallback-links.i18n.yaml create mode 100644 .agents/notes/implemented/bug-fix/2026-08-12-unlink-stale-profile-fallback-links.md create mode 100644 .agents/notes/implemented/bug-fix/2026-08-12-unlink-stale-profile-fallback-links.zh.md diff --git a/.agents/notes/implemented/bug-fix/2026-08-12-unlink-stale-profile-fallback-links.i18n.yaml b/.agents/notes/implemented/bug-fix/2026-08-12-unlink-stale-profile-fallback-links.i18n.yaml new file mode 100644 index 0000000000..0e53f1c748 --- /dev/null +++ b/.agents/notes/implemented/bug-fix/2026-08-12-unlink-stale-profile-fallback-links.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 .agents/notes/implemented/bug-fix/2026-08-12-unlink-stale-profile-fallback-links.md +2026-08-12-unlink-stale-profile-fallback-links.md: 32959eb1b83bcc290d1daa4e4a020a2721be489b +2026-08-12-unlink-stale-profile-fallback-links.zh.md: 1f4da12748c9b57c12bf41a740001d5df770beb6 diff --git a/.agents/notes/implemented/bug-fix/2026-08-12-unlink-stale-profile-fallback-links.md b/.agents/notes/implemented/bug-fix/2026-08-12-unlink-stale-profile-fallback-links.md new file mode 100644 index 0000000000..32959eb1b8 --- /dev/null +++ b/.agents/notes/implemented/bug-fix/2026-08-12-unlink-stale-profile-fallback-links.md @@ -0,0 +1,25 @@ +# Agent Note: Unlink stale profile fallback links instead of rmSync + +Status: implemented + +English | [中文](2026-08-12-unlink-stale-profile-fallback-links.zh.md) + +## Problem + +`healProfilesModuleFallback` re-points `$DSH_HOME/profiles/node_modules` entries when an installation moves, and Windows hosts keep those entries as junctions. `ensureSymlink` deleted a stale entry with `rmSync(link)`, but Node treats a junction as a directory for removal: without `recursive`, `rmSync` throws `ERR_FS_EISDIR`, so every launch from a moved installation or a second worktree crashed before booting. The `replaces a wrong symlink` unit test reproduces that crash on Windows at the exact removal call. + +## Decision + +`ensureSymlink` removes a stale link with `unlinkSync(link)`. `unlink` deletes the reparse point or symlink itself on every platform and never descends into the target, which preserves the function's fail-loud guarantee that a real directory is never deleted. The [profile-plugin-bundles decision](../architecture/2026-08-05-profile-plugin-bundles.md) keeps owning the fallback's two-anchor resolution; this note owns only the removal primitive. + +## Alternatives considered + +**`rmSync(link, { recursive: true })`.** On Node 24 this deletes the junction without following its target, but `recursive` would silently delete a real directory that replaced the link between the `lstat` guard and the removal, weakening the fail-loud contract that motivates the guard. + +**`rmdirSync(link)`.** Removes a junction on Windows as well, but it reads as directory removal for a link, and `unlinkSync` is the repository's existing junction-cleanup idiom. + +**Delete and recreate every entry unconditionally.** Correct but churns unchanged links on every launch and widens the concurrent-heal race window. + +## Consequences + +Windows launches heal moved or second-checkout installations instead of crashing with `ERR_FS_EISDIR`; POSIX behavior is unchanged because `unlinkSync` also unlinks plain symlinks. The existing `replaces a wrong symlink` test now passes on Windows where it previously reproduced the crash. Two concurrent healers deleting the same stale link still surface the second deletion as `ENOENT`, unchanged from the previous `rmSync` implementation. diff --git a/.agents/notes/implemented/bug-fix/2026-08-12-unlink-stale-profile-fallback-links.zh.md b/.agents/notes/implemented/bug-fix/2026-08-12-unlink-stale-profile-fallback-links.zh.md new file mode 100644 index 0000000000..1f4da12748 --- /dev/null +++ b/.agents/notes/implemented/bug-fix/2026-08-12-unlink-stale-profile-fallback-links.zh.md @@ -0,0 +1,25 @@ +# Agent Note: 用 unlink 删除过期的 profile 回退链接而非 rmSync + +Status: implemented + +[English](2026-08-12-unlink-stale-profile-fallback-links.md) | 中文 + +## 问题 + +`healProfilesModuleFallback` 在安装位置迁移时会把 `$DSH_HOME/profiles/node_modules` 中的条目重新指向新目标,而 Windows 主机上这些条目是 junction。`ensureSymlink` 原先用 `rmSync(link)` 删除过期条目,但 Node 在删除时把 junction 当作目录处理:不带 `recursive` 的 `rmSync` 会抛 `ERR_FS_EISDIR`,于是从迁移后的安装或第二个 worktree 启动时,每次都会在应用引导前崩溃。`replaces a wrong symlink` 单元测试在 Windows 上正好在该删除调用处复现了这一崩溃。 + +## 决策 + +`ensureSymlink` 改用 `unlinkSync(link)` 删除过期链接。`unlink` 在所有平台上都只删除重解析点或符号链接本身、绝不进入目标目录,从而保住该函数“真实目录永远不会被删除”的大声失败保证。[profile-plugin-bundles 决策](../architecture/2026-08-05-profile-plugin-bundles.md)继续拥有回退目录的双锚点解析;本 note 只拥有“用哪个删除原语”这一决定。 + +## 考虑过的替代方案 + +**`rmSync(link, { recursive: true })`。** Node 24 上它只删 junction、不跟随目标,但 `recursive` 会在 `lstat` 守卫与删除之间链接被替换成真实目录时静默删除该目录,削弱守卫存在所依据的大声失败契约。 + +**`rmdirSync(link)`。** Windows 上同样能删 junction,但它读起来像“删目录”,而 `unlinkSync` 才是仓库现有的 junction 清理惯例。 + +**无条件删除并重建所有条目。** 正确,但每次启动都翻动未变化的链接,并扩大并发修复的竞态窗口。 + +## 后果 + +Windows 启动现在可以修复迁移后的安装或第二个 checkout,而不是以 `ERR_FS_EISDIR` 崩溃;POSIX 行为不变,因为 `unlinkSync` 同样能 unlink 普通符号链接。现有的 `replaces a wrong symlink` 测试在 Windows 上从复现崩溃变为通过。两个并发 healer 删除同一过期链接时,第二次删除仍会以 `ENOENT` 浮现,与原先的 `rmSync` 实现一致。 diff --git a/packages/boot/app-boot/src/profile.ts b/packages/boot/app-boot/src/profile.ts index 24ba7b66ee..8f982bed80 100644 --- a/packages/boot/app-boot/src/profile.ts +++ b/packages/boot/app-boot/src/profile.ts @@ -24,7 +24,7 @@ import { createRequire } from 'node:module' import { - existsSync, lstatSync, mkdirSync, readFileSync, readlinkSync, rmSync, symlinkSync, writeFileSync, + existsSync, lstatSync, mkdirSync, readFileSync, readlinkSync, symlinkSync, unlinkSync, writeFileSync, } from 'node:fs' import { basename, dirname, join } from 'node:path' import type { EntryOptions } from '@deepseek-ai/cordis-plugin-loader' @@ -182,7 +182,9 @@ function ensureSymlink(link: string, target: string): void { throw new Error(`dsh: ${link} exists and is not a symlink; remove it so dsh can manage the installation fallback`) } if (readlinkSync(link) === target) return - rmSync(link) + // unlink deletes the reparse point itself on Windows too; rmSync treats a + // junction as a directory and throws EISDIR unless recursive. + unlinkSync(link) } try { symlinkSync(target, link, 'junction') From 22d30852f7bba25651411d0efce0be42bdad4f80 Mon Sep 17 00:00:00 2001 From: Huanqi Cao Date: Wed, 12 Aug 2026 11:19:01 +0800 Subject: [PATCH 14/24] test(app-boot): unlink hmr alias junctions before removing the target --- packages/boot/app-boot/tests/hmr-config.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/boot/app-boot/tests/hmr-config.spec.ts b/packages/boot/app-boot/tests/hmr-config.spec.ts index c248643130..5f52ab5e2a 100644 --- a/packages/boot/app-boot/tests/hmr-config.spec.ts +++ b/packages/boot/app-boot/tests/hmr-config.spec.ts @@ -62,7 +62,7 @@ describe('HMR exact config paths', () => { expect(cacheHas).toHaveBeenCalledWith(expected) } finally { await ctx.fiber.dispose() - rmSync(alias, { force: true }) + unlinkSync(alias) rmSync(target, { recursive: true, force: true }) } }) @@ -78,7 +78,7 @@ describe('HMR exact config paths', () => { .rejects.toThrow('config path already registered') } finally { await ctx.fiber.dispose() - rmSync(alias, { force: true }) + unlinkSync(alias) rmSync(target, { recursive: true, force: true }) } }) From a62884a565bf6b71275b13941e8a6c7decd3ec25 Mon Sep 17 00:00:00 2001 From: Huanqi Cao Date: Wed, 12 Aug 2026 11:32:03 +0800 Subject: [PATCH 15/24] docs: add junction-safe unlink rule to defensive patterns --- docs/defensive-patterns.i18n.yaml | 4 ++-- docs/defensive-patterns.md | 4 ++++ docs/defensive-patterns.zh.md | 6 +++++- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/docs/defensive-patterns.i18n.yaml b/docs/defensive-patterns.i18n.yaml index e845e8182b..9a82a28afd 100644 --- a/docs/defensive-patterns.i18n.yaml +++ b/docs/defensive-patterns.i18n.yaml @@ -2,5 +2,5 @@ # 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 docs/defensive-patterns.md -defensive-patterns.md: b6e643cad6180ea35a363f2c4131b2c24f5f70af -defensive-patterns.zh.md: e1a6abdfd2138a51a13ceaf418bf6df1d0c65579 +defensive-patterns.md: 9db582354628a13abd43c5bd052dbbfd6e52f79f +defensive-patterns.zh.md: 7bebbe3c1964f2b826afc523eaa4af7be180e36e diff --git a/docs/defensive-patterns.md b/docs/defensive-patterns.md index b6e643cad6..9db5823546 100644 --- a/docs/defensive-patterns.md +++ b/docs/defensive-patterns.md @@ -27,3 +27,7 @@ A user-supplied listener that throws must not reject the promise it runs inside ## Never hand untrusted output the ambient environment or predictable paths Spawned commands get a scrubbed env (drop `*KEY*`/`*SECRET*`/`*TOKEN*`/`*PASSWORD*`) so harness credentials cannot leak into output, `env`, or spill files. Temp/spill files use a private (0700) dir, random names, and exclusive owner-only opens (`'wx'`, `0o600`) — predictable world-readable paths invite symlink races and disclosure. + +## Unlink link-shaped paths + +A path that may be a symlink or Windows junction is removed with `lstatSync().isSymbolicLink()` then `unlinkSync`: unlink deletes only the link and refuses a real directory, so it never follows the link into its target. Windows `rmSync(link)` throws `ERR_FS_EISDIR` on a junction; recursive deletion may descend through one into its target. Reserve recursive `rmSync` for known real directories. diff --git a/docs/defensive-patterns.zh.md b/docs/defensive-patterns.zh.md index e1a6abdfd2..7bebbe3c19 100644 --- a/docs/defensive-patterns.zh.md +++ b/docs/defensive-patterns.zh.md @@ -26,4 +26,8 @@ ## 绝不将环境变量或可预测路径暴露给不可信输出 -启动的命令应使用经过清理的环境变量,移除名称匹配 `*KEY*`、`*SECRET*`、`*TOKEN*` 或 `*PASSWORD*` 的项,防止 harness 凭证通过命令输出、`env` 或 spill 文件泄漏。临时文件和 spill 文件应放在权限为 0700 的私有目录中,使用随机文件名,并以独占且仅所有者可访问的方式打开(`'wx'`、`0o600`);可预测且所有用户均可读的路径会引发符号链接竞态和信息泄露。 +启动的命令应使用经过清理的环境变量,移除名称匹配 `*KEY*`、`*SECRET*`、`*TOKEN*` 或 `*PASSWORD*` 的项,防止 harness 凭证通过命令输出、`env` 或 spill 文件泄漏。临时文件和 spill 文件应放在权限为 0700 的私有目录中,使用随机文件名,并以独占且仅所有者可访问的方式打开(`'wx'`、`0o600`);可预测且全局可读的路径会引发符号链接竞态和信息泄露。 + +## 用 unlink 删除链接形态的路径 + +可能是符号链接或 Windows junction 的路径,应先用 `lstatSync().isSymbolicLink()` 判断,再用 `unlinkSync` 删除:unlink 只删除链接本身并拒绝真实目录,因此绝不会跟随链接进入其目标。Windows 上对 junction 调用 `rmSync(link)` 会抛 `ERR_FS_EISDIR`;递归删除可能穿过 junction 进入其目标。真实目录才使用带 `recursive` 的 `rmSync`。 From c29246ae31bcff83729f18bf6cf72fb855b44770 Mon Sep 17 00:00:00 2001 From: Huanqi Cao Date: Wed, 12 Aug 2026 14:04:45 +0800 Subject: [PATCH 16/24] fix(pwsh): resolve Store app execution aliases --- ...08-12-resolve-store-pwsh-aliases.i18n.yaml | 6 +++++ .../2026-08-12-resolve-store-pwsh-aliases.md | 23 +++++++++++++++++++ ...026-08-12-resolve-store-pwsh-aliases.zh.md | 23 +++++++++++++++++++ packages/shell/pwsh-local/src/resolve.ts | 17 ++++++++++++-- .../shell/pwsh-local/tests/executor.spec.ts | 20 +++++++++++++--- 5 files changed, 84 insertions(+), 5 deletions(-) create mode 100644 .agents/notes/implemented/bug-fix/2026-08-12-resolve-store-pwsh-aliases.i18n.yaml create mode 100644 .agents/notes/implemented/bug-fix/2026-08-12-resolve-store-pwsh-aliases.md create mode 100644 .agents/notes/implemented/bug-fix/2026-08-12-resolve-store-pwsh-aliases.zh.md diff --git a/.agents/notes/implemented/bug-fix/2026-08-12-resolve-store-pwsh-aliases.i18n.yaml b/.agents/notes/implemented/bug-fix/2026-08-12-resolve-store-pwsh-aliases.i18n.yaml new file mode 100644 index 0000000000..140c7b9ae7 --- /dev/null +++ b/.agents/notes/implemented/bug-fix/2026-08-12-resolve-store-pwsh-aliases.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 .agents/notes/implemented/bug-fix/2026-08-12-resolve-store-pwsh-aliases.md +2026-08-12-resolve-store-pwsh-aliases.md: 20fe58e15e75462dc0a9ba76c7a1a94939f8a004 +2026-08-12-resolve-store-pwsh-aliases.zh.md: bbfa4616127a9dbdb6609fe2973283663de55b31 diff --git a/.agents/notes/implemented/bug-fix/2026-08-12-resolve-store-pwsh-aliases.md b/.agents/notes/implemented/bug-fix/2026-08-12-resolve-store-pwsh-aliases.md new file mode 100644 index 0000000000..20fe58e15e --- /dev/null +++ b/.agents/notes/implemented/bug-fix/2026-08-12-resolve-store-pwsh-aliases.md @@ -0,0 +1,23 @@ +# Agent Note: Resolve Microsoft Store pwsh aliases + +Status: implemented + +English | [中文](2026-08-12-resolve-store-pwsh-aliases.zh.md) + +## Problem + +`resolvePwshPath` documented that Microsoft Store installs resolve through PATH, but its existence probe was `existsSync`, which stats a candidate and therefore follows reparse points. The Store's `%LOCALAPPDATA%\Microsoft\WindowsApps\pwsh.exe` is an app execution alias whose target directory ACL refuses stat (EACCES), so `existsSync` missed it and resolution silently fell through to Windows PowerShell 5.1 on hosts whose only PowerShell 7 is a Store install. + +## Decision + +`candidateExists` accepts a candidate that stats as a file or that lstat sees as a link-shaped reparse point, and `resolvePwshPath` uses it. Spawning the alias path works because CreateProcess resolves app execution aliases. A dangling link-shaped candidate is accepted so a broken pwsh fails loudly at spawn instead of silently downgrading to 5.1. + +## Alternatives considered + +**Probe the WindowsApps package directory directly.** The Store package path is versioned and ACL-hidden; hard-coding it duplicates packaging knowledge that PATH plus the alias already owns. + +**Keep the 5.1 fallback for stat failures.** Rejected: it silently runs a different shell than the one installed, which is the defect this note fixes. + +## Consequences + +Store-installed PowerShell 7 now resolves ahead of the 5.1 fallback on Windows; real-file candidates and non-Windows behavior are unchanged. The dangling-symlink unit test pins the stat/lstat split on every platform. diff --git a/.agents/notes/implemented/bug-fix/2026-08-12-resolve-store-pwsh-aliases.zh.md b/.agents/notes/implemented/bug-fix/2026-08-12-resolve-store-pwsh-aliases.zh.md new file mode 100644 index 0000000000..bbfa461612 --- /dev/null +++ b/.agents/notes/implemented/bug-fix/2026-08-12-resolve-store-pwsh-aliases.zh.md @@ -0,0 +1,23 @@ +# Agent Note: 解析 Microsoft Store 的 pwsh 别名 + +Status: implemented + +[English](2026-08-12-resolve-store-pwsh-aliases.md) | 中文 + +## 问题 + +`resolvePwshPath` 声称 Store 安装经 PATH 解析,但它的存在性探测用的是 `existsSync`,会对候选做 stat、从而跟随重解析点。Store 的 `%LOCALAPPDATA%\Microsoft\WindowsApps\pwsh.exe` 是 app execution alias,其目标目录的 ACL 拒绝 stat(EACCES),于是 `existsSync` 看不到它,解析静默落到 Windows PowerShell 5.1——在这类「唯一的 PowerShell 7 是 Store 安装」的机器上就用了错误的 shell。 + +## 决策 + +`candidateExists` 接受「stat 为文件」或「lstat 为链接形态重解析点」的候选,`resolvePwshPath` 改用它。spawn 别名路径可以工作,因为 CreateProcess 会解析 app execution alias。悬空的链接形态候选同样被接受,让损坏的 pwsh 在 spawn 时响亮失败,而不是静默降级到 5.1。 + +## 考虑过的替代方案 + +**直接探测 WindowsApps 包目录。** Store 包路径带版本且被 ACL 隐藏;硬编码它只是重复了 PATH 加别名已经拥有的打包知识。 + +**对 stat 失败继续走 5.1 回退。** 否决:它静默运行了一个并非所装的 shell,这正是本 note 修复的缺陷。 + +## 后果 + +Windows 上 Store 安装的 PowerShell 7 现在先于 5.1 回退被解析;普通文件候选和非 Windows 平台行为不变。悬空 symlink 单元测试在全部平台上钉住 stat/lstat 的分裂行为。 diff --git a/packages/shell/pwsh-local/src/resolve.ts b/packages/shell/pwsh-local/src/resolve.ts index c6ded2f883..f090c2b4c1 100644 --- a/packages/shell/pwsh-local/src/resolve.ts +++ b/packages/shell/pwsh-local/src/resolve.ts @@ -8,7 +8,7 @@ * @module @deepseek-ai/dsh-pwsh-local/resolve */ -import { existsSync } from 'node:fs' +import { existsSync, lstatSync } from 'node:fs' import { join } from 'node:path' /** @@ -36,6 +36,19 @@ export function candidatePwshPaths(env: NodeJS.ProcessEnv = process.env): string return candidates } +/** Whether a candidate can be spawned: a real file or a link-shaped reparse point. */ +function candidateExists(candidate: string): boolean { + if (existsSync(candidate)) return true + // Microsoft Store app execution aliases are reparse points whose target + // ACL refuses stat(), so existsSync misses them; lstat sees the link + // itself and CreateProcess resolves it when the executor spawns. + try { + return lstatSync(candidate).isSymbolicLink() + } catch { + return false + } +} + /** * Resolve the pwsh executable this executor spawns. * @param configured - an explicit `pwshPath` config value, trusted as-is. @@ -53,7 +66,7 @@ export function resolvePwshPath( if (configured !== undefined && configured.length > 0) return configured if (platform === 'win32') { for (const candidate of candidatePwshPaths(env)) { - if (existsSync(candidate)) return candidate + if (candidateExists(candidate)) return candidate } } return 'pwsh' diff --git a/packages/shell/pwsh-local/tests/executor.spec.ts b/packages/shell/pwsh-local/tests/executor.spec.ts index 2336d216ca..f838c2f577 100644 --- a/packages/shell/pwsh-local/tests/executor.spec.ts +++ b/packages/shell/pwsh-local/tests/executor.spec.ts @@ -9,7 +9,7 @@ * writes CRLF on Windows, so exact text assertions normalize line endings. */ -import { mkdirSync, mkdtempSync, realpathSync, writeFileSync } from 'node:fs' +import { mkdirSync, mkdtempSync, realpathSync, symlinkSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' import { spawnSync } from 'node:child_process' @@ -126,6 +126,18 @@ describe('resolvePwshPath and candidatePwshPaths (pure, every platform)', () => expect(resolvePwshPath(undefined, { ProgramFiles: join(dir, 'missing'), PATH: join(dir, 'empty'), SystemRoot: join(dir, 'no-windows') }, 'win32')) .toBe('pwsh') }) + + it('accepts a link-shaped PATH candidate whose target cannot be stat-ed', () => { + // Store app execution aliases stat as EACCES but lstat as a link; a + // dangling symlink reproduces that split on every platform. + const dir = mkdtempSync(join(tmpdir(), 'dsh-pwsh-resolve-link-')) + const store = join(dir, 'store') + mkdirSync(store, { recursive: true }) + const link = join(store, 'pwsh.exe') + symlinkSync(join(dir, 'no-such-target.exe'), link) + expect(resolvePwshPath(undefined, { ProgramFiles: join(dir, 'missing'), PATH: store }, 'win32')) + .toBe(link) + }) }) describe('spawn construction (pure, every platform)', () => { @@ -298,8 +310,10 @@ describe.skipIf(!hasPwsh)('PwshLocalExecutor.start (background process handles)' it('start returns immediately with a running handle that settles as completed', async () => { const { bash } = await setup() const before = Date.now() - const proc = bash.start(bash.resolve({ command: 'Start-Sleep -Milliseconds 200; Write-Output done' })) - expect(Date.now() - before).toBeLessThan(150) + // The sleep outlasts any realistic spawn latency, so returning while the + // child still sleeps proves start() does not wait for completion. + const proc = bash.start(bash.resolve({ command: 'Start-Sleep -Milliseconds 2000; Write-Output done' })) + expect(Date.now() - before).toBeLessThan(1000) expect(proc.status).toBe('running') await proc.done expect(proc.status).toBe('completed') From 095fde4ffd4d93bb69aff0ae05ea3789c0e3891b Mon Sep 17 00:00:00 2001 From: Huanqi Cao Date: Wed, 12 Aug 2026 14:04:57 +0800 Subject: [PATCH 17/24] test(claude-code): isolate ambient Anthropic model env --- .../tests/real-product.spec.ts | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/packages/subagent/subagent-claude-code/tests/real-product.spec.ts b/packages/subagent/subagent-claude-code/tests/real-product.spec.ts index 0ccd897fe9..f6767817c8 100644 --- a/packages/subagent/subagent-claude-code/tests/real-product.spec.ts +++ b/packages/subagent/subagent-claude-code/tests/real-product.spec.ts @@ -17,7 +17,7 @@ import type { SDKSystemMessage, } from '@anthropic-ai/claude-agent-sdk' import { Context } from '@deepseek-ai/cordis' -import { afterEach, describe, expect, it, vi } from 'vitest' +import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest' import type { Agent } from '@deepseek-ai/dsh-agent' import SubagentRuntime from '@deepseek-ai/dsh-subagent' import type { SubprocessHandle, SubprocessSpawnSpec } from '@deepseek-ai/dsh-subprocess' @@ -87,6 +87,22 @@ const roots: string[] = [] const fixtures: MessagesFixture[] = [] const contexts: Context[] = [] +// Ambient Anthropic model env leaks into the real CLI and overrides the +// fixture settings.json on developer machines; delete it for this file and +// restore it after, like the workspace-context USERPROFILE isolation. +const ambientAnthropicModel = process.env.ANTHROPIC_MODEL +const ambientAnthropicSmallFastModel = process.env.ANTHROPIC_SMALL_FAST_MODEL + +beforeAll(() => { + delete process.env.ANTHROPIC_MODEL + delete process.env.ANTHROPIC_SMALL_FAST_MODEL +}) + +afterAll(() => { + if (ambientAnthropicModel !== undefined) process.env.ANTHROPIC_MODEL = ambientAnthropicModel + if (ambientAnthropicSmallFastModel !== undefined) process.env.ANTHROPIC_SMALL_FAST_MODEL = ambientAnthropicSmallFastModel +}) + afterEach(async () => { await Promise.all(contexts.splice(0).map(ctx => ctx.fiber.dispose())) await Promise.all(fixtures.splice(0).map(fixture => fixture.close())) From 7593842ccfa01b89a83ce5ae999e689bd65121bd Mon Sep 17 00:00:00 2001 From: Huanqi Cao Date: Wed, 12 Aug 2026 15:11:05 +0800 Subject: [PATCH 18/24] fix(workflow): forward the tsconfig pin only in the unbuilt worker --- .../workflow-worker-thread/src/host.ts | 13 ++++++---- .../tests/workflow-worker-thread.spec.ts | 24 ++++++++++--------- 2 files changed, 21 insertions(+), 16 deletions(-) diff --git a/packages/workflow/workflow-worker-thread/src/host.ts b/packages/workflow/workflow-worker-thread/src/host.ts index 9e0c3a514a..fee869753f 100644 --- a/packages/workflow/workflow-worker-thread/src/host.ts +++ b/packages/workflow/workflow-worker-thread/src/host.ts @@ -38,18 +38,21 @@ interface ChildRecord { * The unbuilt shape additionally forwards `TSX_TSCONFIG_PATH` for path * resolution. * @param platform - host platform; overridable so tests exercise both peer arms. + * @param tsconfigPath - the tsconfig pin to forward; only the unbuilt caller + * passes one, so the built worker never observes the host's pin. * @returns the scrubbed worker environment object. */ -export function workerSpawnEnv(platform: NodeJS.Platform = process.platform): NodeJS.ProcessEnv { +export function workerSpawnEnv( + platform: NodeJS.Platform = process.platform, + tsconfigPath: string | undefined = undefined, +): NodeJS.ProcessEnv { const env: NodeJS.ProcessEnv = {} if (platform === 'win32') { const tmp = tmpdir() env.TMP = tmp env.TEMP = tmp } - if (process.env.TSX_TSCONFIG_PATH !== undefined) { - env.TSX_TSCONFIG_PATH = process.env.TSX_TSCONFIG_PATH - } + if (tsconfigPath !== undefined) env.TSX_TSCONFIG_PATH = tsconfigPath return env } @@ -81,7 +84,7 @@ function resolveWorkerSpawn(init: WorkerInit): { entry: string | URL; options: W entry: new URL(`data:text/javascript,${encodeURIComponent(bootstrap)}`), options: { workerData: init, - env: workerSpawnEnv(), + env: workerSpawnEnv(undefined, process.env.TSX_TSCONFIG_PATH), execArgv: [], }, } diff --git a/packages/workflow/workflow-worker-thread/tests/workflow-worker-thread.spec.ts b/packages/workflow/workflow-worker-thread/tests/workflow-worker-thread.spec.ts index 275c92269e..5130071362 100644 --- a/packages/workflow/workflow-worker-thread/tests/workflow-worker-thread.spec.ts +++ b/packages/workflow/workflow-worker-thread/tests/workflow-worker-thread.spec.ts @@ -570,6 +570,11 @@ describe('dsh-workflow-worker-thread', () => { // inside the worker resolves instead of degrading to a cwd-relative // `undefined\temp` (tsx writes its transform cache there). process.env.WORKFLOW_ENV_CANARY = 'leak me' + // The unbuilt worker forwards TSX_TSCONFIG_PATH (a path pin, not a + // credential); clear it so this test observes the empty ambient case + // regardless of the parent's environment. + const tsconfigPath = process.env.TSX_TSCONFIG_PATH + delete process.env.TSX_TSCONFIG_PATH try { const result = await run(ctx, parent, scripted(` const proc = ${ESCAPE} @@ -579,6 +584,8 @@ describe('dsh-workflow-worker-thread', () => { const expectedKeys = process.platform === 'win32' ? ['TEMP', 'TMP'] : [] expect(result.value).toEqual({ canary: null, keys: expectedKeys }) } finally { + if (tsconfigPath === undefined) delete process.env.TSX_TSCONFIG_PATH + else process.env.TSX_TSCONFIG_PATH = tsconfigPath delete process.env.WORKFLOW_ENV_CANARY } }) @@ -591,17 +598,12 @@ describe('dsh-workflow-worker-thread', () => { it('workerSpawnEnv forwards TSX_TSCONFIG_PATH when the snapshot harness pins it', () => { const tsconfig = fileURLToPath(new URL('../../../../tsconfig.json', import.meta.url)) - vi.stubEnv('TSX_TSCONFIG_PATH', tsconfig) - try { - expect(workerSpawnEnv('linux')).toEqual({ TSX_TSCONFIG_PATH: tsconfig }) - expect(workerSpawnEnv('win32')).toEqual({ - TMP: tmpdir(), - TEMP: tmpdir(), - TSX_TSCONFIG_PATH: tsconfig, - }) - } finally { - vi.unstubAllEnvs() - } + expect(workerSpawnEnv('linux', tsconfig)).toEqual({ TSX_TSCONFIG_PATH: tsconfig }) + expect(workerSpawnEnv('win32', tsconfig)).toEqual({ + TMP: tmpdir(), + TEMP: tmpdir(), + TSX_TSCONFIG_PATH: tsconfig, + }) }) it('the unbuilt worker forwards exactly TSX_TSCONFIG_PATH through the scrub: the paths-map pin survives, secrets do not', async () => { From 29d420f1eee0f95674b8833461462f35fb16b6d8 Mon Sep 17 00:00:00 2001 From: Huanqi Cao Date: Wed, 12 Aug 2026 15:11:20 +0800 Subject: [PATCH 19/24] fix(pwsh): accept link-shaped candidates and sync the README contract --- packages/shell/pwsh-local/README.i18n.yaml | 4 ++-- packages/shell/pwsh-local/README.md | 2 +- packages/shell/pwsh-local/README.zh.md | 2 +- packages/shell/pwsh-local/src/resolve.ts | 20 ++++++++++++------- .../shell/pwsh-local/tests/executor.spec.ts | 11 ++++++++++ 5 files changed, 28 insertions(+), 11 deletions(-) diff --git a/packages/shell/pwsh-local/README.i18n.yaml b/packages/shell/pwsh-local/README.i18n.yaml index 84e7bfa43e..c9ce9f1d6b 100644 --- a/packages/shell/pwsh-local/README.i18n.yaml +++ b/packages/shell/pwsh-local/README.i18n.yaml @@ -2,5 +2,5 @@ # 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 packages/shell/pwsh-local/README.md -README.md: cfb58b569022bca11d18c196e0bce104247e4c81 -README.zh.md: 9393c5d210743d8423457f2d7df5900c1e0df182 +README.md: 2eccc59b919d1f729eef52a42581f4da0f1d9e60 +README.zh.md: a03f80343711a1471bc96017e23476a3253ec46c diff --git a/packages/shell/pwsh-local/README.md b/packages/shell/pwsh-local/README.md index cfb58b5690..2eccc59b91 100644 --- a/packages/shell/pwsh-local/README.md +++ b/packages/shell/pwsh-local/README.md @@ -30,7 +30,7 @@ The Windows counterpart of `dsh-bash-local`, deliberately mirroring its semantic - **Spawn per call, no shell state** — every call is a fresh non-interactive `pwsh -Command` (deterministic; no profile files). The `-NoLogo -NoProfile -NonInteractive` flags disable startup banners, profile loading, and prompts that would garble tool output. - **The composition entry is a layer, not the last word** — when a settings provider is composed, this executor registers the capability's [`bash` namespace](../shell/README.md) with the entry above as its base, so a user section in `settings.yaml` layers over it and the next command runs with the new budgets. The namespace is shared with the POSIX family because a host composes exactly one provider of `ctx.shell`; a document written on either platform keeps resolving on the other. Values the schema cannot judge (positive and finite, the `graceMs` timer bound) are refused at the write, leaving the running executor on its last good section. - **UTF-8 output pinned** — every command runs with `[Console]::OutputEncoding` and `$OutputEncoding` set to UTF-8 first, so the Windows PowerShell 5.1 fallback (or any host whose console code page is not UTF-8) cannot garble non-ASCII output: the subprocess collector decodes bytes as UTF-8. Input encoding is left at the host default; pwsh 7 defaults to UTF-8 and is unaffected. -- **Executable resolution** — `resolvePwshPath` prefers an explicit `pwshPath`, then on Windows probes PowerShell 7's install location, every PATH entry (Microsoft Store installs; surrounding quotes stripped), and Windows PowerShell 5.1 as a legacy last resort, checking `existsSync` on each; elsewhere it falls back to a bare `pwsh` resolved through PATH. Resolution is a pure function of `(configured, env, platform)`; it runs at construction and again only when a stored `pwshPath` differs from the one the current executable was resolved from, so an unrelated settings change never re-probes the filesystem. +- **Executable resolution** — `resolvePwshPath` prefers an explicit `pwshPath`, then on Windows probes PowerShell 7's install location, every PATH entry (Microsoft Store installs; surrounding quotes stripped), and Windows PowerShell 5.1 as a legacy last resort, checking each candidate with an lstat probe that accepts a real file or a link-shaped reparse point (a Store app execution alias stat-fails against its target's ACL, but lstat sees the alias itself); elsewhere it falls back to a bare `pwsh` resolved through PATH. Resolution is a pure function of `(configured, env, platform)`; it runs at construction and again only when a stored `pwshPath` differs from the one the current executable was resolved from, so an unrelated settings change never re-probes the filesystem. - **Configured budgets over managed groups** — `resolve()` fills `workdir`/`timeoutMs`/`stdoutMaxBytes` from config, and every spawn hands the service explicit byte caps, spill cap, and `graceMs`. The grace must be positive, finite, and no greater than [`MAX_TIMER_DELAY_MS`](../../util/timeout/README.md), so Node can represent it with one timer. Tree termination (taskkill on Windows, process-group signals on POSIX), the post-exit pipe-drain grace, tail-keep truncation, and bounded spill files are [`dsh-subprocess-local`](../../subprocess/subprocess-local/README.md) mechanics. A foreground `ShellExecRequest.stdoutMaxBytes` can raise stdout's capture budget for one trusted caller; stderr and background runs still use `maxOutputBytes`. - **Timeout and cancel classification** — `run()` fuses its config-clamped timeout with the caller's signal through one deadline; only the executor's own timeout reports `timedOut`, an upstream cancel reports `aborted`, and a self-terminated command reports neither ([timeout-library Agent Note](../../../.agents/notes/implemented/architecture/2026-07-06-timeout-deadline-library.md)). Windows reports forced termination as exit 1 without a signal, so signal-stamped facts (`signal`, `killed` status) are POSIX-only there; the timeout/abort classification is platform-independent. - **Model-friendly terminal env** — `NO_COLOR=1 PAGER=cat GIT_PAGER=cat` (no `TERM=dumb`: that is a POSIX concept; `NO_COLOR` is honored by modern PowerShell renderers) merged as ordinary env under the service's credential scrub and `DSH_*` channel rules; an explicit caller entry still wins. diff --git a/packages/shell/pwsh-local/README.zh.md b/packages/shell/pwsh-local/README.zh.md index 9393c5d210..a03f803437 100644 --- a/packages/shell/pwsh-local/README.zh.md +++ b/packages/shell/pwsh-local/README.zh.md @@ -30,7 +30,7 @@ - **每次调用新建进程,无 shell 状态**——每次调用都是全新的非交互 `pwsh -Command`(确定性;不加载 profile 文件)。`-NoLogo -NoProfile -NonInteractive` 关闭启动横幅、profile 加载与会干扰工具输出的提示符。 - **组装条目是一层,而不是最终值**——当组装中存在 settings 提供方时,本执行器以上面的条目为 base 注册该能力的 [`bash` 命名空间](../shell/README.md),因此 `settings.yaml` 中的用户段会叠加其上,下一条命令即按新预算运行。该命名空间与 POSIX 家族共用,因为一个宿主只组装一个 `ctx.shell` 提供方;在任一平台写下的文档在另一平台仍能解析。schema 无法判定的值(正有限、`graceMs` 的定时器上界)会在写入时被拒绝,运行中的执行器保持它最后一份可用的段。 - **UTF-8 输出固定**——每条命令都先以 UTF-8 设置 `[Console]::OutputEncoding` 与 `$OutputEncoding`,因此 Windows PowerShell 5.1 兜底(或任何控制台代码页非 UTF-8 的主机)不会破坏非 ASCII 输出:subprocess 收集器以 UTF-8 解码字节。输入编码保持宿主默认;pwsh 7 默认为 UTF-8,不受影响。 -- **可执行文件解析**——`resolvePwshPath` 优先显式 `pwshPath`,然后在 Windows 上依次探测 PowerShell 7 安装位置、每个 PATH 条目(Microsoft Store 安装;剥离两端引号)以及作为遗留兜底的 Windows PowerShell 5.1,逐一检查 `existsSync`;其他平台回退为通过 PATH 解析的裸 `pwsh`。解析是 `(configured, env, platform)` 的纯函数;它在构造时执行,此后仅当存储的 `pwshPath` 与当前可执行文件所依据的值不同才再次执行,因此无关的设置变更绝不会重新探测文件系统。 +- **可执行文件解析**——`resolvePwshPath` 优先显式 `pwshPath`,然后在 Windows 上依次探测 PowerShell 7 安装位置、每个 PATH 条目(Microsoft Store 安装;剥离两端引号)以及作为遗留兜底的 Windows PowerShell 5.1,逐一用 lstat 探测检查(接受真实文件或链接形态的重解析点:Store 的 app execution alias 对其目标 stat 会因 ACL 失败,但 lstat 能看到别名本身);其他平台回退为通过 PATH 解析的裸 `pwsh`。解析是 `(configured, env, platform)` 的纯函数;它在构造时执行,此后仅当存储的 `pwshPath` 与当前可执行文件所依据的值不同才再次执行,因此无关的设置变更绝不会重新探测文件系统。 - **受管进程组之上的配置预算**——`resolve()` 从配置填充 `workdir`/`timeoutMs`/`stdoutMaxBytes`,每次 spawn 都向服务提供显式字节上限、spill 上限与 `graceMs`。该宽限期须为正有限值,且不得大于 [`MAX_TIMER_DELAY_MS`](../../util/timeout/README.md),这样 Node 就能用一个定时器表示它。进程树终止(Windows 用 taskkill,POSIX 用进程组信号)、退出后管道排空宽限、保尾截断与有界 spill 文件是 [`dsh-subprocess-local`](../../subprocess/subprocess-local/README.md) 的机制。前台 `ShellExecRequest.stdoutMaxBytes` 可为单个受信调用方提高 stdout 捕获预算;stderr 与后台运行仍使用 `maxOutputBytes`。 - **超时与取消分类**——`run()` 通过一个 deadline 融合按配置上限截取的超时与调用方信号;只有执行器自身超时报告 `timedOut`,上游取消报告 `aborted`,自我终止的命令两者都不报告(见 [timeout 库 Agent Note](../../../.agents/notes/implemented/architecture/2026-07-06-timeout-deadline-library.md))。Windows 将强制终止报告为退出码 1 且无信号,因此带信号标记的事实(`signal`、`killed` 状态)在那里仅限 POSIX;超时/取消分类与平台无关。 - **面向模型的终端环境**——`NO_COLOR=1 PAGER=cat GIT_PAGER=cat`(没有 `TERM=dumb`:那是 POSIX 概念;现代 PowerShell 渲染器遵循 `NO_COLOR`),作为普通 env 在服务的凭据清理与 `DSH_*` 通道规则之下合并;显式调用方条目仍然优先。 diff --git a/packages/shell/pwsh-local/src/resolve.ts b/packages/shell/pwsh-local/src/resolve.ts index f090c2b4c1..191abba963 100644 --- a/packages/shell/pwsh-local/src/resolve.ts +++ b/packages/shell/pwsh-local/src/resolve.ts @@ -8,7 +8,7 @@ * @module @deepseek-ai/dsh-pwsh-local/resolve */ -import { existsSync, lstatSync } from 'node:fs' +import { lstatSync } from 'node:fs' import { join } from 'node:path' /** @@ -36,15 +36,21 @@ export function candidatePwshPaths(env: NodeJS.ProcessEnv = process.env): string return candidates } -/** Whether a candidate can be spawned: a real file or a link-shaped reparse point. */ +/** + * Whether a candidate can be spawned. lstat opens the entry itself instead of + * following reparse points, so it sees the Store app execution alias where + * stat hits the target's ACL (EACCES); Node reports that alias as a symlink + * on current releases and as a plain file on older ones, and CreateProcess + * resolves either shape. A real directory never matches. + */ function candidateExists(candidate: string): boolean { - if (existsSync(candidate)) return true - // Microsoft Store app execution aliases are reparse points whose target - // ACL refuses stat(), so existsSync misses them; lstat sees the link - // itself and CreateProcess resolves it when the executor spawns. try { - return lstatSync(candidate).isSymbolicLink() + const stat = lstatSync(candidate) + return stat.isFile() || stat.isSymbolicLink() } catch { + // ENOENT (the candidate vanished between listing and probing) is the only + // expected failure; any other error names an unspawnable path, so false + // is the safe answer for it too. return false } } diff --git a/packages/shell/pwsh-local/tests/executor.spec.ts b/packages/shell/pwsh-local/tests/executor.spec.ts index f838c2f577..e26211b268 100644 --- a/packages/shell/pwsh-local/tests/executor.spec.ts +++ b/packages/shell/pwsh-local/tests/executor.spec.ts @@ -138,6 +138,17 @@ describe('resolvePwshPath and candidatePwshPaths (pure, every platform)', () => expect(resolvePwshPath(undefined, { ProgramFiles: join(dir, 'missing'), PATH: store }, 'win32')) .toBe(link) }) + + it('skips a directory candidate and falls through to the PATH-resolution default', () => { + const dir = mkdtempSync(join(tmpdir(), 'dsh-pwsh-resolve-dir-')) + const store = join(dir, 'store') + mkdirSync(join(store, 'pwsh.exe'), { recursive: true }) + expect(resolvePwshPath(undefined, { + ProgramFiles: join(dir, 'missing'), + PATH: store, + SystemRoot: join(dir, 'no-windows'), + }, 'win32')).toBe('pwsh') + }) }) describe('spawn construction (pure, every platform)', () => { From 314fedfc14d3fbc14328147515befda34e93dcf8 Mon Sep 17 00:00:00 2001 From: Huanqi Cao Date: Wed, 12 Aug 2026 15:11:38 +0800 Subject: [PATCH 20/24] docs: record the junction-safe fixture teardown decision --- ...-fixture-junctions-before-delete.i18n.yaml | 6 +++++ ...-unlink-fixture-junctions-before-delete.md | 23 +++++++++++++++++++ ...link-fixture-junctions-before-delete.zh.md | 23 +++++++++++++++++++ 3 files changed, 52 insertions(+) create mode 100644 .agents/notes/implemented/bug-fix/2026-08-12-unlink-fixture-junctions-before-delete.i18n.yaml create mode 100644 .agents/notes/implemented/bug-fix/2026-08-12-unlink-fixture-junctions-before-delete.md create mode 100644 .agents/notes/implemented/bug-fix/2026-08-12-unlink-fixture-junctions-before-delete.zh.md diff --git a/.agents/notes/implemented/bug-fix/2026-08-12-unlink-fixture-junctions-before-delete.i18n.yaml b/.agents/notes/implemented/bug-fix/2026-08-12-unlink-fixture-junctions-before-delete.i18n.yaml new file mode 100644 index 0000000000..160f947daa --- /dev/null +++ b/.agents/notes/implemented/bug-fix/2026-08-12-unlink-fixture-junctions-before-delete.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 .agents/notes/implemented/bug-fix/2026-08-12-unlink-fixture-junctions-before-delete.md +2026-08-12-unlink-fixture-junctions-before-delete.md: 4514a33d728866b817f4e9c1393f16c07976ed45 +2026-08-12-unlink-fixture-junctions-before-delete.zh.md: 3c212c052ab0303ccb8d31d2b310a365a1d8cc99 diff --git a/.agents/notes/implemented/bug-fix/2026-08-12-unlink-fixture-junctions-before-delete.md b/.agents/notes/implemented/bug-fix/2026-08-12-unlink-fixture-junctions-before-delete.md new file mode 100644 index 0000000000..4514a33d72 --- /dev/null +++ b/.agents/notes/implemented/bug-fix/2026-08-12-unlink-fixture-junctions-before-delete.md @@ -0,0 +1,23 @@ +# Agent Note: Unlink fixture junctions before recursive deletion + +Status: implemented + +English | [中文](2026-08-12-unlink-fixture-junctions-before-delete.zh.md) + +## Problem + +The install-lefthook and translation-pairing fixtures junction the repository's real `scripts/`, `node_modules`, and tsx package directories into fixture trees so installer probes resolve through them. Windows recursive deletion can treat a junction (a MOUNT_POINT reparse point) as a directory and follow it into its target; Git's `worktree remove` did exactly that and deleted the repository's tracked `scripts/` and tsx package (the incident's instrumentation pinned the deletion to that step). A fixture cleanup that trusts its deleter therefore deletes the repository's own sources instead of the fixture. + +## Decision + +`scripts/test-fixture-cleanup.ts` owns junction-safe fixture teardown: `unlinkFixtureLinks` walks a tree and unlinks every reparse point before `removeFixtureSafely` removes the now link-free tree (with Windows async-handle retries). Every affected `afterEach` and the pre-`worktree remove` hook call it. The general rule lives in `docs/defensive-patterns.md`: remove link-shaped paths with unlink, reserve recursive `rmSync` for known real directories. + +## Alternatives considered + +**Trust recursive deletion alone.** Rejected: whether a given deleter follows junctions is tool- and version-dependent, and one path through `git worktree remove` already destroyed tracked files; no cleanup may bet the repository on that behavior. + +**Copy instead of junctioning the real directories.** Rejected: the fixtures exist to probe the real installer paths through their real contents, so copies would stop exercising the boundary under test. + +## Consequences + +Fixture teardown can no longer reach repository sources through junctions. The extra walk is one lstat/unlink pass over small fixture trees. The data-destroying defect now has its durable why beside the defensive-patterns rule, and the helper is the shared teardown path for future junction fixtures. diff --git a/.agents/notes/implemented/bug-fix/2026-08-12-unlink-fixture-junctions-before-delete.zh.md b/.agents/notes/implemented/bug-fix/2026-08-12-unlink-fixture-junctions-before-delete.zh.md new file mode 100644 index 0000000000..3c212c052a --- /dev/null +++ b/.agents/notes/implemented/bug-fix/2026-08-12-unlink-fixture-junctions-before-delete.zh.md @@ -0,0 +1,23 @@ +# Agent Note: 递归删除前先解链 fixture junction + +Status: implemented + +[English](2026-08-12-unlink-fixture-junctions-before-delete.md) | 中文 + +## 问题 + +install-lefthook 与 translation-pairing 的 fixture 把仓库真实的 `scripts/`、`node_modules` 和 tsx 包目录用 junction 链进 fixture 树,让 installer 探测能穿透解析。Windows 的递归删除可能把 junction(MOUNT_POINT 重解析点)当作目录并跟随进其目标;Git 的 `worktree remove` 正是这样删掉了仓库被跟踪的 `scripts/` 和 tsx 包(事故的插桩把删除定位到这一步)。因此,信任删除器的 fixture 清理删掉的是仓库自己的源码,而不是 fixture。 + +## 决策 + +`scripts/test-fixture-cleanup.ts` 拥有 junction 安全的 fixture 拆除:`unlinkFixtureLinks` 先遍历并解链所有重解析点,`removeFixtureSafely` 再删除已无链接的树(带 Windows 异步句柄重试)。所有受影响的 `afterEach` 和 `worktree remove` 前的钩子都调用它。通用规则记录在 `docs/defensive-patterns.md`:链接形态的路径用 unlink 删除,递归 `rmSync` 只留给确知为真实目录的路径。 + +## 考虑过的替代方案 + +**只信任递归删除。** 否决:特定删除器是否跟随 junction 随工具和版本而异,而 `git worktree remove` 这一条路径已经摧毁过被跟踪文件;任何清理都不该拿仓库去赌这个行为。 + +**复制而不是 junction 真实目录。** 否决:fixture 的意义就是用真实内容探测真实 installer 路径,复制品会失去被测边界。 + +## 后果 + +fixture 拆除不再能穿过 junction 触及仓库源码。额外开销只是对小型 fixture 树的一趟 lstat/unlink。这个摧毁数据的缺陷现在在 defensive-patterns 规则旁有了持久化的原因,helper 也是未来所有 junction fixture 共享的拆除路径。 From ed463d74bfce64463a8f39fd6c46df79670b8db3 Mon Sep 17 00:00:00 2001 From: Huanqi Cao Date: Wed, 12 Aug 2026 16:10:45 +0800 Subject: [PATCH 21/24] fix(workflow): use optional syntax for the tsconfig pin parameter --- packages/workflow/workflow-worker-thread/src/host.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/workflow/workflow-worker-thread/src/host.ts b/packages/workflow/workflow-worker-thread/src/host.ts index fee869753f..394b65ca8a 100644 --- a/packages/workflow/workflow-worker-thread/src/host.ts +++ b/packages/workflow/workflow-worker-thread/src/host.ts @@ -44,7 +44,7 @@ interface ChildRecord { */ export function workerSpawnEnv( platform: NodeJS.Platform = process.platform, - tsconfigPath: string | undefined = undefined, + tsconfigPath?: string, ): NodeJS.ProcessEnv { const env: NodeJS.ProcessEnv = {} if (platform === 'win32') { From b5a130063d8fb26a69765962e6d57f14fe4f02bc Mon Sep 17 00:00:00 2001 From: Chinesezjc Date: Thu, 13 Aug 2026 15:09:44 +0800 Subject: [PATCH 22/24] fix(scripts): widen fixture-cleanup EPERM retry window The failover Windows pool releases child-process and antivirus file handles slower than the hosted pool; a 1-second window (10 x 100 ms) still exhausts before release under load, so install-lefthook fixture cleanup threw EPERM in rmSync. A 10-second window (50 x 200 ms) covers the slower release without pinning afterEach cleanup, since a terminated child's handles drain once rather than reacquire. --- scripts/test-fixture-cleanup.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/scripts/test-fixture-cleanup.ts b/scripts/test-fixture-cleanup.ts index 9c58eea890..bf6fb18fea 100644 --- a/scripts/test-fixture-cleanup.ts +++ b/scripts/test-fixture-cleanup.ts @@ -37,10 +37,13 @@ export function unlinkFixtureLinks(path: string): void { * Remove one fixture tree after its junctions are unlinked (see * {@link unlinkFixtureLinks}). Retries the removal: Windows releases child * process and antivirus file handles asynchronously, and an unretried - * `rmSync` fails immediately with EPERM under load. + * `rmSync` fails immediately with EPERM under load. A 10-second retry window + * (50 attempts × 200 ms) covers the failover pool's slow handle release; + * release is one-shot (a terminated child's handles drain, not reacquired), + * so a bounded window suffices and never pins afterEach cleanup. * @param path - the fixture tree to remove. */ export function removeFixtureSafely(path: string): void { unlinkFixtureLinks(path) - rmSync(path, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 }) + rmSync(path, { recursive: true, force: true, maxRetries: 50, retryDelay: 200 }) } From 9ba718104cd28614588a336764b95a4d02434d41 Mon Sep 17 00:00:00 2001 From: Ag Date: Thu, 13 Aug 2026 15:32:38 +0800 Subject: [PATCH 23/24] docs: align community QR assets --- assets/community-wechat-official-account.png | Bin 156490 -> 35586 bytes assets/community-wecom-assistant.png | Bin 26676 -> 60324 bytes assets/community-wecom-survey.png | Bin 5508 -> 30477 bytes 3 files changed, 0 insertions(+), 0 deletions(-) diff --git a/assets/community-wechat-official-account.png b/assets/community-wechat-official-account.png index 32e36e34797a075d1118299bc772bf390c000761..222a051dc89dfcecd4d80d8992ded2b9108ed4f9 100644 GIT binary patch literal 35586 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 N?;Y$F@vk?-zEvj^J`AhQirHis&0WUh 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 diff --git a/assets/community-wecom-assistant.png b/assets/community-wecom-assistant.png index 71fc1aba61f49f7183ce0ee292daf81c16b62074..b735f2ccc71020628e6840136458d7a602139785 100644 GIT binary patch literal 60324 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 literal 26676 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 diff --git a/assets/community-wecom-survey.png b/assets/community-wecom-survey.png index d6f6040ba700f27fbb35647ed81e8b8d28083b4f..800865f78531d8a2592f9ab01497f3e88690aed7 100644 GIT binary patch literal 30477 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

*fh?~h7zOCCo?e&+eoj- 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 literal 5508 zcmYjVc|6qJ+y2fp!)THznoyXCN!dy!rpPXPmaJn)mO|M^X+kq?LP(Y*TlUDFeQBPj zl%*`gG!H2r*~XN@P$tWJ{NDHTetv(=`JC^W`&{R~@9Vnnb7Ia~nFw!}+zvsIu<02? zTL^-IAq+wZfFDvwi4O!}AX7s<`*7H|tjmw~HS9F%2zdWBb(s9*d+LWJ%hBbccRYjs)7i+ z*X}TGA)Fxe&a5=H!q4g(ScZw@ngl#1ljSKIG+N0K~Pn~7clgh*TPDia(aiTUiK4&f@ z;&K8iPJbB1N{W+Cake!~M;>vv?plmZH>|Hf;|?SCA#nXU0MhBuWBMNA;GpM?^$uVz zT6?Ip~6Gw_V~su-3M=&+Ruib?%g;xqiLbj=vGt-OssUIhovb z&3LR-Ue0FyN%&%n=gQ0YH`hy$Kfix!_=L)PTv+GD=PyphDI2hj<_lBp6vob|`q;nX zpFBLL96LN%AUXZAMZzLfNnGhR$!+>j(Yd}Ho||&&=a%M zx)D<*SLPVl=66$}PYkU)b|qBR$6<5F%Ljo)y|eO6!5%lau4%sqPTlW1Qa&t?M-)8H z5shplSBVG|oU+21U99VkLXKKp5o(Qt8;xPddX03hu@)kPqeAUDh(Zj9LC!ndt{!=9R342P&o7u5wZDfR%)WiAyL_ zEZD*JYk>r6Kj4Dq+qmKbYWwNF+dW~Y3`E>f7FIN*+A5?d1MkD6d%|YXvVEiztcWn` z`i{noK;^!oP_`Sto}~OS+iE$m`p3=g`Wg3H6URtm^)+p;<;V5JLRN%pEcGAHvCV)dGs8{^>?6Fs z6=wI%5@dg=;)L$_9KvP!9sHuvBTY>DLm*OrKeIW8Z~>Y0Qr2`o%Hj<0K5rn;2`=E@J8 zMvxu@J}XFK3-xm~U|3!Myf?QQhKObrD3X(x!mqPdBv+Qze6+|vQeqpSTo~?J^!9#? z-}^U1saY*6wym~v+MUxNsi*DCw2Sz5qxrk1PD=X!xXyAiYum7GB}e|~xN_;VoC6yf zWW4Ro9SE@q7<2t5fM>wPBXm<52=v;PAMFsObi`aX+(wxHG>qa$N)CAMqQpr# zQfkzjRk5LBtNmAW)t7Dt*$vw{l9piK;zpmGh z0j35nuF*2NxG6EbDq42eBVe(AUPyg0%17 zMR<<;iK1usRY6py!d`>`kQ(s-T*@|jHsYQ$M7;c6k+|n-UUGbQozrv4I*TV?69Cf%&$lGZE!eRG=UvX+ z4${z+;pD@b!UtGS$vPe7rj)Qf)ejbpmQ5_YLfe}UhqjqSseH-)GG!~(dt!og;T0!O zqLIWjS<<>46GD{mfi=PhfYi(Y;%2@}>%@p>11|sGWrIWXZYgAvC2&3{mPVHcWXB>QV&}2J}LR*#Z0s_M3tbFhuBZfj2l)T-~Vg3(Uup0 z*HwzJIT4AmWZ91Lm;jTnS<9_lSAKvM2=dX6g13Iw_|tpUncUL8j^4Kq7QHXOR-y%L z`>i*=9YJPoLyw=E>_XFFKnMiH;qtoU=eDv+)|!mkp$im)fjM;kGQ9HJmi#5#WWAT! z^H*qb`G7EeEpry}>!K9(OUI|d7Je90YL)mZV9cgMJ)m@O8JBHUQ`bmJBC7`hin7Ez z!U#u`0#$(rXANd@4XJeILqAzpt5I1 z5$lCmzhJ^DYvMa+l5GdlLHj945!_E1AP_hA|5ini^~YJJ&QN7Gw0hvDK1i&+f8#X} zWJ?n=zZLeaSA`AQt79-S*NlLe*I5_w18V6tTgnN9}1d7Fm3MMVql2 z?~s_#dwMD6s!icCsfT>LLYI5qu`mm<>77N5Ci2aTI+EPsnHCkAZ(N5A{J)x-|10PJ zlwrgRMX-!R$+&S25K;IwnVJ>lLvyvY7C5g_@>m|Gq%tj9i<* zJUQ)*>R5iB$o)9QPXpmpvPHcPsK`aWet8iUST{FTL_u75*ZgFAed4Q=IWNeOHy>;+ z^wl&q?h+l&bT4phPh35{p|wS+4!CB1u%|u*Z`g;XH9&AQP)?;&5iZdXnsyC>O9?$l zi$$u97?ucv@s3)E!e3kFvRy6n1|70;H`Z2{97MC~iD|^9im69dQFYSr3US9aw(W z?)Luv;~x1ozu=>N{UWu{P-aGD%spI-sBoUb{B@51bwZYoF4&@r-8Jq0hZ9lYNW#~g z4AxJlxVw(^(`^8hCsE1j$gv5$$~7rZ}&_AMUiE|XAv34{YAy={&4ovDy~r?ZAG(Q z5`PYarC*WqTPO&1)W1PpmEN;FJ?9u`^7XrRWx7gEMI9+|SLLB1aDpEB$f4*6|2$%5 z&Ya_D#w>4am9vsRU0ZDT-X*@W-|`iNuzU@;ZFL-h-_niMZ=4gG`KR61nKJRqolCLY zH_L!y)hj%p2g4CSAU_()gv=Co*eqQ)Y+&D1{UeXnRdt%AGgyG;6$Bf;L;|A|Sq&Ly z@2t}s_X5O8hKWkUIqv5xOQ_go;~ z<7X^Tx(omkhB&G|v8+qJoRia508OCEYV*qy!R^7SF*nuc=f|`4e6pos$ zD&5*@qfQ3e@naqbR72%9zi99<>W`@CdezB)8G{yz6VPf!vBI=Z(M}4#tRBQQwdY~V)nYjln^z}Rl0OGKM zaU161R=ve2Jo~7icHtLj5GjSWX6vPeDb>94=>11y+GC2ive!}z0_U5E-bV_1?dv=H zrs8VPfwsqu5pw(`B8E4|{&mOEnaZ##N~S zR{Z_Cc=;;AC;kmQJuGBM*eCjt5(FWYx84Hi0syTrZZ{lWyBl0FXYFr@XmqsAFD-2@1Ydnc@lS~K@p*{p(ma&XHA<3j(rZ9b?^!>2ntW~~vc ztxgKCf2p&hRSE%Pmxc#>PRQAeTC{fhCKfKm`(<_jZS1Edkc+=*S3awkw*5}LIOrYn z+?Z`bVF6e<_d{1cU0w{;2KDgk!Ns0`WYY$A3_UFi#;%K@J+SFRO<4L7N)Np8Z!r!G zZVG~SP;(R)gam^l7D>{$hX)z(Gq+w}z6^KdWFz}8J({PU<(D9aUCmOC_z#*kC6}xm z8k2|uo|~SdXq#_N^*nwW$C+r;?}N`8Mz-v1-IRzlw^Z$4U+UYCp6bc#4@+-}8WjAC z^s2I+~HXB`U`)sdUEHYs+m$O(M5o6f|S2hSV%8lcVZ{4vN+uYy2mTqz|i# zMc*`l0<%FIS<{fFTpy)#cRlQI-x0W&7C)EBji0Lqo&#(ojr8@*(FTgSXbL zvacRkwvw`H>Jm7hKc0H@xl1BSr6}Dntnrs&gP_W^1Rwm@quD?C#m0{GE91z7VWTti)VpcbNXx%on@q*~*~WzxU5P~oDL|eE9^SxJsm0bm zdbSGa=62}+Cq#BNE*bzITR;ASt1|Ea%E=YEronWrbh+B7p<%MFVfj+j0{^8h`4Yz~ zbbexLs}tauoA>suo~fi}HvVrpwaT=1P))0d`Yi4B)n^T;=uWf1NCB^pCNdayPQY-Z z#Obz(c}YvcUOSon?AuF8Uslhkwun0B^A^iZ-`LiJLR`~Ctf5xhmuqyrea{!Jg=&Py z1?lQSBy9qEd``6JGztNT4BKykX(R&1v+x!-F(}}9xMuoCg(`iZFEp#upWPoHkJF(l zQde?CCri?@2`pywuYmP(Zs*&^;ccg%1dcz6TWcoJS^}9U5L(@gT@nk%Hz5~|&0oFR652_+o&>eS2y)XmI{(EW+SQ`b_rZ|Itwl2dxui%i=6L5+6 wuL=KcPHlfS!BgtyW}|T2Ab3j^*b>fd&{GG#l$Hq*4`v`!BP+uSeUDrJ1BKcfi2wiq From 3c3121bf8aefdeea030c938ad5e3d76a48eca35f Mon Sep 17 00:00:00 2001 From: Turtle Date: Thu, 13 Aug 2026 15:39:23 +0800 Subject: [PATCH 24/24] Update README.zh.md --- README.zh.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.zh.md b/README.zh.md index f9dacc4e3c..639d519b62 100644 --- a/README.zh.md +++ b/README.zh.md @@ -37,7 +37,7 @@ pnpm dsh web ## 社区与支持 - 欢迎通过 [GitHub Discussions](https://github.com/deepseek-ai/deepseek-harness/discussions) 提交反馈或 bug 报告。 -- 为你的插件仓库添加 [`dsh-plugin`](https://github.com/topics/dsh-plugin) 主题,便于被发现。 +- 为你的插件仓库添加 [`dsh-plugin`](https://github.com/topics/dsh-plugin) 话题,便于被发现。 - 欢迎加入 DeepSeek Harness 企微群:扫码添加企微小助手并填写入群问卷,完成后小助手会邀请你入群。