Merge remote-tracking branch 'origin/master' into turtle/image-cache-stale-0a243f674e12

# Conflicts:
#	packages/client/ui-primitives/README.i18n.yaml
#	packages/client/ui-primitives/README.md
#	packages/client/ui-primitives/README.zh.md
This commit is contained in:
Turtle committed 2026-10-18 14:49:15 +08:00
commit ef0a2b2e3d
4305 files changed
+174021 -23401

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
@@ -18,14 +18,14 @@
en: 5fdde26e9e8c6108
zh: 629559d733c1f0c5
/agent-note-single-file-executable-sdk-runtime-distribution-single-exe/decision/plugin-resolution-the-vfs-holds-a-real-package-tree-the-closure-manifest-is-the-deploy-root:
en: c35ea4b858448b49
zh: 1f8ff0a9f82ca881
en: 985e0eb63f3d1fd6
zh: 337cc44fadc6227e
/agent-note-single-file-executable-sdk-runtime-distribution-single-exe/decision/build-pipeline-and-artifacts:
en: d3bbd24ead02af4f
zh: 64623d88bee4314b
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: 9bd60549d4a52ec0
zh: e2343b7210212ee0
en: b2a740a1ab7a5002
zh: a48be6ca6fc65912
/agent-note-single-file-executable-sdk-runtime-distribution-single-exe/decision/naming-lineage:
en: 31a5aecb8bf828bb
zh: 2dffb6fd2e0f8194
@@ -33,8 +33,8 @@
en: 0426f598f3f44e64
zh: fc66728e17f3bcfb
/agent-note-single-file-executable-sdk-runtime-distribution-single-exe/testing:
en: b3c9820f4dc7c6c8
zh: 3b5768ef1770bfcb
en: 8e7f46bbb595afa9
zh: 4641894ed285d717
/agent-note-single-file-executable-sdk-runtime-distribution-single-exe/alternatives-considered:
en: 50d965ca81a401ba
zh: 0c6296245b97b9ac
@@ -44,19 +44,19 @@ The deploy root is [`python/sdk-runtime/package.json`](../../../../python/sdk-ru
The deploy root includes `@deepseek-ai/dsh-mcp-client` as an explicitly supported custom-configuration plugin even though no shipped preset mounts it. An external config can therefore connect to user-supplied stdio and Streamable HTTP MCP servers and register their tools; the distribution does not carry those servers. The optional resource service provides MCP Resources; MCP Prompts remain unsupported. The executable and installed-wheel smokes start a temporary stdio server, discover its tool, and complete one model-requested call.
The [Office kit](2026-09-14-independent-libreoffice-kit.md) and its installed dependency tree live in `<executable-stem>-office/node_modules`, preserving complete package contents and helper permissions. Top-level `pkg.ignore` excludes kit packages from the SEA archive, including assets selected by dependency `files` fields; small dependencies shared with other consumers remain available in the VFS. The private Python bootstrap uses Node `registerHooks` to resolve the kit entry and its package manifest from the real directory. Native executables and URL Workers therefore retain their ordinary paths without modifying the kit or the public CLI. Missing required packages and missing installed package manifests fail the copy; absent optionals preserve the kit's engine selection.
The [Office kit](2026-09-14-independent-libreoffice-kit.md) and its installed dependency tree live in the explicitly downloaded sidecar’s `node_modules`, preserving complete package contents and helper permissions. Top-level `pkg.ignore` excludes kit packages from the SEA archive, including assets selected by dependency `files` fields; small dependencies shared with other consumers remain available in the VFS. The private Python bootstrap uses Node `registerHooks` to resolve the kit entry and its package manifest from the real directory. Native executables and URL Workers therefore retain their ordinary paths without modifying the kit or the public CLI. The build locks the selected engine and dependency locations to exact npm tarballs; downloads verify their integrity before publication.
### 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 target-specific Office directory 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, `-office/` directory, 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/node_modules/@deepseek-ai/dsh/lib/bin.js` 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, its Office directory, 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.
[`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.
The Python client launches the packaged `dsh` command with the selected profile (`sdk` by default), ordered patch files, and an explicit Harness home. The profile owns JSON-RPC serving and application composition; missing homes, profiles, bundles, patches, and server rows fail without an external complete-config fallback.
@@ -74,7 +74,7 @@ The verification surface has three tiers. Mechanism tier: the measured conclusio
Manual-driving caveat: the bin treats stdin EOF as "the client is gone" and disposes immediately, so a short-lived pipe aborts an in-flight turn — pipe-driven runs must keep stdin open until the turn ends.
The installed-wheel `sdk-office` scenario copies the target's complete payload to a private temporary directory, requires exactly one target backend (the declared native target, or WASM when no native target is declared), and converts DOCX once through the shipped SDK profile with that engine. It checks that the kit module resolves inside the relocated directory, that conversion reports the expected backend, and that the output contains PDF bytes. Wheel validation checks the declared engine assets and native helper execute bit; CI uploads the completed wheel, so nested helper permissions stay inside the ZIP archive.
The installed-wheel `sdk-office` scenario relocates the lightweight wheel payload and explicitly downloads the complete npm Office sidecar with its independent Node. It requires exactly one target backend, verifies that the SDK loads the kit from the downloaded sidecar, and converts DOCX through the skill CLI with an empty PATH. Python Office operations use the same CLI. Explicit downloads reuse the existing build preparation implementation; SDK startup performs no downloads. The [runtime reference](../../../../python/sdk-runtime/README.md) owns cache and activation behavior.
## Alternatives considered
@@ -44,19 +44,19 @@ exe 的 VFS 内是**构建产物形态的真实包树**(各包的 `lib/` + 真
部署根目录显式包含 `@deepseek-ai/dsh-mcp-client`,将其作为自定义配置可用的插件,即使随附 preset 均未挂载该插件。外部配置因此可以连接由用户提供的 stdio 与 Streamable HTTP MCP server 并注册其工具;分发物不包含这些 server。可选资源服务提供 MCP Resources;MCP Prompts 仍不受支持。可执行程序与已安装 wheel 包的冒烟测试会启动临时 stdio server,发现其工具,并完成一次由模型请求的调用。
[Office kit](2026-09-14-independent-libreoffice-kit.zh.md) 及其已安装依赖树位于 `<executable-stem>-office/node_modules`,保留完整包内容与辅助程序权限。顶层 `pkg.ignore` 从 SEA 归档排除 kit 包,包括依赖 `files` 字段选中的资源;与其他使用方共享的小型依赖仍可在 VFS 中使用。Python 私有 bootstrap 使用 Node `registerHooks` 从真实目录解析 kit 入口及其包清单。因此原生可执行程序与 URL Worker 保留普通文件路径,无需修改 kit 或公开 CLI。必需包缺失或已安装包缺少清单会使复制失败;未安装的可选包保留 kit 自身的引擎选择规则。
[Office kit](2026-09-14-independent-libreoffice-kit.zh.md) 及其已安装依赖树位于显式下载的 sidecar 的 `node_modules`,保留完整包内容与辅助程序权限。顶层 `pkg.ignore` 从 SEA 归档排除 kit 包,包括依赖 `files` 字段选中的资源;与其他使用方共享的小型依赖仍可在 VFS 中使用。Python 私有 bootstrap 使用 Node `registerHooks` 从真实目录解析 kit 入口及其包清单。因此原生可执行程序与 URL Worker 保留普通文件路径,无需修改 kit 或公开 CLI。构建将所选引擎和依赖位置锁定到确切的 npm 归档;下载会在发布缓存前校验完整性。
### 构建流水线与产物
[`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 目录一同拷回运行时目录。该 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/` 目录和可选的 macOS helper,以及供仓库开发使用的构建注入 `runtime/node/` 闭包树。`resolve_bundled_launch_args()` 默认选择可执行文件;显式设置 `DSH_RUNTIME_MODE=node` 会在系统 Node 22.19 或更高版本上运行 `runtime/node/node_modules/@deepseek-ai/dsh/lib/bin.js`。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、其 Office 目录及架构匹配的 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 包。
[`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 包。
Python 客户端使用所选 profile(默认 `sdk`)、有序 patch 文件和显式 Harness home 启动打包后的 `dsh` 命令。Profile 负责 JSON-RPC 服务和应用组合;缺失 home、profile、bundle、patch 或 server 配置项都会失败,不存在外部完整配置回退。
@@ -74,7 +74,7 @@ Python 客户端使用所选 profile(默认 `sdk`)、有序 patch 文件和
手工驱动注意:`bin` 将 stdin EOF 视为「客户端已离开」并立即 dispose,生命周期较短的管道会中止进行中的轮次——管道驱动必须保持 stdin 打开,直到轮次结束。
安装后 wheel 的 `sdk-office` 场景会将目标的完整载荷复制到私有临时目录,要求仅存在一个目标后端(使用已声明的原生目标,未声明原生目标时使用 WASM),并通过随附 SDK profile 使用该引擎转换一次 DOCX。它检查 kit 模块确实解析到迁移后的目录、转换报告预期后端,且输出包含 PDF 字节。wheel 校验会检查声明的引擎资源与原生辅助程序执行位;CI 上传已完成的 wheel,因此嵌套辅助程序权限保留在 ZIP 归档中。
安装后 wheel 的 `sdk-office` 场景迁移轻量 wheel 载荷,并显式下载完整 npm Office sidecar 及其独立 Node。它要求仅有一个目标后端,验证 SDK 从下载的 sidecar 加载 kit,并通过 skill CLI 在空 PATH 下转换 DOCX。Python Office 操作使用同一 CLI。显式下载复用现有构建准备实现;SDK 启动不执行下载。[运行时参考](../../../../python/sdk-runtime/README.zh.md)拥有缓存和启用行为的说明。
## 曾考虑的替代方案
@@ -27,8 +27,8 @@
en: fbd101d4576afa92
zh: 35f5d3c2f6cad6a8
/agent-note-cooperative-tool-cancellation-at-the-registry-boundary/alternatives-considered:
en: 1f205f1a78d4fd5b
zh: 6c17e35da2909302
en: c4d86feb135d2ac4
zh: 67c56c0bc2213949
/agent-note-cooperative-tool-cancellation-at-the-registry-boundary/consequences:
en: 24a9ad2c4c7c86b5
zh: debdd29d272e07ea
@@ -62,7 +62,7 @@ No registry test can prove that arbitrary third-party same-process code observes
**Forbid around wrappers from replacing the signal.** Rejected because deadlines and nested operational scopes need lexical derivation. Capturing and fusing the caller signal preserves composition without allowing detachment.
**Race the tool promise against cancellation.** Rejected because it reports completion while side effects may remain live, violating the [quiescent-disposal rule](../../../../docs/defensive-patterns.md#dispose-must-reach-quiescence-not-just-request-it).
**Race the tool promise against cancellation.** Rejected because it reports completion while side effects may remain live, violating the [quiescent-disposal rule](../../../../docs/defensive-patterns/general.md#dispose-must-reach-quiescence-not-just-request-it).
## Consequences
@@ -62,7 +62,7 @@ Status: implemented
**禁止环绕包装层替换信号。** 不予采纳,因为截止时间和嵌套操作作用域需要词法派生信号。捕获并融合调用方信号既保留组合能力,也不允许切断调用方取消。
**让工具 promise 与取消竞速。** 不予采纳,因为这种方式会在副作用仍可能存活时报告完成,违反[dispose(资源释放)必须完全停稳的规则](../../../../docs/defensive-patterns.zh.md#dispose-must-reach-quiescence-not-just-request-it)。
**让工具 promise 与取消竞速。** 不予采纳,因为这种方式会在副作用仍可能存活时报告完成,违反[dispose(资源释放)必须完全停稳的规则](../../../../docs/defensive-patterns/general.zh.md#dispose-must-reach-quiescence-not-just-request-it)。
## 后果
@@ -27,8 +27,8 @@
en: 37e2fa1fe5cb2b95
zh: 80e513db87709cb4
/agent-note-client-plugin-loading-lazy-factories-cordis-lifecycle-and-hot-reload/decision/hot-reload-one-driver-plugin-self-watched-bundles:
en: f8b591d4c41ef492
zh: bc70f1cd540b7753
en: c1fdcb3e6f074a7a
zh: b08c7b1ff25acb09
/agent-note-client-plugin-loading-lazy-factories-cordis-lifecycle-and-hot-reload/package-ownership:
en: 94ca0d09e6fd1e3a
zh: d5949a2bdec795ba
@@ -94,7 +94,7 @@ On the browser side, the transport delegates code replacement to the same module
Bootstrap replacement is rejected before invalidation or teardown: the module system retains its initial exports, so remounting its old code would reset consumers without applying the requested revision. The page reports the required reload and preserves the bootstrap fiber. Every non-bootstrap plugin shares the replacement semantics; an `immediately` row reloads exactly like a lazy one. Dependency cascade costs zero client code: a fiber's activation epoch strings its service providers' uids, so replacing a foundational provider such as connection re-loads every dependent through cordis itself — correct, if heavy.
Reload creates a fresh fiber and component state; it does not preserve React state inside the replaced plugin. Static assembly libraries and the shell require a rebuilt page. Failed imports or activation remain diagnosable and retryable without rolling back unrelated plugins. Self-reload closes the old SSE channel and opens a new one; its full snapshot repairs missed graph changes. Boot, graph updates and rebuilt frames share the same queue, so a code replacement cannot overlap initial module arrival.
Reload creates a fresh fiber and component state; it does not preserve React state inside the replaced plugin. Static assembly libraries and the shell require a rebuilt page; when the Host itself serves another release, the page holds plugin replacement and asks whether to reload ([Host release change prompt](../feature/2026-10-14-host-release-change-refresh-prompt.md)). Failed imports or activation remain diagnosable and retryable without rolling back unrelated plugins. Self-reload closes the old SSE channel and opens a new one; its full snapshot repairs missed graph changes. Boot, graph updates and rebuilt frames share the same queue, so a code replacement cannot overlap initial module arrival.
## Package ownership
@@ -94,7 +94,7 @@ Host SSE 适配器转发现有图变化通知,并在连接时发送当前完
Bootstrap 替换会在失效或卸载前被拒绝:模块系统保留其初始导出,重新挂载旧代码会重置消费者,却无法应用请求的 revision。页面会报告需要刷新,并保留 bootstrap fiber。所有非 bootstrap 插件都共享同一套替换语义;`immediately` 行的重载与 lazy 行分毫不差。依赖级联不花一行 client 代码:fiber 的激活纪元串接着它各服务提供方的 uid,因此替换 connection 等基础 provider 的 fiber 时,每个依赖方都会经 cordis 本身重新装载——行为正确,但代价较高。
重载会创建新的 fiber 和组件状态,不保留被替换插件内部的 React 状态。静态组装库与应用壳需要重建后的页面。导入或激活失败仍可诊断和重试,不会回滚无关插件。自重载关闭旧 SSE 通道并打开新通道;其完整快照补齐遗漏的图变更。启动、图更新和重建帧共用同一队列,因此代码替换不会与初始模块到达重叠。
重载会创建新的 fiber 和组件状态,不保留被替换插件内部的 React 状态。静态组装库与应用壳需要重建后的页面;Host 本身换成另一个 release 时,页面会暂停插件替换并询问是否刷新([Host release 变化提示](../feature/2026-10-14-host-release-change-refresh-prompt.zh.md))。导入或激活失败仍可诊断和重试,不会回滚无关插件。自重载关闭旧 SSE 通道并打开新通道;其完整快照补齐遗漏的图变更。启动、图更新和重建帧共用同一队列,因此代码替换不会与初始模块到达重叠。
## 包归属
@@ -18,5 +18,5 @@
en: fa503dcfaa6f438f
zh: 26052212a124e133
/agent-note-compiler-independent-typert-type-model/consequences:
en: 4a472acf377f8361
zh: cb83602b123a4aae
en: e1022cf9523fb28e
zh: 01dfb764fb98de0d
@@ -51,3 +51,5 @@ For each supported node kind and literal category, Zod emitter tests run both su
New generation targets and static checks can reuse the same TypeGraph, and business categories can extend PackageModel without parsing the AST again. Preserving pre-evaluation types and independent faces makes the model more complex than a flattened schema; emitters must explicitly declare their supported scope and fail on missing capabilities.
Explicit package opt-in keeps artifact publication and exports under package ownership. Repository orchestration may still run the whole-workspace Host contract pass for every opted-in package; that pass remains owned by the later Remote Gateway Agent Note. The static Cordis catalogs remain reproducible from the canonical model without coupling `tool-cordis` to runtime registry state. `ctx.typert` reflects only artifacts mounted in the current runtime, and unloading does not control Zod instances that consumers retain after materializing them.
Ordinary Host and Remote artifacts reuse this model to emit one static method spec per endpoint. Each parameter has normal `codec` and `strictCodec` variants derived from the same checker-resolved type model; neither analysis nor the model carries a per-method input policy. A pure factory returns a lazy info instance whose direct method index and descriptor list share objects; the existing Registry retains those descriptors, while `TYPERT_REMOTE.methods` supplies exact static types. The [unified Typert/Remote metadata note](../../proposed/architecture/2026-09-20-unified-typert-remote-metadata.md) records this delivery mechanism and the incomplete downstream adaptation. Declaration and face-isolation rules remain unchanged; separate Programs and processes do not share object identity.
@@ -51,3 +51,5 @@ Zod emitter 对支持的节点和各类 literal 逐类执行成功与失败 pars
新增生成目标或静态检查可复用同一 TypeGraph,业务类目也可在 PackageModel 上扩展,而无需再次解析 AST。保留计算前类型和独立 face 的代价是模型比打平后的 schema 更复杂,emitter 必须显式声明支持范围并对缺失能力失败。
包级显式 opt-in 使产物发布与 exports 由各包自行管理。仓库编排仍可为每个已 opt-in 的包运行全仓 Host 约定 pass;该 pass 仍由后续 Remote Gateway Agent Note 负责说明。静态 Cordis catalog 可从标准模型复现,同时不把 `tool-cordis` 与运行时注册表状态耦合。`ctx.typert` 只反映当前运行时中已挂载的产物;对于消费方物化后仍持有的 Zod 实例,卸载流程无法控制。
普通 Host 与 Remote 产物复用此模型,为每个端点生成一份静态方法 spec。每个参数都有从同一 checker 解析后类型模型派生的普通 `codec` 和 `strictCodec` 变体;分析过程与模型均不携带逐方法输入策略。纯 factory 返回惰性 info 实例,其直接方法索引与描述符列表共享对象;既有 Registry 保留这些描述符,而 `TYPERT_REMOTE.methods` 提供精确静态类型。[统一 Typert/Remote 元数据 note](../../proposed/architecture/2026-09-20-unified-typert-remote-metadata.zh.md)记录该交付机制及尚未完成的下游适配。声明与编译面隔离规则保持不变;独立 Program 和进程不共享对象身份。
@@ -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。
@@ -12,14 +12,14 @@
en: 44d946b53496974c
zh: 403447c25a5b2d7c
/agent-note-typert-gateway-targeted-method-calls/components-and-cordis-services:
en: aa7f509189ade32e
zh: ef2841bc793f31b2
en: 02c20706e48467e6
zh: 2b0916ae0cee351b
/agent-note-typert-gateway-targeted-method-calls/business-declarations:
en: c6b70d442b137d2e
zh: 6d0dbefe89937ef1
en: b9394d5ef98cc82a
zh: 1fce07fe150722ee
/agent-note-typert-gateway-targeted-method-calls/decorators-and-the-explicit-gateway-facet:
en: 155afbe4b8af600b
zh: 14705698a9cdbd84
en: 161dccf2b4cf565d
zh: fa587fe0b46bf551
/agent-note-typert-gateway-targeted-method-calls/lookup-and-remote-scope-registration:
en: 9f7448898cf5cadc
zh: 419573c589ed6f7d
@@ -33,8 +33,8 @@
en: a244f6a88fdabf3d
zh: e4feedb7297f4865
/agent-note-typert-gateway-targeted-method-calls/three-artifact-kinds-and-two-typescript-programs:
en: 6ea4bddcf35a834d
zh: 4af474c6818b1cf0
en: c5bcd6eb76bf3d42
zh: 9df2fbb364643186
/agent-note-typert-gateway-targeted-method-calls/the-remote-package-entry:
en: 75e977fe0fb71d59
zh: 00760c79da173cb7
@@ -48,8 +48,8 @@
en: 5d14e1b7afc53efb
zh: e2f4b5814083738f
/agent-note-typert-gateway-targeted-method-calls/src-and-lib-operating-modes:
en: 1e366dcfab74831b
zh: 14199f25f20a9352
en: f1b8426d1cfaed35
zh: a370376923c98d6e
/agent-note-typert-gateway-targeted-method-calls/host-gateway-resolution:
en: 88a95d7ab4621caf
zh: 89b63fb6440fca79
@@ -66,8 +66,8 @@
en: b1c0c3dd77c56ce8
zh: 769ffd4aeefd949c
/agent-note-typert-gateway-targeted-method-calls/alternatives-considered:
en: 0d68c499b7780f4f
zh: bdd1e57232b996e2
en: eaa3ffe5ca14005c
zh: 1f0b455e30ade872
/agent-note-typert-gateway-targeted-method-calls/verification:
en: 7433bdb522bb2083
zh: 545c17613b636f00
@@ -28,7 +28,7 @@ The Remote consumer projection contains `.d.ts`, `.d.ts.map`, and `.js` files. T
| Component | Cordis service | Responsibility |
|---|---|---|
| `@deepseek-ai/dsh-typert-protocol` | Declares only the minimal `ctx.typert` protocol | `TypertRemoteService`, decorators, binding fallback, descriptors, lookup/Context, and the Remote map; no dependency on the compiler, Zod, Connection, or Browser |
| `@deepseek-ai/dsh-typert-protocol` | Declares only the minimal `ctx.typert` protocol | `TypertRemoteService`, decorators, binding fallback, descriptors, lookup/Context, the Remote map, and Zod-backed lazy info assembly; no dependency on the compiler, Connection, or Browser |
| Typert registry | `ctx.typert` | Separately stores reflection for the current environment, imported Remote contributions, lookup providers, and Context providers |
| Typert generator/loader | No new business service | Generates three kinds of `lib` artifacts from the Host/Client Programs and registers the current environment's artifacts with `ctx.typert` |
| API Gateway's Host face | `ctx.typertGateway` | Associates Host definitions with live Services, decodes parameters, resolves receivers, invokes methods, and projects unary results into carrier-neutral fields and attachments |
@@ -81,16 +81,18 @@ export class ScopedGoalService extends TypertRemoteService {
An endpoint selects exactly one invocation mode. A flow that needs an explicit `Agent` parameter uses `@Remote`. A flow that first switches to an Agent Context and then resolves a scoped receiver uses `@RemoteScope('agent')`. Typert does not infer either mode from the method body or from a missing parameter.
Business packages depend only on the lightweight `@deepseek-ai/dsh-typert-protocol`. It provides `TypertRemoteService` and declaration protocols for decorators, the binding fallback, lookup, Remote Scope, and descriptors, without depending on the TypeScript compiler, Zod, HTTP, or the Client runtime.
Business packages depend only on `@deepseek-ai/dsh-typert-protocol`. It provides `TypertRemoteService` and declaration protocols for decorators, the binding fallback, lookup, Remote Scope, and descriptors, without depending on the TypeScript compiler, HTTP, or the Client runtime. Its pure `./remote-info` entry uses Zod to assemble lazy named-input schemas without importing Cordis; the root entry also re-exports it alongside Cordis-backed declarations.
A method that cooperatively supports cancellation declares `signal: AbortSignal` as its final Host parameter. This reserved parameter is not a business value, lookup, or JSON field. The generated consumer method exposes it as a final optional parameter so ordinary calls remain unchanged while callers that own cancellation can pass a signal.
## Decorators and the explicit Gateway facet
A decorator only states that a method participates in the Remote contract. It performs no runtime type reflection and injects no hidden symbol into a Service constructor. The arguments to `@Remote('create')` and `@RemoteScope('agent', 'create')` are external method names; the decorated member may be the business method itself or an adapter such as `remoteExportCreate`. The member name becomes the external method name only when no alias is provided. Inheriting `TypertRemoteService` is the normal explicit declaration that a Service has joined the Gateway; its public readonly `typertGateway` field keeps the binding visible on the runtime instance.
A decorator states Remote participation, an optional export name, and stream delivery; `@Remote` options contain only `name` and `mode`. It performs no runtime type reflection and injects no hidden symbol into a Service constructor. The arguments to `@Remote('create')` and `@RemoteScope('agent', 'create')` are external method names; the decorated member may be the business method itself or an adapter such as `remoteExportCreate`. The member name becomes the external method name only when no alias is provided. Inheriting `TypertRemoteService` is the normal explicit declaration that a Service has joined the Gateway; its public readonly `typertRemote` field keeps the binding visible on the runtime instance.
In SRC mode, the decorator records the method name and invocation mode in a versioned descriptor on the Service prototype. The descriptor uses a stable string property name, so `remoteMethods()` can read markers produced by another installed copy of `dsh-typert-protocol`; it writes nothing to the Service instance, constructor, or method function.
Ordinary artifacts construct a lazy Remote info instance from one static method spec per endpoint. Its direct `TypertRemoteMethod` objects retain descriptor fields and add named `input(options?)` and `invoke`. Consumers select matching strict parsing and projection with `input({ strict: true })`; `input()` preserves normal handling. Host parameter codecs and the single normal `result` codec are unchanged. Existing Registry queries retain those objects, while `TYPERT_REMOTE.methods` supplies exact static types. Strict source startup prebuilds and loads ordinary Typert artifacts; fallback cannot recover erased schemas. The [unified metadata note](../../proposed/architecture/2026-09-20-unified-typert-remote-metadata.md) records this delivery mechanism and the incomplete consumer adaptation.
In LIB mode, the Typert compiler performs strict method discovery, type resolution, and descriptor generation. It accepts a literal service key in `TypertRemoteService`'s direct `super()` call or the explicit binding fallback; generation neither rewrites business source nor injects hidden registration metadata.
## Lookup and Remote Scope registration
@@ -215,7 +217,7 @@ Host lib build
→ Vite 构建 Web
```
The existing top-level `build` still runs `build:lib` before `build:web`, but `build:lib` must complete the Host and Remote artifacts before starting Client TypeScript compilation. A clean build must not depend on stale `.d.ts` files from an earlier build.
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.
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.
@@ -349,9 +351,9 @@ The Web already depends on build artifacts such as `lib/client.js`, so it requir
## SRC and LIB operating modes
SRC supports local source startup. The versioned prototype descriptors created by `@Remote` and `@RemoteScope()` provide method names and invocation modes. At runtime, the system reads ordered parameter names from the JavaScript function signature and combines them with registered lookup/Context providers to produce a permissive descriptor.
SRC supports local source startup. For methods without registered strict definitions, the versioned prototype descriptors created by `@Remote` and `@RemoteScope()` provide method names and invocation modes. At runtime, the system reads ordered parameter names from the JavaScript function signature and combines them with registered lookup/Context providers to produce a permissive descriptor.
For example, `@Remote('create') remoteExportCreate(agent, request, signal)` resolves to the external method `create`, implementation member `remoteExportCreate`, two top-level business parameters, and one cancellation injection point. Lookup registration rewrites `agent` to the wire field `agentId`, `request` is passed as a same-named JSON parameter, and the final `signal` stays outside the payload. SRC does not start a `ts.Program`, use a preload or loader hook, generate or rewrite source, or inspect the internal structure of an ordinary JSON object.
For example, `@Remote('create') remoteExportCreate(agent, request, signal)` resolves to the external method `create`, implementation member `remoteExportCreate`, two top-level business parameters, and one cancellation injection point. Lookup registration rewrites `agent` to the wire field `agentId`, `request` is passed as a same-named JSON parameter, and the final `signal` stays outside the payload. The conservative fallback does not start a `ts.Program`, use a preload or loader hook, generate or rewrite source, or inspect the internal structure of an ordinary JSON object.
A signature that SRC cannot resolve unambiguously fails on the first invocation that resolves its descriptor; Service mounting records only the decorator marker and does not inspect the JavaScript signature. SRC does not guess at object destructuring, ambiguity caused by default parameters, rest parameters, nested lookups, or complex types.
@@ -474,7 +476,7 @@ The package topology is `api/remotes → api/gateway → client/connection → h
**Perform strict reflection through decorators at runtime.** JavaScript decorators cannot recover erased TypeScript types, public symbol identity, or complete Zod codecs. Injecting a compiler-private symbol into a constructor would also hide the business class's real dependencies, so Typert generates strict information at compile time.
**Use a preload, loader hook, or complete `ts.Program` during SRC startup.** This could reuse LIB analysis but would add requirements to every source startup entry. SRC needs only a usable permissive descriptor, so it uses decorator markers, function parameter names, and explicit providers; strict checks remain in the LIB contract pass.
**Use a preload, loader hook, or complete `ts.Program` during SRC startup.** This could reuse LIB analysis but would add requirements to every source startup entry. Source startup can load ordinary prebuilt artifacts or use conservative markers without loading a TypeScript Program.
**Hand-write the Client interface.** A hand-written interface cannot guarantee that it contains only Remote-marked methods and can drift from Host signatures, lookup IDs, and Zod schemas. Client types are therefore projected automatically from the Host Program.
@@ -28,7 +28,7 @@ Remote 消费端投影同时包含 `.d.ts`、`.d.ts.map` 和 `.js`。`.d.ts` 只
| 组件 | Cordis 服务 | 职责 |
|---|---|---|
| `@deepseek-ai/dsh-typert-protocol` | 只声明 `ctx.typert` 的最小协议 | `TypertRemoteService`、decorator、binding 回退、descriptor、lookup/Context 和 Remote map;不依赖 compiler、Zod、Connection 或 Browser |
| `@deepseek-ai/dsh-typert-protocol` | 只声明 `ctx.typert` 的最小协议 | `TypertRemoteService`、decorator、binding 回退、descriptor、lookup/Context、Remote map 和基于 Zod 的惰性 info 组装;不依赖 compiler、Connection 或 Browser |
| Typert registry | `ctx.typert` | 分开保存当前环境 reflection、导入的 Remote contribution、lookup provider 和 Context provider |
| Typert generator/loader | 无新增业务服务 | 从 Host/Client Program 生成三类 `lib` 产物,并把当前环境产物注册到 `ctx.typert` |
| API Gateway 的 Host face | `ctx.typertGateway` | 关联 Host definition 与活 Service,解码参数、解析 receiver、调用方法,并将一元结果投影为载体无关的字段与附件 |
@@ -81,16 +81,18 @@ export class ScopedGoalService extends TypertRemoteService {
同一个 endpoint 只能选择一种调用模式。需要显式 `Agent` 参数的流程使用 `@Remote`;需要切换到 Agent Context 再解析 scoped receiver 的流程使用 `@RemoteScope('agent')`,两者不会由 Typert 根据方法体或参数缺失自动猜测。
业务包只依赖轻量的 `@deepseek-ai/dsh-typert-protocol`。它提供 `TypertRemoteService`,以及 decorator、binding 回退、lookup、Remote Scope 和 descriptor 的声明协议,不依赖 TypeScript compiler、Zod、HTTP 或 Client runtime。
业务包只依赖 `@deepseek-ai/dsh-typert-protocol`。它提供 `TypertRemoteService`,以及 decorator、binding 回退、lookup、Remote Scope 和 descriptor 的声明协议,不依赖 TypeScript compiler、HTTP 或 Client runtime。其纯 `./remote-info` 入口使用 Zod 组装惰性具名输入 schema,不导入 Cordis;根入口在提供依赖 Cordis 的声明之余,也重新导出该能力。
支持协作式取消的方法会把 `signal: AbortSignal` 声明为最后一个 Host 参数。这个保留参数不是业务值、lookup 或 JSON 字段。生成的消费方方法将其暴露为最后一个可选参数,因此普通调用保持不变,而拥有取消控制权的调用方可以传入 signal。
## Decorator 与显式 Gateway facet
Decorator 只表达“该方法参与 Remote 约定”,不负责运行时类型反射,也不向 Service constructor 注入隐藏 symbol。`@Remote('create')` 和 `@RemoteScope('agent', 'create')` 的参数是外部方法名;被装饰成员既可以是业务方法本身,也可以是 `remoteExportCreate` 这样的适配器。未给别名时才使用成员名作为外部方法名。继承 `TypertRemoteService` 是 Service 加入 Gateway 的常规显式声明;其 public readonly `typertGateway` 字段使运行时实例上的绑定保持可见。
Decorator 表达 Remote 参与、可选导出名称和流式投递;`@Remote` 选项仅包含 `name` 和 `mode`。它不负责运行时类型反射,也不向 Service constructor 注入隐藏 symbol。`@Remote('create')` 和 `@RemoteScope('agent', 'create')` 的参数是外部方法名;被装饰成员既可以是业务方法本身,也可以是 `remoteExportCreate` 这样的适配器。未给别名时才使用成员名作为外部方法名。继承 `TypertRemoteService` 是 Service 加入 Gateway 的常规显式声明;其 public readonly `typertRemote` 字段使运行时实例上的绑定保持可见。
SRC 模式下,decorator 把方法名和调用模式记录在 Service prototype 上的带版本描述符中。描述符使用稳定的字符串属性名,因此 `remoteMethods()` 可以读取 `dsh-typert-protocol` 另一个已安装副本生成的标记;它不会向 Service 实例、constructor 或方法函数写入任何内容。
普通产物从每个端点唯一的静态方法 spec 构造惰性 Remote info 实例。其直接 `TypertRemoteMethod` 对象保留描述符字段,并增加具名 `input(options?)` 和 `invoke`。消费方通过 `input({ strict: true })` 选择配套的严格解析与投影;`input()` 保留普通处理。Host 参数 codec 与唯一的普通 `result` codec 保持不变。既有 Registry 查询保留这些对象,`TYPERT_REMOTE.methods` 则提供精确静态类型。严格源码启动预构建并加载普通 Typert 产物;fallback 无法恢复被擦除的 schema。[统一元数据 note](../../proposed/architecture/2026-09-20-unified-typert-remote-metadata.zh.md)记录该交付机制及尚未完成的消费方适配。
LIB 的严格方法发现、类型解析和 descriptor 生成由 Typert compiler 完成。它接受 `TypertRemoteService` 直接 `super()` 调用中的字面量 service key,或显式 binding 回退;生成过程不改写业务源码,也不注入隐藏注册元数据。
## Lookup 与 Remote Scope 注册
@@ -215,7 +217,7 @@ Host lib build
→ Vite 构建 Web
```
现有顶层 `build` 仍表现为先 `build:lib`、再 `build:web`,但 `build:lib` 内部必须先完成 Host 与 Remote artifact,再启动 Client TypeScript 编译。一次干净构建不能依赖上次残留的 `.d.ts`。
顶层 `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`。
即使主要输入是源文件,需要通过编译器解析消费方 surface 的仓库门禁也有相同的前置条件。公共 `typecheck`、`lint` 和 `doc-typecheck` 命令会先执行 Host 约定 pass。门禁调度器仅可在显式的 Typert 约定依赖或完整构建依赖完成后使用对应的 `*:contracts-ready` 变体,使并行 lane 既不会读取缺失的声明,也不会针对同一输出并发运行多个生成器。
@@ -349,9 +351,9 @@ Web 本身依赖 `lib/client.js` 等构建产物,因此启动 Web 前要求完
## SRC 与 LIB 运行模式
SRC 面向本地源码启动。`@Remote` 和 `@RemoteScope()` 创建的带版本 prototype 描述符给出方法名和调用模式,运行时从 JavaScript 函数签名读取顺序参数名,并结合已注册 lookup/Context provider 生成弱 descriptor。
SRC 面向本地源码启动。对于没有已注册严格定义的方法,`@Remote` 和 `@RemoteScope()` 创建的带版本 prototype 描述符给出方法名和调用模式,运行时从 JavaScript 函数签名读取顺序参数名,并结合已注册 lookup/Context provider 生成弱 descriptor。
例如 `@Remote('create') remoteExportCreate(agent, request, signal)` 解析为外部方法 `create`、实现成员 `remoteExportCreate`、两个顶层业务参数和一个取消注入点;lookup 注册把 `agent` 改写为 wire 字段 `agentId`,`request` 按同名 JSON 参数传递,最后一个 `signal` 则留在 payload 之外。SRC 不启动 `ts.Program`,不使用 preload、loader hook、源码生成或模块改写,也不检查普通 JSON 对象的内部结构。
例如 `@Remote('create') remoteExportCreate(agent, request, signal)` 解析为外部方法 `create`、实现成员 `remoteExportCreate`、两个顶层业务参数和一个取消注入点;lookup 注册把 `agent` 改写为 wire 字段 `agentId`,`request` 按同名 JSON 参数传递,最后一个 `signal` 则留在 payload 之外。保守回退不启动 `ts.Program`,不使用 preload、loader hook、源码生成或模块改写,也不检查普通 JSON 对象的内部结构。
SRC 无法明确解析的签名会在首次调用解析其 descriptor 时失败;Service 挂载只记录 decorator 标记,不检查 JavaScript 签名。SRC 不会猜测对象解构、默认参数造成的歧义、rest 参数、嵌套 lookup 或复杂类型。
@@ -474,7 +476,7 @@ Connection 提供共享 channel interceptor 与当前 HTTP carrier 映射。WebS
**让 decorator 在运行时完成严格反射。** JavaScript decorator 无法恢复擦除后的 TypeScript 类型、公共符号身份和完整 Zod codec;向 constructor 注入 compiler 私有 symbol 又会隐藏业务类的真实依赖,因此严格信息由 Typert compiler 生成。
**SRC 启动时使用 preload、loader hook 或完整 `ts.Program`。** 这能复用 LIB 分析,但增加所有源码启动入口的要求。SRC 只需要可用的弱 descriptor,因此采用 decorator 标记、函数参数名和显式 provider;严格检查留给 LIB 约定 pass。
**SRC 启动时使用 preload、loader hook 或完整 `ts.Program`。** 这能复用 LIB 分析,但增加所有源码启动入口的要求。源码启动可加载普通预构建产物或使用保守标记,而无需加载 TypeScript Program。
**手写 Client interface。** 手写接口不能保证只包含 Remote 标记的方法,也会与 Host 签名、lookup ID 和 Zod schema 漂移,因此 Client 类型从 Host Program 自动投影。
@@ -3,8 +3,8 @@
# After editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-04-configuration-source-ownership.md
/agent-note-one-ordering-for-configuration-sources-and-what-a-discovered-file-may-not-decide:
en: 1f6ee603f36c2e09
zh: e08197958663219c
en: 35dea8a3a8bdc842
zh: 7a1cdf184ffd889f
/agent-note-one-ordering-for-configuration-sources-and-what-a-discovered-file-may-not-decide/problem:
en: d94bb81479bfa4a1
zh: af44ceaecf84ee51
@@ -4,7 +4,7 @@ Status: implemented
English | [中文](2026-08-04-configuration-source-ownership.zh.md)
The non-secret settings tier below is superseded by [profile-owned live configuration](2026-09-19-profile-owned-live-configuration.md). Credential ordering and environment restrictions remain current.
The non-secret settings tier below is superseded by [profile-owned live configuration](2026-09-19-profile-owned-live-configuration.md), and the credential ordering by [stored credential precedence](2026-10-09-stored-credential-precedence.md). Environment restrictions remain current.
## Problem
@@ -4,7 +4,7 @@ Status: implemented
[English](2026-08-04-configuration-source-ownership.md) | 中文
下文的非秘密 settings 层已由 [profile 所属实时配置](2026-09-19-profile-owned-live-configuration.zh.md) 替代。凭据优先级与环境限制仍然适用。
下文的非秘密 settings 层已由 [profile 所属实时配置](2026-09-19-profile-owned-live-configuration.zh.md) 替代,凭据优先级已由[存储凭据优先级](2026-10-09-stored-credential-precedence.zh.md)替代。环境限制仍然适用。
## Problem
@@ -9,8 +9,8 @@
en: 504dd5f06475d707
zh: 6557fd5649f48944
/agent-note-profile-plugin-bundles-replace-the-fixed-surface-overlays/decision:
en: 4c58d379cd59d879
zh: a6b6322fe82d7b6a
en: 1077d88affd90a59
zh: 73b07bf6dcb130ba
/agent-note-profile-plugin-bundles-replace-the-fixed-surface-overlays/alternatives-considered:
en: d906d7a421c6b5c0
zh: b230cd58c1b7ac79
@@ -12,7 +12,7 @@ The `dsh` launcher hardcoded its compositions: `base.cordis.yml` + `web.cordis.y
Everything becomes a **profile**: a directory `$DSH_HOME/profiles/<name>` with a `package.json` (pnpm-managed out-of-tree plugin `dependencies` plus the profile manifest `dsh.profile` with its ordered `bundles` layer list) and a user `cordis.patch.yml`. A **bundle** is an npm package declaring `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }`; the two manifest kinds live under distinct `dsh.profile` / `dsh.bundle` keys so a package.json states which role it plays. The tree composes over an empty root by applying each bundle's patch in `dsh.profile.bundles` order, then the user layer and `--patch` overlays — one `applyEntryPatches` call shared by boot and `--dump-config`. App invocation values later moved from launcher-derived patches to startup services in the [app-owned command-line decision](../../archived/architecture/2026-08-06-app-owned-command-line.md).
The default Profile templates use `@deepseek-ai/dsh-base` as the shared core for `web`, `headless`, `sdk`, and `acp`, with one mode bundle above it. The [standalone `sdk-minimal` profile](../../../../packages/bundle/sdk-minimal/README.md) instead lists one bundle that owns its complete explicit tree. Generic `dsh --profile <name>` hands its remaining arguments to that profile's command-line startup row: Web owns its flag family, headless owns its task positional, and the protocol profiles accept no app options. Patch overlays use launcher-owned `--patch`. A new, non-shipped target can use `--from-default-profile <template>` to copy one default template's bundle list and patch-reload policy before boot or config dump. This creates an independent profile with empty dependencies and an empty user patch: it neither reads a local profile named by the template nor records an inheritance relationship. The launcher claims the complete target directory exclusively, so existing state and concurrent creators fail without modification. `dsh plugin --profile <name> <args...>` is a thin pnpm forwarder that initializes a base-backed profile and reconciles `dsh.profile.bundles` with installed bundle declarations; a package without a bundle declaration remains a plain dependency. [Headless as a direct core entry point](../../archived/architecture/2026-08-09-headless-direct-core-entry-point.md) owns the headless composition contract.
The default Profile templates use `@deepseek-ai/dsh-base` as the shared core for `web`, `headless`, `sdk`, and `acp`, with one mode bundle above it. The [standalone `sdk-minimal` profile](../../../../packages/bundle/sdk-minimal/README.md) instead lists one bundle that owns its complete explicit tree. Generic `dsh --profile <name>` hands its remaining arguments to that profile's command-line startup row: Web owns its flag family, headless owns its task positional, and the protocol profiles accept no app options. Patch overlays use launcher-owned `--patch`. A non-shipped target can use `--from-default-profile <template>` to replace itself with a copy of one default template's bundle list and patch-reload policy before boot or config dump. This creates an independent profile with empty dependencies and an empty user patch: it neither reads a local profile named by the template nor records an inheritance relationship. The launcher assembles the copy in a staging directory beside the target and renames it into place, so repeated rescue runs reset the profile; a failed rename restores the previous directory. `dsh plugin --profile <name> <args...>` is a thin pnpm forwarder that initializes a base-backed profile and reconciles `dsh.profile.bundles` with installed bundle declarations; a package without a bundle declaration remains a plain dependency. [Headless as a direct core entry point](../../archived/architecture/2026-08-09-headless-direct-core-entry-point.md) owns the headless composition contract.
Resolution is two-anchored by construction: `dsh.profile.bundles` names resolve from the dsh installation first, then the profile directory, so in-box bundles always come from the same installation as the running `dsh` and pnpm never manages them. Bare plugin names in patch rows use the [immutable runtime resolution](2026-09-09-profile-resolution-generations.md), which applies installation-first and ordered-bundle rules in memory and expands dependencies from real package directories.
@@ -12,7 +12,7 @@ Status: implemented
一切都变成 **profile**:即目录 `$DSH_HOME/profiles/<name>`,其中包含一个 `package.json`(pnpm 管理的树外插件 `dependencies`,加上 profile manifest `dsh.profile` 及其有序的 `bundles` 层列表)和一份用户 `cordis.patch.yml`。**组合包**(bundle)是声明了 `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }` 的 npm 包;两种 manifest 分别位于互不相同的 `dsh.profile` / `dsh.bundle` 键下,因此一份 package.json 能说明自己扮演哪种角色。配置树在空的根之上组合:按 `dsh.profile.bundles` 顺序应用每个组合包的 patch,然后是用户层与 `--patch` overlay——启动与 `--dump-config` 共享同一条 `applyEntryPatches` 路径。随后,[应用持有命令行的决策](../../archived/architecture/2026-08-06-app-owned-command-line.md)又把调用期取值从启动器派生的 patch 迁移到了启动服务。
默认 Profile 模板为 `web`、`headless`、`sdk` 与 `acp` 使用 `@deepseek-ai/dsh-base` 作为共享核心,并在其上叠加一个模式组合包。[独立 `sdk-minimal` profile](../../../../packages/bundle/sdk-minimal/README.zh.md)则只列出一个拥有完整显式配置树的组合包。通用的 `dsh --profile <name>` 把剩余参数交给该 profile 的命令行启动行:Web 持有自己的 flag 家族,headless 持有任务位置参数,协议 profile 不接受应用选项。patch overlay 使用启动器持有的 `--patch`。新的非内置目标可以使用 `--from-default-profile <template>`,在启动或配置 dump 之前复制一个默认模板的 bundle 列表与 patch 重载策略。这会创建依赖为空、用户 patch 为空的独立 profile:它既不读取与模板同名的本地 profile,也不记录继承关系。launcher 会以独占方式领取完整的目标目录,因此既有状态和并发创建者都会在不作修改的情况下失败。`dsh plugin --profile <name> <args...>` 是一层薄薄的 pnpm 转发器,负责初始化一个以 base 为基础的 profile,并依据已安装包的组合包声明调和 `dsh.profile.bundles`;没有组合包声明的包保持为普通依赖。[Headless 作为直接 core 入口](../../archived/architecture/2026-08-09-headless-direct-core-entry-point.md)负责 headless 组合约定。
默认 Profile 模板为 `web`、`headless`、`sdk` 与 `acp` 使用 `@deepseek-ai/dsh-base` 作为共享核心,并在其上叠加一个模式组合包。[独立 `sdk-minimal` profile](../../../../packages/bundle/sdk-minimal/README.zh.md)则只列出一个拥有完整显式配置树的组合包。通用的 `dsh --profile <name>` 把剩余参数交给该 profile 的命令行启动行:Web 持有自己的 flag 家族,headless 持有任务位置参数,协议 profile 不接受应用选项。patch overlay 使用启动器持有的 `--patch`。非内置目标可以使用 `--from-default-profile <template>`,在启动或配置 dump 之前,用一个默认模板的 bundle 列表与 patch 重载策略的副本替换自身。这会创建依赖为空、用户 patch 为空的独立 profile:它既不读取与模板同名的本地 profile,也不记录继承关系。launcher 在目标旁的暂存目录中组装副本并重命名到目标位置,因此重复执行 rescue 会重置该 profile;重命名失败时会恢复原有目录。`dsh plugin --profile <name> <args...>` 是一层薄薄的 pnpm 转发器,负责初始化一个以 base 为基础的 profile,并依据已安装包的组合包声明调和 `dsh.profile.bundles`;没有组合包声明的包保持为普通依赖。[Headless 作为直接 core 入口](../../archived/architecture/2026-08-09-headless-direct-core-entry-point.md)负责 headless 组合约定。
解析在构造上就是双锚点的:`dsh.profile.bundles` 中的名称先从 dsh 安装目录解析,再从 profile 目录解析,因此内置组合包始终来自与运行中 `dsh` 相同的安装,pnpm 从不管理它们。patch 行中的裸插件名称使用[不可变 runtime resolution](2026-09-09-profile-resolution-generations.zh.md),在内存中应用安装优先与有序 bundle 规则,并从包的真实目录展开依赖。
@@ -9,14 +9,14 @@
en: 1fe344c95a0de36d
zh: 656c3e0f1117f64e
/agent-note-forked-children-preserve-the-parent-request-prefix/decision:
en: b70aafc9ce30dd00
zh: 74e3ff915bd968a9
en: 3f5b3a4646c9466b
zh: 690172d6a4025b2a
/agent-note-forked-children-preserve-the-parent-request-prefix/alternatives-considered:
en: 80504ffd8331e6c5
zh: 193261bdde24b4bb
en: bc437309b64997ab
zh: 947010eed8d6fac4
/agent-note-forked-children-preserve-the-parent-request-prefix/consequences:
en: 0738d36f17c889b5
zh: b71ee47f75d6d4e0
en: 0f4d735328b3211d
zh: db4adb0f46b33bba
/agent-note-forked-children-preserve-the-parent-request-prefix/consequences/accepted-risks:
en: f634caa9973eaedb
zh: 820291096c8e7352
en: 72f4913b603e8596
zh: 50924c62200070d1
@@ -18,7 +18,7 @@ The model-facing `send_message` tool is registered globally for every Agent in a
The base bundle and the `cordis`, `standard`, and `ptc` CLI presets bind fork to the continuable lifecycle. Base-backed profiles, including headless, inherit that default. `ForkInProcessProvider.prepareContinuable()` and `ctx.subagents.startActivation()` preserve the same request-prefix behavior across these compositions.
Byte-identical prefix reuse is qualified by explicit deployment choices. A fork delegation that applies a child persona or `toolFilter` may still change the request head. In particular, filtering out `send_message` removes both the schema and the return guidance from the child; the runtime does not bypass an explicit allow-list.
Byte-identical prefix reuse depends on the assembled inputs. Structured output or generated SDK differences can change the request head. Historical child descriptors can also restore persona or tool restrictions during cold resume; filtering out `send_message` removes both its schema and return guidance. New fork starts inherit the parent preset without these override fields.
## Alternatives considered
@@ -28,11 +28,11 @@ Byte-identical prefix reuse is qualified by explicit deployment choices. A fork
**Add the return instruction to the system prompt.** This would place child-only bytes ahead of inherited messages. Appending it to the initial user task preserves the inherited prefix and keeps the parent id next to the task that needs it.
**Ignore an explicit child `toolFilter`.** Structural return tools previously bypassed the child allow-list. Rejected because a declared tool restriction must determine both schema visibility and guidance; hidden authority would make the model-facing roster inaccurate.
**Ignore a historical child `toolFilter` on resume.** A stored restriction must continue to determine both schema visibility and guidance; silently discarding it changes the restored agent’s behavior.
## Consequences
- Parent and continuable-fork child expose byte-identical ordered tool schemas when the delegation does not request a persona or tool filter.
- Parent and continuable-fork child expose byte-identical ordered tool schemas when they use the same preset and have no child-only structured output or restored historical overrides.
- The inherited Session seed precedes the child's initial task and return guidance.
- The base bundle and full CLI presets use local continuable fork for both parent and caller delivery, without a child-only request-head addition.
- The child can send messages to its direct parent through `send_message`. Parent delivery adds a separate runtime settlement notice; caller delivery returns the result through the activation receipt.
@@ -40,4 +40,4 @@ Byte-identical prefix reuse is qualified by explicit deployment choices. A fork
### Accepted risks
Provider-side prefix reuse still depends on the selected provider and model and on the absence of explicit persona or tool-filter differences. The harness proves equality of its assembled request-head inputs, not a provider's cache behavior.
Provider-side prefix reuse still depends on the selected provider and model and matching assembled inputs. The harness tests equality of its assembled request-head inputs, not a provider’s cache behavior.
@@ -18,7 +18,7 @@ fork 与 spawn 的差异在于:fork 会用 parent 已完成轮次的前缀作
base 组合包与 `cordis`、`standard` 和 `ptc` CLI preset 将 fork 绑定为可继续生命周期。基于 base 的 profile(包括 headless)继承这一默认值。`ForkInProcessProvider.prepareContinuable()` 与 `ctx.subagents.startActivation()` 在这些组合中保持相同的请求前缀行为。
逐字节相同的前缀复用受显式部署选择约束。配置 child persona 或 `toolFilter` 的 fork 委派仍可能改变请求头。尤其是过滤掉 `send_message` 时,child 会同时失去该 schema 与返回指引;运行时不会绕过显式 allow-list。
逐字节相同的前缀复用取决于组装输入。结构化输出或生成 SDK 差异可以改变请求头。历史子级 descriptor 也可在冷恢复时恢复 persona 或工具限制;过滤掉 `send_message` 会同时移除其 schema 和返回指引。新 fork 启动继承父级 preset,不含这些覆盖字段。
## 考虑过的替代方案
@@ -28,11 +28,11 @@ base 组合包与 `cordis`、`standard` 和 `ptc` CLI preset 将 fork 绑定为
**把返回指令放进系统提示词。** 这会在继承消息之前加入 child-only 字节。将其追加到初始用户任务,可以保留继承前缀,并让 parent id 紧邻需要它的任务。
**忽略显式 child `toolFilter`。** 结构性返回工具过去会绕过 child allow-list。否决该方案,因为声明的工具限制必须同时决定 schema 可见性与指引;隐藏权限会让面向模型的工具清单失真。
**恢复时忽略历史子级 `toolFilter`。** 存储的限制必须继续决定 schema 可见性和指引;静默丢弃它会改变恢复 agent 的行为。
## 后果
- 未请求 persona 或工具过滤时,parent 与可继续 fork child 暴露逐字节相同且顺序一致的工具 schema。
- 使用相同 preset 且没有子级专属结构化输出或恢复的历史覆盖时,父级与可继续的 fork 子级公开逐字节相同、顺序一致的工具 schema。
- 继承的 Session 种子位于 child 初始任务及其返回指引之前。
- base 组合包与完整 CLI preset 对 parent 和 caller 投递均使用本地可继续 fork,不增加 child-only 请求头字段。
- child 可以通过 `send_message` 向直接 parent 发消息。parent 投递会追加独立的运行时结算通知;caller 投递通过 activation 回执返回结果。
@@ -40,4 +40,4 @@ base 组合包与 `cordis`、`standard` 和 `ptc` CLI preset 将 fork 绑定为
### 已接受风险
提供方侧前缀复用仍取决于选定的提供方和模型,以及是否不存在显式 persona 或工具过滤差异。harness 证明的是自己组装出的请求头输入相等,而不是提供方的缓存行为。
提供方侧前缀复用仍取决于所选提供方和模型,以及组装输入是否相同。harness 测试的是自身组装请求头输入相等,而非提供方的缓存行为。
@@ -18,5 +18,5 @@
en: ca7c9088d0793e1e
zh: ea648bac9e726c64
/agent-note-what-stays-host-plane-once-presets-own-the-agent-plane/consequences:
en: de26cde94adda770
zh: 7f39760b77958966
en: 0a426923c2a1f774
zh: 29626804b99b35ff
@@ -38,6 +38,6 @@ Projection key presence is not a per-session capability signal ([session project
## Consequences
The context meter becomes a per-session fact instead of a function of mount history. A preset can no longer opt out of token accounting; no shipped preset did, and `minimal` now says it drops auto-compaction rather than the accounting.
The context meter is a per-session fact independent of mount history. Presets resolve the host meter for token accounting while selecting their own compaction backend; `minimal` supports both automatic compaction and the human `/compact` command.
Production entry points must compose Agents explicitly; installing the registry alone does not bind them. An unjoined Agent receives no tools, prompt sections, or skills and reports no error.
@@ -38,6 +38,6 @@ Status: implemented
## 后果
context meter 成为逐会话的事实,而不再是挂载历史的函数。代价是 preset 不能再选择不做 token 记账;随附的 preset 没有一个这么做,`minimal` 现在也写明它放弃的是自动压缩而非记账。
context meter 是独立于挂载历史的逐会话事实。Preset 使用宿主 meter 进行 token 记账,并选择自己的压缩后端;`minimal` 同时支持自动压缩和用户 `/compact` 命令。
生产入口必须显式组装 Agent;仅安装注册表不会自动绑定。未加入的 Agent 不会获得工具、提示词段落或 skill,也不会报告错误。
@@ -9,8 +9,8 @@
en: c11f54e24be6cc3b
zh: c1c274fbb161a57b
/agent-note-session-log-versioning-one-integer-an-upgrade-chain-and-a-per-event-ignorable-marker/decision:
en: 084bcb4de484ef32
zh: 1edf1445913ac7b1
en: f2c17cd6b83acdba
zh: 148162c0a8043947
/agent-note-session-log-versioning-one-integer-an-upgrade-chain-and-a-per-event-ignorable-marker/decision/writer-and-publication-authority:
en: 461eaa03c6a4f441
zh: 7e5b131326dc702c
@@ -10,15 +10,17 @@ Session logs must be upgradable after release, and the runtime that ships first
## Decision
The [reader-compatibility review decision](../process/2026-10-08-session-reader-compatibility-review.md) supersedes the structural bump criterion. This note owns the integer, unknown-event marker, direction-aware refusal, and generation-preservation decisions.
**One monotonic integer, no major/minor split.** Whether a version step is auto-upgradable is a property of that step — expressed by whether its upgrader exists — not something a two-level numbering scheme should promise in advance; design time rarely reveals whether the next change will turn out "major".
**The writer decides bumps, not the reader.** A bump is required exactly when an old runtime could no longer handle a new log with full semantic correctness. "Parses without error" is not the bar: silently skipping content that shapes reconstruction is a wrong read. Only structural changes qualify — header shape, event envelope, core event semantics, the surface mechanism (`SurfaceEventType` set, `SurfaceOp` variants). When unsure, bump: a near-identity upgrader is almost free, a missed bump silently corrupts old readers.
**Bump when existing discriminators cannot prevent unsafe interpretation.** Successful parsing alone does not establish compatibility: silently skipping content required for reconstruction is a wrong read. Same-version changes require safe old-reader handling and preservation of historical meaning by new readers. The linked reader-compatibility decision defines the review obligations.
**Read rules by direction.** Equal version: read normally. Newer than the reader: refuse, name the direction ("written by a newer harness — upgrade"), and point at the raw log artifact so the user can still see the text (`SessionFormatUnsupportedError`, distinct from `SessionPersistenceCorruptionError` because nothing is damaged). Older than the reader: every event-body operation first runs the complete adjacent chain in memory and leaves the source path, bytes, and inode unchanged. Read handles may consume that current logical result directly; a write open exclusively publishes the final current generation under its canonical versioned filename before append. Header-only listing remains non-mutating and reports the numerically highest canonical generation. Catalog generation and module initialization reject a missing adjacent step, so a published first-party build never exposes a partial historical chain. Retained lower generations are not automatic fallback or a downgrade compatibility promise.
**A per-event `ignorable` marker covers vocabulary growth, so ordinary event additions never bump the version.** The event vocabulary is decided by which plugins are mounted, which a single version integer cannot describe. A reader meeting an unrecognized event type refuses to interpret the log unless the event carries `ignorable: true` in its envelope. The default is *required*: forgetting the marker over-refuses a resumable session (an inconvenience), while a default of ignorable would make the same mistake silently resume a gutted one (a safety failure). The architecture makes this sound: model-visible content flows only through the four `surfaceOp`-marked surface event types plus the `request/header`/`request/context` folds, so the dangerous unknowns are exactly the non-surface events that change how the rest of the log is read (`session/end-seed` is the existing example).
The [accepted compatibility checkpoint](../../../../docs/session-format-status.md#finalization-record) prevents an accepted transition from reusing its header increase for later breaking changes. Backward-compatible evolution uses new acknowledgement records at the same Session version; breaking changes require a higher writer. The checkpoint does not change the unknown-event protocol.
**Accepted machine records remain immutable.** The [accepted compatibility checkpoint](../../../../docs/session-format-status.md#finalization-record) preserves accepted declarations and schemas. Later changes receive new acknowledgements, and a version-bump decision needs its own increasing header transition. The checkpoint does not change the unknown-event protocol.
### Writer and publication authority
@@ -10,15 +10,17 @@ Session log 在发布后必须能升级格式,而最先发布的运行时决
## 决定
[读取器兼容性评审决策](../process/2026-10-08-session-reader-compatibility-review.zh.md)取代结构性升版本标准。本说明负责整数版本、未知事件标记、区分方向的拒绝与代际保留决策。
**一个单调递增的整数,不分大小版本。**某一步能不能自动升级是那一步自己的属性,由它的升级器存在与否表达,不该由两级编号方案提前承诺;设计时很少能预知下一个变更算不算"大"。
**升不升版本由写入方决定,与读取方能力无关。**当且仅当老运行时无法在语义上完全正确地处理新日志时才必须升版本。"解析不报错"不是标准:静默跳过影响重建的内容就是读错。只有结构性变更够得上这条线:header 形状、事件信封、核心事件语义、surface 机制(`SurfaceEventType` 集合、`SurfaceOp` 变体)。拿不准就升:近似恒等的升级器几乎没有成本,漏升一次会让老读取器静默读坏。
**既有判别信息无法阻止不安全解释时升版本。** 仅解析成功不能证明兼容:静默跳过重建所需内容属于错误读取。同版本变更要求旧读取器安全处理,并由新读取器保留历史含义。所链接的读取器兼容性决策定义评审义务。
**读取规则按方向区分。**版本相等:正常读。比读取器新:拒绝,说明方向("由更新的 harness 写入,请升级"),并给出原始日志文件的路径,用户仍能看到文本(`SessionFormatUnsupportedError`,与 `SessionPersistenceCorruptionError` 区分,因为数据没有损坏)。比读取器旧:每个事件正文操作先在内存中运行完整相邻链,并保持源路径、字节与 inode 不变。读句柄可以直接使用该 current 逻辑结果;写 open 则在 append 前把最终 current generation 排他发布到其规范版本文件名。仅 header 的列表保持不变更,并报告数值最高的规范 generation。catalog 生成与模块初始化会拒绝缺失的相邻步骤,因此已发布第一方 build 绝不会暴露不完整历史链。保留的低 generation 不是自动 fallback,也不构成 downgrade compatibility 承诺。
**逐事件的 `ignorable` 标记吸收词汇表增长,普通的新增事件永远不用升版本。**事件词汇表由挂载了哪些插件决定,单个版本整数描述不了它。读取器遇到不认识的事件类型时拒绝解读日志,除非该事件的信封带 `ignorable: true`。默认为必需:忘写标记的后果是把一个本可恢复的会话拒绝过头(体验问题),而默认可忽略会让同样的疏忽静默恢复出残缺会话(安全事故)。架构保证了这条规则成立:模型可见内容只经四种带 `surfaceOp` 标记的 surface 事件加 `request/header`、`request/context` 折叠进入重建,危险的未知事件恰好是那些不进 surface 但改变日志其余部分解读方式的事件(`session/end-seed` 是现存例子)。
[已接受兼容性检查点](../../../../docs/session-format-status.zh.md#finalization-record)阻止已接受转换复用原来的头版本递增来授权后续破坏性变更。向后兼容的演进在相同 Session 版本上使用新的确认记录;破坏性变更要求更高的写入器版本。检查点不改变未知事件协议。
**已接受的机器记录保持不可变。** [已接受兼容性检查点](../../../../docs/session-format-status.zh.md#finalization-record)保留已接受的声明与 schema。后续变更获得新确认,升版本决策需要自身的递增头版本转换。检查点不改变未知事件协议。
### 写入器与发布真源
@@ -15,8 +15,8 @@
en: a1750de2d48551af
zh: ffea0b75c292f186
/agent-note-client-shell-layering-and-dynamic-package-boundaries/decision/shared-module-requests:
en: cedddcca8a7bae6d
zh: a568a1130229f48c
en: 96661da17622132a
zh: dab3424093130c2f
/agent-note-client-shell-layering-and-dynamic-package-boundaries/decision/parser-preloading-and-react-handoff:
en: 4b503618b271fc28
zh: e2148f44efa769d2
@@ -32,7 +32,7 @@ The `staticLinked` preset leaves every bare specifier as an external import in `
### Shared module requests
Dynamic browser bundles implicitly externalize the common baseline: `PLATFORM_MODULES` names shell-seeded React, Cordis, and static UI identities, while `PRELOADED_CLIENT_EXTERNALS` is reserved for a dynamic identity that must arrive before shell boot and is currently empty. A package uses `dsh.client.external` only for an exact non-baseline value request. Type-only imports are erased and create no request; permitted third-party implementation libraries remain private bundle contents.
Dynamic browser bundles implicitly externalize the common baseline: `PLATFORM_MODULES` names shell-seeded React, Cordis, selected third-party libraries, and static UI identities, while `PRELOADED_CLIENT_EXTERNALS` is reserved for a dynamic identity that must arrive before shell boot and is currently empty. A package uses `dsh.client.external` only for an exact non-baseline value request. Type-only imports are erased and create no request; non-baseline third-party implementation libraries remain private bundle contents. Shared third-party libraries use the shell's declared versions without per-plugin version negotiation; promotion into the baseline requires a measured reduction across the shell and shipped plugin bundles.
A request has exactly two suppliers:
@@ -32,7 +32,7 @@ Client npm 依赖区段描述安装和开发关系,但不能可靠描述 bundl
### 共享模块请求
动态浏览器 bundle 会隐式 external 统一基座:`PLATFORM_MODULES` 命名由外壳播种的 React、Cordis 和静态 UI 身份,`PRELOADED_CLIENT_EXTERNALS` 则为必须先于 shell 启动到达的动态身份预留,当前为空。包只在精确请求基座外实体时使用 `dsh.client.external`。纯类型 import 会被擦除,不产生请求;允许的第三方实现库保留为 bundle 私有内容。
动态浏览器 bundle 会隐式 external 统一基座:`PLATFORM_MODULES` 命名由外壳播种的 React、Cordis、选定的第三方库和静态 UI 身份,`PRELOADED_CLIENT_EXTERNALS` 则为必须先于 shell 启动到达的动态身份预留,当前为空。包只在精确请求基座外实体时使用 `dsh.client.external`。纯类型 import 会被擦除,不产生请求;基座外的第三方实现库保留为 bundle 私有内容。共享第三方库使用外壳声明的版本,不为单个插件协商版本;只有测量确认外壳与随包插件 bundle 的总体积减少,才将库纳入基座。
请求只有两种提供方:
@@ -21,8 +21,8 @@
en: 8ee88035cf64eeb3
zh: 6184e50fc4d47348
/agent-note-one-dsh-launcher-for-application-profiles/decision/python-runtime:
en: 5962abadfdece709
zh: 5db20e96deed49d1
en: 4b4cf5071b740c85
zh: 595e5fe8127f9067
/agent-note-one-dsh-launcher-for-application-profiles/decision/enforcement:
en: 9ce8d86b65064f38
zh: 27b8ad426cb319a2
@@ -46,7 +46,7 @@ Direct SDK use follows normal Harness-home resolution: explicit `dshHome`, inher
### Python runtime
The Python runtime wheel stages [`python/sdk-runtime/runtime-bootstrap.mjs`](../../../../python/sdk-runtime/runtime-bootstrap.mjs) as the `dsh-python-runtime-closure` entry. Its ordinary branch calls the public CLI export; a provider-private selector dispatches to the internal subprocess runner before CLI parsing and is not an application entry point. The [native-containment reference](../../../../packages/subprocess/subprocess-local/README.md) owns that private dispatch. The Python client selects `dsh --profile sdk` by default, ordered patch files, and an explicit Harness home; the runnable example under `python/sdk/examples` selects `sdk-minimal`. The installed `dsh` console command exposes the same profile grammar, including the `web` profile.
The Python runtime wheel stages [`python/sdk-runtime/runtime-bootstrap.mjs`](../../../../python/sdk-runtime/runtime-bootstrap.mjs) as the `dsh-python-runtime-closure` entry. Its ordinary branch calls the public CLI export; a provider-private selector dispatches to the internal subprocess runner before CLI parsing and is not an application entry point. A private `DSH_RUNTIME_DOWNLOAD` selector prepares optional resources before application launch and is removed before spawning preparation subprocesses; ordinary CLI arguments do not select downloads. The [native-containment reference](../../../../packages/subprocess/subprocess-local/README.md) owns that private dispatch. The Python client selects `dsh --profile sdk` by default, ordered patch files, and an explicit Harness home; the runnable example under `python/sdk/examples` selects `sdk-minimal`. The installed `dsh` console command exposes the same profile grammar, including the `web` profile.
The executable family is `deepseek-harness-sdk-runtime-<platform>-<arch>`. The SDK wire, wheel and import distribution names, sidecar names, and wire identity `deepseek-harness-sdk-runtime` remain stable. The SDK package family is `@deepseek-ai/dsh-sdk-client`, `@deepseek-ai/dsh-sdk-protocol`, and `@deepseek-ai/dsh-sdk-jsonrpc-server`; `@deepseek-ai/dsh-acp` remains the ACP protocol plugin. There is no Python-specific Node application, checked-in complete config, compatibility package, forwarding executable, fallback parser, or SDK/ACP launcher alias. [docs/architecture.md](../../../../docs/architecture.md) owns this launch, and the [`python/sdk-runtime` README](../../../../python/sdk-runtime/README.md) owns the Windows carrier.
@@ -46,7 +46,7 @@ SDK 用户通过 profile 自定义插件。`dsh plugin --profile <name> ...` 管
### Python 运行时
Python 运行时 wheel 将 [`python/sdk-runtime/runtime-bootstrap.mjs`](../../../../python/sdk-runtime/runtime-bootstrap.mjs) 暂存为 `dsh-python-runtime-closure` 入口。其普通分支调用公开 CLI export;提供方私有选择会在 CLI 解析前分派到内部子进程 runner,而不是应用入口。[原生 containment 参考](../../../../packages/subprocess/subprocess-local/README.zh.md)负责该私有分派。Python 客户端默认选择 `dsh --profile sdk`、有序 patch 文件与显式 Harness home;`python/sdk/examples` 下的可运行示例选择 `sdk-minimal`。安装的 `dsh` 控制台命令暴露相同的 profile 语法,包括 `web` profile。
Python 运行时 wheel 将 [`python/sdk-runtime/runtime-bootstrap.mjs`](../../../../python/sdk-runtime/runtime-bootstrap.mjs) 暂存为 `dsh-python-runtime-closure` 入口。其普通分支调用公开 CLI export;提供方私有选择会在 CLI 解析前分派到内部子进程 runner,而不是应用入口。私有 `DSH_RUNTIME_DOWNLOAD` 选择器在应用启动前准备可选资源,并在启动资源准备子进程前移除;普通 CLI 参数不选择下载。[原生 containment 参考](../../../../packages/subprocess/subprocess-local/README.zh.md)负责该私有分派。Python 客户端默认选择 `dsh --profile sdk`、有序 patch 文件与显式 Harness home;`python/sdk/examples` 下的可运行示例选择 `sdk-minimal`。安装的 `dsh` 控制台命令暴露相同的 profile 语法,包括 `web` profile。
可执行文件族是 `deepseek-harness-sdk-runtime-<platform>-<arch>`。SDK 协议格式、wheel 与 import 分发名称、伴随文件名称,以及协议 identity `deepseek-harness-sdk-runtime` 保持稳定。SDK 包族是 `@deepseek-ai/dsh-sdk-client`、`@deepseek-ai/dsh-sdk-protocol` 与 `@deepseek-ai/dsh-sdk-jsonrpc-server`;`@deepseek-ai/dsh-acp` 继续作为 ACP 协议插件。仓库不保留 Python 专用 Node 应用、检入的完整配置、兼容包、转发可执行文件、后备解析器或 SDK/ACP 启动别名。[docs/architecture.md](../../../../docs/architecture.zh.md)负责该启动方式,[`python/sdk-runtime` README](../../../../python/sdk-runtime/README.zh.md)负责 Windows 载体。
@@ -9,11 +9,11 @@
en: 9f34c53e647c69ca
zh: 551776c7cc9618b5
/agent-note-client-derived-presentation-from-raw-session-tool-events/decision:
en: bdbb99c01b4c4db0
zh: 570040471ad1f7f4
en: 5fcc5a3de9f757cd
zh: a693cefdb82784ee
/agent-note-client-derived-presentation-from-raw-session-tool-events/decision/goals-and-non-goals:
en: 7e7a937dc4fe1b18
zh: 530f508b7e17b607
en: 40aceb58d71303b5
zh: 6954e41ae2fd1efd
/agent-note-client-derived-presentation-from-raw-session-tool-events/terminology:
en: 021106dd41176715
zh: 131caff7bd37251b
@@ -72,11 +72,11 @@
en: f40f0a8ee8c055c0
zh: 6957b7372956113c
/agent-note-client-derived-presentation-from-raw-session-tool-events/client-card-model-design/generic-tool-row:
en: 20ff82b092325c5c
zh: 1afd5441a9816117
en: d336c77d1b09f349
zh: 6e613f9c9108058f
/agent-note-client-derived-presentation-from-raw-session-tool-events/client-card-model-design/terminal-card:
en: eab7f366df331b6e
zh: d82499e2a982206f
en: 492669c2134169b2
zh: 0247a38df2dd8a83
/agent-note-client-derived-presentation-from-raw-session-tool-events/client-card-model-design/diff-card:
en: ea384415c94f82da
zh: 5f146c6cb534e1db
@@ -99,8 +99,8 @@
en: e5b42aa311079bd7
zh: ec4cf97037ece7e0
/agent-note-client-derived-presentation-from-raw-session-tool-events/presentation-equivalence-matrix:
en: 30915c26728eeafe
zh: eea04dea7b4604a5
en: 7974f20a6ac687ac
zh: cb4c350325a6da67
/agent-note-client-derived-presentation-from-raw-session-tool-events/client-extension-contract:
en: 96619dfd5ef299cf
zh: 253df98eb42ad75e
@@ -22,7 +22,7 @@ The required result is one raw Session journal and one Client presentation owner
## Decision
The visual-equivalence requirements below exclude the separately approved [nested terminal-card fix](../../../../packages/client/ui-tool/README.md) and [recorded tool details](../../../../packages/client/ui-tool/README.md); all other presentation and ownership constraints remain.
The visual-equivalence requirements below exclude the separately approved [nested terminal-card fix](../../../../packages/client/ui-tool/README.md), [recorded tool details](../../../../packages/client/ui-tool/README.md), and [background Shell command display](../bug-fix/2026-10-10-background-shell-command-display.md); all other presentation and ownership constraints remain.
The Session Remote journal sends only raw, validated, persistable Session events. `session.page` and `session.follow` do not parse tool arguments, query the Tools registry, restore a presenter scope, execute `presentCall` or `presentResult`, or construct or clone any tool view.
@@ -51,7 +51,7 @@ The Host `ToolDefinition.presentCall`, `ToolDefinition.presentResult`, `ToolCall
| Retained | the Session log format, Remote journal lifecycle, and Conversation identity/topology |
| Retained | the existing keyed slot, Generic fallback, and Chat, Details, and Trajectory structure |
| Forbidden | a new Client presenter service, parallel registry, or wire renderer id |
| Forbidden | new cards, visual redesign, interaction redesign, or PTC dispatch rich-card enhancements except the [nested terminal-card exception](../../../../packages/client/ui-tool/README.md) and [recorded tool details](../../../../packages/client/ui-tool/README.md) |
| Forbidden | new cards, visual redesign, interaction redesign, or PTC dispatch rich-card enhancements except the [nested terminal-card exception](../../../../packages/client/ui-tool/README.md), [recorded tool details](../../../../packages/client/ui-tool/README.md), and [background Shell command display](../bug-fix/2026-10-10-background-shell-command-display.md) |
| Forbidden | compatibility dual-writing, version negotiation, or retention of the old `view` field |
## Terminology
@@ -294,15 +294,16 @@ Unknown tools continue to use `GenericToolCard` with the name, raw arguments, re
The title, kind, rawInput, content, and locations from Generic Host `presentCall` do not currently drive an ordinary Web row. Generic `presentResult.content` also does not drive Web output, so the Client need not copy these unconsumed values.
<a id="terminal-card"></a>
### Terminal card
The Client terminal model derives existing `TerminalBlock` props from the tool name, call arguments, result content, error, and Session cwd, independently of `parentCallId`.
The Client terminal model derives existing `TerminalBlock` props from the tool name, call arguments, result content, error, and Session cwd, independently of `parentCallId`. [Background Shell command display](../bug-fix/2026-10-10-background-shell-command-display.md) governs explicit background `bash`/`pwsh` launches; the remaining ownership and fallback rules below stay in force.
| Input | Preserved result |
|---|---|
| running standard `bash`/`pwsh` foreground call | terminal prompt, description, cwd, and running state |
| successful standard foreground call | terminal output, exit code/signal, and settled state presentation |
| `run_in_background:true` | Generic row and raw result |
| explicit background standard `bash`/`pwsh` | original `background` flag with call `running` at Start; acknowledgement clears `running` and retains a static command without output or job status |
| tool execution error | Generic IN/OUT and error summary |
| running persistent `bash`/`pwsh` | terminal prompt |
| settled persistent `bash`/`pwsh` | Generic flattened result, with no new exit card |
@@ -310,7 +311,7 @@ The Client terminal model derives existing `TerminalBlock` props from the tool n
| background/error `terminal_send` | Generic result |
| PTC dispatch child | same terminal eligibility and fallback rules as a root call |
Standard shell results parse trailing `[exit code: N]` and `[killed by signal: X]` markers. A final recognized spill-policy notice selects Generic output instead: expandable in shell rows and raw in Details, because the exit marker may be displaced or omitted. A parsed marker is removed from the terminal body; timeout, sandbox denial, and markers without a pill remain in the body.
Foreground standard shell results parse trailing `[exit code: N]` and `[killed by signal: X]` markers; explicit background launch acknowledgements do not enter output or exit-status parsing. A final recognized spill-policy notice selects Generic output instead: expandable in shell rows and raw in Details, because the exit marker may be displaced or omitted. A parsed marker is removed from the terminal body; timeout, sandbox denial, and markers without a pill remain in the body.
Call `description` remains above the card and overrides the collapsed summary. Workdir continues handling absolute, relative, and missing values. Relative paths resolve against the Session cwd while preserving normalization for `.`, `..`, drive letters, and UNC roots.
@@ -318,7 +319,7 @@ For `terminal_send`, non-empty input and the session id remain verbatim tool dat
Standard and persistent providers sharing the same tool name are a special compatibility point. The Client uses currently valid argument and result features to preserve their delivered differences. Input that cannot be identified unambiguously uses a Generic settled result rather than gaining new presentation.
`TerminalBlock` ANSI handling, cursor replay, wide characters, line limits, expansion, copying, and assistive text remain unchanged.
Outside the explicit background command-only presentation, `TerminalBlock` ANSI handling, cursor replay, wide characters, line limits, expansion, copying, and assistive text remain unchanged.
### Diff card
@@ -412,7 +413,7 @@ The scenario does not import Host tool packages to compute page presentation and
| orphan result | callId title and Generic output |
| interrupted call | warning/stopped state |
| foreground bash/pwsh | current terminal prompt, body, cwd, and state |
| background/error bash/pwsh | current Generic IN/OUT |
| explicit background bash/pwsh | terminal Start and static command-only acknowledgement; launch errors retain Generic IN/OUT |
| persistent shell | current running terminal and settled Generic form |
| terminal_send | current foreground terminal and background/error Generic form |
| write/edit | current intended/applied diff and error fallback |
@@ -22,7 +22,7 @@ Host presenter 与 Client keyed renderer 分担展示会形成对同一事件的
## Decision
下述展示对等要求不包含已独立批准的[嵌套 terminal 卡片修复](../../../../packages/client/ui-tool/README.zh.md)和[记录结果的工具详情](../../../../packages/client/ui-tool/README.zh.md);其他展示与所有权约束全部保留。
下述展示对等要求不包含已独立批准的[嵌套 terminal 卡片修复](../../../../packages/client/ui-tool/README.zh.md)、[记录结果的工具详情](../../../../packages/client/ui-tool/README.zh.md)和[后台 Shell 命令展示](../bug-fix/2026-10-10-background-shell-command-display.zh.md);其他展示与所有权约束全部保留。
Session Remote journal 只下发原始、已验证、可持久化的 Session event。`session.page` 和 `session.follow` 不解析工具参数,不查询 Tools registry,不恢复 presenter scope,不执行 `presentCall`/`presentResult`,也不构造或克隆任何 tool view。
@@ -51,7 +51,7 @@ Host 的 `ToolDefinition.presentCall`、`ToolDefinition.presentResult`、`ToolCa
| 保留 | Session 日志格式、Remote journal 生命周期与 Conversation identity/topology |
| 保留 | 现有 keyed slot、Generic fallback、Chat、Details 与 Trajectory 结构 |
| 禁止 | 新 Client presenter service、平行 registry 或 wire renderer id |
| 禁止 | 新卡片、视觉改版、交互改版或 PTC dispatch rich-card 增强,[嵌套 terminal 卡片例外](../../../../packages/client/ui-tool/README.zh.md)和[记录结果的工具详情](../../../../packages/client/ui-tool/README.zh.md)除外 |
| 禁止 | 新卡片、视觉改版、交互改版或 PTC dispatch rich-card 增强,[嵌套 terminal 卡片例外](../../../../packages/client/ui-tool/README.zh.md)、[记录结果的工具详情](../../../../packages/client/ui-tool/README.zh.md)和[后台 Shell 命令展示](../bug-fix/2026-10-10-background-shell-command-display.zh.md)除外 |
| 禁止 | 为兼容保留双写、版本协商或旧 `view` 字段 |
## 术语
@@ -294,15 +294,16 @@ Chat 与 Details 通过纯 helper 复用同一 block 的解析。未来缓存必
Generic Host `presentCall` 的 title、kind、rawInput、content 与 locations 当前并不驱动普通 Web 行;Generic `presentResult.content` 也不驱动 Web 输出,因此无需把这些未消费值复制到 Client。
<a id="terminal-card"></a>
### Terminal 卡片
Client terminal model 从工具名称、调用参数、结果 content、error 与 Session cwd 派生现有 `TerminalBlock` props,不依赖 `parentCallId`。
Client terminal model 从工具名称、调用参数、结果 content、error 与 Session cwd 派生现有 `TerminalBlock` props,不依赖 `parentCallId`。[后台 Shell 命令展示](../bug-fix/2026-10-10-background-shell-command-display.zh.md)拥有主动后台 `bash`/`pwsh` 启动的展示决定;下述其余所有权和 fallback 规则继续有效。
| 输入 | 保持的结果 |
|---|---|
| 标准 `bash`/`pwsh` 前台 running | terminal prompt、description、cwd、running 状态 |
| 标准前台 success | terminal output、exit code/signal 与结算状态呈现 |
| `run_in_background:true` | Generic 行与原始结果 |
| 标准 `bash`/`pwsh` 主动后台调用 | Start 使用原 `background` 标志和调用 `running`;回执清除 `running` 后保留静态命令,不显示输出或任务状态 |
| 工具执行 error | Generic IN/OUT 与错误摘要 |
| persistent `bash`/`pwsh` running | terminal prompt |
| persistent `bash`/`pwsh` settled | Generic flattened result,不新增 exit card |
@@ -310,7 +311,7 @@ Client terminal model 从工具名称、调用参数、结果 content、error
| `terminal_send` background/error | Generic 结果 |
| PTC dispatch child | 与根调用相同的 terminal 适用规则与 fallback 规则 |
标准 shell 结果解析末尾 `[exit code: N]` 与 `[killed by signal: X]`。末尾已识别的 spill 策略提示会改用 Generic 输出:在 shell 行中可展开,在 Details 中显示原文,因为退出标记可能被移位或省略。已解析的 marker 从 terminal 正文移除;timeout、sandbox denial 与没有 pill 的 marker 留在正文。
标准前台 shell 结果解析末尾 `[exit code: N]` 与 `[killed by signal: X]`;主动后台启动回执不进入输出或退出状态解析。末尾已识别的 spill 策略提示会改用 Generic 输出:在 shell 行中可展开,在 Details 中显示原文,因为退出标记可能被移位或省略。已解析的 marker 从 terminal 正文移除;timeout、sandbox denial 与没有 pill 的 marker 留在正文。
调用 `description` 继续显示在 card 上方并覆盖折叠摘要。workdir 继续按绝对、相对和缺失三种情况处理;相对路径基于 Session cwd,且保留 `.`、`..`、盘符与 UNC root 的归一化。
@@ -318,7 +319,7 @@ Client terminal model 从工具名称、调用参数、结果 content、error
同名普通与 persistent provider 是特殊兼容点。Client 使用当前有效参数与结果特征保留已交付差异;不足以无歧义识别的输入选择 Generic settled 结果,不增加新表现。
TerminalBlock 的 ANSI、光标重放、宽字符、行数上限、展开、复制与辅助技术文本完全不变。
除主动后台的仅命令展示外,TerminalBlock 的 ANSI、光标重放、宽字符、行数上限、展开、复制与辅助技术文本保持不变。
### Diff 卡片
@@ -412,7 +413,7 @@ Deliverables Definition 按 callId 观察 root `tool/call` 与成功 `tool/resul
| orphan result | callId 标题与 Generic output |
| interrupted call | warning/stopped 状态 |
| bash/pwsh 前台 | 当前 terminal prompt、正文、cwd 与状态 |
| bash/pwsh background/error | 当前 Generic IN/OUT |
| bash/pwsh 主动后台调用 | terminal Start 与静态仅命令回执展示;启动错误保留 Generic IN/OUT |
| persistent shell | 当前 running terminal、settled Generic |
| terminal_send | 当前前台 terminal、后台/error Generic |
| write/edit | 当前 intended/applied diff 与 error fallback |
@@ -9,8 +9,8 @@
en: 01293500639c9d79
zh: 6d19e4843955c811
/agent-note-browser-launch-token-authentication/decision:
en: 54efa59339f75256
zh: 0f53ead3844151c6
en: 51885594bdeb6f0c
zh: 9c2e41ad79ccdd08
/agent-note-browser-launch-token-authentication/verification:
en: 189b36d0fc5f3a17
zh: 08993cb585204fc0
@@ -10,6 +10,8 @@ The Web Host runs tool-capable Sessions with the current operating-system user's
## Decision
Browser callers follow this decision; independent CLI/MCP admission is owned by the [API connect decision](2026-10-09-operator-scoped-connect-handshake.md).
`dsh-client-connection` authenticates the complete Host API before dispatch. Every API Proxy method, Remote unary call, generic Connection channel, and Remote WebSocket stream requires the same browser session; endpoint ownership and method names do not alter authority. The existing Host/Origin checks run first and retain their DNS-rebinding and cross-site-request role, returning 403 when they fail. A trusted Host without a valid browser session receives 401. The browser-trust rules remain owned by the [carrier-level browser trust decision](2026-07-28-api-browser-trust-boundary.md).
Each Host process generates a random launch token, retained by the root application context across Connection hot reloads. `dsh-web-app` prints and opens the normal root URL with that token in the query once per process. `frontend-static` asks Connection to authorize index responses: only `GET /?token=...` exchanges the process token for a cookie, then redirects to clean `./`; the token is not accepted on API paths or in an Authorization header. An obsolete token paired with a valid cookie redirects to clean `./`. Missing and invalid credentials receive one minimal 401 response. Static non-index assets remain public.
@@ -10,6 +10,8 @@ Web Host 以当前操作系统用户的权限运行具有工具能力的 Session
## 决策
浏览器调用方遵循本决策;独立 CLI/MCP 接纳由 [API connect 决策](2026-10-09-operator-scoped-connect-handshake.zh.md)定义。
`dsh-client-connection` 在分发前认证完整 Host API。每个 API Proxy 方法、Remote 一元调用、通用 Connection channel 和 Remote WebSocket stream 都要求同一个浏览器会话;endpoint 所有权与方法名称不改变 authority。既有 Host/Origin 校验先执行,继续负责 DNS rebinding 和跨站请求防御,失败时返回 403。Host 可信但没有有效浏览器会话时返回 401。浏览器信任规则仍由[载体级浏览器信任决策](2026-07-28-api-browser-trust-boundary.zh.md)持有。
每个 Host 进程生成随机启动令牌,并由应用根 context 跨 Connection 热重载保留。`dsh-web-app` 每个进程只打印并打开一次 query 中带该令牌的普通根 URL。`frontend-static` 请求 Connection 授权 index 响应:只有 `GET /?token=...` 会把进程令牌交换为 cookie,再重定向到干净的 `./`;API 路径和 Authorization header 都不接受该令牌。过时令牌如果同时带有有效 cookie,会重定向到干净的 `./`。缺失与无效凭据得到同一份最小 401 响应。非 index 静态资产保持公开。
@@ -21,11 +21,11 @@
en: 29def175ab33b7d6
zh: 87e55ec28c20c7c1
/agent-note-package-and-update-the-electron-desktop-application/updates-and-recovery:
en: e9f140cd22670dfa
zh: bf85cfe9c12a0345
en: 4cb7014acfe71762
zh: 2178e0337cba0324
/agent-note-package-and-update-the-electron-desktop-application/security-and-release-policy:
en: 97ee608e96fc3602
zh: 932b98e7d344cd87
en: 5faaa53b58121311
zh: e1c3fc8d21a27d1a
/agent-note-package-and-update-the-electron-desktop-application/implementation:
en: 041a6d35ed61b149
zh: 617c6b90a6f4fa4d
@@ -75,13 +75,13 @@ The [immediate-window reference](../../../../apps/desktop/README.md) owns the lo
`DSH_DESKTOP_AUTO_UPDATE_ENV` selects the test deployment by default or the production deployment for both the target-specific generic-provider URL and COS destination. Release automation supplies the test HTTPS origin through `DOWNLOAD_TEST_ORIGIN` and each deployment's bucket through `DOWNLOAD_TEST_COS_BUCKET` or `DOWNLOAD_PROD_COS_BUCKET`; keeping mutable test routing and COS storage identities out of source lets deployment infrastructure change without a code release, while the public production origin remains fixed. Packaging resolves only the public updater URL, disables electron-builder publishing, removes every COS credential field from its subprocess environment, and writes a completion record only after electron-builder and every signing or notarization hook succeeds. Target upload additionally requires the selected bucket, then requires the completion record, root dsh version, Desktop version, version-derived channel metadata, artifact names, sizes, and SHA-512 values to agree before it reads the selected credentials or sends data. It uploads immutable versioned updater payloads and any separate blockmaps before replacing the channel metadata emitted by electron-builder, and it never deletes historical objects. Stable versions use the `latest` metadata name; prereleases use the first semantic-version prerelease identifier. NSIS embeds its blockmap in the signed executable; the macOS ZIP carries a separate blockmap. Both let electron-updater download changed blocks when supported, while application replacement and the local pnpm package operation remain separate operations.
Test releases require `DOWNLOAD_TEST_RELEASE_ID` from the platform dotenv file. Their feeds and binaries share `dsh-desk/<release-id>/`; production retains its fixed directories. The ID is part of the packaged update URL, so upload rejects a different ID. Internal distribution can supply a new installer after rotation; installed clients retain their existing feed. Random directories reduce guessing but provide no authorization for link holders. The [release configuration](../../../../apps/desktop/README.md#upload-updates) defines generation and reuse.
Test releases require `DOWNLOAD_TEST_RELEASE_ID` from the platform dotenv file. Their feeds and binaries share `dsh-desk/<release-id>/`; production retains its fixed directories. The ID is part of the packaged update URL, so upload rejects a different ID. Internal distribution can supply a new installer after rotation; installed clients retain their existing feed. Random directories reduce guessing but provide no authorization for link holders. The [release configuration](../../../../apps/desktop/publishing/README.md#upload-updates) defines generation and reuse.
## Security and release policy
Core dsh and the private Desktop Host come only from the signed application resource tree. Plugin installation forwards package specs to pnpm, including local and remote sources, but never accepts raw pnpm commands. pnpm owns dependency resolution and the profile’s `allowBuilds` policy; the Host loads activated bundles.
Electron release artifacts are signed; macOS artifacts are notarized. Release package and upload commands read release settings from the Git-ignored target `.env.windows` or `.env.macos`; subprocesses receive the fields selected by orchestration. The target file exclusively owns release fields so stale shell or system credentials cannot override local selection, and loading does not mutate the parent environment. Packaging validates the mode-specific application ID, update origin, signing identity, and local files before builds, downloads, or release-record cleanup; macOS also requires one complete notarization strategy. The separate `check:package` command runs the same validation without accessing the token or Apple; actual signing and notarization still verify authentication. Configuration loading rejects missing or malformed identifiers and incomplete notarization credentials, while macOS release packaging requires signing so certificate discovery cannot silently select another installed identity or emit an unsigned release. Runtime preparation verifies the exact Authority and Team ID plus the timestamp and hardened-runtime flags on every embedded Mach-O file. An after-sign hook performs Apple's deep strict application verification and requires the same leaf Authority and Team ID before artifact creation continues. The fixed-target installer command uses [isolated App copies for parallel notarization](../process/2026-09-09-parallel-macos-notarization.md): the ZIP contains a stapled App, while the signed DMG carries the ticket covering its unstapled inner App. The DMG artifact-completion hook requires the configured identity, a valid ticket, and Gatekeeper acceptance. Both artifact lanes must succeed before the command promotes their outputs and writes the release completion record; directory-only commands still notarize and staple the App. DMG blockmaps are disabled because macOS updates consume the signed ZIP, and stapling would otherwise invalidate an already-generated DMG blockmap.
Electron release artifacts are signed; macOS artifacts are notarized. Release package and upload commands read release settings from the Git-ignored target `.env.windows` or `.env.macos`; subprocesses receive the fields selected by orchestration. Windows packaging can instead select a machine-owned public `presets/<preset>.env`, plus `secrets/windows-signing.env` for signed builds; unsigned and preparation-only modes omit the signing file. The selected files exclusively own release fields so stale shell or system credentials cannot override local selection, and loading does not mutate the parent environment. Packaging validates the mode-specific application ID, update origin, signing identity, and local files before builds, downloads, or release-record cleanup; macOS also requires one complete notarization strategy. The separate `check:package` command runs the same validation without accessing the token or Apple; actual signing and notarization still verify authentication. Configuration loading rejects missing or malformed identifiers and incomplete notarization credentials, while macOS release packaging requires signing so certificate discovery cannot silently select another installed identity or emit an unsigned release. Runtime preparation verifies the exact Authority and Team ID plus the timestamp and hardened-runtime flags on every embedded Mach-O file. An after-sign hook performs Apple's deep strict application verification and requires the same leaf Authority and Team ID before artifact creation continues. The fixed-target installer command uses [isolated App copies for parallel notarization](../process/2026-09-09-parallel-macos-notarization.md): the ZIP contains a stapled App, while the signed DMG carries the ticket covering its unstapled inner App. The DMG artifact-completion hook requires the configured identity, a valid ticket, and Gatekeeper acceptance. Both artifact lanes must succeed before the command promotes their outputs and writes the release completion record; directory-only commands still notarize and staple the App. DMG blockmaps are disabled because macOS updates consume the signed ZIP, and stapling would otherwise invalidate an already-generated DMG blockmap.
The [pinned osx-sign patch](../../../../patches/@[email protected]) uses `lstat` in both published module builds, so Framework file and directory aliases do not trigger duplicate signing. The patch remains necessary until the selected upstream release skips those aliases. PAK files are resources sealed by the enclosing bundle; individual signatures add serial timestamp requests without additional resource integrity. Desktop preserves all locale files and skips only their standalone signatures. Executable code retains Developer ID signatures, secure timestamps, and hardened runtime. The [signer traversal regression](../../../../apps/desktop/tests/macos-signing-walk.spec.ts) exercises the installed dependency with real Framework aliases; release qualification still requires strict application verification, notarization, and startup.
@@ -91,7 +91,7 @@ Windows package invocations force `ELECTRON_BUILDER_7Z_FILTER=BCJ`. The bundled
Local Windows and macOS installation testing uses an explicit `--unsigned` package invocation with the same build and runtime preparation. It strips certificate inputs, isolates artifacts in `unsigned-artifacts`, and omits updater configuration and the release completion record. macOS local builds ignore dotenv files and inherited release settings, use the standard application ID, and omit mandatory-update policy so local testing needs no deployment configuration. Their App receives a certificate-free ad-hoc signature; their DMG is unsigned and unnotarized. Windows local builds retain their platform configuration requirements. The regular package command explicitly selects signed mode even when its parent environment requests unsigned mode. This separation permits installation diagnosis without release credentials while preventing local test output from qualifying for release upload.
Windows application replacement follows the [directory-installation reference](../../../../apps/desktop/README.md): extract beside the destination with a command-line tool that returns failure status, then rename complete directories on the same volume. The installer retains the old directory through staging and restores it if promotion fails. Registration, shortcuts, and the signed uninstaller remain owned by electron-builder.
Windows application replacement follows the [directory-installation reference](../../../../apps/desktop/packaging/README.md#runtime-file-selection): extract beside the destination with a command-line tool that returns failure status, then rename complete directories on the same volume. The installer retains the old directory through staging and restores it if promotion fails. Registration, shortcuts, and the signed uninstaller remain owned by electron-builder.
Packaged applications ignore development resource and project environment overrides. Only an unpackaged Electron process can replace the pnpm entry, dsh resources, or active project.
@@ -75,13 +75,13 @@ Electron 更新只使用一个 `electron-updater` 发布流和签名 `electron-b
`DSH_DESKTOP_AUTO_UPDATE_ENV` 默认为测试部署,也可以选择生产部署,并同时决定目标专用的 generic-provider URL 与 COS 目标。发布自动化通过 `DOWNLOAD_TEST_ORIGIN` 提供测试 HTTPS origin,并通过 `DOWNLOAD_TEST_COS_BUCKET` 或 `DOWNLOAD_PROD_COS_BUCKET` 提供各部署的 bucket;可变的测试路由与 COS 存储身份不写入源码,部署基础设施变更时无需发布新代码,而公开的生产 origin 仍固定。打包只解析公开更新 URL、禁止 electron-builder 发布、从子进程环境中删除每个 COS 凭据字段,并且只有在 electron-builder 以及每个签名或公证 hook 成功后才写入完成记录。目标上传还必须提供所选 bucket,随后会先要求完成记录、根 dsh 版本、Desktop 版本、根据版本得出的频道元数据、产物名称、大小与 SHA-512 全部一致,再读取所选凭据或发送数据。它先上传不可变且带版本的更新载荷与所有独立 blockmap,最后替换 electron-builder 生成的频道元数据,并且不会删除历史对象。稳定版本使用 `latest` 元数据名称,预发布版本则使用语义化版本的第一个预发布标识符。NSIS 把 blockmap 嵌入已签名的可执行文件,macOS ZIP 则使用独立 blockmap;两者都让 electron-updater 在平台支持时只下载变化的数据块,而应用替换与本地 pnpm 包操作仍是两个独立操作。
测试发布必须从平台 dotenv 文件提供 `DOWNLOAD_TEST_RELEASE_ID`。清单和安装包共用 `dsh-desk/<release-id>/`,生产环境保留固定目录。ID 是打包更新 URL 的一部分,因此上传会拒绝不同的 ID。轮换后可通过内部分发提供新安装包,已安装客户端保留原清单地址。随机目录降低被猜中的概率,但不对持有链接者进行鉴权。[发布配置](../../../../apps/desktop/README.zh.md#upload-updates)定义生成与复用方式。
测试发布必须从平台 dotenv 文件提供 `DOWNLOAD_TEST_RELEASE_ID`。清单和安装包共用 `dsh-desk/<release-id>/`,生产环境保留固定目录。ID 是打包更新 URL 的一部分,因此上传会拒绝不同的 ID。轮换后可通过内部分发提供新安装包,已安装客户端保留原清单地址。随机目录降低被猜中的概率,但不对持有链接者进行鉴权。[发布配置](../../../../apps/desktop/publishing/README.zh.md#upload-updates)定义生成与复用方式。
## 安全与发布策略
核心 dsh 和私有 Desktop Host 只来自签名应用的资源树。插件安装把包规格交给 pnpm,包括本地和远程来源,但不接受原始 pnpm 命令。pnpm 负责依赖解析和 profile 的 `allowBuilds` 策略;Host 加载已启用的 bundle。
Electron 发布产物必须签名;macOS 产物必须公证。发布打包与上传命令从 Git 忽略的目标 `.env.windows` 或 `.env.macos` 读取发布配置,子进程通过编排器选择的环境字段接收配置。目标文件是发布字段的唯一来源,避免旧的 shell 或系统凭据覆盖本地选择;配置加载不修改父进程环境。打包在构建、下载或清理发布记录前校验该模式必需的应用 ID、更新地址、签名身份及本地文件,macOS 还要求一套完整公证凭据。单独的 `check:package` 执行同一校验而不访问 Token 或 Apple;凭据真实性仍由实际签名与公证验证。配置加载会拒绝缺失或格式错误的标识符和不完整的公证凭据,macOS 发布打包还会强制签名,避免证书发现过程静默选择其他已安装身份或生成未签名发布。运行时准备会验证每个内嵌 Mach-O 文件的精确 Authority 与 Team ID,以及时间戳和 hardened-runtime 标记。签名后钩子会执行 Apple 的深度严格应用验证,并要求同一叶证书 Authority 与 Team ID 完全匹配,验证通过后才继续生成产物。固定目标安装包命令使用[隔离的 App 副本并行公证](../process/2026-09-09-parallel-macos-notarization.zh.md):ZIP 包含已钉票的 App,签名 DMG 则携带覆盖其中未钉票 App 的票据。DMG 的 artifact-completion hook 要求其使用配置的身份、具备有效票据并通过 Gatekeeper。只有两条产物流都成功,命令才会移入其输出并写入发布完成记录;仅生成目录的命令仍会公证 App 并钉票。macOS 更新使用签名 ZIP,因此 DMG 不生成 blockmap;否则钉票会让已经生成的 DMG blockmap 失效。
Electron 发布产物必须签名;macOS 产物必须公证。发布打包与上传命令从 Git 忽略的目标 `.env.windows` 或 `.env.macos` 读取发布配置,子进程通过编排器选择的环境字段接收配置。Windows 打包也可选择机器管理的公开 `presets/<preset>.env`,签名构建另读 `secrets/windows-signing.env`;未签名和仅准备模式不读取签名文件。所选文件是发布字段的唯一来源,避免旧的 shell 或系统凭据覆盖本地选择;配置加载不修改父进程环境。打包在构建、下载或清理发布记录前校验该模式必需的应用 ID、更新地址、签名身份及本地文件,macOS 还要求一套完整公证凭据。单独的 `check:package` 执行同一校验而不访问 Token 或 Apple;凭据真实性仍由实际签名与公证验证。配置加载会拒绝缺失或格式错误的标识符和不完整的公证凭据,macOS 发布打包还会强制签名,避免证书发现过程静默选择其他已安装身份或生成未签名发布。运行时准备会验证每个内嵌 Mach-O 文件的精确 Authority 与 Team ID,以及时间戳和 hardened-runtime 标记。签名后钩子会执行 Apple 的深度严格应用验证,并要求同一叶证书 Authority 与 Team ID 完全匹配,验证通过后才继续生成产物。固定目标安装包命令使用[隔离的 App 副本并行公证](../process/2026-09-09-parallel-macos-notarization.zh.md):ZIP 包含已钉票的 App,签名 DMG 则携带覆盖其中未钉票 App 的票据。DMG 的 artifact-completion hook 要求其使用配置的身份、具备有效票据并通过 Gatekeeper。只有两条产物流都成功,命令才会移入其输出并写入发布完成记录;仅生成目录的命令仍会公证 App 并钉票。macOS 更新使用签名 ZIP,因此 DMG 不生成 blockmap;否则钉票会让已经生成的 DMG blockmap 失效。
[固定版本的 osx-sign 补丁](../../../../patches/@[email protected])在两种已发布模块构建中使用 `lstat`,因此 Framework 的文件和目录别名不会触发重复签名。选定的上游版本能够跳过这些别名前,仍需保留该补丁。PAK 文件由外层 bundle 签名记录完整性;逐个签名会增加串行时间戳请求,但不会增加资源完整性保护。Desktop 保留全部语言文件,只跳过其单独签名。可执行代码仍使用 Developer ID 签名、安全时间戳和 hardened runtime。[签名器遍历回归测试](../../../../apps/desktop/tests/macos-signing-walk.spec.ts)使用真实 Framework 别名执行已安装依赖;发布验收仍要求严格应用验证、公证和启动。
@@ -91,7 +91,7 @@ Windows 打包调用强制设置 `ELECTRON_BUILDER_7Z_FILTER=BCJ`。内置的 7-
本地 Windows 和 macOS 安装测试使用显式的 `--unsigned` 打包调用,并执行相同的构建和运行时准备。它清除证书输入,将产物隔离到 `unsigned-artifacts`,并省略更新器配置和发布完成记录。macOS 本地构建忽略 dotenv 文件和继承的发布配置,使用标准应用 ID,并省略强制更新策略,使本地测试无需部署配置。其 App 使用无需证书的 ad-hoc 签名,DMG 不签名也不公证。Windows 本地构建保留平台配置要求。即使父进程环境请求未签名模式,常规打包命令也会显式选择签名模式。这样既能在没有发布凭据时诊断安装问题,也能防止本地测试产物通过发布上传校验。
Windows 应用替换遵循[目录安装参考](../../../../apps/desktop/README.zh.md):使用能返回失败状态的命令行工具解压到目标旁边,再在同卷内改名替换完整目录。安装器在暂存期间保留旧目录,正式替换失败时恢复旧目录。注册信息、快捷方式和签名卸载器仍由 electron-builder 持有。
Windows 应用替换遵循[目录安装参考](../../../../apps/desktop/packaging/README.zh.md#runtime-file-selection):使用能返回失败状态的命令行工具解压到目标旁边,再在同卷内改名替换完整目录。安装器在暂存期间保留旧目录,正式替换失败时恢复旧目录。注册信息、快捷方式和签名卸载器仍由 electron-builder 持有。
打包应用会忽略开发资源和项目环境变量覆盖。只有未打包的 Electron 进程可以替换 pnpm 入口、dsh 资源 或活跃项目。
@@ -12,8 +12,8 @@
en: 1188c699ce6e6ca2
zh: 4d5b974cd1216f06
/agent-note-adjacent-agents-share-one-steer-send-message-operation/decision/one-model-tool-and-one-return-instruction:
en: 8a4be155a4984ca8
zh: 00c2a61da2443530
en: 4b0a25389324fa91
zh: c2cdd9f7e40ef43b
/agent-note-adjacent-agents-share-one-steer-send-message-operation/decision/complete-removal-and-reintroduction-condition:
en: 5a9ec6478cf7430c
zh: f4c2f067cc72040e
@@ -46,7 +46,7 @@ interface SendMessageInput {
}
```
Parents and children inherit the same definition in the same registry order. The standard definition carries a process-stable internal identity that a scoped same-name tool does not satisfy. A child `toolFilter` may explicitly remove the inherited tool, and a scoped replacement may provide different semantics; neither case receives the standard call instruction. When the standard tool remains visible, the continuation manager appends the JSON-encoded direct parent id and the instruction to send one self-contained result before finishing, plus earlier actionable findings, to the child's initial user task. For a fork child this task follows the inherited completed-turn prefix; no child-only system-prompt section or tool schema precedes that prefix.
Parents and children inherit the same definition in the same registry order. The standard definition carries a process-stable internal identity that a scoped same-name tool does not satisfy. An independently selected preset may omit the tool, a historical descriptor filter may remove it on resume, and a scoped replacement may provide different semantics; neither case receives the standard call instruction. When the standard tool remains visible, the continuation manager appends the JSON-encoded direct parent id and the instruction to send one self-contained result before finishing, plus earlier actionable findings, to the child's initial user task. For a fork child this task follows the inherited completed-turn prefix; no child-only system-prompt section or tool schema precedes that prefix.
The instruction is guidance, not settlement enforcement. Sending does not end the child's turn, zero or several calls remain mechanically valid, and the runtime never rejects a child for staying silent. The manager-owned `subagent-settled` notice remains unconditional and separately attributed because it records how an Activation ended and preserves terminal output when the child cannot cooperate.
@@ -46,7 +46,7 @@ interface SendMessageInput {
}
```
parent 与 child 以相同注册表顺序继承相同定义。标准定义携带进程稳定的内部身份,同名的作用域工具不满足该身份。child `toolFilter` 可以显式移除继承的工具,作用域替代工具也可以提供不同语义;两种情况都不会收到标准调用指令。当标准工具仍可见时,继续执行管理器会把经过 JSON 编码的直接 parent id、结束前发送一份自包含结果的指令,以及更早发送可操作发现的指令追加到 child 初始用户任务。对 fork child 而言,该任务位于继承的已完成轮次前缀之后;没有 child 专属系统提示词 section 或工具 schema 位于此前缀之前。
parent 与 child 以相同注册表顺序继承相同定义。标准定义携带进程稳定的内部身份,同名的作用域工具不满足该身份。独立选择的 preset 可以省略该工具,历史 descriptor 过滤器可在恢复时移除该工具,作用域替代工具也可以提供不同语义;两种情况都不会收到标准调用指令。当标准工具仍可见时,继续执行管理器会把经过 JSON 编码的直接 parent id、结束前发送一份自包含结果的指令,以及更早发送可操作发现的指令追加到 child 初始用户任务。对 fork child 而言,该任务位于继承的已完成轮次前缀之后;没有 child 专属系统提示词 section 或工具 schema 位于此前缀之前。
该指令是指导,不是结算强制。发送不会结束 child 轮次,机制仍允许零次或多次调用,runtime 绝不会因 child 保持沉默而拒绝它。由管理器负责的 `subagent-settled` 通知仍无条件发送并采用独立来源,因为它记录 Activation 如何结束,并在 child 无法配合时保留终态输出。
@@ -18,5 +18,5 @@
en: 5917a573cbdc5fa4
zh: 3c3769070539a0a9
/agent-note-handle-based-session-persistence/related:
en: 6b160ed89d2f91a7
zh: 23ee33d03847d0d0
en: 0ff54c05c2a64c07
zh: f3bd6a5d1f37563f
@@ -36,6 +36,7 @@ Resume, fork, subagent, ACP, webhook, and SDK sessions all persist through one e
## Related
- [Cleanup dependency lifetimes](../../proposed/architecture/2026-10-09-cordis-cleanup-dependency-lifetimes.md) proposes retaining final events emitted during graceful cleanup, tightening this decision's permitted root-disposal tail loss while preserving handle ownership and storage format.
- [Session persistence as an abstract service](../../../../packages/session/session-persistence/README.md) — the seam this reshapes; its interface list reflects the handle API.
- [Persistence export() and pre-release trims](../../archived/simplification/2026-08-27-persistence-export-and-pre-release-trims.md) — the preparatory removals, including the retired cold blank probe.
- [Retain ignorable external session events](2026-08-30-retain-ignorable-external-session-events.md) — the read-side refusal contract, now shared through `storage-contract` helpers.
@@ -36,6 +36,7 @@ Status: implemented
## 相关
- [清理依赖生命周期](../../proposed/architecture/2026-10-09-cordis-cleanup-dependency-lifetimes.zh.md)提议保留有序清理期间发出的最终事件,收紧本决定允许的根释放尾部丢失,同时保留句柄所有权与存储格式。
- [作为抽象服务的会话持久化](../../../../packages/session/session-persistence/README.zh.md)——本 Note 重塑的 seam;其接口列表已反映句柄 API。
- [持久化 export() 与预发布读取路径精简](../../archived/simplification/2026-08-27-persistence-export-and-pre-release-trims.md)——预备性的移除,包括已退役的冷空白探测。
- [保留可忽略的外部会话事件](2026-08-30-retain-ignorable-external-session-events.zh.md)——读取侧的拒绝约定,现经由 `storage-contract` 辅助函数共享。
@@ -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」的断言来回应。
@@ -15,5 +15,5 @@
en: 09be2a1cf1d52fd5
zh: 0bf0e18c73542012
/agent-note-one-output-ring-one-projection-one-event-stream-for-background-jobs/consequences:
en: 464eb843a71eb04e
zh: 783062f5b288b1b4
en: 3263801e173b6b86
zh: f6f51bb3f77a817e
@@ -35,3 +35,5 @@ The job seam had grown by accretion. Producers declared their output twice — a
The model-visible text is unchanged: status lines, tool descriptions and schemas, and notice wording are byte-identical, and the only new model-visible fact is the kill reason a `killed` status line now carries. A producer that needs to stream output picks pull sources or `append`; it never formats a consuming delta. The Web client renders every live job as expandable, so a job that never writes shows an empty running panel instead of a static row. `JobKindMap` merges are host-side, so a browser program types `JobView.kind` as the kinds it knows and treats the rest as opaque strings, which is all the roster does with it.
Review follow-ups after the master merge tightened four of these edges. A job's first event is always `registered`: the announcement precedes the pump, whose first drain runs synchronously and may announce output. A settled ring keeps every byte the model cursor has not consumed until the first terminal model read, so a job that finishes before its first `job_output` loses nothing the live cap retained. `dsh-tool-jobs` holds one claim per wait call with a `live` flag that the caller's abort clears synchronously from the abort event, so a settlement announced before the registry's rejection reaches the tool still delivers its notice, and a timed-out wait drops only its own claim while a concurrent wait keeps the settlement covered. The browser records an observation failure that precedes the anchor as an errored view, so the panel shows the notice instead of staying blank. The spill file a pull source keeps is job metadata (`JobView.output.spillPaths`, reported by every pump read and withdrawn once a read stops naming it), not per-chunk metadata, and the model's read renders the shell tools' dropped-output notice for a producer-side gap as well as for ring eviction, naming those files even after the gap chunk itself was evicted — the `before this read` variant this change had introduced is gone. An observation generation that sees its job removed mid-stream (the owner's teardown) closes with the removed job's terminal projection instead of a failed read. A rejected background spawn produces no process output, so the shell providers now serve the `spawn failed: …` note as the whole observed stderr stream (the consuming read still folds it in once), and `ShellProcess.observed` with both readers is required: the model's first `job_output` after such a failure reads exactly what master's consuming read rendered.
The [Session lifecycle decision](../bug-fix/2026-09-30-job-session-lifecycle.md) records registration and settlement for durable SDK consumers while preserving this live event stream and output ownership.
@@ -35,3 +35,5 @@ job seam 是逐步堆出来的。生产者要把输出声明两次——模型
模型可见文本保持不变:状态行、工具描述与 schema、播报措辞逐字节相同,唯一新增的模型可见事实是 `killed` 状态行现在携带的 kill 原因。需要流式输出的生产者选择拉取源或 `append`,从不格式化消耗式增量。Web 客户端把每个进行中的 job 渲染为可展开,因此从不写输出的 job 显示为空的运行中面板而非静态行。`JobKindMap` 的合并发生在宿主侧,浏览器程序把 `JobView.kind` 视为它认识的 kind 集合,其余当作不透明字符串——名册对它也只做这些。
合并 master 之后的评审跟进收紧了其中四条边。job 的第一个事件永远是 `registered`:通告先于泵,而泵的首轮 drain 同步执行且可能通告输出。已结算的环在模型的首次终态读取之前保留模型游标尚未消费的全部字节,因此在首次 `job_output` 之前结束的 job 不会丢失运行期上限保留的任何内容。`dsh-tool-jobs` 为每次等待调用保存一条带 `live` 标记的认领,调用方 abort 时在 abort 事件里同步清掉该标记,因此注册表的拒绝尚未传回工具之前宣布的结算仍会投递通知;超时的等待只丢弃自己的认领,并发的另一个等待仍覆盖该结算。浏览器把先于锚点发生的观察失败记录为带错误的视图,面板显示提示而不是留白。拉取源保留的 spill 文件是 job 元数据(`JobView.output.spillPaths`,每次泵读取都会上报,读取不再点名时即撤回),不是逐块元数据;模型读取对生产者侧缺口与环淘汰都渲染 shell 工具的丢失输出提示并列出这些文件,即使缺口块本身已被淘汰——本次变更曾引入的 `before this read` 变体已删除。观察生成过程若在流中看到其 job 被移除(拥有者 teardown),以被移除 job 的终态投影收尾,而不是一次失败的读取。被拒绝的后台 spawn 不产生任何进程输出,因此 shell provider 现在把 `spawn failed: …` 提示作为 observed stderr 的整条流提供(消耗式读取仍只折入一次),且 `ShellProcess.observed` 及其两个读取器改为必填:这类失败之后模型的首次 `job_output` 读到的正是 master 的消耗式读取所渲染的内容。
[Session 生命周期决策](../bug-fix/2026-09-30-job-session-lifecycle.zh.md)为持久化 SDK 消费方记录注册与结算,同时保留这里的运行期事件流和输出归属。
@@ -12,14 +12,14 @@
en: e01df5ba880c2972
zh: e62074a6a9ba5685
/agent-note-bundle-the-desktop-runtime-and-retain-external-plugins/package-ownership:
en: 37cae6b28fd359ef
zh: 63118e361fa8f84c
en: f683605bf4655db8
zh: de09d610ee1d4193
/agent-note-bundle-the-desktop-runtime-and-retain-external-plugins/transactions-and-upgrades:
en: 53d48f71fcc1c52e
zh: 7d61923484dbec1c
/agent-note-bundle-the-desktop-runtime-and-retain-external-plugins/alternatives-considered:
en: ead5bbd94044c3de
zh: 057cf069413d2a49
en: 67b3ba2b37e92f88
zh: 785083e5de098af8
/agent-note-bundle-the-desktop-runtime-and-retain-external-plugins/consequences:
en: 6dfa581ec6879c5b
zh: f6c735ad6a00b2b3
@@ -26,7 +26,7 @@ The resource descriptor records the exact release, Node version, platform, archi
The [Desktop file policy](../../../../apps/desktop/scripts/runtime-file-policy.ts) applies after production npm installation and before native signing or descriptor generation. npm publication lists serve library consumers and can include declarations, maps, tests, and native build inputs; they do not identify the files needed by the Desktop process. The Desktop copy omits declarations and recognized source maps because Host execution uses JavaScript and generated Typert artifacts. The Host inherits the user environment. Published npm packages and external plugin directories retain their own files. Source debugger navigation is a development-package capability.
Package-specific exclusions remove Domino tests, fs-ext compilation outputs, Koffi's Windows import library, and non-target node-pty prebuilds and debug symbols. The policy retains native executable dependencies, node-pty's ConPTY source distribution, licenses, and unrecognized assets; broad `src`, `test`, `.ts`, or `.map` exclusions could remove executable code or runtime data. Copy tests preserve sentinel assets and seal the filtered inventory; the Electron [payload smoke](../../../../apps/desktop/tests/fixtures/runtime-payload-smoke.mjs) verifies PTY output, native file seeking, FFI, image conversion, and HTML parsing. Runtime preparation still verifies every retained byte and boots the complete Host with an external plugin.
Package-specific exclusions remove Domino tests, fs-ext compilation outputs, Koffi's Windows import library, and non-target node-pty prebuilds and debug symbols. The policy retains native executable dependencies, licenses, and unrecognized assets; node-pty's ConPTY source distribution is retained only for Windows targets; broad `src`, `test`, `.ts`, or `.map` exclusions could remove executable code or runtime data. Copy tests preserve sentinel assets and seal the filtered inventory; the Electron [payload smoke](../../../../apps/desktop/tests/fixtures/runtime-payload-smoke.mjs) verifies PTY output, native file seeking, FFI, image conversion, and HTML parsing. Runtime preparation still verifies every retained byte and boots the complete Host with an external plugin.
The shared profile runner supplies missing installation and selected-bundle dependencies through the runtime resolution. pnpm-installed packages take precedence. Runtime resolution delegates to the selected package paths, so Host and plugin imports reaching the same export share its module instance. Distinct ESM and CommonJS conditional exports remain distinct entry points; runtime resolution does not merge a package’s dual implementations.
@@ -48,7 +48,7 @@ The [immediate-window reference](../../../../apps/desktop/README.md) owns direct
## Alternatives considered
Full runtime verification belongs to packaging. Startup reads the resource descriptor and checks shared package records and required Host entries. The [release-validation reference](../../../../apps/desktop/README.md) assigns release and target compatibility checks to packaging. Startup neither enumerates nor hashes installed runtime files. Reading every file before backend loading adds I/O proportional to distribution size; unusable modules instead fail when loaded. Build-time verification rejects changed, missing, extra, or linked files against the recorded inventory.
Full runtime verification belongs to packaging. Startup reads the resource descriptor and checks shared package records and required Host entries. The [release-validation reference](../../../../apps/desktop/packaging/README.md#desktop-runtime-preparation) assigns release and target compatibility checks to packaging. Startup neither enumerates nor hashes installed runtime files. Reading every file before backend loading adds I/O proportional to distribution size; unusable modules instead fail when loaded. Build-time verification rejects changed, missing, extra, or linked files against the recorded inventory.
- **Install the bundled offline seed at startup.** This preserves an ordinary pnpm installation procedure but repeats core extraction and installation on every affected machine. Materialized resources remove that work at the cost of more application files and release-builder responsibility.
- **Force host dependency versions into plugins.** This unnecessarily couples ordinary plugin dependencies to the host. The runtime resolution supplies missing packages while pnpm-owned entries retain independent versions.
@@ -26,7 +26,7 @@ Desktop 初始化时安装核心依赖图,会重复发布构建器已经完成
[桌面文件规则](../../../../apps/desktop/scripts/runtime-file-policy.ts)在生产 npm 依赖安装之后、原生签名或描述文件生成之前执行。npm 发布列表服务于库的使用者,可以包含声明、map、测试和原生构建输入,不能直接表示桌面进程需要哪些文件。桌面副本排除声明和已识别的 source map,因为 Host 执行 JavaScript 和生成的 Typert 产物。Host 继承用户环境。已发布的 npm 包和外部插件目录保留各自的文件。源码调试导航由开发包提供。
包专用排除项包括 Domino 测试、fs-ext 编译产物、Koffi 的 Windows 导入库,以及非目标平台的 node-pty 预构建文件和调试符号。规则保留原生可执行依赖、node-pty 的 ConPTY 源分发内容、许可证和未知资源;宽泛排除 `src`、`test`、`.ts` 或 `.map` 可能移除可执行代码或运行时数据。复制测试保留哨兵资源并封存过滤后的清单;Electron 的[产物 smoke](../../../../apps/desktop/tests/fixtures/runtime-payload-smoke.mjs)验证 PTY 输出、原生文件定位、FFI、图像转换和 HTML 解析。运行时准备仍会验证每个保留字节,并携带外部插件启动完整 Host。
包专用排除项包括 Domino 测试、fs-ext 编译产物、Koffi 的 Windows 导入库,以及非目标平台的 node-pty 预构建文件和调试符号。规则保留原生可执行依赖、许可证和未知资源;node-pty 的 ConPTY 源分发内容仅在 Windows 目标中保留;宽泛排除 `src`、`test`、`.ts` 或 `.map` 可能移除可执行代码或运行时数据。复制测试保留哨兵资源并封存过滤后的清单;Electron 的[产物 smoke](../../../../apps/desktop/tests/fixtures/runtime-payload-smoke.mjs)验证 PTY 输出、原生文件定位、FFI、图像转换和 HTML 解析。运行时准备仍会验证每个保留字节,并携带外部插件启动完整 Host。
共享 profile runner 通过 runtime resolution 补全安装包与选中 bundle 缺失的依赖。pnpm 安装的包优先。运行时解析委托给所选包路径,因此 Host 与插件导入同一导出时共享其模块实例。不同的 ESM 与 CommonJS 条件导出仍是不同入口;运行时解析不会合并包的双重实现。
@@ -48,7 +48,7 @@ profile manifest 分别记录 pnpm 安装的依赖及已启用 bundle 列表。
## 考虑过的替代方案
完整运行时验证属于打包流程。启动读取资源描述文件,并检查共享包记录及必要的 Host 入口。[发布验证参考](../../../../apps/desktop/README.zh.md)将发布与目标兼容性检查交给打包流程。启动既不枚举已安装运行时文件,也不计算其哈希。后端加载前读取每个文件会增加与分发体积成正比的 I/O;不可用模块改由加载时失败暴露。构建时验证按记录清单拒绝内容变化、缺失、多余或链接文件。
完整运行时验证属于打包流程。启动读取资源描述文件,并检查共享包记录及必要的 Host 入口。[发布验证参考](../../../../apps/desktop/packaging/README.zh.md#desktop-runtime-preparation)将发布与目标兼容性检查交给打包流程。启动既不枚举已安装运行时文件,也不计算其哈希。后端加载前读取每个文件会增加与分发体积成正比的 I/O;不可用模块改由加载时失败暴露。构建时验证按记录清单拒绝内容变化、缺失、多余或链接文件。
- **启动时安装内置离线 seed。** 这保留普通 pnpm 安装流程,但会在每台受影响机器上重复核心解压与安装。物化资源消除了这部分工作,代价是更多应用文件和发布构建器责任。
- **强制插件使用 Host 依赖版本。** 这会让普通插件依赖不必要地耦合于 Host。runtime resolution 提供缺失的包,pnpm 拥有的条目保留独立版本。
@@ -33,8 +33,8 @@
en: 99308bd758859e02
zh: 35f1e7719fe80d2e
/agent-note-add-immutable-profile-resolution-generations/decision/additive-package-changes:
en: 0e5e2006caec3c91
zh: fb3b61dd5692d2e3
en: 39d27cff0d419259
zh: 6cbdc88a450614d0
/agent-note-add-immutable-profile-resolution-generations/decision/filesystem-and-runtime-carriers:
en: b0607713102a093d
zh: 2de9c33bfaa0e3e2
@@ -107,7 +107,7 @@ New Workers inherit the latest published generation. Existing Workers keep the g
A caller adding a package completes its pnpm transaction before constructing a successor generation. Replacement rejects any generation that changes the directory or version of a retained package. Plugin Manager publishes the successor before mounting the new Loader row. A mount failure may leave the package installed but inactive.
A retained package keeps its normalized directory, version, and scope; installation entries also keep their declaring anchor and cannot be removed. Violating these restrictions requires process restart because Node's ESM Module Map, CommonJS cache, existing object references, and running Workers can retain the old module identity. A profile entry may change its declaring anchor: deselecting the first bundle that reaches a shared dependency can leave another bundle selecting the same physical package. Profile-scoped mappings and profile-local package names may be removed; the caller stops their plugins before deleting package files. These restrictions do not prohibit removing a linked root from the interception scope. Generation replacement does not claim to unload modules.
A retained package keeps its normalized directory, version, and scope; installation entries also keep their declaring anchor and cannot be removed. Violating these restrictions requires process restart because Node's ESM Module Map, CommonJS cache, existing object references, and running Workers can retain the old module identity. A profile entry may change its declaring anchor: deselecting the first bundle that reaches a shared dependency can leave another bundle selecting the same physical package. Profile-scoped mappings and profile-local package names may be removed; the caller stops their plugins before deleting package files. A profile mapping moves to another directory only through removal and a new installation; a linked root may point to another target ([replacement Note](2026-10-01-replace-changed-package-modules-on-recomposition.md)). These restrictions do not prohibit removing a linked root from the interception scope. Generation replacement does not claim to unload modules.
### Filesystem and runtime carriers
@@ -107,7 +107,7 @@ runtime resolution 列出它提供的包;Loader entries 组成活动插件列
添加包的调用方先完成 pnpm 事务,再构造下一代。替换操作会拒绝改变任何保留包的目录或版本。Plugin Manager 先发布后继 generation,再挂载新的 Loader 配置项。挂载失败可以留下已安装但未启用的包。
保留包的规范化目录、版本和作用域必须不变;安装条目还必须保留声明锚点,且不能移除。违反这些限制需要重启,因为 Node 的 ESM Module Map、CommonJS cache、现存对象引用和运行中的 Worker 都可能保留旧模块 identity。profile 条目可以改变声明锚点:取消选入最先找到共享依赖的组合包后,另一个组合包仍可能选中同一物理包。profile 范围的映射和 profile 本地包名可以移除;调用方在删除包文件之前先停止相关插件。这些限制不禁止从拦截范围移除 linked root。generation 换代不声称卸载模块。
保留包的规范化目录、版本和作用域必须不变;安装条目还必须保留声明锚点,且不能移除。违反这些限制需要重启,因为 Node 的 ESM Module Map、CommonJS cache、现存对象引用和运行中的 Worker 都可能保留旧模块 identity。profile 条目可以改变声明锚点:取消选入最先找到共享依赖的组合包后,另一个组合包仍可能选中同一物理包。profile 范围的映射和 profile 本地包名可以移除;调用方在删除包文件之前先停止相关插件。profile 映射只能通过卸载和重新安装移到其他目录;linked root 可以指向其他目标([替换 Note](2026-10-01-replace-changed-package-modules-on-recomposition.zh.md))。这些限制不禁止从拦截范围移除 linked root。generation 换代不声称卸载模块。
### 文件系统与运行时载体
@@ -9,8 +9,11 @@
en: f67f6add03a36e85
zh: 656be288f8b2e1d2
/agent-note-posix-ssh-execution-providers/decision:
en: b2d1e370d008f0f3
zh: 3e98919538d3fb78
en: bc22949098fc3028
zh: 76618180bc0bac56
/agent-note-posix-ssh-execution-providers/decision/helper-deployment:
en: c479fdb58ed617f1
zh: 17cfefa778e3fd71
/agent-note-posix-ssh-execution-providers/alternatives-considered:
en: 4efffb3edba91689
zh: 40100a70e5f55a42
@@ -18,8 +21,8 @@
en: f29359ab89feb084
zh: db7505b8d4a5351a
/agent-note-posix-ssh-execution-providers/verification:
en: 822c7c029a77f3c0
zh: 4a2eaaf3372cba8b
en: ee72e88fd40d6769
zh: 56f0e0c8f3365973
/agent-note-posix-ssh-execution-providers/deferred-work:
en: f300f112c9a09f7e
zh: b4708a0b7f596420
@@ -22,12 +22,20 @@ Each stream receives a fresh 256-bit TLS pre-shared key through private administ
Collected stdout and stderr carry bounded tail snapshots through handlers that only update output observations. Capture continues while a snapshot is waiting for transport. Final snapshots preserve raw-byte offsets, and completed spill files use the local provider’s retained-output storage after connection disposal.
Readiness verifies the installed helper digest and, when PTC is configured, the installed Node bootstrap digest. These checks pin expected deployment artifacts; they do not authenticate a malicious remote operating system. The helper disables Node debugger activation through `SIGUSR1`: file-effect confinement can still permit same-user signals, which must not expose the helper's unrestricted filesystem and process services. File-effect confinement delegates to the remote local sandbox provider and retains its full/partial disclosure and platform limitations.
Readiness verifies the installed helper script or executable digest and, when a script deployment supplies a PTC bootstrap, that bootstrap's digest. Executable deployments embed the PTC worker and require no separate bootstrap. These checks pin expected deployment artifacts; they do not authenticate a malicious remote operating system. The helper disables Node debugger activation through `SIGUSR1`: file-effect confinement can still permit same-user signals, which must not expose the helper's unrestricted filesystem and process services. File-effect confinement delegates to the remote local sandbox provider and retains its full/partial disclosure and platform limitations.
Path canonicalization belongs where the files exist. The shared policy resolver preserves absolute execution-world spelling; enforcing providers resolve symlinks and `..` on their own filesystem. Headless records and validates cwd through `ctx.fs`. Host-path projection remains unavailable for SSH, so Node execution requires an explicitly installed remote bootstrap.
Path canonicalization belongs where the files exist. The shared policy resolver preserves absolute execution-world spelling; enforcing providers resolve symlinks and `..` on their own filesystem. Headless records and validates cwd through `ctx.fs`. Host-path projection remains unavailable for SSH; PTC uses an explicitly installed remote bootstrap for script deployments or the helper executable's embedded worker.
A lost connection invalidates pending operations without reconnect or replay. Helper EOF, signals and a heartbeat lease initiate remote native cleanup. The client cannot turn lease expiry into an observed successful termination: an interrupted mutation or launch can have an unknown outcome. Administrative request deadlines, consumer execution deadlines and cleanup remain separate owners.
### Helper deployment
The explicit `launch.kind` selects remote `node-script` or `executable` invocation independently of the local Harness carrier. A local `process.pkg` flag cannot describe the remote installation. Protocol 2 includes the remote runtime kind in its readiness response and rejects protocol or launch-kind mismatches, so a client cannot apply script bootstrap rules to an embedded worker. Client and helper protocol upgrades must be deployed together.
The executable embeds Node, the helper, managed subprocess runners and the PTC worker. It reuses the [SDK executable packaging decision](2026-07-10-single-file-executable-sdk-runtime-distribution.md): `pkg --sea` preserves a package tree and dynamic module resolution instead of maintaining a separate static plugin registry. That record owns the comparison with bare Node SEA. The embedded runtime serves these private entry points; project commands still depend on the remote machine's development tools.
The four release targets cover Linux and macOS on x64 and arm64, matching the POSIX providers and their supported native resources. Packaging does not add Windows or musl support. Landlock and macOS PTY launchers remain real files beside the executable because the operating system cannot execute paths inside pkg's virtual filesystem. The installation unit is therefore the complete archive directory, with native resources and integrity metadata, rather than the executable alone. The [helper runtime reference](../../../../packages/ssh/ssh-helper-runtime/README.md) owns the build matrix, deployment baselines and archive verification commands.
## Alternatives considered
**Frame every stream over SSH exec stdout/stdin.** This avoids Unix-socket forwarding, but requires per-stream credits, queue bounds and independent EOF semantics inside the application protocol. Independent SSH channels use the transport’s existing flow control and keep program output away from administrative framing.
@@ -48,7 +56,7 @@ The portable-consumer decision remains active; this note supplies its SSH realiz
## Verification
Required protocol and lifecycle evidence covers malformed frames, bounds, reservation cancellation, authenticated stream publication and disconnect errors. Native SSH acceptance must exercise remote file guards and symlink identity, Bash confinement, fd 7 binary traffic, control progress under paused output, terminal operations, LSP and Node execution. Live checks require an explicitly configured disposable remote workspace; keyless tests do not provision one.
Required protocol and lifecycle evidence covers malformed frames, bounds, reservation cancellation, authenticated stream publication and disconnect errors. Native SSH acceptance must exercise remote file guards and symlink identity, Bash confinement, fd 7 binary traffic, control progress under paused output, terminal operations, LSP and Node execution. External-host checks require an explicitly configured disposable remote workspace. Packaged Linux acceptance provisions a disposable OpenSSH container without Node; native-host checks own confinement evidence.
Security evidence requires both a same-user connector and a replaced socket listener that cannot claim a reserved stream, learn its key, alter another run or receive its plaintext output. Process-lifecycle evidence distinguishes direct exit from managed-range quiescence and tests cancellation both before launch publication and after the payload starts. Source and built profile checks verify the installed helper/bootstrap arrangement without transferring host credentials to program environments.
@@ -22,12 +22,20 @@ SSH 提供经过认证的字节通道及逐通道流量控制,但普通 exec
收集的 stdout 与 stderr 通过仅更新输出观测的处理器传递有界尾部快照。快照等待传输时,输出捕获继续进行。最终快照保留原始字节偏移,已完成的 spill 文件在连接释放后使用本地提供方的保留输出存储。
就绪流程验证已安装辅助程序的摘要,并在配置 PTC 时验证已安装 Node 引导程序的摘要。这些检查固定预期部署产物,不用于认证恶意远端操作系统。辅助程序禁用通过 `SIGUSR1` 启动 Node 调试器:文件效果限制仍可能允许同用户信号,这些信号不得暴露辅助程序不受限的文件系统和进程服务。文件效果限制委托给远端本地沙箱提供方,保留其完整/部分执行披露及平台限制。
就绪流程验证已安装辅助脚本或可执行文件的摘要;脚本部署提供 PTC 引导程序时,还验证该引导程序的摘要。可执行文件部署内嵌 PTC 工作进程,无需单独的引导程序。这些检查固定预期部署产物,不用于认证恶意远端操作系统。辅助程序禁用通过 `SIGUSR1` 启动 Node 调试器:文件效果限制仍可能允许同用户信号,这些信号不得暴露辅助程序不受限的文件系统和进程服务。文件效果限制委托给远端本地沙箱提供方,保留其完整/部分执行披露及平台限制。
路径规范化属于文件实际存在的位置。共享策略解析器保留执行环境中的绝对路径写法;执行限制的提供方在自己的文件系统上解析符号链接与 `..`。headless 通过 `ctx.fs` 记录和验证 cwd。SSH 不提供主机路径投影,因此 Node 执行需要显式安装的远端引导程序。
路径规范化属于文件实际存在的位置。共享策略解析器保留执行环境中的绝对路径写法;执行限制的提供方在自己的文件系统上解析符号链接与 `..`。headless 通过 `ctx.fs` 记录和验证 cwd。SSH 不提供主机路径投影;PTC 在脚本部署中使用显式安装的远端引导程序,在可执行文件部署中使用辅助程序内嵌的工作进程。
连接丢失会使待处理操作失效,不进行重连或重放。辅助进程 EOF、信号及心跳租期会启动远端原生清理。客户端不能把租期到期当作观察到的成功终止:被中断的修改或启动可能结果未知。管理请求截止时限、消费方执行截止时限及清理分别由各自归属方负责。
### 辅助程序部署
显式的 `launch.kind` 选择远端的 `node-script` 或 `executable` 调用方式,与本地 Harness 的运行载体相互独立。本地 `process.pkg` 标志无法描述远端安装。协议 2 在就绪响应中包含远端运行时类型,并拒绝协议版本或启动类型不匹配,避免客户端把脚本引导规则用于内嵌工作进程。客户端与辅助程序的协议升级必须一起部署。
可执行文件内嵌 Node、辅助程序、托管子进程运行器与 PTC 工作进程。它复用 [SDK 可执行文件打包决策](2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md):`pkg --sea` 保留包目录树与动态模块解析,无需维护独立的静态插件注册表。该记录负责与裸 Node SEA 的方案比较。内嵌运行时服务于这些私有入口;项目命令仍依赖远端机器的开发工具。
四个发布目标覆盖 Linux 和 macOS 的 x64 与 arm64,对应 POSIX 提供方及其支持的原生资源。打包不会增加 Windows 或 musl 支持。Landlock 与 macOS PTY 启动器保留为可执行文件旁的真实文件,因为操作系统无法执行 pkg 虚拟文件系统中的路径。因此,安装单元是包含原生资源与完整性元数据的完整归档目录,而非单个可执行文件。[辅助程序运行时参考](../../../../packages/ssh/ssh-helper-runtime/README.zh.md)负责构建矩阵、部署基线及归档验证命令。
## 考虑过的替代方案
**在 SSH exec stdout/stdin 上为所有流分帧。** 这避免了 Unix 套接字转发,但要求应用协议自行实现逐流额度、队列上限及独立 EOF 语义。独立 SSH 通道使用传输层既有流控,并将程序输出与管理帧分开。
@@ -48,7 +56,7 @@ SSH 提供经过认证的字节通道及逐通道流量控制,但普通 exec
## 验证
必要的协议及生命周期证据覆盖畸形消息、上限、预留取消、经过认证的流发布及断连错误。原生 SSH 验收必须覆盖远端文件保护与符号链接身份、Bash 限制、fd 7 二进制流、输出暂停时的控制进展、终端操作、LSP 及 Node 执行。实时检查要求显式配置的可丢弃远端工作区;无密钥测试不创建远端环境。
必要的协议及生命周期证据覆盖畸形消息、上限、预留取消、经过认证的流发布及断连错误。原生 SSH 验收必须覆盖远端文件保护与符号链接身份、Bash 限制、fd 7 二进制流、输出暂停时的控制进展、终端操作、LSP 及 Node 执行。外部主机检查要求显式配置的可丢弃远端工作区。打包产物的 Linux 验收会创建未安装 Node 的可丢弃 OpenSSH 容器;原生主机检查负责限制执行的证据。
安全证据要求同时尝试同用户连接与替换套接字监听端点,并确认其无法占用预留流、获知流密钥、修改另一执行或收到其明文输出。进程生命周期证据区分直接退出与托管进程范围静止,并测试启动发布前及程序启动后的取消。源码与构建后配置组合检查验证已安装辅助程序/引导安排,不向程序环境传递主机凭据。
@@ -9,8 +9,8 @@
en: 66ac3d7ce7eb8e15
zh: abb9d41b71900bbf
/agent-note-sandboxed-node-execution-for-ptc/decision:
en: cf072f53de09faca
zh: 9edd5c4a2bfaae5a
en: d6b9c22603a4f933
zh: f324b91830078d9a
/agent-note-sandboxed-node-execution-for-ptc/decision/resolved-inputs-and-policy:
en: 3aef37bb4f8a8e53
zh: a7b477324d486ab3
@@ -18,8 +18,8 @@
en: 2c7a694b04951772
zh: 133b95b0477b127f
/agent-note-sandboxed-node-execution-for-ptc/decision/resource-limits:
en: 84c73cf3ff33bc03
zh: a228080683d6252c
en: 3f1d8a30f6d48243
zh: faa7c01d7c959893
/agent-note-sandboxed-node-execution-for-ptc/alternatives-considered:
en: 8ffcc2b32a2243d7
zh: 81d48d38947abc76
@@ -12,7 +12,7 @@ The [PTC foundation](../feature/2026-06-15-ptc.md) remains responsible for regis
## Decision
`dsh-ptc-runtime-node` runs each program in one fresh Node process. The host resolves execution choices, confines the launch through the same `ctx.sandbox` provider as Bash, and gives process lifetime to `ctx.subprocess`. The child evaluates erasable TypeScript with direct Node APIs, an empty model environment and host-provided asynchronous bindings. No worker or persistent kernel remains inside this provider.
`dsh-ptc-runtime-node` in its default `per-call` mode runs each program in one fresh Node process. The host resolves execution choices, confines the launch through the same `ctx.sandbox` provider as Bash, and gives process lifetime to `ctx.subprocess`. The child evaluates erasable TypeScript with direct Node APIs, an empty model environment and host-provided asynchronous bindings. No worker or persistent kernel remains after a per-call execution.
The host preserves `ELECTRON_RUN_AS_NODE` for child startup; the bootstrap removes it from the native environment before evaluation, and model-visible `process.env` stays empty. Nested Electron launches require their own explicit Node-mode selection. Desktop uses Electron as its Node executable; removing this selector launches Electron's application path instead of the PTC bootstrap. Sandbox permission changes cannot repair that launch mismatch. The macOS Desktop regression uses real Electron to verify binding writes, direct workspace writes, and rejection of writes outside the workspace under restricted policy. It requires an installed Electron binary; ordinary runtime tests cover environment filtering without that dependency.
@@ -34,6 +34,8 @@ Program completion, timeout, cancellation and protocol failure all close executi
The default elapsed deadline is 120 seconds, capped at 600 seconds by default. Trusted service consumers may request `timeoutMs: null` to disable this timer; [workflow sandbox reuse](2026-09-13-workflow-ptc-sandbox-reuse.md) owns that caller-controlled lifetime. Omitted and numeric requests, including model-facing `run_code`, retain the numeric defaults and caps. An enabled deadline includes runtime setup and nested tool or approval waits. V8 old-generation memory, serialized outer output and control traffic have separate configured bounds. The heap limit excludes native allocations and descendant memory, and elapsed time is not a process-tree CPU budget.
The opt-in [in-memory execution mode](2026-09-23-best-effort-persistent-ptc.md) retains disposable memory under the same file policy; [retained-process permissions](2026-10-08-persistent-ptc-sandbox.md) govern policy changes and temporary escalation.
## Alternatives considered
**Keep the worker and add tool checks.** Tool checks cannot intercept direct Node imports or establish OS confinement. Keeping a worker inside a confined supervisor process preserves worker metering but adds another execution lifetime without supplying a process-tree CPU limit.
@@ -12,7 +12,7 @@ Node worker 隔离 JavaScript 状态,但不应用调用 Session 的 OS 沙箱
## 决策
`dsh-ptc-runtime-node` 在一个全新 Node 进程中运行每个程序。Host 解析执行选择,通过与 Bash 相同的 `ctx.sandbox` 提供方约束启动,并将进程生命周期交给 `ctx.subprocess`。子进程以直接 Node API、空模型环境和 Host 提供的异步绑定求值可擦除 TypeScript。本提供方不保留 worker 或持久内核。
`dsh-ptc-runtime-node` 的默认 `per-call` 模式在一个全新 Node 进程中运行每个程序。Host 解析执行选择,通过与 Bash 相同的 `ctx.sandbox` 提供方约束启动,并将进程生命周期交给 `ctx.subprocess`。子进程以直接 Node API、空模型环境和 Host 提供的异步绑定求值可擦除 TypeScript。逐次执行结束后不保留 worker 或持久内核。
Host 在子进程启动时保留 `ELECTRON_RUN_AS_NODE`;bootstrap 在求值前将其从原生环境中删除,模型可见的 `process.env` 仍为空。嵌套启动 Electron 需要自行显式选择 Node 模式。桌面端使用 Electron 作为 Node 可执行文件;删除此选择变量会启动 Electron 应用路径,而不是 PTC bootstrap。更改沙箱权限无法修复这一启动模式不匹配。macOS 桌面端回归测试使用真实 Electron 验证绑定写入、直接工作区写入以及受限策略对工作区外写入的拒绝。该测试需要已安装的 Electron 二进制文件;普通运行时测试无需此依赖即可覆盖环境过滤。
@@ -34,6 +34,8 @@ Host 在子进程启动时保留 `ELECTRON_RUN_AS_NODE`;bootstrap 在求值前
默认经过时间截止为 120 秒,默认上限为 600 秒。可信服务消费方可以请求 `timeoutMs: null` 来禁用该定时器;[工作流沙箱复用](2026-09-13-workflow-ptc-sandbox-reuse.zh.md)负责这种由调用方控制的生命周期。省略 timeout 或使用数值的请求(包括面向模型的 `run_code`)保留数值默认值与上限。启用的截止包括运行时准备以及嵌套工具或审批等待。V8 老生代内存、序列化外层输出与控制通信具有独立配置的上限。堆限制不包含原生分配和后代内存,经过时间也不是进程树 CPU 预算。
显式启用的[内存保留执行模式](2026-09-23-best-effort-persistent-ptc.zh.md)在相同文件策略下保留可丢弃内存;[常驻进程权限](2026-10-08-persistent-ptc-sandbox.zh.md)负责策略切换和临时提权。
## 考虑过的替代方案
**保留 worker 并增加工具检查。** 工具检查不能拦截直接 Node 导入,也不能建立 OS 约束。在受限监督进程中保留 worker 可以保留 worker 计量,但会增加一层执行生命周期,仍不能提供进程树 CPU 限制。
@@ -15,8 +15,8 @@
en: 3c2dd2c948ffb51b
zh: d019197df0ee3dcf
/agent-note-deepseek-account-browser-login/consequences:
en: a6eff4a0350e9018
zh: 2e3200ca089023ab
en: 3c5681b492cd34cf
zh: d7707fed9241f9bc
/agent-note-deepseek-account-browser-login/verification:
en: 881edcb454b941a6
zh: 7dc5a9b7a066f623
@@ -46,7 +46,7 @@ Profile and recharge-wallet queries use independent getProfile and getBalance op
Deployment authentication uses explicit Host-only requestHeaders on the configured Platform origin. The provider rejects redirects and reserved-header overrides so deployment cookies cannot replace account authorization or follow a browser destination. Environment-specific authentication protocols remain outside the account provider.
The bundled account UI is Desktop-only: its preload bridge enables the account launcher, settings and sign-in onboarding. Plain Web retains API-key onboarding and the standard Settings launcher without account subscription or login. The Host protocol accepts login_source (desktop or web) in auth_init; the bundled UI sends desktop. The backend accepts localhost callbacks. DSH preserves the browser-supplied localhost hostname and port without DNS resolution or conversion to an IP literal.
The bundled account UI mounts the account launcher, settings and sign-in onboarding in both Desktop and plain Web. The Host sends login_source (desktop or web) in auth_init so Platform can choose the completion experience; the UI sends desktop when the Desktop preload bridge is present and web otherwise. Web failures close the authorization tab while the original tab receives Host state; no Web UI return URL is used. The backend accepts localhost callbacks. DSH preserves the browser-supplied localhost hostname and port without DNS resolution or conversion to an IP literal.
The macOS development launcher registers an isolated, ad-hoc-signed application bundle for `dsh://open`. The bundle retains the workspace entry and development paths for Launch Services cold starts without copying credentials or changing the package-manager-owned Electron application. Protocol registration targets the latest launched development or packaged application.
@@ -46,7 +46,7 @@ DSH 授权通过 x-dsh-auth-token 请求头鉴权 Platform、推理和 Files 请
开发环境认证通过配置的平台来源上的显式 Host 请求头 requestHeaders 完成。提供者拒绝重定向和保留请求头覆盖,防止开发环境 Cookie 替换账号授权或跟随浏览器跳转地址。特定环境的认证协议不属于账号提供者。
内置账号界面仅用于 Desktop:preload 桥接启用账号入口、设置和登录引导。普通 Web 保留 API Key 引导及标准设置入口,不订阅账号状态或显示登录。Host 协议在 auth_init 中接受 login_source(desktop 或 web);内置界面发送 desktop。后端接受 localhost 回调。DSH 保留浏览器提供的 localhost 主机名和端口,不做 DNS 解析或 IP 字面量转换。
内置账号界面在 Desktop 和普通 Web 中都挂载账号入口、设置和登录引导。Host 在 auth_init 中发送 login_source(desktop 或 web),以便 Platform 选择完成页体验;存在 Desktop preload 桥接时界面发送 desktop,否则发送 web。Web 登录失败时关闭授权标签页,原标签页通过 Host 状态获知结果;不使用 Web UI 返回 URL。后端接受 localhost 回调。DSH 保留浏览器提供的 localhost 主机名和端口,不做 DNS 解析或 IP 字面量转换。
macOS 开发启动器为 `dsh://open` 注册独立、经临时签名的应用包。该应用包保留工作区入口和开发路径,使 Launch Services 能够冷启动;凭证不会被复制,也不修改包管理器安装的 Electron 应用。协议注册指向最近启动的开发版或打包版应用。
@@ -0,0 +1,19 @@
# Bilingual-pair consistency record for 2026-09-17-experimental-cua-object-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/architecture/2026-09-17-experimental-cua-object-api.md
/agent-note-native-cua-objects-independent-of-evaluator-ownership:
en: 1ea218552c24946e
zh: 83524cf60abec6c8
/agent-note-native-cua-objects-independent-of-evaluator-ownership/problem:
en: a22ddcfb339e503d
zh: a1f4fadb46fce1e7
/agent-note-native-cua-objects-independent-of-evaluator-ownership/decision:
en: 2780d1b0f5ce9ea8
zh: ff986f8c1c90300f
/agent-note-native-cua-objects-independent-of-evaluator-ownership/alternatives-considered:
en: 3d0bc77e3c432cb9
zh: 50a20eda1811f16c
/agent-note-native-cua-objects-independent-of-evaluator-ownership/consequences:
en: 528c644d5810e60b
zh: 9649dfc3b1172a6d
@@ -0,0 +1,43 @@
# Agent Note: Native CUA objects independent of evaluator ownership
Status: implemented
English | [中文](2026-09-17-experimental-cua-object-api.zh.md)
## Problem
Computer-use programs need consistent app and window operations. Coupling those operations to a CUA-specific interpreter makes input handling depend on unrelated choices about variable persistence, imports and cancellation.
## Decision
The shared `cua` library owns application/window objects, consent, and logged dispatch. `cua-macos` owns the Swift helper, its build, and its local connection lifecycle. Providers pass a prepared executable and a connection factory to the shared installer, keeping platform code out of object consumers without adding another runtime registry.
The experimental CUA package owns a native-app subset of the Codex-style object interface and selected MIT-licensed macOS behavior adapted from Cua Driver in a Swift helper. A source manifest records the adapted upstream files and local implementation. The helper is locally compiled; the package has no external Cua Driver runtime dependency. The existing [provider registration](2026-09-12-computer-use-provider-registration.md) prevents competing computer-use providers within one composition.
The facade accepts an operation dispatcher. Its tool adapter resolves the current tool binding for each operation, preserving normal authorization, logging and image admission. The execution host owns variable persistence and completion serialization. CUA defines no persistence directive, worker or run_code replacement.
Native process ownership follows the [shared helper decision](2026-10-09-cua-shared-native-helper.md), with isolated Agent connections. App handles bind process identity and kernel start time; replacing the process or helper never revives old handles. AX snapshots are scoped to an app and optional window, so independent window observations remain usable. Coordinate actions select an owned window; the Host maps [screenshot coordinates](2026-10-13-cua-hover-routes-and-coordinates.md) to native window-local points. Keyboard input remains app-scoped because macOS delivers it to the app's current focus.
The operation schema owns shared data declarations. Host-only limits and launch continuations remain separate from model parameters, and generated native field/enum tables preserve independent native admission without hand-maintained copies. Native code continues to own system calls, permissions and cancellation. AX formatting consumes immutable observations; the walker retains sole ownership of indices and AX references.
Clipboard paste is distinct from Unicode key input: Return and Tab key events can trigger actions instead of inserting clipboard content. Pasteboard access runs on the AppKit main actor and retains promised data until a reader requests it. AX command completion alone does not establish that the application has fetched the data. Restoration checks ownership before replacing contents; the caller still verifies the application result. After successful command dispatch, `pasteReadTimeoutMs` bounds waiting for a data request, with a default of 1000 ms. Expiry returns `PASTE_NOT_CONSUMED`, closes the data provider against late delivery, and restores the clipboard only while still owned. The failure reports no read within that interval; it does not prove the application will never process its queued Paste command. A delayed command may read restored clipboard contents, so callers inspect the target and never retry automatically. Foreground-dependent consumption was observed in the installed ChatGPT/Codex application (`com.openai.codex`); other applications need their own validation. Guidance presents this as an example and applies recovery instructions to any `PASTE_NOT_CONSUMED` result.
CUA registers its own Harness tool and returns operation values directly. Screenshot capture requires a resolved image-capable model and the shared attachment service. The tool admits each screenshot once and renders its durable image reference; the object API, combined observation and image sink retain the same `CuaScreenshot` containing encoded `bytes`, `mimeType`, `pixelWidth`, `pixelHeight` and `windowId` from the validated screenshot result. The bridge preserves this metadata without another upload or image emission. Callers use the reported format and dimensions when saving or declaring images.
## Alternatives considered
Depending on the complete upstream driver would simplify upgrades but does not meet the requirement for locally owned implementation. Copying browser and other platform code would add unsupported scope. The JSON operation protocol allows another native implementation without changing object consumers.
A CUA-specific persistent evaluator duplicates execution lifecycle policy and prevents independent iteration on PTC. Requiring callers to manage raw app and snapshot tokens alone loses reusable object semantics. The facade and small tool adapter provide those semantics while leaving the evaluator outside this package.
Reusing the MCP result adapter would impose MCP envelopes and wire validation on native CUA operations. Direct Harness tool output and shared attachment admission provide the needed logging and images without a second protocol.
**Fixing the screenshot format** would require otherwise unnecessary re-encoding and discard the attachment pipeline’s pass-through optimization. A format convention alone still forces callers to guess, and cannot tell them when encoding policy changes. Returning the already computed format and dimensions preserves pass-through and makes each result describe its actual bytes.
**Waiting until the full native-operation timeout** hides successful command dispatch behind a generic timeout. A separate configurable read budget reports the missing acknowledgement without treating slow applications as unsupported. Requiring every application to be foreground would claim a prerequisite that the available observations do not establish.
## Consequences
The package remains opt-in and macOS-only. Observations do not lock the desktop, and input success records dispatch rather than task completion. Callers must verify effects and must not retry uncertain input automatically. Native fixture tests verify actual window state separately from protocol and model-transcript tests. Persistent execution, binary distribution, permission onboarding and real-model task success remain separately owned work.
Source builds retain a content-addressed executable per compiled generation. The [shared helper decision](2026-10-09-cua-shared-native-helper.md) owns capture-client identity and cross-Host lifetime. Retained source generations avoid replacing a live executable, while the package payload remains separately published for distribution.
@@ -0,0 +1,43 @@
# Agent Note:独立于执行器管理的原生 CUA 对象
Status: implemented
[English](2026-09-17-experimental-cua-object-api.md) | 中文
## 问题
Computer-use 程序需要一致的应用和窗口操作。将这些操作与 CUA 专用解释器绑定,会使输入处理依赖变量持久化、导入和取消等独立设计。
## 决策
共享的 `cua` 库负责应用与窗口对象、授权和已记录的派发。`cua-macos` 负责 Swift helper、构建和本地连接生命周期。提供方将准备好的可执行文件和连接工厂交给共享安装函数,使对象调用方无需包含平台代码,也无需增加另一套运行时注册表。
实验 CUA 包提供 Codex 风格的原生应用对象接口,并在 Swift 辅助进程中改写 Cua Driver 中选取的 MIT 许可 macOS 行为。源码清单记录改写的上游文件和本地实现。辅助进程在本地编译,本包不依赖外部 Cua Driver 运行时。已有的 [provider 注册机制](2026-09-12-computer-use-provider-registration.zh.md)防止同一组合中出现竞争 provider。
对象接口接收操作分派器。工具适配模块每次操作都获取当前工具绑定,保留标准授权、日志和图片接入。执行宿主负责变量持久化和最终表达式值的序列化。CUA 不定义持久化指令、执行 worker 或 run_code 替代实现。
原生进程归属遵循[共享 helper 决策](2026-10-09-cua-shared-native-helper.zh.md),Agent 连接保持隔离。应用句柄绑定进程身份及内核启动时间,替换进程或辅助进程不会使旧句柄重新有效。AX 快照按应用和可选窗口保存,不同窗口的观察可以分别使用。坐标操作选择属于该应用的窗口;Host 按[截图坐标规则](2026-10-13-cua-hover-routes-and-coordinates.zh.md)换算为原生窗口内 point。键盘输入保持应用范围,因为 macOS 将其送往应用的当前焦点。
操作 schema 负责共享数据声明。Host 专属限制和启动续接与模型参数分开,生成的原生字段、枚举表保留独立原生校验,同时避免手工维护副本。系统调用、权限和取消仍由原生代码负责。AX 格式化只消费不可变观察数据,遍历器独自负责索引与 AX 引用。
剪贴板粘贴与 Unicode 按键输入分别实现,因为 Return 和 Tab 按键可能触发动作,而不是插入剪贴板内容。剪贴板访问使用 AppKit 主线程,保留延迟提供的数据直到读取方请求。AX 命令完成本身不能证明应用已取得数据。恢复前检查剪贴板归属;调用方仍需验证应用中的结果。 命令成功分派后,`pasteReadTimeoutMs` 限制等待数据请求的时间,默认 1000 毫秒。到期返回 `PASTE_NOT_CONSUMED`,关闭数据提供方以阻止迟到的数据交付,并仅在仍持有剪贴板时恢复原内容。该失败只说明这段时间内没有读取,不证明应用以后不会处理已排队的 Paste 命令。迟到的命令可能读取恢复后的剪贴板内容,因此调用方必须检查目标,不能自动重试。已安装的 ChatGPT/Codex 应用(`com.openai.codex`)已观察到依赖前台才能读取的行为,其他应用需要分别验证。指引将其写为例子,并对任何 `PASTE_NOT_CONSUMED` 结果提供恢复步骤。
CUA 注册自己的 Harness 工具,直接返回操作结果。截图要求解析到支持图片输入的模型,并提供共享附件服务。工具只接入一次截图,再渲染持久图片引用;对象 API、组合观察和图片输出回调保留同一个 `CuaScreenshot`,其中的编码 `bytes`、`mimeType`、`pixelWidth`、`pixelHeight` 和 `windowId` 来自已校验的截图结果。桥接保留这些元数据,不再次上传或输出图片。调用方落盘或声明图片时使用返回的格式和尺寸。
## 考虑过的替代方案
依赖完整上游驱动可以简化升级,但不符合本地维护实现的要求。复制浏览器及其他平台代码会引入尚不支持的范围。JSON 操作协议允许替换原生实现,而不改变对象调用方。
CUA 专用持久执行器会重复实现执行生命周期规则,使 PTC 无法独立演进。仅让调用方管理原始应用和快照 token,又会失去可复用的对象语义。对象接口和轻量工具适配模块提供这些语义,并将执行器留在本包之外。
复用 MCP 结果适配器会给原生 CUA 操作引入 MCP 信封和协议校验。直接使用 Harness 工具输出与共享附件接入,即可取得所需的日志和图片能力,无需再经过一种协议。
**固定截图格式**会要求原本不需要的重新编码,并失去附件处理的原样直通优化。仅约定格式仍要求调用方猜测,也无法告知编码策略的变化。返回已经算出的格式和尺寸,既保留直通优化,也让每个结果说明其实际字节。
**等到原生操作的总超时**会用一条通用超时掩盖命令已经成功分派的事实。独立、可配置的读取等待上限能报告缺失的读取确认,而不将响应慢的应用认定为不支持粘贴。要求所有应用都必须位于前台,则超出了现有观察能支持的结论。
## 后果
本包需要显式启用,且只提供 macOS 实现。观察不会锁定桌面,输入成功只记录分派完成,不能证明任务完成。调用方必须验证效果,不能自动重试结果不确定的输入。原生测试通过实际窗口状态验证效果,与协议和模型消息测试分别进行。持久执行、二进制分发、权限引导及真实模型任务成功率是分别负责的工作。
源码构建按编译产物的内容摘要保留可执行文件。[共享 helper 决策](2026-10-09-cua-shared-native-helper.zh.md)负责捕获客户端身份与跨 Host 生命期。源码各代副本避免替换运行中的可执行文件,包内产物仍单独发布。
@@ -9,14 +9,14 @@
en: 837ebb0071a5611b
zh: 23d0f0713889ead6
/agent-note-writer-acquisition-for-blank-session-reuse/decision:
en: 7f2676feb224e7ea
zh: babfda5b31dfd0fe
en: a54db0d4a97aa9b8
zh: a52d257b657fe99b
/agent-note-writer-acquisition-for-blank-session-reuse/alternatives-considered:
en: 2a67a53f7f396ba8
zh: 6df6ccc071fb0de2
/agent-note-writer-acquisition-for-blank-session-reuse/consequences:
en: 2e1f1894337689a2
zh: d0cab07cc48083ee
en: 8307612b7476e8f3
zh: 87fa39e58d33d4f1
/agent-note-writer-acquisition-for-blank-session-reuse/verification:
en: 4680e845e2ab294e
zh: ea5890c5ffb09831
en: 4367d55ae9dce5c1
zh: c610626f4e15eb77
@@ -10,7 +10,7 @@ Two Hosts sharing Session storage can select the same blank Session when opening
## Decision
The Session catalog includes persisted blanks. Startup restoration selects the saved blank; ordinary Workspace navigation selects the first eligible unarchived member in catalog order. Explicit-id `session.create` resumes that Session and acquires its writer before opening history. Only `session/writer-held` triggers creation of a new Session. Other errors propagate. Concurrent Workspace connects in one Client share the acquisition and fallback. Later navigation cancels a pending startup selection.
The Session catalog includes persisted blanks. Startup restoration selects the saved blank; ordinary Workspace navigation selects the first eligible unarchived member in catalog order. Explicit-id `session.create` names that Session; the Host resumes it and acquires its writer before opening history, and creates a new Session when another owner holds the writer ([reuse and preset rules](../bug-fix/2026-09-29-reused-blank-runs-requested-preset.md)). Other errors propagate. A Workspace connect in one Client joins the latest in-flight connect to that Workspace only when both request the same preset. Later navigation cancels a pending startup selection.
The Agent retains the acquired handle throughout its lifetime. Checking ownership and releasing a probe lock would leave a race before resume. No PID or process identity enters persisted data or the Remote schema.
@@ -28,8 +28,8 @@ This decision extends the blank-reuse policy in the [Client Session scope refere
## Consequences
A Host can reuse its own live blank or reclaim an unowned persisted blank with its slash state. Separate Hosts create distinct blanks while existing writers remain held. A held selected blank causes fresh creation even when another blank is free; navigation does not search for another candidate. No cleanup is added. Opening the same established conversation from two Hosts can still encounter writer contention. Unknown projection hints remain visible without cold body scans; automatic reuse requires known blank metadata. A non-contention acquisition failure aborts New Session and is currently reported only to the console; hidden blanks have no recovery action in this flow.
A Host can reuse its own live blank or reclaim an unowned persisted blank with its slash state. Separate Hosts create distinct blanks while existing writers remain held. A held selected blank causes fresh creation even when another blank is free; navigation does not search for another candidate. No cleanup is added. Opening the same established conversation from two Hosts can still encounter writer contention. Unknown projection hints remain visible without cold body scans; automatic reuse requires known blank metadata. A non-contention acquisition failure aborts the navigation; a hero Workspace pick or explicit draft preparation reports it as a notice and startup restoration only to the console, and a hidden blank that fails this way has no recovery action in this flow.
## Verification
Navigation tests cover saved selection, single-candidate contention fallback, non-contention failures, overlapping acquisition, and superseding navigation. A shipped Web-composition test holds a real persistence writer, verifies fresh creation despite another free blank, and reclaims the released selected blank with its plan state. The fresh-round-trip recorded Session scenario reloads its blank before submitting the recorded prompt.
Navigation tests cover saved selection, opening the Session the Host returns for a held candidate, non-contention failures, overlapping acquisition, and superseding navigation. A shipped Web-composition test holds a real persistence writer, verifies fresh creation despite another free blank, and reclaims the released selected blank with its plan state. The fresh-round-trip recorded Session scenario reloads its blank before submitting the recorded prompt.
@@ -10,7 +10,7 @@ Status: implemented
## Decision
Session 列表包含持久化空白会话。启动恢复选择已保存的空白会话;普通 Workspace 导航按列表顺序选择第一个符合条件且未归档的成员。显式 ID 的 `session.create` 会恢复该 Session,并在打开历史前取得写锁。只有 `session/writer-held` 会触发新建 Session。其他错误返回调用方。同一 Client 的并发 Workspace 连接共享获取与回退过程。后续导航会取消尚未完成的启动选择。
Session 列表包含持久化空白会话。启动恢复选择已保存的空白会话;普通 Workspace 导航按列表顺序选择第一个符合条件且未归档的成员。显式 ID 的 `session.create` 指明该 Session;Host 恢复它并在打开历史前取得写锁,写锁被其他写者持有时改为新建 Session([复用与 preset 规则](../bug-fix/2026-09-29-reused-blank-runs-requested-preset.zh.md))。其他错误返回调用方。同一 Client 中的 Workspace 连接仅在与该 Workspace 最近一次进行中的连接请求相同 preset 时与之合并。后续导航会取消尚未完成的启动选择。
Agent 在整个生命周期内持有取得的句柄。检查占用后释放探测锁,会在恢复前留下竞态。持久化数据与 Remote schema 均不引入 PID 或进程身份。
@@ -28,8 +28,8 @@ Agent 在整个生命周期内持有取得的句柄。检查占用后释放探
## Consequences
Host 可以复用自己的活动空白会话,也可以接管无人占用的持久化空白会话并保留其 slash 状态。现有写锁被持有时,不同 Host 创建不同的空白会话。所选空白会话被占用时,即使另有空闲空白会话也会新建;导航不再寻找其他候选。不增加清理逻辑。两个 Host 打开同一个已有对话时仍可能遇到写锁争用。未知投影提示仍可见且不扫描冷日志正文;自动复用要求已知的空白元数据。非锁争用的获取错误会中止新建会话,目前只报告到控制台;此流程没有针对隐藏空白会话的恢复操作。
Host 可以复用自己的活动空白会话,也可以接管无人占用的持久化空白会话并保留其 slash 状态。现有写锁被持有时,不同 Host 创建不同的空白会话。所选空白会话被占用时,即使另有空闲空白会话也会新建;导航不再寻找其他候选。不增加清理逻辑。两个 Host 打开同一个已有对话时仍可能遇到写锁争用。未知投影提示仍可见且不扫描冷日志正文;自动复用要求已知的空白元数据。非锁争用的获取错误会中止该次导航;在 hero 中选择工作区或显式准备草稿时以提示展示该错误,启动恢复只报告到控制台,以此方式失败的隐藏空白会话在此流程中没有恢复操作。
## Verification
导航测试覆盖已保存选择、单候选锁争用回退、其他错误、重叠获取以及后续导航替代。实际 Web 组合测试持有真实持久化写锁,验证另有空闲空白会话时仍新建,并接管已释放的所选空白会话及其 plan 状态。fresh-round-trip 的录制 Session 场景在提交已录制的提示词前刷新空白会话。
导航测试覆盖已保存选择、为被占用候选打开 Host 返回的 Session、其他错误、重叠获取以及后续导航替代。实际 Web 组合测试持有真实持久化写锁,验证另有空闲空白会话时仍新建,并接管已释放的所选空白会话及其 plan 状态。fresh-round-trip 的录制 Session 场景在提交已录制的提示词前刷新空白会话。
@@ -9,8 +9,8 @@
en: 725f773bb350218e
zh: dd8fbc09ef48208d
/agent-note-declarative-agent-presets-and-retained-revisions/decision:
en: 53fd72bf4bbe4eb3
zh: 406eee6e4f818e7c
en: 88edb925b556300c
zh: c6f05b641513d768
/agent-note-declarative-agent-presets-and-retained-revisions/alternatives-considered:
en: 09f40eab4659e9f6
zh: 7a70da0d0ae6845b
@@ -16,7 +16,7 @@ The registry owns each revision's scope and Loader tree. Agents link their own s
The Web editor and `agent_preset` tool accept only child plugin YAML. Saving writes the active profile's user patch, preserving display metadata and unrelated configuration. Revision checks and profile locking prevent overwriting concurrent saves; validation rejects a higher-priority override that would defeat the edit. Persistence and activation outcomes are reported separately. Tool writes require approval or full access because configuration executes in the Host.
Session data continues to record the preset identity. Restart resolves that identity against current configuration and rejects a missing definition. Old executable revisions are process-local, not serialized. New sessions without an explicit preset resolve `selectedDefault`; Client choice filtering and default correction follow [the preset UI policy](../../../../packages/client/ui-agent-preset/README.md).
Session data continues to record the preset identity. Restart resolves that identity against current configuration and rejects a missing definition; reusing a blank Session for a new conversation does not resolve it ([decision](../bug-fix/2026-09-29-reused-blank-runs-requested-preset.md)). Old executable revisions are process-local, not serialized. New sessions without an explicit preset resolve `selectedDefault`; Client choice filtering and default correction follow [the preset UI policy](../../../../packages/client/ui-agent-preset/README.md).
## Alternatives considered
@@ -16,7 +16,7 @@ Preset 目录重复承担 Cordis 配置的所有权。独立的发现、元数
Web 编辑器与 `agent_preset` 工具只接受子插件 YAML。保存写入当前 profile 的用户补丁,保留显示元数据和无关配置。版本检查和 profile 锁防止覆盖并发保存;验证拒绝会使编辑失效的更高优先级覆盖。持久化与激活结果分别报告。配置在 Host 中执行,因此工具写入需要批准或完全访问权限。
会话数据继续记录 preset 标识。重启时根据当前配置解析该标识,定义缺失则拒绝恢复。旧的可执行代际仅保留在进程内,不会序列化。未显式指定 preset 的新会话解析 `selectedDefault`;客户端选项过滤和默认值修正遵循[预设 UI 策略](../../../../packages/client/ui-agent-preset/README.zh.md)。
会话数据继续记录 preset 标识。重启时根据当前配置解析该标识,定义缺失则拒绝恢复;为新对话复用空白会话时不解析该标识([决策](../bug-fix/2026-09-29-reused-blank-runs-requested-preset.zh.md))。旧的可执行代际仅保留在进程内,不会序列化。未显式指定 preset 的新会话解析 `selectedDefault`;客户端选项过滤和默认值修正遵循[预设 UI 策略](../../../../packages/client/ui-agent-preset/README.zh.md)。
## Alternatives considered
@@ -15,5 +15,5 @@
en: 4089464ad1fb60bd
zh: 3168e9e4a205145d
/agent-note-keep-live-configuration-in-the-owning-profile/consequences:
en: 6033aaa12b2a521b
zh: 43934413c97670de
en: 41bb170694c785d6
zh: 17419fdd6d937dce
@@ -24,6 +24,6 @@ The rejected alternative is retaining settings as a second store behind a config
## Consequences
This supersedes only the non-secret settings tier in [configuration source ownership](2026-08-04-configuration-source-ownership.md); its environment restrictions and credential ordering remain applicable. [Volatile references](2026-09-18-volatile-config-references.md) own reference lifetime and ordinary update behavior.
This supersedes only the non-secret settings tier in [configuration source ownership](2026-08-04-configuration-source-ownership.md); its environment restrictions remain applicable, and [stored credential precedence](2026-10-09-stored-credential-precedence.md) supersedes its credential ordering. [Volatile references](2026-09-18-volatile-config-references.md) own reference lifetime and ordinary update behavior.
Real profile tests cover multiple instances, persisted restart, invalid edits, stale revisions, higher-layer rejection, secret redaction, and configuration expressions. Browser tests exercise form writes and reset through the assembled application. Runtime reference identity is checked across live edits.
@@ -24,6 +24,6 @@ Cordis config patch 替换完整 entry config。编辑会保留普通字段、
## 后果
本决定只替代 [配置来源所有权](2026-08-04-configuration-source-ownership.zh.md) 中的非秘密 settings 层;其中的环境限制和凭据优先级仍适用。[Volatile 引用](2026-09-18-volatile-config-references.zh.md) 负责引用生命周期和普通更新行为。
本决定只替代 [配置来源所有权](2026-08-04-configuration-source-ownership.zh.md) 中的非秘密 settings 层;其中的环境限制仍适用,其凭据优先级已由[存储凭据优先级](2026-10-09-stored-credential-precedence.zh.md)替代。[Volatile 引用](2026-09-18-volatile-config-references.zh.md) 负责引用生命周期和普通更新行为。
真实 profile 测试覆盖多实例、持久化重启、非法编辑、过期修订、高优先级层拒绝、秘密脱敏及配置表达式。浏览器测试通过完整应用执行表单写入和重置,并检查实时编辑前后的运行时引用身份。
@@ -9,14 +9,14 @@
en: 70a969dce3283824
zh: 18c61bf840a6518a
/agent-note-remote-duplex-stream-one-kind-of-stream-an-uplink-channel-and-an-invocation-context/problem/current-state:
en: 8fb06638898de59e
zh: 79ed0cfe79e40be1
/agent-note-remote-duplex-stream-one-kind-of-stream-an-uplink-channel-and-an-invocation-context/problem/three-unrelated-uplink-mechanisms:
en: 386be7b691ab63e7
zh: 935e0db22ae1cf5d
/agent-note-remote-duplex-stream-one-kind-of-stream-an-uplink-channel-and-an-invocation-context/problem/concrete-costs:
en: f374976b43645d15
zh: 26b2c7aaafa41fba
en: 2de84c3fc0a3552e
zh: da9cd565588bf2dd
/agent-note-remote-duplex-stream-one-kind-of-stream-an-uplink-channel-and-an-invocation-context/problem/uplink-consumers:
en: 403103ef08958529
zh: 2813d9d38c2c55b7
/agent-note-remote-duplex-stream-one-kind-of-stream-an-uplink-channel-and-an-invocation-context/problem/consumer-responsibilities:
en: 28f8a3112d6ec5fe
zh: c70d68233e3f6b7f
/agent-note-remote-duplex-stream-one-kind-of-stream-an-uplink-channel-and-an-invocation-context/problem/relation-to-background-jobs:
en: 5e67475010d52ddb
zh: ce23aad2d4aa0d67
@@ -30,8 +30,8 @@
en: 943208904785f7eb
zh: bda1969132a16101
/agent-note-remote-duplex-stream-one-kind-of-stream-an-uplink-channel-and-an-invocation-context/decision/terms:
en: 401703adbd9cc1c2
zh: 56d960c0b6fdff57
en: a22cd84756773075
zh: 49efa0c5a37d4b57
/agent-note-remote-duplex-stream-one-kind-of-stream-an-uplink-channel-and-an-invocation-context/decision/type-signatures:
en: 5f9cd00890b6f4c3
zh: 600ecba7e60b63f6
@@ -42,8 +42,8 @@
en: a7180d3c194cf9d9
zh: 0dd7833e0e5d0a71
/agent-note-remote-duplex-stream-one-kind-of-stream-an-uplink-channel-and-an-invocation-context/decision/peerscope:
en: 202aa6e8e8b04bd6
zh: b1fc897c09074a7c
en: 182634406ed2b25b
zh: 0bd5900a39802224
/agent-note-remote-duplex-stream-one-kind-of-stream-an-uplink-channel-and-an-invocation-context/decision/client-face:
en: 9f10347a73402b8b
zh: b939c7a254a8e579
@@ -75,8 +75,8 @@
en: 46a0c0b3ce1e5671
zh: 7743707de42b1d1d
/agent-note-remote-duplex-stream-one-kind-of-stream-an-uplink-channel-and-an-invocation-context/decision/in-process-and-worker-carriers:
en: ec339e57dc2c0603
zh: 3570be8c094ea24c
en: df4ae60d8403cdbb
zh: 70c6024a2a9a1e84
/agent-note-remote-duplex-stream-one-kind-of-stream-an-uplink-channel-and-an-invocation-context/decision/relation-to-events:
en: a011793e5a4400ed
zh: df8b20115f0348b8
@@ -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: 41128746334320c7
zh: 558a676340aae3a2
en: ecfaff2b9b2ef021
zh: 85b392d79364e0c3
/agent-note-remote-duplex-stream-one-kind-of-stream-an-uplink-channel-and-an-invocation-context/key-sequences:
en: 130e4f9ec7296a10
zh: 2a5aa1302a8db61a
@@ -126,17 +126,17 @@
en: b42ab0932f8b31b4
zh: 7fed8cb5b6e36788
/agent-note-remote-duplex-stream-one-kind-of-stream-an-uplink-channel-and-an-invocation-context/alternatives-considered:
en: 04e72789417d6e3a
zh: b57c5cc68dff45a5
en: b0046c9f7c12a876
zh: f3c37b466746b0fd
/agent-note-remote-duplex-stream-one-kind-of-stream-an-uplink-channel-and-an-invocation-context/consequences:
en: 07524e0c2dc22ad5
zh: 5bf4f27160d78b43
en: dbc54ce699ef8e71
zh: 83144f07ae8f8759
/agent-note-remote-duplex-stream-one-kind-of-stream-an-uplink-channel-and-an-invocation-context/testing:
en: 5c8188930b9d4fdb
zh: 1d94e6563cedde13
en: 06fbeed7b82ded32
zh: 66accb4bd2bf64a2
/agent-note-remote-duplex-stream-one-kind-of-stream-an-uplink-channel-and-an-invocation-context/deferred:
en: 3b99a327464edc3e
zh: dcffdc75369925dc
en: 8368c592f30af97f
zh: 2ff89acb7b3ed477
/agent-note-remote-duplex-stream-one-kind-of-stream-an-uplink-channel-and-an-invocation-context/related:
en: 85d77fb246564024
zh: 8b400818a91a3e3a
en: 475d212ae1f9c1c9
zh: 21fe6c6cdb900a6b
@@ -8,41 +8,41 @@ English | [中文](2026-09-19-remote-duplex-stream.zh.md)
### Current state
`dsh-api-gateway` multiplexes every Typert Remote stream over one WebSocket at the fixed path `/api/remote.mux` (`packages/api/gateway/src/stream-protocol.ts`). A Host method decorated with `@Remote({ mode: 'stream' })` that returns an `Iterable` or `AsyncIterable` becomes one Host-to-Client logical stream; the optional final `signal: AbortSignal` is the only reserved parameter, never enters the wire arguments, and is appended by the Gateway after the decoded business parameters (`packages/api/gateway/src/index.ts`). The generated method the Client receives returns a bare `AsyncIterable`.
`dsh-api-gateway` multiplexes Typert Remote streams over one WebSocket at `/api/remote.mux`. A Host method declares both directions with `@Remote({ mode: 'stream' })` and `RemoteStream<Out, In>`; the generated Client method returns `RemoteStreamHandle<Out, In>`. Existing `Iterable` and `AsyncIterable` return types declare downlink-only methods. The optional final `signal: AbortSignal` is supplied by the Gateway and does not enter the wire arguments.
There are only five wire frames:
Both directions share one `streamId`:
| Direction | Frame | Definition |
| --- | --- | --- |
| Client to Host | `{ type: 'open', streamId, endpoint, payload }` | `stream-protocol.ts` |
| Client to Host | `{ type: 'item', streamId, value? }` | `stream-protocol.ts` |
| Client to Host | `{ type: 'end', streamId }` | `stream-protocol.ts` |
| Client to Host | `{ type: 'cancel', streamId }` | `stream-protocol.ts` |
| Host to Client | `{ type: 'item', streamId, value? }` | `stream-protocol.ts` |
| Host to Client | `{ type: 'error', streamId, error: { code, message, details } }` | `stream-protocol.ts` |
| Host to Client | `{ type: 'end', streamId }` | `stream-protocol.ts` |
The Client can send only open and cancel. The parser `parseRemoteStreamClientMessage` (`stream-protocol.ts`) throws on any other frame, and a Host connection that receives one closes the whole socket with 1008 (`stream-server.ts`). In short, the Gateway today supports only "a backend iterable sent to the frontend". Every feature that needs the frontend to send data to the backend continuously takes its own detour.
The Client handle's `send()` supplies uplink items, `end()` half-closes the uplink, and `dispose()` cancels the logical stream. The Host reads decoded items through `this.ctx.invocation.uplink<In>()`; the invocation also carries the admitted Peer and cancellation signal.
A Host method also has no notion of who initiated this call: `InvokeRemoteRequest = { namespace, method, args, signal }` (`packages/api/gateway/src/types.ts`) has no caller slot, `RemoteStreamOpener` has the signature `(endpoint, payload, signal)` (`stream-server.ts`), and the WebSocket checks the cookie once, at the handshake.
The transport owns validation, cancellation, and its bounded inbox. Consumer protocols own input authority, acknowledgements, and recovery after disconnection.
### Three unrelated uplink mechanisms
### Uplink consumers
| Feature | Uplink path | Location | Cost |
| --- | --- | --- | --- |
| Web terminal keystrokes | One unary RPC `terminal.write(agent, id, attachmentId, data)` per xterm `onData`; the Client serializes them through a promise chain and caps its own byte budget | `packages/api/terminal-controller/src/client/model.ts`; Host `src/index.ts` | One HTTP round trip and one authentication per keystroke; two carriers correlated by hand through `attachmentId`; `inputFull` when the budget is exhausted |
| Web terminal input and resize | `terminal.attach` carries `TerminalInput` uplink items and `TerminalFrame` downlink items; the Client pipelines commands and releases its raw-input byte budget on `ack` | `packages/api/terminal-controller/src/client/model.ts`; Host `src/terminal.ts` | The current attachment owns input; `inputFull` when the Client budget is exhausted; input is never replayed after reconnection |
| File upload | A `Blob` or `ReadableStream` goes to the native `POST /api/session/uploadFileBinary` outside the Remote layer, with `duplex: 'half'` | `packages/client/file-upload/src/client/runtime.ts`; route `src/index.ts` | Bypasses Typert descriptors; no type projection |
| Answers to approvals and questions | The Host sends waterfall frames down the `$events` stream, the Client answers through a separate unary RPC `$events/result`, correlated by `clientId` plus `eventId` | `packages/api/gateway/src/client/remote-events.ts`; Host `index.ts` | A second correlation key; `index.ts` records the race this creates |
These three mechanisms solve the same problem: the Client wants to write into a logical stream that is already open.
These consumers need Client-to-Host data while a related operation remains active. Web terminals use the duplex stream; file upload and event answers retain their own transports.
### Concrete costs
### Consumer responsibilities
- **Aligning two carriers.** The terminal uses `attachmentId` to correlate the `follow` stream with the `write` RPC and to decide who owns input. Approvals use `clientId` to correlate the `$events` stream with the `$events/result` RPC.
- **No ordering guarantee.** A unary RPC travels over HTTP and is unordered relative to the WebSocket downlink; the terminal can preserve order only by serializing writes on the Client.
- **Separate authentication and lookup per uplink.** Every `write(agent, id, attachmentId, data)` resolves `agent`, looks up the terminal, and validates the attachment.
- **No EOF.** The Client cannot express "I am done writing"; a half-close needs yet another RPC.
- **Cancellation does not cover both halves.** Cancelling the downlink stream does not cancel queued uplink writes, and vice versa.
- **Backpressure implemented three times.** The downlink pulls the iterator at the pace of the socket write callback (`stream-server.ts`); the terminal uses a bounded follower that makes a slow consumer fail explicitly (`terminal-controller/src/stream.ts`); the uplink relies on the Client capping `maxInputBytes` itself.
- **The documentation is already stale.** `docs/api-gateway.md:160` still says Remote handles one request and one result, a sentence that lags the stream mode the README records.
- **Input authority.** A terminal retains its controlling attachment as a Host-local follower. Snapshot and state frames report each attachment's `writable` state; older attachments keep receiving output after control transfers.
- **Ordering and acknowledgement.** Terminal writes and resizes share a consecutive command sequence and the terminal's operation queue. An `ack` reports provider acceptance or a control refusal; it never authorizes replay after disconnection.
- **Independent lifetimes.** Cancelling `attach` stops its input and output delivery, drops unstarted commands, and joins an in-flight input operation. The PTY lifetime is owned separately by retention and explicit close.
- **Coordinated byte limits.** The Gateway exposes its complete-frame inbox capacity and this stream's JSON envelope cost through `invocation.uplinkWindow`. The terminal publishes that window in its snapshot and uses provider acknowledgements to release in-flight capacity. Writes and resizes wait in a bounded Client queue when the window is full. `maxInputBytes` limits raw UTF-8 input; `maxBufferedBytes` separately limits the complete encoded input retained by the Client and output retained by each follower.
- **Remaining separate carriers.** Approval answers still correlate `$events` with the `$events/result` RPC through `clientId` and `eventId`. Moving this protocol onto the duplex stream remains deferred.
### Relation to background jobs
@@ -66,7 +66,7 @@ Every Remote stream accepts a Client uplink. The method's return type `RemoteStr
Client Host
───────────────────────────────── ───────────────────────────────────────────
const s = remote.job.attach(req, signal)
│ open { streamId, endpoint, payload } ───────▶ opener(endpoint, payload, uplink, peer, control)
│ open { streamId, endpoint, payload } ───────▶ opener(endpoint, payload, uplink, peer, control, window)
│ │ prepareInvocation
│ s.send(v1) → item { streamId, v1 } ───────▶ inbox.push(v1) │ invocation = { request, service, peer, signal, uplink() }
│ s.send(v2) → item { streamId, v2 } ───────▶ inbox.push(v2) │ receiverContext.extend({ invocation }).get(service)
@@ -88,7 +88,7 @@ Every Remote stream accepts a Client uplink. The method's return type `RemoteStr
| Half-close | One direction has ended while the other remains open |
| Item | One business value in one direction; every item is validated by the codec independently |
| inbox | The bounded uplink queue the Host keeps per logical stream |
| Peer | The party the connection layer admitted, one `PeerScope`; this Host has exactly one Peer, the operator |
| Peer | The party one connection speaks for, one `PeerScope` carrying its principal; every browser connection shares `connection.browserPeer`, and in-process code creates its own through `connection.createPeer()` ([Peer principals](../../implemented/architecture/2026-10-12-peer-principal-and-remote-access.md)) |
| Invocation context | `RemoteInvocation`: the request, receiving service, Peer, cancellation signal, and uplink entry of this call |
### Type signatures
@@ -135,10 +135,11 @@ export interface RemoteInvocation {
}
/** Cordis service key of the receiving service. */
readonly service: string
/** The Peer that initiated the call. In-process carriers and unadmitted calls are the operator. */
/** The Peer that initiated the call; every carrier names it explicitly. */
readonly peer: PeerScope
/** Carrier cancellation: Client cancel, socket closure, uplink failure. */
readonly signal: AbortSignal
readonly uplinkWindow?: RemoteUplinkWindow | undefined
/**
* The uplink items of this call. Can be taken once; the second call throws. With an uplink codec
* in the descriptor each item is decoded to In; without one, items are delivered as unknown with
@@ -179,24 +180,30 @@ The mechanism relies on two existing Cordis behaviors:
/** The opaque identity of one Peer. */
export type PeerId = Branded<'PeerId'>
/** One Peer's session on this Host: opened and released by the admitting side; ctx owns connection-lifetime registrations. */
/** One Peer's session on this Host: opened and released by its creator; ctx owns connection-lifetime registrations. */
export interface PeerScope {
readonly id: PeerId
readonly ctx: Context
/** Caller identity fixed at creation. */
readonly principal: Principal
/** Aborted once the Peer is disposed. */
readonly signal: AbortSignal
/** Participation in forwarded interactions; independent of RBAC. */
readonly interactions: 'answer' | 'none'
dispose(): Promise<void>
}
```
This Host has exactly one Peer: the operator. `@deepseek-ai/dsh-client-connection` creates it with `createScope(connectionCtx, peer)` when the service applies, the same mechanism an Agent uses to create its own scope: the Peer object is the ScopeKey, `peer.ctx` carries connection-lifetime effects, and `dispose()` runs with the Connection's release and quiets the fiber. It is exposed as `connection.operator`.
`@deepseek-ai/dsh-client-connection` implements the one Peer class, `Peer(owner, { principal, interactions? })`, with `createScope(owner, peer)`, the same mechanism an Agent uses to create its own scope: the Peer object is the ScopeKey, `peer.ctx` carries connection-lifetime effects, `peer.signal` aborts when the scope ends, and `dispose()` quiets the fiber. Connection creates `connection.browserPeer` with the owner principal when the service applies; [Peer principals and method-level Remote access control](../../implemented/architecture/2026-10-12-peer-principal-and-remote-access.md) owns principals, `createPeer()`, and access checks.
| Member | Semantics |
| --- | --- |
| `connection.operator: PeerScope` | The only Peer; lives and dies with the Connection |
| `connection.admit(request): PeerAdmission` | Runs the existing `requestRejection`: returns `{ rejection: 401 \| 403 }` when refused, otherwise `{ peer: operator }`. Future admitters plug in here |
| `connection.browserPeer: PeerScope` | The Peer every authenticated browser connection shares; owner principal; lives and dies with the Connection |
| `connection.admit(request): PeerAdmission` | Checks Host/Origin and signed browser or API Cookies; returns `{ rejection: 401 \| 403 }` or the credential's live Peer; connect publishes the API Peer and its lifecycle socket releases it |
The Gateway's `handleUpgrade(req, socket, head, peer)` binds an opener with a fixed Peer to that socket and registers socket closure in `peer.ctx.effect`; when the scope is already inactive, it closes the socket directly with 1001 instead. When `InvokeRemoteRequest.peer?` or the `peer` parameter of `RemoteStreamOpener` is absent, the answer is the operator, so existing in-process callers change nothing; in a composition without a Connection, the operator stand-in the Gateway builds for itself is likewise created through `createScope`, with the same `dispose()` contract. Looking up a Peer by id, associating a carrier with a second Peer, and Peer open and close events all wait until the first consumer that needs multiple Peers appears.
The Gateway's `handleUpgrade(req, socket, head, peer)` binds an opener with a fixed Peer to that socket and registers socket closure in `peer.ctx.effect`; when the scope is already inactive, it closes the socket directly with 1001 instead. `InvokeRemoteRequest.peer` and the `peer` parameter of `RemoteStreamOpener` and `wireStream.open` are required, so every in-process caller names its Peer, and the Gateway creates none. Looking up a Peer by id and Peer open and close events wait until the first consumer that needs them appears.
The Peer carries no access model: who the Peer is and what it may do are attached by business plugins on their own through `peer.ctx`.
The Peer carries its principal; what that principal may call is decided by the roles, bindings, and policies that [Peer principals and method-level Remote access control](../../implemented/architecture/2026-10-12-peer-principal-and-remote-access.md) defines.
### Client face
@@ -311,7 +318,7 @@ Credit frames wait until a consumer that needs sustained high-volume uplink appe
| Worker tunnel (`packages/experimental/webworker-runtime`) | The page adds `stream-uplink-item` / `stream-uplink-end` frames; `serveStream` assembles them into an `uplink` handed to `seams.openStream`. Same-origin trusted boundary: no inbox limit, and items after `end` are dropped |
| `remote-mock` (`packages/test-support/remote-mock`) | `StreamHandle.uplink: AsyncIterable<unknown>`; `rpc.open` passes it through |
An in-process carrier with no `peer` means the operator.
An in-process carrier passes its Peer explicitly; the in-page Host of the worker tunnel passes `connection.browserPeer`.
### Relation to `$events`
@@ -325,7 +332,7 @@ An in-process carrier with no `peer` means the operator.
// types.ts
export type RemoteStream<Out, In = never> = AsyncIterable<Out> & { readonly [STREAM_UPLINK]?: In }
export type PeerId = Branded<'PeerId'>
export interface PeerScope { readonly id: PeerId; readonly ctx: Context; dispose(): Promise<void> }
export interface PeerScope { readonly id: PeerId; readonly ctx: Context; readonly principal: Principal; readonly signal: AbortSignal; dispose(): Promise<void> }
export interface RemoteInvocation { … } // see Host face
declare module '@deepseek-ai/cordis' { interface Context { readonly invocation: RemoteInvocation | undefined } }
@@ -362,13 +369,13 @@ export interface InvokeRemoteRequest {
readonly args: Readonly<Record<string, unknown>>
/** Uplink items; absent is equivalent to an iterator that ends immediately. */
readonly uplink?: AsyncIterable<unknown>
/** The Peer that initiated the call; absent means the operator. */
readonly peer?: PeerScope
/** The Peer that initiated the call; required on every path. */
readonly peer: PeerScope
readonly signal?: AbortSignal
}
export interface TypertGatewayWireStream {
open: (endpoint: string, payload: unknown, uplink: AsyncIterable<unknown>, peer: PeerScope | undefined, signal: AbortSignal) => Promise<AsyncIterable<unknown>>
open: (endpoint: string, payload: unknown, uplink: AsyncIterable<unknown>, peer: PeerScope, signal: AbortSignal) => Promise<AsyncIterable<unknown>>
failure: (error: unknown) => RemoteStreamFailure
}
@@ -379,6 +386,7 @@ export type RemoteStreamOpener = (
uplink: AsyncIterable<unknown>,
peer: PeerScope,
control: AbortController,
window: RemoteUplinkWindow,
) => Promise<AsyncIterable<unknown>>
// index.ts Config addition
@@ -394,7 +402,7 @@ const signal = request.signal ?? NEVER_ABORTED_SIGNAL
const invocation = new GatewayInvocation(
{ namespace: request.namespace, method: request.method, args: request.args },
descriptor.service,
request.peer ?? this.operatorPeer(),
request.peer,
signal,
new UplinkDecoder(request.uplink ?? EMPTY_ASYNC_ITERABLE, descriptor.uplink?.codec, endpoint, control),
)
@@ -444,8 +452,8 @@ cancel : missing → ignore; otherwise control.abort(new Error('Remote stream ca
| `dsh-typert-protocol` (`types.ts`, `index.ts`) | `RemoteStream`, `RemoteStreamHandle`, `PeerId`, `PeerScope`, `RemoteInvocation`, the `ctx.invocation` declaration merge, `InvocationDescriptor.uplink`; the decorator recognizes only `mode: 'stream'`; `TypertRemoteService` registers the `invocation` accessor when constructed |
| `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` (`operator-peer.ts`, `rpc.ts`, `rpc-host.ts`, `index.ts`) | The operator `PeerScope`; `connection.operator` 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)`; `operatorPeer()` |
| `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` 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 |
@@ -560,7 +568,7 @@ for await (const reply of stream) replies.push(reply) // ['> a', '> b']
**Putting the Client's `send` on the `AsyncIterable` the generated method returns.** Hanging methods on an `AsyncIterable` is unconventional; the handle type `RemoteStreamHandle` extends it explicitly.
**Copying the Access mechanism.** Access control is business semantics, served by `vouch` and the `remote/invoke` waterfall; this Note only needs "whose stream is this", and `PeerScope` suffices.
**Copying the Access mechanism.** Access control is business semantics, served by `vouch` and the `remote/invoke` waterfall; this Note only needs "whose stream is this", and `PeerScope` suffices. Method-level access control later moved to [Peer principals and method-level Remote access control](../../implemented/architecture/2026-10-12-peer-principal-and-remote-access.md), which supersedes this part.
**Keeping unary RPC for the uplink.** One HTTP round trip, one authentication, and one lookup per uplink; two carriers with no ordering relation that only Client-side serialization can order; no EOF; cancellation that does not cover both halves; a hand-made correlation key per feature.
@@ -574,10 +582,11 @@ for await (const reply of stream) replies.push(reply) // ['> a', '> b']
- **Bought**: one stream with two directions, so interactive scenarios such as terminal keystrokes, approval answers, and background-job stdin no longer each build their own "one stream plus one unary"; a Host method knows its caller; the uplink and downlink types are declared in one place, the return type; uplink items come from the browser and are validated strictly per item at the Host, while downlink items are Host-produced typed values that pass straight through; the generated output and wire frames of existing `AsyncIterable<Out>` methods and unary methods are completely unchanged.
- **`this.ctx.invocation` is `undefined` outside a Remote call**, so a method that reads it must handle the optional; a service method invoked only directly in process correctly reads `undefined`.
- **Web terminals use `attach`** for screen output and input commands, with provider acknowledgements and per-attachment writable state; their Client retains the current generation's handle and never replays input.
- **The inbox limit applies to every stream**: a method that does not read its uplink fails as a whole when it receives many uplink frames. This is a deliberate explicit failure, recorded in the README.
- **The operator is the only Peer**, and `admit` is the only admission point; the first consumer that needs a second Peer must add opening, association, and events in `dsh-client-connection`.
- **Every browser connection shares `browserPeer`**, and `admit` is the only network admission point; in-process code creates further Peers with `connection.createPeer()`.
- **No cross-direction ordering guarantee between uplink and downlink**; a protocol that needs request-response pairing carries its own sequence numbers.
- **Upper-layer protocols do not yet expose `send`**: consumers of `$stream`, snapshot, and journal use only the downlink today; the object they receive is already the handle.
- **Upper-layer supervisors expose only the downlink**: `$stream`, snapshot and journal do not expose `send`; a domain that writes uplink retains its generated physical handle for that generation.
- **The worker tunnel behaves differently from the WebSocket carrier**: no inbox limit, and items after `end` are dropped, because page to worker is a same-origin trusted boundary.
## Testing
@@ -588,17 +597,15 @@ Retention cases in `gateway-stream.host.spec.ts` and `gateway.client.spec.ts` co
- Gateway Host: exact-key validation in frame parsing; `UplinkInbox` buffering during opening, `item` after `end`, overflow, ignored unknown `streamId`, and cleanup on socket closure; `UplinkDecoder` delivery with and without a codec and abort on decode failure; `uplink()` taken only once; the `extend({ invocation })` view and the origin of `peer`; `cancellableStream` closing the uplink before `return()`.
- Gateway Client: the handle's `send` (JSON validation, queued before open, throws after termination), idempotent `end`, `dispose` driving the `cancel` frame and ending later reads; the pump's three exits; the pump stopping as soon as a terminal frame arrives.
- typert: the analyzer's handling of `RemoteStream<Out, In>`, `RemoteStream<Out>`, `never`, and the existing `AsyncIterable<Out>`; decorator options; registry load-time validation.
- connection: the three outcomes of `admit`, 401, 403, and the operator; the operator reaching handlers through the bridge and the shared handler; release with the Connection.
- connection: the three outcomes of `admit`, 401, 403, and `browserPeer`; `browserPeer` reaching handlers through the bridge and the shared handler; release with the Connection.
- remote-mock and the worker tunnel: uplink pass-through; the direct proxy returning the real handle.
- Existing stream and unary method tests pass without changed expectations.
## Deferred
- `$stream` / snapshot / journal expose the handle's `send` to upper-layer protocols.
- Web terminal: one `attach` stream replaces `follow` plus `write` plus `resize`, and `attachmentId` retires.
- Duplex `$events`: waterfall answers become uplink items on the same `streamId`, and `$events/result` and `clientId` retire.
- Credit frames: wait for a consumer that needs sustained high-volume uplink; the inbox limit is the initial credit.
- The `streamInboxBytes` default of 262144 is far more than keyboard and stdin need; if the first consumer is terminal paste, it may be aligned with the `maxInputBytes` of `terminal-controller`.
- A Host-initiated half-close frame (the Host stops reading the uplink but keeps writing the downlink): no consumer; not added.
- `uplink()` on unary methods: it can read items that arrive while the method runs, semantically valid but of doubtful use; allowed, noted in the README.
@@ -606,4 +613,4 @@ Retention cases in `gateway-stream.host.spec.ts` and `gateway.client.spec.ts` co
- [Remote event delivery](../../../../packages/api/remotes/README.md): the unary `$events/result` answer path; the duplex `$events` item under Deferred partially supersedes it once it lands.
- [Session history and event transport](2026-08-18-session-history-and-event-transport.md): the `$stream()` supervisor and the journal / snapshot protocols above it, which this note leaves unchanged.
- [Web sidebar terminal](../feature/2026-09-09-web-sidebar-terminal.md): the unary terminal `write` and `attachmentId`; the `attach` stream under Deferred replaces it once it lands.
- [Web sidebar terminal](../feature/2026-09-09-web-sidebar-terminal.md): the `attach` stream carries input commands, provider acknowledgements and per-attachment writable state; reconnect never replays input.
@@ -8,41 +8,41 @@ Status: implemented
### 现状
`dsh-api-gateway` 在一条 WebSocket 上复用全部 Typert Remote 流,路径固定为 `/api/remote.mux`(`packages/api/gateway/src/stream-protocol.ts`)。一个 Host 方法用 `@Remote({ mode: 'stream' })` 修饰并返回 `Iterable` 或 `AsyncIterable`,就成为一条 Host 到 Client 的逻辑流;可选的末位 `signal: AbortSignal` 是唯一的保留参数,不进线路参数,由网关在解码后的业务参数之后追加(`packages/api/gateway/src/index.ts`)。客户端拿到的生成方法返回一个裸 `AsyncIterable`。
`dsh-api-gateway` 在 `/api/remote.mux` 的一条 WebSocket 上复用 Typert Remote 流。Host 方法通过 `@Remote({ mode: 'stream' })` 和 `RemoteStream<Out, In>` 声明两个方向;生成的 Client 方法返回 `RemoteStreamHandle<Out, In>`。既有的 `Iterable` 和 `AsyncIterable` 返回类型声明仅下行的方法。可选的末位 `signal: AbortSignal` 由 Gateway 提供,不进入线路参数。
线路帧只有五种:
两个方向共用一个 `streamId`:
| 方向 | 帧 | 定义 |
| --- | --- | --- |
| Client 到 Host | `{ type: 'open', streamId, endpoint, payload }` | `stream-protocol.ts` |
| Client 到 Host | `{ type: 'item', streamId, value? }` | `stream-protocol.ts` |
| Client 到 Host | `{ type: 'end', streamId }` | `stream-protocol.ts` |
| Client 到 Host | `{ type: 'cancel', streamId }` | `stream-protocol.ts` |
| Host 到 Client | `{ type: 'item', streamId, value? }` | `stream-protocol.ts` |
| Host 到 Client | `{ type: 'error', streamId, error: { code, message, details } }` | `stream-protocol.ts` |
| Host 到 Client | `{ type: 'end', streamId }` | `stream-protocol.ts` |
客户端能发的只有打开与取消。解析器 `parseRemoteStreamClientMessage`(`stream-protocol.ts`)对其他任何帧抛错,Host 连接收到后以 1008 关闭整个 socket(`stream-server.ts`)。结论:网关今天只支持"后端 iterable 发到前端"。需要前端向后端持续发数据的功能,各自绕路。
Client 句柄的 `send()` 提供上行项,`end()` 半关闭上行,`dispose()` 取消逻辑流。Host 通过 `this.ctx.invocation.uplink<In>()` 读取解码后的项;调用上下文同时携带获准接入的 Peer 和取消信号。
Host 方法也没有"这次调用是谁发起的"这个概念:`InvokeRemoteRequest = { namespace, method, args, signal }`(`packages/api/gateway/src/types.ts`)没有调用者槽位,`RemoteStreamOpener` 的签名是 `(endpoint, payload, signal)`(`stream-server.ts`),WebSocket 只在握手时校一次 cookie。
传输层负责校验、取消和有界 inbox。消费者协议负责输入权限、确认以及断线后的恢复。
### 三套各自为政的上行机制
### 上行消费者
| 功能 | 上行方式 | 位置 | 代价 |
| --- | --- | --- | --- |
| Web 终端按键 | 每次 xterm `onData` 一个 unary RPC `terminal.write(agent, id, attachmentId, data)`,客户端用 promise 链串行化并自限字节预算 | `packages/api/terminal-controller/src/client/model.ts`;Host `src/index.ts` | 一次按键一次 HTTP 往返与鉴权;两条载体靠 `attachmentId` 手工关联;满了报 `inputFull` |
| Web 终端输入与尺寸调整 | `terminal.attach` 承载上行 `TerminalInput` 和下行 `TerminalFrame`;Client 连续发送命令,并在收到 `ack` 后释放原始输入字节预算 | `packages/api/terminal-controller/src/client/model.ts`;Host `src/terminal.ts` | 当前附件拥有输入权;Client 预算耗尽时报 `inputFull`;重连后绝不重放输入 |
| 文件上传 | `Blob` 或 `ReadableStream` 走 Remote 层之外的原生 `POST /api/session/uploadFileBinary`,`duplex: 'half'` | `packages/client/file-upload/src/client/runtime.ts`;路由 `src/index.ts` | 不经 Typert 描述符,没有类型投影 |
| 审批与提问的回答 | Host 在 `$events` 流上下发 waterfall 帧,Client 用一个独立的 unary RPC `$events/result` 回答,靠 `clientId` 加 `eventId` 关联 | `packages/api/gateway/src/client/remote-events.ts`;Host `index.ts` | 第二套关联键;`index.ts` 记录了由此产生的竞态 |
这三套机制解决的是同一个问题:一条已经打开的逻辑流,客户端想往里写。
这些消费者都需要在相关操作仍进行时从 Client 向 Host 传送数据。Web 终端使用双工流;文件上传和事件回答保留各自的传输方式。
### 具体代价
### 消费者职责
- **两条载体对齐**。终端要用 `attachmentId` 把 `follow` 流与 `write` RPC 关联起来,并用它决定谁拥有输入权。审批要用 `clientId` 把 `$events` 流与 `$events/result` RPC 关联起来。
- **没有顺序保证**。unary RPC 走 HTTP,与 WebSocket 下行互不排序;终端只能在客户端串行化写入来自保顺序。
- **每次上行单独鉴权与查找**。`write(agent, id, attachmentId, data)` 每次解析 `agent`、查终端、校验 attachment。
- **没有 EOF**。客户端无法表达"我这边写完了",半关闭要再加一个 RPC。
- **取消不覆盖两半**。取消下行流不会取消排队中的上行写;反之亦然。
- **背压各写一套**。下行按 socket 写回调节拍拉迭代器(`stream-server.ts`);终端用有界 follower 让慢消费者显式失败(`terminal-controller/src/stream.ts`);上行靠客户端自限 `maxInputBytes`。
- **文档已经过时**。`docs/api-gateway.md:160` 仍写着 Remote 只处理一请求一结果,这一句已经落后于 README 记录的 stream 模式。
- **输入权限**。终端通过 Host 本地的 follower 持有控制附件。快照和状态帧报告各附件的 `writable` 状态;控制权转移后,旧附件继续接收输出。
- **顺序与确认**。终端输入和尺寸调整共用连续命令序列及终端操作队列。`ack` 报告提供者已接受操作或输入权限拒绝,绝不授权断线后重放。
- **独立生命周期**。取消 `attach` 会停止其输入和输出传递、丢弃尚未开始的命令,并等待在途输入操作收尾。PTY 的生命周期由保留机制和显式关闭独立管理。
- **协调字节限额**。Gateway 通过 `invocation.uplinkWindow` 提供完整帧 inbox 容量及本流的 JSON 封装开销。终端在快照中公布窗口,并通过提供者确认释放在途额度。窗口满时,输入和尺寸调整命令在有界 Client 队列中等待。`maxInputBytes` 限制原始 UTF-8 输入;`maxBufferedBytes` 分别限制 Client 保留的完整编码输入及每个 follower 保留的输出。
- **仍独立的载体**。审批回答仍通过 `clientId` 和 `eventId` 将 `$events` 与 `$events/result` RPC 关联。将该协议迁入双工流仍属后续工作。
### 与后台 job 的关系
@@ -66,7 +66,7 @@ Host 方法也没有"这次调用是谁发起的"这个概念:`InvokeRemoteReq
Client Host
───────────────────────────────── ───────────────────────────────────────────
const s = remote.job.attach(req, signal)
│ open { streamId, endpoint, payload } ───────▶ opener(endpoint, payload, uplink, peer, control)
│ open { streamId, endpoint, payload } ───────▶ opener(endpoint, payload, uplink, peer, control, window)
│ │ prepareInvocation
│ s.send(v1) → item { streamId, v1 } ───────▶ inbox.push(v1) │ invocation = { request, service, peer, signal, uplink() }
│ s.send(v2) → item { streamId, v2 } ───────▶ inbox.push(v2) │ receiverContext.extend({ invocation }).get(service)
@@ -88,7 +88,7 @@ Host 方法也没有"这次调用是谁发起的"这个概念:`InvokeRemoteReq
| 半关闭 | 一个方向已结束而另一个方向仍开放 |
| 项 | 一个方向上的一个业务值;每项独立经 codec 校验 |
| inbox | Host 侧每逻辑流一个的有界上行队列 |
| Peer | 连接层被接纳的一方,一个 `PeerScope`;本 Host 只有操作者一个 Peer |
| Peer | 一条连接代表的一方,一个携带 principal 的 `PeerScope`;所有浏览器连接共享 `connection.browserPeer`,进程内代码经 `connection.createPeer()` 自建([Peer 身份属性](../../implemented/architecture/2026-10-12-peer-principal-and-remote-access.zh.md)) |
| 调用上下文 | `RemoteInvocation`:本次调用的请求、接收服务、Peer、取消信号与上行入口 |
### 类型签名
@@ -135,10 +135,11 @@ export interface RemoteInvocation {
}
/** Cordis service key of the receiving service. */
readonly service: string
/** The Peer that initiated the call. In-process carriers and unadmitted calls are the operator. */
/** The Peer that initiated the call; every carrier names it explicitly. */
readonly peer: PeerScope
/** Carrier cancellation: Client cancel, socket closure, uplink failure. */
readonly signal: AbortSignal
readonly uplinkWindow?: RemoteUplinkWindow | undefined
/**
* The uplink items of this call. Can be taken once; the second call throws. With an uplink codec
* in the descriptor each item is decoded to In; without one, items are delivered as unknown with
@@ -179,24 +180,30 @@ declare module '@deepseek-ai/cordis' {
/** The opaque identity of one Peer. */
export type PeerId = Branded<'PeerId'>
/** One Peer's session on this Host: opened and released by the admitting side; ctx owns connection-lifetime registrations. */
/** One Peer's session on this Host: opened and released by its creator; ctx owns connection-lifetime registrations. */
export interface PeerScope {
readonly id: PeerId
readonly ctx: Context
/** Caller identity fixed at creation. */
readonly principal: Principal
/** Aborted once the Peer is disposed. */
readonly signal: AbortSignal
/** Participation in forwarded interactions; independent of RBAC. */
readonly interactions: 'answer' | 'none'
dispose(): Promise<void>
}
```
本 Host 只有一个 Peer:操作者。`@deepseek-ai/dsh-client-connection` 在服务 apply 时用 `createScope(connectionCtx, peer)` 建它,与 Agent 建自己的 scope 是同一机制:Peer 对象就是 ScopeKey,`peer.ctx` 承接连接期效果,`dispose()` 随 connection 释放让 fiber 静默。它以 `connection.operator` 暴露。
`@deepseek-ai/dsh-client-connection` 实现唯一的 Peer 类 `Peer(owner, { principal, interactions? })`,用 `createScope(owner, peer)` 建立,与 Agent 建自己的 scope 是同一机制:Peer 对象就是 ScopeKey,`peer.ctx` 承接连接期效果,scope 结束时 `peer.signal` 中止,`dispose()` 让 fiber 静默。Connection 在服务 apply 时以 owner principal 创建 `connection.browserPeer`;principal、`createPeer()` 与访问检查归 [Peer 身份属性与 Remote 方法级访问控制](../../implemented/architecture/2026-10-12-peer-principal-and-remote-access.zh.md)。
| 成员 | 语义 |
| --- | --- |
| `connection.operator: PeerScope` | 唯一的 Peer,随 connection 生灭 |
| `connection.admit(request): PeerAdmission` | 过既有的 `requestRejection`:被拒返回 `{ rejection: 401 \| 403 }`,否则返回 `{ peer: operator }`。将来的接纳方在这里接入 |
| `connection.browserPeer: PeerScope` | 所有通过认证的浏览器连接共享的 Peer;principal 为 owner;随 connection 生灭 |
| `connection.admit(request): PeerAdmission` | 检查 Host/Origin 与浏览器或 API 签名 Cookie;返回 `{ rejection: 401 \| 403 }` 或凭据对应的存活 Peer;API Peer 在 connect 时发布,并由其生命周期 socket 负责释放 |
网关 `handleUpgrade(req, socket, head, peer)` 为该 socket 绑一个固定 Peer 的 opener,并在 `peer.ctx.effect` 里登记 socket 关闭;scope 已失效时改为直接以 1001 关闭 socket。`InvokeRemoteRequest.peer?` 与 `RemoteStreamOpener` 的 `peer` 形参缺席时按操作者作答,现有进程内调用方零改动;没有 connection 的组合里网关自建的操作者替身同样经 `createScope` 建立,`dispose()` 契约一致。按 id 查 Peer、把载体关联到第二个 Peer、Peer 的开关事件,都等第一个需要多 Peer 的消费者出现时再加。
网关 `handleUpgrade(req, socket, head, peer)` 为该 socket 绑一个固定 Peer 的 opener,并在 `peer.ctx.effect` 里登记 socket 关闭;scope 已失效时改为直接以 1001 关闭 socket。`InvokeRemoteRequest.peer`、`RemoteStreamOpener` 与 `wireStream.open` 的 `peer` 形参均为 required,每个进程内调用方都显式给出 Peer,网关不自建 Peer。按 id 查 Peer、Peer 的开关事件,都等第一个需要它们的消费者出现时再加。
Peer 不携带访问模型:Peer 是谁、能做什么,由业务插件通过 `peer.ctx` 自行附加。
Peer 携带自己的 principal;该 principal 能调用什么,由 [Peer 身份属性与 Remote 方法级访问控制](../../implemented/architecture/2026-10-12-peer-principal-and-remote-access.zh.md) 定义的 role、binding 与 policy 判定。
### Client 面
@@ -311,7 +318,7 @@ credit 帧留待需要持续大流量上行的消费者出现时再加;inbox
| worker 隧道(`packages/experimental/webworker-runtime`) | 页面新增 `stream-uplink-item` / `stream-uplink-end` 帧;`serveStream` 把它们组装成 `uplink` 交给 `seams.openStream`。同源可信边界:没有 inbox 上限,`end` 之后的项丢弃 |
| `remote-mock`(`packages/test-support/remote-mock`) | `StreamHandle.uplink: AsyncIterable<unknown>`;`rpc.open` 透传 |
进程内载体的 `peer` 缺席即操作者。
进程内载体显式传入 Peer;worker 隧道的页内 Host 传 `connection.browserPeer`。
### 与 `$events` 的关系
@@ -325,7 +332,7 @@ credit 帧留待需要持续大流量上行的消费者出现时再加;inbox
// types.ts
export type RemoteStream<Out, In = never> = AsyncIterable<Out> & { readonly [STREAM_UPLINK]?: In }
export type PeerId = Branded<'PeerId'>
export interface PeerScope { readonly id: PeerId; readonly ctx: Context; dispose(): Promise<void> }
export interface PeerScope { readonly id: PeerId; readonly ctx: Context; readonly principal: Principal; readonly signal: AbortSignal; dispose(): Promise<void> }
export interface RemoteInvocation { … } // see Host face
declare module '@deepseek-ai/cordis' { interface Context { readonly invocation: RemoteInvocation | undefined } }
@@ -362,13 +369,13 @@ export interface InvokeRemoteRequest {
readonly args: Readonly<Record<string, unknown>>
/** Uplink items; absent is equivalent to an iterator that ends immediately. */
readonly uplink?: AsyncIterable<unknown>
/** The Peer that initiated the call; absent means the operator. */
readonly peer?: PeerScope
/** The Peer that initiated the call; required on every path. */
readonly peer: PeerScope
readonly signal?: AbortSignal
}
export interface TypertGatewayWireStream {
open: (endpoint: string, payload: unknown, uplink: AsyncIterable<unknown>, peer: PeerScope | undefined, signal: AbortSignal) => Promise<AsyncIterable<unknown>>
open: (endpoint: string, payload: unknown, uplink: AsyncIterable<unknown>, peer: PeerScope, signal: AbortSignal) => Promise<AsyncIterable<unknown>>
failure: (error: unknown) => RemoteStreamFailure
}
@@ -379,6 +386,7 @@ export type RemoteStreamOpener = (
uplink: AsyncIterable<unknown>,
peer: PeerScope,
control: AbortController,
window: RemoteUplinkWindow,
) => Promise<AsyncIterable<unknown>>
// index.ts Config addition
@@ -394,7 +402,7 @@ const signal = request.signal ?? NEVER_ABORTED_SIGNAL
const invocation = new GatewayInvocation(
{ namespace: request.namespace, method: request.method, args: request.args },
descriptor.service,
request.peer ?? this.operatorPeer(),
request.peer,
signal,
new UplinkDecoder(request.uplink ?? EMPTY_ASYNC_ITERABLE, descriptor.uplink?.codec, endpoint, control),
)
@@ -444,8 +452,8 @@ cancel : missing → ignore; otherwise control.abort(new Error('Remote stream ca
| `dsh-typert-protocol`(`types.ts`、`index.ts`) | `RemoteStream`、`RemoteStreamHandle`、`PeerId`、`PeerScope`、`RemoteInvocation`、`ctx.invocation` 声明合并、`InvocationDescriptor.uplink`;修饰器只认 `mode: 'stream'`;`TypertRemoteService` 构造时注册 `invocation` accessor |
| `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`(`operator-peer.ts`、`rpc.ts`、`rpc-host.ts`、`index.ts`) | 操作者 `PeerScope`;`connection.operator` 与 `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)`;`operatorPeer()` |
| `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` 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 目录 |
@@ -560,7 +568,7 @@ for await (const reply of stream) replies.push(reply) // ['> a', '> b']
**客户端 `send` 放在生成方法返回的 `AsyncIterable` 上。** `AsyncIterable` 上挂方法不常规;句柄类型 `RemoteStreamHandle` 显式扩展它。
**照搬 Access 机制。** 访问控制是业务语义,`vouch` 与 `remote/invoke` waterfall 为它服务;本 Note 只需要"这条流是谁的",`PeerScope` 就够。
**照搬 Access 机制。** 访问控制是业务语义,`vouch` 与 `remote/invoke` waterfall 为它服务;本 Note 只需要"这条流是谁的",`PeerScope` 就够。方法级访问控制后来归入 [Peer 身份属性与 Remote 方法级访问控制](../../implemented/architecture/2026-10-12-peer-principal-and-remote-access.zh.md),该部分由其 supersede。
**继续用 unary RPC 做上行。** 每次上行一次 HTTP 往返、一次鉴权、一次查找;两条载体没有顺序关系,只能靠客户端串行化;没有 EOF;取消不覆盖两半;每个功能各造一套关联键。
@@ -574,10 +582,11 @@ for await (const reply of stream) replies.push(reply) // ['> a', '> b']
- **买到的**:一条流两个方向,终端按键、审批回答、后台任务 stdin 这类交互式场景不再各造一套"一条流加一个 unary";Host 方法知道调用者;上行类型与下行类型在返回类型里一处声明,上行项来自浏览器,在 Host 逐项严格校验,下行项是 Host 自产的类型值,直接透传;既有 `AsyncIterable<Out>` 方法与 unary 方法的生成物与线路帧完全不变。
- **`this.ctx.invocation` 在非 Remote 调用下是 `undefined`**,方法读它要处理可选;一个只被进程内直接调用的服务方法读到 `undefined` 是正确行为。
- **Web 终端使用 `attach`** 传输屏幕输出和输入命令,并提供提供者确认和附件专属可写状态;Client 保留当前代次的句柄,绝不重放输入。
- **inbox 上限对所有流生效**:一个不读上行的方法收到大量上行帧会整流失败。这是刻意的显式失败,README 记明。
- **Peer 只有操作者一个**,`admit` 是唯一的接纳点;第一个需要第二个 Peer 的消费者要在 `dsh-client-connection` 加上打开、关联与事件。
- **所有浏览器连接共享 `browserPeer`**,`admit` 是唯一的网络接纳点;进程内代码经 `connection.createPeer()` 创建其他 Peer。
- **上行与下行之间没有跨方向顺序保证**;需要请求应答配对的协议自带序号。
- **上层协议尚未透出 `send`**:`$stream`、snapshot、journal 的消费者今天只用下行;它们拿到的对象已经是句柄。
- **上层监督器仅暴露下行**:`$stream`、snapshot 和 journal 不暴露 `send`;需要写入上行的领域保留该代次的生成物理句柄。
- **worker 隧道与 WebSocket 载体行为不同**:没有 inbox 上限,`end` 之后的项丢弃,因为页面到 worker 是同源可信边界。
## 测试
@@ -588,17 +597,15 @@ for await (const reply of stream) replies.push(reply) // ['> a', '> b']
- 网关 Host:帧解析的精确键校验;`UplinkInbox` 的 opening 缓冲、`end` 后 `item`、超限、未知 `streamId` 忽略、socket 关闭时的收尾;`UplinkDecoder` 有无 codec 两种交付与解码失败中止;`uplink()` 只能取一次;`extend({ invocation })` 视图与 `peer` 的来源;`cancellableStream` 先关 uplink 再 `return()`。
- 网关 Client:句柄的 `send`(JSON 校验、open 前入队、终止后抛错)、`end` 幂等、`dispose` 驱动 `cancel` 帧并结束后续读取;泵的三种退出;终止帧到达即停泵。
- typert:analyzer 对 `RemoteStream<Out, In>`、`RemoteStream<Out>`、`never` 与既有 `AsyncIterable<Out>` 的处理;修饰器选项;registry 加载校验。
- connection:`admit` 的 401、403 与操作者三种结果;操作者经桥与共享处理器到达;随 connection 释放。
- connection:`admit` 的 401、403 与 `browserPeer` 三种结果;`browserPeer` 经桥与共享处理器到达;随 connection 释放。
- remote-mock、worker 隧道:上行透传;直接代理返回真句柄。
- 既有 stream 与 unary 方法的测试不改期望值即通过。
## 后续
- `$stream` / snapshot / journal 把句柄的 `send` 透出给上层协议。
- Web 终端:一条 `attach` 流取代 `follow` 加 `write` 加 `resize`,`attachmentId` 退场。
- 双工 `$events`:waterfall 的回答成为同一 `streamId` 上的上行项,`$events/result` 与 `clientId` 退场。
- credit 帧:等需要持续大流量上行的消费者出现;inbox 上限就是初始授信。
- `streamInboxBytes` 默认值 262144 对键盘与 stdin 远超需要;若第一个消费者是终端粘贴,可能与 `terminal-controller` 的 `maxInputBytes` 对齐。
- Host 发起的半关闭帧(Host 不再读上行但继续写下行):没有消费者,不加。
- unary 方法的 `uplink()`:能读到方法运行期间到达的项,语义成立但用处存疑;允许,README 注明。
@@ -606,4 +613,4 @@ for await (const reply of stream) replies.push(reply) // ['> a', '> b']
- [Remote 事件投递](../../../../packages/api/remotes/README.zh.md):`$events/result` 的 unary 回答路径;「后续」里的双工 `$events` 落地后部分取代它。
- [会话历史与事件传输](2026-08-18-session-history-and-event-transport.zh.md):`$stream()` 监督器与其上的 journal / snapshot 协议,本 Note 不改它们。
- [Web 侧边栏终端](../feature/2026-09-09-web-sidebar-terminal.zh.md):终端按键的 unary `write` 与 `attachmentId`;「后续」里的 `attach` 流落地后取代它。
- [Web 侧边栏终端](../feature/2026-09-09-web-sidebar-terminal.zh.md):`attach` 流承载输入命令、提供者确认和附件专属可写状态;重连绝不重放输入。
@@ -15,8 +15,8 @@
en: adccc5dc758ad8f6
zh: 50304d68db42e8b1
/agent-note-timed-user-questions-as-two-settlements/decision/ownership:
en: 0543678ad9d26fc5
zh: aa6ba207e661c567
en: a28e37fda6e9eaff
zh: 9ca622e09ab24b4d
/agent-note-timed-user-questions-as-two-settlements/decision/foreground-wait-and-claim-lifetime:
en: 447713bbdf1ab4d8
zh: 2757678ae47822aa
@@ -30,14 +30,14 @@
en: 3a33f2583e30e32a
zh: 0ad7e8d1e47dc510
/agent-note-timed-user-questions-as-two-settlements/decision/reply-node-and-grouping:
en: 10323b0e0340a40b
zh: a17fc3a4d4d820a7
en: d87d7432c37405a7
zh: 49f1bbf53856851a
/agent-note-timed-user-questions-as-two-settlements/alternatives-considered:
en: bf61d0ec80c63bb4
zh: d4ce4d4438ffb013
en: 1a070542bfa40e68
zh: 82e32964da912dd8
/agent-note-timed-user-questions-as-two-settlements/verification:
en: d912a73a5ff409eb
zh: 7b626794773e2301
en: 1d19d196c363a273
zh: 7cb0475f0cb554d1
/agent-note-timed-user-questions-as-two-settlements/consequences:
en: 2bc99c99be8ca9da
zh: 8f262ec8b9b0945e
en: 0580e85cc12439c5
zh: a4b151d2c9e5b747
@@ -29,7 +29,7 @@ Foreground answers use the existing Remote Event waterfall. A timeout ends that
| `UserQuestionService` | Foreground waits and Client claims, unattended deadlines, late-answer validation and steering, and the durable question projection. |
| Gateway and Remote runtime | Existing request delivery, settlement, cancellation, and Remote stream transport; no question-specific timer or event-name branch. |
| `ui-user-questions` | One card per Session and call, answer-channel selection, local countdown and drafts, and the reply Definition and renderer. |
| `ui-chat` | Message-id-based presentation aggregation and ordinary process/Turn folding. |
| `ui-chat` | Ordinary process/Turn folding. |
| Question-specific tool row | Pending/answered presentation and panel actions, composed in `QuestionToolRow` without adding `rowAction` to generic `ToolRow`. |
The [question service](../../../../packages/interaction/user-questions/README.md), [tool](../../../../packages/interaction/tool-ask-user/README.md), and [questions UI](../../../../packages/client/ui-user-questions/README.md) own their package contracts.
@@ -72,11 +72,11 @@ Autofocus applies only when the answer channel is ready, no countdown is active,
The questions UI identifies a late reply from `source.kind`, then parses its recorded JSON for question-and-answer presentation. Pasting identical JSON in an ordinary user message does not identify it as a reply. Unreadable recorded payloads retain their raw-text presentation.
The ordinary message Definition and the question-reply Definition both match the appended message and retain separate Node Store projections with the same message id. Chat business grouping omits the `turn-trigger` presentation when a `question-reply` with that id is present. The duplicate trigger neither renders nor splits a group; the reply renders once. The same aggregation applies to unscoped historical input.
The question-reply Definition claims the appended reply message on the Chat target through an [exclusive match](2026-10-12-exclusive-conversation-matches.md), so the ordinary message Definition builds no `turn-trigger` or context projection for it and the reply renders once, including for unscoped historical input.
The reply participates in ordinary process grouping and whole-Turn folding. It is not independently visible: reading it can require expanding both its Turn and process group. Turn disclosure stays before the group it controls, including a reply that opens the Turn.
This decision adds no process-role field, independent-layout flag, user-message structure change, or target-local fallback dispatch. The independent-kind rules stay in Chat's existing grouping logic. A node-data-owned declaration of grouping and overlap semantics is deferred infrastructure, not part of the question feature.
This decision adds no independent-layout flag or user-message structure change. The reply declares no group-tree position, so it follows the default placement of process content.
## Alternatives considered
@@ -90,7 +90,7 @@ This decision adds no process-role field, independent-layout flag, user-message
**Reclassify JSON inside generic `MessageItem`, borrow the relay form, or use the default context row.** Parsing text to identify the business message confuses its source with its content; the relay form describes another agent's message, and a generic context row exposes JSON. A dedicated source, Definition, and renderer own the question presentation.
**Independent reply layout through generic node metadata.** It broadens a question feature into a conversation-infrastructure change. Ordinary folding and Chat-owned aggregation are sufficient for this decision; independently declared node-data grouping semantics remain a separate design task.
**Independent reply layout through generic node metadata.** It broadens a question feature into a conversation-infrastructure change. Ordinary folding is sufficient for this decision; placing the reply as user input through its group-tree position is separate work.
**A log-only close event or a manufactured dismissal message on panel close.** A log-only close hides the user's decision from the model and adds another durable mechanism. Manufacturing a reply treats hiding the panel as an answer. Panel close therefore leaves a timed question answerable.
@@ -101,7 +101,7 @@ This decision adds no process-role field, independent-layout flag, user-message
- [Service tests](../../../../packages/interaction/user-questions/tests/user-questions.spec.ts) and [wait tests](../../../../packages/interaction/user-questions/tests/timed-wait.spec.ts) cover claimed and unclaimed waits, last-claim disconnect, cancellation, timeout error mapping, continued-only answers, and exact answer-id validation.
- [Projection tests](../../../../packages/interaction/user-questions/tests/projection.spec.ts) distinguish legacy and timed schemas and cover native and PTC pending calls, other failures, resume repair, queued reply cancellation, and admitted settlement.
- [Client listener tests](../../../../packages/client/ui-user-questions/tests/browser-plugin.client.spec.ts) and [composer tests](../../../../packages/client/ui-user-questions/tests/user-questions-composer.client.spec.tsx) cover claim retention through Host acceptance, focus before handshake, indefinite waits, draft retention, RPC resubmission, and panel actions.
- [Grouping tests](../../../../packages/client/ui-chat/tests/process-groups.client.spec.ts) and [browser replay](../../../../apps/web/tests/question-composer.e2e.ts) cover one rendered reply with both projections retained, ordinary collapsed placement, disclosure ordering, and the original tool row's read-only late-answer panel.
- [Grouping tests](../../../../packages/client/ui-chat/tests/process-groups.client.spec.ts) and [browser replay](../../../../apps/web/tests/question-composer.e2e.ts) cover ordinary collapsed placement, disclosure ordering, and the original tool row's read-only late-answer panel.
- Legacy round-trip, cancellation, plan-review intent, and Session-remount regressions remain covered by their owning tests. Live multi-Client delivery races and real Host restart remain integration coverage gaps beyond isolated lifecycle and projection tests.
## Consequences
@@ -111,7 +111,6 @@ The model can continue independent work while the question remains answerable. B
- Take time affects only one Client. Another Client can answer or expire the shared waterfall; local focus does not establish a global hold.
- A terminal claim-stream failure can reject the foreground question rather than produce pending. No unattended-wait fallback is provided for that failure.
- Host and Client clocks need not align, but Client system-clock changes can move expiry and transport latency can delay the local countdown's start.
- The duplicate projections remain stored. Aggregation adds linear scans during structural grouping rebuilds; ordinary text-only streaming updates retain their incremental path. This is an algorithmic bound, not a measured latency claim.
- Unanswered continued questions can remain after reopening a Session. Panel close records nothing, and visibility follows normal folding rather than an always-visible reply guarantee.
- A queued late reply can be canceled before the agent admits it. The question stays answerable until an admitted `user/message` records the answer.
- A PTC sub-call interrupted before its `tool/ptc-dispatch` result is recorded cannot be reconstructed from the parent `run_code` failure, so it cannot take a late answer after recovery.
@@ -29,7 +29,7 @@ Status: implemented
| `UserQuestionService` | 前台等待与 Client 接手、无人接手时的 deadline、迟到回答校验与 steering(中途引导),以及持久问题投影。 |
| Gateway 与 Remote 运行时 | 现有请求投递、结算、取消及 Remote stream 传输;不包含问题专属计时器或按事件名分支。 |
| `ui-user-questions` | 每个 Session 和调用的一张卡片、回答通道选择、本地倒计时与草稿,以及回复 Definition 和渲染器。 |
| `ui-chat` | 基于消息 id 的呈现聚合,以及普通过程/轮次折叠。 |
| `ui-chat` | 普通过程/轮次折叠。 |
| 问题专用工具行 | pending/已回答呈现与面板操作,在 `QuestionToolRow` 中组合,不向通用 `ToolRow` 添加 `rowAction`。 |
[问题服务](../../../../packages/interaction/user-questions/README.zh.md)、[工具](../../../../packages/interaction/tool-ask-user/README.zh.md)和[问题 UI](../../../../packages/client/ui-user-questions/README.zh.md)分别拥有各包的约定。
@@ -72,11 +72,11 @@ Client 计算 `Date.now() + remainingMs`。手动聚焦未编辑的回答区会
问题 UI 通过 `source.kind` 识别迟到回复,再解析已记录的 JSON 来呈现问答。普通用户消息粘贴相同 JSON 不会被识别为回复。无法读取的已记录载荷保留原始文本呈现。
普通消息 Definition 和问答回复 Definition 都匹配追加的消息,在 Node Store 中保留消息 id 相同的独立投影。存在同 id 的 `question-reply` 时,Chat 业务分组省略 `turn-trigger` 呈现。重复触发节点既不渲染,也不切开分组,回复只显示一次。相同聚合也适用于无轮次归属的历史输入。
问答回复 Definition 通过[独占匹配](2026-10-12-exclusive-conversation-matches.zh.md)在 Chat target 上认领追加的回复消息,普通消息 Definition 因此不再为它建立 `turn-trigger` 或 context 投影,回复只显示一次;无轮次归属的历史输入同样如此。
回复参与普通过程分组和整轮折叠,不保证独立可见:阅读时可能需要展开所属轮次与过程分组。轮次折叠控件始终在所控制的分组之前,包括由回复开启轮次的情况。
本决策不添加 process-role 字段、独立布局标记、用户消息结构变更或目标内 fallback 派发。独立 kind 规则保留在 Chat 现有分组逻辑中。由节点 data 声明分组与重叠语义的机制是留待拆分的基础设施,不属于问题功能。
本决策不添加独立布局标记或用户消息结构变更。回复不声明分组树位置,因此按过程内容的默认位置放置。
## Alternatives considered
@@ -90,7 +90,7 @@ Client 计算 `Date.now() + remainingMs`。手动聚焦未编辑的回答区会
**在通用 `MessageItem` 内重新分类 JSON、借用 relay form,或使用默认 context 行。** 解析文本来识别业务消息混淆了来源与内容;relay form 表示其他 agent 的消息,通用 context 行则会暴露 JSON。专用来源、Definition 和渲染器拥有问题呈现。
**通过通用节点元数据让回复独立布局。** 这会把问题功能扩大为对话基础设施变更。普通折叠与 Chat 内聚合足以满足本决策;由节点 data 独立声明分组语义仍是单独的设计任务。
**通过通用节点元数据让回复独立布局。** 这会把问题功能扩大为对话基础设施变更。普通折叠足以满足本决策;通过分组树位置把回复放置为用户输入是单独的工作。
**面板关闭时写仅入日志的关闭事件,或伪造放弃消息。** 仅入日志会让模型看不到用户决定,还增加另一套持久机制。伪造回复则把隐藏面板当成回答。因此关闭面板仍让计时问题保持可回答。
@@ -101,7 +101,7 @@ Client 计算 `Date.now() + remainingMs`。手动聚焦未编辑的回答区会
- [服务测试](../../../../packages/interaction/user-questions/tests/user-questions.spec.ts)与[等待测试](../../../../packages/interaction/user-questions/tests/timed-wait.spec.ts)覆盖有人与无人接手、最后接手方断开、取消、超时错误映射、仅接受已继续问题的回答,以及答案 id 的精确校验。
- [投影测试](../../../../packages/interaction/user-questions/tests/projection.spec.ts)区分 legacy 与 timed schema,并覆盖原生及 PTC pending 调用、其他失败、恢复修复、排队回复取消和准入后结算。
- [Client 监听器测试](../../../../packages/client/ui-user-questions/tests/browser-plugin.client.spec.ts)与[composer 测试](../../../../packages/client/ui-user-questions/tests/user-questions-composer.client.spec.tsx)覆盖接手保留到 Host 接受结果、握手前聚焦、无限期等待、草稿保留、RPC 重提及面板操作。
- [分组测试](../../../../packages/client/ui-chat/tests/process-groups.client.spec.ts)与[浏览器回放](../../../../apps/web/tests/question-composer.e2e.ts)覆盖保留两个投影但只渲染一次回复、普通折叠位置、折叠控件顺序,以及原工具行的迟到答案只读面板。
- [分组测试](../../../../packages/client/ui-chat/tests/process-groups.client.spec.ts)与[浏览器回放](../../../../apps/web/tests/question-composer.e2e.ts)覆盖普通折叠位置、折叠控件顺序,以及原工具行的迟到答案只读面板。
- legacy 问答往返、取消、计划评审意图及 Session 重新挂载的回归仍由各自测试覆盖。真实多 Client 投递竞争与真实 Host 重启仍是独立生命周期和投影测试之外的集成覆盖缺口。
## Consequences
@@ -111,7 +111,6 @@ Client 计算 `Date.now() + remainingMs`。手动聚焦未编辑的回答区会
- 「慢慢回答」只影响一个 Client。其他 Client 可以回答或让共享 waterfall 到期,本地聚焦不构成全局暂停。
- 接手 stream 的终止性失败可能让前台问题失败,而不是产生 pending。不为该失败提供无人接手等待的 fallback。
- Host 与 Client 时钟不必对齐,但 Client 系统时钟变化可能移动到期时间,传输延迟可能推迟本地倒计时的开始。
- 重复投影仍保留在存储中。聚合在结构性分组重建时增加线性扫描;普通纯文本 stream 更新保留增量路径。这是算法复杂度界限,不是实测延迟结论。
- 无人回答的已继续问题可以在重新打开 Session 后继续存在。关闭面板不写记录,可见性遵循普通折叠,而不是保证回复始终可见。
- 排队的迟到回复可能在 agent 准入前被取消。只有已准入的 `user/message` 记录答案,问题在此之前保持可回答。
- PTC 子调用若在记录 `tool/ptc-dispatch` 结果前中断,无法从父 `run_code` 的失败中重建,因此恢复后不能接受迟到回答。
Loaded 100 of 4305 files, more files were not shown because too many files have changed in this diff. Show more