Merge origin/master into fix/sdk-desktop-alignment

This commit is contained in:
fz committed 2026-10-19 11:40:37 +08:00
commit 8614ca9fb4
1157 files changed
+22143 -3350

No files matched your search

+3
View File
@@ -2077,6 +2077,9 @@
"process/2026-07-06-generated-config-catalog.i18n.yaml": "sha256:48e64ccfc7622559a9b964e2741ad82a9fa37243a0d5c472f0482d9c748d7739",
"process/2026-07-06-generated-config-catalog.md": "sha256:c8654a4e084bfbf4f7d8eb78239dc1d5d62e1384b2aed309fa2526a4d6de0f04",
"process/2026-07-06-generated-config-catalog.zh.md": "sha256:5ab8d964f12e0cf9a04251e988f0b2a114d7ae8ea7ed337c00f625213cb1dc94",
"process/2026-07-06-node-engine-floor.i18n.yaml": "sha256:65d73bf7a6c2fb7609ff3a8f18a435e83bb488e005365d065d184942958bfb35",
"process/2026-07-06-node-engine-floor.md": "sha256:b62c8e8dd008b0ab7874b5d57c20c629ffb71e4b4a62922afaf6a1039d25898e",
"process/2026-07-06-node-engine-floor.zh.md": "sha256:3cd685e59f913ef700b3e2ed3b941c2266bb2de706810cbe570f4832a26a5034",
"process/2026-07-06-parallel-github-ci-gates.i18n.yaml": "sha256:0f6ece268d9a51bc20cb8eb929f26d8838761603a64eb08dc24521198f10da36",
"process/2026-07-06-parallel-github-ci-gates.md": "sha256:6249bd7396ae7f2d0dc671879ce21cefab33a47ace6ef17a25a70e8650b815af",
"process/2026-07-06-parallel-github-ci-gates.zh.md": "sha256:cf7edb9bcf97ab1d4e452330c0df0b127a664509ec3f11597ace3eabeb663a5b",
@@ -0,0 +1,3 @@
# Frozen bilingual archive consistency record: Git blob hashes after archive metadata insertion.
2026-07-06-node-engine-floor.md: 4e399b2a71bb4a76a28bc4b643a761ce65c55865
2026-07-06-node-engine-floor.zh.md: abd4913688cc4b3af02fa2f0b06d4b8206652d13
@@ -1,6 +1,7 @@
# Agent Note: Raise the Node LTS engine floor to 22.19
Status: implemented
Archived: 2026-10-15
English | [中文](2026-07-06-node-engine-floor.zh.md)
@@ -1,6 +1,7 @@
# Agent Note: 将 Node LTS 引擎下限提升至 22.19
Status: implemented
Archived: 2026-10-15
[English](2026-07-06-node-engine-floor.md) | 中文
@@ -18,8 +18,8 @@
en: b432f80a424c3e49
zh: faa26925fdd98387
/agent-note-web-capability-seam-stable-tools-over-multiple-providers/provider-availability-and-selection:
en: dff78c46432311a1
zh: 58d5db4c3c85b886
en: 4e4ec068c2a22383
zh: b7030d0f00bfdb74
/agent-note-web-capability-seam-stable-tools-over-multiple-providers/search-request-and-result-schema:
en: ec3703b7e7c6b23d
zh: 325755e6e90092da
@@ -60,6 +60,7 @@ flowchart LR
exa["@deepseek-ai/dsh-web-search-exa"] -->|registerSearchProvider| web["@deepseek-ai/dsh-web / ctx.web"]
perplexity["@deepseek-ai/dsh-web-search-perplexity"] -->|registerSearchProvider| web
deepseek["@deepseek-ai/dsh-web-search-deepseek"] -->|registerSearchProvider| web
deepseekApi["@deepseek-ai/dsh-web-deepseek-api"] -->|registerSearchProvider / registerFetchProvider| web
fetchLocal["@deepseek-ai/dsh-web-fetch-http"] -->|registerFetchProvider| web
toolWeb["@deepseek-ai/dsh-tool-web"] -->|search/fetch| web
toolWeb -->|ctx.tools.register| webSearch["tool: web_search"]
@@ -151,6 +152,8 @@ The "single provider auto-selects" rule is for tests, demos, and simple deployme
Operational overrides feed the same explicit selection path: `DSH_WEB_SEARCH_PROVIDER=perplexity` is equivalent to config `searchProvider: perplexity`, not a hidden priority chain inside `dsh-tool-web`.
Each search or fetch reads the configured id when it runs, so a saved settings change selects another provider for the next operation without remounting `dsh-web`. The Web search settings switch relies on this to move both capabilities to the [DeepSeek Search and Fetch APIs](../feature/2026-10-15-deepseek-search-fetch-api.md) at once.
`ctx.web.search()` and `ctx.web.fetch()` resolve the provider at execution time using the selection rules above. If the selected capability is unavailable, they throw `WebError` with a structured code such as `WEB_PROVIDER_UNAVAILABLE`, `WEB_PROVIDER_CONFIGURED_MISSING`, `WEB_PROVIDER_CONFIGURED_UNAVAILABLE`, or `WEB_PROVIDER_AMBIGUOUS`. If no provider is explicitly configured and no usable provider exists, the execution error is the generic `WEB_PROVIDER_UNAVAILABLE` case; there is deliberately no diagnostic summary of every unavailable provider.
## Search request and result schema
@@ -60,6 +60,7 @@ flowchart LR
exa["@deepseek-ai/dsh-web-search-exa"] -->|registerSearchProvider| web["@deepseek-ai/dsh-web / ctx.web"]
perplexity["@deepseek-ai/dsh-web-search-perplexity"] -->|registerSearchProvider| web
deepseek["@deepseek-ai/dsh-web-search-deepseek"] -->|registerSearchProvider| web
deepseekApi["@deepseek-ai/dsh-web-deepseek-api"] -->|registerSearchProvider / registerFetchProvider| web
fetchLocal["@deepseek-ai/dsh-web-fetch-http"] -->|registerFetchProvider| web
toolWeb["@deepseek-ai/dsh-tool-web"] -->|search/fetch| web
toolWeb -->|ctx.tools.register| webSearch["tool: web_search"]
@@ -151,6 +152,8 @@ interface WebRuntime {
运维覆盖走同一条显式选择路径:`DSH_WEB_SEARCH_PROVIDER=perplexity` 等同于配置 `searchProvider: perplexity`,而非 `dsh-tool-web` 内部的隐式优先级链。
每次搜索或抓取在执行时读取配置的 id,因此保存的设置变更无需重新挂载 `dsh-web` 即对下一次操作选择另一个提供方。网页搜索设置页的开关借此把两种能力同时切换到 [DeepSeek Search 与 Fetch API](../feature/2026-10-15-deepseek-search-fetch-api.zh.md)。
`ctx.web.search()` 和 `ctx.web.fetch()` 在执行时按上述选择规则解析提供方。如果选定的能力不可用,它们抛出带有结构化代码的 `WebError`,如 `WEB_PROVIDER_UNAVAILABLE`、`WEB_PROVIDER_CONFIGURED_MISSING`、`WEB_PROVIDER_CONFIGURED_UNAVAILABLE` 或 `WEB_PROVIDER_AMBIGUOUS`。如果未显式配置提供方且不存在可用提供方,执行错误是通用的 `WEB_PROVIDER_UNAVAILABLE` 情况;刻意不提供对每个不可用提供方的诊断汇总。
## 搜索请求与结果 schema
@@ -21,11 +21,11 @@
en: 985e0eb63f3d1fd6
zh: 337cc44fadc6227e
/agent-note-single-file-executable-sdk-runtime-distribution-single-exe/decision/build-pipeline-and-artifacts:
en: 921085cb9d894688
zh: 12d99fe153d36738
en: 9d885e8d58bc8bbb
zh: 41a9779fbdea8a1a
/agent-note-single-file-executable-sdk-runtime-distribution-single-exe/decision/python-sdk-distribution-two-carriers-exe-for-production-node-for-development:
en: 02e6b36f9f92c83a
zh: edd8b2db7a787483
en: b2a740a1ab7a5002
zh: a48be6ca6fc65912
/agent-note-single-file-executable-sdk-runtime-distribution-single-exe/decision/naming-lineage:
en: 31a5aecb8bf828bb
zh: 2dffb6fd2e0f8194
@@ -48,13 +48,13 @@ The [Office kit](2026-09-14-independent-libreoffice-kit.md) and its installed de
### Build pipeline and artifacts
[`scripts/build-exe-for-python-sdk.ts`](../../../../scripts/build-exe-for-python-sdk.ts): runtime closure verification → `pnpm run build` → (after clearing) `pnpm --filter dsh-python-runtime-closure deploy --legacy --prod --config.allow-unused-patches=true --config.node-linker=hoisted --config.auto-install-peers=false --config.link-workspace-packages=true --config.hoist-workspace-packages=false` **directly into** `python/sdk-runtime/src/deepseek_harness_runtime/runtime/node/`, including [`python/sdk-runtime/runtime-bootstrap.mjs`](../../../../python/sdk-runtime/runtime-bootstrap.mjs) as the carrier-root `runtime-bootstrap.mjs` → restore direct workspace packages omitted by legacy deploy and reject any remaining manifest gap → replace staged dependency symlinks with their target bytes, remove package-manager `.bin` links, and fail if any symlink remains → verify the deployed bootstrap and inject pkg configuration with that bin plus assets covering dynamic profile, bundle, frontend, preset, native-library, and configuration reads → stage the target `node-pty` addon → invoke `pkg --sea` once per target → write `deepseek-harness-sdk-runtime-<platform>-<arch>` under `dist-exe/` and copy it with its lightweight resource manifest and Office skill assets into the runtime directory. The Python runtime owns that bootstrap. It calls the public CLI export for ordinary launches and dispatches a provider-private selection to the same `@deepseek-ai/dsh-subprocess-local/runner` core without changing CLI grammar or adding another executable; the [native-containment reference](../../../../packages/subprocess/subprocess-local/README.md) owns that private path. Linux CI rebuilds `pty.node` inside the matching manylinux 2.28 container because legacy deploy omits that install side effect. Every target copies its native `@vscode/ripgrep` binary beside the executable as the required `-rg` sidecar; pkg runtimes select that sidecar through `process.pkg`, while ordinary Node execution uses `@vscode/ripgrep` directly. macOS uses its target prebuild and also emits the required `-spawn-helper`. The deploy flags preserve the measured runtime layout: `--legacy` is the mandatory path with inject-workspace-packages off; hoisted supplies package directories that staging makes symlink-free; staging removes nested workspace copies only when pnpm records the same dependency identity at the payload root, preserving singleton services; disabling workspace hoisting excludes unrelated workspace source packages; disabling automatic peer installation prevents undeclared peers from expanding the closure; link-workspace-packages selects direct workspace dependencies. [`pnpm-workspace.yaml`](../../../../pnpm-workspace.yaml) overrides the transitive `@deepseek-ai/cosmokit` and `@deepseek-ai/schemastery` semver requests to the pinned vendor sources so legacy deploy never resolves those unpublished names from a registry. Production deployment permits unused workspace patches because development-only tools can be absent from its closure; included-package patch failures still fail deployment. Workspace installs retain strict unused-patch validation.
[`scripts/build-exe-for-python-sdk.ts`](../../../../scripts/build-exe-for-python-sdk.ts): runtime closure verification → `pnpm run build` → (after clearing) `pnpm --filter dsh-python-runtime-closure deploy --legacy --prod --config.allow-unused-patches=true --config.node-linker=hoisted --config.auto-install-peers=false --config.link-workspace-packages=true --config.hoist-workspace-packages=false` **directly into** `python/sdk-runtime/src/deepseek_harness_runtime/runtime/node/`, including [`python/sdk-runtime/runtime-bootstrap.mjs`](../../../../python/sdk-runtime/runtime-bootstrap.mjs) as the carrier-root `runtime-bootstrap.mjs` → restore direct workspace packages omitted by legacy deploy and reject any remaining manifest gap → replace staged dependency symlinks with their target bytes, remove package-manager `.bin` links, and fail if any symlink remains → verify the deployed bootstrap and inject pkg configuration with that bin plus assets covering dynamic profile, bundle, frontend, preset, native-library, and configuration reads → stage the target `node-pty` addon → invoke `pkg --sea --compress Brotli` once per target → write `deepseek-harness-sdk-runtime-<platform>-<arch>` under `dist-exe/` and copy it with its lightweight resource manifest and Office skill assets into the runtime directory. The Python runtime owns that bootstrap. It calls the public CLI export for ordinary launches and dispatches a provider-private selection to the same `@deepseek-ai/dsh-subprocess-local/runner` core without changing CLI grammar or adding another executable; the [native-containment reference](../../../../packages/subprocess/subprocess-local/README.md) owns that private path. Linux CI rebuilds `pty.node` inside the matching manylinux 2.28 container because legacy deploy omits that install side effect. Every target copies its native `@vscode/ripgrep` binary beside the executable as the required `-rg` sidecar; pkg runtimes select that sidecar through `process.pkg`, while ordinary Node execution uses `@vscode/ripgrep` directly. macOS uses its target prebuild and also emits the required `-spawn-helper`. The deploy flags preserve the measured runtime layout: `--legacy` is the mandatory path with inject-workspace-packages off; hoisted supplies package directories that staging makes symlink-free; staging removes nested workspace copies only when pnpm records the same dependency identity at the payload root, preserving singleton services; disabling workspace hoisting excludes unrelated workspace source packages; disabling automatic peer installation prevents undeclared peers from expanding the closure; link-workspace-packages selects direct workspace dependencies. [`pnpm-workspace.yaml`](../../../../pnpm-workspace.yaml) overrides the transitive `@deepseek-ai/cosmokit` and `@deepseek-ai/schemastery` semver requests to the pinned vendor sources so legacy deploy never resolves those unpublished names from a registry. Production deployment permits unused workspace patches because development-only tools can be absent from its closure; included-package patch failures still fail deployment. Workspace installs retain strict unused-patch validation.
CI: [`.github/workflows/build-exe-for-python-sdk.yml`](../../../../.github/workflows/build-exe-for-python-sdk.yml) runs [installed-wheel validation](../testing/2026-08-23-installed-python-wheel-black-box-ci.md) on Linux/Windows x64 for pull requests and Linux ARM64 plus both macOS architectures for master pushes. The [public publication workflow](../../archived/process/2026-08-11-python-publication-workflow.md) calls it for all five targets; `workflow_dispatch` can still select a subset. Native builds run on linux-x64 / linux-arm64 (`ubuntu-24.04-arm`) / macos-arm64 / macos-x64 (`macos-15-intel`) / win-x64 (`windows-2025`), with `~/.pkg-cache` cached where applicable, and pkg handles macOS ad-hoc signing. Each leg installs the release-shaped SDK and runtime wheels into a clean venv outside the checkout, verifies their package and executable origin, then drives the complete keyless scenario set through the public SDK and direct NDJSON JSON-RPC. Trusted pull requests and master pushes additionally run a real DeepSeek two-turn tool smoke on their selected targets; fork and Dependabot heads receive no key. Linux inspects the executable and native addon's GLIBC requirements and runs an additional manylinux 2.28 smoke, while macOS checks the runtime, ripgrep, and PTY helper architectures and verifies that all three deployment targets fit the wheel tag. A full five-target run retains six artifacts, each containing one release file: the platform-independent SDK wheel and five native runtime wheels; a subset dispatch retains the SDK wheel and selected runtime wheels. Bare executables and source bundles remain intermediate test inputs. [`.gitlab-ci.yml`](../../../../.gitlab-ci.yml) accepts `python-v<repository-version>` tag pipelines whose version matches the root `package.json`, builds one SDK wheel and five native runtime wheels, then a single serialized job checks and publishes all six to the project PyPI registry. The [`python/sdk-runtime` README](../../../../python/sdk-runtime/README.md) owns the Windows target and the explicit exclusion of Windows arm64.
### Python SDK distribution: two carriers, exe for production, node for development
The Python SDK lives at [`python/`](../../../../python/README.md): `python/sdk` is the client and `python/sdk-runtime` is the runtime carrier package. The runtime package's data directory holds the build-injected platform executable with its required `-rg` sidecar, lightweight download metadata and Office skills, and optional macOS helper, plus the build-injected `runtime/node/` closure tree for repository development. `resolve_bundled_launch_args()` selects the executable by default; explicit `DSH_RUNTIME_MODE=node` runs `runtime/node/runtime-bootstrap.mjs` on system Node 22.19 or newer. The node carrier never enters wheel distributions, and neither carrier uses a checked-in complete `cordis.yml`.
The Python SDK lives at [`python/`](../../../../python/README.md): `python/sdk` is the client and `python/sdk-runtime` is the runtime carrier package. The runtime package's data directory holds the build-injected platform executable with its required `-rg` sidecar, lightweight download metadata and Office skills, and optional macOS helper, plus the build-injected `runtime/node/` closure tree for repository development. `resolve_bundled_launch_args()` selects the executable by default; explicit `DSH_RUNTIME_MODE=node` runs `runtime/node/runtime-bootstrap.mjs` on system Node 24.9 or newer. The node carrier never enters wheel distributions, and neither carrier uses a checked-in complete `cordis.yml`.
[`scripts/build-python-release.py`](../../../../scripts/build-python-release.py) reads the authoritative `X.Y.Z` or prerelease version from the repository root `package.json`, converts prereleases to their PEP 440 spelling, and stages both packages at that wheel version, with `deepseek-harness-sdk` depending exactly on the matching `deepseek-harness-runtime-bin`. An optional `python-v<repository-version>` release tag is a consistency assertion and is rejected when it differs from the repository version; the source `pyproject.toml` development sentinel never determines a release version. Staging also carries the repository license into both wheels and the third-party notices into the bundled runtime wheel. The SDK is a `py3-none-any` wheel; each wheel-only runtime package contains one exe, lightweight resource metadata and skills, and its architecture-matched ripgrep sidecar, and the macOS wheel also contains its architecture-matched spawn helper. Runtime wheels use `py3-none-manylinux_2_28_x86_64`, `py3-none-manylinux_2_28_aarch64`, `py3-none-macosx_14_0_arm64`, `py3-none-macosx_14_0_x86_64`, or `py3-none-win_amd64`; the Hatch hook rejects sdists, universal tags, mixed-platform payloads, missing or extra sidecars, and unsupported platforms. Both macOS tags deliberately declare a conservative 14.0 installation floor: the packaged Node 24 executables declare macOS 13.5, and the x64 PTY helper declares 10.7, but release validation proves the complete payload only against the 14.0 wheel claim rather than promising each observed component minimum as a supported host. The two macOS wheels remain architecture-specific; no universal2 wheel is published.
@@ -48,13 +48,13 @@ exe 的 VFS 内是**构建产物形态的真实包树**(各包的 `lib/` + 真
### 构建流水线与产物
[`scripts/build-exe-for-python-sdk.ts`](../../../../scripts/build-exe-for-python-sdk.ts):运行时闭包校验 → `pnpm run build` →(清空后)`pnpm --filter dsh-python-runtime-closure deploy --legacy --prod --config.allow-unused-patches=true --config.node-linker=hoisted --config.auto-install-peers=false --config.link-workspace-packages=true --config.hoist-workspace-packages=false` **直接写入** `python/sdk-runtime/src/deepseek_harness_runtime/runtime/node/`,其中包含作为载体根目录 `runtime-bootstrap.mjs` 的 [`python/sdk-runtime/runtime-bootstrap.mjs`](../../../../python/sdk-runtime/runtime-bootstrap.mjs) → 恢复 legacy deploy 遗漏的直接工作区包,并拒绝剩余的 manifest 缺口 → 将暂存依赖中的符号链接替换为目标文件内容,删除包管理器的 `.bin` 链接,并在仍有任何符号链接时失败 → 验证已部署的 bootstrap,并注入以该文件为 bin 的 pkg 配置及覆盖动态读取 profile、bundle、前端、preset、原生库与配置文件的 assets → 暂存目标平台的 `node-pty` addon → 每个构建目标调用一次 `pkg --sea` → 将 `deepseek-harness-sdk-runtime-<platform>-<arch>` 写入 `dist-exe/`,并将它与轻量资源清单及 Office skill 资源一同拷回运行时目录。该 bootstrap 由 Python runtime 拥有;普通启动时它调用公开 CLI export,而提供方私有选择会分派到同一个 `@deepseek-ai/dsh-subprocess-local/runner` 核心,不改变 CLI 语法,也不增加另一个可执行文件;[原生 containment 参考](../../../../packages/subprocess/subprocess-local/README.zh.md)负责这条私有路径。Linux CI 会在匹配的 manylinux 2.28 容器中重新构建 `pty.node`,因为 legacy deploy 会遗漏这一安装副作用。每个目标都会把对应的原生 `@vscode/ripgrep` 二进制复制到可执行文件旁,作为必需的 `-rg` 伴随文件;pkg 运行时通过 `process.pkg` 选择该伴随文件,普通 Node 执行则直接使用 `@vscode/ripgrep`。macOS 使用对应目标的预构建产物,并额外生成所需的 `-spawn-helper`。部署标志都有实测依据:未启用 `inject-workspace-packages` 时必须使用 `--legacy`;`hoisted` 提供包目录,再由暂存步骤消除符号链接;只有 pnpm 在载荷根目录记录了相同依赖标识时,暂存步骤才删除对应的嵌套工作区副本,以保留服务单例;关闭工作区提升可排除无关的工作区源码包;关闭对等依赖自动安装可防止未声明的对等依赖扩大闭包;`link-workspace-packages` 选择直接工作区依赖。[`pnpm-workspace.yaml`](../../../../pnpm-workspace.yaml) 将传递的 `@deepseek-ai/cosmokit` 与 `@deepseek-ai/schemastery` semver 请求覆盖到固定的 vendor 源码,使 legacy deploy 不会从注册表解析这些未发布名称。 生产部署允许未使用的工作区补丁,因为开发工具可能不属于运行时闭包;闭包内包的补丁应用失败仍会中止部署。工作区安装继续严格校验未使用的补丁。
[`scripts/build-exe-for-python-sdk.ts`](../../../../scripts/build-exe-for-python-sdk.ts):运行时闭包校验 → `pnpm run build` →(清空后)`pnpm --filter dsh-python-runtime-closure deploy --legacy --prod --config.allow-unused-patches=true --config.node-linker=hoisted --config.auto-install-peers=false --config.link-workspace-packages=true --config.hoist-workspace-packages=false` **直接写入** `python/sdk-runtime/src/deepseek_harness_runtime/runtime/node/`,其中包含作为载体根目录 `runtime-bootstrap.mjs` 的 [`python/sdk-runtime/runtime-bootstrap.mjs`](../../../../python/sdk-runtime/runtime-bootstrap.mjs) → 恢复 legacy deploy 遗漏的直接工作区包,并拒绝剩余的 manifest 缺口 → 将暂存依赖中的符号链接替换为目标文件内容,删除包管理器的 `.bin` 链接,并在仍有任何符号链接时失败 → 验证已部署的 bootstrap,并注入以该文件为 bin 的 pkg 配置及覆盖动态读取 profile、bundle、前端、preset、原生库与配置文件的 assets → 暂存目标平台的 `node-pty` addon → 每个构建目标调用一次 `pkg --sea --compress Brotli` → 将 `deepseek-harness-sdk-runtime-<platform>-<arch>` 写入 `dist-exe/`,并将它与轻量资源清单及 Office skill 资源一同拷回运行时目录。该 bootstrap 由 Python runtime 拥有;普通启动时它调用公开 CLI export,而提供方私有选择会分派到同一个 `@deepseek-ai/dsh-subprocess-local/runner` 核心,不改变 CLI 语法,也不增加另一个可执行文件;[原生 containment 参考](../../../../packages/subprocess/subprocess-local/README.zh.md)负责这条私有路径。Linux CI 会在匹配的 manylinux 2.28 容器中重新构建 `pty.node`,因为 legacy deploy 会遗漏这一安装副作用。每个目标都会把对应的原生 `@vscode/ripgrep` 二进制复制到可执行文件旁,作为必需的 `-rg` 伴随文件;pkg 运行时通过 `process.pkg` 选择该伴随文件,普通 Node 执行则直接使用 `@vscode/ripgrep`。macOS 使用对应目标的预构建产物,并额外生成所需的 `-spawn-helper`。部署标志都有实测依据:未启用 `inject-workspace-packages` 时必须使用 `--legacy`;`hoisted` 提供包目录,再由暂存步骤消除符号链接;只有 pnpm 在载荷根目录记录了相同依赖标识时,暂存步骤才删除对应的嵌套工作区副本,以保留服务单例;关闭工作区提升可排除无关的工作区源码包;关闭对等依赖自动安装可防止未声明的对等依赖扩大闭包;`link-workspace-packages` 选择直接工作区依赖。[`pnpm-workspace.yaml`](../../../../pnpm-workspace.yaml) 将传递的 `@deepseek-ai/cosmokit` 与 `@deepseek-ai/schemastery` semver 请求覆盖到固定的 vendor 源码,使 legacy deploy 不会从注册表解析这些未发布名称。 生产部署允许未使用的工作区补丁,因为开发工具可能不属于运行时闭包;闭包内包的补丁应用失败仍会中止部署。工作区安装继续严格校验未使用的补丁。
CI 使用 [`.github/workflows/build-exe-for-python-sdk.yml`](../../../../.github/workflows/build-exe-for-python-sdk.yml):[安装后 wheel 包验证](../testing/2026-08-23-installed-python-wheel-black-box-ci.zh.md)在拉取请求上运行 Linux/Windows x64,在 master 推送上运行 Linux ARM64 与两种 macOS 架构。[公开发布工作流](../../archived/process/2026-08-11-python-publication-workflow.md)调用它构建全部五个目标;`workflow_dispatch` 仍可选择部分目标。linux-x64、linux-arm64(`ubuntu-24.04-arm`)、macos-arm64、macos-x64(`macos-15-intel`)与 win-x64(`windows-2025`)分别进行原生构建,并在适用平台缓存 `~/.pkg-cache`;macOS 的 ad-hoc 签名由 pkg 处理。每个平台都把发布形态的 SDK wheel 包与运行时 wheel 包安装到 checkout 外的干净 venv,证明包与可执行文件来源,再通过公开 SDK 与直接 NDJSON JSON-RPC 运行完整 keyless 场景。可信拉取请求与 master 推送还会在各自选定的目标上运行真实 DeepSeek 双轮工具冒烟测试;fork 与 Dependabot head 不会获得密钥。Linux 会检查可执行文件和原生 addon 各自的 GLIBC 依赖,并额外运行 manylinux 2.28 冒烟测试;macOS 则检查 runtime、ripgrep 与 PTY helper 的架构,并验证三个载荷的部署目标都符合 wheel 包标签。完整构建五个目标时保留 6 个产物,每个产物只含一个发布文件:平台无关的 SDK wheel 包与 5 个原生运行时 wheel 包;手动选择部分目标时保留 SDK wheel 与所选运行时 wheel。裸 exe 与源码包只作为测试中间输入。[`.gitlab-ci.yml`](../../../../.gitlab-ci.yml) 只接受版本与根目录 `package.json` 匹配的 `python-v<repository-version>` 标签流水线,构建一个 SDK wheel 包和 5 个原生运行时 wheel 包,再由单个串行任务校验并将这 6 个文件发布到项目的 PyPI 注册表。[`python/sdk-runtime` README](../../../../python/sdk-runtime/README.zh.md)负责 Windows 目标及对 Windows arm64 的明确排除。
### Python SDK 分发:双载体,exe 用于生产,`node` 用于开发
Python SDK 位于 [`python/`](../../../../python/README.zh.md):`python/sdk` 是客户端,`python/sdk-runtime` 是运行时载体包。运行时包的数据目录包含构建注入的平台可执行文件及其必需的 `-rg` 伴随文件、轻量下载元数据与 Office skills 和可选的 macOS helper,以及供仓库开发使用的构建注入 `runtime/node/` 闭包树。`resolve_bundled_launch_args()` 默认选择可执行文件;显式设置 `DSH_RUNTIME_MODE=node` 会在系统 Node 22.19 或更高版本上运行 `runtime/node/runtime-bootstrap.mjs`。node 载体从不进入 wheel 分发,两种载体都不使用检入的完整 `cordis.yml`。
Python SDK 位于 [`python/`](../../../../python/README.zh.md):`python/sdk` 是客户端,`python/sdk-runtime` 是运行时载体包。运行时包的数据目录包含构建注入的平台可执行文件及其必需的 `-rg` 伴随文件、轻量下载元数据与 Office skills 和可选的 macOS helper,以及供仓库开发使用的构建注入 `runtime/node/` 闭包树。`resolve_bundled_launch_args()` 默认选择可执行文件;显式设置 `DSH_RUNTIME_MODE=node` 会在系统 Node 24.9 或更高版本上运行 `runtime/node/runtime-bootstrap.mjs`。node 载体从不进入 wheel 分发,两种载体都不使用检入的完整 `cordis.yml`。
[`scripts/build-python-release.py`](../../../../scripts/build-python-release.py) 从仓库根目录的 `package.json` 读取权威的 `X.Y.Z` 或预发布版本,把预发布版本转换为 PEP 440 写法,并以该 wheel 包版本暂存两个包,让 `deepseek-harness-sdk` 精确依赖匹配版本的 `deepseek-harness-runtime-bin`。可选的 `python-v<repository-version>` 发布标签只是一项一致性断言,与仓库版本不同时会被拒绝;源码 `pyproject.toml` 中的开发占位版本从不决定发布版本。暂存过程还会把仓库许可证放入两个 wheel 包,并把第三方声明放入内置运行时 wheel 包。SDK 是 `py3-none-any` wheel 包;每个只提供 wheel 包的运行时包都包含一个 exe、轻量资源元数据与 skills 及架构匹配的 ripgrep 伴随文件,macOS wheel 包还包含与其架构匹配的 spawn helper。运行时 wheel 包使用 `py3-none-manylinux_2_28_x86_64`、`py3-none-manylinux_2_28_aarch64`、`py3-none-macosx_14_0_arm64`、`py3-none-macosx_14_0_x86_64` 或 `py3-none-win_amd64`;Hatch 钩子拒绝 sdist、通用标签、混合平台载荷、伴随文件缺失或多余,以及不支持的平台。两个 macOS 标签都会特意声明保守的 14.0 安装下限:打包后的 Node 24 可执行文件声明 macOS 13.5,x64 PTY helper 声明 10.7,但发布验证只按照 14.0 wheel 声明证明完整载荷,不会把各组件实测的最低版本承诺为受支持宿主。两个 macOS wheel 包仍按架构分别发布,不发布 universal2 wheel 包。
@@ -6,8 +6,8 @@
en: 55846ac57e07eeda
zh: 3de03730d4b28863
/agent-note-dsh-source-launch-through-the-tsx-esm-hook/problem:
en: 68385f7e47d3ac1f
zh: 285ca290832e2939
en: d13aa56d01c1181d
zh: 837427ed6963c39d
/agent-note-dsh-source-launch-through-the-tsx-esm-hook/decision:
en: 12adf8ed56b4d5c3
zh: fbbb9add935730c0
@@ -8,7 +8,7 @@ English | [中文](2026-07-29-dsh-source-launch-tsx-esm.zh.md)
## Problem
The [archived native source-launch decision](../../archived/architecture/2026-07-28-dsh-native-typescript-source-launch.md) ran `apps/cli/src/bin.ts` under `node --experimental-transform-types` with a resolve-only paths loader, so Node owned TypeScript transformation. Node 26.0.0 removed `--experimental-transform-types` (the process rejects the flag with `bad option`), keeping only strip mode, and strip mode rejects syntax this source graph requires: vendored Cordis parameter properties (`constructor(private ctx: Context)`), the `@Inject` decorators in `vendor/hmr`, and runtime enums/namespaces throughout `vendor/` and `packages/workflow`. The repository's engines range (`^22.19.0 || >=24.0.0`) includes Node 26, so the native launch chain could not start at all there — and no CI job executed the real launch vector, so the incompatibility shipped silently.
The [archived native source-launch decision](../../archived/architecture/2026-07-28-dsh-native-typescript-source-launch.md) ran `apps/cli/src/bin.ts` under `node --experimental-transform-types` with a resolve-only paths loader, so Node owned TypeScript transformation. Node 26.0.0 removed `--experimental-transform-types` (the process rejects the flag with `bad option`), keeping only strip mode, and strip mode rejects syntax this source graph requires: vendored Cordis parameter properties (`constructor(private ctx: Context)`), the `@Inject` decorators in `vendor/hmr`, and runtime enums/namespaces throughout `vendor/` and `packages/workflow`. The supported engines range includes Node 26, so the native launch chain could not start at all there — and no CI job executed the real launch vector, so the incompatibility shipped silently.
Startup latency also mattered: the off-thread `module.register()` hooks worker serialized every resolution across threads (~440ms of `makeSyncRequest` wait during TUI boot), and the full tsx default (`--import tsx`) pays ~0.4s in its CJS hook's resolution amplification.
@@ -8,7 +8,7 @@ Status: implemented
## 问题
[已归档的原生源码启动决策](../../archived/architecture/2026-07-28-dsh-native-typescript-source-launch.md)让 `apps/cli/src/bin.ts` 在 `node --experimental-transform-types` 下运行,配合一个只做解析的 paths loader,由 Node 负责 TypeScript 转换。Node 26.0.0 移除了 `--experimental-transform-types`(进程以 `bad option` 拒绝该 flag),只保留 strip 模式,而 strip 模式无法接受这个源码图必需的语法:vendor Cordis 中的参数属性(`constructor(private ctx: Context)`)、`vendor/hmr` 中的 `@Inject` 装饰器,以及遍布 `vendor/` 与 `packages/workflow` 的运行时 enum/namespace。仓库的 engines 范围(`^22.19.0 || >=24.0.0`)包含 Node 26,因此原生启动链在其上完全无法启动——且没有任何 CI 任务执行过真实启动向量,这一不兼容悄然发布。
[已归档的原生源码启动决策](../../archived/architecture/2026-07-28-dsh-native-typescript-source-launch.md)让 `apps/cli/src/bin.ts` 在 `node --experimental-transform-types` 下运行,配合一个只做解析的 paths loader,由 Node 负责 TypeScript 转换。Node 26.0.0 移除了 `--experimental-transform-types`(进程以 `bad option` 拒绝该 flag),只保留 strip 模式,而 strip 模式无法接受这个源码图必需的语法:vendor Cordis 中的参数属性(`constructor(private ctx: Context)`)、`vendor/hmr` 中的 `@Inject` 装饰器,以及遍布 `vendor/` 与 `packages/workflow` 的运行时 enum/namespace。受支持的 engines 范围包含 Node 26,因此原生启动链在其上完全无法启动——且没有任何 CI 任务执行过真实启动向量,这一不兼容悄然发布。
启动延迟同样是问题:off-thread 的 `module.register()` 钩子工作线程把每次解析都跨线程序列化(TUI 启动期间约 440ms 的 `makeSyncRequest` 等待),而完整的 tsx 默认形态(`--import tsx`)会因其 CJS 钩子放大解析开销而多花约 0.4s。
@@ -33,8 +33,8 @@
en: a244f6a88fdabf3d
zh: e4feedb7297f4865
/agent-note-typert-gateway-targeted-method-calls/three-artifact-kinds-and-two-typescript-programs:
en: c5bcd6eb76bf3d42
zh: 9df2fbb364643186
en: 4479d41466723100
zh: 3a7f398e0d0070a5
/agent-note-typert-gateway-targeted-method-calls/the-remote-package-entry:
en: 75e977fe0fb71d59
zh: 00760c79da173cb7
@@ -207,17 +207,9 @@ Client Program
`remote-client` is the Host Program's second emitter, not a third Program or the Client's local face. It contains no Host Cordis merge, Service class, Context class, or implementation code, and it does not enter the Host-local reflection registry.
The Host lib build performs strict Host analysis and emits both the Host-local and Remote consumer artifacts. The Client lib then consumes the Remote DTS. The complete order is:
Source-only Host analysis writes the Host-local and Remote consumer artifacts before package compilation. [The generation-order decision](../process/2026-10-18-source-first-metadata-builds.md) owns placement and its tradeoffs; [Development](../../../../docs/development.md#typescript-project-layout) owns the command sequence.
```text
Host lib build
→ 生成 typert.host.{js,d.ts}
→ 生成各业务包 lib/typert.remote-client.{js,d.ts,d.ts.map}
→ 完成 Client lib 和 typert.client 产物
→ Vite 构建 Web
```
The top-level `build` runs the step graph in `scripts/build.ts`, which completes the Host bundle and the Remote artifacts before starting Client TypeScript compilation. `build:lib` remains the complete serial lib build for `build:bench`, and its Host half `build:lib:host` serves the vendor release workflows. `typecheck` writes the Remote artifacts through `scripts/generate-typert.ts`, beside Host compilation, before Client TypeScript compilation. A clean build must not depend on stale `.d.ts` files from an earlier build.
The top-level `build`, declaration-only `typecheck`, and serial `build:lib` share metadata preparation before package compilation. `build:lib` remains the complete serial lib build for `build:bench`; its Host half `build:lib:host` serves the vendor release workflows and retains aggregate checking. A clean build must not depend on stale `.d.ts` files from an earlier build.
Compiler-backed repository gates that resolve the consumer surface have the same prerequisite even when their primary inputs are source files. The public `typecheck`, `lint`, and `doc-typecheck` commands run the Host contract pass first. The gate scheduler may use their `*:contracts-ready` variants only after an explicit Typert-contract or complete-build dependency, so parallel lanes neither read missing declarations nor run concurrent generators against the same outputs.
@@ -207,17 +207,9 @@ Client Program
`remote-client` 是 Host Program 的第二个 emitter,不是第三个 Program,也不是 Client 本地 face。它不包含 Host Cordis merge、Service class、Context class 或实现代码,不进入 Host 本地 reflection registry。
Host lib 构建负责完成严格 Host 分析并产出 Host 本地 artifact 与 Remote 消费端 artifact;Client lib 随后消费 Remote DTS。完整顺序为:
仅源码的 Host 分析在包编译之前写出 Host 本地 artifact 与 Remote 消费端 artifact。[生成顺序决策](../process/2026-10-18-source-first-metadata-builds.zh.md)负责生成位置及其取舍;[开发指南](../../../../docs/development.zh.md#typescript-project-layout)负责命令序列。
```text
Host lib build
→ 生成 typert.host.{js,d.ts}
→ 生成各业务包 lib/typert.remote-client.{js,d.ts,d.ts.map}
→ 完成 Client lib 和 typert.client 产物
→ Vite 构建 Web
```
顶层 `build` 运行 `scripts/build.ts` 中的步骤图,先完成 Host bundle 与 Remote artifact,再启动 Client TypeScript 编译。`build:lib` 仍是 `build:bench` 使用的完整串行 lib 构建,其 Host 部分 `build:lib:host` 供 vendor 发布 workflow 使用。`typecheck` 与 Host 编译并行,经 `scripts/generate-typert.ts` 写出 Remote artifact,再启动 Client TypeScript 编译。一次干净构建不能依赖上次残留的 `.d.ts`。
顶层 `build`、仅生成声明的 `typecheck` 与串行 `build:lib` 都在包编译之前准备元数据。`build:lib` 仍是 `build:bench` 使用的完整串行 lib 构建;其 Host 部分 `build:lib:host` 供 vendor 发布 workflow 使用,并保留 aggregate 检查。一次干净构建不能依赖上次残留的 `.d.ts`。
即使主要输入是源文件,需要通过编译器解析消费方 surface 的仓库门禁也有相同的前置条件。公共 `typecheck`、`lint` 和 `doc-typecheck` 命令会先执行 Host 约定 pass。门禁调度器仅可在显式的 Typert 约定依赖或完整构建依赖完成后使用对应的 `*:contracts-ready` 变体,使并行 lane 既不会读取缺失的声明,也不会针对同一输出并发运行多个生成器。
@@ -6,14 +6,14 @@
en: b19652a0e1cc1826
zh: 4d3511e4f0db5b9d
/agent-note-one-outbound-proxy-policy-installed-before-anything-can-request/problem:
en: 70e5f3ed33010c98
zh: 5437d943953f5a40
en: 6379f0a2522f4c46
zh: c45ffeb5089be274
/agent-note-one-outbound-proxy-policy-installed-before-anything-can-request/decision:
en: efe23161a54f70c5
zh: e24ee097918b2724
en: 92526818ff4870c2
zh: 8c54ff3f4941d5ad
/agent-note-one-outbound-proxy-policy-installed-before-anything-can-request/alternatives-considered:
en: df44eb15557b95fe
zh: b757e53da4c0a99f
en: df6c078f239a11f5
zh: 914da26836aa373b
/agent-note-one-outbound-proxy-policy-installed-before-anything-can-request/consequences:
en: 6a4acbab915f1bd1
zh: 2c8ffd62ba74ff96
@@ -10,7 +10,7 @@ Node's built-in `fetch` ignores `HTTP_PROXY` and `HTTPS_PROXY`. Every other tool
The repository had briefly had an answer and lost it without noticing. PR #971 set `NODE_USE_ENV_PROXY=1` in `bin/dsh`; eleven days later the “cleanup: remove managed source installer” change deleted that launcher wholesale, taking the flag with it. What survived was one sentence in `apps/cli/reference/README.md` telling the reader to set a variable that nothing consumed any more.
That sentence could not have worked anyway, for three measured reasons. `NODE_USE_ENV_PROXY` samples the environment at process start, while `loadLayeredEnv()` merges the `.env` layers afterwards, so a proxy declared in `$DSH_HOME/.env` is invisible to it. It reaches Node 24.0+ and, on the 22 line, only 22.21+ — while `engines` admits `^22.19.0`, where the variable does not exist and setting it warns about nothing. And it does not reach `web-fetch-http` at all: that provider passes its own `dispatcher` to `fetch`, and an explicit dispatcher overrides the global one whatever the flag says.
That sentence could not have worked anyway, for three measured reasons. `NODE_USE_ENV_PROXY` samples the environment at process start, while `loadLayeredEnv()` merges the `.env` layers afterwards, so a proxy declared in `$DSH_HOME/.env` is invisible to it. It reaches Node 24.0+ and, on the 22 line, only 22.21+ — the runtime floor at adoption was Node 22.19, where the variable did not exist and setting it warned about nothing. And it does not reach `web-fetch-http` at all: that provider passes its own `dispatcher` to `fetch`, and an explicit dispatcher overrides the global one whatever the flag says.
## Decision
@@ -48,7 +48,7 @@ This accepts a documented seam. Such a context matches bypass entries by Node's
**SDK transports need independent verification.** The OTLP exporter selects `http-exporter-transport` on Node, which posts through `node:http` and bypasses the global fetch dispatcher; a `globalThis.fetch` reference in its browser implementation does not prove Node routing. The [E2B removal](../simplification/2026-09-11-remove-e2b-providers.md) retires a second SDK transport integration without changing this requirement.
**Telemetry stays direct on purpose.** Routing it needs one of two things, and both cost more than the channel is worth. An `http.Agent` reads the environment through `proxyEnv`, which arrived in Node 22.21 and 24.5 — inside the engines range, so 22.19, 22.20, and 24.0–24.4 would stay direct regardless, and the proxy package would have to keep a `createNodeHttpAgent` export for a path that works on some runtimes. Replacing the transport with the SDK's `fetch` delegate covers every runtime, but that delegate has no compression, and the shipped `base` bundle enables gzip: a realistic OTLP batch measures 6.4x smaller with it. An attempt that refused `exporter.compression` instead broke every test that boots the shipped bundle, and one that gzipped at the serializer worked but put transport code in a telemetry plugin to keep it working.
**Telemetry stays direct on purpose.** Routing it needs one of two things, and both cost more than the channel is worth. An `http.Agent` reads the environment through `proxyEnv`, which arrived in Node 22.21 and 24.5 — above the runtime floor at adoption, so Node 22.19, 22.20, and 24.0–24.4 stayed direct regardless, and the proxy package would have to keep a `createNodeHttpAgent` export for a path that worked on some runtimes. The Node 24.9 floor removes this API gap; routing still needs independent transport integration and must preserve compression. Replacing the transport with the SDK's `fetch` delegate covers every runtime, but that delegate has no compression, and the shipped `base` bundle enables gzip: a realistic OTLP batch measures 6.4x smaller with it. An attempt that refused `exporter.compression` instead broke every test that boots the shipped bundle, and one that gzipped at the serializer worked but put transport code in a telemetry plugin to keep it working.
Weighed against that, telemetry is the one outbound channel whose loss costs the user nothing: no tool, no model request, and no session depends on it, and an export that cannot connect is already dropped silently. A user behind a mandatory proxy is left exactly where they were before this change rather than regressed. `egress.spec.ts` now asserts the exclusion — an SDK upgrade that moved the exporter onto `fetch` would start routing telemetry through a proxy silently, and that case is what makes it visible.
@@ -58,7 +58,7 @@ Weighed against that, telemetry is the one outbound channel whose loss costs the
## Alternatives considered
**Document `NODE_USE_ENV_PROXY=1` and stop.** Rejected on three measurements, above: invisible to `$DSH_HOME/.env`, absent on the lowest supported Node, and bypassed by `web-fetch-http` regardless. It is also what the repository already claimed to do.
**Document `NODE_USE_ENV_PROXY=1` and stop.** Rejected at adoption on the three measurements above: invisible to `$DSH_HOME/.env`, absent on the Node 22.19 floor, and bypassed by `web-fetch-http` regardless. The environment-snapshot and explicit-dispatcher limitations remain on the current runtime floor.
**Thread a policy value to every call site.** DeepSeek-Reasonix does this across 98 sites, buying a per-provider opt-out. Rejected: that opt-out exists for a need this harness does not have, and nine sites changed by hand means the tenth is forgotten — Pi's changelog records OAuth and Bedrock as two separate after-the-fact fixes of exactly that kind. The isolation argument for it is real, and is answered instead by handling worker threads explicitly and by proving disposal restores the previous dispatcher.
@@ -10,7 +10,7 @@ Node 内置的 `fetch` 会忽略 `HTTP_PROXY` 与 `HTTPS_PROXY`。开发者运
仓库曾短暂拥有过答案,又在无人察觉时弄丢了。PR #971 在 `bin/dsh` 里设置了 `NODE_USE_ENV_PROXY=1`;十一天后的“cleanup: remove managed source installer”改动整体删除了那个启动器,把该标志一并带走。留下的只有 `apps/cli/reference/README.md` 里的一句话,让读者去设置一个已经无人消费的变量。
即便照做,那句话也不可能生效,原因有三条且都经过实测。`NODE_USE_ENV_PROXY` 在进程启动时对环境取快照,而 `loadLayeredEnv()` 是在之后才合并 `.env` 层,因此写在 `$DSH_HOME/.env` 中的代理对它不可见。它只覆盖 Node 24.0+,在 22 线上只覆盖 22.21+——而 `engines` 允许 `^22.19.0`,那里根本没有这个变量,设置了也不会有任何警告。它也完全触及不到 `web-fetch-http`:该提供方向 `fetch` 传入自己的 `dispatcher`,而显式 dispatcher 无论标志如何都会覆盖全局的那个。
即便照做,那句话也不可能生效,原因有三条且都经过实测。`NODE_USE_ENV_PROXY` 在进程启动时对环境取快照,而 `loadLayeredEnv()` 是在之后才合并 `.env` 层,因此写在 `$DSH_HOME/.env` 中的代理对它不可见。它只覆盖 Node 24.0+,在 22 线上只覆盖 22.21+——采用此决策时的运行时下限为 Node 22.19,那里根本没有这个变量,设置了也不会有任何警告。它也完全触及不到 `web-fetch-http`:该提供方向 `fetch` 传入自己的 `dispatcher`,而显式 dispatcher 无论标志如何都会覆盖全局的那个。
## Decision
@@ -48,7 +48,7 @@ URL 层策略未受影响:仅 `http(s)`、禁止内嵌凭据、长度上限与
**SDK 传输需要独立验证。** OTLP 导出器在 Node 上选择 `http-exporter-transport`,通过 `node:http` 投递并绕过全局 fetch dispatcher;其浏览器实现中的 `globalThis.fetch` 引用不能证明 Node 路由。[E2B 移除决策](../simplification/2026-09-11-remove-e2b-providers.zh.md)撤下另一项 SDK 传输集成,但不改变这一要求。
**遥测的直连是有意为之。** 要让它走代理只有两条路,代价都超过这条通道本身的价值。`http.Agent` 通过 `proxyEnv` 读取环境,而该选项自 Node 22.21 与 24.5 才有——落在 engines 范围之内,因此 22.19、22.20 与 24.0–24.4 无论如何仍是直连,而代理包还得为一条只在部分运行时生效的路径保留 `createNodeHttpAgent` 导出。改用 SDK 的 `fetch` delegate 替换传输可以覆盖所有运行时,但该 delegate 没有压缩能力,而随附的 `base` bundle 启用了 gzip:实测一批真实规模的 OTLP 数据启用后体积只有 1/6.4。曾有一版转而在加载期拒绝 `exporter.compression`,结果凡是启动随附 bundle 的测试全部失败;另一版在 serializer 处 gzip 确实能跑通,但代价是把传输层代码塞进了遥测插件。
**遥测的直连是有意为之。** 要让它走代理只有两条路,代价都超过这条通道本身的价值。`http.Agent` 通过 `proxyEnv` 读取环境,而该选项自 Node 22.21 与 24.5 才有——高于采用此决策时的运行时下限,因此 Node 22.19、22.20 与 24.0–24.4 无论如何仍是直连,而代理包还得为一条只在当时部分运行时生效的路径保留 `createNodeHttpAgent` 导出。Node 24.9 下限消除了这一 API 缺口;代理路由仍需要独立集成传输,并保留压缩。改用 SDK 的 `fetch` delegate 替换传输可以覆盖所有运行时,但该 delegate 没有压缩能力,而随附的 `base` bundle 启用了 gzip:实测一批真实规模的 OTLP 数据启用后体积只有 1/6.4。曾有一版转而在加载期拒绝 `exporter.compression`,结果凡是启动随附 bundle 的测试全部失败;另一版在 serializer 处 gzip 确实能跑通,但代价是把传输层代码塞进了遥测插件。
与之相比,遥测是唯一一条丢失了对用户毫无代价的出网通道:没有任何工具、模型请求或会话依赖它,而连不上的导出本就被静默丢弃。处在强制代理后的用户,只是停留在本次改动之前的状态,而不是被弄坏。`egress.spec.ts` 现在断言这一排除——若某次 SDK 升级把导出器挪到 `fetch` 上,遥测就会开始静默走代理,而该用例正是让这件事暴露出来的东西。
@@ -58,7 +58,7 @@ URL 层策略未受影响:仅 `http(s)`、禁止内嵌凭据、长度上限与
## Alternatives considered
**只写文档,让用户设 `NODE_USE_ENV_PROXY=1`。** 基于上文三条实测被否决:对 `$DSH_HOME/.env` 不可见、在最低支持的 Node 上不存在、且无论如何被 `web-fetch-http` 绕过。而这恰恰是仓库此前声称的做法。
**只写文档,让用户设 `NODE_USE_ENV_PROXY=1`。** 采用此决策时基于上文三条实测被否决:对 `$DSH_HOME/.env` 不可见、在 Node 22.19 下限上不存在、且无论如何被 `web-fetch-http` 绕过。环境快照和显式 dispatcher 的限制在当前运行时下限上仍然存在。
**把策略值传递到每一个调用点。** DeepSeek-Reasonix 在 98 处这样做,换来每提供方的 opt-out。被否决:该能力服务于本 Harness 并不具备的需求,而手工改九处意味着第十处会被遗忘——Pi 的变更日志正记录了 OAuth 与 Bedrock 两次事后补漏。它关于隔离性的论点确实成立,本方案改为显式处理 worker 线程、并以「dispose 后还原前一个 dispatcher」的断言来回应。
@@ -72,8 +72,8 @@
en: 607a1afd89892d15
zh: a0aa85124340458c
/agent-note-remote-duplex-stream-one-kind-of-stream-an-uplink-channel-and-an-invocation-context/decision/security-and-validation:
en: 46a0c0b3ce1e5671
zh: 7743707de42b1d1d
en: 2426665452a6a37b
zh: 282572d66e17a237
/agent-note-remote-duplex-stream-one-kind-of-stream-an-uplink-channel-and-an-invocation-context/decision/in-process-and-worker-carriers:
en: df4ae60d8403cdbb
zh: 70c6024a2a9a1e84
@@ -102,8 +102,8 @@
en: 26dcd8bebdc8d974
zh: e74bcef7c549e70c
/agent-note-remote-duplex-stream-one-kind-of-stream-an-uplink-channel-and-an-invocation-context/placement:
en: ecfaff2b9b2ef021
zh: 85b392d79364e0c3
en: 40c838816d0941ed
zh: 4f5428a040b33b74
/agent-note-remote-duplex-stream-one-kind-of-stream-an-uplink-channel-and-an-invocation-context/key-sequences:
en: 130e4f9ec7296a10
zh: 2a5aa1302a8db61a
@@ -309,6 +309,7 @@ Credit frames wait until a consumer that needs sustained high-volume uplink appe
| Peer | Admitted at upgrade as one `PeerScope`; `invocation.peer` of every stream on that socket is that Peer |
| Item validation | With an uplink codec, every item is decoded strictly, and a failure fails the whole stream with the existing `gateway/input-invalid` (details `{ endpoint, field: 'uplink' }`); without a codec, only JSON-safety validation applies |
| Frame size | A single frame keeps the `maxPayload` of `ws`; the inbox limit bounds the accumulated total |
| Frame compression | Each connection negotiates `permessage-deflate` when the Gateway config `websocketCompression` is `'deflate'` (the default); frames below 1,024 characters are sent uncompressed. See [the compression decision](2026-10-18-websocket-permessage-deflate.md) |
### In-process and worker carriers
@@ -392,6 +393,8 @@ export type RemoteStreamOpener = (
// index.ts Config addition
/** Uplink inbox limit per logical stream, in UTF-8 bytes of frames (default 262144). */
readonly streamInboxBytes?: number
/** Per-message WebSocket compression; 'none' disables the extension (default 'deflate'). */
readonly websocketCompression?: 'none' | 'deflate'
```
`prepareInvocation`:
@@ -453,7 +456,7 @@ cancel : missing → ignore; otherwise control.abort(new Error('Remote stream ca
| `dsh-typert-registry` | Load-time validation that `uplink.codec` is a valid strict codec |
| `dsh-typert-generator` (`model.ts`, `analyzer.ts`, `emitter.ts`) | `remoteResultType` recognizes `RemoteStream<Out, In>` and generates the `uplink` boundary; the descriptor emits `uplink: { codec }`; the generated Client signature returns `RemoteStreamHandle<Out, In>`; the parameter loop accepts only business parameters and a final `signal` |
| `dsh-client-connection` (`peer.ts`, `rpc.ts`, `rpc-host.ts`, `index.ts`) | The `Peer` class; `connection.browserPeer` and `admit`; the `/api` and upgrade routes obtain the Peer through `admit`; the optional `uplink` of `rpc.open` |
| `dsh-api-gateway` Host (`stream-protocol.ts`, `stream-server.ts`, `types.ts`, `index.ts`) | `item` / `end` frames; `UplinkInbox`; `Config.streamInboxBytes`; `GatewayInvocation` and `extend({ invocation })`; `UplinkDecoder`; `handleUpgrade(req, socket, head, peer)` |
| `dsh-api-gateway` Host (`stream-protocol.ts`, `stream-server.ts`, `types.ts`, `index.ts`) | `item` / `end` frames; `UplinkInbox`; `Config.streamInboxBytes` and `Config.websocketCompression`; `GatewayInvocation` and `extend({ invocation })`; `UplinkDecoder`; `handleUpgrade(req, socket, head, peer)` |
| `dsh-api-gateway` Client (`client/index.ts`, `client/stream-client.ts`) | `invokeStream` returns the handle; the uplink pump is driven by the handle's queue |
| `dsh-webworker-runtime`, `dsh-remote-mock` | The worker tunnel's two uplink frames and the `serveStream` assembly; `StreamHandle.uplink`; the direct proxy returns the real handle |
| Documentation | The READMEs of gateway, the api group, connection, protocol, and generator; `docs/api-gateway`; the type-equiv blocks of `docs/subsystems/typert`; the config and Cordis catalogs |
@@ -309,6 +309,7 @@ credit 帧留待需要持续大流量上行的消费者出现时再加;inbox
| Peer | 升级时接纳为一个 `PeerScope`,该 socket 上所有流的 `invocation.peer` 都是它 |
| 项校验 | 有 uplink codec 时逐项严格解码,失败即整条流以既有的 `gateway/input-invalid` 失败(details `{ endpoint, field: 'uplink' }`);无 codec 时只做 JSON 安全校验 |
| 帧大小 | 单帧沿用 `ws` 的 `maxPayload`;inbox 上限限制累计量 |
| 帧压缩 | Gateway 配置 `websocketCompression` 为 `'deflate'`(默认)时,每条连接协商 `permessage-deflate`;少于 1024 个字符的帧不压缩。见[压缩决定](2026-10-18-websocket-permessage-deflate.zh.md) |
### 进程内与 worker 载体
@@ -392,6 +393,8 @@ export type RemoteStreamOpener = (
// index.ts Config addition
/** Uplink inbox limit per logical stream, in UTF-8 bytes of frames (default 262144). */
readonly streamInboxBytes?: number
/** Per-message WebSocket compression; 'none' disables the extension (default 'deflate'). */
readonly websocketCompression?: 'none' | 'deflate'
```
`prepareInvocation`:
@@ -453,7 +456,7 @@ cancel : missing → ignore; otherwise control.abort(new Error('Remote stream ca
| `dsh-typert-registry` | 加载校验 `uplink.codec` 为有效的严格 codec |
| `dsh-typert-generator`(`model.ts`、`analyzer.ts`、`emitter.ts`) | `remoteResultType` 识别 `RemoteStream<Out, In>` 并生成 `uplink` boundary;描述符输出 `uplink: { codec }`;生成的 Client 签名返回 `RemoteStreamHandle<Out, In>`;参数循环只认业务参数与末位 `signal` |
| `dsh-client-connection`(`peer.ts`、`rpc.ts`、`rpc-host.ts`、`index.ts`) | `Peer` 类;`connection.browserPeer` 与 `admit`;`/api` 与升级路由经 `admit` 取 Peer;`rpc.open` 的可选 `uplink` |
| `dsh-api-gateway` Host(`stream-protocol.ts`、`stream-server.ts`、`types.ts`、`index.ts`) | `item` / `end` 帧;`UplinkInbox`;`Config.streamInboxBytes`;`GatewayInvocation` 与 `extend({ invocation })`;`UplinkDecoder`;`handleUpgrade(req, socket, head, peer)` |
| `dsh-api-gateway` Host(`stream-protocol.ts`、`stream-server.ts`、`types.ts`、`index.ts`) | `item` / `end` 帧;`UplinkInbox`;`Config.streamInboxBytes` 与 `Config.websocketCompression`;`GatewayInvocation` 与 `extend({ invocation })`;`UplinkDecoder`;`handleUpgrade(req, socket, head, peer)` |
| `dsh-api-gateway` Client(`client/index.ts`、`client/stream-client.ts`) | `invokeStream` 返回句柄;上行泵由句柄的队列驱动 |
| `dsh-webworker-runtime`、`dsh-remote-mock` | worker 隧道的两种上行帧与 `serveStream` 组装;`StreamHandle.uplink`;直接代理返回真句柄 |
| 文档 | gateway、api 组、connection、protocol、generator 的 README;`docs/api-gateway`;`docs/subsystems/typert` 的 type-equiv 块;config 与 Cordis 目录 |
@@ -33,5 +33,5 @@
en: 939cedceed200cb2
zh: c7bb02d0da400c8d
/agent-note-refresh-runtime-resolution-after-package-operations-and-expire-hmr-package-configuration/consequences:
en: 1b9e31b4e773fac5
zh: c140ce07fc64bd55
en: 5a0120a6a0d48912
zh: 649e7bc118bda1a2
@@ -113,5 +113,5 @@ Tests need no API key and make no model calls.
- With HMR, supported package operations publish the updated private dependencies and remove unused mappings after plugins stop. Without HMR, a pending startup-bundle disablement defers later publications until restart, preserving running plugins' mappings.
- In the supported thread, package.json configuration reads use the changed fields; JSON modules retain their own reload behavior. The separate module, loader-thread, and Worker limitations above still apply.
- Package configuration below expired directories is parsed in JavaScript and must keep the native reader's field and error semantics; tests compare it with the native reader on Node 22, 24, and 26.
- Package configuration below expired directories is parsed in JavaScript and must keep the native reader's field and error semantics; tests compare it with the native reader on Node 24 and 26.
- The implementation depends on several Node internal interfaces, and these tests must run again for Node upgrades.
@@ -113,5 +113,5 @@ binding 的 `getNearestParentPackageJSON` 只被 `package_json_reader` 自己调
- 有 HMR 时,支持的包操作发布更新后的私有依赖,并在插件停止后移除不再需要的映射。没有 HMR 时,待生效的启动 bundle 停用会将后续发布延迟到重启,保留运行中插件的映射。
- 在支持的线程内,package.json 配置读取使用更新后的字段;JSON 模块保留自己的重载行为。上述模块、loader 线程及 Worker 的独立限制仍然适用。
- 包配置读取改由 JS 解析 package.json,需要与原生读取保持字段和错误语义一致;测试在 Node 22、24、26 上与原生读取逐项比较。
- 包配置读取改由 JS 解析 package.json,需要与原生读取保持字段和错误语义一致;测试在 Node 24、26 上与原生读取逐项比较。
- 依赖多个 Node internal 接口,Node 升级时需要重跑这些测试。
@@ -9,8 +9,8 @@
en: 84858bc0e1e268ab
zh: 39ebb2979968d6cc
/agent-note-chat-group-tree-positions-and-exchanges/decision:
en: ca3153ae96994baa
zh: de6c7d925559efbb
en: 043e48fc04003f7d
zh: 15842c3db3fc83f4
/agent-note-chat-group-tree-positions-and-exchanges/why-mark-the-opening-rather-than-the-notice:
en: db8d77cd773ad646
zh: ab2f03b8fc58e952
@@ -14,7 +14,7 @@ A notice is neither body text nor a user message: folding and bottom advancement
- **Group-tree positions.** `ChatConversationViewNode.groupTree` declares a Node's position in the group tree, typed `ChatGroupTree = 'exchange' | 'input' | 'turn' | 'process'` and ordered outermost first; a Node at a position also has the effect of every inner position. `exchange` opens an exchange. `input` makes the Node an input of its Turn: before the Turn's process it is the opening input, after it an interleaved input that keeps the whole Turn open. `turn` keeps the Node outside the Turn's process disclosure, `process` keeps it inside the disclosure but outside step runs, and a Node without a position joins the surrounding step run. `isAtOrOutside(node, position)` is the single reader of that order: process grouping reads `process`, whole-Turn folding reads `turn`, Turn-input detection reads `input`, and exchanges read `exchange`.
- **Exchanges.** A Turn holding an `exchange` Node opens an exchange, which lasts until the next opening Turn. The business message Definition marks only human input claimed in Step 1 (`user` or `steering`) as `exchange`; later human input and trigger notices stand at `input` and never open an exchange.
- **Built-in positions.** `turn-error`, `turn-max-tokens`, `turn-tail`, and the `turn-process` control stand at `turn`; `model-retry` stands at `process`. Two placement rules stay with ui-chat's own `assistant-step` Node: its reasoning row joins the step run while its reply row stands outside it, and the Turn's process specification keeps the finalized answer's reply outside the disclosure because which step answers changes as later steps arrive.
- **Built-in positions.** `turn-error`, `turn-max-tokens`, `turn-tail`, and the `turn-process` control stand at `turn`; `model-retry` stands at `process`. Two placement rules stay with ui-chat's own `assistant-step` Node: its reasoning row joins the step run while its reply row stands outside it, and with the default Replies After Collapse choice every reply row also stays outside the whole-Turn disclosure ([keep every Assistant reply](../feature/2026-10-15-chat-keep-assistant-replies.md)).
- **Mechanism.** `GroupTreeSegments(position)` splits the loaded Turns at the Turns holding a Node at that position. The latest segment starts at the last opening Turn, or at the first loaded Turn when the window holds none. It counts per Turn incrementally without scanning loaded Nodes, and a start move returns only the Turns that entered or left the latest segment. The Chat Builder's `bottomSource` is "the Node is visible and belongs to the latest exchange".
As a result, a notice-woken Turn no longer folds earlier Turns, so no room is reserved; the human's next ordinary message folds the whole exchange through the own-input path.
@@ -14,7 +14,7 @@ Status: implemented
- **分组树位置。** `ChatConversationViewNode.groupTree` 声明节点在分组树中的位置,类型为 `ChatGroupTree = 'exchange' | 'input' | 'turn' | 'process'`,由外到内有序;站在某个位置的节点同时具有所有内层位置的效果。`exchange` 开启一个交互段。`input` 表示节点是所在 Turn 的输入:出现在该轮过程之前即为开场输入,之后出现即为中途输入,使整个 Turn 保持展开。`turn` 让节点留在 Turn 的整轮折叠之外,`process` 让节点留在整轮折叠之内、步骤组之外,没有位置的节点并入相邻的步骤组。`isAtOrOutside(node, position)` 是读取这一顺序的唯一入口:过程分组读 `process`,整轮折叠读 `turn`,轮次输入判断读 `input`,交互段读 `exchange`。
- **交互段。** 含 `exchange` 节点的 Turn 开启一个交互段,交互段持续到下一个开启 Turn。业务层的 message Definition 只给 Step 1 中被认领的真人输入(`user` 或 `steering`)标 `exchange`;之后的真人输入和触发通知位于 `input`,不开启交互段。
- **内置位置。** `turn-error`、`turn-max-tokens`、`turn-tail` 与 `turn-process` 控件位于 `turn`;`model-retry` 位于 `process`。ui-chat 自己的 `assistant-step` 节点保留两条放置规则:推理行并入步骤组、回复行在步骤组之外;该轮的过程规格让最终答案的回复留在整轮折叠之外,因为哪一步是最终答案会随后续步骤到来而变化。
- **内置位置。** `turn-error`、`turn-max-tokens`、`turn-tail` 与 `turn-process` 控件位于 `turn`;`model-retry` 位于 `process`。ui-chat 自己的 `assistant-step` 节点保留两条放置规则:推理行并入步骤组、回复行在步骤组之外;在默认的「收起后显示的回复」选项下,每个回复行也留在整轮折叠之外([整轮折叠保留全部 Assistant 回复](../feature/2026-10-15-chat-keep-assistant-replies.zh.md))。
- **机制。** `GroupTreeSegments(position)` 按含该位置节点的 Turn 切分已加载的 Turn,最新一段从最后一个开启 Turn 开始,窗口中没有开启 Turn 时从第一个已加载 Turn 开始。它按 Turn 增量计数,不扫描已加载节点;起点移动时只返回进入或离开最新一段的 Turn。Chat Builder 的 `bottomSource` 为"节点可见且属于最新交互段"。
结果是:通知唤醒的 Turn 不再折叠前面的 Turn,没有折叠就没有补白;真人下一次普通输入时,整个交互段经由自己输入的路径一起折叠。
@@ -0,0 +1,19 @@
# Bilingual-pair consistency record for 2026-10-16-separate-instruction-chain-budgets.md (docs/i18n/README.md): per heading
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
# After editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-10-16-separate-instruction-chain-budgets.md
/agent-note-separate-byte-budgets-for-the-user-global-and-project-instruction-chains:
en: 8ad05eb6bd0511b9
zh: bd0658fa58edc70d
/agent-note-separate-byte-budgets-for-the-user-global-and-project-instruction-chains/problem:
en: 77f3bb05053ce59f
zh: 4a1759900b71cc44
/agent-note-separate-byte-budgets-for-the-user-global-and-project-instruction-chains/decision:
en: 5dd811039ee2fef3
zh: c9c1dc27890ff204
/agent-note-separate-byte-budgets-for-the-user-global-and-project-instruction-chains/alternatives-considered:
en: 157837c9fbc377cb
zh: 32fb2d8427529772
/agent-note-separate-byte-budgets-for-the-user-global-and-project-instruction-chains/consequences:
en: 186092d64376de0c
zh: 92fe504ff77cba71
@@ -0,0 +1,34 @@
# Agent Note: Separate byte budgets for the user-global and project instruction chains
Status: implemented
English | [中文](2026-10-16-separate-instruction-chain-budgets.zh.md)
## Problem
`dsh-agent-instructions` rendered the user-global and project files as one chain under one `maxBytes`, the rule of the [original workspace-context decision](../../archived/feature/2026-06-24-workspace-context.md). Rendering omits the broadest files first, and user-global files are the broadest, so a large project chain removed them: under the shipped 65,536-byte budget, 20 KiB user-global, 30 KiB project root, and 20 KiB nested files rendered without the user-global file.
## Decision
The renderer splits the loaded files into the user-global and project chains, keeping their order, and renders each under the same `maxBytes`: omit the broadest files, truncate the last file, then fall back to the compact intro, to the notice with an empty heading, and to the notice alone. The project chain keeps the result it has when rendered alone; user-global files are omitted or truncated only beyond their own budget, except in the fallback below.
When all files fit `maxBytes` together, or only one chain has files, the output is unchanged byte for byte. Otherwise one `<system-reminder>` frame of at most twice `maxBytes` holds the merged notice when a file was omitted or truncated, one intro when a chain kept its frame, and the rendered sections in loaded order, with user-global files first in a baseline. If a chain renders without a frame and the merged frame would exceed that bound, all files render as one chain under one `maxBytes`, which can omit a user-global file that fit its own budget; this needs a budget of a few hundred bytes or a notice that nearly fills or exceeds `maxBytes`. Reconciliation batches apply the same split.
The `maxBytes` key and the baseline identity are unchanged, so a resumed session keeps its visible baseline and receives a user-global file that baseline omitted as one additional-instructions transition.
## Alternatives considered
**One budget that gives user-global files priority.** A large user-global file then truncates or omits the project root file, and bytes near the limit decide which chain loses content.
**One budget with a user-global share.** The project chain still shrinks, and no measurement supports a share size.
**A configuration field for the second chain.** No deployment needs different sizes, and the field would join the baseline identity, so resumed sessions would resend their baselines.
**The Codex layout: an unbounded user-global file and a root-first project chain.** Output has no bound, and the project chain keeps its broadest files, not its most specific ones.
## Consequences
- A baseline or batch holding both chains can reach twice `maxBytes`; deployments that sized `maxBytes` as a total budget halve it, per the [upgrade guide](../../../../docs/upgrade-guide/v0.2.1-rc.1/agent-instructions-chain-budgets/guide.md).
- User-global instructions survive a large project chain.
- The `agent-instructions-budget` snapshot shows a truncated user-global file beside the whole project file; `agent-instructions-budget-chains` shows two chains that fit only separately.
- The package README, `Config.maxBytes` JSDoc, and configuration catalog state the per-chain bound.
@@ -0,0 +1,34 @@
# Agent Note: 用户全局指令链与项目指令链分别使用字节预算
Status: implemented
[English](2026-10-16-separate-instruction-chain-budgets.md) | 中文
## 问题
`dsh-agent-instructions` 曾把用户全局文件与项目文件作为一条指令链,在一个 `maxBytes` 内渲染,这是[最初的工作区上下文决策](../../archived/feature/2026-06-24-workspace-context.md)定下的规则。渲染会最先省略最宽泛的文件,而用户全局文件最宽泛,因此较大的项目指令链会移除它们:在默认的 65,536 字节预算下,20 KiB 的用户全局文件、30 KiB 的项目根文件与 20 KiB 的嵌套文件一起渲染时,用户全局文件被省略。
## 决策
渲染器把已加载的文件分成用户全局指令链与项目指令链,各自保持原有顺序,并分别在同一个 `maxBytes` 内渲染:先省略最宽泛的文件,再截断最后一个文件,然后依次退回到精简引言、带空标题的通知和仅含通知的文本。项目指令链的结果与它单独渲染时相同;用户全局文件只有超出自身预算时才会被省略或截断,下文的回退情形除外。
所有文件合起来能放进 `maxBytes`,或只有一条链有文件时,输出逐字节不变。其他情况下,一个至多为 `maxBytes` 两倍的 `<system-reminder>` 框架包含合并后的通知(仅当有文件被省略或截断时)、一段引言(仅当某条链保留了框架时),以及按加载顺序排列的渲染段落,因此基线中用户全局文件在前。若某条链在没有框架的情况下渲染,且合并后的框架会超过该上限,则所有文件作为一条链在一个 `maxBytes` 内渲染,这可能省略一个本可放进自身预算的用户全局文件;这只会在预算只有几百字节,或通知接近或超过 `maxBytes` 时发生。对账批次采用同样的拆分。
`maxBytes` 配置键与基线标识都不变,因此恢复的会话保留其可见基线;该基线曾省略的用户全局文件会以一次附加指令变更的形式加入。
## 备选方案
**一个预算,并优先保留用户全局文件。** 较大的用户全局文件会截断或省略项目根文件,而且临界值附近的字节差异就决定哪条链丢失内容。
**一个预算,并为用户全局文件划出份额。** 项目指令链仍会缩小,而且没有测量数据支撑份额的大小。
**为第二条链增加配置字段。** 没有部署需要为两条链设定不同大小,而且该字段会进入基线标识,使恢复的会话重新发送基线。
**采用 Codex 的布局:不设上限的用户全局文件,加上从根目录开始保留的项目指令链。** 输出没有上限,项目指令链会保留最宽泛的文件,而不是最具体的文件。
## 影响
- 同时包含两条链的基线或批次最多可达 `maxBytes` 的两倍;把 `maxBytes` 当作总预算设定的部署应将其减半,见[升级指南](../../../../docs/upgrade-guide/v0.2.1-rc.1/agent-instructions-chain-budgets/guide.zh.md)。
- 用户全局指令不会因项目指令链较大而被移除。
- `agent-instructions-budget` snapshot 显示被截断的用户全局文件与完整的项目文件;`agent-instructions-budget-chains` 显示两条只能各自放下的链。
- 包 README、`Config.maxBytes` 的 JSDoc 与配置目录说明了按链计算的上限。
@@ -0,0 +1,22 @@
# Bilingual-pair consistency record for 2026-10-16-steering-batch-start-in-definition-state.md (docs/i18n/README.md): per heading
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
# After editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-10-16-steering-batch-start-in-definition-state.md
/agent-note-precompute-the-steering-batch-start-in-conversation-node-definition-state:
en: 6f27c4f6133966ba
zh: 36b1e3bf1b809f09
/agent-note-precompute-the-steering-batch-start-in-conversation-node-definition-state/problem:
en: 6901df0549909883
zh: 3e7f9698a26fe568
/agent-note-precompute-the-steering-batch-start-in-conversation-node-definition-state/decision:
en: 581f8768c22fda63
zh: 1db7d743dbe65e96
/agent-note-precompute-the-steering-batch-start-in-conversation-node-definition-state/alternatives-considered:
en: 479dc0bcce2c7674
zh: a9fa5ed6eaa40919
/agent-note-precompute-the-steering-batch-start-in-conversation-node-definition-state/consequences:
en: 66b21a77e1fa35f1
zh: 8a3e13e94a27a3bf
/agent-note-precompute-the-steering-batch-start-in-conversation-node-definition-state/testing:
en: 4b0da9e7eeb3906a
zh: 35ea5569f39939d6
@@ -0,0 +1,37 @@
# Agent Note: Precompute the steering batch start in Conversation Node Definition state
Status: implemented
English | [中文](2026-10-16-steering-batch-start-in-definition-state.zh.md)
## Problem
Chat keeps a 44px slot, the height of the running status row, above the first steering row of each claim that lands after the running Turn's process output, so admitting steering swaps the status and the bubble without moving the bubble ([conversation-nodes README](../../../../packages/client/ui-chat/src/client/conversation-nodes/README.md)). Deciding that a steering row starts such a batch needs two facts its own `user/message` event does not carry: whether it is the first human message of its `next-step` claim, and whether the same Turn already has durable visible output before it, which places the row after the Turn's process control. The answer must hold for live appends, replay, and loading earlier history, and must not scan the event window or the Chat Nodes.
## Decision
The batch start is a fact in Conversation Node State, computed by Definitions through the existing [`reader.previous`](2026-08-09-client-conversation-node-assembly.md) lookup:
- [`inbox.ts`](../../../../packages/client/ui-chat/src/client/conversation-nodes/inbox.ts) records `firstClaimedHuman`, the first human message removed by each `next-step` claim, in the Client Inbox State.
- [`turn-process.ts`](../../../../packages/client/ui-chat/src/client/conversation-nodes/turn-process.ts) registers `processEvidenceDefinition` (kind `process-evidence`, `publication: () => 'none'`): one Context per Step with durable visible output, id `turn:step`, matched from `assistant/message`, `tool/call`, `tool/result`, and `llm/retry` through the existing `processEvidence` test. Streaming chunks never reach it. Its State holds the Turn number; the Context's matches accumulate that Step's evidence events, and nothing reads them.
- [`message.ts`](../../../../packages/client/ui-chat/src/client/conversation-nodes/message.ts) calls `reader.previous('process-evidence')` in `start`, only for the claim's first human message. When the nearest earlier record belongs to the same Turn, the steering State carries `batchStart: true`. Later appends have larger seqs and cannot change the answer; loading earlier history replays the dependency through the assembler's tracking of `reader.previous`, as Message already does for Inbox.
- The steering renderer reads only `data.batchStart` and writes `data-chat-steering-batch="start"` on its bubble root; CSS gives the containing row the slot.
The lookup costs O(log N) per claim-leading human message, where N is the number of Steps with output in the loaded window. The change adds no framework API and no persisted or wire data.
## Alternatives considered
**Compute it in the renderer from props.** The renderer would need neighbouring rows and the Turn's process state, which puts a steering branch into the business-free node list and Seat, and the row would not re-render when only a neighbour changes.
**Compute it in the Chat snapshot builder.** A projector over the ordered entries would put a steering-specific rule and an extra pass into the generic build layer.
**Add tracked Turn-data reads to the framework (`reader.turnData`).** It changes the `ConversationNodeBuildReader` contract and the assembler's dependency tracking for one consumer, while `reader.previous` already gives the needed ordering, dependency, and replay.
## Consequences
The decision lives in node data, follows replay and earlier-history loading through existing dependency tracking, and leaves the renderer, Seat, node list, and ui-conversation framework unchanged. It costs one more Definition and one Context per Step with output; each State is one number, and each Context holds references to its Step's evidence events. The slot follows the claim's first human message rather than its first rendered row, so a runtime-context notice logged before that message in the same claim sits above the slot; this follows from the README's rule that the claim's first human message keeps the slot.
## Testing
- [conversation-node-definitions.client.spec.ts](../../../../packages/client/ui-chat/tests/conversation-node-definitions.client.spec.ts): `records durable process evidence once per Step and ignores live chunks and invisible replies` pins evidence matching; `marks each claim lead and starts a batch only after the Turn control`, run for replay, live appends, and prepends, pins batch starts for the first and a later claim, no batch start for the second row of a claim, an idle claim that opens a Turn, or a claim after a Step without output.
- [chat-view.client.spec.tsx](../../../../packages/client/ui-chat/tests/chat-view.client.spec.tsx): `marks the steering row whose data carries a batch start, and no other input` pins the rendered attribute; [turn-tail-spacing.client.spec.ts](../../../../packages/client/ui-chat/tests/turn-tail-spacing.client.spec.ts) pins the 44px slot rule.
@@ -0,0 +1,37 @@
# Agent Note: 在 Conversation Node Definition 的 State 中预计算插话批首
Status: implemented
[English](2026-10-16-steering-batch-start-in-definition-state.md) | 中文
## 问题
Chat 在每次 claim 的第一条插话行上方保留 44px 的位置(等于运行状态行的高度),条件是这批插话落在运行中 Turn 的过程输出之后;这样插话被领取时,运行状态与气泡交换位置,气泡不动([conversation-nodes README](../../../../packages/client/ui-chat/src/client/conversation-nodes/README.zh.md))。判断一条插话是否是批首,需要两个它自己的 `user/message` 事件里没有的事实:它是否是所在 `next-step` claim 的第一条人类消息;同一 Turn 在它之前是否已有持久可见的输出,也就是这一行是否排在 Turn 过程控件之后。结论在实时追加、回放和加载更早历史时都必须成立,并且不能扫描事件窗口或 Chat Node。
## 决策
批首是 Conversation Node State 中的事实,由 Definition 通过现有的 [`reader.previous`](2026-08-09-client-conversation-node-assembly.zh.md) 查询计算:
- [`inbox.ts`](../../../../packages/client/ui-chat/src/client/conversation-nodes/inbox.ts) 在 Client Inbox State 中记录 `firstClaimedHuman`,即每次 `next-step` claim 取走的第一条人类消息。
- [`turn-process.ts`](../../../../packages/client/ui-chat/src/client/conversation-nodes/turn-process.ts) 注册 `processEvidenceDefinition`(kind `process-evidence`,`publication: () => 'none'`):每个有持久可见输出的 Step 一个 Context,id 为 `turn:step`,通过现有的 `processEvidence` 判断从 `assistant/message`、`tool/call`、`tool/result`、`llm/retry` 匹配。流式 chunk 不会进入它。它的 State 只保存 Turn 编号;Context 的 matches 累积该 Step 的证据事件,没有任何代码读取它们。
- [`message.ts`](../../../../packages/client/ui-chat/src/client/conversation-nodes/message.ts) 只对 claim 的第一条人类消息,在 `start` 中调用 `reader.previous('process-evidence')`。最近的一条更早记录属于同一 Turn 时,插话 State 带上 `batchStart: true`。之后追加的事件 seq 都更大,不会改变结论;加载更早历史时,组装器对 `reader.previous` 的依赖追踪会重放这个依赖,与 Message 读取 Inbox 的方式相同。
- 插话渲染器只读取 `data.batchStart`,在气泡根元素上写 `data-chat-steering-batch="start"`;CSS 给所在行保留这个位置。
每条领头的人类消息做一次 O(log N) 查询,N 为已加载窗口中有输出的 Step 数。这一改动不新增框架 API,也不涉及持久化或传输数据。
## 考虑过的替代方案
**在渲染器里根据 props 计算。** 渲染器需要相邻行和 Turn 的过程状态,这会把插话分支带进本应不含业务的节点列表和 Seat;而且只有相邻行变化时,这一行不会重新渲染。
**在 Chat 快照构建层计算。** 对有序条目做一次投影,会把插话专用的规则和额外一轮遍历放进通用构建层。
**给框架增加带追踪的 Turn 数据读取(`reader.turnData`)。** 为了一个使用方修改 `ConversationNodeBuildReader` 的约定和组装器的依赖追踪;而 `reader.previous` 已经提供所需的顺序、依赖与重放。
## 后果
结论保存在节点数据中,借助现有依赖追踪在回放和加载更早历史时保持正确;渲染器、Seat、节点列表和 ui-conversation 框架都不变。代价是多一个 Definition,每个有输出的 Step 多一个 Context:每个 State 只有一个数字,每个 Context 持有该 Step 证据事件的引用。保留的位置跟随 claim 的第一条人类消息,而不是该批第一条渲染出来的行;因此同一次 claim 中记录在它之前的 runtime-context 通知会排在这个位置上方;这由 README 中“claim 的第一条人类消息保留该位置”的规则推出。
## 测试
- [conversation-node-definitions.client.spec.ts](../../../../packages/client/ui-chat/tests/conversation-node-definitions.client.spec.ts):`records durable process evidence once per Step and ignores live chunks and invisible replies` 固定证据匹配;`marks each claim lead and starts a batch only after the Turn control` 分别以回放、实时追加和加载更早历史三种方式运行,固定第一次和之后一次 claim 的批首,以及同一 claim 的第二行、开启 Turn 的空闲 claim、无输出 Step 之后的 claim 都不是批首。
- [chat-view.client.spec.tsx](../../../../packages/client/ui-chat/tests/chat-view.client.spec.tsx):`marks the steering row whose data carries a batch start, and no other input` 固定渲染出的属性;[turn-tail-spacing.client.spec.ts](../../../../packages/client/ui-chat/tests/turn-tail-spacing.client.spec.ts) 固定 44px 位置的规则。
@@ -0,0 +1,22 @@
# Bilingual-pair consistency record for 2026-10-18-session-list-summary-projections.md (docs/i18n/README.md): per heading
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
# After editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-10-18-session-list-summary-projections.md
/agent-note-serve-session-list-rows-from-a-summary-key-set:
en: 983b028b1d2aeb7c
zh: 2a2d3ced786ee686
/agent-note-serve-session-list-rows-from-a-summary-key-set/problem:
en: 9f84c9bd85b6aace
zh: 8ebb10962c219624
/agent-note-serve-session-list-rows-from-a-summary-key-set/decision:
en: 6e8f43d7a53c35fd
zh: a6e2bccc20474515
/agent-note-serve-session-list-rows-from-a-summary-key-set/alternatives-considered:
en: ce8320f1032018ed
zh: 4c5fad5e605bf524
/agent-note-serve-session-list-rows-from-a-summary-key-set/consequences:
en: d3465b78160d8c1b
zh: dcbad6768b74b2bf
/agent-note-serve-session-list-rows-from-a-summary-key-set/testing:
en: 6b34571bdadb0a9d
zh: 41ebfc1b5325b8d6
@@ -0,0 +1,52 @@
# Agent Note: Serve Session-list rows from a summary key set
Status: implemented
English | [中文](2026-10-18-session-list-summary-projections.zh.md)
## Problem
A Session-list row carries every registered client view. Four of those views are never rendered by a row: the whole-log turn outline, the whole-log conversation figures, the current todo list, and the host-wide image limits. The outline grows with the Session's log, so a list payload scales with corpus size and log length even though no row shows it. Omitting values from the list needs a rule that keeps the wire and client types honest: a summary consumer must not obtain an omitted key, a value that is omitted must still reach an opened Session, and the owning packages must not drift from whatever declaration drives the omission.
## Decision
The Session summary is every registered client view except the keys whose `SessionProjectionMap` entry carries the summary-exclusion mark.
- [`packages/session/session-projection/src/types.ts`](../../../../packages/session/session-projection/src/types.ts) declares the type-only mark `SessionProjectionSummaryExcluded<T>` over a unique symbol that is never exported and never present at runtime. `SessionProjectionViewValue<T>` strips it distributively, so a nullable entry keeps its `null`; `SessionProjectionSummaryExcludedKey` tests the non-nullish part; `SessionProjectionSummaryValues` is every view value minus those keys.
- Each owning package wraps only its own map entry: `turnOutline` (session-turn-outline), `sessionStats` (session-stats), `todos` (tool-todo), and `imageLimits` (session-controller's boot-constant attachment limits, which every Session shares). The todo entry is `SessionProjectionSummaryExcluded<TodoItem[]> | null`, because an intersection with `null` would absorb the null member.
- Registration carries the same decision as a plain boolean: `wire.summary` says whether a Session summary includes the unit, and omitting it means `true`. A key whose map entry carries the mark declares `false` so the runtime key set and the client view type agree. The mark itself has no runtime representation — `wire.view` and `viewSchema` are typed with `SessionProjectionViewValue`, which strips it, so the erase in `register` only widens the state type.
- [`packages/session/session-projection/src/index.ts`](../../../../packages/session/session-projection/src/index.ts) exposes `summaryKeys(): ReadonlySet<key>`, derived from the live registrations and rebuilt only when a new key registers or a key's last registrant disposes. `cachedSnapshot`, `viewCheckpoint`, and `snapshot` accept `ProjectionKeySelection` (`readonly Key[] | ReadonlySet<Key>`) and use a passed set as-is.
- [`packages/session/session-projection-cache/src/index.ts`](../../../../packages/session/session-projection-cache/src/index.ts) forwards that selection unchanged, and [`packages/api/session-controller/src/list.ts`](../../../../packages/api/session-controller/src/list.ts) passes `summaryKeys()` to the live and cold cached faces. `cachedPredecessorTitle` keeps its title-only read.
- Client types only: `SessionSummary.projectionValues` narrows to `Partial<SessionProjectionSummaryValues>`, while `SessionProjectionSnapshot.values` keeps every view value. No wire field, function, or UI name changes, and an opened Session still receives every key through `session/follow`, the control baseline, and `session.projections`.
Estimated effect: with logs whose outlines dominate the projection payload, the four keys are estimated at roughly two thirds of a Session-list response for a few hundred Sessions — the outline alone near 57%, the figures and todo list near 5% together, and the shared image limits near 5% — and at roughly 85% of the same response after transport compression, where the outline's unique text barely compresses while the remaining keys do.
## Alternatives considered
**A summary-specific read face per source** (`viewSummaryRecord` for checkpoint rows and `cachedSummarySnapshot` for live cells). Rejected: the summary key set would then be defined and carried by two read faces plus their call sites, and a caller could still pass explicit keys to the complete faces. The derived set plus the existing key-taking faces keeps one definition, one code path, and no per-read key copy.
**A hardcoded excluded-key list in the session controller.** Rejected: the list would drift from the owning packages, and nothing would fail when a package renamed a key or added a large value.
**Filtering keys after the complete block is built.** Rejected: the per-row state parse and view work would still happen, and the wire type would keep claiming the excluded keys are present.
**Making every projection opt in to summaries.** Rejected: it inverts today's default, requires touching every owning package, and silently drops a new large key from summaries whenever a package forgets to opt in.
**A lazy per-value read for large projections.** Rejected: rows do not need the values at all, and deferring one value per row adds a request shape and loading states the list never had.
## Consequences
Bought: a list response no longer scales with outline length or log count, a summary consumer cannot obtain an excluded key through the derived set (the complete faces still serve an explicitly named key), and reading an excluded key from a list row's type is a type error. `wire.summary` is a plain boolean, so keeping a marked key at `false` is the registering package's responsibility rather than a compile error.
A list row carries the summary projection, never the full projection: it promises the summary key set and nothing more. A connected Session's client store may hold more values from its control/follow baseline, but those are outside the list contract and must not be read from a row; the opened Session's own baselines keep every value.
No upgrade guide records this change. The externally perceptible part is the wire: `session/list` rows no longer carry the four keys, and a client type that reads `projectionValues.turnOutline` no longer compiles. A consumer migrating reads the opened Session instead — its `session/follow` opening snapshot, the `session/control` baseline, or a `session.projections` read — which keep every projected value; the package README and the subsystem page state the rule.
Paid: `ProjectionKeySelection` is a new public signature type and `ReadonlySet` joins the catalog's foundation types; `summaryKeys()` has two invalidation points (a new key, the last disposer) that a future registration path must not miss; the summary key set, like the unit table, is process-wide, so a key excluded by one preset is excluded for every Session; and a nullable value cannot be marked directly — its `null` must stay outside the wrapper.
## Testing
- [`packages/api/session-controller/tests/summary-list.host.spec.ts`](../../../../packages/api/session-controller/tests/summary-list.host.spec.ts): a list row's keys equal the registry's complete snapshot minus the marked keys, a `session/follow` opening snapshot still carries them with equal values, and two consecutive pulls do not re-add them.
- [`packages/session/session-projection/tests/summary.spec.ts`](../../../../packages/session/session-projection/tests/summary.spec.ts): the derived set, its stable identity, its rebuild after registration and disposal, summary-shaped reads through the key-taking faces, an empty summary set, and the nullable mark.
- [`packages/session/session-projection-cache/tests/cache.spec.ts`](../../../../packages/session/session-projection-cache/tests/cache.spec.ts): a summary read from stored rows omits the marked key and takes its watermark from the served rows only.
- [`packages/api/session-controller/tests/summary-types.client.spec.ts`](../../../../packages/api/session-controller/tests/summary-types.client.spec.ts): `@ts-expect-error` reads of `turnOutline`, `sessionStats`, and `todos` on `SessionSummary.projectionValues`, with the full snapshot still exposing them and `todos` still nullable.
- [`packages/api/session-controller/tests/manager.client.spec.ts`](../../../../packages/api/session-controller/tests/manager.client.spec.ts): a reconnect refresh replaces the row block, so a stale excluded key does not survive.
@@ -0,0 +1,52 @@
# Agent Note: Serve Session-list rows from a summary key set
Status: implemented
[English](2026-10-18-session-list-summary-projections.md) | 中文
## Problem
Session 列表行携带每个已注册的客户端视图。其中四个视图从不被行渲染:整份日志的 turn outline、整份日志的会话统计、当前 todo 清单,以及宿主级的图片上限。outline 随 Session 日志增长,因此列表响应会随语料规模与日志长度增长,而没有一行展示它。从列表省略取值需要一条规则同时守住 wire 与客户端类型:摘要消费方不能取得被省略的键;被省略的值仍要送达打开的 Session;owning package 不得与驱动省略的声明漂移。
## Decision
Session 摘要就是每个已注册客户端视图,减去 `SessionProjectionMap` 条目带摘要排除标记的键。
- [`packages/session/session-projection/src/types.ts`](../../../../packages/session/session-projection/src/types.ts) 声明类型层标记 `SessionProjectionSummaryExcluded<T>`,它基于一个从不导出、运行时从不存在的 unique symbol。`SessionProjectionViewValue<T>` 以分发方式去掉标记,可空条目因此保留 `null`;`SessionProjectionSummaryExcludedKey` 取非空部分判断;`SessionProjectionSummaryValues` 是全部视图值减去这些键。
- 每个 owning package 只包装自己的 map 条目:`turnOutline`(session-turn-outline)、`sessionStats`(session-stats)、`todos`(tool-todo),以及 `imageLimits`(session-controller 的启动常量附件上限,每个 Session 共享)。todo 条目写作 `SessionProjectionSummaryExcluded<TodoItem[]> | null`,因为与 `null` 求交会吞掉 null 成员。
- 注册用普通布尔值承载同一决定:`wire.summary` 表示 Session 摘要是否包含该单元,省略即 `true`。map 条目带标记的 key 声明 `false`,让运行时键集合与客户端视图类型一致。标记本身没有运行时表示 —— `wire.view` 与 `viewSchema` 的签名已用 `SessionProjectionViewValue` 去标记,因此 `register` 里的擦除只是放宽状态类型。
- [`packages/session/session-projection/src/index.ts`](../../../../packages/session/session-projection/src/index.ts) 提供 `summaryKeys(): ReadonlySet<key>`,从实时注册派生,只有新键注册或某键最后一个注册者注销时才重建。`cachedSnapshot`、`viewCheckpoint`、`snapshot` 接受 `ProjectionKeySelection`(`readonly Key[] | ReadonlySet<Key>`),传入 Set 时直接使用。
- [`packages/session/session-projection-cache/src/index.ts`](../../../../packages/session/session-projection-cache/src/index.ts) 原样转发该选择,[`packages/api/session-controller/src/list.ts`](../../../../packages/api/session-controller/src/list.ts) 把 `summaryKeys()` 传给 live 与 cold 两个缓存读取面。`cachedPredecessorTitle` 保持只读 title。
- 客户端只有类型变化:`SessionSummary.projectionValues` 收窄为 `Partial<SessionProjectionSummaryValues>`,而 `SessionProjectionSnapshot.values` 保留全部视图值。wire 字段、函数名与 UI 名称均不变;打开的 Session 仍通过 `session/follow`、控制基线与 `session.projections` 收到每个键。
估算效果:当 outline 在投影负载中占主导时,四个键估计占几百个 Session 时列表响应的约三分之二 —— outline 单项约 57%,统计与 todo 合计约 5%,共享的图片上限约 5% —— 占传输压缩后同一响应的约 85%,因为 outline 的唯一文本几乎无法压缩,而其余键可以。
## Alternatives considered
**为每个来源各做一个摘要专用读取面**(checkpoint 行的 `viewSummaryRecord` 与实时 cell 的 `cachedSummarySnapshot`)。否决:摘要键集合会由两个读取面及其调用点各定义一次,而调用方仍可向完整读取面传显式键。派生集合加既有带键读取面只保留一处定义、一条代码路径,且每次读取无需复制键。
**在 session controller 里硬编码被排除键清单。** 否决:清单会与 owning package 漂移,且包改名或新增一个大值时没有任何检查会失败。
**先构建完整块再按键过滤。** 否决:逐行的 state 解析与 view 工作仍会发生,而且 wire 类型会继续声称被排除键存在。
**让每个投影主动选择加入摘要。** 否决:这反转了当前默认,需要改动每个 owning package,并且某包忘记加入时新的大键会被静默地从摘要中漏掉。
**为大投影做按值惰性读取。** 否决:行根本不需要这些值,逐行延迟一个值会引入列表从未有过的请求形状与加载态。
## Consequences
换来的:列表响应不再随 outline 长度或日志数量增长;摘要消费方无法通过派生集合取得被排除的键(完整读取面仍按显式键提供);从列表行的类型读取被排除键是类型错误。`wire.summary` 是普通布尔值,因此让带标记的键保持 `false` 是注册方自己的责任,而不是编译错误。
列表行携带的是摘要投影,绝不是全量投影:它只承诺摘要键集合,不承诺更多。已连接 Session 的客户端 store 可能因 control/follow 基线持有更多值,但那不属于列表契约,不得从行上读取;打开的 Session 自身的基线保留全部取值。
本次没有升级指南记录该变更。外部可感知的部分在 wire:`session/list` 行不再携带四个键,读取 `projectionValues.turnOutline` 的客户端类型不再编译。迁移方式是改读打开的 Session —— 其 `session/follow` 初始快照、`session/control` 基线或 `session.projections` 读取 —— 它们保留全部投影值;包 README 与子系统页写明了该规则。
付出的:`ProjectionKeySelection` 是新的公开签名类型,`ReadonlySet` 也需加入 catalog 的 foundation 类型;`summaryKeys()` 有两个失效点(新键、最后一个 disposer),未来的注册路径不得漏掉;摘要键集合与单元表一样是进程级的,某个 preset 排除的键对所有 Session 都排除;可空值不能直接标记 —— 它的 `null` 必须留在包装之外。
## Testing
- [`packages/api/session-controller/tests/summary-list.host.spec.ts`](../../../../packages/api/session-controller/tests/summary-list.host.spec.ts):列表行的键等于注册表完整快照减去被标记的键;`session/follow` 初始快照仍携带它们且值相等;连续两次拉取不会重新加入它们。
- [`packages/session/session-projection/tests/summary.spec.ts`](../../../../packages/session/session-projection/tests/summary.spec.ts):派生集合、其稳定实例、注册与注销后的重建、经带键读取面的摘要式读取、空摘要集合,以及可空标记。
- [`packages/session/session-projection-cache/tests/cache.spec.ts`](../../../../packages/session/session-projection-cache/tests/cache.spec.ts):从持久行读取摘要时省略被标记键,且水位只取自被服务的行。
- [`packages/api/session-controller/tests/summary-types.client.spec.ts`](../../../../packages/api/session-controller/tests/summary-types.client.spec.ts):在 `SessionSummary.projectionValues` 上读取 `turnOutline`、`sessionStats`、`todos` 触发 `@ts-expect-error`,而完整快照仍暴露它们且 `todos` 仍可空。
- [`packages/api/session-controller/tests/manager.client.spec.ts`](../../../../packages/api/session-controller/tests/manager.client.spec.ts):断线重连刷新会替换行块,残留的被排除键不会存活。
@@ -0,0 +1,25 @@
# Bilingual-pair consistency record for 2026-10-18-websocket-permessage-deflate.md (docs/i18n/README.md): per heading
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
# After editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-10-18-websocket-permessage-deflate.md
/agent-note-compress-the-remote-stream-websocket-with-permessage-deflate:
en: 0bc2e23413bf72c1
zh: 5bb252767d0a9fac
/agent-note-compress-the-remote-stream-websocket-with-permessage-deflate/problem:
en: 4b2b62b888cf4735
zh: 9d5c1ffce7c253b5
/agent-note-compress-the-remote-stream-websocket-with-permessage-deflate/decision:
en: f0d660d42cd9cfbf
zh: a1fff91323e6b213
/agent-note-compress-the-remote-stream-websocket-with-permessage-deflate/decision/negotiation-and-degradation:
en: efd339349731b4eb
zh: d4fafd884b3cfc75
/agent-note-compress-the-remote-stream-websocket-with-permessage-deflate/decision/failure-and-capacity:
en: 5f666978c80f0098
zh: 69afb0f62622c5cb
/agent-note-compress-the-remote-stream-websocket-with-permessage-deflate/alternatives-considered:
en: 1df91aee779c7c51
zh: b8db98186b21143d
/agent-note-compress-the-remote-stream-websocket-with-permessage-deflate/consequences:
en: b6e6e5fb4dc0f38d
zh: fbaa18b8c4b53940
@@ -0,0 +1,72 @@
# Agent Note: Compress the Remote stream WebSocket with permessage-deflate
Status: implemented
English | [中文](2026-10-18-websocket-permessage-deflate.zh.md)
## Problem
The Web GUI carries every live Remote stream over one physical WebSocket, `/api/remote.mux`: model token deltas, session events, tool output, terminal output, and the `$events` fan-out. The mux itself is the implemented [Remote duplex stream](2026-09-19-remote-duplex-stream.md) design; this note adds one transport option to it. The Host's HTTP surface already exposes `compression: 'none' | 'gzip'`; before this option the WebSocket had no equivalent, so the link stayed uncompressed.
Over a tunnel, a reverse proxy, or an admitted public URL, the large JSON frames — session replay, long tool output, terminal scrollback — spend bandwidth the protocol can already reclaim. Browsers offer `permessage-deflate` in the upgrade handshake without asking, so the missing half was the server's.
## Decision
The Gateway Config adds `websocketCompression: 'none' | 'deflate'`, defaulting to `'deflate'`, and passes the resolved boolean into the mux. The mux constructs `WebSocketServer` with these transport options and decides compression per outbound frame:
| Option | Value | Why |
|---|---|---|
| Frame compression threshold | `1024` characters of the JSON text, decided in the mux send path | `ws` ignores its `threshold` option while context takeover is enabled ([upstream 41ae5631](https://github.com/websockets/ws/commit/41ae5631)), so the mux passes `compress` per frame; frames below 1,024 characters (most token-level `item` frames) stay uncompressed and out of the zlib pool |
| `perMessageDeflate.zlibDeflateOptions.level` | `1` | Interactive frames value latency over ratio; the HTTP gzip default in this repository is also `1` |
| Context takeover | `ws` default (enabled both ways) | Best ratio for repetitive JSON; no per-message dictionary rebuild |
The mux constructor takes one options object carrying `heartbeatIntervalMs`, `streamInboxBytes`, and `compression`, because the Gateway resolves the config vocabulary and `stream-server.ts` does not learn `'none' | 'deflate'`.
Nothing on the client changes: the browser API offers the extension already, the Node carrier uses `ws` with `perMessageDeflate: true` by default, and a client that offers nothing gets an uncompressed upgrade.
### Negotiation and degradation
| Peer | Result |
|---|---|
| Browser (`client/connection` opens a native `WebSocket`) | Negotiates the extension; both directions compressed |
| Node `ws` carrier (tests, authenticated Desktop callers) | Negotiates the extension; both directions compressed |
| Client that does not offer the extension | Upgrade succeeds without `Sec-WebSocket-Extensions`; frames identical to an uncompressed connection |
| New Host with an old Client, or the reverse | No protocol change; the extension is per connection and optional |
| `websocketCompression: 'none'` | No extension is negotiated; frames are uncompressed |
### Failure and capacity
| Condition | Behavior |
|---|---|
| Buffered uplink exceeds `streamInboxBytes` | Still fails the logical stream with `gateway/uplink-overflow`; the budget counts decompressed frame bytes, so its meaning does not change |
| Ping/Pong heartbeat | Ping cadence is unaffected: control frames are never compressed, and the activity rule's Pong and inbound-frame arms are unchanged |
| Downlink write liveness | A sub-threshold write stays synchronous, so it counts as peer activity only when the local send buffer was already full; a frame whose JSON text reaches 1,024 characters compresses asynchronously, so its completed write counts as buffered activity |
| zlib saturation | Compression runs on the libuv thread pool behind `ws`'s global limiter; the event loop is not blocked, but thread-pool capacity is shared with file I/O |
## Alternatives considered
**Compress frames with gzip.** Rejected because WebSocket has no gzip frame format; RFC 7692 defines `permessage-deflate` as the only registered per-message compression extension.
**Let the client choose.** Rejected because the browser `WebSocket` API exposes no extension parameters and no way to disable negotiation; the decision can only live on the server.
**Mirror the HTTP surface with `compressionLevel` and `compressionThresholdBytes`.** Deferred, not rejected: no measurement justifies exposing tuning knobs yet, and the repository forbids hiding a real tunable in a constant while also forbidding speculative configuration. Add the fields when a deployment needs a different trade-off.
**Enable compression unconditionally.** Rejected because the choice varies by deployment: a loopback deployment pays CPU for no bandwidth gain and must be able to turn it off, which is what the Config field is for.
**Compress only the downlink.** Rejected because the extension is negotiated per connection and applies to both directions; a one-way accept is not expressible, and the uplink frames are small anyway.
## Consequences
Compression reclaims the bandwidth of large JSON frames over a tunnel, a reverse proxy, or an admitted public URL. Frames below 1,024 characters stay out of the zlib pool, and context takeover keeps the dictionary across messages, so repetitive JSON compresses well without per-message rebuilds.
**Thread-pool contention.** zlib shares the libuv pool with file I/O. The 1,024-character threshold keeps small streaming frames out of it; a saturated pool delays compressed frames rather than blocking the loop.
**Per-connection memory.** Each negotiated connection holds a deflate and an inflate context for its lifetime. One socket per browser tab keeps this proportional to open tabs; an implementation that expects many idle tabs should measure before enabling by default.
**No benefit over loopback.** The common local deployment gains nothing but CPU, which is why the field exists; the default is chosen for remote and tunneled use.
**Unmeasured default.** The `'deflate'` default rests on the HTTP gzip precedent and on remote deployments, not on an on/off byte-and-latency comparison. Record that comparison when one is run and revisit the default if local overhead proves visible.
**Weakened write liveness.** A compressed downlink write completes when `ws` has deflated and written it, not when the TCP peer acknowledges data, so continuous compressed downlink traffic counts as buffered activity and delays missed-heartbeat termination. The heartbeat rule and its code are unchanged.
The Gateway unit tests pin default negotiation, no-extension behavior, a client that offers nothing, the per-frame threshold, and the compressed-write liveness consequence; the config default and the generated config catalog are updated in the same change.
@@ -0,0 +1,72 @@
# Agent Note: 用 permessage-deflate 压缩 Remote 流 WebSocket
Status: implemented
[English](2026-10-18-websocket-permessage-deflate.md) | 中文
## 问题
Web GUI 的所有实时 Remote 流共用一条物理 WebSocket `/api/remote.mux`:模型 token 增量、session 事件、工具输出、终端输出,以及 `$events` 广播。这条 mux 本身是已实现的 [Remote 双工流](2026-09-19-remote-duplex-stream.zh.md)设计,本 Note 只给它加一个传输选项。Host 的 HTTP 面已经暴露 `compression: 'none' | 'gzip'`;在此选项之前 WebSocket 没有对应能力,链路因此从不压缩。
走隧道、反向代理或被接纳的公网地址时,大 JSON 帧——会话回放、长工具输出、终端滚动历史——占掉的带宽协议本来就能省下来。浏览器在升级握手时本来就会主动 offer `permessage-deflate`,缺的只是服务端这一半。
## 决定
Gateway Config 增加一个字段 `websocketCompression: 'none' | 'deflate'`,默认 `'deflate'`,把解析后的布尔值交给 mux。mux 用以下传输选项构造 `WebSocketServer`,并在发送时逐帧决定是否压缩:
| 选项 | 取值 | 理由 |
|---|---|---|
| 帧压缩阈值 | `1024` 个 JSON 文本字符,由 mux 在发送路径判定 | `ws` 在开启上下文接管时会忽略自己的 `threshold` 选项([上游 41ae5631](https://github.com/websockets/ws/commit/41ae5631)),因此 mux 逐帧传入 `compress`;少于 1024 个字符的帧(多数 token 级 `item` 帧)不压缩,也不进入 zlib 线程池 |
| `perMessageDeflate.zlibDeflateOptions.level` | `1` | 交互帧优先延迟而非压缩率;本仓库 HTTP gzip 默认也是 `1` |
| 上下文接管 | `ws` 默认(双向开启) | 重复 JSON 压缩率最好,不必逐帧重建字典 |
mux 构造函数接收一个 options 对象,携带 `heartbeatIntervalMs`、`streamInboxBytes` 与 `compression`,因为配置词汇由 Gateway 解析,`stream-server.ts` 不认识 `'none' | 'deflate'`。
客户端零改动:浏览器 API 本来就会 offer 该扩展,Node 载体用的 `ws` 默认 `perMessageDeflate: true`,完全不 offer 的客户端拿到的就是不压缩的升级响应。
### 协商与降级
| 对端 | 结果 |
|---|---|
| 浏览器(`client/connection` 打开原生 `WebSocket`) | 协商成功,双向压缩 |
| Node `ws` 载体(测试、认证的 Desktop 调用方) | 协商成功,双向压缩 |
| 不 offer 该扩展的客户端 | 升级成功但不带 `Sec-WebSocket-Extensions`,帧与不压缩连接一致 |
| 新 Host 配旧 Client,或反过来 | 没有协议变更;扩展按连接、可选协商 |
| `websocketCompression: 'none'` | 不协商扩展;帧不压缩 |
### 失败与容量
| 情形 | 行为 |
|---|---|
| 缓冲的上行超过 `streamInboxBytes` | 仍以 `gateway/uplink-overflow` 失败该逻辑流;预算统计的是解压后的帧字节,语义不变 |
| Ping/Pong 心跳 | Ping 周期不受影响:控制帧从不压缩,活动判据中 Pong 与收到帧这两支不变 |
| 下行写入活性 | 阈值以下的写入保持同步,只有本机发送缓冲区已满时才计为对端活动;JSON 文本达到 1024 个字符的帧异步压缩,其写入完成会计为缓冲活动 |
| zlib 打满 | 压缩跑在 libuv 线程池、受 `ws` 全局并发限制约束;事件循环不被阻塞,但线程池容量与文件 IO 共享 |
## 备选方案
**用 gzip 压缩帧。** 否决:WebSocket 没有 gzip 帧格式,RFC 7692 只注册了 `permessage-deflate` 这一种逐消息压缩扩展。
**让客户端决定。** 否决:浏览器 `WebSocket` API 不暴露扩展参数,也没有关闭协商的手段,这个决定只能落在服务端。
**照 HTTP 面配齐 `compressionLevel` 与 `compressionThresholdBytes`。** 暂缓而非否决:目前没有测量依据支持暴露调优旋钮,而仓库既禁止把真正的可调项藏进常量,也禁止投机式配置。等某个部署需要不同取舍时再加字段。
**无条件开启压缩。** 否决:这个选择随部署变化——本地 loopback 部署付出 CPU 却换不到带宽,必须能关掉,这正是 Config 字段的用途。
**只压下行。** 否决:该扩展按连接协商并同时作用于两个方向,单向接受不可表达,何况上行帧本来就小。
## 后果
压缩为走隧道、反向代理或被接纳公网地址的大 JSON 帧省下带宽。少于 1024 个字符的帧不进入 zlib 线程池,上下文接管跨消息保留字典,因此重复 JSON 的压缩率好且不必逐帧重建。
**线程池争用。** zlib 与文件 IO 共用 libuv 线程池。1024 字符阈值把小的流式帧挡在外面;线程池打满时被延迟的是压缩帧,事件循环仍不被阻塞。
**每连接内存。** 每条协商成功的连接在整个生命周期内持有 deflate 与 inflate 一对上下文。每个浏览器标签页一条 socket 时,内存与打开的标签页成正比;如果实现预期大量空闲标签页,应先测量再默认开启。
**loopback 无收益。** 最常见的本地部署只多付 CPU,所以才有这个字段;默认值是为远端与隧道场景选的。
**默认值未经测量。** `'deflate'` 这个默认值依据的是 HTTP gzip 的先例与远端场景,不是开/关两态的字节与延迟对比。做过对比后要记录结论,本地开销若明显则重新考虑默认值。
**写入活性被削弱。** 压缩后的下行写入在 `ws` 完成 deflate 并写出时即完成,而不是等到 TCP 对端确认数据,因此持续压缩下行会把写入计为缓冲活动并推迟心跳超时终止。心跳规则及其代码未改动。
Gateway 单测钉住默认协商、不协商、不支持扩展的客户端、逐帧阈值与压缩写入的活性后果;配置默认值与生成的 config catalog 在同一次改动中更新。
@@ -9,14 +9,14 @@
en: 3238dfb7db7dfdd8
zh: ec3bdd9a9da0a321
/agent-note-user-authorized-subagent-model-routes/decision:
en: 0166e452a774ade5
zh: 747061b9424b3610
en: 9eeb42f10be1bd28
zh: 24e6cca2db77444a
/agent-note-user-authorized-subagent-model-routes/alternatives-considered:
en: e41c17226190ea8b
zh: d3b1395fafe75f01
/agent-note-user-authorized-subagent-model-routes/consequences:
en: f1927d31ffc1dee9
zh: b0fed04b80043c71
en: 26244ba796f6ed70
zh: 47e9744719b05f0b
/agent-note-user-authorized-subagent-model-routes/related-decisions:
en: 595a9485d91592c6
zh: 7b32611ea380498c
en: 0eead87ec1172d81
zh: 4e776a583f5a00a5
@@ -12,11 +12,11 @@ Registering an LLM adapter makes its routes reachable, but does not authorize an
The Host-owned `subagent-model-selection` settings section stores an explicit `enabled` switch and `allowedModels`, an array of exact `{ provider, model }` routes. Enabling requires at least one route; disabling may retain the selected routes for later reuse. The Plugins settings card reads the live adapter directory through `session/modelCatalog`, lets the user stage the switch and routes, and saves both fields in one revision-fenced settings mutation. It stores no adapter-owned display names, descriptions, or reasoning-effort metadata. A stored or staged route absent from the current directory remains visible as unavailable and removable; a provider-local catalog failure does not block other providers or erase saved authorization or an unsaved selection. A connection reset discards the draft because namespace revisions are comparable only within one Host process.
A newly composed top-level Session snapshots the route list in `subagent/model-selection-policy` when the setting is enabled, before its model-selectable definitions can reach a request. Event presence means selection was enabled; the event does not store the global switch. Child Sessions inherit that exact list from their live parent, and resumed Sessions use the recorded event instead of current settings. Settings changes therefore affect only subsequently composed top-level Sessions, while a legacy Session without the event remains disabled, including an explicitly empty restored Session.
The Host settings service snapshots the route list for each fresh top-level Session in `subagent/model-selection-policy` when the setting is enabled, before its first model request. Event presence means selection was enabled; the event does not store the global switch. Child Sessions inherit that exact list from their live parent, and resumed Sessions use the recorded event instead of current settings. Settings changes therefore affect only subsequently created top-level Sessions. Without the event, including an explicitly empty restored Session, explicit selection remains restricted to the current parent route.
The fixed `list_subagent_models` schema does not enumerate the policy. At call time, provider and model listings are the intersection of the Session route list and the adapter's live advertised directory. An exact provider/model lookup first requires authorization, then resolves the adapter-owned model metadata and all advertised reasoning efforts. The delegation executor independently rejects any explicit provider, model, or effort selection whose effective provider/model route is outside the Session list before `resolveCallConfig()` validates adapter availability and effort support. A call that supplies no selection field retains configured or inherited routing because the model made no route choice.
The fixed `list_subagent_models` schema does not enumerate the policy. At call time, it lists authorized registered providers and intersects their advertised model catalogs with the recorded route list. Exact lookup requires authorization before resolving model metadata and all supported reasoning efforts, including for ids absent from the catalog. Without a recorded policy, discovery resolves only the parent Agent’s current effective route, even if the catalog omits it. The delegation executor independently checks any explicit provider, model, or effort selection against the same authorization before adapter preflight. A call with no selection fields retains configured or inherited routing.
Model selection has no unrestricted static mode. The default-off Host setting is the only authority, and an enabled Session always carries an exact allowlist. The primary spawn tool reads that setting; the shipped fork tool still exposes no route selection so inherited conversation prefixes remain eligible for provider-side KV Cache reuse.
Model selection has no unrestricted static mode. The default-off Host setting authorizes alternative child routes; without it, the parent route and its supported efforts remain selectable. Each delegation tool’s `modelSelection` option controls only its route fields. One delegation instance per scope enables `modelDiscovery` to own discovery; the setting, discovery, and delegation read the same package-local authorization projection. Web and Desktop presets mount discovery by default, enable spawn route fields, and leave fork route fields disabled to preserve inherited-prefix reuse; custom fork compositions may enable them under the same Session authorization. Tool installation does not sample or expand authorization.
## Alternatives considered
@@ -33,11 +33,11 @@ Model selection has no unrestricted static mode. The default-off Host setting is
## Consequences
- New adapter registrations and newly advertised models do not expand user authorization.
- Adapter removals or catalog failures can reduce what discovery currently lists without deleting the saved route decision; an exact authorized route remains usable when its adapter accepts it even if the advisory catalog omits it.
- The allowlist itself consumes no parent-request tokens. Only a `list_subagent_models` result enters the transcript.
- Adapter removal or catalog lookup failure can make provider discovery unavailable without deleting the saved route decision; an allowed model remains usable when its adapter accepts it even if the advisory catalog omits it.
- The allowlist itself consumes no parent-request tokens. Only a `list_subagent_models` result enters the transcript. The `modelSelection` option adds three route fields; `modelDiscovery` adds a fixed discovery schema. Both options default off; shipped headless and SDK profiles carry neither.
- The policy event is log-only and is appended while an Agent is composed, before either SDK begins its run subscription. Shipped SDK profiles do not enable this Web-owned preference, so the event changes neither SDK's expected notifications or persisted-session output; package restore tests own its durable projection instead of fabricating an SDK composition solely to emit it.
- Unit coverage pins settings validation, malformed durable values, Session sampling and inheritance, discovery intersection, executor denial, live UI catalog invalidation, staged-route retention, connection-generation invalidation, staged whole-array writes, stale-revision rejection, and retry after scoped installation failure. The assembled Web scenario pins the real settings document and Plugins card flow.
- Unit coverage pins settings validation, malformed durable values, Session sampling and inheritance, shared definitions, parent-route defaults, discovery restrictions, executor denial, and the settings UI’s staged writes and revision checks. The assembled Web scenario pins the real settings document and Plugins card flow.
## Related decisions
The route arguments, adapter preflight, discovery tool, and fork cache restriction remain owned by [model-selected subagent routes](../../../../packages/subagent/tool-subagent/README.md).
The route arguments, adapter preflight, discovery tool, and fork restriction remain owned by [model-selected subagent routes](../../../../packages/subagent/tool-subagent/README.md).
@@ -12,11 +12,11 @@ Status: implemented
Host 自有的 `subagent-model-selection` 设置 section 保存显式 `enabled` 开关与 `allowedModels`,后者是由精确 `{ provider, model }` 路由组成的数组。启用时必须至少有一条路由;关闭时可以保留已选路由,供以后重新启用。Plugins 设置卡通过 `session/modelCatalog` 读取实时适配器目录,让用户暂存开关与路由,再在一次带 revision 限制的设置 mutation 中保存两个字段。它不保存适配器自有的显示名称、描述或推理强度元数据。当前目录中缺失的已存或暂存路由仍显示为不可用并允许移除;某个提供方的目录失败不会阻塞其他提供方,也不会清除已存授权或未保存选择。连接重置会丢弃草稿,因为 namespace revision 只能在同一个 Host 进程内比较。
设置启用时,新组合的顶层 Session 会在模型可选定义进入请求之前,把路由列表快照记录为 `subagent/model-selection-policy`。事件存在就表示模型选择已启用;事件不保存全局开关。子 Session 从在线父级继承同一份精确列表,恢复的 Session 使用已记录事件而不是当前设置。因此,设置修改只影响之后组合的顶层 Session,而没有该事件的旧 Session 仍保持禁用,包括显式为空的恢复 Session。
设置启用时,Host 设置服务为每个新顶层 Session 在首个模型请求前将路由列表记录为 `subagent/model-selection-policy`。事件存在表示选择已启用;事件不保存全局开关。子 Session 从在线父级继承同一精确列表,恢复的 Session 使用已记录事件而非当前设置。因此设置变更只影响之后创建的顶层 Session。没有该事件时,包括显式为空的恢复 Session,显式选择仍仅限当前父级路由。
固定的 `list_subagent_models` schema 不会枚举该策略。调用时,提供方和模型列表是 Session 路由列表与适配器实时公布目录的交集。精确 provider/model 查询先要求授权,再解析适配器自有的模型元数据和全部已公布推理强度。委派执行器还会独立拒绝任何生效 provider/model 路由不在 Session 列表内的显式提供方、模型或强度选择,然后才由 `resolveCallConfig()` 校验适配器可用性与强度支持。完全没有选择字段的调用保留配置或继承路由,因为模型没有作出路由选择。
固定的 `list_subagent_models` schema 不枚举策略。调用时,它列出已授权且已注册的提供方,并取其公布模型目录与已记录路由列表的交集。精确查询先要求授权,再解析模型元数据及全部支持的推理强度,也允许目录未列出的 ID。没有已记录策略时,发现只解析父 Agent 当前生效的路由,即使目录未列出它。委派执行器会在适配器预检前独立校验显式提供方、模型或强度选择是否符合相同授权。没有选择字段的调用保留配置或继承的路由。
模型选择不再有无限制的静态模式。默认关闭的 Host 设置是唯一授权来源,启用的 Session 始终携带精确允许列表。主 spawn 工具读取该设置;随附 fork 工具仍不公开路由选择,使继承的对话前缀继续符合提供方侧 KV Cache 复用条件。
模型选择没有无限制的静态模式。默认关闭的 Host 设置授权其他子级路由;没有该授权时,仍可选择父级路由及其支持的强度。各委派工具的 `modelSelection` 选项只控制自身路由字段。每个作用域的一个委派实例启用 `modelDiscovery` 并负责发现;设置、发现和委派读取包内同一个授权投影。Web 与 Desktop preset 默认挂载发现工具,启用 spawn 路由字段,并为 fork 保持关闭,以保留对继承前缀的复用;自定义 fork 组合可以在相同 Session 授权下启用这些字段。工具安装不采样或扩大授权。
## Alternatives considered
@@ -33,11 +33,11 @@ Host 自有的 `subagent-model-selection` 设置 section 保存显式 `enabled`
## Consequences
- 新适配器注册和新公布模型不会扩大用户授权。
- 适配器移除或目录失败可以减少发现当前列出的内容,但不会删除已存路由决定;即使建议性目录省略某条精确已授权路由,只要适配器接受它,该路由仍然可用。
- 允许列表本身不消耗父级请求 token。只有 `list_subagent_models` 结果进入 transcript。
- 适配器移除或目录查询失败可能使提供方发现不可用,但不会删除已存路由决定;只要适配器接受某个获准模型,即使建议性目录省略它,该模型仍可使用。
- 允许列表本身不消耗父级请求 token。只有 `list_subagent_models` 结果进入 transcript。`modelSelection` 选项增加三个路由字段;`modelDiscovery` 增加固定发现 schema。两个选项都默认关闭;随附 headless 与 SDK profile 两者都不携带。
- 策略事件仅存在于日志,并在 Agent 组合期间、两套 SDK 开始订阅运行前追加。随附 SDK profile 不启用这项 Web 自有偏好,因此该事件不会改变任一 SDK 的预期通知或持久 Session 输出;其持久投影由包级恢复测试负责,不会为了发出该事件而虚构 SDK 组合。
- 单元覆盖固定设置校验、异常持久值、Session 取样与继承、发现交集、执行器拒绝、UI 实时目录失效、暂存路由保留、连接换代失效、暂存后的整数组写入、陈旧 revision 拒绝,以及作用域安装失败后的重试。组装 Web 场景固定真实设置文档与 Plugins 设置卡流程。
- 单元覆盖固定设置校验、异常持久值、Session 取样与继承、共享定义、父级路由默认值、发现限制、执行器拒绝,以及设置 UI 的暂存写入与 revision 检查。组装 Web 场景固定真实设置文档与 Plugins 设置卡流程。
## Related decisions
路由参数、适配器预检、发现工具与 fork 缓存限制仍由[模型选择的 subagent 路由](../../../../packages/subagent/tool-subagent/README.zh.md)负责。
路由参数、适配器预检、发现工具与 fork 限制仍由[模型选择的 subagent 路由](../../../../packages/subagent/tool-subagent/README.zh.md)负责。
@@ -9,23 +9,23 @@
en: 492ba2355360d81a
zh: 825c0e7ab1cf4beb
/agent-note-experimental-auto-review-before-each-tool-call/decision:
en: 3868c5c4b087bdba
zh: c3dff98fc5f05f19
en: d31f010f9c22c452
zh: 5c3966a953227998
/agent-note-experimental-auto-review-before-each-tool-call/decision/effects-and-authority:
en: 55a79d8572fb4332
zh: f63d03d0f3bb5811
en: db0a2deb9193a00d
zh: dab5a65b88957464
/agent-note-experimental-auto-review-before-each-tool-call/decision/one-complete-reviewer-request:
en: d93bd9b8af6b0bfc
zh: 9514c2e5554a721e
en: 3a07a59b6c5ec26c
zh: e4f3290dc6d26368
/agent-note-experimental-auto-review-before-each-tool-call/decision/result-and-cancellation:
en: 237018634b5f9dcf
zh: cbc49d9c1b794065
en: 647fe68c52b907e0
zh: 267a739bad44ad65
/agent-note-experimental-auto-review-before-each-tool-call/decision/process-catalog-and-children:
en: 0042c645360c8361
zh: 5dba39466d376559
/agent-note-experimental-auto-review-before-each-tool-call/alternatives-considered:
en: fe97128ed05b9e43
zh: 4aaa344933fa89b4
en: a7e7f25cf98215bc
zh: 1ddcabec2bba3539
/agent-note-experimental-auto-review-before-each-tool-call/consequences:
en: 1aca35a6d0784f67
zh: 51cfa397db101600
@@ -10,7 +10,7 @@ Full access lets useful project work proceed without repeated approvals, but it
## Decision
[`dsh-experimental-auto-review`](../../../../packages/experimental/auto-review/README.md) is an explicitly installed experimental Web layer, published under the [experimental package publication policy](../../../../packages/experimental/README.md). Default Web retains Read Only, Workspace Write, and Full access. The layer contributes current-session `auto`, whose only durable identity is `permission/preset:auto`; it uses Full access's `danger-full-access` sandbox and tool definitions with the approval policy chosen by [the user-approval fallback decision](2026-09-24-auto-review-user-approval-fallback.md). Headless, General settings, and new-session defaults exclude the integration.
[`dsh-experimental-auto-review`](../../../../packages/experimental/auto-review/README.md) is an explicitly installed experimental profile layer, published under the [experimental package publication policy](../../../../packages/experimental/README.md). Default Web retains Read Only, Workspace Write, and Full access. The layer contributes current-session `auto`, whose only durable identity is `permission/preset:auto`; it uses Full access's `danger-full-access` sandbox and tool definitions with the approval policy chosen by [the user-approval fallback decision](2026-09-24-auto-review-user-approval-fallback.md). Default Headless, General settings, and new-session defaults exclude the integration. [Headless caller admission](2026-10-18-headless-auto-review.md) defines explicit headless opt-in and transport-independent user-source authority.
Every native call and started PTC `tools.*` inner call receives one review before its body. The outer `run_code` transport and direct Node effects in a PTC program remain outside this guarantee. There are no tool-name exemptions, cached grants, retries, configurable policy, or second authorization check; [the user-approval fallback decision](2026-09-24-auto-review-user-approval-fallback.md) owns what follows a denial. A repeated call receives a fresh review.
@@ -21,12 +21,12 @@ The fixed policy follows the allow/soft-deny/hard-deny distinction and classifie
| Risk | Examples | Decision |
| --- | --- | --- |
| `low` | Ordinary project-local reads/writes, analysis, format/lint/test/build, non-destructive Git, and exact cleanup of an object whose retained calls establish creation in this Session | Allow |
| `medium` | Irreversible deletion of pre-existing state, force push/history rewrite, production reads/writes/deployment, non-sensitive external writes/sends, and permission/security/system changes | Allow only with explicit current human or direct-parent authorization naming action, exact target, and necessary scope |
| `medium` | Irreversible deletion of pre-existing state, force push/history rewrite, production reads/writes/deployment, non-sensitive external writes/sends, and permission/security/system changes | Allow only with explicit current user or direct-parent authorization naming action, exact target, and necessary scope |
| `high` | Sensitive data exfiltration across the current trust boundary and equivalent hard-deny effects | Deny, including an explicitly requested action |
Actual effects that are ambiguous or broader than established scope fail closed. A later instruction clears an earlier conflict only by explicitly revoking or replacing it; direct-parent instructions cannot override human restrictions. History can establish a session-created object but cannot authorize medium work or remove a high-risk prohibition.
Actual effects that are ambiguous or broader than established scope fail closed. A later instruction clears an earlier conflict only by explicitly revoking or replacing it; direct-parent instructions cannot override user restrictions. History can establish a session-created object but cannot authorize medium work or remove a high-risk prohibition.
The reviewer derives authority from existing Session facts. A shipped Web human instruction has `source.kind === 'user'` and the `rpcId` written at browser prompt admission. A child's initial direct-parent prompt is identified after its own existing creation descriptor; a later `agent-message` is direct-parent instruction only when `senderSessionId` matches `SessionHeader.parentSession`. Human text defines or replaces the task and restrictions. Direct-parent text defines the child task within those restrictions. Project instructions only constrain; checkpoints restore lossy context; images, attachment metadata, and historical calls provide facts. Compaction does not promote a checkpoint into the human authority of text that left the surface.
The reviewer derives authority from existing Session facts. Text with `source.kind === 'user'` defines or replaces the task and restrictions, independent of its input transport. A child's initial direct-parent prompt is identified after its own existing creation descriptor; a later `agent-message` is direct-parent instruction only when `senderSessionId` matches `SessionHeader.parentSession`. Inherited and unverified child creation prompts remain facts. [User-source admission](2026-10-18-headless-auto-review.md) defines caller and restored-history semantics. Direct-parent text defines the child task within user restrictions. Project instructions only constrain; checkpoints restore lossy context; images, attachment metadata, and historical calls provide facts. Compaction does not promote a checkpoint into the instruction authority of text that left the surface.
### One complete reviewer request
@@ -37,7 +37,7 @@ The integration uses only the latest `request/header.config` provider/model and
| `REVIEW_POLICY` | Fixed classification, source authority, and strict result rules; allow executes immediately with Full access and no later confirmation |
| `ENVIRONMENT` | Existing Session header `cwd` only |
| `PROJECT_INSTRUCTIONS` | Visible project instructions with their original sources and constraint role |
| `FILTERED_HISTORY` | Current compaction surface's sourced human/direct-parent messages, checkpoints, image/attachment facts, and historical call names with logged arguments |
| `FILTERED_HISTORY` | Current compaction surface's sourced user/direct-parent messages, checkpoints, image/attachment facts, and historical call names with logged arguments |
| `PENDING_ACTION` | Tool name, description, parameter schema, and parsed arguments |
The reviewer builds those two action sections from the Session's complete action history: an authorization is the earlier call the `REVIEW_POLICY` source rules make eligible, and a duplicate or conflicting identity has to be visible anywhere in the log rather than inside a recent window, so neither the Session projections nor a bounded read serves the decision. The read is the deprecated synchronous `snapshotEvents()` under a line-scoped `typescript/no-deprecated` waiver.
@@ -50,7 +50,7 @@ The reviewer may emit reasoning blocks followed by exactly one JSON text block a
A final denial, which [the user-approval fallback decision](2026-09-24-auto-review-user-approval-fallback.md) limits to the `never` approval policy, gives native results and PTC settle events the same structured `AutoReviewDeniedError` / `AUTO_REVIEW_DENIED` and optional raw reason. For a final denial the main agent receives only `Auto review rejected tool "<name>"; its body was not executed` through ordinary failure rendering. PTC retains the existing program exception/catch behavior; catching a denial does not elevate it to an outer failure. The generic Web tool card supplies the denial identity for the collapsed row and one not-executed output line for the expanded row, with no input body. Only that display trims and collapses line separators or supplies the localized empty-reason fallback; persistence and both SDKs preserve the complete raw reason, without a new length or redaction rule.
Admission and active-review enrollment occur synchronously before the first await. The integration owns one lifecycle controller and one set of active operations. Unload closes new selection/review admission, changes live Auto Sessions to Full access through the existing preset writer, which writes the `never` approval policy without changing the sandbox value or closing terminals, then aborts and drains reviews before removing listener and contribution. After provider settlement, a lifecycle abort always produces canonical pre-dispatch cancellation, including late allow, deny, or failure. Caller cancellation retains ToolRuntime's priority: late allow cancels before dispatch; a late failure or final denial retains that outcome, and a late denial under `ask` produces a cancelled approval and canonical pre-dispatch cancellation. No cancelled review starts a tool body.
Admission and active-review enrollment occur synchronously before the first await. The integration owns one lifecycle controller and one set of active operations. Unload closes new selection/review admission, then aborts and drains reviews before removing listener and contribution. Only unload while the root context remains active changes live Auto Sessions to Full access through the existing preset writer, which writes the `never` approval policy without changing the sandbox value or closing terminals. Root shutdown preserves selection: stopping the application does not revoke its installed integration, and its child disposers can race Session persistence cleanup. The root fiber enters `UNLOADING` before child cleanup starts, so that lifecycle state distinguishes shutdown without a new shutdown service or timing heuristic. After provider settlement, a lifecycle abort always produces canonical pre-dispatch cancellation, including late allow, deny, or failure. Caller cancellation retains ToolRuntime's priority: late allow cancels before dispatch; a late failure or final denial retains that outcome, and a late denial under `ask` produces a cancelled approval and canonical pre-dispatch cancellation. No cancelled review starts a tool body.
A persisted Auto Session cannot publish when the complete integration is absent or failed. Its log is not rewritten, and no background retry runs. Reinstallation allows a user to reopen it; live Sessions migrated to Full access remain there until explicitly switched.
@@ -64,7 +64,7 @@ The [delegation-time policy capture](../../../../packages/subagent/subagent/READ
## Alternatives considered
**Default or released integration** would make an experimental model approval policy part of every deployment. A private source Web patch keeps opt-in explicit while using the existing plugin installer.
**Default or released integration** would make an experimental model approval policy part of every deployment. An explicit profile patch keeps opt-in explicit while using the existing plugin installer.
**A new sandbox or approval-policy value** would couple review to enforcement and create combinations without a current consumer. An explicit preset identity preserves the unchanged Full access execution behavior.
@@ -10,7 +10,7 @@ Full access 让有用的项目工作无需反复审批即可继续,但也允
## 决策
[`dsh-experimental-auto-review`](../../../../packages/experimental/auto-review/README.zh.md)是显式安装的实验性 Web 层,按[实验包发布策略](../../../../packages/experimental/README.zh.md)参与发布。默认 Web 保持 Read Only、Workspace Write 与 Full access。此层贡献仅限当前会话的 `auto`,唯一持久身份为 `permission/preset:auto`;它使用 Full access 的 `danger-full-access` 沙箱与工具定义,审批策略由[用户审批兜底决策](2026-09-24-auto-review-user-approval-fallback.zh.md)决定。Headless、通用设置与新会话默认值都排除此 integration。
[`dsh-experimental-auto-review`](../../../../packages/experimental/auto-review/README.zh.md)是显式安装的实验性 profile 层,按[实验包发布策略](../../../../packages/experimental/README.zh.md)参与发布。默认 Web 保持 Read Only、Workspace Write 与 Full access。此层贡献仅限当前会话的 `auto`,唯一持久身份为 `permission/preset:auto`;它使用 Full access 的 `danger-full-access` 沙箱与工具定义,审批策略由[用户审批兜底决策](2026-09-24-auto-review-user-approval-fallback.zh.md)决定。默认 Headless、通用设置与新会话默认值都排除此 integration。[Headless 调用者入口](2026-10-18-headless-auto-review.zh.md)定义显式 headless 启用方式与不依赖传输的 user-source 权威。
每个原生调用与已开始的 PTC `tools.*` inner call 都在 body 前接受一次审查。外层 `run_code` transport 与 PTC 程序内直接 Node 效果不在保证范围内。不提供按工具名豁免、缓存 grant、重试、可配置策略或第二授权检查;拒绝之后的行为由[用户审批兜底决策](2026-09-24-auto-review-user-approval-fallback.zh.md)负责。重复调用也重新审查。
@@ -21,12 +21,12 @@ Full access 让有用的项目工作无需反复审批即可继续,但也允
| 风险 | 示例 | 决定 |
| --- | --- | --- |
| `low` | 普通项目内读写、分析、format/lint/test/build、非破坏性 Git,以及保留调用能证明在本 Session 创建的对象的精确清理 | 允许 |
| `medium` | 不可逆删除既有状态、force push/历史改写、生产读写/部署、非敏感外部写入/发送,以及权限/安全/系统变更 | 只有当前 human 或直接父级明确授权动作、准确目标与必要范围时才允许 |
| `medium` | 不可逆删除既有状态、force push/历史改写、生产读写/部署、非敏感外部写入/发送,以及权限/安全/系统变更 | 只有当前用户或直接父级明确授权动作、准确目标与必要范围时才允许 |
| `high` | 敏感数据跨越当前信任边界外泄及等价 hard-deny 效果 | 拒绝,包括显式请求的动作 |
实际效果不明确或超出已确立范围时按拒绝处理。后续指令只有明确撤销或替换旧指令才能消除冲突;直接父级指令不能覆盖 human 限制。历史可以证明对象由本会话创建,但不能授权 medium 操作或解除 high 禁令。
实际效果不明确或超出已确立范围时按拒绝处理。后续指令只有明确撤销或替换旧指令才能消除冲突;直接父级指令不能覆盖用户限制。历史可以证明对象由本会话创建,但不能授权 medium 操作或解除 high 禁令。
Reviewer 从既有 Session 事实派生权威。Shipped Web human 指令具有 `source.kind === 'user'` 和浏览器 prompt admission 写入的 `rpcId`。Child 初始直接父级 prompt 在其自身既有创建 descriptor 之后识别;后续 `agent-message` 只有 `senderSessionId` 与 `SessionHeader.parentSession` 匹配时才属于直接父级指令。Human 文本定义或替换任务与限制。直接父级文本在这些限制内定义 child 任务。项目指令只施加约束;checkpoint 恢复有损语境;图片、附件元数据和历史调用提供事实。Compaction 不会把 checkpoint 提升为已经离开 surface 的原文本所拥有的 human 权威。
Reviewer 从既有 Session 事实派生权威。`source.kind === 'user'` 的文本定义或替换任务与限制,不区分输入传输。Child 初始直接父级 prompt 在其自身既有创建 descriptor 之后识别;后续 `agent-message` 只有 `senderSessionId` 与 `SessionHeader.parentSession` 匹配时才属于直接父级指令。继承的 child 创建 prompt 与无法验证的 child 创建 prompt 仍是事实。[User-source 接纳](2026-10-18-headless-auto-review.zh.md)定义调用者与恢复历史的语义。直接父级文本在用户限制内定义 child 任务。项目指令只施加约束;checkpoint 恢复有损语境;图片、附件元数据和历史调用提供事实。Compaction 不会把 checkpoint 提升为已经离开 surface 的原文本所拥有的指令权威。
### 一次完整 reviewer 请求
@@ -37,7 +37,7 @@ Integration 只使用最新 `request/header.config` 的 provider/model 与 shi
| `REVIEW_POLICY` | 固定分类、来源权威与严格结果规则;allow 后立即以 Full access 执行,没有后续确认 |
| `ENVIRONMENT` | 仅既有 Session header `cwd` |
| `PROJECT_INSTRUCTIONS` | 可见项目指令,保留原始来源并赋予约束角色 |
| `FILTERED_HISTORY` | 当前 compaction surface 中带来源的 human/直接父级消息、checkpoint、图片/附件事实,以及历史调用名称与日志参数 |
| `FILTERED_HISTORY` | 当前 compaction surface 中带来源的用户/直接父级消息、checkpoint、图片/附件事实,以及历史调用名称与日志参数 |
| `PENDING_ACTION` | 工具名称、描述、参数 schema 与解析后的 arguments |
reviewer 从 Session 的完整动作历史构建这两个动作分节:授权是符合 `REVIEW_POLICY` 来源规则的那个更早调用,而重复或冲突的身份必须在整份日志的任何位置都可见,而不是只在近期窗口内可见,因此 Session 投影与有界读取都无法支撑该判定。这次读取使用已废弃的同步 `snapshotEvents()`,并带有行级 `typescript/no-deprecated` 豁免。
@@ -50,7 +50,7 @@ Reviewer 可以输出 reasoning blocks,随后恰好一个 JSON text block 和
最终拒绝([用户审批兜底决策](2026-09-24-auto-review-user-approval-fallback.zh.md)将其限定在 `never` 审批策略下)让原生结果与 PTC 结算事件携带同形结构化 `AutoReviewDeniedError`/`AUTO_REVIEW_DENIED` 及可选原始理由。对最终拒绝,主 agent 通过普通失败渲染只收到 `Auto review rejected tool "<name>"; its body was not executed`。PTC 保留既有程序异常/catch 行为;捕获拒绝不会将其提升为外层失败。通用 Web 工具卡片为折叠行提供拒绝身份,为展开行提供一行未执行输出,不提供输入正文。只有该显示过程会 trim、折叠行分隔符,或提供本地化空理由 fallback;持久化与两套 SDK 保留完整原始理由,不增加长度或脱敏规则。
准入与在途 review 登记在首次 await 前同步完成。Integration 拥有一个生命周期 controller 和一个在途操作集合。卸载先关闭新选择/review admission,经由既有 preset writer 把存活 Auto Session 切为 Full access,该 writer 写入 `never` 审批策略,不改变沙箱值、不关闭终端,然后中止并等待 review 结清,最后移除 listener 与 contribution。Provider 结算后,lifecycle abort 始终形成规范的 dispatch 前取消,包括晚到 allow、deny 或 failure。Caller 取消保留 ToolRuntime 优先级:晚到 allow 在 dispatch 前取消;晚到 failure 或最终拒绝保留原结果,`ask` 下晚到的拒绝产生已取消的审批与规范的 dispatch 前取消。被取消的 review 不启动工具 body。
准入与在途 review 登记在首次 await 前同步完成。Integration 拥有一个生命周期 controller 和一个在途操作集合。卸载先关闭新选择/review admission,然后中止并等待 review 结清,最后移除 listener 与 contribution。只有在根 context 仍活跃时卸载插件,才经由既有 preset writer 把存活 Auto Session 切为 Full access;该 writer 写入 `never` 审批策略,不改变沙箱值、不关闭终端。根关闭保留选择:停止应用不等于撤销已安装的 integration,且子级 disposer 可能与 Session 持久化清理并发。根 fiber 在子级清理开始前进入 `UNLOADING`,因此可用该生命周期状态区分关闭,无需新建关闭服务或依赖时序推测。Provider 结算后,lifecycle abort 始终形成规范的 dispatch 前取消,包括晚到 allow、deny 或 failure。Caller 取消保留 ToolRuntime 优先级:晚到 allow 在 dispatch 前取消;晚到 failure 或最终拒绝保留原结果,`ask` 下晚到的拒绝产生已取消的审批与规范的 dispatch 前取消。被取消的 review 不启动工具 body。
完整 integration 缺失或失败时,持久 Auto Session 不能发布。日志不改写,也不后台重试。重装后用户可以重新打开它;已经迁移为 Full access 的存活 Session 保持原状,直到显式切换。
@@ -64,7 +64,7 @@ Auto 在选择器选项中带 `EXP` 标记;选中后的 composer 控件不显
## 考虑过的替代方案
**默认或正式发布 integration** 会把实验模型审批策略带入每个部署。私有源码 Web patch 在复用既有插件安装器的同时保持显式 opt-in。
**默认或正式发布 integration** 会把实验模型审批策略带入每个部署。显式 profile patch 在复用既有插件安装器的同时保持显式 opt-in。
**新增 sandbox 或 approval-policy 值**会把 review 与执行耦合,产生没有当前消费者的组合。显式 preset 身份保留 Full access 未改变的执行行为。
@@ -9,8 +9,8 @@
en: ed725dff16cd2339
zh: 555d4ba70af96790
/agent-note-auto-review-asks-the-user-after-a-denial/decision:
en: f5ec55bcfe5e9a9a
zh: 1b7462f8f06320e9
en: 6bd32636c775b690
zh: 5579111a3dbb6bf9
/agent-note-auto-review-asks-the-user-after-a-denial/alternatives-considered:
en: 01f71dd10849f925
zh: 8b79a7e149a9a60c
@@ -14,7 +14,7 @@ A reviewer denial returns the tools pipeline's existing `ask` decision when the
Selecting Auto writes `danger-full-access` with the `ask` approval policy. A recorded Auto selection also matches the `never` policy. A [delegated in-process child pins `never`](../../../../packages/subagent/subagent/README.md), so a child Auto keeps the final `AutoReviewDeniedError` denial and its reason, and a Session recorded under the earlier `never` bundle still resolves to Auto instead of Full access. Selecting Auto again writes `ask`.
A reviewer failure denies the call with `Auto review of tool "<name>" failed; its body was not executed: <error>` and no structured error info. A provider failure's message carries the finish kind, failure code, and provider message, so the model and the generic tool card show the actual cause.
A reviewer failure denies the call with `Auto review of tool "<name>" failed; its body was not executed: <error>` and structured `AutoReviewFailedError` / `AUTO_REVIEW_FAILED` info. [Headless callers](2026-10-18-headless-auto-review.md) use that identity to distinguish failure from denial. A provider failure's message carries the finish kind, failure code, and provider message, so the model and the generic tool card show the actual cause.
## Alternatives considered
@@ -14,7 +14,7 @@ Status: implemented
选择 Auto 会写入 `danger-full-access` 与 `ask` 审批策略。已记录的 Auto 选择也匹配 `never` 策略。[进程内委派 child 固定 `never`](../../../../packages/subagent/subagent/README.zh.md),因此 child 的 Auto 保留最终的 `AutoReviewDeniedError` 拒绝及其理由;按先前 `never` 组合记录的 Session 仍解析为 Auto,而不是 Full access。再次选择 Auto 会写入 `ask`。
Reviewer 失败以 `Auto review of tool "<name>" failed; its body was not executed: <error>` 拒绝调用,不带结构化错误信息。Provider 失败消息包含 finish 类型、失败代码与 provider 消息,因此模型与通用工具卡片显示实际原因。
Reviewer 失败以 `Auto review of tool "<name>" failed; its body was not executed: <error>` 拒绝调用,带结构化 `AutoReviewFailedError` / `AUTO_REVIEW_FAILED` 信息。[Headless 调用者](2026-10-18-headless-auto-review.zh.md)用该身份区分失败与拒绝。Provider 失败消息包含 finish 类型、失败代码与 provider 消息,因此模型与通用工具卡片显示实际原因。
## Alternatives considered
@@ -0,0 +1,31 @@
# Bilingual-pair consistency record for 2026-10-15-chat-keep-assistant-replies.md (docs/i18n/README.md): per heading
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
# After editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-10-15-chat-keep-assistant-replies.md
/agent-note-keep-every-assistant-reply-when-a-turn-folds:
en: 813e0e2590457f2d
zh: 12b01305ae439712
/agent-note-keep-every-assistant-reply-when-a-turn-folds/problem:
en: 967c8df85ef30286
zh: 7cd5f219fe42f567
/agent-note-keep-every-assistant-reply-when-a-turn-folds/decision:
en: 7f59f284df26b5fe
zh: b6ae20ffdb506ac3
/agent-note-keep-every-assistant-reply-when-a-turn-folds/decision/what-stays-visible:
en: 41d8322fb0a8d010
zh: a413bfdffb34833f
/agent-note-keep-every-assistant-reply-when-a-turn-folds/decision/what-does-not-change:
en: 352c5bdf25c09c62
zh: 24878584535b4d57
/agent-note-keep-every-assistant-reply-when-a-turn-folds/decision/implementation:
en: e40156780daec8fe
zh: 4447a61c2b1574c8
/agent-note-keep-every-assistant-reply-when-a-turn-folds/alternatives-considered:
en: 28694c0a82df72ac
zh: 8f6f81c833cf8f2c
/agent-note-keep-every-assistant-reply-when-a-turn-folds/consequences:
en: d648f688c7e6c87d
zh: d84f1a43de8992b2
/agent-note-keep-every-assistant-reply-when-a-turn-folds/testing:
en: 70133688614f2a5c
zh: 931a2710211ced3f
@@ -0,0 +1,111 @@
# Agent Note: Keep Every Assistant Reply When a Turn Folds
Status: implemented
English | [中文](2026-10-15-chat-keep-assistant-replies.zh.md)
## Problem
In Compact, Standard, and Detailed, a normally completed Turn folds as a whole. Under the previous rule the reader saw only the input, the whole-Turn control, the **final answer**, and the footer. The final answer is chosen by position: the last Assistant message in the latest Step that has visible reply content and no tool-call block (`latestAnswer` in [turn-process.ts](../../../../packages/client/ui-chat/src/client/conversation-nodes/turn-process.ts)). Every intermediate reply before it was hidden with the process.
Position is not content. A Turn often continues after the answer: a settled-subagent notice wakes another Step, the Agent updates its records, or it calls a tool after answering. The last message then becomes a short addendum, or there is no text-only ending at all, and the real answer folds into the process (#6132, #5429). For example, after a long research summary, a subagent completion notice woke one more Step whose message added a single remark; that remark was all the reader saw.
We made a rough estimate of how intermediate replies are distributed. The proportions below are order-of-magnitude judgments, not statistical results:
- In main Sessions, intermediate replies before the last reply make up about 10% of the Assistant text; subagent Sessions write more fragmented replies, about 20%.
- By count, more than half are one- or two-line "next I will…" messages, so showing all of them adds redundancy.
- A few longer intermediate replies carry actual conclusions or deliverables. Under the previous rule they folded with the process, which directly affected whether the reader understood the result.
- Some subagent tasks end with a structured-result tool call and no text-only reply, so under the previous rule the whole Turn showed no text at all.
## Decision
By default, a folded Turn hides only its process, never its replies: every `assistant-step` reply row stays outside whole-Turn folding, whichever Step it belongs to. Main and subagent Sessions use the same rule, without a branch on the Session kind.
Settings → General → Replies After Collapse sits directly below When to Collapse Work Details:
| Option | Behavior |
|---|---|
| All replies (default, `all`) | Every reply stays visible; only the process folds |
| Final answer only (`final`) | Previous rule: intermediate replies fold with the process and only the final answer stays visible |
- The choice is saved as `ui-chat.foldedReplies` in the same Host profile settings as Work details; without a saved value it uses `all` and does not write the default.
- Switching applies immediately to rendered Turns by changing visibility only, without rebuilding rows.
- Verbose never folds a Turn, so the option is hidden in that mode.
### What stays visible
| Viewing state (Compact/Standard/Detailed, `all`) | Visible content |
|---|---|
| Whole Turn collapsed | Input, whole-Turn control, **every reply row** in its original order, and Nodes at the `turn` position (terminal errors, max-token notices, the completed-turn footer) |
| Whole Turn open | Process group headers and every reply row; group bodies follow the mode and manual disclosure |
A collapsed Turn hides:
- the reasoning rows of every Step, including the final answer's reasoning;
- tool calls, their results, and step groups;
- Nodes at the `process` position, such as model retries;
- every other process row without a group-tree position.
A Turn with replies but no process content keeps the control's title without a collapse action.
### What does not change
| Aspect | Detail |
|---|---|
| Folding eligibility | Running, stopped, failed, and interleaved-input Turns offer no whole-Turn collapse; the rules for a missing start or end are unchanged |
| Whole-Turn control | One per Turn, with unchanged position and title (duration or status). Hidden step groups between replies simply disappear; no per-segment fold rows appear |
| Collapse timing | On completion, On next message, and the exchange exemption are unchanged |
| Manual disclosure | Still saved per answer generation |
| Footer, branch, and copy | Attached to the Turn's last settled Assistant message with text (`closing` of `turn-tail`), independent of whole-Turn folding |
### Implementation
Only the rule that decides process membership changes, and it follows the setting. `TurnProcessSpec` with its answer fields (`answerAnchorSeq`, `answerStep`, `inlineReasoning`, `messageCount`), the `latestAnswer` selection, and manual disclosure saved per answer Step stay as they were; with Final answer only they still apply and the behavior matches the previous rule.
| Location | Content |
|---|---|
| [chat-settings.ts](../../../../packages/client/ui-chat/src/chat-settings.ts), [index.ts](../../../../packages/client/ui-chat/src/index.ts) | `FOLDED_REPLIES_FIELD`, `FOLDED_REPLIES = ['all', 'final']`, and `DEFAULT_FOLDED_REPLIES = 'all'`, registered in `ChatSettingsFields` and the Host `Config` |
| [transcript-view.ts](../../../../packages/client/ui-chat/src/client/transcript-view.ts) | The `foldedReplies` snapshot and `setFoldedReplies` of `TranscriptViewPolicy`, read and written like `collapseTiming` |
| [presentation-policy.ts](../../../../packages/client/ui-chat/src/client/presentation-policy.ts) | `ChatPresentationPolicy.foldedReplies`; `presentationPolicyFor(mode, timing, replies)` returns one fixed object for each of the 16 combinations; `derivePresentationPolicy` subscribes to all three preferences |
| [apply.ts](../../../../packages/client/ui-chat/src/client/apply.ts), [settings/TranscriptViewRow.tsx](../../../../packages/client/ui-chat/src/client/settings/TranscriptViewRow.tsx), [locale.ts](../../../../packages/client/ui-chat/src/client/locale.ts) | The selector below collapse timing, not rendered in Verbose; Chinese and English copy |
| [chat/ChatNodeSeat.tsx](../../../../packages/client/ui-chat/src/client/chat/ChatNodeSeat.tsx) | With `foldedReplies === 'all'`, a `replyRow` (the non-reasoning part of an `assistant-step` with reply content) is not a `processMember` and owns the disclosure (`ownsDisclosure`) to read `foldable`/`open`; collapsible content uses `hasNonReplyProcess` or `hasExternalProcess` according to the setting |
| [chat/AssistantNodeView.tsx](../../../../packages/client/ui-chat/src/client/chat/AssistantNodeView.tsx) | With `all`, a folded Turn hides the inline reasoning of every reply row; with `final`, only the final answer's inline reasoning |
| [conversation-nodes/turn-process-presentation.ts](../../../../packages/client/ui-chat/src/client/conversation-nodes/turn-process-presentation.ts) | `hasNonReplyProcess` beside `hasExternalProcess`: an `assistant-step` from a Step other than the answer's counts as process content only through its reasoning |
The [Chat Node business rules](../../../../packages/client/ui-chat/src/client/conversation-nodes/README.md) describe the reader-visible rule and setting; [Chat group-tree positions and exchanges](../architecture/2026-10-11-chat-group-tree-exchanges.md) records that `assistant-step` reply rows stay outside whole-Turn folding.
## Alternatives considered
**Keep showing only the final answer.** The final answer is chosen by position. When a Turn continues after answering, the reader loses the real answer.
**Distinguish by message structure: show only replies without tool calls, except messages that call `present` or `ask_user_question`.** It removes part of the redundancy while keeping most of the effective content, but it needs a maintained list of tool-name exceptions, a fine-grained rule.
**Distinguish by a length threshold: keep folding short replies that precede a tool call.** It removes most short previews, but the threshold has no general basis, and Chinese and English character counts are not comparable.
**Keep the previous rule for subagent Sessions.** ui-chat would need to read `sessions.subagentAddress()` and branch on the Session kind. Subagent replies are more fragmented, so showing all of them adds more redundancy, but those views are rarely read directly, and a reader who opens one usually wants the process; subagent tasks that end with a structured result showed no text under the previous rule and now show the summary written before submitting the result.
**Give each process segment its own fold row.** Replies split the process into segments, adding many fold rows. The whole-Turn control already opens and closes the entire process at once.
**Also delete the final-answer structure (the `answerStep` fields, `latestAnswer`, and per-generation manual disclosure).** Less code, but a larger change, and Final answer only could no longer be offered.
**Change the default rule without a setting.** Readers who prefer the previous presentation would have no way back; the previous rule's code remains anyway, so a preference adds only one read/write path.
**Bind the option to the Work details mode (for example, Compact = final answer only).** No new settings row, but two independent choices would be tied together and could not be switched separately.
**Merge short action previews into the title of the following step group.** No text is lost and no row is added, but group titles need their own design, outside this change.
## Consequences
- Gains: Turns that receive a notice, record keeping, or a tool call after answering no longer lose the answer; subagent tasks that end with a structured result are no longer blank; readers who want the previous presentation can switch back with one choice.
- Collapsed text grows by an estimated 10% in main Sessions and 20% in subagent views. The added text includes actual conclusions that previously folded away as well as redundancy from one- or two-line action previews; subagent views are rarely read, so the absolute cost is small.
- After collapsing, consecutive replies no longer show the process between them and may read as one passage; each reply remains its own row with the original spacing.
- A collapsed Turn no longer gives its final answer the control's 16px clearance: in both choices, a reply separated from the control by folded rows keeps the 12px reply gap, and only a row directly following the control receives 16px. Because no reply changes its spacing when the fold finishes, the final answer no longer moves down about 4px at the end of a fold (#6323).
- The retained answer structure leaves one small asymmetry under `all`: `data-turn-process-messages` still counts the replies before the final answer although they are visible.
- These proportions are rough estimates and vary with ways of working and models.
## Testing
- `packages/client/ui-chat/tests/chat-view.client.spec.tsx`: replies from every Step stay visible while reasoning and tools fold, a Turn without a text-only ending still shows its replies, and a reply-only Turn offers no collapse; switching between `all` and `final` does not rebuild rows; cases that used an intermediate reply as a process stand-in now use reasoning-only rows.
- `packages/client/ui-chat/tests/presentation-policy.client.spec.tsx`: fixed policy objects for the 16 combinations and subscriptions to all three preferences.
- `packages/client/ui-chat/tests/transcript-view-policy.client.spec.ts`, `transcript-view-row.client.spec.tsx`, and `config.host.spec.ts`: default, persistence, and adoption; the settings row's position, Chinese and English copy, and hiding in Verbose; Host configuration validation and restoration.
- Web e2e: `turn-tail-actions.e2e.ts` and `chat-flow-layout.e2e.ts` assert that the first visible reply after a folded control keeps the 12px reply gap; the settings and folded-Turn aria goldens include the new settings row and intermediate replies.
@@ -0,0 +1,111 @@
# Agent Note: 整轮折叠保留全部 Assistant 回复
Status: implemented
[English](2026-10-15-chat-keep-assistant-replies.md) | 中文
## Problem
简洁、标准、详细模式下,正常结束的 Turn 会整轮折叠。原规则下读者只看到输入、整轮控件、**最终答案**和页脚。最终答案按位置选取,即最后一个 Step 里最后一条有可见回复、且不带工具调用块的 Assistant 消息([turn-process.ts](../../../../packages/client/ui-chat/src/client/conversation-nodes/turn-process.ts) 的 `latestAnswer`);在它之前的阶段回复和过程一起被隐藏。
位置不等于内容。回答之后 Turn 往往还会继续:子 agent 结束的通知唤醒一步,Agent 记账、收尾,或回答后又调用了工具。这时最后一条变成一句简短补充,或者根本没有纯文字收尾,真正的回答被折进了过程(#6132、#5429)。例如一段很长的调研汇总之后,子 agent 结束的通知又唤醒一步,最后一条只写了一句补充;读者看到的就只有这句补充。
我们对阶段回复的分布做了粗略估计。以下比例是量级判断,不是统计结果:
- 主会话里,最后一条正文之前的阶段回复约占 Assistant 正文的 10%;子 agent 会话的阶段回复更碎,约占 20%。
- 按条数算,一半以上是一两行的"接下来要做什么",全部显示会带来冗余。
- 少数较长的阶段回复装着实际结论或交付内容。原规则下它们随过程被折叠,直接影响读者理解问题。
- 一部分子任务以结构化结果的工具调用结束,没有纯文字回复,原规则下整轮看不到任何文字。
## Decision
整轮收起默认只隐藏过程,不再隐藏回复:所有 `assistant-step` 的回复行都留在整轮折叠之外,不论它属于哪个 Step。主会话和子 agent 会话使用同一规则,不按会话类型分支。
设置 → 通用设置的「收起后显示的回复」位于「工作步骤收起时机」正下方:
| 选项 | 行为 |
|---|---|
| 全部回复(默认,`all`) | 回复全部可见,只收起过程 |
| 仅最终回答(`final`) | 原规则:阶段回复随过程一起收起,只露最终答案 |
- 选择以 `ui-chat.foldedReplies` 保存到与「工作步骤展示」相同的 Host profile 设置;未保存时按 `all`,不自动写入默认值。
- 切换立即作用于已渲染的 Turn,只改显隐,不重建行。
- 完全展开模式从不折叠 Turn,该选项在这一模式下隐藏。
### 可见范围
| 查看状态(简洁/标准/详细,`all`) | 可见内容 |
|---|---|
| 整轮收起 | 输入、整轮控件、按原顺序排列的**全部回复行**、`turn` 位置的节点(终止错误、最大 token 提示、已完成轮次页脚) |
| 整轮展开 | 过程组头与全部回复行;组正文按模式与手动开合显示 |
整轮收起时隐藏的内容:
- 所有 Step 的推理行,包括最终答案附带的推理;
- 工具调用与结果、步骤组;
- `process` 位置的节点(如模型重试);
- 其余没有分组树位置的过程行。
只有回复、没有任何过程内容的 Turn,整轮控件保留标题,不提供折叠操作。
### 不变的部分
| 方面 | 说明 |
|---|---|
| 折叠资格 | 运行中、已停止、失败、过程开始后有可见输入的 Turn 不提供整轮收起;缺起点、缺结束记录的规则不变 |
| 整轮控件 | 每个 Turn 一个,位置和标题(时长或状态)不变。回复之间被隐藏的步骤组直接消失,不为每段过程单独生成折叠行 |
| 收起时机 | 「完成时收起」「下次发送时收起」与交互段例外不变 |
| 手动开合 | 仍按答案世代保存 |
| 页脚、分支、复制 | 挂在本 Turn 最后一条带文字的已结算 Assistant 消息上(`turn-tail` 的 `closing`),与整轮折叠无关 |
### 实现
只改"谁是过程成员"这一条规则,并让它跟随设置。`TurnProcessSpec` 及其答案字段(`answerAnchorSeq`、`answerStep`、`inlineReasoning`、`messageCount`)、`latestAnswer` 选取、按答案 Step 保存的手动开合保持原样;选「仅最终回答」时这些结构照旧生效,行为与原规则一致。
| 位置 | 内容 |
|---|---|
| [chat-settings.ts](../../../../packages/client/ui-chat/src/chat-settings.ts)、[index.ts](../../../../packages/client/ui-chat/src/index.ts) | `FOLDED_REPLIES_FIELD`、`FOLDED_REPLIES = ['all', 'final']`、`DEFAULT_FOLDED_REPLIES = 'all'`,登记在 `ChatSettingsFields` 与 Host `Config` |
| [transcript-view.ts](../../../../packages/client/ui-chat/src/client/transcript-view.ts) | `TranscriptViewPolicy` 的 `foldedReplies` 快照与 `setFoldedReplies`,读写方式与 `collapseTiming` 相同 |
| [presentation-policy.ts](../../../../packages/client/ui-chat/src/client/presentation-policy.ts) | `ChatPresentationPolicy.foldedReplies`;`presentationPolicyFor(mode, timing, replies)` 对 16 种组合各返回固定对象;`derivePresentationPolicy` 订阅三项偏好 |
| [apply.ts](../../../../packages/client/ui-chat/src/client/apply.ts)、[settings/TranscriptViewRow.tsx](../../../../packages/client/ui-chat/src/client/settings/TranscriptViewRow.tsx)、[locale.ts](../../../../packages/client/ui-chat/src/client/locale.ts) | 设置行在收起时机下方的选择框,完全展开模式下不渲染;中英文案 |
| [chat/ChatNodeSeat.tsx](../../../../packages/client/ui-chat/src/client/chat/ChatNodeSeat.tsx) | `foldedReplies === 'all'` 时的 `replyRow`(`assistant-step` 中非推理部分且有回复内容的行)不是 `processMember`,并拥有开合状态(`ownsDisclosure`)以读取 `foldable`/`open`;可折叠内容按设置取 `hasNonReplyProcess` 或 `hasExternalProcess` |
| [chat/AssistantNodeView.tsx](../../../../packages/client/ui-chat/src/client/chat/AssistantNodeView.tsx) | `all` 时整轮收起隐藏回复行自带的内联推理;`final` 时只隐藏最终答案的内联推理 |
| [conversation-nodes/turn-process-presentation.ts](../../../../packages/client/ui-chat/src/client/conversation-nodes/turn-process-presentation.ts) | `hasExternalProcess` 之外的 `hasNonReplyProcess`:非答案 Step 的 `assistant-step` 只有带推理时才算过程内容 |
[Chat 节点业务规则](../../../../packages/client/ui-chat/src/client/conversation-nodes/README.zh.md)描述读者可见的规则与设置;[Chat 分组树位置与交互段](../architecture/2026-10-11-chat-group-tree-exchanges.zh.md)记录 `assistant-step` 回复行留在整轮折叠之外。
## Alternatives considered
**保持只露最终答案。** 最终答案按位置选取。回答之后 Turn 还在继续时,读者会丢掉真正的回答。
**按消息结构区分:只露不带工具调用的回复,带 `present`、`ask_user_question` 的消息例外。** 能去掉一部分冗余,同时保住大部分有效内容,但需要按工具名维护例外名单,属于微观规则。
**按字数阈值区分:较短且紧跟工具调用的回复继续折叠。** 能去掉大部分短预告,但阈值没有通用依据,中英文字数也不可比。
**子 agent 会话保持原规则。** ui-chat 需要新读 `sessions.subagentAddress()` 并按会话类型分支。子 agent 的阶段回复更碎,全部显示后冗余更多,但这类视图很少被直接阅读,读者点进去通常就是想看过程;以结构化结果收尾的子任务原规则下整轮空白,改后能看到提交结果前写的总结。
**每段过程各给一个折叠行。** 回复把过程切成多段,会多出大量折叠行。整轮控件已经能一次开合全部过程。
**同时删除最终答案结构(`answerStep` 等字段、`latestAnswer`、按答案世代的手动开合)。** 代码更少,但改动面大,也无法再提供「仅最终回答」选项。
**不提供设置,只改默认规则。** 想看原呈现的用户没有退路;原规则的代码本来就保留,加一个偏好只多一条读写链。
**把选项绑到「工作步骤展示」模式上(例如简洁 = 仅最终回答)。** 不需要新设置行,但两件独立的事被绑在一起,不能单独切换。
**把短的动作预告并入后续步骤组的标题。** 不丢文字,也不占行,但需要单独设计组标题,不在本次范围内。
## Consequences
- 收益:回答之后又有通知、记账或工具调用的 Turn 不再丢失回答;以结构化结果收尾的子任务不再整轮空白;想要原呈现的读者可以一键切回。
- 收起后的正文按估计在主会话多出约 10%,在子 agent 视图多出约 20%。多出的内容里既有原本被折掉的实际结论,也有一两行动作预告带来的冗余;子 agent 视图阅读频率低,代价按绝对量计较小。
- 收起后连续的回复之间不再显示过程,几条回复可能被连读成一段;每条回复仍是独立行,保持原有间距。
- 收起的 Turn 不再给最终答案额外的控件 16px 间距:两种选项下,与控件之间隔着已收起行的回复都保持 12px 回复间距,只有紧跟控件的那一行得到 16px。折叠结束时没有回复改变间距,最终答案不再在折叠末尾下移约 4px(#6323)。
- 保留的答案结构在 `all` 下留下一处小的不对称:`data-turn-process-messages` 仍统计最终答案之前的回复数,尽管它们已可见。
- 上述比例是粗略估计,不同的工作方式和模型下会有差异。
## Testing
- `packages/client/ui-chat/tests/chat-view.client.spec.tsx`:收起后各 Step 的回复可见、推理与工具收起、无纯文字收尾的 Turn 仍显示回复、只有回复的 Turn 不提供折叠;`all` 与 `final` 实时切换不重建行;把阶段回复当作过程替身的用例改为纯推理行。
- `packages/client/ui-chat/tests/presentation-policy.client.spec.tsx`:16 种组合的固定策略对象与三项偏好订阅。
- `packages/client/ui-chat/tests/transcript-view-policy.client.spec.ts`、`transcript-view-row.client.spec.tsx`、`config.host.spec.ts`:默认值、持久化与采纳,设置行位置、中英文案与完全展开时隐藏,Host 配置校验与恢复。
- Web e2e:`turn-tail-actions.e2e.ts` 与 `chat-flow-layout.e2e.ts` 断言收起后控件之后第一条可见回复保持 12px 回复间距;设置页与收起后 aria 黄金快照包含新设置行与阶段回复。
@@ -0,0 +1,37 @@
# Bilingual-pair consistency record for 2026-10-15-deepseek-search-fetch-api.md (docs/i18n/README.md): per heading
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
# After editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-10-15-deepseek-search-fetch-api.md
/agent-note-direct-deepseek-search-and-fetch-apis:
en: fb19be7e7fc65810
zh: 865aaed5b7f802cc
/agent-note-direct-deepseek-search-and-fetch-apis/problem:
en: d1aad580a4db9872
zh: 1eb3bac59353291d
/agent-note-direct-deepseek-search-and-fetch-apis/decision:
en: e70efbb3aa9d53b6
zh: 1d25eae2461346df
/agent-note-direct-deepseek-search-and-fetch-apis/decision/protocol-selection-and-deployment:
en: ec653b59d62ed95f
zh: 0d675616059930ba
/agent-note-direct-deepseek-search-and-fetch-apis/decision/search-results-and-model-input:
en: a95618f172ef63ee
zh: 306556be09c56bdf
/agent-note-direct-deepseek-search-and-fetch-apis/decision/fetch-metadata-and-ownership:
en: 8051417b08739ed0
zh: c11d35a346014436
/agent-note-direct-deepseek-search-and-fetch-apis/decision/default-migration:
en: d5a8e36b545f1c04
zh: d741fd0789401c62
/agent-note-direct-deepseek-search-and-fetch-apis/decision/supplementary-messages-fix:
en: 738e32b488bbc1a1
zh: f33afe570f62599a
/agent-note-direct-deepseek-search-and-fetch-apis/alternatives-considered:
en: 641cbfefc76f2f95
zh: e3e718473eace85f
/agent-note-direct-deepseek-search-and-fetch-apis/consequences:
en: a55da02f4d3c06c0
zh: 3c1a624f03f34f28
/agent-note-direct-deepseek-search-and-fetch-apis/verification-and-remaining-api-adoption:
en: 41f9a331c7aca397
zh: 17a0fd53e133c2f5
@@ -0,0 +1,84 @@
# Agent Note: Direct DeepSeek Search and Fetch APIs
Status: implemented
English | [中文](2026-10-15-deepseek-search-fetch-api.zh.md)
## Problem
DeepSeek web retrieval through Messages requires an auxiliary model request for each query. Harness extracts sources and citation excerpts but discards the generated answer. The dedicated Search and Fetch APIs provide structured sources and Markdown directly, so retrieval can run without Harness starting that auxiliary generation. Adopting these APIs requires explicit selection of both operations, independent deployment addresses, and a mapping of Fetch results onto the existing fetch result.
## Decision
The [DeepSeek Search and Fetch guide](https://api-docs.deepseek.com/zh-cn/guides/search), checked on 2026-10-16, defines the upstream API. The [web-deepseek-api plugin](../../../../packages/web/web-deepseek-api/README.md) implements it behind the existing `web_search` and `web_fetch` tools, and [web-search-deepseek](../../../../packages/web/web-search-deepseek/README.md) keeps Messages search. The [web capability decision](../architecture/2026-06-24-web-capability-seam.md) continues to own provider selection, tool responsibilities, and the fetch result fields.
### Protocol selection and deployment
The APIs register as `deepseek-search-api` and `deepseek-fetch-api`; Messages search keeps `deepseek-official`. The setting **Use the new Web Search API** (**启用新版 Web 搜索 API**) on the Web search page writes `web.searchProvider` and `web.fetchProvider` together: on selects both APIs, and off restores Messages search with local HTTP fetch, the shipped selection. The switch defaults off and takes effect after saving, because the web service reads both selections for each operation. Messages remains the default until the Search API launch is announced, so existing users keep working search without configuration. Each operation resolves one settings snapshot, including its endpoint and credentials. Both APIs read one credential and one API base, because the guide serves them from one base and the switch selects them together.
The two protocols retain independent API bases. Messages keeps `https://api.deepseek.com/anthropic/v1` and appends `/messages`. Direct retrieval uses `web-deepseek-api.baseURL`, which defaults to the guide's public base `https://api.deepseek.com/v1/tools`, and appends `/search` or `/fetch`; the verified development base is `http://api-dev.deepseek.com:8031/api/v1/tools`. These prefixes are different and are never inferred from the hostname, so another deployment enters its complete base. The switch is already the explicit opt-in, so the documented public deployment serves as its default. A blank or unparseable base leaves both providers unavailable.
The guide specifies API-key Bearer authentication, and requests send only the documented authorization and JSON content type. A Session on the DeepSeek account route sends its account token instead, as Messages search does; the public Search and Fetch routes accepted it on 2026-10-16, so account-only users can enable direct retrieval. An Anthropic-compatible gateway checked the same day served Messages web search and had no `/v1/tools` routes; such deployments keep Messages search.
### Search results and model input
Direct Search sends `query`, `count`, `need_content: false`, and `ori_question`. `count` is the configured value from 1 to 50, defaulting to the service's 10, unless the request's result bound is smaller; the tool always sends its limit, 8 by default. `ori_question` is the text of the initiating Session's latest human prompt with whitespace collapsed and cut to the API's 128-character limit. The API rejects blank or longer values, so a search without such a prompt omits the field, and `sendOriginalQuestion: false` disables it. In 2026-10-16 probes, different values left result order, scores, and summaries unchanged. Queries pass through unchanged, including the documented site, language, date, exclusion, and exact-phrase operators. The service enforces the 1–128 Unicode-character query constraint. Empty `results` is successful.
The provider preserves service order and maps URL, title, `snippet`, and nullable `date` into shared sources. The current tool gives the same short excerpt to the model and search card. It does not consume `summary`, full `content`, `site_name`, `site_icon`, `rerank_score`, or `authority`, and it does not send `localization`. Those omissions are integration limits, not absent API capabilities.
Direct retrieval emits no auxiliary model-request event because Harness makes no such request. The normal tool-call and tool-result events retain the query and model-visible result. This says nothing about the search service's internal use of models. Returned text remains external, untrusted data.
### Fetch metadata and ownership
Fetch sends a URL and returns Markdown without an extra Harness summarization request. The API supplies neither a final redirected URL nor a target HTTP status, and the shared result has no title field. The provider keeps the requested URL, returns the Markdown body, and reports HTTP 200 for a successful response: the Fetch API answers 404 when the URL is unavailable, blocked by site policy, or returns an error, or when no Markdown can be extracted. On 2026-10-16 an unresolvable host returned HTTP 500 instead, so every non-success status is a retrieval failure rather than a specific target diagnosis. The shared fetch result and its consumers stay unchanged.
The HTTP provider retains its own public-address validation, connection pinning, and resource limits. Selecting DeepSeek delegates page retrieval and target access policy to that service; Harness rejects redirects of the API request and retains cancellation and bounded tool output. The reported HTTP 200 therefore means the service retrieved the page, not that Harness observed the target response. The guide's Fetch 403 means crawler-policy rejection, while 404 means no usable page content.
### Default migration
Changing the default to the Search API is a separate change after the launch is announced. API keys and account tokens both reach the public routes; deployments behind a gateway without `/v1/tools` routes need an explicit `web.searchProvider: deepseek-official` before the default changes, and a stored Messages selection keeps Messages.
### Supplementary Messages fix
The retained Messages path explicitly sends `thinking: { type: 'disabled' }` and records that same body before dispatch. This avoids reasoning work in an auxiliary request whose generated answer is discarded; it does not implement or control the new retrieval API. `tool_choice` remains omitted. The [persistence acknowledgement](../../../../docs/persistence-changes/2026-10-13-deepseek-search-thinking.md) owns compatibility for the optional logged field.
## Alternatives considered
**Keep Messages as the only search implementation.** It preserves auxiliary generation and response extraction even when callers need only sources. Direct Search removes that Harness model request while retaining Messages as an explicit mode.
**Make the Search API the default now.** The launch is not announced, and some gateways expose only Messages; their users would lose working search.
**Reuse the Messages address or infer a route prefix.** The protocols and deployed prefixes differ. Separate bases preserve existing configurations and avoid sending a valid request to the wrong route.
**Require an explicit Search API base.** Enabling the switch is already an explicit choice, and an empty default would make every user copy the documented public address. Other deployments still override it.
**Keep the APIs inside the Messages provider behind a provider-local switch.** A switch inside `web-search-deepseek` cannot change fetch routing, so turning it on left `web_fetch` on HTTP unless the user also edited `web.fetchProvider`; it also tied the APIs' lifecycle to Messages search, which the default migration may retire. Separate registrations let the web service's existing selection express the switch, and each package can change or retire alone.
**Share one transport between Messages search and the new APIs.** Both resolve credentials through the same seams, `ctx.credentials` and `ctx.deepseekAccount`, so a shared module would save only the dispatch and failure-mapping code. It would make the Messages package depend on the new API package and tie their releases, while the default migration may retire Messages search. Each package keeps its own transport until then.
**Split Search and Fetch into separate packages.** The guide serves both APIs from one base with one credential, and the switch selects them together; separate packages would duplicate the settings a user edits once. Each operation lives in its own module, so either API can change without the other.
**List every registered provider in a settings dropdown.** A generic provider form derives editors from Config schemas and needs host-side display metadata on the provider interfaces. The page needs only the DeepSeek choice, and other providers keep their own settings pages.
**Let the model supply `ori_question` as a tool argument.** The API defines the field as the user's original question, which the latest human prompt already records. A tool argument would change the model-facing schema shared by every search provider and invite paraphrase.
**Keep HTTP fetch when the switch turns on.** The switch adopts the new DeepSeek retrieval APIs, and leaving fetch on HTTP made Fetch API adoption depend on a hand-edited `web.fetchProvider`. HTTP fetch, with its own access policy, remains the selection whenever the switch is off, and configuration can still select either fetch provider alone.
**Add an extraction tool or widen the fetch result.** The API retrieves one concrete web page as Markdown, which fits the existing fetch operation. Its success already implies a retrieved page, and the Markdown body carries the page heading, so widening the result would change every fetch consumer for little model-visible gain.
## Consequences
Retrieval no longer requires auxiliary model tokens in direct mode, but its results still consume the main model's context. One Messages web search through a gateway on 2026-10-16 consumed 8,154 input and 828 output tokens for a single server-side search. The guide prices Search and Fetch independently at CNY 5 per 1,000 successful requests with results; result count does not change a request's price. Multiple queries remain multiple requests. The stated Claude Code and Codex promotion does not establish a Harness exemption. Search API mode also sends up to 128 characters of the user's prompt to the search service with each query unless `sendOriginalQuestion` is off.
Search and Fetch each have an account-wide 5 QPS limit, independent of model concurrency and shared across that account's API keys. Fetch also has a target-domain limit. The current tool can issue four queries concurrently and fails the batch if one query fails. The provider has no shared rate scheduler, automatic retries, or fallback; other Sessions or processes can exhaust the same account allowance.
## Verification and remaining API adoption
Provider tests cover request mapping, empty results, malformed responses, independent and default bases, account-token authentication, cancellation, and blank configuration. Settings tests and the [plugin configuration browser scenario](../../../../apps/web/tests/plugin-config.e2e.ts) cover staged saves, the default base, and switching both selections. The [web search replay](../../../../apps/web/tests/web-search-round.e2e.ts) and [Fetch Session replay](../../../../snapshots/session/deepseek-fetch/snapshot.yml) pin model-visible results. On 2026-10-16 the [real provider tests](../../../../packages/web/web-deepseek-api/tests/web-deepseek-api.e2e.ts) passed Search and Fetch against the public base, and a headless round on the shipped profile returned eight sources and fetched Markdown without an auxiliary request event. The account-token, gateway, and status observations above come from manual probes the same day.
The following capabilities remain outside the implemented integration:
- Separate model-facing `summary` from display `snippet`, with per-source and total output budgets and matching Session records. The guide's typical summary length is not a maximum: returned summaries measured 1,000–2,300 characters on 2026-10-16.
- Expose search syntax in provider-appropriate tool guidance and define a bounded localization input before forwarding it. API `localization` is an IP address, not an inferred city or UI language.
- Retain quality metadata before defining selection policies. `rerank_score` and `authority` describe service assessments, not guarantees of truth; a hard cutoff can discard useful niche sources.
- Add cancellation-aware scheduling and bounded retry ownership. The guide recommends waiting before retrying 408, 429, and 5xx; 402 requires funding first, and other 4xx require a changed request. Local pacing cannot enforce an account-wide quota across unrelated processes.
@@ -0,0 +1,84 @@
# Agent Note: 直接接入 DeepSeek Search 与 Fetch API
Status: implemented
[English](2026-10-15-deepseek-search-fetch-api.md) | 中文
## 问题
通过 Messages 获取 DeepSeek 网页搜索结果时,每个查询都需要一次辅助模型请求。Harness 提取来源与引用摘录,却丢弃生成的回答。独立 Search 和 Fetch API 直接提供结构化来源与 Markdown,使检索无需由 Harness 发起这次辅助生成。接入这些 API 需要明确选择两种操作的提供方、独立配置部署地址,并把 Fetch 结果映射到现有抓取结果。
## 决策
2026-10-16 核对的 [DeepSeek Search 与 Fetch 指南](https://api-docs.deepseek.com/zh-cn/guides/search)定义上游 API。[web-deepseek-api 插件](../../../../packages/web/web-deepseek-api/README.zh.md)通过现有 `web_search` 和 `web_fetch` 工具接入它,[web-search-deepseek](../../../../packages/web/web-search-deepseek/README.zh.md) 保留 Messages 搜索。[Web 能力决策](../architecture/2026-06-24-web-capability-seam.zh.md)继续负责提供方选择、工具职责和抓取结果字段。
### 协议选择与部署
两个 API 注册为 `deepseek-search-api` 和 `deepseek-fetch-api`;Messages 搜索保留 `deepseek-official`。网页搜索页上的设置 **Use the new Web Search API**(**启用新版 Web 搜索 API**)同时写入 `web.searchProvider` 和 `web.fetchProvider`:开启时选择两个 API,关闭时恢复内置选择,即 Messages 搜索与本地 HTTP 抓取。开关默认关闭,保存后生效,因为 Web 服务在每次操作时读取两项选择。在 Search API 正式发布公告之前,Messages 仍是默认值,已有用户无需配置即可继续使用搜索。每次操作解析一份设置快照,包含端点和凭据。两个 API 读取同一份凭据和同一个 API 基础地址,因为指南从同一基础地址提供它们,开关也同时选择它们。
两种协议分别保存 API 基础地址。Messages 保留 `https://api.deepseek.com/anthropic/v1`,追加 `/messages`。直接检索使用 `web-deepseek-api.baseURL`,默认值为指南中的公网地址 `https://api.deepseek.com/v1/tools`,追加 `/search` 或 `/fetch`;已验证的开发地址为 `http://api-dev.deepseek.com:8031/api/v1/tools`。两者前缀不同,程序不会根据主机名推断,其他部署需填写完整基础地址。开关本身已是显式启用,因此以文档中的公网部署作为默认值。基础地址为空白或无法解析时,两个提供方都不可用。
指南规定使用 API 密钥进行 Bearer 鉴权,请求只发送文档规定的鉴权与 JSON 内容类型请求头。DeepSeek 账号路由上的会话改为发送其账号 token,与 Messages 搜索一致;2026-10-16 公网 Search 与 Fetch 路由已接受该 token,因此只登录账号的用户也可以启用直接检索。同日检查的一个 Anthropic 兼容网关提供 Messages 网页搜索,但没有 `/v1/tools` 路由;此类部署继续使用 Messages 搜索。
### 搜索结果与模型输入
直接 Search 发送 `query`、`count`、`need_content: false` 和 `ori_question`。`count` 为 1 至 50 的配置值,默认采用服务的 10;请求的结果上限更小时以后者为准,工具总会发送其上限,默认为 8。`ori_question` 是发起会话中最近一条用户提示的文本,空白折叠后截取至 API 的 128 个字符上限。API 拒绝空白或更长的值,因此没有此类提示的搜索不发送该字段,`sendOriginalQuestion: false` 可将其关闭。2026-10-16 的探测中,不同取值未改变结果顺序、评分或摘要。查询原样传递,包括文档中的站点、语言、日期、排除和精确短语操作符。服务端执行查询长度为 1–128 个 Unicode 字符的约束。空 `results` 表示成功。
提供方保留服务端顺序,将 URL、标题、`snippet` 和可为 null 的 `date` 映射为共享来源。当前工具将同一段短摘要提供给模型和搜索卡片。它不使用 `summary`、完整 `content`、`site_name`、`site_icon`、`rerank_score` 或 `authority`,也不发送 `localization`。这些是当前接入的限制,并非 API 缺少这些能力。
直接检索不产生辅助模型请求事件,因为 Harness 没有发起该请求。普通工具调用与工具结果事件继续保留查询及模型可见的结果。这不代表搜索服务内部不使用模型。返回文本仍是外部不受信任的数据。
### 抓取元数据与职责
Fetch 发送 URL,返回 Markdown,无需 Harness 额外发起摘要请求。API 既不提供重定向后的最终 URL,也不提供目标 HTTP 状态,而共享结果没有标题字段。提供方保留请求 URL,返回 Markdown 正文,并对成功响应报告 HTTP 200:URL 不可用、被站点策略拦截、目标站点返回错误或无法提取 Markdown 时,Fetch API 会返回 404。2026-10-16 无法解析的主机实际返回 HTTP 500,因此任何非成功状态都视为抓取失败,而不是对目标的具体诊断。共享抓取结果及其消费方保持不变。
HTTP 提供方继续负责自身的公开地址校验、连接固定和资源限制。选择 DeepSeek 会将页面抓取与目标访问策略交给该服务;Harness 拒绝 API 请求重定向,并保留取消与工具输出限额。因此报告的 HTTP 200 表示服务已获取页面,而不是 Harness 观察到了目标响应。指南中的 Fetch 403 表示抓取器策略拒绝,404 则表示没有可用的页面内容。
### 默认值迁移
将默认值改为 Search API 是发布公告之后的另一项变更。API 密钥和账号 token 都能访问公网路由;位于没有 `/v1/tools` 路由的网关之后的部署,需要在默认值变更前显式设置 `web.searchProvider: deepseek-official`,已保存的 Messages 选择仍使用 Messages。
### 附带的 Messages 修复
保留的 Messages 路径显式发送 `thinking: { type: 'disabled' }`,并在发送前记录同一请求体。这避免了辅助请求在生成回答被丢弃的情况下执行思考;它不实现或控制新的检索 API。`tool_choice` 仍不传递。[持久化确认记录](../../../../docs/persistence-changes/2026-10-13-deepseek-search-thinking.zh.md)负责日志中可选字段的兼容性。
## 考虑过的替代方案
**仅保留 Messages 搜索实现。** 即使调用方只需要来源,这也保留了辅助生成和响应提取。直接 Search 去掉这次 Harness 模型请求,同时保留 Messages 供显式选择。
**立即将 Search API 设为默认值。** 尚未发布公告,部分网关也只提供 Messages;这些用户会失去可用的搜索。
**复用 Messages 地址或推断路由前缀。** 协议与部署前缀不同。分别保存基础地址可保留现有配置,避免把有效请求发往错误路由。
**要求显式填写 Search API 基础地址。** 开启开关已是显式选择,空默认值会迫使每位用户复制文档中的公网地址。其他部署仍可覆盖它。
**把两个 API 放在 Messages 提供方内部,由提供方自己的开关切换。** `web-search-deepseek` 内的开关无法改变抓取路由,因此开启后除非用户另外修改 `web.fetchProvider`,`web_fetch` 仍走 HTTP;它还把两个 API 的生命周期绑在 Messages 搜索上,而默认值迁移后 Messages 搜索可能被移除。独立注册让 Web 服务现有的选择来表达这个开关,每个包都可以单独修改或移除。
**让 Messages 搜索与新 API 共用一套传输层。** 两者都通过同样的 seam,即 `ctx.credentials` 和 `ctx.deepseekAccount` 解析凭据,共享模块只能省下请求派发与失败映射代码。它会让 Messages 包依赖新 API 包、两者必须一起发版,而默认值迁移后 Messages 搜索可能被移除。在此之前,两个包各自保留传输层。
**把 Search 和 Fetch 拆成两个包。** 指南从同一基础地址、以同一份凭据提供两个 API,开关也同时选择它们;拆成两个包会让用户只需编辑一次的设置重复出现。每种操作位于独立模块,任一 API 都可以单独修改。
**在设置页用下拉框列出所有已注册的提供方。** 通用提供方表单从 Config schema 推导编辑控件,还需要在提供方接口上增加宿主侧展示元数据。该页面只需要 DeepSeek 的选择,其他提供方保留各自的设置页。
**让模型以工具参数提供 `ori_question`。** API 将该字段定义为用户的原始问题,而最近一条用户提示已记录了这段文本。工具参数会改变所有搜索提供方共用的面向模型的 schema,并引入改写。
**开启开关时保留 HTTP 抓取。** 该开关用于启用 DeepSeek 新的检索 API,保留 HTTP 抓取会让使用 Fetch API 依赖手动修改 `web.fetchProvider`。开关关闭时,抓取仍选择带有自身访问策略的 HTTP 提供方,配置中也仍可单独选择任一抓取提供方。
**新增提取工具或扩展抓取结果。** API 以 Markdown 获取一个具体网页,符合现有抓取操作。其成功本身即表示已获取页面,Markdown 正文也带有页面标题,因此扩展结果会改动每个抓取消费方,而模型可见的收益很小。
## 影响
直接模式中的检索不再需要辅助模型 token,但结果仍消耗主模型上下文。2026-10-16 通过网关进行的一次 Messages 网页搜索,为一次服务端搜索消耗了 8,154 个输入 token 和 828 个输出 token。指南对 Search 和 Fetch 分别按每 1,000 次成功且有结果的请求收取 5 元;结果条数不影响单次价格。多个查询仍对应多次请求。文档中的 Claude Code 与 Codex 优惠不能作为 Harness 免收费用的依据。除非关闭 `sendOriginalQuestion`,Search API 模式还会随每个查询把用户提示的至多 128 个字符发送给搜索服务。
Search 和 Fetch 各有账号级 5 QPS 限制,独立于模型并发,同一账号的 API 密钥共享额度。Fetch 还有目标域名限制。当前工具可并发发送四个查询,任一查询失败会使整批失败。提供方没有共享限速调度器、自动重试或回退;其他会话或进程也会消耗同一账号的额度。
## 验证与尚未接入的 API 能力
提供方测试覆盖请求映射、空结果、格式错误的响应、独立地址与默认地址、账号 token 鉴权、取消与空白配置。设置测试及[插件配置浏览器场景](../../../../apps/web/tests/plugin-config.e2e.ts)覆盖暂存后保存、默认地址和同时切换两项选择。[网页搜索回放](../../../../apps/web/tests/web-search-round.e2e.ts)与 [Fetch Session 回放](../../../../snapshots/session/deepseek-fetch/snapshot.yml)固定模型可见结果。2026-10-16,[真实提供方测试](../../../../packages/web/web-deepseek-api/tests/web-deepseek-api.e2e.ts)在公网地址通过了 Search 与 Fetch 验证;在内置配置上运行的一轮 headless 对话返回了八个来源并抓取到 Markdown,且没有辅助请求事件。上文的账号 token、网关和状态码观察来自同日的手动探测。
以下能力尚未包含在已实现的接入中:
- 区分面向模型的 `summary` 与用于展示的 `snippet`,设置每个来源和总输出限额,并记录对应的 Session 内容。指南中的典型摘要长度不是最大值:2026-10-16 实测返回的摘要为 1,000–2,300 个字符。
- 在适用于提供方的工具说明中介绍搜索语法,先定义有界的本地化输入,再进行转发。API 的 `localization` 是 IP 地址,不能用推测的城市或界面语言代替。
- 先保留质量元数据,再定义筛选策略。`rerank_score` 与 `authority` 表示服务端评估,不保证内容真实;固定阈值可能丢弃有用的冷门来源。
- 增加支持取消的调度并明确有限重试的职责。指南建议对 408、429 和 5xx 等待后重试;402 需要先充值,其他 4xx 需要修改请求。本地节流无法跨无关进程执行账号级配额。
@@ -0,0 +1,19 @@
# Bilingual-pair consistency record for 2026-10-18-headless-auto-review.md (docs/i18n/README.md): per heading
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
# After editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-10-18-headless-auto-review.md
/agent-note-headless-caller-authority-for-auto-review:
en: 9be347ac387545f7
zh: b360764b7d22d1d6
/agent-note-headless-caller-authority-for-auto-review/problem:
en: 01dcd32fe547f90c
zh: f9f577c4d72ebdbe
/agent-note-headless-caller-authority-for-auto-review/decision:
en: 372c020d57c6186a
zh: 31c00bb5175930b1
/agent-note-headless-caller-authority-for-auto-review/alternatives-considered:
en: 7e7aefd55c59837c
zh: e119a25a2171d376
/agent-note-headless-caller-authority-for-auto-review/consequences:
en: e3c7afb422c73478
zh: 2acb65eaa03ad716
@@ -0,0 +1,33 @@
# Agent Note: Headless caller authority for Auto review
Status: implemented
English | [中文](2026-10-18-headless-auto-review.zh.md)
## Problem
Headless, SDK, and ACP admit tasks with `source.kind: 'user'`. Auto must recognize those tasks as instructions without requiring a browser RPC identity. The message role alone is insufficient: injected context also uses the user role but names its own source.
## Decision
The [experimental Auto layer](../../../../packages/experimental/auto-review/README.md#headless-supervision) supports explicit headless selection. Default profiles remain unchanged. This extends the Web-only scope of [per-tool Auto review](2026-08-28-auto-review.md); its risk classes, full-access execution, PTC exclusions, and lifecycle remain authoritative.
The reviewer assigns user-sourced text the `user-instruction` role, including existing history and tasks admitted before Auto was selected. The user may be a human or a supervising caller. Both can define the task and authorize an exact medium-risk action, target, and scope. The current child's creation prompt and direct-parent messages keep their subordinate role; inherited and unverified creation prompts remain facts. Files, constraints, checkpoints, images, and tool results do not acquire instruction authority. Input producers own the source attribution; task text never selects its source. The message types and persisted fields remain unchanged.
Headless applies an explicit `--approval-policy` after selecting the permission preset. Saved policy continues when both options are omitted on resume. Pass `--permission-mode auto --approval-policy never` to select unattended Auto explicitly. This retains the structured denial and avoids dependence on a human answerer. `ask` still follows [the approval fallback decision](2026-09-24-auto-review-user-approval-fallback.md); it does not add a CLI approval transport. Application shutdown preserves the saved Auto selection. Restoring it requires the integration even when selecting another preset; removing the plugin while the application remains active still migrates live Sessions to Full access.
The JSON projection includes existing durable error fields under its normal output bounds. A final denial carries `AutoReviewDeniedError` / `AUTO_REVIEW_DENIED` and optional reviewer detail; a technical or parsing failure carries `AutoReviewFailedError` / `AUTO_REVIEW_FAILED`. Neither executes the tool body. Completed agent execution does not imply that every requested action succeeded.
## Alternatives considered
**Reuse a browser RPC id.** A headless invocation is not a browser request. Faking that identity would couple authorization to a different transport and misdescribe the persisted source.
**Add a headless admission marker.** The existing user source already identifies task input. A transport-specific marker adds persisted metadata and reader rules without changing the required authority decision.
**Promote every user-role message.** Plugins also supply user-role context. The existing source discriminator distinguishes admitted user input from injected content.
**A live caller-approval channel.** Headless consumes stdin as one task. Approving a suspended call needs a bidirectional protocol with correlation, disconnect and cancellation handling, and an answerer lifecycle. A later invocation can instead supply scoped authorization and receive a fresh review, without claiming to approve the earlier call.
## Consequences
A supervisor can use Auto for implementation and gates, inspect structured denials, and resume with revision instructions. Native tools keep all supported tool bodies under review; direct Node effects inside PTC remain outside it. The reviewer remains a fallible model with full-access consequences, and the integration remains experimental and opt-in. Custom producers that use the user source are treated as the user; background context must use its own source.
@@ -0,0 +1,33 @@
# Agent Note: Headless 调用者对 Auto review 的授权
Status: implemented
[English](2026-10-18-headless-auto-review.md) | 中文
## Problem
Headless、SDK 和 ACP 以 `source.kind: 'user'` 接纳任务。Auto 必须将这些任务识别为指令,而不要求浏览器 RPC 身份。仅有消息角色不足以判断:注入上下文也使用 user role,但声明自己的 source。
## Decision
[实验性 Auto 层](../../../../packages/experimental/auto-review/README.zh.md#headless-supervision)支持显式 headless 选择。默认 profile 保持不变。这扩展了[逐工具 Auto review](2026-08-28-auto-review.zh.md)仅限 Web 的范围;其风险分类、完全访问执行、PTC 排除项和生命周期仍具权威性。
Reviewer 将 user-source 文本归为 `user-instruction`,包括已有历史和选择 Auto 前接纳的任务。用户可以是人或监督调用者;两者都可定义任务,并明确授权 medium 风险动作、准确目标和范围。当前 child 的创建 prompt 和直接父级消息保留从属角色;继承的创建 prompt 与无法验证的创建 prompt 仍是事实。文件、约束、checkpoint、图片和工具结果不会获得指令权威。输入生产者负责来源归属;任务文本不能选择自己的 source。消息类型和持久字段保持不变。
Headless 在选择权限预设后应用显式 `--approval-policy`。恢复时省略两个选项会延续已保存的策略。传入 `--permission-mode auto --approval-policy never` 可显式选择无人值守 Auto。这保留结构化拒绝,并避免依赖人工应答者。`ask` 仍遵循[审批兜底决策](2026-09-24-auto-review-user-approval-fallback.zh.md);它不添加 CLI 审批传输。应用关闭保留已保存的 Auto 选择。恢复时需要该 integration,即使要选择其他预设也是如此;应用仍运行时移除插件则仍将存活 Session 迁移到 Full access。
JSON 投影在正常输出限制内包含已有的持久错误字段。最终拒绝携带 `AutoReviewDeniedError` / `AUTO_REVIEW_DENIED` 和可选 reviewer 详情;技术或解析失败携带 `AutoReviewFailedError` / `AUTO_REVIEW_FAILED`。两者都不执行工具 body。Agent 执行完成不代表每个请求动作都成功。
## Alternatives considered
**复用浏览器 RPC id。** Headless 调用不是浏览器请求。伪造该身份会将授权耦合到另一种传输,并错误描述持久来源。
**添加 headless 入口标记。** 已有 user source 已标识任务输入。传输专属标记增加持久元数据和读取规则,却不改变所需的权威判断。
**提升每条 user-role 消息。** 插件也提供 user-role 上下文。已有 source 判别字段区分接纳的用户输入与注入内容。
**实时调用者审批通道。** Headless 将 stdin 作为一个任务消费。审批暂停的调用需要双向协议、请求关联、断连与取消处理,以及应答者生命周期。后续调用可以改为提供限定范围的授权并接受新审查,不声称批准了先前调用。
## Consequences
监督者可以使用 Auto 实现修改和运行检查、查看结构化拒绝,并通过修订指令恢复。原生工具让所有支持的工具 body 都经过审查;PTC 内的直接 Node 效果仍不在范围内。Reviewer 仍是可能出错的模型,执行具有完全访问权限的后果,integration 仍保持实验性与显式启用。使用 user source 的自定义生产者被视为用户;后台上下文必须使用自己的 source。
@@ -1,19 +0,0 @@
# Bilingual-pair consistency record for 2026-07-06-node-engine-floor.md (docs/i18n/README.md): per heading
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
# After editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-07-06-node-engine-floor.md
/agent-note-raise-the-node-lts-engine-floor-to-22-19:
en: 01f43f2b4c6bd4fa
zh: 7f34e5b3c1c55419
/agent-note-raise-the-node-lts-engine-floor-to-22-19/problem:
en: d7bf1b1699ce0dc8
zh: f10dfd0e4787bb44
/agent-note-raise-the-node-lts-engine-floor-to-22-19/decision:
en: 0051c0d8c1948f63
zh: cbab7aa0f53f7200
/agent-note-raise-the-node-lts-engine-floor-to-22-19/consequences:
en: 0b9fbe932d9a8b1f
zh: 642e19d06caecef2
/agent-note-raise-the-node-lts-engine-floor-to-22-19/alternatives-considered:
en: c433389b2064eaf9
zh: 57de2af812def1c4
@@ -9,8 +9,8 @@
en: 3ecedc74cceced3b
zh: f4708f1887d42c61
/agent-note-ci-failover-runbook-hosted-pools-in-house-pool/decision:
en: fbc8d8ef03350ac9
zh: 7fc81bf75eaab0b7
en: 4a76615718dbc8f0
zh: f46b885a11d5dd1d
/agent-note-ci-failover-runbook-hosted-pools-in-house-pool/decision/release-rehearsals-share-the-linux-switch:
en: 01e74068e0a96375
zh: aa811861ce30cb47
@@ -24,8 +24,8 @@
en: 2e3829fd72d6b1b0
zh: 563f0abade7974f9
/agent-note-ci-failover-runbook-hosted-pools-in-house-pool/capacity-during-failover:
en: 22611ed4316adc7c
zh: 29acd02b4a07313d
en: 3dffe12ecf2356c2
zh: 7e2385fcabab1398
/agent-note-ci-failover-runbook-hosted-pools-in-house-pool/capacity-during-failover/switch-back:
en: 63cf911019db8d2f
zh: 5eb3903b0ceb30db
@@ -10,7 +10,7 @@ The three required Linux worker jobs in [CI](../../../../.github/workflows/ci.ym
## Decision
The three primary Linux jobs (`node-24`, `node-24-coverage`, `node-24-consumers`), the three `node-compat` matrix entries, and `all-checks-passed` resolve through `DSH_CI_FAILOVER_LINUX`; the native Windows jobs resolve through `DSH_CI_FAILOVER_WINDOWS`. A platform switch does not redirect the other platform. Set to `selfhosted` by a repository writer, the applicable trusted jobs select `vm-backup` or `dsh-win-ci`; the `blacksmith` value routes the participating jobs per the [blacksmith failover leg note](2026-09-09-blacksmith-failover-leg.md); unset or any other value retains the workflow-defined hosted fallbacks. Node compatibility jobs require a same-repository, non-fork head and a non-Dependabot author, use isolated runtime setup, and retain the `ubuntu-latest` fallback under unset and non-special values; the blacksmith branch carries none of those predicates. Under the `selfhosted` value, Linux failover bounds snapshot concurrency and skips hosted package-cache restores. The verdict follows its workers so it does not remain queued on an unavailable hosted pool. Each switch is writer-manageable repository state, not a merge, so it works while checks are red. The `serial / linux (self-hosted standby)` and `serial / windows (self-hosted standby)` lanes re-prove the complete unsharded aggregates on master pushes.
The three primary Linux jobs (`node-24`, `node-24-coverage`, `node-24-consumers`), the two `node-compat` matrix entries, and `all-checks-passed` resolve through `DSH_CI_FAILOVER_LINUX`; the native Windows jobs resolve through `DSH_CI_FAILOVER_WINDOWS`. A platform switch does not redirect the other platform. Set to `selfhosted` by a repository writer, the applicable trusted jobs select `vm-backup` or `dsh-win-ci`; the `blacksmith` value routes the participating jobs per the [blacksmith failover leg note](2026-09-09-blacksmith-failover-leg.md); unset or any other value retains the workflow-defined hosted fallbacks. Node compatibility jobs require a same-repository, non-fork head and a non-Dependabot author, use isolated runtime setup, and retain the `ubuntu-latest` fallback under unset and non-special values; the blacksmith branch carries none of those predicates. Under the `selfhosted` value, Linux failover bounds snapshot concurrency and skips hosted package-cache restores. The verdict follows its workers so it does not remain queued on an unavailable hosted pool. Each switch is writer-manageable repository state, not a merge, so it works while checks are red. The `serial / linux (self-hosted standby)` and `serial / windows (self-hosted standby)` lanes re-prove the complete unsharded aggregates on master pushes.
The [superseded-CI cancellation policy](../../../../.github/workflows/ci.yml) governs master pushes and manual runs in the same workflow/ref group, including standby drills. Rapid master updates can starve a drill before it reaches a verdict. Use the latest completed standby verdict and check its age and commit before treating it as readiness evidence; a cancelled or merely scheduled run is not proof of readiness.
@@ -40,7 +40,7 @@ The two switches are independent: flip only the one whose platform is degraded.
## Capacity during failover
Capacity includes the master standby, main-CI jobs, and three release-rehearsal jobs for each eligible PR or master push while the Linux switch is set. Each trusted PR also adds three Node compatibility jobs at gate concurrency one, including the build-backed Node 22 leg and cold temporary runtime downloads. The release rehearsal workflows cancel superseded runs within each workflow/ref group under the [cancellation policy](../../../../.github/workflows/ci.yml); different refs can still add concurrent build, pack, and install load. Check current CPU, memory, disk, and queue pressure before extending self-hosted operation; extra registrations on this VM add scheduling slots, not machine resources. Do not infer spare capacity from the standby alone. When host resources permit extra registrations, use an org registration token (org Settings → Actions → Runners → New runner). Clone an existing runner directory **excluding its identity files** — `rsync -a --exclude '.runner*' --exclude '.credentials*' --exclude '_diag' --exclude '_work' <src>/ <dst>/` (the globs also catch `.runner_migrated`/`.credentials_migrated`, which GitHub writes on migrated runners and which equally trigger the already-configured refusal) — then run `config.sh` (copying `.runner`/`.credentials` verbatim makes it refuse with "already configured"), and **start the listener**: `sudo ./svc.sh install ubuntu && sudo ./svc.sh start`. Registration alone leaves the runner offline; a started service adds a scheduling slot, not CPU or memory.
Capacity includes the master standby, main-CI jobs, and three release-rehearsal jobs for each eligible PR or master push while the Linux switch is set. Each trusted PR also adds two Node compatibility jobs at gate concurrency one, including the build-backed Node 24.9 leg and cold temporary runtime downloads. The release rehearsal workflows cancel superseded runs within each workflow/ref group under the [cancellation policy](../../../../.github/workflows/ci.yml); different refs can still add concurrent build, pack, and install load. Check current CPU, memory, disk, and queue pressure before extending self-hosted operation; extra registrations on this VM add scheduling slots, not machine resources. Do not infer spare capacity from the standby alone. When host resources permit extra registrations, use an org registration token (org Settings → Actions → Runners → New runner). Clone an existing runner directory **excluding its identity files** — `rsync -a --exclude '.runner*' --exclude '.credentials*' --exclude '_diag' --exclude '_work' <src>/ <dst>/` (the globs also catch `.runner_migrated`/`.credentials_migrated`, which GitHub writes on migrated runners and which equally trigger the already-configured refusal) — then run `config.sh` (copying `.runner`/`.credentials` verbatim makes it refuse with "already configured"), and **start the listener**: `sudo ./svc.sh install ubuntu && sudo ./svc.sh start`. Registration alone leaves the runner offline; a started service adds a scheduling slot, not CPU or memory.
### Switch back
@@ -10,7 +10,7 @@ Status: implemented
## 决策
三个主要 Linux 作业(`node-24`、`node-24-coverage`、`node-24-consumers`)、三个 `node-compat` 矩阵条目和 `all-checks-passed` 通过 `DSH_CI_FAILOVER_LINUX` 解析;原生 Windows 作业通过 `DSH_CI_FAILOVER_WINDOWS` 解析。一个平台的开关不会重定向另一个平台。仓库写者将变量设为 `selfhosted` 时,适用的可信作业选择 `vm-backup` 或 `dsh-win-ci`;`blacksmith` 取值按 [blacksmith 故障切换支路笔记](2026-09-09-blacksmith-failover-leg.zh.md) 路由参与切换的作业;未设置或任何其它值保留工作流定义的托管回退。Node 兼容性作业要求同仓库且非 fork 的头部以及非 Dependabot 作者,使用隔离运行时设置,并在未设置与非特殊值下保留 `ubuntu-latest` 回退;blacksmith 分支不带上述任何条件。在 `selfhosted` 取值下,Linux 故障切换会限制快照并发,并跳过托管软件包缓存恢复。判定作业跟随工作作业,避免继续在不可用的托管池排队。每个开关都是写者可管理的仓库状态而非一次合并,因此在检查失败时仍然有效。`serial / linux (self-hosted standby)` 与 `serial / windows (self-hosted standby)` 通道在 master 推送上重新验证完整的未分片聚合流程。
三个主要 Linux 作业(`node-24`、`node-24-coverage`、`node-24-consumers`)、两个 `node-compat` 矩阵条目和 `all-checks-passed` 通过 `DSH_CI_FAILOVER_LINUX` 解析;原生 Windows 作业通过 `DSH_CI_FAILOVER_WINDOWS` 解析。一个平台的开关不会重定向另一个平台。仓库写者将变量设为 `selfhosted` 时,适用的可信作业选择 `vm-backup` 或 `dsh-win-ci`;`blacksmith` 取值按 [blacksmith 故障切换支路笔记](2026-09-09-blacksmith-failover-leg.zh.md) 路由参与切换的作业;未设置或任何其它值保留工作流定义的托管回退。Node 兼容性作业要求同仓库且非 fork 的头部以及非 Dependabot 作者,使用隔离运行时设置,并在未设置与非特殊值下保留 `ubuntu-latest` 回退;blacksmith 分支不带上述任何条件。在 `selfhosted` 取值下,Linux 故障切换会限制快照并发,并跳过托管软件包缓存恢复。判定作业跟随工作作业,避免继续在不可用的托管池排队。每个开关都是写者可管理的仓库状态而非一次合并,因此在检查失败时仍然有效。`serial / linux (self-hosted standby)` 与 `serial / windows (self-hosted standby)` 通道在 master 推送上重新验证完整的未分片聚合流程。
[被取代 CI 的取消策略](../../../../.github/workflows/ci.yml) 管理同一工作流/引用组内的 master 推送和手动运行,包括热备演练。master 快速更新可能让演练因反复被取消而始终无法得出结论。判断就绪状态时,使用最近一次已完成的热备结论,并核对其时间和提交;已取消或仅被调度的运行不构成就绪证据。
@@ -40,7 +40,7 @@ Status: implemented
## 切换期间的容量
Linux 开关启用期间,容量需覆盖 master 热备、主 CI 作业,以及每个符合条件的 PR 或 master 推送的三个发布演练作业。每个可信 PR 还会增加三个门禁并发度为一的 Node 兼容性作业,包括需要构建的 Node 22 条目和冷临时运行时下载。发布演练工作流依据[取消策略](../../../../.github/workflows/ci.yml)取消各工作流/引用组内被取代的运行;不同引用仍可能增加并发构建、打包和安装负载。延长自托管运行前,检查当前 CPU、内存、磁盘和队列压力;同一虚拟机上新增注册只增加调度槽位,不增加机器资源。不能只依据热备负载推断空闲容量。主机资源允许增加注册实例时,使用组织级注册 token(组织 Settings → Actions → Runners → New runner)。复制现有 runner 目录时**必须排除身份文件**——`rsync -a --exclude '.runner*' --exclude '.credentials*' --exclude '_diag' --exclude '_work' <src>/ <dst>/`(通配同时排除 `.runner_migrated`/`.credentials_migrated`——GitHub 会在迁移过的运行器上写入这些文件,它们同样会触发 already-configured 拒绝)——再跑 `config.sh`(原样拷贝 `.runner`/`.credentials` 会使其以 "already configured" 拒绝),然后**启动监听器**:`sudo ./svc.sh install ubuntu && sudo ./svc.sh start`。仅注册不会上线;启动服务增加的是调度槽位,而非 CPU 或内存。
Linux 开关启用期间,容量需覆盖 master 热备、主 CI 作业,以及每个符合条件的 PR 或 master 推送的三个发布演练作业。每个可信 PR 还会增加两个门禁并发度为一的 Node 兼容性作业,包括需要构建的 Node 24.9 条目和冷临时运行时下载。发布演练工作流依据[取消策略](../../../../.github/workflows/ci.yml)取消各工作流/引用组内被取代的运行;不同引用仍可能增加并发构建、打包和安装负载。延长自托管运行前,检查当前 CPU、内存、磁盘和队列压力;同一虚拟机上新增注册只增加调度槽位,不增加机器资源。不能只依据热备负载推断空闲容量。主机资源允许增加注册实例时,使用组织级注册 token(组织 Settings → Actions → Runners → New runner)。复制现有 runner 目录时**必须排除身份文件**——`rsync -a --exclude '.runner*' --exclude '.credentials*' --exclude '_diag' --exclude '_work' <src>/ <dst>/`(通配同时排除 `.runner_migrated`/`.credentials_migrated`——GitHub 会在迁移过的运行器上写入这些文件,它们同样会触发 already-configured 拒绝)——再跑 `config.sh`(原样拷贝 `.runner`/`.credentials` 会使其以 "already configured" 拒绝),然后**启动监听器**:`sudo ./svc.sh install ubuntu && sudo ./svc.sh start`。仅注册不会上线;启动服务增加的是调度槽位,而非 CPU 或内存。
### 切回
@@ -9,8 +9,8 @@
en: e5c984fa32cc5e69
zh: 25d5742b2106d830
/agent-note-pr-artifact-builds-and-aggregate-typescript-checks/decision:
en: af93d8849bb8831a
zh: 53e15e801ab844da
en: 26a903fdfeffcea7
zh: f6ea094ec32a7b14
/agent-note-pr-artifact-builds-and-aggregate-typescript-checks/alternatives-considered:
en: f61da066f9ddcd62
zh: 816fd6e0063017cd
@@ -10,7 +10,7 @@ PR jobs need the same compiled packages for benchmarks, compatibility, packaging
## Decision
Required Linux consumer and native Windows build jobs retain normal builds, including both complete aggregate programs. Public `build` and `typecheck` keep the same checks. [Node version ownership](2026-07-06-node-engine-floor.md) and [native Windows ownership](2026-08-08-native-windows-pull-request-ci.md) remain in force.
Required Linux consumer and native Windows build jobs retain normal builds, including both complete aggregate programs. Public `build` and `typecheck` keep the same checks. [Node version ownership](2026-10-15-node-24-runtime-floor.md) and [native Windows ownership](2026-08-08-native-windows-pull-request-ci.md) remain in force.
Checked PR artifact builds compile every existing project reference from each compiler face with one serial `tsc -b` process, then run the ordinary Typert, bundling, Desktop, and Web stages. The helper reads the existing Host and Client aggregates rather than maintaining another package list. It omits only each root's additional test and script program; package diagnostics, emitted outputs, and Host-before-Client ordering remain required. Non-PR release and real-API workflow builds retain their normal checks. That artifact build is `pnpm run build:artifacts`; [the build split](2026-10-11-build-artifacts-check-split.md) owns its steps and scheduling.
@@ -20,9 +20,9 @@ The Python executable builder selects `build:artifacts` artifact compilation wit
PR artifact builds in CI invoke the root manifest's `build:artifacts`; a PR that edits these root manifest scripts activates the separate native-addon platform matrix, a trade-off [the build split](2026-10-11-build-artifacts-check-split.md) records. The vendor rehearsal and benchmark lanes invoke the internal build helpers directly.
Job inventory, runner selectors, and benchmark sample counts stay fixed; gate and worker ceilings do not increase. The three compatibility cases run one job at a time; the two-target Python runtime matrix starts after the keyless Python SDK job succeeds. Four Linux lanes, the Python SDK, three Windows lanes, and one compatibility job permit at most nine worker jobs initially. Once the SDK job finishes, the remaining seven independent lanes, one compatibility job, and two runtime targets permit at most ten. The runtime plan and wheel stages precede those targets and each use one job. This permits runtime validation to overlap the Node 22 build within the original ten-job ceiling. Coverage partitions reuse compact file-cost metadata within their platform and runner pool. Parallel release packing uses pnpm's existing bounded scheduler; serial publication keeps family order.
Job inventory, runner selectors, and benchmark sample counts stay fixed; gate and worker ceilings do not increase. The two compatibility cases run one job at a time; the two-target Python runtime matrix starts after the keyless Python SDK job succeeds. Four Linux lanes, the Python SDK, three Windows lanes, and one compatibility job permit at most nine worker jobs initially. Once the SDK job finishes, the remaining seven independent lanes, one compatibility job, and two runtime targets permit at most ten. The runtime plan and wheel stages precede those targets and each use one job. This permits runtime validation to overlap the Node 24.9 build within the original ten-job ceiling. Coverage partitions reuse compact file-cost metadata within their platform and runner pool. Parallel release packing uses pnpm's existing bounded scheduler; serial publication keeps family order.
Each Node version runs the same seven source compatibility specs in one serial Vitest invocation. Both file parallelism and the CLI worker limit are fixed to one, and the gate sets `VITEST_MAX_WORKERS=1` because that environment variable overrides Vitest's CLI limit. Files retain fork isolation; the build-backed Node 22 lazy-search smoke keeps its separate build dependency. Three paired local samples preserve all 253 assertion outcomes and reduce startup-only median time from 11.52 to 8.69 seconds, without increasing owned descendant-process peak.
Each Node version runs the same eight source compatibility specs in one serial Vitest invocation. Both file parallelism and the CLI worker limit are fixed to one, and the gate sets `VITEST_MAX_WORKERS=1` because that environment variable overrides Vitest's CLI limit. Files retain fork isolation; the build-backed Node 24.9 lazy-search smoke keeps its separate build dependency. The original seven-spec paired samples preserve all 253 assertion outcomes and reduce startup-only median time from 11.52 to 8.69 seconds, without increasing owned descendant-process peak.
## Alternatives considered
@@ -10,7 +10,7 @@ PR job 需要相同的编译包来运行 benchmark、兼容性、打包和运行
## Decision
必需的 Linux consumer 与原生 Windows build job 保留普通构建,包括两个完整 aggregate program。公开的 `build` 和 `typecheck` 保留相同检查。[Node 版本职责](2026-07-06-node-engine-floor.zh.md)与[原生 Windows 职责](2026-08-08-native-windows-pull-request-ci.zh.md)仍然有效。
必需的 Linux consumer 与原生 Windows build job 保留普通构建,包括两个完整 aggregate program。公开的 `build` 和 `typecheck` 保留相同检查。[Node 版本职责](2026-10-15-node-24-runtime-floor.zh.md)与[原生 Windows 职责](2026-08-08-native-windows-pull-request-ci.zh.md)仍然有效。
带检查的 PR 产物构建通过一个串行 `tsc -b` 进程编译各 compiler face 的全部现有 project reference,然后运行普通 Typert、bundling、Desktop 和 Web 阶段。helper 读取现有 Host 与 Client aggregate,而不维护另一份包清单。它只省略各 root 附加的测试和脚本 program;包诊断、emit 输出及 Host 先于 Client 的顺序仍然必需。非 PR 的 release 和真实 API 工作流构建保留普通检查。该产物构建即 `pnpm run build:artifacts`,其步骤与调度见[构建拆分](2026-10-11-build-artifacts-check-split.zh.md)。
@@ -20,9 +20,9 @@ Python executable builder 在验证 runtime closure 后,通过 `--build-artifa
CI 的 PR 产物构建调用 root manifest 中的 `build:artifacts`;修改 root manifest 中这些 script 的 PR 会激活独立的 native-addon 平台矩阵,[构建拆分](2026-10-11-build-artifacts-check-split.zh.md)记录这一取舍。vendor 演练与 benchmark lane 直接调用内部 build helper。
job 清单、runner 选择和 benchmark sample 数量保持固定;gate 与 worker 上限不提高。三个 compatibility case 每次运行一个 job;双目标 Python runtime 矩阵在 keyless Python SDK job 成功后启动。四个 Linux lane、Python SDK、三个 Windows lane 和一个 compatibility job,初始最多占用九个 worker job。SDK job 完成后,剩余七个独立 lane、一个 compatibility job 和两个 runtime target,最多占用十个 job。runtime plan 与 wheel 阶段先于这些 target,各占一个 job。这允许 runtime 验证与 Node 22 构建重叠,同时保持原有十个 job 的上限。覆盖率分区在各自的平台和 runner pool 内复用精简的文件成本元数据。并行 release 打包使用 pnpm 现有的有界调度器;串行发布保持 family 顺序。
job 清单、runner 选择和 benchmark sample 数量保持固定;gate 与 worker 上限不提高。两个 compatibility case 每次运行一个 job;双目标 Python runtime 矩阵在 keyless Python SDK job 成功后启动。四个 Linux lane、Python SDK、三个 Windows lane 和一个 compatibility job,初始最多占用九个 worker job。SDK job 完成后,剩余七个独立 lane、一个 compatibility job 和两个 runtime target,最多占用十个 job。runtime plan 与 wheel 阶段先于这些 target,各占一个 job。这允许 runtime 验证与 Node 24.9 构建重叠,同时保持原有十个 job 的上限。覆盖率分区在各自的平台和 runner pool 内复用精简的文件成本元数据。并行 release 打包使用 pnpm 现有的有界调度器;串行发布保持 family 顺序。
每个 Node 版本在一次串行 Vitest 调用中运行相同的七个 source compatibility spec。文件并行和 CLI worker 上限均固定为一,gate 还设置 `VITEST_MAX_WORKERS=1`,因为该环境变量会覆盖 Vitest 的 CLI 上限。文件保持 fork 隔离;依赖构建的 Node 22 lazy-search smoke 保留独立的 build 依赖。三对本地样本保持全部 253 个 assertion outcome,将启动开销比较的 median 从 11.52 秒降至 8.69 秒,且不增加所拥有 descendant process 的峰值。
每个 Node 版本在一次串行 Vitest 调用中运行相同的八个 source compatibility spec。文件并行和 CLI worker 上限均固定为一,gate 还设置 `VITEST_MAX_WORKERS=1`,因为该环境变量会覆盖 Vitest 的 CLI 上限。文件保持 fork 隔离;依赖构建的 Node 24.9 lazy-search smoke 保留独立的 build 依赖。原来七个 spec 的成对样本保持全部 253 个 assertion outcome,将启动开销比较的 median 从 11.52 秒降至 8.69 秒,且不增加所拥有 descendant process 的峰值。
## Alternatives considered
@@ -12,23 +12,23 @@
en: a15c7c13f167e5ac
zh: 63bcc2ef60c2f344
/agent-note-split-build-artifacts-and-build-check-with-build-running-both-concurrently/decision/commands:
en: 174241330463841d
zh: 7fe361c07e634053
en: cc797cf688f8de64
zh: 5d997b686a435cc5
/agent-note-split-build-artifacts-and-build-check-with-build-running-both-concurrently/decision/steps-and-dependencies:
en: 43e78e255306cd43
zh: a741d5ef1c7379f3
en: c6c77bb1c7acf5e3
zh: d53c2358bef42502
/agent-note-split-build-artifacts-and-build-check-with-build-running-both-concurrently/decision/scheduling-failure-and-output:
en: 4b324f92307d3691
zh: e0cf4411a747f7e5
en: 6a1b4ed0d9910133
zh: f32c6882208ded71
/agent-note-split-build-artifacts-and-build-check-with-build-running-both-concurrently/decision/callers:
en: 55c55eea07fdb459
zh: ab78cdfa4d04a5e7
en: 895968956b86a9d2
zh: 5218d9ab8c60a53a
/agent-note-split-build-artifacts-and-build-check-with-build-running-both-concurrently/decision/measurements:
en: b3a753ac2aab6073
zh: 19cbd0e32751896f
/agent-note-split-build-artifacts-and-build-check-with-build-running-both-concurrently/decision/completeness-and-lossless-evidence:
en: 3cd40e0db9a2c89a
zh: 3f708be239779d0a
en: bed9a43f84716794
zh: 300ba712a7812a99
/agent-note-split-build-artifacts-and-build-check-with-build-running-both-concurrently/decision/intentional-trade-offs-not-lossless:
en: 14370bc5b54e1278
zh: 7ac933092d3e9954
@@ -36,5 +36,5 @@
en: 54905b912d17c74e
zh: 86a298004c8024e3
/agent-note-split-build-artifacts-and-build-check-with-build-running-both-concurrently/consequences:
en: 083467f68d0f03ed
zh: 93400e9994b8c645
en: 48a8cbb9e8fee654
zh: ece1aeaee843b48a
@@ -18,41 +18,25 @@ Each command does only its own job.
| Command | Responsibility | Steps | Scheduling | Client build record |
|---|---|---|---|---|
| `pnpm run build:artifacts` | Produces every artifact of `build`; package projects are fully type-checked as usual, the two aggregate root programs are not checked | native → host-src → host-bundle → desktop-bundle → client-src → client-bundle → web | One linear chain | Deleted at start, written after every step passes |
| `pnpm run build:check` | Type-checks only the two aggregate root programs | host-check, client-check | Both steps concurrent, no prerequisite steps | Neither read nor written |
| `pnpm run build:artifacts` | Produces every artifact of `build`; package projects are fully type-checked as usual, the Host and Client aggregate programs are not checked | [Artifact order](../../../../docs/development.md#typescript-project-layout) | Dependency order | Deleted at start, written after every step passes |
| `pnpm run build:check` | Type-checks only the Host and Client aggregate programs | host-check, client-check | Host and Client checks concurrent, no prerequisite steps | Neither read nor written |
| `pnpm run build` | Every step of `build:artifacts` and `build:check` | The dependency graph below | Concurrent through the run-gates scheduler | Deleted at start, written after every step passes |
`build:check` neither detects, rebuilds, nor validates `build:artifacts` outputs; when prerequisite outputs are missing or stale, the compiler's own diagnostics (for example TS6305 and TS2307) report it, and no command adds prerequisite checks, guards, or fallbacks. `build:artifacts` produces the same artifacts and record as `build` and replaces `--artifacts-only`; that flag and the `libraries` mode of `compile-referenced-projects.ts` are removed, and the Python executable builder uses `--build-artifacts`. `build:official` remains `tsx scripts/build.ts --profile official` and runs the complete `build`.
### Steps and dependencies
| Step | Command | `needs` in `build` |
|---|---|---|
| native | `pnpm run build:native-system` | — |
| host-src | `node --max-old-space-size=4096 <tsc> -b <references of tsconfig.host.json>` | native |
| host-bundle | `tsdown --config-loader native --env.DSH_BUILD_FACE host` (runs Typert) | host-src |
| desktop-bundle | `pnpm --filter @deepseek-ai/dsh-desktop run bundle` | host-bundle |
| client-src | `<tsc> -b <references of tsconfig.client.json>` | desktop-bundle |
| client-bundle | `tsdown --config-loader native --env.DSH_BUILD_FACE client` | client-src |
| web | `pnpm run build:web` | client-bundle |
| host-check | `node --max-old-space-size=4096 <tsc> -p tsconfig.host.json` | host-src |
| client-check | `<tsc> -p tsconfig.client.json` | client-src |
[Source-first ordering](2026-10-18-source-first-metadata-builds.md) prepares metadata before package compilation. [Development](../../../../docs/development.md#typescript-project-layout) and `buildGates()` own the current step graph; each aggregate check starts after its own package declarations exist.
```text
native → host-src ─┬─ host-check ───────────────────────────────────────────────────────┐
└─ host-bundle → desktop-bundle → client-src ─┬─ client-check ───────┤ → record
└─ client-bundle → web ┘
```
- client-src must follow host-bundle: Client packages import `@deepseek-ai/dsh-*/remote`, whose types `lib/typert.remote-client.d.ts` Typert writes, inside Host tsdown in these scopes and through `scripts/generate-typert.ts` in the typecheck scope. host-check reads no Typert output; clean serial builds have always finished the Host aggregate check before Host tsdown.
- The artifact chain keeps the two non-data edges native → host-src and desktop-bundle → client-src; only the two check steps attach to the chain in parallel.
- Client compilation reads generated Remote declarations from the source-only metadata pass. Desktop bundling remains before Client compilation; changing that non-data edge requires separate evidence.
- The artifact chain keeps the two non-data edges native → host-src and desktop-bundle → client-src; aggregate checks attach after their package compilation; native and metadata generation may run together.
- [`compile-referenced-projects.ts`](../../../../scripts/compile-referenced-projects.ts) owns the package-library step list `libraryBuildSteps`, the references argument construction `referencedProjectsCompilerArgs` (relative paths keep the command below Windows' command-line limit), and the aggregate-check arguments `aggregateCheckArgs`. The vendor release rehearsal (`host-libraries`) and CI benchmarks (`--noCheck`) run the same step list serially.
- The check steps use project mode (`tsc -p`): it reads the declarations that references already emitted and compiles no reference, writing incremental state to the aggregates' existing `tsconfig.{host,client}.tsbuildinfo`. `build` and `build:check` run the same command, so their diagnostics are identical.
### Scheduling, failure, and output
- [`scripts/build.ts`](../../../../scripts/build.ts) owns the step table and the three scopes and executes them through `runAggregate` from [`scripts/run-gates.ts`](../../../../scripts/run-gates.ts): each step is a gate with `needs`, and shell-free spawning, per-step buffered output, fail-fast process-tree termination, Windows `taskkill`, and host signal forwarding come from the run-gates implementation. `build:artifacts` is `tsx scripts/build.ts --scope artifacts`, `build:check` is `--scope check`, and `build` omits `--scope` and runs both as one graph; `--profile` applies only to artifact scopes, and combining it with `--scope check` fails argument parsing.
- The concurrency limit is `DSH_GATE_CONCURRENCY`, defaulting to the available CPU count; the dependency graph is at most 3 wide (client-check and client-bundle can start while host-check is still running). Master serial reference lanes set `DSH_GATE_CONCURRENCY=1`, so `build` runs serially in topological order there, consistent with the [serial reference decision](2026-07-21-serial-cross-platform-ci-reference.md) and without workflow edits.
- The concurrency limit is `DSH_GATE_CONCURRENCY`, using the repository worker budget by default; aggregate checks may overlap later artifact steps. Master serial reference lanes set `DSH_GATE_CONCURRENCY=1`, so `build` runs serially in topological order there, consistent with the [serial reference decision](2026-07-21-serial-cross-platform-ci-reference.md) and without workflow edits.
- By default (locally), every dependent of a failed step is recorded as skipped, while running steps and steps unrelated to the failure run to completion; the executed set depends only on step outcomes, so one run reports every independent type error. With `DSH_GATE_FAIL_FAST=1` (set in every building PR lane), the first failure terminates the process trees of the other running steps and records every unfinished step as skipped.
- A failed step prints `== FAILED <step> ==`, its command, the exit reason, and the step's complete buffered output, and the final summary lists failed and skipped steps; a passing step prints only its PASS line, and `DSH_GATE_VERBOSE=1` prints all output. The exit code is 1 when any step failed or was skipped, and the client build record is not written. Every step inherits the public Client environment selected by `clientBuildProcessEnvironment`.
- In CI, `build` itself is one gate of an outer run-gates aggregate; outer fail-fast termination propagates to the inner scheduler. `check:node-compat` and `check:ci:lint:contracts-ready` already nest the same way.
@@ -63,11 +47,11 @@ native → host-src ─┬─ host-check ─────────────
|---|---|---|
| `build`, `build:official`, `Makefile` `build`, `scripts/publish-npm-baseline.ts`, `apps/desktop/scripts/package-target.ts`, `scripts/dev-bundle-registry.ts`, `release-publish.yml`, `sandbox.yml`, and the non-PR branches of `e2e.yml` and `release.yml` | `build` / `build:official` | Yes |
| `test:web`, `test:web:refresh`, `test:web:perf`, `test:web:stress`, `apps/desktop` `test:updates:local`, `test:bench` / `build:bench` | `build`; `build:bench` runs `build:native-system`, `build:lib`, and the benchmark tsdown | Yes |
| `typecheck` (run by the pre-push hook) | `build.ts --scope typecheck`: host-src beside host-typert (the bundle-free, source-only Typert pass `generate-typert.ts`) → client-src, with `tsc -b tsconfig.{host,client}.json` after each face's package compilation; no bundle | Yes |
| `typecheck` (run by the pre-push hook) | `build.ts --scope typecheck`: host-typert (the source-only Typert pass `generate-typert.ts`) → package compilation, with `tsc -b tsconfig.{host,client}.json` after each face's package compilation; no bundle | Yes |
| `dev:web` ([`scripts/dev-web.ts`](../../../../scripts/dev-web.ts)), `dev:desktop` (`apps/desktop/scripts/dev.ts`) | `build:artifacts` | None before launch; after `dev:web` launches, its `tsc -b tsconfig.client.json --watch` keeps checking the Client aggregate root program |
| Prerequisite step of `lint`, `lint:fix`, `doc-typecheck`, and the CI Typert contracts gate | Host package compilation beside the bundle-free Typert pass (`compile-referenced-projects.ts host-contracts`), without any bundle | None |
| Prerequisite step of `lint`, `lint:fix`, `doc-typecheck`, and the CI Typert contracts gate | Source-only Typert generation before Host package compilation (`compile-referenced-projects.ts host-contracts`), without any bundle | None |
| CI node 24 / snapshots and artifacts (`check:ci:consumers`), windows node 24 / build & observational (`check:ci:windows-blocking`) | `build` | Yes, the two required lanes |
| CI node-compat 22.19 | `build:artifacts` (through run-gates) | None |
| CI node-compat 24.9 | `build:artifacts` (through run-gates) | None |
| CI python runtime | `build-exe-for-python-sdk.ts --build-artifacts` | None |
| PR rehearsals in `e2e.yml` and `release.yml` | `pnpm run build:artifacts --profile official` | None |
| `build-preview-cloudflare.yml` | `pnpm run build:artifacts` | None |
@@ -114,7 +98,7 @@ The serial branch sample overlapped other work on the host (742 s of foreign CPU
| Package source type error | host-src / client-src | Not reported (reads existing declarations) | host-src / client-src | Reported | Reported |
| Type error only in an aggregate root program's tests, scripts, benchmarks, or website | Not reported | host-check / client-check | host-check / client-check | Reported | consumers, windows-build |
| Package tsconfig configuration error | host-src / client-src | Not reported (`tsc -p` does not validate referenced projects' tsconfig options or syntax) | host-src / client-src | Reported | Reported |
| Typert analysis error | host-bundle | Not reported | host-bundle | Reported (through host-typert) | Reported |
| Typert analysis error | host-typert | Not reported | host-typert | Reported (through host-typert) | Reported |
| Missing prerequisite output | — | Compiler diagnostic (TS6305, TS2307) | — | — | — |
Negative controls: after a type error (TS2322) is added to `scripts/pnpm-invocation.spec.ts`, which only the Host aggregate root program contains, `build:artifacts` exits 0; `build:check` exits 1 with the Host aggregate check failing and the Client aggregate check passing; `build` exits 1 with 8 of 9 steps passing and 1 failing, and writes no client build record. In a scratch fixture whose referenced project's tsconfig has an invalid `target`, a JSON syntax error, or an unknown compiler option, `tsc -p` exits 0, while `tsc -b` reports TS6046, TS5092 with TS1012, and TS5023 respectively and exits 1; when the referenced project lacks `composite`, both report TS6306.
@@ -150,8 +134,8 @@ CI evidence: on PR #6005 both required lanes that run the concurrent `pnpm run b
Development iterations can skip the two aggregate root programs with `build:artifacts` (51.6% faster cold than master's `build`) and complete the checks with `build:check` or `build` before declaring work complete. `build` stays complete: the Host aggregate check is off the critical path, the Client aggregate check runs alongside the Client bundle and web, and a cold build is 46.7% faster than master (including the lossless speed-ups note's changes; medians of 2). The costs are a higher memory peak, a little extra CPU from concurrent contention, less passing output by default, and several development and prerequisite commands that do not check test types (see the trade-offs above).
Removing or changing `build` in the two required lanes, consumers and windows-build, requires first transferring both aggregate checks to another blocking owner. The typecheck scope and the lint prerequisites generate Typert contracts without bundling ([local gate speed-ups](2026-10-12-local-gate-speedups.md)); the artifact scopes keep generating them inside Host tsdown.
Removing or changing `build` in the two required lanes, consumers and windows-build, requires first transferring both aggregate checks to another blocking owner. The typecheck scope and the lint prerequisites generate Typert contracts without bundling ([local gate speed-ups](2026-10-12-local-gate-speedups.md)); artifact scopes use [source-first metadata ordering](2026-10-18-source-first-metadata-builds.md).
client-src's dependency on host-bundle guarantees the "Host and Remote artifacts complete before Client compilation" ordering that [Typert Remote method calls](../architecture/2026-08-02-typert-remote-method-calls.md) describes.
host-src depends on host-typert, so generated Remote declarations precede package compilation. [Typert Remote method calls](../architecture/2026-08-02-typert-remote-method-calls.md) describes the emitted contributions.
Verification: `scripts/build.spec.ts` covers the type-stripping check, rejection of the removed flag and invalid scopes, the conflict between `--profile` and `--scope check`, scope selection, step dependencies, and the environment; `compile-referenced-projects.spec.ts` covers project mode checking only the aggregate program, the diagnostic for missing declarations, and `tsc -b --dry` accepting the tsbuildinfo that `tsc -p` writes; failure propagation, output buffering, and process-tree termination come from the run-gates scheduler and its tests; `run-gates.spec.ts`, `ci-workflow.spec.ts`, `preview-workflow.spec.ts`, `build-exe-for-python-sdk.spec.ts`, `dev-web.spec.ts`, and `tests/ci-release-selfhosted.spec.ts` pin each caller's command.
@@ -18,41 +18,25 @@ Status: implemented
| 命令 | 职责 | 步骤 | 调度 | client build record |
|---|---|---|---|---|
| `pnpm run build:artifacts` | 产出 `build` 的全部产物;包 project 照常完整类型检查,不检查两个聚合根程序 | native → host-src → host-bundle → desktop-bundle → client-src → client-bundle → web | 一条线性链 | 开始时删除,全部通过后写入 |
| `pnpm run build:check` | 只类型检查两个聚合根程序 | host-check、client-check | 两步并发,无前置步骤 | 不读写 |
| `pnpm run build:artifacts` | 产出 `build` 的全部产物;包 project 照常完整类型检查,不检查Host 与 Client 聚合程序 | [产物顺序](../../../../docs/development.zh.md#typescript-project-layout) | 依赖顺序 | 开始时删除,全部通过后写入 |
| `pnpm run build:check` | 只类型检查Host 与 Client 聚合程序 | host-check、client-check | Host 与 Client 检查并发,无前置步骤 | 不读写 |
| `pnpm run build` | `build:artifacts` 与 `build:check` 的全部步骤 | 下文依赖图 | run-gates 调度器并发执行 | 开始时删除,全部通过后写入 |
`build:check` 不检测、不重建、也不校验 `build:artifacts` 的产物;前置产物缺失或过期时,由编译器自身的诊断(例如 TS6305、TS2307)报告,任何命令都不增加前置检查、守卫或回退逻辑。`build:artifacts` 产出的产物与 record 和 `build` 相同,并取代 `--artifacts-only`;该参数与 `compile-referenced-projects.ts` 的 `libraries` 模式已删除,Python executable builder 使用 `--build-artifacts`。`build:official` 仍是 `tsx scripts/build.ts --profile official`,执行完整的 `build`。
### 步骤与依赖
| 步骤 | 命令 | `build` 中的 `needs` |
|---|---|---|
| native | `pnpm run build:native-system` | — |
| host-src | `node --max-old-space-size=4096 <tsc> -b <tsconfig.host.json 的 references>` | native |
| host-bundle | `tsdown --config-loader native --env.DSH_BUILD_FACE host`(运行 Typert) | host-src |
| desktop-bundle | `pnpm --filter @deepseek-ai/dsh-desktop run bundle` | host-bundle |
| client-src | `<tsc> -b <tsconfig.client.json 的 references>` | desktop-bundle |
| client-bundle | `tsdown --config-loader native --env.DSH_BUILD_FACE client` | client-src |
| web | `pnpm run build:web` | client-bundle |
| host-check | `node --max-old-space-size=4096 <tsc> -p tsconfig.host.json` | host-src |
| client-check | `<tsc> -p tsconfig.client.json` | client-src |
[先生成顺序](2026-10-18-source-first-metadata-builds.zh.md)在包编译前准备元数据。[开发指南](../../../../docs/development.zh.md#typescript-project-layout)与 `buildGates()` 负责当前步骤图;每个 aggregate 检查在自己的包声明就绪后开始。
```text
native → host-src ─┬─ host-check ───────────────────────────────────────────────────────┐
└─ host-bundle → desktop-bundle → client-src ─┬─ client-check ───────┤ → record
└─ client-bundle → web ┘
```
- client-src 必须在 host-bundle 之后:Client 包导入 `@deepseek-ai/dsh-*/remote`,其类型 `lib/typert.remote-client.d.ts` 由 Typert 写出:这些 scope 在 Host tsdown 中生成,typecheck scope 经 `scripts/generate-typert.ts` 生成。host-check 不读取 Typert 输出,干净的串行构建一直在 Host tsdown 之前完成 Host 聚合检查。
- 产物链保留 native → host-src 与 desktop-bundle → client-src 这两条非数据依赖;只有两个 check 步骤并行挂接到链上。
- Client 编译读取仅源码元数据阶段生成的 Remote 声明。Desktop 打包仍在 Client 编译之前;改变该非数据依赖需要单独的证据。
- 产物链保留 native → host-src 与 desktop-bundle → client-src 这两条非数据依赖;aggregate 检查在包编译后挂接;native 与元数据生成可以并行。
- [`compile-referenced-projects.ts`](../../../../scripts/compile-referenced-projects.ts) 拥有包库步骤列表 `libraryBuildSteps`、references 参数构造 `referencedProjectsCompilerArgs`(相对路径传参,避开 Windows 命令行长度上限)与聚合检查参数 `aggregateCheckArgs`。vendor 发布演练(`host-libraries`)与 CI benchmark(`--noCheck`)串行执行同一步骤列表。
- check 步骤使用 project mode(`tsc -p`):它读取 references 已有的声明而不编译任何 reference,incremental 状态写入聚合配置已有的 `tsconfig.{host,client}.tsbuildinfo`。`build` 与 `build:check` 使用同一条命令,因此诊断相同。
### 调度、失败与输出
- [`scripts/build.ts`](../../../../scripts/build.ts) 拥有步骤表与三个范围,执行复用 [`scripts/run-gates.ts`](../../../../scripts/run-gates.ts) 的 `runAggregate`:每个步骤是一个带 `needs` 的 gate,shell-free 启动、按步骤缓冲输出、fail-fast 进程树终止、Windows `taskkill` 与宿主信号转发都沿用 run-gates 的实现。`build:artifacts` 为 `tsx scripts/build.ts --scope artifacts`,`build:check` 为 `--scope check`,`build` 不带 `--scope`,把两者作为一张图执行;`--profile` 只适用于产生产物的范围,与 `--scope check` 同用时参数解析失败。
- 并发上限取 `DSH_GATE_CONCURRENCY`,未设置时为可用 CPU 数;依赖图宽度最多为 3(host-check 尚未结束时,client-check 与 client-bundle 可能已开始)。master 串行参考 lane 设置 `DSH_GATE_CONCURRENCY=1`,`build` 在那里按拓扑顺序串行执行,符合[串行参考决策](2026-07-21-serial-cross-platform-ci-reference.zh.md),无需修改 workflow。
- 并发上限取 `DSH_GATE_CONCURRENCY`,未设置时采用仓库 worker 预算;aggregate 检查可与后续产物步骤重叠。master 串行参考 lane 设置 `DSH_GATE_CONCURRENCY=1`,`build` 在那里按拓扑顺序串行执行,符合[串行参考决策](2026-07-21-serial-cross-platform-ci-reference.zh.md),无需修改 workflow。
- 缺省(本地)时,失败步骤的所有依赖方记为 skipped,正在运行的步骤和与失败无关的步骤继续运行到结束;执行集合只取决于各步骤的结果,因此一次运行报告所有相互独立的类型错误。`DSH_GATE_FAIL_FAST=1`(所有构建型 PR lane 已设置)时,第一个失败终止其余正在运行步骤的进程树,所有未完成步骤记为 skipped。
- 失败步骤打印 `== FAILED <步骤> ==`、命令、退出原因与该步骤完整的缓冲输出,结尾汇总列出 failed 与 skipped 步骤;成功步骤只打印 PASS 行,`DSH_GATE_VERBOSE=1` 打印全部输出。任一步骤 failed 或 skipped 时退出码为 1,且不写 client build record。所有步骤继承 `clientBuildProcessEnvironment` 选定的公开 Client 环境。
- CI 中 `build` 本身是外层 run-gates 的一个 gate;外层 fail-fast 终止会转发到内层调度。`check:node-compat` 与 `check:ci:lint:contracts-ready` 已使用同样的嵌套。
@@ -63,11 +47,11 @@ native → host-src ─┬─ host-check ─────────────
|---|---|---|
| `build`、`build:official`、`Makefile` `build`、`scripts/publish-npm-baseline.ts`、`apps/desktop/scripts/package-target.ts`、`scripts/dev-bundle-registry.ts`、`release-publish.yml`、`sandbox.yml`、`e2e.yml` 与 `release.yml` 的非 PR 分支 | `build` / `build:official` | 有 |
| `test:web`、`test:web:refresh`、`test:web:perf`、`test:web:stress`、`apps/desktop` `test:updates:local`、`test:bench` / `build:bench` | `build`;`build:bench` 运行 `build:native-system`、`build:lib` 与 benchmark tsdown | 有 |
| `typecheck`(pre-push hook 运行它) | `build.ts --scope typecheck`:host-src 与 host-typert(不打包、只读源码的 Typert 生成 `generate-typert.ts`)并行 → client-src,各 face 包编译后运行 `tsc -b tsconfig.{host,client}.json`;不含任何 bundle | 有 |
| `typecheck`(pre-push hook 运行它) | `build.ts --scope typecheck`:host-typert(只读源码的 Typert 生成 `generate-typert.ts`)→ 包编译,各 face 包编译后运行 `tsc -b tsconfig.{host,client}.json`;不含任何 bundle | 有 |
| `dev:web`([`scripts/dev-web.ts`](../../../../scripts/dev-web.ts))、`dev:desktop`(`apps/desktop/scripts/dev.ts`) | `build:artifacts` | 启动前无;`dev:web` 启动后的 `tsc -b tsconfig.client.json --watch` 持续检查 Client 聚合根程序 |
| `lint`、`lint:fix`、`doc-typecheck` 与 CI 的 Typert contracts gate 的前置步骤 | Host 包编译与不打包的 Typert 生成并行(`compile-referenced-projects.ts host-contracts`),不含任何 bundle | 无 |
| `lint`、`lint:fix`、`doc-typecheck` 与 CI 的 Typert contracts gate 的前置步骤 | 仅源码 Typert 生成后进行 Host 包编译(`compile-referenced-projects.ts host-contracts`),不含任何 bundle | 无 |
| CI node 24 / snapshots and artifacts(`check:ci:consumers`)、windows node 24 / build & observational(`check:ci:windows-blocking`) | `build` | 有,两个必需 lane |
| CI node-compat 22.19 | `build:artifacts`(经 run-gates) | 无 |
| CI node-compat 24.9 | `build:artifacts`(经 run-gates) | 无 |
| CI python runtime | `build-exe-for-python-sdk.ts --build-artifacts` | 无 |
| `e2e.yml`、`release.yml` 的 PR 演练 | `pnpm run build:artifacts --profile official` | 无 |
| `build-preview-cloudflare.yml` | `pnpm run build:artifacts` | 无 |
@@ -114,7 +98,7 @@ wall 中位数从 249.4 s 降到 172.1 s(−77.3 s,−31%)。各步骤单
| 包源码类型错误 | host-src / client-src | 不报告(读取已有声明) | host-src / client-src | 报告 | 报告 |
| 只属于聚合根程序的测试、脚本、benchmark、website 类型错误 | 不报告 | host-check / client-check | host-check / client-check | 报告 | consumers、windows-build |
| 包 tsconfig 配置错误 | host-src / client-src | 不报告(`tsc -p` 不校验被引用 project 的 tsconfig 选项与语法) | host-src / client-src | 报告 | 报告 |
| Typert 分析错误 | host-bundle | 不报告 | host-bundle | 报告(经 host-typert) | 报告 |
| Typert 分析错误 | host-typert | 不报告 | host-typert | 报告(经 host-typert) | 报告 |
| 前置产物缺失 | — | 编译器诊断(TS6305、TS2307) | — | — | — |
负对照:在只属于 Host 聚合根程序的 `scripts/pnpm-invocation.spec.ts` 中加入类型错误(TS2322)后,`build:artifacts` 以 0 退出;`build:check` 以 1 退出,其中 Host 聚合检查失败、Client 聚合检查通过;`build` 以 1 退出,9 个步骤中 8 个通过、1 个失败,且不写 client build record。在临时 fixture 中,被引用 project 的 tsconfig 含无效 `target`、JSON 语法错误或未知编译选项时,`tsc -p` 以 0 退出,`tsc -b` 分别报告 TS6046、TS5092 与 TS1012、TS5023 并以 1 退出;被引用 project 缺少 `composite` 时,两者都报告 TS6306。
@@ -150,8 +134,8 @@ CI 证据:PR #6005 上,两个运行并发 `pnpm run build` 的必需 lane
开发迭代可以用 `build:artifacts` 跳过两个聚合根程序(冷启动比 master 的 `build` 快 51.6%),在声明完成前用 `build:check` 或 `build` 补齐检查。`build` 保持完整:Host 聚合检查不在关键路径上,Client 聚合检查与 Client bundle、web 并行,冷启动比 master 快 46.7%(含无损提速 Note 的改动,2 个样本的中位数)。代价是更高的内存峰值、并发争用带来的少量额外 CPU、默认更少的成功输出,以及多个开发与前置命令不检查测试类型(见上文取舍)。
删除或改变 consumers 与 windows-build 两个必需 lane 的 `build`,必须先把两个聚合检查转交另一处阻塞型 owner。typecheck scope 与 lint 类前置步骤不打包即生成 Typert 约定([本地门禁提速](2026-10-12-local-gate-speedups.zh.md));产物 scope 仍在 Host tsdown 中生成。
删除或改变 consumers 与 windows-build 两个必需 lane 的 `build`,必须先把两个聚合检查转交另一处阻塞型 owner。typecheck scope 与 lint 类前置步骤不打包即生成 Typert 约定([本地门禁提速](2026-10-12-local-gate-speedups.zh.md));产物 scope 采用[先生成元数据的顺序](2026-10-18-source-first-metadata-builds.zh.md)。
[Typert Remote 方法调用](../architecture/2026-08-02-typert-remote-method-calls.zh.md)描述的“Client 编译前先完成 Host 与 Remote 产物”由 client-src 对 host-bundle 的依赖保证。
host-src 依赖 host-typert,因此生成的 Remote 声明先于包编译。[Typert Remote 方法调用](../architecture/2026-08-02-typert-remote-method-calls.zh.md)说明生成的贡献。
验证:`scripts/build.spec.ts` 覆盖类型剥离检查、已删除参数与非法范围的拒绝、`--profile` 与 `--scope check` 的冲突、范围选择、步骤依赖与环境;`compile-referenced-projects.spec.ts` 覆盖 project mode 只检查聚合程序、缺失声明的诊断,以及 `tsc -b --dry` 接受 `tsc -p` 写出的 tsbuildinfo;失败传播、输出缓冲与进程树终止沿用 run-gates 的调度实现及其测试;`run-gates.spec.ts`、`ci-workflow.spec.ts`、`preview-workflow.spec.ts`、`build-exe-for-python-sdk.spec.ts`、`dev-web.spec.ts` 与 `tests/ci-release-selfhosted.spec.ts` 固定各调用方的命令。
@@ -27,8 +27,8 @@
en: cd6e05c61a7cc495
zh: 480d2d85d78d2d7f
/agent-note-build-speed-ups-that-keep-outputs-unchanged/decision/5-the-typert-workspace-root-also-requires-packages:
en: 8b89436df201141c
zh: 2fab82419c8ba704
en: b2bea05b59abe4b3
zh: 1e3304dec8a77146
/agent-note-build-speed-ups-that-keep-outputs-unchanged/decision/6-typert-generates-in-a-worker-thread:
en: 0362483a79c07881
zh: d84a2390b49c4012
@@ -39,8 +39,8 @@
en: 80c2ddc5fdff0b5f
zh: f5ce309a2e88ff4f
/agent-note-build-speed-ups-that-keep-outputs-unchanged/alternatives-considered:
en: 4372c82ef0c944dd
zh: 5fb70d499baadea7
en: 8def70ac09c5250c
zh: 5385aa58fc4b0b3b
/agent-note-build-speed-ups-that-keep-outputs-unchanged/consequences:
en: 93a96d3ed398f348
zh: 93b7c7405be981a6
@@ -84,7 +84,7 @@ All numbers come from one shared Linux host (256 vCPU, 4 TB memory, load average
- **Purpose:** Correct workspace-root resolution. 24 dual-face packages carry their own `tsconfig.host.json` leaf; searching upward for `tsconfig.host.json` alone took each of them as a root, so workspace mode ran one empty discovery per such package per build, and `package` mode silently produced no artifacts for dual-face packages.
- **Change:** The workspace root is the nearest ancestor of the bundle output that contains both `tsconfig.host.json` and `packages/`.
- **Performance:** Host tsdown 14.82 s → 15.39 s (medians of 3), within noise.
- **Why it is lossless:** The repository root [`tsdown.config.ts`](../../../../tsdown.config.ts) uses only workspace mode, and the 743 output files are byte-identical. The test `resolves the workspace root above a dual-face package leaf` pins the rule.
- **Why it is lossless:** The workspace-mode comparison found the 743 output files byte-identical; [source-first ordering](2026-10-18-source-first-metadata-builds.md) owns current root-build generation. The test `resolves the workspace root above a dual-face package leaf` pins the rule.
- **Behavior difference:** A `package`-mode consumer building a dual-face package receives that package's Typert artifacts; repository builds do not use `package` mode.
### 6. Typert generates in a Worker Thread
@@ -118,7 +118,7 @@ All numbers come from one shared Linux host (256 vCPU, 4 TB memory, load average
- **Narrow `paths` to each face's reference closure (item 1):** The 328-entry version that keeps wildcard keys checks in 55.73 s against 59.08 s (one sample), within noise; the version without wildcard keys produces 18 diagnostics.
- **Split the Client test aggregate program (item 1):** Every sub-program must inject the 242 files carrying module augmentations to pass, and the visible location count stays at 33–49; splitting into 2, 4, or 8 groups leaves the Check sum nearly flat while user CPU rises by 9%, 30%, and 57%, lowering only wall, and the 4- and 8-group splits each produce 1–2 diagnostics.
- **Cache resolution results and make the discovery queue O(1) (Typert):** After items 2–4 only 28 surface walks remain, about 0.18 s, below noise.
- **Move Typert into a separate build step in artifact builds (Typert):** After item 6 the worker already overlaps bundling, leaving at most about 1 s. Type checks, which need no bundle, run a source-only generation step ([local gate speed-ups](2026-10-12-local-gate-speedups.md)).
- **Move Typert into a separate build step in artifact builds (Typert):** After item 6 the worker already overlaps bundling, leaving at most about 1 s. Type checks, which need no bundle, run a source-only generation step ([local gate speed-ups](2026-10-12-local-gate-speedups.md)). The [source-first ordering decision](2026-10-18-source-first-metadata-builds.md) selects a separate step for compiler dependency ordering and records its no-change build cost; the public worker plugin remains available.
- **Filter `transform` by code content (item 7):** rolldown interprets `^` and `\s` with Rust regex semantics, which differ from JavaScript on lone CR, U+2028, and BOM, so a decorated module could be missed.
- **Build a process-wide manifest index on `globalThis` (item 8):** The prototype saves about as much time as item 8, but it is process-global state without an owner, violating the shared-state ownership rule in the [package rules](../../../../packages/AGENTS.md).
- **Disable tsdown's gzip report (item 8):** `--no-report` lowers Client tsdown by 0.95 s, within the 1.56 s baseline spread.
@@ -84,7 +84,7 @@ Status: implemented
- **目的:** 修正工作区根目录的判定。24 个双 face 包带有自己的 `tsconfig.host.json` 叶子配置;只按 `tsconfig.host.json` 向上查找时,每个这样的包都被当作根目录,workspace 模式每次构建为它们各做一次空发现,`package` 模式下双 face 包得不到产物且没有报错。
- **改动:** 工作区根目录是打包输出最近的、同时包含 `tsconfig.host.json` 与 `packages/` 的祖先目录。
- **性能:** Host tsdown 14.82 s → 15.39 s(各 3 个样本的中位数),在噪声内。
- **无损依据:** 仓库根 [`tsdown.config.ts`](../../../../tsdown.config.ts) 只使用 workspace 模式,743 个产物文件逐字节一致。测试 `resolves the workspace root above a dual-face package leaf` 固定该判定。
- **无损依据:** workspace 模式的比较中,743 个产物文件逐字节一致;[先生成顺序](2026-10-18-source-first-metadata-builds.zh.md)负责当前根构建的生成。测试 `resolves the workspace root above a dual-face package leaf` 固定该判定。
- **行为差异:** `package` 模式的使用方构建双 face 包时得到该包的 Typert 产物;仓库构建不使用 `package` 模式。
### 6. Typert 在 Worker Thread 中生成
@@ -118,7 +118,7 @@ Status: implemented
- **把 `paths` 收窄为各 face 的 reference closure(第 1 项):** 保留通配键的 328 条版本 Check 为 55.73 s 对 59.08 s(1 个样本),在噪声内;去掉通配键的版本产生 18 个诊断。
- **拆分 Client 测试聚合程序(第 1 项):** 每个子程序都必须注入 242 个含模块增强的文件才能通过检查,可见位置数仍为 33–49;拆成 2、4、8 组时 Check 总和基本不变,user CPU 分别增加 9%、30%、57%,只降低 wall,且 4 组与 8 组各出现 1–2 个诊断。
- **缓存解析结果并把发现队列改为 O(1)(Typert):** 第 2–4 项之后只剩 28 次表面遍历,约 0.18 s,低于噪声。
- **在产物构建中把 Typert 移到独立构建步骤(Typert):** 第 6 项之后 worker 已与打包重叠,剩余收益不超过约 1 s。不需要 bundle 的类型检查运行只读源码的生成步骤([本地门禁提速](2026-10-12-local-gate-speedups.zh.md))。
- **在产物构建中把 Typert 移到独立构建步骤(Typert):** 第 6 项之后 worker 已与打包重叠,剩余收益不超过约 1 s。不需要 bundle 的类型检查运行只读源码的生成步骤([本地门禁提速](2026-10-12-local-gate-speedups.zh.md))。 [先生成顺序决策](2026-10-18-source-first-metadata-builds.zh.md)为了编译依赖顺序而选择独立步骤,并记录无改动构建的代价;公开 worker 插件仍可使用。
- **按代码内容过滤 `transform`(第 7 项):** rolldown 以 Rust 正则语义解释 `^` 与 `\s`,在孤立 CR、U+2028 与 BOM 上与 JavaScript 不同,可能漏掉含装饰器的模块。
- **在 `globalThis` 上建立进程级 manifest 索引(第 8 项):** 原型收益与第 8 项相当,但它是没有 owner 的进程全局状态,违反[包规则](../../../../packages/AGENTS.md)中的共享状态归属要求。
- **关闭 tsdown 的 gzip 报告(第 8 项):** `--no-report` 使 Client tsdown 降低 0.95 s,在 1.56 s 的基线跨度内。
@@ -15,8 +15,8 @@
en: 7430353afba96352
zh: 6d2a09619a67ecba
/agent-note-local-gate-hook-and-test-speed-ups/decision/1-type-checks-generate-typert-declarations-without-bundling:
en: 4dc48e24a3323b7d
zh: 7c88325160a41d12
en: e9c08ebd00049b44
zh: 36e90dc485b4acb7
/agent-note-local-gate-hook-and-test-speed-ups/decision/2-the-pre-commit-hook-does-not-regenerate-third-party-notices:
en: e2e344f184621172
zh: 97d8785aaf9105bf
@@ -39,23 +39,15 @@ All numbers are wall times from one shared Linux container with a 64-CPU quota o
### 1. Type checks generate Typert declarations without bundling
- **Purpose:** Give `typecheck` and the prerequisite of `lint`, `lint:fix`, `doc-typecheck`, and the CI `typert-contracts` gate the declarations they read, package declarations from `tsc` and Typert declarations, without the Host tsdown pass or the Desktop bundle.
- **Change:** `typecheck` is `tsx scripts/build.ts --scope typecheck`, which runs the steps below through the run-gates scheduler ([`scripts/build.ts`](../../../../scripts/build.ts)). The scope builds no native addon, tsdown bundle, Desktop bundle, or Web output, writes no client build record, and leaves runnable `lib/` output as the last `build:artifacts` left it; run `build:artifacts` before launching or testing built output. The prerequisite of `lint`, `lint:fix`, `doc-typecheck`, and `typert-contracts` is `compile-referenced-projects.ts host-contracts`, which runs host-src beside host-typert ([`compile-referenced-projects.ts`](../../../../scripts/compile-referenced-projects.ts)). `build`, `build:artifacts`, benchmark builds, and the PR rehearsal in `release-vendor.yml` run Typert inside the Host tsdown pass.
- **Change:** `typecheck` is `tsx scripts/build.ts --scope typecheck`, which runs the steps below through the run-gates scheduler ([`scripts/build.ts`](../../../../scripts/build.ts)). The scope builds no native addon, tsdown bundle, Desktop bundle, or Web output, writes no client build record, and leaves runnable `lib/` output as the last `build:artifacts` left it; run `build:artifacts` before launching or testing built output. The prerequisite of `lint`, `lint:fix`, `doc-typecheck`, and `typert-contracts` is `compile-referenced-projects.ts host-contracts`, which runs host-typert before host-src ([`compile-referenced-projects.ts`](../../../../scripts/compile-referenced-projects.ts)). `build`, `build:artifacts`, benchmark builds, and the vendor rehearsal use the same source-only generation before compilation ([ordering decision](2026-10-18-source-first-metadata-builds.md)).
| Step | Command | `needs` |
|---|---|---|
| host-src | `node --max-old-space-size=4096 <tsc> -b <references of tsconfig.host.json>` | — |
| host-typert | `node --import tsx/esm scripts/generate-typert.ts` | — |
| client-src | `<tsc> -b <references of tsconfig.client.json>` | host-src, host-typert |
| host-check | `node --max-old-space-size=4096 <tsc> -b tsconfig.host.json` | host-src |
| client-check | `<tsc> -b tsconfig.client.json` | client-src |
- **Shared generation:** [`scripts/generate-typert.ts`](../../../../scripts/generate-typert.ts) calls `WorkspaceTypertGenerator.generateContributors()` for `WORKSPACE_TYPERT_FACES` ([`scripts/typert-faces.ts`](../../../../scripts/typert-faces.ts)), the faces the Host tsdown plugin also generates, and writes the result with `writeWorkspaceArtifacts()` from [`artifact-files.ts`](../../../../packages/typert/generator/src/artifact-files.ts), the writer the plugin uses. Like the plugin, it skips TypeScript diagnostics, which Host package compilation reports; a Typert analysis error fails host-typert as it fails the Host bundle in `build`. host-typert runs under Node with tsx's ESM hook rather than through `pnpm exec`, whose startup would lengthen the path to Client compilation.
- **Shared generation:** [`scripts/generate-typert.ts`](../../../../scripts/generate-typert.ts) calls `WorkspaceTypertGenerator.generateContributors()` for `WORKSPACE_TYPERT_FACES` ([`scripts/typert-faces.ts`](../../../../scripts/typert-faces.ts)), the repository generation faces, and writes the result with `writeWorkspaceArtifacts()` from [`artifact-files.ts`](../../../../packages/typert/generator/src/artifact-files.ts), the writer the plugin uses. Like the plugin, it skips TypeScript diagnostics, which Host package compilation reports; a Typert analysis error fails host-typert before dependent compilation. host-typert runs under Node with tsx's ESM hook rather than through `pnpm exec`, whose startup would lengthen the path to Client compilation.
- **Aggregate checks in build mode:** host-check and client-check start after their own face's package compilation, so build mode finds every reference up to date, checks only the aggregate root program, and skips a root program whose tsbuildinfo is current.
- **Source-only analysis:** host-typert can run beside host-src because its Host program reads no workspace build output. `tsconfig.base.json` maps `@deepseek-ai/dsh-client-file-upload/types` to source; without that alias the specifier resolves through package `exports` to `lib/types/types.d.ts`. With it, the Host program loads 1,289 files, none of them under a workspace package's `lib/`. `generate-typert.ts` enables `rejectBuildOutputs`: [`build-output-guard.ts`](../../../../packages/typert/generator/src/build-output-guard.ts) fails analysis with a `TypertAnalysisError` when a face program loads a file from a workspace package's `lib/` directory, or cannot resolve an import of a workspace package, as on a tree whose packages are not yet compiled, naming each such import specifier and the `paths` entry that maps it to source. Either way a missing alias fails generation whether or not host-src has run. The tsdown plugin leaves the option off because in `build` Typert always runs after Host package compilation. The [Typert generator README](../../../../packages/typert/generator/README.md) documents the writers and the option.
- **Source-only analysis:** host-typert can run before host-src because its Host program reads no workspace build output. `tsconfig.base.json` maps `@deepseek-ai/dsh-client-file-upload/types` to source; without that alias the specifier resolves through package `exports` to `lib/types/types.d.ts`. With it, the Host program loads 1,289 files, none of them under a workspace package's `lib/`. `generate-typert.ts` enables `rejectBuildOutputs`: [`build-output-guard.ts`](../../../../packages/typert/generator/src/build-output-guard.ts) fails analysis with a `TypertAnalysisError` when a face program loads a file from a workspace package's `lib/` directory, or cannot resolve an import of a workspace package, as on a tree whose packages are not yet compiled, naming each such import specifier and the `paths` entry that maps it to source. Either way a missing alias fails generation whether or not host-src has run. The public tsdown plugin retains its independent generation policy; root artifact builds use the source-only pass. The [Typert generator README](../../../../packages/typert/generator/README.md) documents the writers and the option.
- **Why it is lossless:** No workspace tsconfig sets `allowJs`, every `exports` `types` condition points into `lib/types/` or at `lib/typert.*`, and every tsdown config sets `dts: false`, so no type check reads tsdown output. The 140 Typert files (28 packages × 5 outputs) are byte-identical across the baseline tsdown pass, the current tsdown pass, `generate-typert.ts` after every Typert output is deleted, and `generate-typert.ts` with all 344 `lib/types` directories hidden; with one Remote method temporarily added to `packages/goal/goal`, the tsdown pass and `generate-typert.ts` also write identical files. `typecheck` checks every package project of both faces and both aggregate root programs, the programs that `build` checks.
- **Performance:** `typecheck` with nothing to recheck takes 34.1/35.0 → 21.9/20.0 s. When both aggregate root programs must be rechecked, it takes 83.5 → 76.0/72.1 s, and its critical path is the Host aggregate check at about 66–70 s. The prerequisite of `lint` and `doc-typecheck` takes 52.2/29.6 → 12.5/11.3 s.
- **Trade-off:** `typecheck`, `lint`, `lint:fix`, `doc-typecheck`, and `typert-contracts` write Typert declarations but no bundle, so runnable `lib/` JavaScript can lag behind those declarations until the next `build:artifacts`.
- **Relation to the build:** [The lossless build speed-ups note](2026-10-11-lossless-build-speedups.md) rejects moving Typert out of the Host tsdown pass for builds, where its Worker Thread already overlaps bundling and a separate step saves at most about 1 s. Type-check commands run no bundler, so generating without one removes the whole Host tsdown pass and the Desktop bundle from their critical path. [The build split note](2026-10-11-build-artifacts-check-split.md) lists every caller and its command.
- **Relation to the build:** [Source-first metadata ordering](2026-10-18-source-first-metadata-builds.md) owns generation placement and its measured cost. Declaration-only commands still omit bundling. [The build split note](2026-10-11-build-artifacts-check-split.md) owns command responsibilities.
- **Known gap:** Incremental `tsc -b` does not record `lib/typert.remote-client.d.ts`, which Client sources reach through package `exports`, as an input of the consuming project; when a Remote signature changes and no Client source does, an incremental check does not recheck the consumers. The gap is independent of which pass writes the declarations; Issue #6022 tracks it.
### 2. The pre-commit hook does not regenerate third-party notices
@@ -39,23 +39,15 @@ Status: implemented
### 1. 类型检查不打包地生成 Typert 声明
- **目的:** 让 `typecheck` 以及 `lint`、`lint:fix`、`doc-typecheck` 与 CI `typert-contracts` 门禁的前置步骤得到它们读取的声明,即 `tsc` 产出的包声明与 Typert 声明,而不运行 Host tsdown 阶段或 Desktop bundle。
- **改动:** `typecheck` 是 `tsx scripts/build.ts --scope typecheck`,它通过 run-gates 调度器运行下表中的步骤([`scripts/build.ts`](../../../../scripts/build.ts))。该 scope 不构建 native addon、tsdown bundle、Desktop bundle 或 Web 产物,不写 client build record,可运行的 `lib/` 产物保持上一次 `build:artifacts` 的状态;启动或测试构建产物之前先运行 `build:artifacts`。`lint`、`lint:fix`、`doc-typecheck` 与 `typert-contracts` 的前置步骤是 `compile-referenced-projects.ts host-contracts`,它让 host-src 与 host-typert 并行([`compile-referenced-projects.ts`](../../../../scripts/compile-referenced-projects.ts))。`build`、`build:artifacts`、benchmark 构建与 `release-vendor.yml` 的 PR(Pull Request)演练在 Host tsdown 阶段内运行 Typert。
- **改动:** `typecheck` 是 `tsx scripts/build.ts --scope typecheck`,它通过 run-gates 调度器运行下表中的步骤([`scripts/build.ts`](../../../../scripts/build.ts))。该 scope 不构建 native addon、tsdown bundle、Desktop bundle 或 Web 产物,不写 client build record,可运行的 `lib/` 产物保持上一次 `build:artifacts` 的状态;启动或测试构建产物之前先运行 `build:artifacts`。`lint`、`lint:fix`、`doc-typecheck` 与 `typert-contracts` 的前置步骤是 `compile-referenced-projects.ts host-contracts`,它在 host-src 之前运行 host-typert([`compile-referenced-projects.ts`](../../../../scripts/compile-referenced-projects.ts))。`build`、`build:artifacts`、benchmark 构建与 vendor 演练都在编译前采用同一仅源码生成([顺序决策](2026-10-18-source-first-metadata-builds.zh.md))。
| 步骤 | 命令 | `needs` |
|---|---|---|
| host-src | `node --max-old-space-size=4096 <tsc> -b <references of tsconfig.host.json>` | — |
| host-typert | `node --import tsx/esm scripts/generate-typert.ts` | — |
| client-src | `<tsc> -b <references of tsconfig.client.json>` | host-src、host-typert |
| host-check | `node --max-old-space-size=4096 <tsc> -b tsconfig.host.json` | host-src |
| client-check | `<tsc> -b tsconfig.client.json` | client-src |
- **共用生成:** [`scripts/generate-typert.ts`](../../../../scripts/generate-typert.ts) 对 `WORKSPACE_TYPERT_FACES`([`scripts/typert-faces.ts`](../../../../scripts/typert-faces.ts),即 Host tsdown 插件同样生成的 face)调用 `WorkspaceTypertGenerator.generateContributors()`,并用 [`artifact-files.ts`](../../../../packages/typert/generator/src/artifact-files.ts) 中插件同样使用的 `writeWorkspaceArtifacts()` 写出结果。与插件一样,它跳过 TypeScript 诊断,这些诊断由 Host 包编译报告;Typert 分析错误使 host-typert 失败,正如它在 `build` 中使 Host bundle 失败。host-typert 由 Node 加 tsx 的 ESM 钩子运行,而不经 `pnpm exec`,因为后者的启动开销会拉长通往 Client 编译的路径。
- **共用生成:** [`scripts/generate-typert.ts`](../../../../scripts/generate-typert.ts) 对 `WORKSPACE_TYPERT_FACES`([`scripts/typert-faces.ts`](../../../../scripts/typert-faces.ts),即仓库生成的 face)调用 `WorkspaceTypertGenerator.generateContributors()`,并用 [`artifact-files.ts`](../../../../packages/typert/generator/src/artifact-files.ts) 中插件同样使用的 `writeWorkspaceArtifacts()` 写出结果。与插件一样,它跳过 TypeScript 诊断,这些诊断由 Host 包编译报告;Typert 分析错误使 host-typert 失败,依赖它的编译不会启动。host-typert 由 Node 加 tsx 的 ESM 钩子运行,而不经 `pnpm exec`,因为后者的启动开销会拉长通往 Client 编译的路径。
- **以 build mode 运行聚合检查:** host-check 与 client-check 在本侧 face 的包编译之后才开始,因此 build mode 发现所有 references 都是最新的,只检查聚合根程序,并在根程序的 tsbuildinfo 为最新时跳过它。
- **只读源码的分析:** host-typert 能与 host-src 并行,是因为它的 Host program 不读取任何工作区构建产物。`tsconfig.base.json` 把 `@deepseek-ai/dsh-client-file-upload/types` 映射到源码;没有这条别名时,该说明符经包 `exports` 解析到 `lib/types/types.d.ts`。有了它,Host program 加载 1,289 个文件,其中没有一个位于工作区包的 `lib/` 下。`generate-typert.ts` 开启 `rejectBuildOutputs`:face program 加载工作区包 `lib/` 目录下的文件,或无法解析对工作区包的 import(例如包尚未编译的工作区)时,[`build-output-guard.ts`](../../../../packages/typert/generator/src/build-output-guard.ts) 以 `TypertAnalysisError` 使分析失败,并指出每个这样的 import 说明符,以及把它映射到源码的 `paths` 条目。因此无论 host-src 是否已运行,缺失的别名都会使生成失败。tsdown 插件不开启该选项,因为在 `build` 中 Typert 总在 Host 包编译之后运行。[Typert generator README](../../../../packages/typert/generator/README.zh.md) 记录了这些写入函数与该选项。
- **只读源码的分析:** host-typert 能在 host-src 之前运行,是因为它的 Host program 不读取任何工作区构建产物。`tsconfig.base.json` 把 `@deepseek-ai/dsh-client-file-upload/types` 映射到源码;没有这条别名时,该说明符经包 `exports` 解析到 `lib/types/types.d.ts`。有了它,Host program 加载 1,289 个文件,其中没有一个位于工作区包的 `lib/` 下。`generate-typert.ts` 开启 `rejectBuildOutputs`:face program 加载工作区包 `lib/` 目录下的文件,或无法解析对工作区包的 import(例如包尚未编译的工作区)时,[`build-output-guard.ts`](../../../../packages/typert/generator/src/build-output-guard.ts) 以 `TypertAnalysisError` 使分析失败,并指出每个这样的 import 说明符,以及把它映射到源码的 `paths` 条目。因此无论 host-src 是否已运行,缺失的别名都会使生成失败。公开 tsdown 插件保留独立的生成策略;根产物构建使用仅源码阶段。[Typert generator README](../../../../packages/typert/generator/README.zh.md) 记录了这些写入函数与该选项。
- **无损依据:** 没有任何工作区 tsconfig 设置 `allowJs`,每个 `exports` 的 `types` 条件都指向 `lib/types/` 内或 `lib/typert.*`,每个 tsdown 配置都设置 `dts: false`,因此没有类型检查读取 tsdown 产物。140 个 Typert 文件(28 个包 × 5 个输出)在以下场景中逐字节一致:基线 tsdown 阶段、当前 tsdown 阶段、删除全部 Typert 输出后运行 `generate-typert.ts`、隐藏全部 344 个 `lib/types` 目录后运行 `generate-typert.ts`;临时给 `packages/goal/goal` 加一个 Remote 方法时,tsdown 阶段与 `generate-typert.ts` 写出的文件也相同。`typecheck` 检查两个 face 的全部包 project 与两个聚合根程序,即 `build` 检查的程序。
- **性能:** 无需重查时 `typecheck` 为 34.1/35.0 → 21.9/20.0 s。两个聚合根程序都需要重查时为 83.5 → 76.0/72.1 s,其关键路径是约 66–70 s 的 Host 聚合检查。`lint` 与 `doc-typecheck` 的前置步骤为 52.2/29.6 → 12.5/11.3 s。
- **取舍:** `typecheck`、`lint`、`lint:fix`、`doc-typecheck` 与 `typert-contracts` 写出 Typert 声明但不写 bundle,因此在下一次 `build:artifacts` 之前,可运行的 `lib/` JavaScript 可能落后于这些声明。
- **与构建的关系:** [无损构建提速 Note](2026-10-11-lossless-build-speedups.zh.md) 否决了在构建中把 Typert 移出 Host tsdown 阶段:构建中它的 Worker Thread 已与打包重叠,独立步骤最多只能省约 1 s。类型检查命令不运行打包器,因此不打包地生成会把整个 Host tsdown 阶段与 Desktop bundle 移出它们的关键路径。[构建拆分 Note](2026-10-11-build-artifacts-check-split.zh.md) 列出每个调用方及其命令。
- **与构建的关系:** [先生成元数据的顺序](2026-10-18-source-first-metadata-builds.zh.md)负责生成位置及其测得的代价。仅生成声明的命令仍省略打包。[构建拆分 Note](2026-10-11-build-artifacts-check-split.zh.md)负责命令职责。
- **已知缺口:** 增量 `tsc -b` 不把 Client 源码经包 `exports` 访问到的 `lib/typert.remote-client.d.ts` 记为消费方 project 的输入;Remote 签名改变而 Client 源码未变时,增量检查不会重查消费方。这一缺口与由哪一种生成写出声明无关,由 Issue #6022 跟踪。
### 2. pre-commit 钩子不重新生成第三方声明
@@ -0,0 +1,19 @@
# Bilingual-pair consistency record for 2026-10-15-node-24-runtime-floor.md (docs/i18n/README.md): per heading
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
# After editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-10-15-node-24-runtime-floor.md
/agent-note-node-runtime-floor-and-compatibility-coverage:
en: 256156264ba84f11
zh: 29680265e82fc2c6
/agent-note-node-runtime-floor-and-compatibility-coverage/problem:
en: 1e45546d7452029f
zh: d99966ac42bb193f
/agent-note-node-runtime-floor-and-compatibility-coverage/decision:
en: 13b84da1ca9bbb1f
zh: 5bf45cf6f742cd5a
/agent-note-node-runtime-floor-and-compatibility-coverage/consequences:
en: 224ce4b13f297f8b
zh: 1bb5f8ebdf3243b3
/agent-note-node-runtime-floor-and-compatibility-coverage/alternatives-considered:
en: 413d1133689bd55c
zh: fc77301991e1f425
@@ -0,0 +1,33 @@
# Agent Note: Node runtime floor and compatibility coverage
Status: implemented
English | [中文](2026-10-15-node-24-runtime-floor.zh.md)
## Problem
The root `engines.node` range promises that the installed workspace supports each admitted runtime. Its minimum must cover both the runtime APIs DSH uses and the declared requirements of installed dependencies. Otherwise strict installation can reject a supported version before DSH starts.
## Decision
Set `engines.node` to `>=24.9.0`. Node 24.9 is the minimum for DSH and its development carriers, including the experimental OAR subagent integration, whose `@botiverse/oar` library requires Node 24. This supersedes the [Node 22.19 floor](../../archived/process/2026-07-06-node-engine-floor.md); its Node 23 exclusion remains satisfied, and the declaration policy below remains narrower than the runtime floor. Migration instructions live in the [upgrade guide](../../../../docs/upgrade-guide/v0.2.1-rc.2/node-24/guide.md).
The primary Node 24 jobs own complete typechecking and unit coverage. Focused compatibility jobs run on pinned Node 24.9 and Node 26. Node 24.9 supports the existing `import.meta.main` script entrypoints and native TypeScript stripping. Pinning 24.9 exercises this verified minimum and its v1 private module loader; the primary jobs track the latest Node 24 and exercise its v2 loader, introduced in 24.12.0. A floating `24` compatibility job would duplicate the primary loader coverage and stop testing the minimum.
The Node 24.9 compatibility job also builds artifacts and checks that the shipped Web CLI starts and disposes with full-text Session search disabled. Command-entrypoint, source-worker, Zstandard, source-launch, profile-resolution, and jsdom-storage checks run on both compatibility versions. Real-API e2e jobs stay on Node 24 because they validate provider integration.
The root, Web, and desktop projects retain `@types/node` on the 22.x line. Their current code needs no Node 24-only declarations. A narrower type API remains valid on the supported runtime and avoids unrelated declaration changes. Raise these declarations when a supported API requires them, and keep their major no newer than the runtime floor. The independently published native addon has its own runtime and type support policy.
## Consequences
- Node 22 installations must upgrade before running DSH. Packaged applications with an embedded runtime do not require a system Node upgrade.
- Supported runtimes provide `node:sqlite` and native TypeScript stripping without opt-in flags. The source CLI still uses its declared `tsx/esm` launcher.
- The compatibility graph retains both private loader interfaces used by supported Node versions.
- A future runtime-floor change updates `engines.node`, compatibility jobs, current documentation, this note, and an upgrade guide together.
## Alternatives considered
- **Keep Node 22 support.** It excludes the OAR library from the supported installation.
- **Use OAR’s declared minimum of Node 24.0.** Existing script entrypoints depend on `import.meta.main`, which Node 24.0 lacks. A dependency minimum alone does not establish the application minimum.
- **Test only the latest Node 24.** It does not exercise the minimum or the older private loader interface still used within the supported 24.x line.
- **Raise Node declarations ahead of the minimum.** It allows code to compile against APIs that a supported runtime can lack.
@@ -0,0 +1,33 @@
# Agent Note: Node 运行时下限与兼容性覆盖
Status: implemented
[English](2026-10-15-node-24-runtime-floor.md) | 中文
## 问题
根 `engines.node` 范围承诺安装后的工作区支持其允许的每一个运行时。其下限必须覆盖 DSH 使用的运行时 API 和所安装依赖声明的要求。否则,严格安装可能会在 DSH 启动前拒绝一个受支持的版本。
## 决策
将 `engines.node` 设为 `>=24.9.0`。Node 24.9 是 DSH 及其开发载体的下限,也允许实验性 OAR 子代理集成使用要求 Node 24 的 `@botiverse/oar` 库。此决策取代 [Node 22.19 下限](../../archived/process/2026-07-06-node-engine-floor.md);Node 23 仍被排除,下文的声明策略也仍比运行时下限更窄。迁移说明见[升级指南](../../../../docs/upgrade-guide/v0.2.1-rc.2/node-24/guide.zh.md)。
主要的 Node 24 任务负责完整的类型检查和单元测试覆盖率。聚焦的兼容性任务在固定的 Node 24.9 和 Node 26 上运行。Node 24.9 支持现有 `import.meta.main` 脚本入口和原生 TypeScript 类型剥离。固定 24.9 能验证这个已验证的下限及其 v1 私有模块加载器;主要任务跟随最新的 Node 24,验证 24.12.0 引入的 v2 加载器。浮动的 `24` 兼容性任务会重复主要任务的加载器覆盖,并停止验证下限。
Node 24.9 兼容性任务还构建产物,并检查随产品提供的 Web CLI 能在禁用全文 Session 搜索时启动和释放资源。command-entrypoint、source-worker、Zstandard、source-launch、profile-resolution 和 jsdom-storage 检查在两个兼容性版本上运行。真实 API 的 e2e 任务保留在 Node 24 上,因为它们验证的是提供方集成。
根项目、Web 和桌面项目将 `@types/node` 保留在 22.x 线。它们当前的代码不需要 Node 24 专属声明。较窄的类型 API 仍适用于受支持的运行时,也避免无关的声明变更。当受支持的 API 需要时再提升这些声明,并确保其主版本不高于运行时下限。独立发布的原生 addon 有自己的运行时和类型支持策略。
## 后果
- 使用 Node 22 的安装必须先升级,才能运行 DSH。内嵌运行时的打包应用不要求升级系统 Node。
- 受支持的运行时无需启用标志即可使用 `node:sqlite` 和原生 TypeScript 类型剥离。源码 CLI 仍使用声明的 `tsx/esm` 启动器。
- 兼容性任务图保留受支持 Node 版本使用的两种私有加载器接口。
- 后续提升运行时下限时,必须一起更新 `engines.node`、兼容性任务、当前文档、本文和升级指南。
## 考虑过的替代方案
- **保留 Node 22 支持。** 这会将 OAR 库排除在受支持的安装之外。
- **采用 OAR 声明的 Node 24.0 下限。** 现有脚本入口依赖 `import.meta.main`,而 Node 24.0 缺少该功能。仅凭依赖下限无法确定应用下限。
- **只验证最新的 Node 24。** 这既不能验证下限,也不能验证受支持 24.x 系列仍使用的旧私有加载器接口。
- **将 Node 声明提升至下限之上。** 这会允许代码使用受支持运行时可能缺少的 API 编译通过。
@@ -0,0 +1,22 @@
# Bilingual-pair consistency record for 2026-10-18-source-first-metadata-builds.md (docs/i18n/README.md): per heading
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
# After editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-10-18-source-first-metadata-builds.md
/agent-note-generate-metadata-before-package-compilation:
en: 734f339ae4f8dc93
zh: 5adf12518d4be31d
/agent-note-generate-metadata-before-package-compilation/problem:
en: 3c40c9bd375290a6
zh: dc8d892bdd08c6b4
/agent-note-generate-metadata-before-package-compilation/decision:
en: c9f896731a173a13
zh: 9978e46528caa456
/agent-note-generate-metadata-before-package-compilation/measurements:
en: 42f488af4497f671
zh: 00eeb4cc51799da3
/agent-note-generate-metadata-before-package-compilation/alternatives-considered:
en: 81fde4514a8b6fc0
zh: 4739f5c7978e7db2
/agent-note-generate-metadata-before-package-compilation/consequences:
en: b12bcb7cc9467706
zh: 8896ac7cd011e3c0
@@ -0,0 +1,46 @@
# Agent Note: Generate metadata before package compilation
Status: implemented
English | [中文](2026-10-18-source-first-metadata-builds.zh.md)
## Problem
A Host package can consume generated Remote declarations without becoming a Client package. Generating metadata during Host bundling forces the build to identify those consumers, delay their compilation, and propagate that delay to their callers. TypeScript project references already describe the package dependencies; maintaining another source-import graph duplicates ordering work.
## Decision
Artifact and declaration-only builds run source-only Typert generation before package compilation. The existing generator rejects workspace build outputs and unresolved workspace imports, so a metadata producer cannot depend on its own generated declarations through another source package. TypeScript retains compiler-face ownership and rejects project-reference cycles. Generation failure stops dependent compilation and bundling.
The root bundler uses `decoratorPlugin()` to lower standard TypeScript decorators without generating metadata again. The public `typertPlugin()` retains package/workspace generation and its Worker Thread behavior. [Development](../../../../docs/development.md#typescript-project-layout) owns the current command order.
This ordering replaces only the generation placement in [local gate speed-ups](2026-10-12-local-gate-speedups.md), [lossless build speed-ups](2026-10-11-lossless-build-speedups.md), [the build/check split](2026-10-11-build-artifacts-check-split.md), and [Remote method generation](../architecture/2026-08-02-typert-remote-method-calls.md). Their other optimizations, failure guarantees, public plugin behavior, and historical measurements remain useful and stay active.
## Measurements
The local experiment uses macOS ARM64, Node 24.19.0 and TypeScript 6.0.3. `DSH_GATE_CONCURRENCY=1 pnpm run build:artifacts --profile official` runs baseline/candidate/candidate/baseline. Each run starts after `pnpm run clean`, then repeats without source changes. Installation and cleanup are excluded; native preparation, package compilation, bundles, Desktop, Web and build-record publication are included. Other owned heavy jobs pause; unrelated host load remains possible. Filesystem caches are not flushed.
| Inputs | Clean baseline / candidate (s) | No-change baseline / candidate (s) | Clean peak RSS baseline / candidate (MiB) |
|---|---|---|---|
| Without Node Session consumers | 56.67, 53.55 / 56.29, 56.37 | 13.64, 13.74 / 15.32, 15.26 | 3855, 3858 / 3255, 3268 |
| Separate prototype: Node Session consumers and delayed-stage removal | 61.18, 61.41 / 56.27, 56.44 | 20.08, 20.09 / 15.98, 16.09 | 4013, 4012 / 3278, 3291 |
RSS is summed over descendant processes approximately every 100ms; shared pages count more than once, so this is not unique physical memory. Two samples per case are local evidence, not a CI budget. All 150 generated Typert/Remote files are byte-identical in both comparisons. Complete inventories contain 12,246 and 12,352 files respectively; only the changed decorator-plugin module's four compiler outputs differ.
## Alternatives considered
**Keep generation inside Host bundling.** It preserves overlap and the faster standalone no-change build, but consumers of generated declarations require a separate delayed compilation schedule.
**Hand-maintain delayed packages or change their compiler faces.** Both make package ownership depend on build placement and add an inventory beside project references.
**Use source-first generation only for type checks.** Artifact builds would still need the consumer scanner, leaving two schedules to maintain.
**Remove the public generation plugin.** Its package/workspace API and Worker Thread failure ownership remain supported; repository scheduling does not justify deleting that API.
## Consequences
The consumer row is evidence from a separate integration prototype. It does not describe the packages or validation delivered by this prerequisite.
The standalone prerequisite costs 1.60s (+11.7%) in median no-change artifact time because generation no longer overlaps bundling. Its clean difference is +1.22s, smaller than the baseline spread. This is ordering support, not an independent speedup. With delayed stages removed, the measured consumer build improves by 4.94s clean and 4.05s without changes. Peak summed RSS decreases in both cases.
Source-generation tests reject metadata dependency cycles with absent and preexisting outputs. A fresh Host-consumer fixture compiles against metadata generated in the same build. Decorator tests exercise both plugin entry points, and complete builds verify application artifacts. Wine's separate command sequence prepares metadata explicitly; its source guard rejects missing preparation, while native Windows execution remains CI-owned.
@@ -0,0 +1,46 @@
# Agent Note: 在包编译前生成元数据
Status: implemented
[English](2026-10-18-source-first-metadata-builds.md) | 中文
## Problem
Host 包可以消费生成的 Remote 声明,而不必成为 Client 包。如果在 Host 打包期间生成元数据,构建就必须识别这些消费方、延迟其编译,并把延迟传播给调用方。TypeScript project reference 已经描述包依赖;再维护一张源码导入图会重复处理顺序。
## Decision
产物构建与仅生成声明的构建都在包编译之前运行仅源码的 Typert 生成。现有生成器拒绝工作区构建产物与无法解析的工作区导入,因此元数据生产者不能经由另一个源码包依赖自身生成的声明。TypeScript 保留 compiler face 的归属,并拒绝 project reference 环。生成失败后不启动依赖它的编译与打包。
根打包器使用 `decoratorPlugin()` 转换标准 TypeScript 装饰器,不重复生成元数据。公开的 `typertPlugin()` 保留 package/workspace 生成与 Worker Thread 行为。[开发指南](../../../../docs/development.zh.md#typescript-project-layout)负责当前命令顺序。
该顺序只替代[本地门禁提速](2026-10-12-local-gate-speedups.zh.md)、[无损构建提速](2026-10-11-lossless-build-speedups.zh.md)、[构建/检查拆分](2026-10-11-build-artifacts-check-split.zh.md)与 [Remote 方法生成](../architecture/2026-08-02-typert-remote-method-calls.zh.md)中的生成位置。它们的其他优化、失败保证、公开插件行为与历史测量仍有价值,因此保留为活跃记录。
## Measurements
本地实验使用 macOS ARM64、Node 24.19.0 与 TypeScript 6.0.3。`DSH_GATE_CONCURRENCY=1 pnpm run build:artifacts --profile official` 按基线/候选/候选/基线顺序运行。每轮先执行 `pnpm run clean`,再在源码不变的情况下重跑。安装与清理不计时;native 准备、包编译、bundle、Desktop、Web 与构建记录发布计入时间。暂停本任务的其他重负载工作;主机上仍可能有无关负载。不清空文件系统缓存。
| 输入 | 干净构建基线/候选 (s) | 无改动构建基线/候选 (s) | 干净构建 RSS 峰值基线/候选 (MiB) |
|---|---|---|---|
| 不含 Node Session 消费方 | 56.67, 53.55 / 56.29, 56.37 | 13.64, 13.74 / 15.32, 15.26 | 3855, 3858 / 3255, 3268 |
| 独立原型:Node Session 消费方与后置阶段删除 | 61.18, 61.41 / 56.27, 56.44 | 20.08, 20.09 / 15.98, 16.09 | 4013, 4012 / 3278, 3291 |
RSS 约每 100ms 汇总所有子孙进程;共享页面会重复计数,因此不代表唯一物理内存。每种情况的两个样本是本地证据,不是 CI 预算。两组比较中的全部 150 个 Typert/Remote 生成文件逐字节相同。完整清单分别包含 12,246 与 12,352 个文件;只有装饰器插件模块的四个编译产物不同。
## Alternatives considered
**保留 Host 打包内的生成。** 它保留重叠执行与更快的独立无改动构建,但生成声明的消费方需要单独的后置编译调度。
**手工维护后置包,或改变它们的 compiler face。** 两者都让包归属受构建位置影响,并在 project reference 之外增加一份清单。
**仅在类型检查中采用先生成顺序。** 产物构建仍需要消费方扫描器,留下两套需要维护的调度。
**删除公开生成插件。** 它的 package/workspace API 与 Worker Thread 失败归属仍受支持;仓库调度改变不能成为删除该 API 的理由。
## Consequences
消费方一行是独立集成原型的测量证据,不表示本前置改动交付的包或验证。
独立前置改动使无改动产物构建的中位数增加 1.60s(+11.7%),因为生成不再与打包重叠。干净构建增加 1.22s,小于基线的样本波动。该改动提供顺序支持,并非独立提速。删除后置阶段后,测得的消费方构建在干净状态下快 4.94s,无改动时快 4.05s。两组的进程树 RSS 峰值都下降。
源码生成测试在产物缺失和已存在时均拒绝元数据依赖环。干净的 Host 消费方 fixture 使用同一次构建生成的元数据完成编译。装饰器测试覆盖两个插件入口,完整构建验证应用产物。Wine 的独立命令序列显式准备元数据;源码守卫拒绝缺失的准备步骤,原生 Windows 执行仍由 CI 负责。
@@ -0,0 +1,22 @@
# Bilingual-pair consistency record for 2026-10-18-borrow-auto-review-inputs.md (docs/i18n/README.md): per heading
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
# After editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/proposed/simplification/2026-10-18-borrow-auto-review-inputs.md
/agent-note-borrow-auto-review-inputs-until-serialization:
en: 7935a9077525c95c
zh: f54640700003b6a3
/agent-note-borrow-auto-review-inputs-until-serialization/problem:
en: ae55e01074cf34e0
zh: c9778fdb8fb157e3
/agent-note-borrow-auto-review-inputs-until-serialization/proposal:
en: 67b001f13952f165
zh: 09f849fad8817aad
/agent-note-borrow-auto-review-inputs-until-serialization/alternatives-considered:
en: 86495afdbc23ebe1
zh: d5c237aea3f14eed
/agent-note-borrow-auto-review-inputs-until-serialization/acceptance-criteria:
en: b193bda5eb966372
zh: e493c784adec4f05
/agent-note-borrow-auto-review-inputs-until-serialization/risks:
en: df5aebcbd42a13df
zh: 5945aa631bd4e07f
@@ -0,0 +1,25 @@
# Agent Note: Borrow Auto review inputs until serialization
Status: proposed
English | [中文](2026-10-18-borrow-auto-review-inputs.zh.md)
## Problem
[`snapshotAutoReview()`](../../../../packages/experimental/auto-review/src/index.ts) reads frozen Session history, request headers, and tool arguments, but also copies `session.surface.nodes` and freezes its private result. Its only caller, `classifyRisk()`, immediately serializes that result without an intervening await or publication.
## Proposal
Borrow `session.surface.nodes` and return the readonly intermediate result without another `deepFreeze()`. Preserve all action validation and the final `GenerateOptions` freeze. [Session-owned immutability](../../implemented/architecture/2026-06-11-dev-invariants-over-deep-readonly.md) and [request-only input freezing](../../implemented/architecture/2026-09-17-persistence-attribution-policy.md) remain unchanged. This removes a private array allocation and freeze traversal without changing the reviewer request.
## Alternatives considered
**Keep the intermediate snapshot.** A future retained or asynchronous reader would need stable inputs. No current consumer retains this object or awaits before rendering.
## Acceptance criteria
Native and PTC requests retain identical text, source filtering, and action checks. Existing request-freezing and cancellation assertions pass. Serialization does not mutate borrowed Session or execution data.
## Risks
Future asynchronous work or publication of the intermediate object would require reconsidering ownership. Removing the final request freeze or the source owners' snapshots is outside this proposal.
@@ -0,0 +1,25 @@
# Agent Note: Auto review 输入在序列化前采用借用
Status: proposed
[English](2026-10-18-borrow-auto-review-inputs.md) | 中文
## Problem
[`snapshotAutoReview()`](../../../../packages/experimental/auto-review/src/index.ts) 读取已冻结的 Session 历史、请求头和工具参数,但还会复制 `session.surface.nodes` 并冻结私有结果。它唯一的调用方 `classifyRisk()` 会立即序列化该结果,中间没有 await,也不会发布结果。
## Proposal
借用 `session.surface.nodes`,返回只读的中间结果,不再调用一次 `deepFreeze()`。保留全部操作校验和最终 `GenerateOptions` 的冻结。[Session 拥有的不可变性](../../implemented/architecture/2026-06-11-dev-invariants-over-deep-readonly.zh.md) 和[仅用于请求的输入冻结](../../implemented/architecture/2026-09-17-persistence-attribution-policy.zh.md) 保持不变。这会移除一次私有数组分配和冻结遍历,同时保持审核请求不变。
## Alternatives considered
**保留中间快照。** 未来若有留存数据或异步读取的消费方,就需要稳定输入。当前没有消费方留存该对象,也没有消费方在渲染前执行 await。
## Acceptance criteria
原生工具和 PTC 请求保留完全相同的文本、来源过滤和操作检查。现有请求冻结与取消断言通过。序列化不修改借用的 Session 或执行数据。
## Risks
未来若引入异步工作或发布中间对象,需要重新考虑所有权。移除最终请求的冻结或数据所有者提供的快照不在本提案范围内。
@@ -0,0 +1,22 @@
# Bilingual-pair consistency record for 2026-10-18-parse-ssh-policy-once.md (docs/i18n/README.md): per heading
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
# After editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/proposed/simplification/2026-10-18-parse-ssh-policy-once.md
/agent-note-parse-ssh-policies-once-at-request-admission:
en: 0c215d7a2d291857
zh: df3c91383d122974
/agent-note-parse-ssh-policies-once-at-request-admission/problem:
en: 3cb8bcfb464726d4
zh: f87f7cc23e7d161c
/agent-note-parse-ssh-policies-once-at-request-admission/proposal:
en: 71eb2e823861a3b0
zh: 0f6fb08df9b3217f
/agent-note-parse-ssh-policies-once-at-request-admission/alternatives-considered:
en: 98230674c8b68772
zh: 068c70ede9914a05
/agent-note-parse-ssh-policies-once-at-request-admission/acceptance-criteria:
en: e48ed6ba31628951
zh: 240022dd565487bc
/agent-note-parse-ssh-policies-once-at-request-admission/risks:
en: 0d2e4cd1658f0d1a
zh: 6838c9083b629c8b
@@ -0,0 +1,25 @@
# Agent Note: Parse SSH policies once at request admission
Status: proposed
English | [中文](2026-10-18-parse-ssh-policy-once.zh.md)
## Problem
The [`sandbox` and `fs.write`/`fs.edit` handlers](../../../../packages/ssh/ssh/src/helper.ts) are the private policy resolver's only callers. They validate requests with `policySchema`; the resolver repeats that parse, including a policy and extra-root array copy.
## Proposal
Accept `z.output<typeof policySchema>` in the private resolver and remove its parse. Retain both wire parsers, execution-host path resolution, cancellation, SessionId branding, and explicit omission of undefined optional fields. The [SSH execution decision](../../implemented/architecture/2026-09-11-posix-ssh-runtime.md) continues to own remote policy enforcement.
## Alternatives considered
**Keep the resolver's unknown input.** It protects future unparsed callers, but current callers already decode. A typed parameter states the required precondition without repeated runtime work.
## Acceptance criteria
Both request handlers still reject malformed policies, relative roots, and unexpected fields. Remote physical-root checks and returned policies remain unchanged. Existing helper tests cover absent and supplied roots, read-only policy, and denied writes.
## Risks
A future wire caller must validate before invoking the resolver. Preserve optional-field omission because Zod permits explicit `undefined` while the execution-policy type does not.
@@ -0,0 +1,25 @@
# Agent Note: 在请求接收处只解析一次 SSH 策略
Status: proposed
[English](2026-10-18-parse-ssh-policy-once.md) | 中文
## Problem
[`sandbox` 和 `fs.write`/`fs.edit` 处理函数](../../../../packages/ssh/ssh/src/helper.ts) 是私有策略解析器仅有的调用方。它们使用 `policySchema` 校验请求;解析器重复该解析,包括复制策略对象和额外根目录数组。
## Proposal
让私有解析器接收 `z.output<typeof policySchema>`,移除其中的解析。保留两个线上请求解析器、执行主机上的路径解析、取消、SessionId 品牌类型转换,以及对值为 undefined 的可选字段的显式省略。[SSH 执行决策](../../implemented/architecture/2026-09-11-posix-ssh-runtime.zh.md) 继续规定远程策略执行。
## Alternatives considered
**保留解析器的 unknown 输入。** 它能保护未来未经解析的调用方,但当前调用方已经解码。类型化参数可表达必要前提,无需重复执行运行时工作。
## Acceptance criteria
两个请求处理函数仍拒绝格式错误的策略、相对根目录和意外字段。远程物理根目录检查和返回策略保持不变。现有辅助进程测试覆盖未提供和已提供根目录、只读策略以及被拒绝的写入。
## Risks
未来的线上请求调用方必须先校验,再调用解析器。保留可选字段省略,因为 Zod 允许显式 `undefined`,而执行策略类型不允许。
@@ -0,0 +1,22 @@
# Bilingual-pair consistency record for 2026-10-18-single-workspace-canonicalization.md (docs/i18n/README.md): per heading
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
# After editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/proposed/simplification/2026-10-18-single-workspace-canonicalization.md
/agent-note-canonicalize-each-sandbox-workspace-once:
en: d9beb1e7a988d598
zh: 9ff16ea850cb3d57
/agent-note-canonicalize-each-sandbox-workspace-once/problem:
en: 04219da3d9301a2d
zh: 92a399597d315cba
/agent-note-canonicalize-each-sandbox-workspace-once/proposal:
en: d5017ea68bf366ea
zh: 6aa157b402d4d8d0
/agent-note-canonicalize-each-sandbox-workspace-once/alternatives-considered:
en: f73a3a3a1a90fb16
zh: 3ec1d23f24436e26
/agent-note-canonicalize-each-sandbox-workspace-once/acceptance-criteria:
en: 0a2aa37511fa5574
zh: 15258e58b498d944
/agent-note-canonicalize-each-sandbox-workspace-once/risks:
en: 1fdb53b7c48b1f8d
zh: 999debc230df8d39
@@ -0,0 +1,25 @@
# Agent Note: Canonicalize each sandbox workspace once
Status: proposed
English | [中文](2026-10-18-single-workspace-canonicalization.zh.md)
## Problem
[`LocalSandboxProvider.confine()`](../../../../packages/sandbox/sandbox-local/src/index.ts) canonicalizes `workspaceRoot` and copies the policy before dispatch. POSIX profile builders call [`workspaceWritableRoots()`](../../../../packages/sandbox/sandbox/src/roots.ts), which resolves that workspace again. The common preprocessing also runs for POSIX read-only policies that grant no workspace writes.
## Proposal
Remove the common policy copy. Keep canonicalization in shared POSIX root derivation; canonicalize once inside `windowsAclRunnerArgv()`, reusing that value for argv, SID derivation, and ACL grants. Preserve per-call extra-root validation and each backend's temporary-directory rules.
## Alternatives considered
**Keep common preprocessing.** It supplies every backend with one physical workspace identity, but POSIX repeats the filesystem work. Explicit Windows normalization preserves the [Windows ACL decision](../../implemented/feature/2026-08-08-windows-acl-restricted-token-sandbox.md).
## Acceptance criteria
Direct profile builders and `runnerCommand` retain canonical roots. Windows argv, SID, and grant keys agree. Existing alias-rejection and read-only checks pass; a focused trace confirms removal of the duplicate workspace lookup.
## Risks
Incomplete relocation can split Windows grant identity. Do not cache extra roots: enforcement must recheck a directory replaced by a symlink.
@@ -0,0 +1,25 @@
# Agent Note: 每个沙箱工作区只规范化一次
Status: proposed
[English](2026-10-18-single-workspace-canonicalization.md) | 中文
## Problem
[`LocalSandboxProvider.confine()`](../../../../packages/sandbox/sandbox-local/src/index.ts) 在分派前规范化 `workspaceRoot` 并复制策略。POSIX 配置构建函数调用 [`workspaceWritableRoots()`](../../../../packages/sandbox/sandbox/src/roots.ts),再次解析同一工作区。对于不授予工作区写权限的 POSIX 只读策略,公共预处理也会执行。
## Proposal
移除公共策略复制。保留共享 POSIX 根目录推导中的规范化;在 `windowsAclRunnerArgv()` 内规范化一次,并在 argv、SID 推导和 ACL 授权中复用该值。保留每次调用的额外根目录校验,以及各后端的临时目录规则。
## Alternatives considered
**保留公共预处理。** 它为每个后端提供统一的物理工作区标识,但 POSIX 会重复访问文件系统。显式保留 Windows 规范化可维持 [Windows ACL 决策](../../implemented/feature/2026-08-08-windows-acl-restricted-token-sandbox.zh.md)。
## Acceptance criteria
直接调用配置构建函数和使用 `runnerCommand` 时仍得到规范化的根目录。Windows argv、SID 与授权键保持一致。现有别名拒绝和只读检查通过;聚焦的跟踪结果确认重复的工作区查找已移除。
## Risks
迁移不完整可能使 Windows 授权标识不一致。不得缓存额外根目录:执行权限限制时必须重新检查被符号链接替换的目录。
@@ -9,8 +9,8 @@
en: 7b7248fc12313b69
zh: a55e2c3376c401a7
/agent-note-dependency-swaps-rejected-by-the-2026-07-nih-audit/proposal:
en: d95debbdb2a7e730
zh: 1edbf6ca992a2319
en: fcd5edbdfefd224c
zh: 0172065e20f9b3ee
/agent-note-dependency-swaps-rejected-by-the-2026-07-nih-audit/alternatives-considered:
en: 229a0110ca47384e
zh: 3f86f858d56190c5
@@ -58,7 +58,7 @@ Adopt the following dependency swaps. Rejected — per-item evidence below; a fu
**SQLite and storage:**
- **`better-sqlite3` for the three SQLite backends**: all use builtin `node:sqlite`, intentional twice over — it gates the [Node engine floor](../../implemented/process/2026-07-06-node-engine-floor.md) and works inside the single-file executable where a native addon would complicate packaging. No hand-rolled migrations or busy-retry loops exist.
- **`better-sqlite3` for the three SQLite backends**: all use builtin `node:sqlite`, intentional twice over — it is available throughout the [supported Node range](../../implemented/process/2026-10-15-node-24-runtime-floor.md) and works inside the single-file executable where a native addon would complicate packaging. No hand-rolled migrations or busy-retry loops exist.
**Repo tooling:**
@@ -58,7 +58,7 @@ Status: rejected — 下列每一项替换在证据上都未达到净简化门
**SQLite 与存储:**
- **以 `better-sqlite3` 承担三个 SQLite 后端**:三者全部使用内置 `node:sqlite`,且是双重有意为之——它是 [Node 引擎下限](../../implemented/process/2026-07-06-node-engine-floor.zh.md)的把关依据,也能在单文件可执行体内工作,原生 addon 反而会让打包复杂化。不存在任何手写的迁移或 busy 重试循环。
- **以 `better-sqlite3` 承担三个 SQLite 后端**:三者全部使用内置 `node:sqlite`,且是双重有意为之——它在 [受支持的 Node 范围](../../implemented/process/2026-10-15-node-24-runtime-floor.zh.md)内可用,也能在单文件可执行体内工作,原生 addon 反而会让打包复杂化。不存在任何手写的迁移或 busy 重试循环。
**仓库工具链:**
+3 -3
View File
@@ -41,9 +41,9 @@ description: Use when reviewing a pull request in the deepseek-harness repo —
- **Intent and interface contracts:** trace both sides of every changed interface. Confirm the implementation matches the PR and any Agent Note, including errors, cancellation, ownership, and disposal.
- **Error handling:** apply [dsh-error-handling](../dsh-error-handling/SKILL.md) to changed failure paths, including their state guarantees and final user-visible reports. Reject catch-and-continue unless the state owner establishes that subsequent work's preconditions hold, including after recovery.
- **Lifecycle and concurrency:** for async setup, callbacks, processes, or teardown, apply [defensive-patterns/general.md](../../../docs/defensive-patterns/general.md). Check races before publication, cancellation during awaits, independent error reporting, callback containment, ownership before reentry, complete detach cleanup, and quiescent disposal.
- **Capability and consumer fit:** trace every current consumer, then flag consumer-specific behavior leaking into the interface under [the package rules](../../../packages/AGENTS.md). Flag the inverse too: a new public method on a generic service (registry, session, agent) whose only caller is one internal consumer is an unnecessary API expansion — require a private capability closure handed to that consumer at construction instead.
- **Scope, ownership, and necessity:** map each abstraction, state machine, option, defensive copy, and compatibility path to its current contract, production consumer, and owning plugin or service. Challenge unrelated features and speculative generality, then test the PR against [the root rules](../../../AGENTS.md#conventions).
- **Configuration and public choices:** ask what current-consumer evidence or prior art supports each default, public operation set, format, or imported external concept. Require an explicit choice or deferral when that evidence is absent.
- **Scope, ownership, and necessity:** first identify the required behavior behind each new distinction, field, adapter, state machine, option, defensive copy, and compatibility path. A requirement, check, or Agent Note introduced by the PR is a design choice to assess, not independent evidence of necessity. Compare existing source fields, discriminants, and their consumers before accepting another representation; distinguish deleting a concept from relocating its declaration. Moving an unnecessary concept closer to its owner is not a simplification. Challenge unrelated features and speculative generality against [the root rules](../../../AGENTS.md#conventions).
- **Capability and consumer fit:** trace every current consumer, then flag consumer-specific behavior leaking into the interface under [the package rules](../../../packages/AGENTS.md). For required behavior, prefer an existing extension point near its producer or consumer when the local implementation adds little complexity, following [the ownership rule](../../../docs/defensive-patterns/general.md#a-business-capability-owns-itself); compare producer-owned declaration merging with changes to distant core or shared type declarations. Flag the inverse too: a new public method on a generic service (registry, session, agent) whose only caller is one internal consumer is an unnecessary API expansion — require a private capability closure handed to that consumer at construction instead.
- **Configuration and public choices:** trace authoritative reads and precedence before accepting another CLI or configuration pass-through; a forwarded value may be shadowed before use. Ask what current-consumer evidence or prior art supports each default, public operation set, format, or imported external concept. Require an explicit choice or deferral when that evidence is absent.
- **Model perspective:** inspect the exact prompts, tool schemas, results, and diagnostics the model receives across affected modes. Flag concepts outside the model's task, then verify stable text verbatim and dynamic behavior through snapshots or end-to-end coverage.
- **Enforcement:** follow every denial path to the operation that executes it; exercise direct and alternate callers that can bypass schemas, prompts, facades, wrappers, or listener ordering.
- **Borrowed and derived state:** determine whether each retained value is borrowed or owned under the package contract, then trace notifications and every cache, prompt, UI echo, replay, and query view to the documented success point and authoritative source.
@@ -17,14 +17,14 @@ For broad requests, divide independent domains among subagents: loop and persist
## Search for removable obligations
Use these questions to guide discovery. Read the relevant section of [historical patterns](references/historical-patterns.md) when a candidate needs calibration; the examples illustrate decisions, not a current deletion inventory.
Use these questions to guide discovery. Requirements, checks, and Agent Notes introduced by the code under review are design choices to assess, not independent evidence that its distinctions are necessary. Read the relevant section of [historical patterns](references/historical-patterns.md) when a candidate needs calibration; the examples illustrate decisions, not a current deletion inventory.
- **Does a declared feature have a complete effect path?** Trace producer, transformations, provider or endpoint support, and observable result. A field copied everywhere may still have no setter, no reader, or only implementations that reject it. Search constructors and discriminant emitters as well as callers.
- **Which distinctions change a consumer's action?** Several internal states may require one model-facing status or one usable identifier. Keep internal distinctions that control authorization, residency, durability, or ownership; remove public distinctions that only invite an unusable choice.
- **Does a declared feature have a complete effect path?** Trace producer, transformations, provider or endpoint support, and observable result. A field copied everywhere may still have no setter, no reader, or only implementations that reject it. For CLI and configuration pass-throughs, follow authoritative reads and precedence to find forwarded values that are shadowed before use. Search constructors and discriminant emitters as well as callers.
- **Which distinctions change a consumer's action?** First ask whether the distinction, field, or adapter is needed at all. Trace existing source fields and discriminants through their emitters and authorization or capability checks before adding a transport-specific marker; a broad envelope category need not have the source field's meaning. Consider deleting a distinction separately from relocating its declaration, and state which inputs or capabilities each alternative admits. Several internal states may require one model-facing status or one usable identifier. Preserve required authorization, residency, durability, and ownership behavior; remove public distinctions that only invite an unusable choice.
- **Could a smaller explicit behavior remove the subsystem?** Fixed intervals, caller-supplied time zones, or reported activation failure can eliminate generalized calendars, persisted defaults, or automatic rollback. Name the capability given up and the machinery deleted. A production caller makes this a behavior decision, not an automatic rejection; implementation must remain within the user's authorization.
- **Can a consumer read the authoritative value when needed?** Look for copied histories, promotion ledgers, invalidation events followed by reads, and feature caches beside a shared projection stream. Derivation or reuse may delete subscriptions, retained data, stale-response handling, and recovery paths together. Establish the required time of observation first.
- **Is composition being mistaken for policy?** Optional service presence, a provider method, or a sibling tool can express availability without deciding deployment behavior. Explicit configuration or a caller-intent operation may remove registries and dependency checks. Preserve distinct operations when their ownership or completion timing differs.
- **What owns the complete maintenance cost?** Trace pass-through configuration, duplicate application trees, demo launchers used by products, resolver-only workspaces, and installation helpers with their own upgrade lifecycle. Compare the entire removed system with the replacement, including residual glue; moving complexity or adding a gate to keep duplicate definitions equal does not remove it.
- **What owns the complete maintenance cost?** Trace duplicate application trees, demo launchers used by products, resolver-only workspaces, and installation helpers with their own upgrade lifecycle. For required behavior, compare existing producer/consumer extension points, including declaration merging, with changes to distant core or shared type declarations under [the ownership rule](../../../docs/defensive-patterns/general.md#a-business-capability-owns-itself). Compare the entire removed system with the replacement, including residual glue; moving an unnecessary concept closer to its owner or adding a gate to keep duplicate definitions equal does not remove its maintenance cost.
## Prove reachability and the trade-off
@@ -32,7 +32,7 @@ Start with `rg`, then read the matches. Search exact symbols, property reads and
Include `packages/*/*/src`, `apps/`, `python/`, runtime scripts, shipped profiles and overlays, package manifests and exports, loader resolution, generated runtime assets, and installed-package consumers. Classify examples and fixtures by their actual entry point rather than directory name. Follow generated catalogs to their consumers: a catalog used by model-written plugin mounts is a dynamic product API, while a documentation-only listing is not a fixed caller. Public third-party extension paths can be intentional despite zero repository call sites. A static unused-code checker must cover manifest-driven loading, generated outputs, and compiler faces without maintaining a parallel per-workspace exception inventory.
For each candidate, record the current owner, effective producer/consumer path, what disappears, what remains, and the strongest reason to retain it. Distinguish:
For each candidate, record the current owner, effective producer/consumer path, what disappears, what remains, capability and compatibility effects, and the strongest reason to retain it. Distinguish:
- removal of unreachable or unread behavior;
- a narrower public or product behavior with an explicit loss;
+172
View File
@@ -0,0 +1,172 @@
---
name: dsh-pr-description
description: Use when creating a deepseek-harness pull request, or when updating its description after later changes or a handled review round. Defines each description section, Issue references, test design, change-specific disclosures, and the Human and Agent Roles section with its levels, stages, grading rules, and update timing.
user-invocable: false
---
# Writing PR descriptions
This skill defines what a deepseek-harness PR description contains, how to write it, and when to update it. The [PR template](../../../.github/pull_request_template.md) supplies only the section headings and placeholders; this skill owns the requirements for each section. Every PR description follows this skill, including PRs created with `gh pr create --body-file`, which skips the template.
## Scope
- Write the complete description when you create the PR.
- After the PR exists, update the affected sections when the description no longer matches the PR, at times you judge suitable.
- Each PR in a stack has its own description, which covers only that layer's changes against its base branch.
## Description structure
The description uses four second-level headings, in this order: `## Motivation`, `## Changes`, `## Testing`, and `## Human and Agent Roles`.
Write all headings and labels, such as **Interface changes**, **Behavior changes**, **Test design**, and **Test entries**, in English. Write the rest of the description in Chinese unless a human asked for another language for this PR.
### Motivation
- State the problem in one sentence.
- Reference at least one same-repository Issue: use `Fixes #N`, `Closes #N`, or `Resolves #N` when the PR resolves it, and `Refs #N` when it only gives context. PR numbers do not count as Issue references, and neither do references inside HTML comments, code fences, or inline code. [Issue management](../../../.github/issue-management/README.md) owns the exact rules and the Priority matching.
### Changes
- Interface changes: summarize changes to commands (including repository scripts), flags, configuration keys, environment variables, public exports, plugin registration values, protocols, or persisted formats; write None when there are none.
- Behavior changes: summarize results that users, models, developers, or the system can observe, including defaults, performance, timing, and output; write None when there are none. Changes to documentation and agent instructions, such as READMEs, AGENTS.md, and skills, are behavior changes for the readers they guide.
- State only behavior that the diff guarantees and the tests cover.
### Testing
Testing starts with the test design, followed by the test entries.
**Test design**: summarize how the tests cover each item in Changes and why that coverage is complete. Choose the form by change type:
| Change type | What the test design shows |
|---|---|
| Behavior change | A behavior matrix of scenario × behavior before × behavior after × covering test, showing which scenarios failed or differed before the change |
| Performance change | Before and after values per stage or scenario, with the measurement conditions: machine, run count, and how the value is taken |
| Refactoring or migration without behavior change | How the unchanged behavior is shown: byte-for-byte comparison, unchanged snapshots, or old-versus-new comparison tests |
| Documentation only | How the content is checked: links, bilingual pairing, example compilation, or similar |
When the PR mixes change types, give one form for each type present. Items that Changes lists as None need no test design.
**Test entries**:
- Add one entry per test method; state the command or steps visibly, and the behavior it covers.
- List only tests that have run, with their results; add later runs when the description is updated. State whether the evidence comes from a local run or from CI.
- Put reviewable evidence, such as test output, screenshots, recordings, or logs, in that entry's `<details><summary>Proof</summary>` block; when the full output is long, the block may hold the command, the counts, and a result summary.
### Change-specific disclosures
Write each disclosure in the section it concerns, usually Changes, after a bold English label that names it, such as **Existing test behavior**; do not add headings.
| Change | What the description states | Rule owner |
|---|---|---|
| Changing the behavior an existing test describes | Why the behavior changed | The Tests describe behavior rule in [root AGENTS.md](../../../AGENTS.md) |
| Raising a ceiling in `scripts/doc-budgets.manifest.json` | Each raised ceiling with its old and new value, and why the words are necessary | [Wordcount budgets](../../../docs/AGENTS.md#wordcount-budgets) |
| Changing product-user-visible GUI behavior | An embedded demonstration GIF, with the disclosures that record-browser-gif requires beside the embed; for each GUI change without a GIF, a statement that none is embedded and why | [record-browser-gif](../record-browser-gif/SKILL.md#every-gui-pull-request-includes-a-gif) |
| Using a translation term the terminology list lacks | The source of the rendering, or a proposed rendering under pending terms | [Translation rules](../../../docs/i18n/translation-rules.md) |
## Human and Agent Roles
This section records who made the decisions in each stage of the PR, and how autonomously the Agent worked.
### Levels
Levels are ordered from least to most AI involvement; each level pairs an English name with a Chinese name. Autonomy is the Agent's autonomy in that stage, a multiple of 5% within the chosen level's range.
| Level | Meaning | Agent autonomy |
|---|---|---|
| hint 人工独立 | A human does the work; AI takes no part or only offers passive suggestions that the human accepts or rejects | 0–10% |
| assist 人工主导 | A human supplies the main content or every key decision; AI completes part of the work | 15–25% |
| pair 人工引导 | A human gives the key direction or constraints and revises the AI's output several times; the human understands the internals | 30–50% |
| copilot 人工把关 | AI completes the whole task; a human reviews, selects, or rejects, and decides or clarifies key points without supplying substantive content | 55–85% |
| auto 模型自主 | AI completes the task autonomously; a human only sets the goal or accepts the output | 90–100% |
| n/a 暂不适用 | The stage does not exist or has not happened yet | Omitted |
### Stages and criteria
Stages appear in a fixed order; each stage pairs an English name with a Chinese name. The criteria below cover assist through auto; judge hint by its meaning in the level table.
**breakdown 问题拆解**: after the problem appears, whether a human takes part in breaking it down, or the Agent acts on it directly. Confirming that a problem should be fixed belongs to this stage, not to design.
- auto 模型自主: the Agent acts on the problem directly.
- copilot 人工把关: the Agent identifies the subproblems, root cause, and scope; a human only confirms them or removes items from the Agent's list.
- pair 人工引导: a human contributes one or two key facts, such as a suspected direction, a key subproblem, a trigger condition, or a scope limit, or requires tracing the root cause; the Agent identifies the rest.
- assist 人工主导: a human supplies the list of problems or subproblems that make up the PR's scope, or the root-cause judgment, and sets the scope; the Agent traces the root causes or works from that list.
**design 方案设计**: who supplies the logical design, and how many trade-offs and detailed changes the human makes. Grade by who supplied the design structure that was adopted, not by who proposed candidates first: when the Agent proposed candidates and the human then supplied the adopted structure, the stage is assist. Design structure means the module split, data flow, key interfaces, or state transitions; for documentation, the directory, page split, and responsibility layering. Constraints stated as observable outcomes are constraints, not structure. A structure that the human reached through successive corrections counts as supplied by the human.
- auto 模型自主: a human adopts the Agent's design as proposed, or only confirms an option the Agent recommends.
- copilot 人工把关: a human chooses among the Agent's designs, or rejects parts of one.
- pair 人工引导: the Agent proposes the design structure; a human gives key constraints, names, or several revisions.
- assist 人工主导: a human supplies the design structure (module split, data flow, or key interfaces); the Agent fills in the mechanism or writes it up.
**testing 验收测试**: who sets the acceptance goals, test cases, and test matrix, and who checks the results. For documentation-only changes, the acceptance goals are content requirements. Instructions that only order the work, such as writing tests before the fix, do not count.
- auto 模型自主: the Agent sets the goals and cases; a human at most approves publishing or confirms a scope the Agent recommends.
- copilot 人工把关: a human reviews or comments on the cases, the test matrix, or the results the Agent reports.
- pair 人工引导: a human sets quantified targets or required scenarios, such as a trigger condition or a before-and-after contrast that must be covered, or, for documentation-only changes, content requirements; the Agent designs the concrete cases.
- assist 人工主导: a human defines the test matrix, its dimensions or rows, and runs or tries the result to check it; the Agent may fill in the values and cases.
**implementation 技术实现**: human intervention in the changed content, including production code, documentation (READMEs, JSDoc, Agent Notes, comments), CI, and release scripts.
- auto 模型自主: the Agent writes every change, and the records show no human diff review.
- copilot 人工把关: a human reviews the diff, including staging it piece by piece, and accepts it or makes a few comments that do not require redoing work.
- pair 人工引导: a human requires reverting or redoing any part of the work, or specifies a refactoring or an implementation approach.
- assist 人工主导: a human writes the key parts in the diff; the Agent completes the rest. Dictating content for the Agent to write is not writing it.
**review 评审处理**: who decides whether each handled review comment is fixed or declined. Comments from bots and from people both count; grade by who decided, not by who posted the reply. When no review comment has been handled, the line stays n/a, even if comments exist or arrived after merge.
- auto 模型自主: the Agent decides on and replies to every comment.
- copilot 人工把关: a human gives only a blanket authorization or category-level instructions, or responds to only a few items on a handling list the Agent prepared and leaves the rest to the Agent.
- pair 人工引导: a human responds to every item other than wording, in the threads or on the Agent's list, deciding to fix, decline, or defer, even with a short acceptance; merged duplicate comments count as one item, and wording comments may be handled by category; the Agent carries out the decisions and replies.
- assist 人工主导: a human posts replies to reviewers directly, or leads the discussion of disputed items; deciding while the Agent posts the replies is pair.
### Grading rules
- Grade from session records and task records. Messages a human sends in a subagent session count; the instructions an Agent writes to a subagent are not human messages.
- Count only work that ends up in this PR, including work from earlier sessions that ends up in this PR. A human's request to revert or redo work counts even when the reverted work does not remain in the PR.
- In breakdown, design, testing, and implementation, the human is the author working with the Agent; reviewers' approvals and comments count only in review.
- Process and ordering instructions, such as branching, working order, staging, and when to commit, push, or open the PR, do not count as human involvement in any stage.
- When the records suffice, grade at the level with the most human involvement whose criterion the records meet, and express the human and Agent shares through the autonomy value. Only when the records do not suffice, take the level with less human involvement that the records support.
- When a stage covers several changes, grade by the main change and state how the other changes differ in the description. The main change is the one with the largest share of the diff and review effort; when no single change dominates, grade by the change with the most human involvement. When later work changes which change is main, regrade the stage.
- Within the chosen level's range, choose a lower autonomy value the closer the human involvement comes to the criterion of the level with more human involvement, and a higher value the closer it comes to the level with less.
### Line format
The section starts with one level line, followed by one line per stage in the fixed order:
```markdown
## Human and Agent Roles
Levels and Agent autonomy: <ins>**hint 人工独立**</ins> 0–10%, <ins>**assist 人工主导**</ins> 15–25%, <ins>**pair 人工引导**</ins> 30–50%, <ins>**copilot 人工把关**</ins> 55–85%, <ins>**auto 模型自主**</ins> 90–100%; <ins>**n/a 暂不适用**</ins> marks a stage that does not exist or has not happened yet.
- <ins>**<level English name> <level Chinese name>**</ins> <autonomy> · <stage English name> <stage Chinese name> — <description>
```
- Write the level in bold and underlined; omit the autonomy value for n/a 暂不适用.
- Write the description in the PR description's language, in one or two sentences of at most 80 Chinese characters or 40 English words. State who made which decisions: name one or two representative decisions, and give only a count for the rest.
- State the description as settled fact, without hedges such as "not seen in the session" or "pending author confirmation".
- Refer to people as the author or a reviewer, never by username. Leave out judgments, session times, session ids, direct quotes, and the running or handling of checks and CI (local skips, hooks, reruns, unrelated failures).
### Update timing
- When you create the PR, the review line reads `- <ins>**n/a 暂不适用**</ins> · review 评审处理 — ` followed by a statement, in the description's language, that no review comments have been handled yet.
- When the description is updated after the PR exists, rewrite the affected lines so that they cover all work so far, including earlier stages whose decisions or human share the later work changed; do not append a history of changes.
- The review line records only handled review comments; it omits approvals, merge timing, and unhandled comments.
- A review round ends when its fixes are pushed and its comments answered. Comments that arrive after merge do not trigger an update.
### Example
Filled in from the session and review records of #6013:
```markdown
- <ins>**pair 人工引导**</ins> 40% · breakdown 问题拆解 — The author identified two process-specific values that do not belong in the system prompt and pointed to the custom model route; the Agent traced the root cause.
- <ins>**assist 人工主导**</ins> 20% · design 方案设计 — The author supplied the design structure: runtime context, frozen legacy sections for old sessions, and a separate package; the Agent filled in the projection matching.
- <ins>**pair 人工引导**</ins> 40% · testing 验收测试 — The author required two scenarios: restoring an old session after upgrade, and changing ports after upgrade; the Agent wrote the cases and snapshots.
- <ins>**pair 人工引导**</ins> 35% · implementation 技术实现 — The Agent wrote the code and the Agent Note; the author required one revert and specified 3 approaches, such as getOrigin() instead of an origin property.
- <ins>**pair 人工引导**</ins> 40% · review 评审处理 — The author decided each bot comment, such as no upgrade guide and keeping a devDependency; the Agent fixed and replied.
```
## Creating and updating the description
1. Write the description to a file that git ignores, such as `.artifacts/pr/body.md`. Create the PR with `gh pr create --body-file <file>`, or update it with `gh pr edit <number> --body-file <file>`; when the description embeds a GIF, follow [record-browser-gif](../record-browser-gif/SKILL.md) and use `--attach`.
2. Before an update, save the current description with `gh pr view <number> --json body --jq .body`, and change only the sections that need it: keep content and media the author wrote, and remove statements that no longer match the code.
3. After writing, read the description back and confirm that all four sections are present and that Human and Agent Roles matches the current code and review state.
+36 -8
View File
@@ -206,19 +206,47 @@ test('keeps Task Issues limited to summary and deliverables', () => {
)
})
test('structures the pull request template around motivation, changes, and testing', () => {
test('keeps the pull request template aligned with the dsh-pr-description skill', () => {
const source = readFileSync(new URL('../pull_request_template.md', import.meta.url), 'utf8')
for (const heading of ['## Motivation', '## Changes', '## Testing']) {
assert.match(source, new RegExp(`^${heading.replaceAll('#', '\\#')}$`, 'm'))
}
const skillPath = '.agents/skills/dsh-pr-description/SKILL.md'
const skill = readFileSync(new URL(`../../${skillPath}`, import.meta.url), 'utf8')
assert.ok(source.startsWith(`<!-- 本 PR 正文按 ${skillPath} 填写。 -->\n`), 'the template points to the skill')
assert.deepEqual(source.match(/^## .+$/gm), [
'## Motivation',
'## Changes',
'## Testing',
'## Human and Agent Roles',
])
assert.doesNotMatch(source, /^### /m)
assert.match(
source,
/<!-- 高层次说明命令[^\n]+ -->\n<!-- 高层次说明用户[^\n]+ -->/,
)
assert.match(source, /^## Motivation\n\n<!-- [^\n]*Fixes #N[^\n]* -->$/m, 'the template prompts for an Issue reference')
assert.match(source, /- <!-- [^\n]+ -->\n\n <details>\n <summary>Proof<\/summary>/)
assert.equal(source.match(/<details>/g)?.length, 1)
assert.equal(source.match(/<\/details>/g)?.length, 1)
const levelLine = /^Levels and Agent autonomy: .+$/m
assert.ok(skill.match(levelLine), 'the skill shows the role level line')
assert.equal(source.match(levelLine)?.[0], skill.match(levelLine)?.[0], 'the template level line matches the skill')
const stagesSection = skill.match(/^### Stages and criteria\n([\s\S]*?)(?=^### )/m)?.[1]
assert.ok(stagesSection, 'the skill has a Stages and criteria section')
const skillStages = [...stagesSection.matchAll(/^\*\*(.+?)\*\*:/gm)].map(([, stage]) => stage)
assert.ok(skillStages.length > 0, 'the skill lists role stages')
for (const stage of skillStages) {
assert.match(stage, /^[a-z]+ [\u4e00-\u9fff]+$/, `stage name pairs one English word with a Chinese name: ${stage}`)
}
const reviewStage = 'review 评审处理'
assert.equal(skillStages.at(-1), reviewStage, 'review is the last stage')
assert.deepEqual(
[...source.matchAll(/^- <ins>\*\*(?:待填级别\*\*<\/ins> \?%|n\/a 暂不适用\*\*<\/ins>) · (.+?) — .+$/gm)].map(([, stage]) => stage),
skillStages,
'the template has one line per skill stage, in skill order',
)
for (const stage of skillStages.slice(0, -1)) {
assert.ok(source.includes(`- <ins>**待填级别**</ins> ?% · ${stage} — 待填说明\n`), `${stage} has a placeholder line`)
}
assert.ok(
source.includes(`- <ins>**n/a 暂不适用**</ins> · ${reviewStage} — 尚无已处理的评审意见。\n`),
'the review line starts as n/a, as the skill requires at PR creation',
)
})
test('ignores Issue title, body presentation, and assignee ownership', () => {
+15 -8
View File
@@ -1,21 +1,28 @@
<!-- 本 PR 正文按 .agents/skills/dsh-pr-description/SKILL.md 填写。 -->
## Motivation
<!-- 用一句话说明需要解决的问题,并以 Fixes #NN 或 Related #NN 引用同仓库 Issue。 -->
<!-- 引用同仓库 Issue:Fixes #N 或 Refs #N。 -->
## Changes
<!-- 高层次说明命令、配置、API、协议或持久化格式的变化;没有则写 None。 -->
<!-- 高层次说明用户、模型或系统可观察行为的变化;没有则写 None。 -->
## Testing
<!-- 每种测试方法添加一个条目。方法保持可见,将可复核证据放进对应的 Proof 区域。 -->
<!-- 测试设计 -->
- <!-- 说明命令或步骤,以及它覆盖的行为。 -->
- <!-- 测试条目 -->
<details>
<summary>Proof</summary>
<!-- 提供测试输出、截图、录屏、日志或其他可复核证据。 -->
</details>
## Human and Agent Roles
Levels and Agent autonomy: <ins>**hint 人工独立**</ins> 0–10%, <ins>**assist 人工主导**</ins> 15–25%, <ins>**pair 人工引导**</ins> 30–50%, <ins>**copilot 人工把关**</ins> 55–85%, <ins>**auto 模型自主**</ins> 90–100%; <ins>**n/a 暂不适用**</ins> marks a stage that does not exist or has not happened yet.
- <ins>**待填级别**</ins> ?% · breakdown 问题拆解 — 待填说明
- <ins>**待填级别**</ins> ?% · design 方案设计 — 待填说明
- <ins>**待填级别**</ins> ?% · testing 验收测试 — 待填说明
- <ins>**待填级别**</ins> ?% · implementation 技术实现 — 待填说明
- <ins>**n/a 暂不适用**</ins> · review 评审处理 — 尚无已处理的评审意见。
+6 -8
View File
@@ -415,13 +415,8 @@ jobs:
max-parallel: 1
matrix:
include:
- node: '22.19'
name: node 22.19
runner: ubuntu-latest
gate_concurrency: '1'
# Pinned inside 24.0-24.11.1: those releases carry the v1 internal
# loader while reporting major 24, and every other job tracks the
# latest 24, which is v2. A bare `24` here would retest that same v2.
# Pin the supported floor, whose internal loader is v1. Primary
# jobs track latest 24 and exercise v2, introduced in 24.12.0.
- node: '24.9'
name: node 24.9
runner: ubuntu-latest
@@ -575,12 +570,15 @@ jobs:
- name: Parse Windows runner supervision scripts
shell: pwsh
run: |
foreach ($path in @('apps/desktop/scripts/windows-runner-task.ps1', 'apps/desktop/scripts/run-windows-runner-task.ps1', 'apps/desktop/tests/windows-runner-task.ps1')) {
foreach ($path in @('apps/desktop/scripts/windows-runner-task.ps1', 'apps/desktop/scripts/run-windows-runner-task.ps1', 'apps/desktop/tests/windows-runner-task.ps1', 'apps/desktop/tests/windows-runner-launcher.ps1')) {
$tokens = $null
$parseErrors = $null
$null = [Management.Automation.Language.Parser]::ParseFile((Join-Path $env:GITHUB_WORKSPACE $path), [ref]$tokens, [ref]$parseErrors)
if ($parseErrors.Count) { throw ($parseErrors -join "`n") }
}
- name: Verify windowless Windows runner launcher
shell: pwsh
run: ./apps/desktop/tests/windows-runner-launcher.ps1
- name: Run Windows observational gates on built artifacts
id: observational
continue-on-error: true
+6
View File
@@ -74,6 +74,9 @@ jobs:
- uses: actions/[email protected]
with:
node-version: '24'
- name: Check local publication configuration before dependency installation
working-directory: desktop-publisher
run: node apps/desktop/scripts/preflight-desktop-publish-local.mjs
- run: pnpm install --frozen-lockfile
working-directory: desktop-publisher
- name: Resolve retained package and approval environment
@@ -115,6 +118,9 @@ jobs:
- uses: actions/[email protected]
with:
node-version: '24'
- name: Check local publication configuration before dependency installation
working-directory: desktop-publisher
run: node apps/desktop/scripts/preflight-desktop-publish-local.mjs
- run: pnpm install --frozen-lockfile
working-directory: desktop-publisher
- name: Verify local package without upload credentials
+3 -2
View File
@@ -81,7 +81,7 @@ Package groups: [packages/README.md](packages/README.md). [Experimental status](
## Commands
```sh
pnpm install # pnpm workspaces, node ^22.19 || >=24
pnpm install # pnpm workspaces, node >=24.9
pnpm run clean # remove build outputs and safe residue from deleted packages
pnpm run test # unit tests
pnpm run test:coverage # CI coverage gate: per-file 100% on packages/*/*/src
@@ -129,7 +129,7 @@ Real-API tests/demos read `DEEPSEEK_API_KEY`, optional `DEEPSEEK_BASE_URL`, and
## Conventions
- Packages use `@deepseek-ai/dsh-<name>`; vendor is [rescoped](docs/rescope.md) and `private: true`. Harness packages declare `@deepseek-ai/cordis` in `peerDependencies`/`devDependencies`. Workspace dependency sections use DSH `workspace:*`, vendor/native `workspace:~` ([rules](.agents/notes/implemented/process/2026-08-10-npm-release-sequences.md)).
- ESM everywhere (`"type": "module"`). Use package names across packages and `.ts` in local relative imports. Config subprocesses run built `lib/` under plain Node; source regressions use their declared launcher ([testing policy](docs/testing.md#test-subprocess-launch-modes)). The `dsh` CLI source launch runs through tsx's ESM-only hook (`node --import tsx/esm`); modules it reaches must stay ESM (no CJS-only exports) — Node's native TypeScript modes are unavailable across the engines range ([source-launch contract](.agents/notes/implemented/architecture/2026-07-29-dsh-source-launch-tsx-esm.md)). Raw/Web `cordis.yml` bare plugins must appear in their resolver manifest's `dependencies`; `verify-cordis-config` enforces it.
- ESM everywhere (`"type": "module"`). Use package names across packages and `.ts` in local relative imports. Config subprocesses run built `lib/` under plain Node; source regressions use their declared launcher ([testing policy](docs/testing.md#test-subprocess-launch-modes)). The `dsh` CLI source launch runs through tsx's ESM-only hook (`node --import tsx/esm`); modules it reaches must stay ESM (no CJS-only exports) ([source-launch contract](.agents/notes/implemented/architecture/2026-07-29-dsh-source-launch-tsx-esm.md)). Raw/Web `cordis.yml` bare plugins must appear in their resolver manifest's `dependencies`; `verify-cordis-config` enforces it.
- **Registrations are effects**: every contribution goes through `ctx.effect()` / `ctx.on()`; a registry's `register()` returns the disposer.
- **Typed events use declaration merging** and merge-extensible maps. Event JSDoc needs `@mode` and payload `@param`. Public service methods document parameters and non-void returns. `SessionEventMap` members are required-on-read unless the envelope carries `ignorable: true`. Bump `SESSION_FORMAT_VERSION` only when existing discriminators cannot prevent unsafe interpretation; same-version changes require safe reader behavior ([review](.agents/notes/implemented/process/2026-10-08-session-reader-compatibility-review.md)).
- **Switch on discriminant tags.** Closed unions end in `assertNever`; merge-extensible unions fall through a documented default.
@@ -161,6 +161,7 @@ Real-API tests/demos read `DEEPSEEK_API_KEY`, optional `DEEPSEEK_BASE_URL`, and
- **Both SDKs project the loop.** Agent-loop, session-lifecycle, and `SessionEventMap` changes update the TypeScript and Python SDK expected outputs in the same PR; `pnpm run test` covers neither ([surfaces](docs/testing.md#when-a-snapshot-test-is-required)).
- **Choose PR history deliberately.** Split independent changes and fix the introducing PR before propagation. Standalone/stack branches may merge-forward or rebase. Rewrites use `--force-with-lease`, abort on remote movement, never raw `--force`; preserve an in-progress merge-forward checkpoint before taking a newer base ([rationale](.agents/notes/implemented/process/2026-08-02-native-github-stacks-and-optional-rebases.md)).
- **Labels:** one PR `kind/*`, all material `area/*`, and native Issue Type ([taxonomy](.agents/notes/implemented/process/2026-08-08-unified-github-label-taxonomy.md)).
- **PR descriptions:** write and update every PR description with [dsh-pr-description](.agents/skills/dsh-pr-description/SKILL.md).
- TODO markers: `FIXME`/`TODO`/`XXX` by urgency ([semantics](docs/development.md)).
- Files end with exactly one trailing newline; `git diff --cached --check` (pre-commit) gates it.
Loaded 100 of 1157 files, more files were not shown because too many files have changed in this diff. Show more