Merge origin/master into refactor/unify-time-vocabulary
Resolutions: - packages/util/time/package.json keeps this branch's description and the polyfill dependency, with master's 0.2.1-alpha.2 version. - docs/module-graph.md and .zh.md are regenerated: master's graph plus this branch's context/time-context and schedule edges to util-time.
This commit is contained in:
commit
183cbf4df4
2839 files changed
+68732
-15060
No files matched your search
+8
-8
@@ -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: 921085cb9d894688
|
||||
zh: 12d99fe153d36738
|
||||
/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: 02e6b36f9f92c83a
|
||||
zh: edd8b2db7a787483
|
||||
/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
|
||||
|
||||
+5
-5
@@ -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` 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 22.19 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
|
||||
|
||||
|
||||
+5
-5
@@ -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` → 将 `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 22.19 或更高版本上运行 `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)拥有缓存和启用行为的说明。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
|
||||
+2
-2
@@ -15,8 +15,8 @@
|
||||
en: 4921a060c657b790
|
||||
zh: 2f9632469c6f0b60
|
||||
/agent-note-provider-routed-llm-adapters-and-a-generic-pi-ai-backend/decision/explicit-pi-ai-provider-profiles:
|
||||
en: e24610c474a94bc4
|
||||
zh: 96a89035cb8eabf9
|
||||
en: 1346315312eb35e2
|
||||
zh: 3555f91c3afb49f8
|
||||
/agent-note-provider-routed-llm-adapters-and-a-generic-pi-ai-backend/decision/recorded-assistant-route-and-replay-state:
|
||||
en: ed60d078eacc67df
|
||||
zh: a22631faf93deee2
|
||||
|
||||
@@ -36,6 +36,8 @@ The adapter calls pi-ai's `streamSimple()` so each catalog model chooses its reg
|
||||
|
||||
pi-ai's common stream options do not expose stop sequences. `dsh-llm-pi-ai` rejects a defined Harness `stop` option with `UNSUPPORTED_OPTION` rather than silently ignoring it or growing a second provider-specific payload implementation. `dsh-llm-deepseek` continues to support `stop` through its native request serializer.
|
||||
|
||||
pi-ai owns protocol-specific system-message placement and the concatenation fallback for models without in-history system support. Harness preserves later system messages in transcript order and reports catalog capabilities to prompt admission and shared tool-history projection. Duplicating pi-ai's fallback or applying Anthropic's placement rules to every protocol would create a second serialization policy that can drift when pi-ai changes. Capability reporting remains a Harness responsibility because the loop needs it before serialization.
|
||||
|
||||
### Recorded assistant route and replay state
|
||||
|
||||
Assistant messages carry the request's `provider` and `model`, plus an optional JSON-serializable adapter replay state. A successful `assistant/message` session event records those fields and `deriveMessages()` returns them with the assistant message. User, system, context, and tool-result messages carry no assistant route fields. The provider/model fields are authoritative loop data; an adapter owns only its opaque replay-state payload.
|
||||
|
||||
@@ -36,6 +36,8 @@ Status: implemented
|
||||
|
||||
pi-ai 的通用流选项不支持停止序列。若 Harness `stop` 选项已定义,`dsh-llm-pi-ai` 会以 `UNSUPPORTED_OPTION` 拒绝请求,不会静默忽略,也不会增加第二套提供方专用 payload 实现。`dsh-llm-deepseek` 继续通过原生请求序列化器支持 `stop`。
|
||||
|
||||
pi-ai 负责各协议的系统消息位置,以及不支持历史内系统消息的模型所需的拼接回退。Harness 按对话记录顺序保留后续系统消息,并向提示词准入和共享工具历史投影报告目录能力。重复实现 pi-ai 的回退,或将 Anthropic 的位置规则应用于所有协议,会形成第二套序列化策略,并可能随 pi-ai 的变化而偏离。能力报告仍由 Harness 负责,因为循环在序列化之前就需要这些信息。
|
||||
|
||||
### 已记录的助手路由与回放状态
|
||||
|
||||
助手消息携带请求的 `provider` 和 `model`,以及可选的 JSON 可序列化适配器回放状态。成功的 `assistant/message` 会话事件记录这些字段,`deriveMessages()` 返回助手消息时也会包含它们。用户、系统、上下文与工具结果消息不携带助手路由字段。提供方/模型字段是 agent loop 的权威数据;适配器仅拥有其不透明回放状态 payload。
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
|
||||
+1
-1
@@ -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)。
|
||||
|
||||
## 后果
|
||||
|
||||
|
||||
+4
-4
@@ -9,11 +9,11 @@
|
||||
en: c25db2ef111b2276
|
||||
zh: 557891ca9e030ae5
|
||||
/agent-note-one-carrier-level-browser-trust-boundary-for-all-api-routes/decision:
|
||||
en: 1a1af1e8125ba4b0
|
||||
zh: dd6c282651a06af5
|
||||
en: ca8044c9e0013be6
|
||||
zh: d60af7af291299c3
|
||||
/agent-note-one-carrier-level-browser-trust-boundary-for-all-api-routes/alternatives-considered:
|
||||
en: 1a4b0ae8072e8e50
|
||||
zh: d469553ad3b5d112
|
||||
/agent-note-one-carrier-level-browser-trust-boundary-for-all-api-routes/consequences:
|
||||
en: 8acf5e6f55a747c2
|
||||
zh: bbd0b10927302965
|
||||
en: e31df28a2e82a64d
|
||||
zh: 4ca7097ebbc24cd2
|
||||
@@ -13,9 +13,11 @@ The web GUI host serves `/api` over plain loopback HTTP (default `127.0.0.1:3080
|
||||
Enforce browser trust once, at the carrier, for the entire `/api` prefix — two halves:
|
||||
|
||||
- **Media-type fence (dsh-client-connection)**: every `/api` POST must declare `application/json`, else 415 before parsing. Cross-site "simple" requests thereby stop existing: any cross-site attempt is forced into a CORS preflight this server never answers.
|
||||
- **Authority fence (dsh-client-connection, `src/api-request-trust.ts`)**: every request must present a `Host` that is loopback or matches a `trustedHosts` entry (exact on `host:port`, any port on port-less entries, WHATWG-normalized; rebinding defense). Deliberately no shortcut for unmarked requests: over plain HTTP a browser attaches neither `Origin` nor Fetch-Metadata to reads (EventSource, images, navigations — those headers go only to trustworthy destinations), so an unmarked request may be a rebound browser read whose response the page can read, and Host is the one header rebinding cannot forge; non-browser clients pass via loopback, the derived LAN IP literals, or a declared authority. An attached `Origin` must equal the Host authority; `sec-fetch-site: cross-site` is refused outright. A `trustedHosts` entry that is not a bare, canonical authority fails the plugin load — WHATWG parsing would otherwise quietly authorize the hostname inside a typo or broaden an exact-port grant. `host.pickDirectory` loses its bespoke guard and rides the same fence.
|
||||
- **Authority fence (dsh-client-connection, `src/api-request-trust.ts`)**: every request must present a `Host` that is loopback or matches a `trustedHosts` entry (exact on `host:port`, any port on port-less entries, WHATWG-normalized; rebinding defense). Deliberately no shortcut for unmarked requests: over plain HTTP a browser attaches neither `Origin` nor Fetch-Metadata to reads (EventSource, images, navigations — those headers go only to trustworthy destinations), so an unmarked request may be a rebound browser read whose response the page can read, and Host is the one header rebinding cannot forge; non-browser clients pass via loopback, a declared authority, or the listener's own bind literal that [Bind one concrete Web address](2026-09-19-concrete-web-bind-address.md) admits. An attached `Origin` must equal the Host authority; `sec-fetch-site: cross-site` is refused outright. A `trustedHosts` entry that is not a bare, canonical authority fails the plugin load — WHATWG parsing would otherwise quietly authorize the hostname inside a typo or broaden an exact-port grant. `host.pickDirectory` loses its bespoke guard and rides the same fence.
|
||||
|
||||
Reachability is the webserver binding's policy (`host: 127.0.0.1 | 0.0.0.0`), and this fence is a confused-deputy defense rather than identity. Connection applies the separate [browser token authentication](2026-08-24-browser-token-authentication.md) after the fence. The fence does not inspect peer socket addresses: binding expresses reachability, `trustedHosts` names accepted authorities, and the socket address adds nothing the Host/Origin checks need.
|
||||
Reachability is the webserver binding's policy (`host`; [Bind one concrete Web address](2026-09-19-concrete-web-bind-address.md) owns which literals it accepts), and this fence is a confused-deputy defense rather than identity. Connection applies the separate [browser token authentication](2026-08-24-browser-token-authentication.md) after the fence. The fence does not inspect peer socket addresses: binding expresses reachability, `trustedHosts` names accepted authorities, and the socket address adds nothing the Host/Origin checks need.
|
||||
|
||||
`HostConnectionHandle.allowsRemoteAuthorities` projects whether any validated `trustedHosts` entry names a non-loopback hostname: a loopback listener behind a proxy can still serve remote browsers, so the directory picker requires a policy without remote authorities before it picks the native chooser.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
@@ -26,6 +28,6 @@ Reachability is the webserver binding's policy (`host: 127.0.0.1 | 0.0.0.0`), an
|
||||
## Consequences
|
||||
|
||||
- Any future `/api` method is covered by construction; there is no per-route trust decision left to forget.
|
||||
- A custom non-loopback composition must trust its serving authorities or requests are refused, then satisfy browser authentication like every loopback request. The shipped CLI rejects `--host 0.0.0.0`; `--trusted-host` only extends the Host/Origin fence and grants no identity.
|
||||
- A non-loopback listener accepts its own bind IP directly ([Bind one concrete Web address](2026-09-19-concrete-web-bind-address.md)); other remote authorities require configured trust. Every accepted authority still requires browser authentication. `--trusted-host` extends only the Host/Origin fence and grants no identity.
|
||||
- Clients must label POST bodies `application/json` (ours always did; raw-fetch tests gained the header).
|
||||
- Host and Origin remain request-routing evidence only. The process token and signed cookie establish the browser identity used by every Host method.
|
||||
@@ -13,9 +13,11 @@ Web GUI 宿主以纯 loopback HTTP 提供 `/api`(默认 `127.0.0.1:3080`;CLI
|
||||
在载体层对整个 `/api` 前缀一次性执行浏览器信任检查——分为两部分:
|
||||
|
||||
- **媒体类型栅栏(dsh-client-connection)**:每个 `/api` POST 必须声明 `application/json`,否则在解析前以 415 拒绝。跨站「简单请求」由此不复存在:任何跨站尝试都被逼进一次本服务器从不应答的 CORS 预检。
|
||||
- **权威栅栏(dsh-client-connection,`src/api-request-trust.ts`)**:每个请求的 `Host` 都必须是回环地址,或与某个 `trustedHosts` 条目匹配(带端口的 `host:port` 条目精确匹配,不带端口的条目匹配任意端口,均经 WHATWG 归一化;rebinding 防御)。刻意不为无标记请求开捷径:明文 HTTP 下浏览器的读取(EventSource、图片、导航——这些头只发给可信目标)既不带 `Origin` 也不带 Fetch-Metadata,因此无标记请求可能是被重绑页面发起且响应可被读走的读取,而 Host 是重绑唯一伪造不了的请求头;非浏览器客户端经由回环地址、推导的 LAN IP 字面量或已声明的权威通过。若带 `Origin` 则必须与 Host 权威完全一致;`sec-fetch-site: cross-site` 一律拒绝。不是单纯规范化 authority 的 `trustedHosts` 条目会导致插件加载失败——否则 WHATWG 解析会悄悄授权笔误里的 hostname,或放大精确端口授权。`host.pickDirectory` 失去专属守卫,与其他请求同栅而行。
|
||||
- **权威栅栏(dsh-client-connection,`src/api-request-trust.ts`)**:每个请求的 `Host` 都必须是回环地址,或与某个 `trustedHosts` 条目匹配(带端口的 `host:port` 条目精确匹配,不带端口的条目匹配任意端口,均经 WHATWG 归一化;rebinding 防御)。刻意不为无标记请求开捷径:明文 HTTP 下浏览器的读取(EventSource、图片、导航——这些头只发给可信目标)既不带 `Origin` 也不带 Fetch-Metadata,因此无标记请求可能是被重绑页面发起且响应可被读走的读取,而 Host 是重绑唯一伪造不了的请求头;非浏览器客户端经由回环地址、已声明的权威,或[绑定一个具体的 Web 地址](2026-09-19-concrete-web-bind-address.zh.md)所接纳的监听器自身绑定字面量通过。若带 `Origin` 则必须与 Host 权威完全一致;`sec-fetch-site: cross-site` 一律拒绝。不是单纯规范化 authority 的 `trustedHosts` 条目会导致插件加载失败——否则 WHATWG 解析会悄悄授权笔误里的 hostname,或放大精确端口授权。`host.pickDirectory` 失去专属守卫,与其他请求同栅而行。
|
||||
|
||||
可达性由 webserver 的绑定配置(`host: 127.0.0.1 | 0.0.0.0`)控制,这道栅栏是混淆代理人防御,而不是身份。Connection 在栅栏之后应用独立的[浏览器令牌认证](2026-08-24-browser-token-authentication.zh.md)。栅栏不检查对端 socket 地址:绑定表达可达性,`trustedHosts` 点名接受的 authority,socket 地址提供不了 Host/Origin 校验需要的额外信息。
|
||||
可达性由 webserver 的绑定配置(`host`;[绑定一个具体的 Web 地址](2026-09-19-concrete-web-bind-address.zh.md)拥有它接受哪些字面量)控制,这道栅栏是混淆代理人防御,而不是身份。Connection 在栅栏之后应用独立的[浏览器令牌认证](2026-08-24-browser-token-authentication.zh.md)。栅栏不检查对端 socket 地址:绑定表达可达性,`trustedHosts` 点名接受的 authority,socket 地址提供不了 Host/Origin 校验需要的额外信息。
|
||||
|
||||
`HostConnectionHandle.allowsRemoteAuthorities` 表示是否有通过校验的 `trustedHosts` 条目命名非回环主机名:代理后的回环监听器也能服务远程浏览器,因此目录选择器要求策略不接纳远程 authority,才会选择原生交互。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
@@ -26,6 +28,6 @@ Web GUI 宿主以纯 loopback HTTP 提供 `/api`(默认 `127.0.0.1:3080`;CLI
|
||||
## 后果
|
||||
|
||||
- 未来任何 `/api` 方法天然在覆盖范围内;不存在会被遗忘的按路由信任决定。
|
||||
- 自定义非 loopback 组合必须信任其服务 authority,否则请求会被拒绝;随后仍像每个 loopback 请求一样满足浏览器认证。随附 CLI 拒绝 `--host 0.0.0.0`;`--trusted-host` 只扩展 Host/Origin 栅栏,绝不授予身份。
|
||||
- 非回环监听器直接接受自己的绑定 IP([绑定一个具体的 Web 地址](2026-09-19-concrete-web-bind-address.zh.md));其他远程 authority 需要配置的信任。每个被接受的 authority 仍须满足浏览器认证。`--trusted-host` 只扩展 Host/Origin 栅栏,绝不授予身份。
|
||||
- 客户端必须给 POST 体标注 `application/json`(我们自己的客户端一向如此;裸 fetch 测试补上了该头)。
|
||||
- Host 与 Origin 仍只是请求路由证据。进程令牌与签名 cookie 建立每个 Host 方法使用的浏览器身份。
|
||||
+4
-4
@@ -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
|
||||
@@ -72,5 +72,5 @@
|
||||
en: 7433bdb522bb2083
|
||||
zh: 545c17613b636f00
|
||||
/agent-note-typert-gateway-targeted-method-calls/consequences:
|
||||
en: c3459afdb606cfbe
|
||||
zh: ec69f2bb39170a7c
|
||||
en: abab48251550f584
|
||||
zh: 2568c46d78dbefdf
|
||||
@@ -215,7 +215,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.
|
||||
|
||||
@@ -520,7 +520,7 @@ A consumer may import a Remote contract that is not currently mounted on the Hos
|
||||
|
||||
Connection's general channel API must suit both the current HTTP carrier and a future WebSocket carrier. If the Client Remote or Gateway exposes `fetch`, an HTTP request, or a route handle, WebSocket migration will pierce the Remote layer again. Those physical objects must therefore remain internal to Connection.
|
||||
|
||||
Remote endpoints use Connection's `trusted-host` authority. Loopback is accepted by default and LAN callers require an explicit trusted-host configuration, but this layer adds no per-method caller authorization; every trusted host can invoke a mounted Remote endpoint.
|
||||
Remote endpoints use Connection's `trusted-host` authority. Loopback and the listener's own bind address are accepted by default; any other LAN authority requires an explicit trusted-host configuration. This layer adds no per-method caller authorization; every accepted authority can invoke a mounted Remote endpoint.
|
||||
|
||||
`hasSeen()` favors strict-definition safety over SRC availability. While a strict descriptor is withdrawn, such as during HMR, the Gateway continues to claim the endpoint and reports it unavailable instead of falling back to a weak SRC descriptor. Re-registration restores it; only a Typert registry restart forgets the historical strict definition.
|
||||
|
||||
|
||||
@@ -215,7 +215,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 既不会读取缺失的声明,也不会针对同一输出并发运行多个生成器。
|
||||
|
||||
@@ -520,7 +520,7 @@ SRC 弱 descriptor 不验证普通 JSON 内部结构。Host Remote 签名变化
|
||||
|
||||
Connection 的通用 channel API 必须同时适合当前 HTTP carrier 和后续 WebSocket carrier。若 Client Remote 或 Gateway 暴露 `fetch`、HTTP request 或 route handle,WebSocket 迁移会再次穿透 Remote 层,因此这些物理对象必须留在 Connection 内部。
|
||||
|
||||
Remote endpoint 使用 Connection 的 `trusted-host` authority。系统默认接受 loopback;LAN 调用方必须通过显式 trusted-host 配置接入,但本层不增加逐方法调用方授权,因此每个 trusted host 都能调用已挂载的 Remote endpoint。
|
||||
Remote endpoint 使用 Connection 的 `trusted-host` authority。系统默认接受 loopback 与监听器自身的绑定地址;其他 LAN authority 必须通过显式 trusted-host 配置接入。本层不增加逐方法调用方授权,因此每个被接受的 authority 都能调用已挂载的 Remote endpoint。
|
||||
|
||||
`hasSeen()` 优先保障 strict definition 的安全性,而非 SRC 可用性。strict descriptor 撤回时(例如 HMR 期间),Gateway 会继续认领 endpoint 并报告不可用,而不会回退到弱 SRC descriptor。重新注册即可恢复;只有重启 Typert 注册表才会忘记历史 strict definition。
|
||||
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
|
||||
+4
-2
@@ -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。后续变更获得新确认,升版本决策需要自身的递增头版本转换。检查点不改变未知事件协议。
|
||||
|
||||
### 写入器与发布真源
|
||||
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
+1
-1
@@ -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:
|
||||
|
||||
|
||||
+1
-1
@@ -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 的总体积减少,才将库纳入基座。
|
||||
|
||||
请求只有两种提供方:
|
||||
|
||||
|
||||
+5
-5
@@ -9,8 +9,8 @@
|
||||
en: 4945ce54f66df55c
|
||||
zh: 9fbd833c1d8122a7
|
||||
/agent-note-deepseek-llm-api-request-extensions-for-session-logs-and-plugin-packages/decision:
|
||||
en: 2388519a1afaac92
|
||||
zh: 6bf758f0560e29a5
|
||||
en: d2452e4a39db0b6b
|
||||
zh: 87f7e0be1f613285
|
||||
/agent-note-deepseek-llm-api-request-extensions-for-session-logs-and-plugin-packages/incremental-session-log-field:
|
||||
en: 4e3138fbdd94259b
|
||||
zh: 8fda801522045b88
|
||||
@@ -21,7 +21,7 @@
|
||||
en: eb69576e756f4aa1
|
||||
zh: 5c1923159da3f7a2
|
||||
/agent-note-deepseek-llm-api-request-extensions-for-session-logs-and-plugin-packages/verification:
|
||||
en: 9935338a0970d09d
|
||||
en: ed730f117365f007
|
||||
zh: a9a36702bc72a9f7
|
||||
/agent-note-deepseek-llm-api-request-extensions-for-session-logs-and-plugin-packages/alternatives-considered:
|
||||
en: 5faf7699272f99b4
|
||||
@@ -33,5 +33,5 @@
|
||||
en: b6eff63f49220dcc
|
||||
zh: 8e4a6c785ed2256a
|
||||
/agent-note-deepseek-llm-api-request-extensions-for-session-logs-and-plugin-packages/consequences:
|
||||
en: 853cd7b21db6208b
|
||||
zh: bfaf459f006fe2db
|
||||
en: fed545acefb91f2a
|
||||
zh: c474a94bb956bbf6
|
||||
+3
-3
@@ -14,7 +14,7 @@ Both values belong only on the official DeepSeek adapter path. Adding them to `G
|
||||
|
||||
## Decision
|
||||
|
||||
`@deepseek-ai/dsh-deepseek-llm-api-extensions` registers `ctx.deepseekLlmApiExtensions`, an additive registry of top-level fields for `deepseek-official` request bodies. A contributor claims one declaration-merged field with `register()`. The adapter invokes `prepare()` after serializing the exact wire messages, passes the request cancellation signal, rejects preparation or base-field collision before HTTP, merges the detached fields, and calls the captured `accept()` transaction after HTTP 2xx. A merged body that fails to serialize is sent without extension fields or acceptance under the [bounded-upload decision](2026-09-24-bounded-session-log-upload.md). The registry stops awaiting preparation after cancellation even if a contributor ignores the signal. Acceptance failures remain request failures under `REQUEST_EXTENSION`; transport and non-2xx failures never accept a contribution. A composition without the registry retains the reusable base adapter. Shipped compositions mount the registry and both contributors: both package metadata and Session-log upload are enabled by default; `session-log-deepseek.enabled: false` disables log upload under the [default-upload decision](2026-09-14-session-log-upload-default.md). Keyless `deepseek-official` replay invokes preparation with a synthetic empty base body and the same acceptance transaction before its first recorded chunk, preserving post-2xx extension side effects rather than field bytes.
|
||||
`@deepseek-ai/dsh-deepseek-llm-api-extensions` registers `ctx.deepseekLlmApiExtensions`, an additive registry of top-level fields for `deepseek-official` request bodies. A contributor claims one declaration-merged field with `register()`. The adapter invokes `prepare()` after serializing the exact wire messages, passes the request cancellation signal, rejects base-field collision before HTTP, merges the detached fields, and calls the captured `accept()` transaction after HTTP 2xx. A merged body that fails to serialize is sent without extension fields or acceptance under the [bounded-upload decision](2026-09-24-bounded-session-log-upload.md). The registry stops awaiting preparation after cancellation even if a contributor ignores the signal. Extension fields are auxiliary and best effort: the registry logs and omits a contributor whose preparation throws, and the adapter logs an acceptance failure without failing the model request; transport and non-2xx failures never accept a contribution. A composition without the registry retains the reusable base adapter. Shipped compositions mount the registry and both contributors: both package metadata and Session-log upload are enabled by default; `session-log-deepseek.enabled: false` disables log upload under the [default-upload decision](2026-09-14-session-log-upload-default.md). Keyless `deepseek-official` replay invokes preparation with a synthetic empty base body and the same acceptance transaction before its first recorded chunk, preserving post-2xx extension side effects rather than field bytes.
|
||||
|
||||
The provider-neutral `llm` package and `llm-pi-ai` contain no extension type, service lookup, field merge, or acceptance call.
|
||||
|
||||
@@ -50,7 +50,7 @@ The process-lifetime manifest-identity cache remains separate because in-process
|
||||
|
||||
## Verification
|
||||
|
||||
Registry tests pin duplicate ownership, effect-scoped disposal, detached field values, concurrent and abortable preparation, receiver-preserving acceptance, one acceptance settlement, and failure aggregation. Session tests pin default-on and explicit-off policies, full-first/suffix-later delivery, direct complete event envelopes independent of base-body messages, incremental watermark folding, persisted restart recovery, fork identity fencing, and out-of-order acceptance. Package-inventory tests pin default-on and explicit-off policies, host and standing-preset discovery, conflicting Loader resolution bases, manifest resolution, lifecycle filtering, and exact name/version ordering. The direct adapter mock proves pre-HTTP preparation failure, cancellation, non-2xx non-acceptance, 2xx acceptance before a later stream failure, and field collision. Keyless replay pins post-2xx extension acceptance, and the TypeScript JSON-RPC plus Python packaged-runtime snapshots project the acceptance event through both SDKs. Real Loader composition pins default package metadata and Session upload plus explicit upload disablement, one real-API request mounts both shipped extensions and proves the official endpoint accepts them, and pi-ai tests retain their unchanged wire requests.
|
||||
Registry tests pin duplicate ownership, effect-scoped disposal, detached field values, concurrent and abortable preparation, receiver-preserving acceptance, one acceptance settlement, and failure aggregation. Session tests pin default-on and explicit-off policies, full-first/suffix-later delivery, direct complete event envelopes independent of base-body messages, incremental watermark folding, persisted restart recovery, fork identity fencing, and out-of-order acceptance. Package-inventory tests pin default-on and explicit-off policies, host and standing-preset discovery, conflicting Loader resolution bases, manifest resolution, lifecycle filtering, and exact name/version ordering. The direct adapter mock proves pre-HTTP preparation rejection, cancellation, non-2xx non-acceptance, 2xx acceptance before a later stream failure, and field collision. Keyless replay pins post-2xx extension acceptance, and the TypeScript JSON-RPC plus Python packaged-runtime snapshots project the acceptance event through both SDKs. Real Loader composition pins default package metadata and Session upload plus explicit upload disablement, one real-API request mounts both shipped extensions and proves the official endpoint accepts them, and pi-ai tests retain their unchanged wire requests.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
@@ -85,7 +85,7 @@ About 98% of the measured v1 real-session events were `assistant/chunk`. Omittin
|
||||
|
||||
## Consequences
|
||||
|
||||
Official DeepSeek requests carry active package versions to their resolved `baseURL`, including configured gateways. Unless Session-log upload is disabled, each eligible request also carries newly unaccepted Session events, up to `maxBytes` per request. The fields are model-hidden and add no prompt tokens or KV-cache changes, but can substantially increase HTTP body size. Manifest resolution, field collision, acceptance logging, or provider schema rejection fails the model request rather than silently dropping metadata. A merged body that fails to serialize is the exception: the request proceeds without extension fields and logs a warning.
|
||||
Official DeepSeek requests carry active package versions to their resolved `baseURL`, including configured gateways. Unless Session-log upload is disabled, each eligible request also carries newly unaccepted Session events, up to `maxBytes` per request. The fields are model-hidden and add no prompt tokens or KV-cache changes, but can substantially increase HTTP body size. Field collision or provider schema rejection fails the model request. A contributor preparation failure, an acceptance failure, or a merged body that fails to serialize logs a warning and lets the request proceed; a package without a usable version reports only its name.
|
||||
|
||||
The `delivery-accepted` event becomes part of the canonical log and is itself delivered on a later request. Crash recovery can duplicate a suffix but does not infer acceptance from assistant output or create a second local cursor store. Direct calls without a live Session omit the session field; host package inventory remains available.
|
||||
|
||||
|
||||
+2
-2
@@ -14,7 +14,7 @@ Status: implemented
|
||||
|
||||
## 决策
|
||||
|
||||
`@deepseek-ai/dsh-deepseek-llm-api-extensions` 注册 `ctx.deepseekLlmApiExtensions`,即 `deepseek-official` 请求正文顶层字段的增量注册表。贡献方通过 `register()` 认领一个经声明合并的字段。适配器在序列化确切协议消息后调用 `prepare()`、传入请求取消信号,在 HTTP 前拒绝准备失败或基础字段冲突,合并分离字段,并在 HTTP 2xx 后调用捕获的 `accept()` 事务。合并后的正文无法序列化时,按[有上限上传决策](2026-09-24-bounded-session-log-upload.zh.md)不带扩展字段发送,也不执行接受。即使贡献方忽略信号,注册表也会在取消后停止等待准备。接受失败仍以 `REQUEST_EXTENSION` 使请求失败;传输失败与非 2xx 失败绝不会接受贡献。未挂载注册表的组合会保留可复用基础适配器。随附组合会挂载注册表与两个贡献方:插件包元数据和会话日志上传均默认开启;按[默认上传决策](2026-09-14-session-log-upload-default.zh.md),设置 `session-log-deepseek.enabled: false` 可关闭日志上传。无密钥 `deepseek-official` 回放会使用合成的空基础正文执行准备,并在第一个已记录分片前调用同一接受事务;它保持的是 2xx 后扩展副作用,而非字段字节。
|
||||
`@deepseek-ai/dsh-deepseek-llm-api-extensions` 注册 `ctx.deepseekLlmApiExtensions`,即 `deepseek-official` 请求正文顶层字段的增量注册表。贡献方通过 `register()` 认领一个经声明合并的字段。适配器在序列化确切协议消息后调用 `prepare()`、传入请求取消信号,在 HTTP 前拒绝基础字段冲突,合并分离字段,并在 HTTP 2xx 后调用捕获的 `accept()` 事务。合并后的正文无法序列化时,按[有上限上传决策](2026-09-24-bounded-session-log-upload.zh.md)不带扩展字段发送,也不执行接受。即使贡献方忽略信号,注册表也会在取消后停止等待准备。扩展字段是辅助信息,按尽力而为处理:贡献方准备时抛错,注册表会记录告警并省略该字段;接受失败时,适配器记录告警而不使模型请求失败;传输失败与非 2xx 失败绝不会接受贡献。未挂载注册表的组合会保留可复用基础适配器。随附组合会挂载注册表与两个贡献方:插件包元数据和会话日志上传均默认开启;按[默认上传决策](2026-09-14-session-log-upload-default.zh.md),设置 `session-log-deepseek.enabled: false` 可关闭日志上传。无密钥 `deepseek-official` 回放会使用合成的空基础正文执行准备,并在第一个已记录分片前调用同一接受事务;它保持的是 2xx 后扩展副作用,而非字段字节。
|
||||
|
||||
提供方无关的 `llm` 包与 `llm-pi-ai` 不包含任何扩展类型、服务查找、字段合并或接受调用。
|
||||
|
||||
@@ -85,7 +85,7 @@ Status: implemented
|
||||
|
||||
## 后果
|
||||
|
||||
DeepSeek 官方请求会把存活包版本发送到解析后的 `baseURL`,包括已配置 gateway。除非关闭会话日志上传,否则符合条件的请求还会携带新的未接受会话事件,每次最多 `maxBytes`。这些字段对模型不可见,不增加提示词 token,也不改变 KV Cache,但可能显著增大 HTTP 正文。Manifest 解析、字段冲突、接受记录或提供方 schema 拒绝会使模型请求失败,而不会静默丢弃元数据。合并后的正文无法序列化是例外:请求不带扩展字段继续发送,并记录一条告警。
|
||||
DeepSeek 官方请求会把存活包版本发送到解析后的 `baseURL`,包括已配置 gateway。除非关闭会话日志上传,否则符合条件的请求还会携带新的未接受会话事件,每次最多 `maxBytes`。这些字段对模型不可见,不增加提示词 token,也不改变 KV Cache,但可能显著增大 HTTP 正文。字段冲突或提供方 schema 拒绝会使模型请求失败。贡献方准备失败、接受失败或合并后的正文无法序列化时,系统记录告警并让请求继续;没有可用版本的包只报告名称。
|
||||
|
||||
`delivery-accepted` 事件会成为权威日志的一部分,并在后续请求中自行交付。崩溃恢复可能重复后缀,但不会根据 assistant 输出推断接受,也不会创建第二份本地游标存储。缺少存活会话的直接调用会省略会话字段;宿主包清单仍然可用。
|
||||
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
+1
-1
@@ -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.
|
||||
|
||||
|
||||
+1
-1
@@ -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 载体。
|
||||
|
||||
|
||||
+10
-10
@@ -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
|
||||
|
||||
+8
-7
@@ -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 |
|
||||
|
||||
+8
-7
@@ -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 |
|
||||
|
||||
+4
-4
@@ -9,8 +9,8 @@
|
||||
en: 01293500639c9d79
|
||||
zh: 6d19e4843955c811
|
||||
/agent-note-browser-launch-token-authentication/decision:
|
||||
en: 063f9698b8a94287
|
||||
zh: 848b2c1134b7a100
|
||||
en: 54efa59339f75256
|
||||
zh: 0f53ead3844151c6
|
||||
/agent-note-browser-launch-token-authentication/verification:
|
||||
en: 189b36d0fc5f3a17
|
||||
zh: 08993cb585204fc0
|
||||
@@ -18,5 +18,5 @@
|
||||
en: 5275c87f19fe6eee
|
||||
zh: ccb922367c4bb3e2
|
||||
/agent-note-browser-launch-token-authentication/consequences:
|
||||
en: 0714a8a7cc74918a
|
||||
zh: 0f3878b09463ab2e
|
||||
en: af55274a2f345cdd
|
||||
zh: 4d1810141ef03bae
|
||||
@@ -14,13 +14,13 @@ The Web Host runs tool-capable Sessions with the current operating-system user's
|
||||
|
||||
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.
|
||||
|
||||
The cookie is a signed, authority-bound bearer. Its deterministic name and signed payload both include the normalized hostname plus port, so one Harness home can run independent Web ports without cookie collisions. The payload carries safe-integer issue and expiry times under an absolute lifetime; `cookieMaxAgeDays` defaults to 30. The cookie is host-only, `Path=/`, `HttpOnly`, and `SameSite=Strict`. It omits `Secure` because the shipped server uses loopback HTTP. There is no logout operation.
|
||||
The cookie is a signed, authority-bound bearer. Its deterministic name and signed payload both include the normalized hostname plus port, so one Harness home can run independent Web ports without cookie collisions. The payload carries safe-integer issue and expiry times under an absolute lifetime; `cookieMaxAgeDays` defaults to 30. The cookie is host-only, `Path=/`, `HttpOnly`, and `SameSite=Strict`. The HTTP listener omits `Secure`. There is no logout operation.
|
||||
|
||||
The HMAC secret is a versioned `grant` record at `client-connection/browser-session` in `ctx.credentials`; the local provider stores it in `$DSH_HOME/.credentials.yaml`. Connection loads or creates the record during activation and retains the secret for synchronous request verification. An active Connection continues using its loaded secret if the durable record changes; the next activation loads the replacement or creates a missing record, so deleting the record and restarting the process revokes every existing cookie. Invalid owner payloads fail loud instead of being replaced. The launch token itself is never persisted and changes on every process start, while an unexpired cookie remains valid across restarts on the same authority.
|
||||
|
||||
The in-page Web Worker preview exposes no network socket. Its page-owned `postMessage` tunnel enters the real route first, then retries a 401 or 403 through the worker-local fetch handler. This keeps Connection interceptors while limiting the authentication bypass to the page that created the Host worker.
|
||||
|
||||
The shipped CLI continues to reject `--host 0.0.0.0`. Authentication does not imply supported network deployment, TLS, forwarding-header interpretation, or proxy configuration.
|
||||
The shipped CLI accepts concrete local bind addresses but rejects wildcard addresses such as `--host 0.0.0.0`. Authentication does not configure TLS, forwarding-header interpretation, or proxies.
|
||||
|
||||
## Verification
|
||||
|
||||
@@ -42,6 +42,8 @@ Unit coverage pins process-token retention across Connection reloads, one secret
|
||||
|
||||
Possession of the browser cookie authorizes the complete tool-capable Host API, matching the authority the Web application exposes after Session creation. `Host` does not grant a higher method tier, and a method migration between API Proxy and Typert Remote cannot change its caller set.
|
||||
|
||||
The persistent secret makes cookies survive restarts but gives a stolen cookie up to the configured absolute lifetime. Deleting the record and restarting the process is the global revocation mechanism; the active Connection intentionally avoids credential-provider work on each request. Omitting `Secure` preserves loopback HTTP and permits plaintext transmission if an operator makes the same cookie authority reachable over an unencrypted network. The startup URL contains a process credential and must be treated as sensitive output; runtime diagnostics do not repeat it.
|
||||
The persistent secret makes cookies survive restarts but gives a stolen cookie up to the configured absolute lifetime. Deleting the record and restarting the process is the global revocation mechanism; the active Connection intentionally avoids credential-provider work on each request. An HTTP listener permits plaintext cookie transmission if its authority is reachable over an unencrypted network. The startup URL contains a process credential and must be treated as sensitive output; runtime diagnostics do not repeat it.
|
||||
|
||||
The decision partially supersedes the authentication deferral and unauthenticated non-loopback consequences in the [browser trust note](2026-07-28-api-browser-trust-boundary.md). That note remains active authority for media-type, Host, Origin, Fetch-Metadata, and configured-authority validation. No active Agent Note is archived: the overlap is partial and both security rules retain future decision value.
|
||||
|
||||
The [native HTTPS listener decision](2026-09-23-native-https-web-listener.md) supersedes the HTTP-only transport and omitted-`Secure` assumptions here. Listener protocol now determines `Secure`; the token exchange, signed-cookie authority, and credential lifetime remain owned by this note.
|
||||
+5
-3
@@ -14,13 +14,13 @@ Web Host 以当前操作系统用户的权限运行具有工具能力的 Session
|
||||
|
||||
每个 Host 进程生成随机启动令牌,并由应用根 context 跨 Connection 热重载保留。`dsh-web-app` 每个进程只打印并打开一次 query 中带该令牌的普通根 URL。`frontend-static` 请求 Connection 授权 index 响应:只有 `GET /?token=...` 会把进程令牌交换为 cookie,再重定向到干净的 `./`;API 路径和 Authorization header 都不接受该令牌。过时令牌如果同时带有有效 cookie,会重定向到干净的 `./`。缺失与无效凭据得到同一份最小 401 响应。非 index 静态资产保持公开。
|
||||
|
||||
cookie 是签名且绑定 authority 的 bearer。确定性名称与签名 payload 都包含规范化 hostname 和 port,因此同一 Harness home 可以在不同 Web port 运行而不发生 cookie 冲突。payload 在绝对有效期内携带安全整数形式的签发与过期时间;`cookieMaxAgeDays` 默认为 30。cookie 是 host-only、`Path=/`、`HttpOnly`、`SameSite=Strict`。随附服务器使用 loopback HTTP,因此不设置 `Secure`。这里没有 logout 操作。
|
||||
cookie 是签名且绑定 authority 的 bearer。确定性名称与签名 payload 都包含规范化 hostname 和 port,因此同一 Harness home 可以在不同 Web port 运行而不发生 cookie 冲突。payload 在绝对有效期内携带安全整数形式的签发与过期时间;`cookieMaxAgeDays` 默认为 30。cookie 是 host-only、`Path=/`、`HttpOnly`、`SameSite=Strict`。HTTP 监听器不设置 `Secure`。这里没有 logout 操作。
|
||||
|
||||
HMAC 密钥是 `ctx.credentials` 中位于 `client-connection/browser-session` 的版本化 `grant` 记录;本地提供方将其存入 `$DSH_HOME/.credentials.yaml`。Connection 在激活期间加载或创建该记录,并保留密钥以同步校验请求。持久记录发生变化后,当前 Connection 继续使用已加载的密钥;下一次激活会加载替换记录或创建缺失记录,因此删除记录并重启进程会撤销全部既有 cookie。无效 owner payload 会明确失败,而不是被覆盖。启动令牌本身绝不持久化并在每次进程启动时变化;未过期 cookie 则能在相同 authority 上跨重启继续有效。
|
||||
|
||||
页内 Web Worker preview 不暴露网络 socket。其由页面持有的 `postMessage` tunnel 先进入真实 route,收到 401 或 403 后再经 worker 本地 fetch handler 重试。这样既保留 Connection interceptor,又把认证绕过限制在创建 Host worker 的页面内。
|
||||
|
||||
随附 CLI 继续拒绝 `--host 0.0.0.0`。认证不代表支持网络部署、TLS、转发 header 解释或代理配置。
|
||||
随附 CLI 接受本机具体绑定地址,但拒绝 `--host 0.0.0.0` 等通配地址。认证不会配置 TLS、转发 header 解释或代理。
|
||||
|
||||
## 验证
|
||||
|
||||
@@ -42,6 +42,8 @@ HMAC 密钥是 `ctx.credentials` 中位于 `client-connection/browser-session`
|
||||
|
||||
持有浏览器 cookie 就能调用完整的工具型 Host API,这与 Web 应用在创建 Session 后暴露的 authority 一致。`Host` 不授予更高的方法层级,方法在 API Proxy 与 Typert Remote 之间迁移也不会改变调用者集合。
|
||||
|
||||
持久密钥使 cookie 跨重启生效,也让被盗 cookie 最多保有配置的绝对有效期。删除记录并重启进程是全局撤销机制;当前 Connection 刻意避免在每个请求上访问凭据提供方。不设置 `Secure` 保留 loopback HTTP,但如果操作者让同一 cookie authority 经未加密网络可达,cookie 会以明文传输。启动 URL 含进程凭据,必须视为敏感输出;运行时诊断不会重复它。
|
||||
持久密钥使 cookie 跨重启生效,也让被盗 cookie 最多保有配置的绝对有效期。删除记录并重启进程是全局撤销机制;当前 Connection 刻意避免在每个请求上访问凭据提供方。如果 HTTP listener 的 authority 经未加密网络可达,cookie 就可能以明文传输。启动 URL 含进程凭据,必须视为敏感输出;运行时诊断不会重复它。
|
||||
|
||||
本决策部分取代[浏览器信任说明](2026-07-28-api-browser-trust-boundary.zh.md)中的认证延期与未认证非 loopback 后果。该说明仍是媒体类型、Host、Origin、Fetch-Metadata 和配置 authority 校验的有效权威。没有 active Agent Note 被归档:重叠只发生在局部,两条安全规则都保有未来决策价值。
|
||||
|
||||
[原生 HTTPS 监听器决策](2026-09-23-native-https-web-listener.zh.md)取代本说明仅支持 HTTP 传输及不设置 `Secure` 的前提。`Secure` 由监听器协议决定;令牌交换、签名 cookie 的 authority 与凭据有效期仍由本说明定义。
|
||||
+4
-4
@@ -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
|
||||
|
||||
+3
-3
@@ -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.
|
||||
|
||||
|
||||
+3
-3
@@ -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 资源 或活跃项目。
|
||||
|
||||
|
||||
@@ -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 消费方记录注册与结算,同时保留这里的运行期事件流和输出归属。
|
||||
+4
-4
@@ -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
|
||||
+2
-2
@@ -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.
|
||||
|
||||
+2
-2
@@ -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 拥有的条目保留独立版本。
|
||||
|
||||
@@ -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 容器;原生主机检查负责限制执行的证据。
|
||||
|
||||
安全证据要求同时尝试同用户连接与替换套接字监听端点,并确认其无法占用预留流、获知流密钥、修改另一执行或收到其明文输出。进程生命周期证据区分直接退出与托管进程范围静止,并测试启动发布前及程序启动后的取消。源码与构建后配置组合检查验证已安装辅助程序/引导安排,不向程序环境传递主机凭据。
|
||||
|
||||
|
||||
+4
-4
@@ -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 限制。
|
||||
|
||||
+6
-6
@@ -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.
|
||||
+3
-3
@@ -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 场景在提交已录制的提示词前刷新空白会话。
|
||||
+2
-2
@@ -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
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
# Bilingual-pair consistency record for 2026-09-19-concrete-web-bind-address.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-19-concrete-web-bind-address.md
|
||||
/agent-note-bind-one-concrete-web-address:
|
||||
en: f70f2b6b24ef16dd
|
||||
zh: 64100dbc55eee33d
|
||||
/agent-note-bind-one-concrete-web-address/problem:
|
||||
en: ffc265e61eadc31b
|
||||
zh: fcc70f0a8817914c
|
||||
/agent-note-bind-one-concrete-web-address/decision:
|
||||
en: 5fe4f2ddba2ee2da
|
||||
zh: bb140a16f056d4b4
|
||||
/agent-note-bind-one-concrete-web-address/alternatives-considered:
|
||||
en: 0e4d475347796821
|
||||
zh: e621e606bc5b1045
|
||||
/agent-note-bind-one-concrete-web-address/consequences:
|
||||
en: ba7a839bca09992d
|
||||
zh: a11171005405d1e2
|
||||
/agent-note-bind-one-concrete-web-address/testing:
|
||||
en: 4bb8f4bf4ad943c3
|
||||
zh: 2e24d56d01b52574
|
||||
@@ -0,0 +1,35 @@
|
||||
# Agent Note: Bind one concrete Web address
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-09-19-concrete-web-bind-address.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
The Web listener accepted `127.0.0.1` or `0.0.0.0`. A wildcard bind exposed every interface of the machine, so the bundle sampled the LAN IPv4 addresses at bind time into a `webRuntime` service and appended them to `trustedHosts`, granting authorities for interfaces the operator never named and mixing inferred reachability into configured trust. A container or workstation that should expose exactly one address had no way to say so, IPv6 addresses were not accepted at all, and a browser dialing the exposed address sent it as `Host` only to be refused unless the sampled list happened to contain it.
|
||||
|
||||
## Decision
|
||||
|
||||
`webserver.config.host` accepts one concrete IPv4 or IPv6 address literal, loopback by default; the unspecified address in every spelling (`0.0.0.0`, `::`, `::ffff:0.0.0.0`, and their expansions) is rejected when the config loads, and the shipped `dsh web` command refuses the same spellings for `--host` before any consumer activates. An IPv6 `%zone` is kept for `listen`; classification through the exported `isWildcardHost` and `isLoopbackHost` reads the address alone.
|
||||
|
||||
The browser Host/Origin fence accepts the listener's own bind IP literal as `Host` on any port, independently of `trustedHosts` and ignoring the zone. A browser dialing that literal presents it as `Host`, the literal is no rebinding target, and the Origin and cross-site checks still apply. DNS names resolving to the address, other interfaces, and proxy authorities still require `--trusted-host`. Connection reads the invocation authorities directly from the `webStartup` provider; the `webRuntime` service and LAN sampling no longer exist.
|
||||
|
||||
The advertised URL uses IPv4 for mapped loopback binds, brackets other IPv6 binds, drops a loopback zone, and requires `--public-url` for a zoned non-loopback bind because a zone cannot appear in a URL. A non-loopback bind serves plain HTTP and warns at startup even when an HTTPS `--public-url` fronts it. `web-runtime` is a required startup entry, so a bind failure fails startup instead of leaving a silent listener-less process. Desktop pins `--host 127.0.0.1`, the address Electron dials.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Keep sampling LAN addresses into `trustedHosts`.** Rejected: the listener already owns the concrete bind address. Sampling grants authorities for other interfaces and mixes inferred reachability with configured trust; direct bind-IP acceptance preserves that distinction without another runtime service.
|
||||
|
||||
**Accept the bind IP only on the bound port.** Rejected: a port-less `trustedHosts` entry already matches any port, a proxy on the same address may listen elsewhere, and the port adds nothing to the rebinding defense the Host check provides.
|
||||
|
||||
**Keep `0.0.0.0` with a warning.** Rejected: binding every interface exposes remote code execution to every network the machine joins, and the operator can name each address that should be exposed.
|
||||
|
||||
## Consequences
|
||||
|
||||
An operator exposes exactly the interfaces named by `--host` and reaches the listener through that literal without further configuration; every other remote authority is a `--trusted-host` decision. Compositions that bound `0.0.0.0` fail at load with a message naming the rejected spelling. IPv6-only hosts are supported end to end, including the printed and opened URL.
|
||||
|
||||
The [native HTTPS listener decision](2026-09-23-native-https-web-listener.md) supersedes this note's HTTP-only transport assumption. The non-loopback warning applies only without TLS; bind-address validation and authority admission remain unchanged.
|
||||
|
||||
## Testing
|
||||
|
||||
`packages/host/webserver/tests/webserver.spec.ts` classifies wildcard and loopback spellings and rejects wildcard config; `packages/bundle/web-app/tests/startup.spec.ts` refuses wildcard `--host` before consumers activate; `packages/client/connection/tests/api-request-trust.host.spec.ts` and `node-half.host.spec.ts` admit the bind literal on any port without admitting other authorities; `packages/bundle/web-app/tests/web-app.spec.ts` covers the bracketed IPv6 URL and the plain-HTTP warning; `apps/cli/tests/profiles/web/tests/public-url.expected.e2e.ts` boots the built profile on a non-loopback and a zoned loopback address; `web-failure-matrix.expected.e2e.ts` and `packages/boot/app-boot/tests/app-boot.spec.ts` report the required `web-runtime` entry.
|
||||
@@ -0,0 +1,35 @@
|
||||
# Agent Note: 绑定一个具体的 Web 地址
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-09-19-concrete-web-bind-address.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
Web 监听器只接受 `127.0.0.1` 或 `0.0.0.0`。通配绑定会暴露机器的每个接口,因此 bundle 在绑定时把 LAN IPv4 地址采样进 `webRuntime` 服务并追加到 `trustedHosts`,为操作者从未点名的接口授予 authority,并把推断的可达性混入配置的信任。应当只暴露一个地址的容器或工作站无法表达这一点,IPv6 地址完全不被接受,浏览器拨号已暴露地址时把它作为 `Host` 发送,却只在采样列表恰好包含它时才被接受。
|
||||
|
||||
## 决策
|
||||
|
||||
`webserver.config.host` 接受一个具体的 IPv4 或 IPv6 地址字面量,默认回环;未指定地址的所有拼写(`0.0.0.0`、`::`、`::ffff:0.0.0.0` 及其展开形式)在配置加载时被拒绝,随附的 `dsh web` 命令在任何消费者激活前对 `--host` 拒绝同样的拼写。IPv6 的 `%zone` 保留给 `listen`;通过导出的 `isWildcardHost` 与 `isLoopbackHost` 分类时只读取地址本身。
|
||||
|
||||
浏览器 Host/Origin 栅栏接受监听器自身的绑定 IP 字面量作为任意端口上的 `Host`,独立于 `trustedHosts` 并忽略 zone。拨号该字面量的浏览器把它作为 `Host` 呈现,该字面量不是重绑目标,Origin 与跨站检查仍然生效。解析到该地址的 DNS 名称、其他接口与代理 authority 仍需 `--trusted-host`。Connection 直接从 `webStartup` 提供方读取调用 authority;`webRuntime` 服务与 LAN 采样不再存在。
|
||||
|
||||
公告 URL 对 mapped 回环绑定使用 IPv4,为其他 IPv6 绑定加方括号、剥离回环 zone,并因 zone 不能出现在 URL 中而要求带 zone 的非回环绑定显式提供 `--public-url`。非回环绑定提供明文 HTTP,即使前置 HTTPS 的 `--public-url` 也会在启动时警告。`web-runtime` 是必需的启动条目,因此绑定失败会使启动失败,而不是留下一个没有监听器的静默进程。Desktop 固定 `--host 127.0.0.1`,即 Electron 拨号的地址。
|
||||
|
||||
## 备选方案
|
||||
|
||||
**继续把采样的 LAN 地址加入 `trustedHosts`。** 否决:监听器已经持有具体绑定地址。采样会授权其他接口的 authority,并把推断的可达性混入配置的信任;直接接受绑定 IP 可以保留两者的区分,无需另设运行时服务。
|
||||
|
||||
**只在绑定端口上接受绑定 IP。** 否决:不带端口的 `trustedHosts` 条目本就匹配任意端口,同一地址上的代理可能监听其他端口,端口对 Host 检查提供的重绑防御没有增益。
|
||||
|
||||
**保留 `0.0.0.0` 并给出警告。** 否决:绑定所有接口会向机器加入的每个网络暴露远程代码执行,而操作者可以逐个点名应暴露的地址。
|
||||
|
||||
## 影响
|
||||
|
||||
操作者只暴露 `--host` 点名的接口,并无需额外配置即可通过该字面量访问监听器;其他每个远程 authority 都是一个 `--trusted-host` 决定。曾绑定 `0.0.0.0` 的组合在加载时失败,并给出点名被拒绝拼写的消息。纯 IPv6 主机得到端到端支持,包括打印与打开的 URL。
|
||||
|
||||
[原生 HTTPS 监听器决策](2026-09-23-native-https-web-listener.zh.md)取代本说明仅支持 HTTP 传输的前提。非回环警告只适用于未启用 TLS 的监听器;绑定地址校验与 authority 接纳保持不变。
|
||||
|
||||
## 测试
|
||||
|
||||
`packages/host/webserver/tests/webserver.spec.ts` 分类通配与回环拼写并拒绝通配配置;`packages/bundle/web-app/tests/startup.spec.ts` 在消费者激活前拒绝通配 `--host`;`packages/client/connection/tests/api-request-trust.host.spec.ts` 与 `node-half.host.spec.ts` 在任意端口上接纳绑定字面量而不接纳其他 authority;`packages/bundle/web-app/tests/web-app.spec.ts` 覆盖带方括号的 IPv6 URL 与明文 HTTP 警告;`apps/cli/tests/profiles/web/tests/public-url.expected.e2e.ts` 在非回环地址与带 zone 的回环地址上启动已构建的 profile;`web-failure-matrix.expected.e2e.ts` 与 `packages/boot/app-boot/tests/app-boot.spec.ts` 报告必需的 `web-runtime` 条目。
|
||||
+2
-2
@@ -30,8 +30,8 @@
|
||||
en: 743f2dceda3072aa
|
||||
zh: f4d53ddc13a4127e
|
||||
/agent-note-projection-cache-read-only-face-matches-the-lifecycle-identity-the-client-store-separates-cached-and-sequenced-rows/decision/the-client-store-separates-cached-and-sequenced-rows:
|
||||
en: 8a4e9e9ac2ada06f
|
||||
zh: b5884535cd934f72
|
||||
en: dd1cc7ac96a5a25d
|
||||
zh: bdea08d20f166e65
|
||||
/agent-note-projection-cache-read-only-face-matches-the-lifecycle-identity-the-client-store-separates-cached-and-sequenced-rows/decision/what-stays-untouched:
|
||||
en: 1cefba02bbfb2647
|
||||
zh: 5a96942565ab9d72
|
||||
|
||||
+1
-1
@@ -97,7 +97,7 @@ Writer classification:
|
||||
| the `projections` block of an `api-session/added` summary (`manager.handleSessionAdded`) | by the block's `kind`; the summary comes from a live session, so in practice `sequenced` |
|
||||
| the history first page's `projections` (`projections.seed` in `session.ts`) | sequenced |
|
||||
| the control baseline, live sessions only (`manager.replaceControlBaseline`) | sequenced |
|
||||
| the `session.projections` result of `refreshProjections` (a body observation) | sequenced |
|
||||
| the `session.projections` result of `refreshProjections` | sequenced for an exact read; cached only when [migration is required](2026-09-29-explicit-session-history-preparation.md) |
|
||||
| push frames (the `projection` frame handled by `manager`) | sequenced |
|
||||
| the `title` after a successful rename (`session.ts`) | sequenced |
|
||||
|
||||
|
||||
+1
-1
@@ -97,7 +97,7 @@ seq 比较只在同一条 Host 连接内发生:`handleConnected` 先整表 `cl
|
||||
| `api-session/added` 摘要的 `projections` block(`manager.handleSessionAdded`) | 按 block 的 `kind`;该摘要来自活会话,实际为 `sequenced` |
|
||||
| history 首页 `projections`(`session.ts` 的 `projections.seed`) | sequenced |
|
||||
| control 基线,仅活会话(`manager.replaceControlBaseline`) | sequenced |
|
||||
| `refreshProjections` 的 `session.projections` 结果(正文观察) | sequenced |
|
||||
| `refreshProjections` 的 `session.projections` 结果 | 准确读取为 sequenced;仅[需要迁移](2026-09-29-explicit-session-history-preparation.zh.md)时为 cached |
|
||||
| 推送 frame(`manager` 处理 `projection` frame) | sequenced |
|
||||
| rename 成功后的 `title`(`session.ts`) | sequenced |
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -129,14 +129,14 @@
|
||||
en: 04e72789417d6e3a
|
||||
zh: b57c5cc68dff45a5
|
||||
/agent-note-remote-duplex-stream-one-kind-of-stream-an-uplink-channel-and-an-invocation-context/consequences:
|
||||
en: 07524e0c2dc22ad5
|
||||
zh: 5bf4f27160d78b43
|
||||
en: 35326fed46e31d74
|
||||
zh: 6f47baa645a1e370
|
||||
/agent-note-remote-duplex-stream-one-kind-of-stream-an-uplink-channel-and-an-invocation-context/testing:
|
||||
en: 5c8188930b9d4fdb
|
||||
zh: 1d94e6563cedde13
|
||||
/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)
|
||||
@@ -139,6 +139,7 @@ export interface RemoteInvocation {
|
||||
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
|
||||
@@ -379,6 +380,7 @@ export type RemoteStreamOpener = (
|
||||
uplink: AsyncIterable<unknown>,
|
||||
peer: PeerScope,
|
||||
control: AbortController,
|
||||
window: RemoteUplinkWindow,
|
||||
) => Promise<AsyncIterable<unknown>>
|
||||
|
||||
// index.ts Config addition
|
||||
@@ -574,10 +576,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`.
|
||||
- **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
|
||||
@@ -595,10 +598,8 @@ Retention cases in `gateway-stream.host.spec.ts` and `gateway.client.spec.ts` co
|
||||
## 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 +607,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)
|
||||
@@ -139,6 +139,7 @@ export interface RemoteInvocation {
|
||||
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
|
||||
@@ -379,6 +380,7 @@ export type RemoteStreamOpener = (
|
||||
uplink: AsyncIterable<unknown>,
|
||||
peer: PeerScope,
|
||||
control: AbortController,
|
||||
window: RemoteUplinkWindow,
|
||||
) => Promise<AsyncIterable<unknown>>
|
||||
|
||||
// index.ts Config addition
|
||||
@@ -574,10 +576,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` 加上打开、关联与事件。
|
||||
- **上行与下行之间没有跨方向顺序保证**;需要请求应答配对的协议自带序号。
|
||||
- **上层协议尚未透出 `send`**:`$stream`、snapshot、journal 的消费者今天只用下行;它们拿到的对象已经是句柄。
|
||||
- **上层监督器仅暴露下行**:`$stream`、snapshot 和 journal 不暴露 `send`;需要写入上行的领域保留该代次的生成物理句柄。
|
||||
- **worker 隧道与 WebSocket 载体行为不同**:没有 inbox 上限,`end` 之后的项丢弃,因为页面到 worker 是同源可信边界。
|
||||
|
||||
## 测试
|
||||
@@ -595,10 +598,8 @@ for await (const reply of stream) replies.push(reply) // ['> a', '> b']
|
||||
## 后续
|
||||
|
||||
- `$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 +607,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` 流承载输入命令、提供者确认和附件专属可写状态;重连绝不重放输入。
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
# Bilingual-pair consistency record for 2026-09-23-best-effort-persistent-ptc.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-23-best-effort-persistent-ptc.md
|
||||
/agent-note-node-ptc-execution-modes-and-in-memory-state:
|
||||
en: 078a89753b06cd6a
|
||||
zh: 4004e5e4bb42f2cc
|
||||
/agent-note-node-ptc-execution-modes-and-in-memory-state/problem:
|
||||
en: e42db40b879fdbf4
|
||||
zh: 6017da9c4da17835
|
||||
/agent-note-node-ptc-execution-modes-and-in-memory-state/decision:
|
||||
en: d2d08c54c6520f02
|
||||
zh: 520a9b3421ff5c65
|
||||
/agent-note-node-ptc-execution-modes-and-in-memory-state/alternatives-considered:
|
||||
en: 396157a9fc293198
|
||||
zh: a061f05b4423e26f
|
||||
/agent-note-node-ptc-execution-modes-and-in-memory-state/consequences:
|
||||
en: 9b9172f4aafe907f
|
||||
zh: 022a6bd52d7caf05
|
||||
@@ -0,0 +1,37 @@
|
||||
# Agent Note: Node PTC execution modes and in-memory state
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-09-23-best-effort-persistent-ptc.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
Interactive automation retains application objects, imported modules and helper functions between model calls. Recreating every object for every program prevents consumers from using ordinary JavaScript references across observations and actions.
|
||||
|
||||
## Decision
|
||||
|
||||
The existing `@deepseek-ai/dsh-ptc-runtime-node` plugin selects its execution mode through `executionMode`. The default `per-call` mode starts a fresh process under the configured file policy. Opt-in `persistent-in-memory` mode keeps a Node REPL process per explicit, object-identity state owner. Both modes provide the same `ctx.ptcRuntime` service. PTC uses the live Agent as owner and awaits release during its context disposal. Changing the configuration reloads the provider and releases its processes. The [sandboxed Node decision](2026-09-11-sandboxed-node-ptc-runtime.md) owns per-call confinement; the [PTC foundation](../feature/2026-06-15-ptc.md) owns tool dispatch and logged model requests.
|
||||
|
||||
Mode names describe state retention rather than a process implementation or source syntax. `per-call` retains state for one invocation; `persistent-in-memory` retains it across invocations until the process or owner ends. `durable` names the deferred ability to save and restore execution state across restarts. It is not an accepted configuration value: a future implementation must define its recovery point, recoverable values and external-resource handling before exposing that mode. The [retained-process permission decision](2026-10-08-persistent-ptc-sandbox.md) owns confinement, policy changes and temporary escalation.
|
||||
|
||||
One npm package and one plugin entry keep existing installation and loading paths stable. Communication, binding proxies, JSON encoding and output limits remain package-local modules. The Node provider delegates retained process ownership to an internal execution object, which registers no additional Cordis service. Per-call and in-memory execution keep their distinct lifecycle rules. `programFormat` describes source evaluation independently of retention: a REPL need not retain state. The runtime owns syntax, completion and retention instructions through `executionInstructions`; tools own call arguments, SDK declarations and image attachment. Workflow uses `programFormat` to wrap its program and refuses restricted file policies when the selected runtime cannot enforce them.
|
||||
|
||||
The Session records submitted programs, nested tool calls and returned output. It does not serialize the REPL heap or rerun earlier programs after restart, resume or fork: replaying automation could repeat external side effects. A missing variable is a normal program error, and the model-visible instructions require recreating variables and reacquiring resources. No environment identifier or new durable Session event is required because the provider exposes no unlogged model input.
|
||||
|
||||
Cancellation, deadline expiry, protocol or output-limit failure, owner disposal and process exit discard the environment. Ordinary exceptions retain earlier mutations. A retained function uses the active evaluation's host bindings; asynchronous work from an earlier evaluation cannot acquire a later evaluation's tool authority or output sink.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
Separate npm providers plus a shared library allow independent distribution, at the cost of another package, dependency declarations and cross-package exports for shared implementation details. The two Node modes have no current need for separate release schedules or independent dependencies. A second plugin subpath permits separate loading but asks deployments to replace plugin entries to select an execution policy. A configuration field expresses that selection while preserving the existing entry. Independent distribution or substantially different dependencies can justify revisiting this choice.
|
||||
|
||||
A new Cordis service for common helpers would require another registration and injected dependency without adding an independently replaceable runtime capability. Internal functions and classes provide the required reuse. A common execution superclass would also couple fresh-process cleanup with owner-retained lifetimes; the provider shares mechanisms without merging those lifecycles.
|
||||
|
||||
Serializing arbitrary JavaScript objects cannot preserve native resources and closures. Replaying prior programs can repeat writes and input actions. Both would imply recovery guarantees that in-memory execution does not offer.
|
||||
|
||||
A realm inside the Harness process shares process lifetime with model code and cannot terminate a synchronous loop independently. A managed child process permits cancellation and resource cleanup while retaining imports and variables during healthy use. Process ownership alone does not provide a file sandbox or multi-tenant isolation.
|
||||
|
||||
## Consequences
|
||||
|
||||
Selecting `persistent-in-memory` retains execution state; the [permission decision](2026-10-08-persistent-ptc-sandbox.md) defines its file restrictions. Nested tools keep their normal authorization and logging. Memory and native handles may disappear without a Harness restart. The model must recover from reference or resource errors; the runtime never retries a program automatically.
|
||||
|
||||
An idle owner retains a process with no expiration timer. Callers release owners explicitly; ownerless service calls use temporary processes. Programs use REPL statements and a final expression rather than top-level return, and calls for one retained owner cannot overlap. Tool schemas and program guidance follow the selected mode.
|
||||
@@ -0,0 +1,37 @@
|
||||
# Agent Note: Node PTC 执行模式与内存状态
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-09-23-best-effort-persistent-ptc.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
交互式自动化需要在模型调用之间保留应用对象、导入模块和辅助函数。如果每次执行都重新创建对象,消费者就无法在观察与操作之间继续使用普通 JavaScript 引用。
|
||||
|
||||
## 决策
|
||||
|
||||
现有 `@deepseek-ai/dsh-ptc-runtime-node` 插件通过 `executionMode` 选择执行模式。默认的 `per-call` 模式按配置的文件策略启动新进程。显式启用的 `persistent-in-memory` 模式为每个以对象身份标识的状态所有者保留一个 Node REPL 进程。两种模式都提供同一个 `ctx.ptcRuntime` 服务。PTC 使用存活的 Agent 作为所有者,在其上下文销毁时等待释放。修改配置会重载提供方并释放其进程。[Node 沙箱决策](2026-09-11-sandboxed-node-ptc-runtime.zh.md)负责逐次执行的文件约束,[PTC 基础决策](../feature/2026-06-15-ptc.zh.md)负责工具分派与模型请求日志。
|
||||
|
||||
模式名称描述状态保留方式,不绑定进程实现或代码语法。`per-call` 只为一次调用保留状态;`persistent-in-memory` 跨调用保留状态,直到进程或所有者结束。`durable` 用于命名未来跨重启保存与恢复执行状态的能力。它不是可接受的配置值:实现必须先明确恢复点、可恢复的值以及外部资源的处理方式,才能公开这个模式。[常驻进程权限决策](2026-10-08-persistent-ptc-sandbox.zh.md)负责文件约束、权限切换与临时提权。
|
||||
|
||||
一个 npm 包和一个插件入口保持现有安装与加载路径稳定。通信、绑定代理、JSON 编码和输出限制继续作为包内模块复用。Node 提供方将保留进程的所有权交给内部执行对象,该对象不注册额外的 Cordis 服务。逐次执行和内存保留各自管理不同的生命周期。`programFormat` 描述代码求值形式,与状态保留独立:REPL 不必跨调用保留状态。运行时通过 `executionInstructions` 负责语法、完成值和状态保留指引;工具层负责调用参数、SDK 声明和图片附加。工作流根据 `programFormat` 包装程序;所选运行时无法执行受限文件策略时,工作流拒绝执行。
|
||||
|
||||
Session 记录提交的程序、嵌套工具调用和返回输出。它不序列化 REPL 堆,也不在重启、恢复或派生后重新执行旧程序,因为重放自动化可能重复外部操作。缺失变量产生普通程序错误,模型指引要求重新创建变量并获取资源。提供方没有引入日志之外的模型输入,因此不需要环境标识或新的持久 Session 事件。
|
||||
|
||||
取消、截止时间到期、协议失败、输出超限、所有者销毁和进程退出都会丢弃环境。普通异常保留此前发生的修改。保留的函数使用当前执行的宿主绑定;较早执行中的异步任务不能获取后续执行的工具权限或向其输出。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
将提供方拆成独立 npm 包并增加共享库,可以分别分发,但需要维护额外的包、依赖声明和共享实现的跨包导出。两种 Node 模式目前没有不同发布节奏或独立依赖的需求。第二个插件子路径允许独立加载,但要求部署配置通过替换插件入口来选择执行策略。配置字段直接表达这个选择,并保留已有入口。需要独立分发或出现明显不同的依赖时,可以重新评估拆包。
|
||||
|
||||
为公共辅助代码新增 Cordis 服务,会增加注册和注入依赖,却没有提供独立替换的运行能力。内部函数和类已经能够完成复用。共同的执行基类还会把新进程清理与按所有者保留进程的生命周期耦合起来,因此提供方共用处理机制,各自管理生命周期。
|
||||
|
||||
序列化任意 JavaScript 对象无法保留原生资源和闭包。重放旧程序可能重复写入和输入操作。这两种做法都会暗示内存保留模式没有提供的恢复保证。
|
||||
|
||||
Harness 进程内的独立执行上下文与模型代码共享进程生命周期,无法单独终止同步死循环。托管子进程可以在正常使用时保留导入和变量,同时支持取消与资源清理。仅有进程所有权不能提供文件沙箱或多租户隔离。
|
||||
|
||||
## 影响
|
||||
|
||||
选择 `persistent-in-memory` 会保留执行状态;[权限决策](2026-10-08-persistent-ptc-sandbox.zh.md)定义其文件限制。嵌套工具仍遵循原有授权和日志规则。即使 Harness 没有重启,内存和原生句柄也可能丢失。模型需要从引用或资源错误中恢复,运行时不会自动重试程序。
|
||||
|
||||
空闲所有者保留一个没有过期计时器的进程。调用方显式释放所有者,没有所有者的服务调用使用临时进程。程序使用 REPL 语句和末尾表达式,不支持顶层 return;同一保留所有者的调用不能重叠。工具 schema 和程序指引跟随所选模式。
|
||||
@@ -0,0 +1,22 @@
|
||||
# Bilingual-pair consistency record for 2026-09-23-native-https-web-listener.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-23-native-https-web-listener.md
|
||||
/agent-note-native-https-on-the-web-listener:
|
||||
en: bdb6e7794af113f0
|
||||
zh: b9f325b0cbe8d34d
|
||||
/agent-note-native-https-on-the-web-listener/problem:
|
||||
en: 685a00e6f84f642d
|
||||
zh: 404e463f56ec5a1f
|
||||
/agent-note-native-https-on-the-web-listener/decision:
|
||||
en: 7f9ea8a4f2577d2f
|
||||
zh: e9b2a1fc6944428e
|
||||
/agent-note-native-https-on-the-web-listener/alternatives-considered:
|
||||
en: 44c051936dd2529e
|
||||
zh: 7588669ec4c302ef
|
||||
/agent-note-native-https-on-the-web-listener/consequences:
|
||||
en: 6a8be07b24f957ba
|
||||
zh: fbf48476e092dfc8
|
||||
/agent-note-native-https-on-the-web-listener/testing:
|
||||
en: f92f7ec224dba94d
|
||||
zh: 0f0e0304c834f0af
|
||||
@@ -0,0 +1,41 @@
|
||||
# Agent Note: Native HTTPS on the Web listener
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-09-23-native-https-web-listener.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
A directly reached Web listener needs to encrypt launch tokens and session cookies without requiring a separate reverse proxy. Advertising an HTTPS URL does not encrypt the listener or establish certificate trust.
|
||||
|
||||
## Decision
|
||||
|
||||
WebServer owns optional TLS through `tls: { certFile, keyFile }`, exposed by `dsh web` as paired `--tls-cert` and `--tls-key` flags. Certificate and unencrypted private-key PEM files are loaded before binding; unreadable, empty, invalid, or mismatched material fails activation without an HTTP fallback. HTTP remains the default. Both transports share routing, upgrades, and disposal; disposal also owns incomplete TLS handshakes.
|
||||
|
||||
`webServer.protocol` is the source for the bind-address URL, the non-loopback plain-HTTP warning, and Connection's `Secure` cookie attribute. Neither forwarded headers nor `publicUrl` control encryption or that attribute. Explicit Host-port grants normalize under the listener's scheme, while Origin comparison uses the Origin's HTTP(S) scheme so HTTPS proxies can retain HTTP upstreams. TLS grants no additional Host authority and does not replace authentication.
|
||||
|
||||
The API authority check does not equate a proxy's upstream transport with the browser page's scheme. An HTTPS listener can accept a matching HTTP Origin, but `sec-fetch-site: cross-site` still rejects the request and authentication remains required. Schemeful `SameSite=Strict` cookies prevent cross-scheme browser credential delivery; this is not support for serving a public browser frontend over HTTP with a TLS upstream.
|
||||
|
||||
HTTP cookie audiences retain their existing spelling so enabling TLS support does not invalidate unrelated HTTP sessions. HTTPS cookie names and signed audiences include the `https:` scheme, preventing replay of an HTTP credential on any HTTPS port even if the cookie is renamed. Default ports follow URL normalization without a separate port-443 spelling.
|
||||
|
||||
Certificate issuance, trust, renewal, and replacement belong to the operator. Files are read once per activation; replacement requires a listener reload or process restart. The certificate must cover the hostname or IP the TLS client dials. The default port remains 3080; `--port` selects another port, subject to operating-system permission.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Require a TLS-terminating proxy.** This remains supported, but requires another service for a directly reached listener.
|
||||
|
||||
**Derive transport or cookie security from `publicUrl` or forwarding headers.** An advertisement does not describe the receiving socket, and request headers cannot establish trusted proxy identity. Native listener state keeps this decision independent of proxy policy.
|
||||
|
||||
**Issue, renew, or generate certificates.** This would add certificate lifecycle and trust-distribution responsibilities. An automatically generated certificate would not establish browser trust.
|
||||
|
||||
**Fall back to HTTP on certificate errors.** This would expose credentials despite an explicit request for encryption; activation must fail instead.
|
||||
|
||||
## Consequences
|
||||
|
||||
Direct HTTPS needs no TLS-terminating proxy. An HTTPS proxy in front of an HTTP listener still owns external cookie rewriting. An HTTPS listener emits `Secure` cookies, so public browser-facing deployments must retain HTTPS rather than depend on local-browser exceptions for HTTP. Deployment procedures belong to the [public deployments guide](../../../../docs/user/guide/public-deployments.md).
|
||||
|
||||
This partially supersedes the HTTP-only assumptions in [browser launch-token authentication](2026-08-24-browser-token-authentication.md) and [concrete bind addresses](2026-09-19-concrete-web-bind-address.md). Those notes remain active for cookie payloads and lifetimes, and bind admission respectively.
|
||||
|
||||
## Testing
|
||||
|
||||
The WebServer suite covers verified HTTPS, invalid material, and disposal. Connection covers listener-owned cookie attributes, cross-protocol credential replay, and exact-port grants; Web startup rejects unpaired flags. The built CLI's `public-url.expected.e2e.ts` authenticates over verified TLS with a `Secure` cookie while retaining HTTP and proxy cases; its failure matrix verifies unreadable TLS material exits without opening a listener. Desktop's startup suite covers matching WS/WSS credentials and refusal on a different transport.
|
||||
@@ -0,0 +1,41 @@
|
||||
# Agent Note: Web 监听器原生 HTTPS
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-09-23-native-https-web-listener.md) | 中文
|
||||
|
||||
## Problem
|
||||
|
||||
浏览器直接访问的 Web 监听器需要在不依赖独立反向代理的情况下加密启动 token 与会话 cookie。公告 HTTPS URL 不会加密监听器,也不会建立证书信任。
|
||||
|
||||
## Decision
|
||||
|
||||
WebServer 通过 `tls: { certFile, keyFile }` 拥有可选 TLS,由 `dsh web` 的成对 `--tls-cert` 与 `--tls-key` flag 暴露。证书与不带口令的私钥 PEM 文件在绑定前加载;材料不可读、为空、无效或不匹配时激活失败,绝不回退到 HTTP。HTTP 仍为默认值。两种传输共用路由、升级与释放;释放也负责未完成的 TLS 握手。
|
||||
|
||||
`webServer.protocol` 决定绑定地址 URL、非 loopback 明文 HTTP 警告及 Connection 的 `Secure` cookie 属性。转发 header 与 `publicUrl` 都不控制加密或该属性。显式 Host 端口授权按监听器的 scheme 归一化,Origin 比较则按 Origin 的 HTTP(S) scheme 进行,以允许 HTTPS 代理保留 HTTP 上游。TLS 不授予额外的 Host authority,也不替代认证。
|
||||
|
||||
API authority 校验不会把代理的上游传输等同于浏览器页面的 scheme。HTTPS 监听器可接受 authority 匹配的 HTTP Origin,但 `sec-fetch-site: cross-site` 仍会拒绝请求,且请求仍须认证。区分 scheme 的 `SameSite=Strict` cookie 阻止浏览器跨 scheme 携带凭据;这不表示支持以 HTTP 向公开浏览器前端提供页面、同时使用 TLS 上游。
|
||||
|
||||
HTTP cookie audience 保留既有写法,避免启用 TLS 支持时使无关的 HTTP 会话失效。HTTPS cookie 名称与签名 audience 包含 `https:` scheme,因此即使修改 cookie 名称,HTTP 凭据也不能在任何 HTTPS 端口上重放。默认端口遵循 URL 归一化,不为 443 端口另设写法。
|
||||
|
||||
证书签发、信任、续期与更换归操作者负责。每次激活只读取一次文件;更换后需重新加载监听器或重启进程。证书必须覆盖 TLS 客户端拨号的 hostname 或 IP。默认端口仍为 3080;`--port` 可选择其他端口,但受操作系统权限约束。
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**要求终止 TLS 的代理。** 仍然支持,但会为直接访问的监听器增加一个服务。
|
||||
|
||||
**根据 `publicUrl` 或转发 header 推导传输或 cookie 安全属性。** 公告不描述接收请求的 socket,请求 header 也不能建立可信代理身份。原生监听器状态使这一决策独立于代理策略。
|
||||
|
||||
**签发、续期或生成证书。** 这会增加证书生命周期与信任分发职责。自动生成证书本身不能建立浏览器信任。
|
||||
|
||||
**证书错误时回退到 HTTP。** 这会在操作者明确要求加密时暴露凭据;激活必须失败。
|
||||
|
||||
## Consequences
|
||||
|
||||
直接 HTTPS 不需要终止 TLS 的代理。HTTP 监听器前的 HTTPS 代理仍负责外部 cookie 重写。HTTPS 监听器签发 `Secure` cookie,因此公开部署的浏览器侧必须保持 HTTPS,而不应依赖本地浏览器对 HTTP 的例外。部署步骤归[公开部署指南](../../../../docs/user/guide/public-deployments.zh.md)所有。
|
||||
|
||||
本决策部分取代[浏览器启动 token 认证](2026-08-24-browser-token-authentication.zh.md)与[具体绑定地址](2026-09-19-concrete-web-bind-address.zh.md)中仅支持 HTTP 的前提。这些说明分别继续定义 cookie 载荷与有效期及绑定准入,保持活跃。
|
||||
|
||||
## Testing
|
||||
|
||||
WebServer 套件覆盖校验证书的 HTTPS、无效材料与释放。Connection 覆盖由监听器决定的 cookie 属性、跨协议凭据重放和精确端口授权;Web 启动拒绝未配对的 flag。已构建 CLI 的 `public-url.expected.e2e.ts` 使用 `Secure` cookie 经校验证书的 TLS 完成认证,并保留 HTTP 与代理用例;失败矩阵验证 TLS 材料不可读时进程退出且不打开监听器。Desktop 启动套件覆盖匹配的 WS/WSS 凭据及不同传输下的拒绝。
|
||||
+2
-2
@@ -15,5 +15,5 @@
|
||||
en: 88985b4e1f5c7a4f
|
||||
zh: 32617f7ff75ee825
|
||||
/agent-note-bounded-deepseek-session-log-upload/consequences:
|
||||
en: e19fb6136ffd2250
|
||||
zh: 01dacd7a44c3f837
|
||||
en: 288834c514df377a
|
||||
zh: a33a4af2b4d9ebf4
|
||||
@@ -25,6 +25,6 @@ This supersedes complete-suffix delivery and fail-closed serialization in the [r
|
||||
|
||||
## Consequences
|
||||
|
||||
A backlog drains at up to 8 MiB per accepted request by default. At the end-to-end rate measured for the 269 MiB upload, about 8 MiB/s, an 8 MiB field adds about 1 s to each request while a backlog drains, and a 0.86 GB backlog drains in about 100 requests. In the same 96,341-event Session, the median per-step append is 11 KiB and the largest event is 414 KiB, so ordinary steps stay far below the limit and only an unusual event or a small configured limit reaches the oversized-event path. While a backlog drains, the endpoint receives a `throughSeq` behind the Session's current tip. Preparation failures, acceptance failures, and provider rejections still fail the model request.
|
||||
A backlog drains at up to 8 MiB per accepted request by default. At the end-to-end rate measured for the 269 MiB upload, about 8 MiB/s, an 8 MiB field adds about 1 s to each request while a backlog drains, and a 0.86 GB backlog drains in about 100 requests. In the same 96,341-event Session, the median per-step append is 11 KiB and the largest event is 414 KiB, so ordinary steps stay far below the limit and only an unusual event or a small configured limit reaches the oversized-event path. While a backlog drains, the endpoint receives a `throughSeq` behind the Session's current tip. Provider rejections still fail the model request; preparation and acceptance failures are logged and do not.
|
||||
|
||||
Plugin tests cover the exact byte boundary, UTF-8 counting, backlog draining across accepted requests, the oversized-event warning with its unchanged watermark, an event past the string limit, and the 8 MiB default through plugin configuration. Adapter tests cover the base-body fallback through the provider plugin's logger and through an adapter without a reporter.
|
||||
@@ -25,6 +25,6 @@ DeepSeek 适配器把合并后请求正文的序列化失败视为扩展失败
|
||||
|
||||
## 后果
|
||||
|
||||
默认情况下,积压按每次已接受请求最多 8 MiB 补传。按那次 269 MiB 上传实测的端到端速度(约 8 MiB/s),补传期间 8 MiB 的字段让每次请求多约 1 秒,0.86 GB 的积压约 100 次请求补完。在同一个 96,341 条事件的会话中,每步追加量的中位数是 11 KiB,最大单条事件是 414 KiB,因此普通步骤远低于上限,只有异常事件或很小的配置上限才会走到超限事件路径。积压补传期间,端点收到的 `throughSeq` 会落后于会话当前末尾。准备失败、接受失败与提供方拒绝仍会使模型请求失败。
|
||||
默认情况下,积压按每次已接受请求最多 8 MiB 补传。按那次 269 MiB 上传实测的端到端速度(约 8 MiB/s),补传期间 8 MiB 的字段让每次请求多约 1 秒,0.86 GB 的积压约 100 次请求补完。在同一个 96,341 条事件的会话中,每步追加量的中位数是 11 KiB,最大单条事件是 414 KiB,因此普通步骤远低于上限,只有异常事件或很小的配置上限才会走到超限事件路径。积压补传期间,端点收到的 `throughSeq` 会落后于会话当前末尾。提供方拒绝仍会使模型请求失败;准备失败与接受失败只记录告警,不会使请求失败。
|
||||
|
||||
插件测试覆盖精确字节边界、UTF-8 计数、跨已接受请求的积压补传、超限事件告警及其不变的水位、超过字符串上限的事件,以及经插件配置生效的 8 MiB 默认值。适配器测试覆盖经提供方插件 logger 报告的基础正文回退,以及没有 reporter 的适配器上的同一回退。
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
# Bilingual-pair consistency record for 2026-09-24-deepseek-account-remote-model-catalog.md (docs/i18n/README.md): per heading
|
||||
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
|
||||
# After editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-24-deepseek-account-remote-model-catalog.md
|
||||
/agent-note-deepseek-account-models-come-from-the-account-listing-and-gate-by-client-version:
|
||||
en: e6d82714ac89a7ae
|
||||
zh: 54c14278e749e7e1
|
||||
/agent-note-deepseek-account-models-come-from-the-account-listing-and-gate-by-client-version/problem:
|
||||
en: 2eeb916eb65c66e7
|
||||
zh: 7bf5c0abb8bb61e4
|
||||
/agent-note-deepseek-account-models-come-from-the-account-listing-and-gate-by-client-version/decision:
|
||||
en: 42e800c4cd3b6ebe
|
||||
zh: 06fabe5a9681fa27
|
||||
/agent-note-deepseek-account-models-come-from-the-account-listing-and-gate-by-client-version/alternatives-considered:
|
||||
en: 7c7880fdb0cc95f6
|
||||
zh: 2b8a539192626789
|
||||
/agent-note-deepseek-account-models-come-from-the-account-listing-and-gate-by-client-version/consequences:
|
||||
en: 352ece6c2decaaf7
|
||||
zh: 1d0c3620fd26a35c
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
# Agent Note: DeepSeek account models come from the account listing and gate by client version
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-09-24-deepseek-account-remote-model-catalog.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
The account route's catalog was deployment configuration: a build advertised the models it shipped with, a new model reached users only through a release, and nothing kept an older build from requesting a model it cannot drive. The account service publishes per-account model metadata at `GET /models`, and that listing can name models an older build does not understand.
|
||||
|
||||
## Decision
|
||||
|
||||
- [`llm-deepseek-account`](../../../../packages/llm/llm-deepseek-account/README.md) loads the listing lazily for each credential, waits for it once up to `modelsTimeoutMs`, refreshes it in the background after `modelsMaxAgeMs`, keeps the last good listing when a load fails or returns no model, and serves the bundled catalog before any listing loads. The cache lives in memory and is keyed by endpoint and token fingerprint, so per-account rollouts stay separate.
|
||||
- A listed model is available when its `client_requirements["deepseek-harness"].min_version` is at most the current DeepSeek Harness version in semantic-version order. Without that declaration, only ids the built-in catalog knows are available; the others stay hidden, because a client cannot tell which future version supports them. A request for a refused listed model fails before network I/O with `CLIENT_UPGRADE_REQUIRED` or `MODEL_UNSUPPORTED`.
|
||||
- Listed capabilities replace bundled ones field by field; the bundled entry keeps client-side knowledge such as tool-update mode and image budgets; `modelOverrides` applies last and reuses the `models` entry fields and validation. `max_output_tokens` bounds the request default instead of becoming it.
|
||||
- The shared transport gains only two provider hooks, `connect` and `admit`; listing, gating, and merging stay in the account package.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Server-side filtering by client version only.** Codex sends `client_version` and lets its backend drop models. An older build then cannot show that a newer version supports a model, and a listing that omits a declaration would expose an unknown model.
|
||||
|
||||
**Default parameters for unknown models.** Reasonix and Pi drive unknown ids with generic defaults, and MiniMax treats that as a defect. A model that needs client changes would run with the wrong behavior.
|
||||
|
||||
**Whole-listing validation.** Codex, ZCode, and Kimi reject the whole listing on one bad field. One malformed row would then send every account back to the bundled catalog.
|
||||
|
||||
**Persisting the last good listing.** A durable cache adds a persistence format and its migration duty; the bundled catalog already covers offline starts.
|
||||
|
||||
**Keeping `models` with merge-by-id semantics.** The API-key route's `models` replaces its catalog, so the same field name with merge semantics would invert its meaning between sibling routes.
|
||||
|
||||
**Generalizing the shared transport.** Per-model effort levels, an output-capability field, and catalog-change announcements would widen the Messages package for one consumer. The listed effort levels match the bundled defaults, and a model that needs different ones needs a release, which the gate enforces.
|
||||
|
||||
## Consequences
|
||||
|
||||
- An older build never offers a model it does not know unless the service declares a minimum version that build satisfies, and the service must declare `min_version` for a future build to show that an upgrade enables a model.
|
||||
- A modified client can still request any id; the service enforces version floors only if it rejects such requests itself.
|
||||
- The first account access after sign-in may wait up to `modelsTimeoutMs`; an offline start uses bundled capabilities until a listing loads; a listing change during a running process reaches the Web picker at its next catalog refresh.
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
# Agent Note: DeepSeek 账号模型来自账号列表,并按客户端版本门控
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-09-24-deepseek-account-remote-model-catalog.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
账号路由的目录原本是部署配置:每个构建只提供它发布时自带的模型,新模型只能随发版到达用户,也没有办法阻止旧构建请求它驱动不了的模型。账号服务在 `GET /models` 上按账号发布模型元数据,这份列表可能包含旧构建不认识的模型。
|
||||
|
||||
## 决策
|
||||
|
||||
- [`llm-deepseek-account`](../../../../packages/llm/llm-deepseek-account/README.zh.md) 按凭据懒加载列表,首次访问最多等待 `modelsTimeoutMs`,超过 `modelsMaxAgeMs` 后在后台刷新;获取失败或返回的列表中没有模型时保留上一份,还没有加载到任何列表时使用内置目录。缓存只在内存中,以端点和 token 指纹为键,因此按账号灰度互不干扰。
|
||||
- 列出的模型在其 `client_requirements["deepseek-harness"].min_version` 按 semver 顺序不高于当前 DeepSeek Harness 版本时可用。没有这个声明时,只有内置目录认识的 id 可用,其余保持隐藏,因为客户端无法判断将来哪个版本会支持它们。请求被拒绝的已列出模型时,在网络请求之前以 `CLIENT_UPGRADE_REQUIRED` 或 `MODEL_UNSUPPORTED` 失败。
|
||||
- 列表中的能力逐字段覆盖内置能力;内置条目保留工具更新模式、图片预算等客户端知识;`modelOverrides` 最后叠加,复用 `models` 条目的字段与校验。`max_output_tokens` 只限制请求默认值,不直接成为默认值。
|
||||
- 共享传输层只新增 `connect` 与 `admit` 两个提供方钩子;列表、门控与合并都留在账号包中。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
**只在服务端按客户端版本过滤。** Codex 发送 `client_version`,由后端去掉模型。这样旧构建无法提示新版本支持某个模型,而且列表漏掉声明时会暴露未知模型。
|
||||
|
||||
**未知模型套用默认参数。** Reasonix 和 Pi 用通用默认值驱动未知 id,MiniMax 把这种做法当作缺陷。需要客户端改动的模型会以错误的方式运行。
|
||||
|
||||
**整份列表严格校验。** Codex、ZCode 和 Kimi 会因为一个坏字段拒绝整份列表。一条格式错误的记录就会让所有账号退回内置目录。
|
||||
|
||||
**持久化上一次成功的列表。** 持久缓存要新增持久化格式并承担迁移责任;内置目录已经覆盖离线启动。
|
||||
|
||||
**保留 `models` 并改为按 id 合并。** API key 路由的 `models` 会替换整个目录,同名字段在兄弟路由上改成合并语义会让含义相反。
|
||||
|
||||
**扩展共享传输层。** 按模型的推理档位、输出能力字段和目录变更通知,会为一个使用方扩大 Messages 包的范围。列表中的推理档位与内置默认值一致;需要不同档位的模型需要发版适配,门控正是用来保证这一点。
|
||||
|
||||
## 影响
|
||||
|
||||
- 旧构建永远不会提供它不认识的模型,除非服务端声明了该构建满足的最低版本;服务端必须声明 `min_version`,将来的构建才能提示升级后可用。
|
||||
- 改过的客户端仍然可以请求任意 id;只有服务端自己拒绝这类请求时,版本下限才会被强制执行。
|
||||
- 登录后的第一次账号访问最多等待 `modelsTimeoutMs`;离线启动在加载到列表之前使用内置能力;进程运行中途的列表变化要等 Web 选择器下一次刷新目录时才会显示。
|
||||
@@ -0,0 +1,19 @@
|
||||
# Bilingual-pair consistency record for 2026-09-27-progressive-office-skills.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-27-progressive-office-skills.md
|
||||
/agent-note-progressive-office-skills-with-ordinary-shell-execution:
|
||||
en: 42e2f062c37e9ef1
|
||||
zh: 5cd3d421adec1cac
|
||||
/agent-note-progressive-office-skills-with-ordinary-shell-execution/problem:
|
||||
en: a9984a594cdac98d
|
||||
zh: 38768e7699bc8d39
|
||||
/agent-note-progressive-office-skills-with-ordinary-shell-execution/decision:
|
||||
en: d057c558d6b9803c
|
||||
zh: acf3fdbc4cd7cee3
|
||||
/agent-note-progressive-office-skills-with-ordinary-shell-execution/alternatives-considered:
|
||||
en: 8bca955255e6d982
|
||||
zh: bc313a4bcab3f6f9
|
||||
/agent-note-progressive-office-skills-with-ordinary-shell-execution/consequences:
|
||||
en: cd4ff2423b2f5f9e
|
||||
zh: 63135aeae9c46415
|
||||
@@ -0,0 +1,32 @@
|
||||
# Agent Note: Progressive Office skills with ordinary shell execution
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-09-27-progressive-office-skills.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
Combined authoring and rendering instructions spend context on operations unrelated to ordinary editing. Rendering tool activation adds a second discovery path and durable state even though shell tools already own execution permissions and process management. A missing renderer can prevent loading otherwise usable editing guidance.
|
||||
|
||||
## Decision
|
||||
|
||||
|
||||
The [Office provider](../../../../packages/skill/skill-office/README.md) lists a compact router and six format-specific authoring or operation skills. The model loads full instructions by name and selects rendering for appearance-focused changes unless the user explicitly skips it. Rendering uses a private, randomly named temporary directory inside the task workspace and removes it after inspection. Skills invoke the shared Python checker and configured LibreOffice Kit CLI through ordinary Bash or PowerShell. SDK and Desktop own their Office composition separately; ordinary Web and the shared base do not mount these plugins.
|
||||
|
||||
Carriers own interpreter and resource locations. Operation skills append the configured Node and CLI paths only when their bodies are loaded; authoring skills use the selected Python and shared checker resource. Loading instructions does not prepare engines or authoring payloads. Ordinary shell execution retains its existing permissions and process management.
|
||||
|
||||
Office dependency ranges allow patch updates; the root lock records the resolved adapter and engine versions.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Keep a dedicated renderer tool.** A small typed schema constrains rendering arguments, but requires its own execution configuration, scoped registration, projection, and activation lifecycle. The CLI operation skills accept shell quoting and model-selected arguments in exchange for reusing the existing approval, sandbox, cancellation, and output mechanisms.
|
||||
|
||||
**Hide subskills from the catalog.** This requires a generic visibility policy independent of invocation permission. Listing seven short summaries preserves the existing skill API while full instruction bodies remain demand-loaded.
|
||||
|
||||
**Automatically inject guides for file extensions.** Extensions do not distinguish text edits, recalculation, conversion, and requested visual checks. Model-selected routing keeps unrelated operations out of context and permits explicit direct invocation.
|
||||
|
||||
## Consequences
|
||||
|
||||
Instruction loading appends ordinary logged skill results; completion does not erase them. Historical tool records remain readable and existing declaration reconciliation handles resumed sessions without a Session format change or agent-loop modification. A dedicated renderer would need a new independently justified user or security requirement before reintroduction.
|
||||
|
||||
Verification covers catalog entries, denied and explicit invocation, authoring without executable paths, native/PTC requests, resume and fork, ordinary and minimal profile composition, resource packaging, context budgets, and real-model operation selection. Rendering failures preserve usable sources and stop visual checks; successful commands alone cannot establish usable images.
|
||||
@@ -0,0 +1,32 @@
|
||||
# Agent Note: Office 技能渐进加载与普通 shell 执行
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-09-27-progressive-office-skills.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
合并的创作和渲染说明会为普通编辑无关的操作消耗上下文。渲染工具激活增加第二条发现路径和持久状态,而 shell 工具已经负责执行权限及进程管理。缺少渲染器也可能阻止加载原本可用的编辑指南。
|
||||
|
||||
## 决策
|
||||
|
||||
|
||||
[Office 提供者](../../../../packages/skill/skill-office/README.zh.md) 列出精简路由和六个各格式创作或操作技能。模型按名称加载完整说明;以外观为目标的修改会选择渲染,除非用户明确要求跳过。渲染使用任务工作区内随机命名的私有临时目录,并在检查后删除。技能通过普通 Bash 或 PowerShell 调用共享 Python 检查器和配置的 LibreOffice Kit CLI。SDK 和 Desktop 分别拥有 Office 组合;普通 Web 和共享 base 不挂载这些插件。
|
||||
|
||||
载体负责解释器和资源位置。操作技能仅在加载正文时附加配置的 Node 与 CLI 路径;创作技能使用选定的 Python 和共享检查器资源。加载说明不准备引擎或创作 payload。普通 shell 执行保留现有权限和进程管理。
|
||||
|
||||
Office 依赖范围允许补丁版本更新;根锁文件记录实际解析的适配器与引擎版本。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
**保留专属渲染工具。** 小型类型化 schema 能约束渲染参数,但需要专属执行配置、作用域注册、投影和激活生命周期。CLI 操作技能接受 shell 引用和模型选择参数的成本,以复用现有审批、沙箱、取消及输出机制。
|
||||
|
||||
**从目录隐藏子技能。** 这需要独立于调用权限的通用可见性策略。列出七条简短摘要可保留现有技能 API,完整说明仍按需加载。
|
||||
|
||||
**按文件扩展名自动注入指南。** 扩展名无法区分文本编辑、重计算、转换和用户要求的视觉检查。模型选路使无关操作不进入上下文,并允许显式直接调用。
|
||||
|
||||
## 影响
|
||||
|
||||
说明加载追加普通的已记录技能结果;操作完成不会擦除这些结果。历史工具记录仍可读取,恢复会话使用现有声明更新机制,无需修改 Session 格式或 agent loop。重新引入专属渲染器需要新的、独立成立的用户或安全要求。
|
||||
|
||||
验证覆盖目录条目、拒绝及显式调用、无可执行文件路径时的创作、native/PTC 请求、恢复和 fork、常规及 minimal 组合、资源打包、上下文预算和真实模型操作选择。渲染失败时保留可用源文件并停止视觉检查;命令成功本身不能证明图片可用。
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
# Bilingual-pair consistency record for 2026-09-29-explicit-session-history-preparation.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-29-explicit-session-history-preparation.md
|
||||
/agent-note-open-the-session-when-migration-is-required:
|
||||
en: d409772933407317
|
||||
zh: ac258a6d1743965f
|
||||
/agent-note-open-the-session-when-migration-is-required/problem:
|
||||
en: ab63c8127d68d294
|
||||
zh: d3f99489c96ff1d4
|
||||
/agent-note-open-the-session-when-migration-is-required/decision:
|
||||
en: 32627d744a6e7410
|
||||
zh: 2836a95ee5390111
|
||||
/agent-note-open-the-session-when-migration-is-required/alternatives-considered:
|
||||
en: baea781018a49252
|
||||
zh: bcc452029bf229fd
|
||||
/agent-note-open-the-session-when-migration-is-required/consequences:
|
||||
en: 979eebb53a6bf711
|
||||
zh: 013743b0b7a000ce
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
# Agent Note: Open the Session when migration is required
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-09-29-explicit-session-history-preparation.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
Expanding a subagent catalog or forking from the sidebar can trigger an old Session's format migration.
|
||||
|
||||
## Decision
|
||||
|
||||
The WorkspaceBrowser row-menu fork request carries `allowMigration: false`, which the Host handles by checking `stat` format information before calling the original fork flow. Only a source format requiring migration is refused, with an app-wide Toast offering to open the original Session. That action only navigates and does not retry fork. Neither an absent live Session nor unopened Client history justifies refusal.
|
||||
|
||||
Other fork callers omit that option and allow automatic migration.
|
||||
|
||||
`session.projections` also checks the stored format first; current-format cold history still uses the original observation flow for exact projections. Success returns `kind: sequenced` and its real `asOfSeq`; required migration returns `kind: migration-required` hints. The Client retains that read state, and cached values cannot replace sequenced values published by history or control.
|
||||
|
||||
The `formatStatus` returned by `stat` carries the migration library's existing classification; `header` remains the current logical format. It describes the stored format, not migration-task progress or prepared in-memory history.
|
||||
|
||||
When migration is required and `subagentCatalog` is absent, Web marks the Session's row “Migration required” and keeps its history-opening actions without descendant expansion or changing its subagent mode. Opening the Session supplies its catalog through history and control projections. Available catalogs remain usable, including empty catalogs. Session-list lineage resolves navigation addresses, not missing catalog membership. Ordinary loading, cancellation, and storage errors do not become this migration branch.
|
||||
|
||||
Create/adopt, full-text search, and ordinary history opening allow automatic migration.
|
||||
|
||||
This decision adds a migration exception to [catalog projection reads](../../archived/simplification/2026-09-08-web-subagent-catalog-projections.md). Exact reads and leases in [Session observations](2026-08-25-session-observations-and-projection-owned-client-state.md), and identity checks and cached/sequenced ordering in [projection-cache reads](2026-09-19-projection-cache-listing-identity-and-cached-rows.md), still apply.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Treat every cold Session as needing migration.** Residency and stored format are different; this would reject otherwise valid forks and discard catalog information available on disk.
|
||||
|
||||
**Globally forbid forks that automatically migrate on the Host.** The sidebar's interaction choice does not govern other callers, so its request explicitly carries the restriction.
|
||||
|
||||
**Automatically open and retry the sidebar fork.** Opening is an explicit user action, and fork does not run automatically after migration.
|
||||
|
||||
**Derive catalogs from Session-list parent ids.** This adds a second membership derivation; explicit Session opening obtains the catalog.
|
||||
|
||||
**Give cached hints `asOfSeq: -1`.** Hints have no event cut in current history and cannot participate in sequence comparisons.
|
||||
|
||||
## Consequences
|
||||
|
||||
Sessions requiring migration use the existing opening flow to prepare history; Sessions that need no migration remain cold-readable and forkable. Catalog hints may lag the log and yield to subsequent exact projections.
|
||||
|
||||
A separate follow-redirection proposal discusses future single-Session migration progress; this decision does not implement that flow.
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
# Agent Note: 需要迁移时引导打开会话
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-09-29-explicit-session-history-preparation.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
展开 subagent 目录或从侧栏 fork 可能触发旧会话的格式迁移。
|
||||
|
||||
## 决定
|
||||
|
||||
WorkspaceBrowser 行菜单的 fork 请求携带 `allowMigration: false`,由 Host 在调用原 fork 流程前检查 `stat` 的格式信息。只有源格式需要迁移时才拒绝,并显示带“打开原会话”操作的应用级 Toast。该操作只导航,不自动重发 fork。没有 live Session 或 Client 尚未打开历史不构成拒绝理由。
|
||||
|
||||
其他 fork 调用者省略该选项,允许自动迁移。
|
||||
|
||||
`session.projections` 同样先检查存储格式,当前格式冷历史继续通过原观察流程读取准确投影。成功时返回 `kind: sequenced` 和真实 `asOfSeq`;需要迁移时返回 `kind: migration-required` 的缓存提示。Client 保留这一读取状态,缓存值不能覆盖 history 或 control 已发布的有序值。
|
||||
|
||||
`stat` 返回的 `formatStatus` 携带迁移库已有的分类;`header` 仍是当前逻辑格式。它描述存储格式,不描述迁移任务进度或内存准备缓存。
|
||||
|
||||
在需要迁移且缺少 `subagentCatalog` 时,Web 将该会话行标为“需要迁移”,保留打开历史的操作,但不能展开下级,也不改变子会话的 mode。打开会话后,历史与 control 投影提供其目录。已有目录仍可使用,包括空目录。会话列表的谱系用于解析导航地址,不用于补齐缺失的目录成员。普通加载、取消和存储错误不转换成这一迁移分支。
|
||||
|
||||
create/adopt、全文搜索和普通历史打开允许自动迁移。
|
||||
|
||||
本决策为[目录投影查询](../../archived/simplification/2026-09-08-web-subagent-catalog-projections.md)增加迁移例外。[会话观察](2026-08-25-session-observations-and-projection-owned-client-state.zh.md)的准确读取和租约,以及[投影缓存读取](2026-09-19-projection-cache-listing-identity-and-cached-rows.zh.md)的身份校验与缓存/有序分层继续适用。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
**把所有冷会话都视作需要迁移。** 是否驻留与存储格式不同;这样会拒绝可以正常完成的 fork,并丢失能够从磁盘读取的目录信息。
|
||||
|
||||
**Host 全局禁止自动迁移的 fork。** 侧栏的交互选择不适用于其他调用者,因此限制由该请求显式携带。
|
||||
|
||||
**侧栏 fork 自动打开并重试。** 打开会话由用户明确选择,fork 不在迁移后自动执行。
|
||||
|
||||
**按会话列表中的父 id 派生目录。** 这需要第二套成员派生逻辑;显式打开会话即可获取目录。
|
||||
|
||||
**让缓存提示携带 `asOfSeq: -1`。** 提示没有当前历史的事件切点,不能参与序号比较。
|
||||
|
||||
## 影响
|
||||
|
||||
需要迁移的会话通过现有打开流程完成准备;无需迁移的会话仍可冷读和 fork。目录的缓存提示可能落后于日志,后续准确投影替换它们。
|
||||
|
||||
后续单会话迁移进度由独立的 follow 重定向提案讨论,本决策不实现该流程。
|
||||
+2
-2
@@ -9,8 +9,8 @@
|
||||
en: 5e10347fa87f98b5
|
||||
zh: cf3569631af8d66f
|
||||
/agent-note-anonymous-translation-remains-separate-from-model-history/decision:
|
||||
en: f13c134f53ed63c9
|
||||
zh: 868aacd453d26c82
|
||||
en: 3c4745cc96d89ffd
|
||||
zh: 939d5cd494346311
|
||||
/agent-note-anonymous-translation-remains-separate-from-model-history/alternatives-considered:
|
||||
en: 083d50f8564b0378
|
||||
zh: 50af332e64ad06f3
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ The experimental translator owns explicit Google or Bing routing, provider-langu
|
||||
|
||||
Bing is the configured default. Enabling the bundle permits transmission of expanded reasoning to the chosen service, disclosed in plugin metadata and settings. Failure never selects another provider. Original remains available; translated text neither replaces persisted assistant output nor enters model input. The [optional-bundle admission policy](2026-09-21-experimental-capabilities-as-optional-bundles.md) keeps the feature disabled until selected.
|
||||
|
||||
The reasoning-body chain carries original text and streaming status. Native Chat owns its original Markdown fallback; consumers do not replace the assistant renderer or alter Session projection. Only expanded bodies start translation. Collapsing, changing selection, or unloading aborts outstanding work and excludes late results.
|
||||
The GUI registers a translation wrapper in the standard `conversation.chat.reasoning.body` Slot and calls the official `conversation.chat.reasoning.content` Factory for original or translated text. The default body is also an ordinary registration; the host supplies no inline Markdown fallback or plugin action state. The translation wrapper owns its toolbar and passes its localized labels to the Factory, which supplies default labels when callers omit them and fixes compact typography. The [reasoning Body Slot and Content Factory design](2026-10-09-reasoning-content-slot-factory.md) supersedes this record's body composition mechanism; external requests remain separate from model history. Consumers do not replace the assistant renderer or alter Session projection. Only expanded bodies start translation. Collapsing, changing selection, or unloading aborts outstanding work and excludes late results.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ Status: implemented
|
||||
|
||||
配置的默认服务为 Bing。启用 Bundle 允许将展开的思考内容发送给所选服务,插件元数据与设置公开说明接收方。失败时不选择其他 Provider。原文始终可访问;译文不替换持久化 assistant 输出,也不进入模型输入。[可选 Bundle 准入规则](2026-09-21-experimental-capabilities-as-optional-bundles.zh.md) 保证功能在显式选择前保持关闭。
|
||||
|
||||
reasoning-body chain 传递原始文本与流式状态。原生 Chat 负责原始 Markdown fallback;消费者不替换 assistant 渲染器,也不改变 Session projection。只有展开内容才开始翻译。折叠、切换设置或卸载都会中止未完成工作并忽略迟到结果。
|
||||
GUI 向标准 `conversation.chat.reasoning.body` Slot 注册翻译包装,并调用官方 `conversation.chat.reasoning.content` Factory 展示原文或译文。默认正文同样是普通注册项;宿主不提供内联 Markdown fallback 或插件操作状态。翻译包装自行维护工具栏,并将自己的本地化 labels 传入 Factory;Factory 在调用者省略 labels 时提供默认文案,并固定紧凑排版。[思考正文的 Slot 与 Factory 设计](2026-10-09-reasoning-content-slot-factory.zh.md)取代本记录的正文组合方式,外部请求与模型历史隔离的决策保持不变。消费者不替换 assistant 渲染器,也不改变 Session projection。只有展开内容才开始翻译。折叠、切换设置或卸载都会中止未完成工作并忽略迟到结果。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
# Bilingual-pair consistency record for 2026-10-08-persistent-ptc-sandbox.md (docs/i18n/README.md): per heading
|
||||
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
|
||||
# After editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-10-08-persistent-ptc-sandbox.md
|
||||
/agent-note-retained-ptc-processes-follow-file-policy:
|
||||
en: ef07a82c85e490e1
|
||||
zh: b1662d1970d2b83b
|
||||
/agent-note-retained-ptc-processes-follow-file-policy/problem:
|
||||
en: b8cb574032d4e6e4
|
||||
zh: 6a1a25dec5f96086
|
||||
/agent-note-retained-ptc-processes-follow-file-policy/decision:
|
||||
en: 5109a4bb5be39867
|
||||
zh: 2dcc602aba9eae61
|
||||
/agent-note-retained-ptc-processes-follow-file-policy/alternatives-considered:
|
||||
en: 10bebc7f982cf7f4
|
||||
zh: 9247a2ba57921a11
|
||||
/agent-note-retained-ptc-processes-follow-file-policy/consequences:
|
||||
en: b3ddf273158d3754
|
||||
zh: 916356f4074b3622
|
||||
@@ -0,0 +1,31 @@
|
||||
# Agent Note: Retained PTC processes follow file policy
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-10-08-persistent-ptc-sandbox.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
Retaining JavaScript variables does not require broader file access. A process that outlives one execution can also outlive a one-time approval or a Session policy change. File-policy changes must invalidate retained environments and begin stopping their file handles and background work.
|
||||
|
||||
## Decision
|
||||
|
||||
`persistent-in-memory` starts its Node process through the same sandbox provider as `per-call`. A retained owner uses one resolved mode, workspace root and Session identity until release. The [memory decision](2026-09-23-best-effort-persistent-ptc.md) owns retained values, failure recovery and package composition; this note owns file authority.
|
||||
|
||||
The [PTC-owned cleanup decision](2026-10-09-ptc-owned-policy-cleanup.md) supersedes this note’s permission-setter completion and cleanup-ownership decision. Permission changes commit synchronously; PTC owns immediate cancellation and waits internally before subsequent execution. The tools package keeps Agent ownership and omits the owner for temporary escalations.
|
||||
|
||||
An approved one-time escalation omits the state owner and receives a fresh temporary environment. Its process is released before the call returns. It cannot access standing variables, and its declarations do not survive. The standing environment keeps its original policy and variables. Nested tools continue to apply their own permissions and approvals.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Unconfined retained execution.** Omitting confinement simplifies a temporary prototype but couples variable retention to full host file access. The existing process sandbox supports long-lived processes, so this grant is unnecessary.
|
||||
|
||||
**Changing the running process policy.** Existing backends apply restrictions at launch. Arbitrary retained objects, open handles and background work cannot be transferred into another policy with the existing interface. Releasing the environment prevents later evaluations from reusing those resources.
|
||||
|
||||
**Coordinating through permission presets.** File-policy changes also have direct callers. PTC observes committed Session mode events from both paths, independently of user-facing preset selection.
|
||||
|
||||
**Keeping an elevated environment.** A one-time approval cannot authorize future programs or idle background work. Omitting the state owner makes the approval lifetime explicit while preserving the standing environment.
|
||||
|
||||
## Consequences
|
||||
|
||||
Normal calls retain variables without changing file permissions. Changing sandbox mode discards variables. Escalated programs must be self-contained. Runtime consumers must release a retained owner before supplying a different policy without a Session mode change; the provider rejects inconsistent reuse. The sandbox inherits the existing backends’ file-effect coverage and platform limitations.
|
||||
@@ -0,0 +1,31 @@
|
||||
# Agent Note: 保留状态的 PTC 进程遵循文件策略
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-10-08-persistent-ptc-sandbox.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
保留 JavaScript 变量不需要更大的文件访问权限。跨调用存活的进程也可能在单次审批结束或 Session 策略变化后继续运行。文件策略变化必须使保留环境失效,并开始停止其文件句柄和后台工作。
|
||||
|
||||
## 决策
|
||||
|
||||
`persistent-in-memory` 与 `per-call` 通过同一个沙箱提供方启动 Node 进程。保留状态的所有者在释放前使用固定的已解析权限模式、工作区根目录和 Session 身份。[内存决策](2026-09-23-best-effort-persistent-ptc.zh.md)负责保留值、失败恢复和包组成;本文负责文件权限。
|
||||
|
||||
[PTC 自行等待清理的决策](2026-10-09-ptc-owned-policy-cleanup.zh.md)替代本文关于权限设置完成时点和清理归属的决策。权限变化同步提交;PTC 负责立即取消,并在后续执行前内部等待清理。工具包保留 Agent 所有权,在临时提权时省略所有者。
|
||||
|
||||
单次获准的提权省略状态所有者,使用全新的临时环境。该次调用返回前释放进程,无法访问原环境的变量,也不会保留新声明。原环境保持原权限和变量。嵌套工具继续应用各自的权限和审批规则。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
**不限制保留状态的执行环境。** 省略沙箱能简化临时原型,却把变量保留与完整宿主文件访问权限绑定。现有进程沙箱支持长期运行的进程,因此这项授权没有必要。
|
||||
|
||||
**修改运行中进程的策略。** 现有后端在启动时应用限制。现有接口无法把任意保留对象、打开的句柄和后台工作转移到另一策略。释放环境可以防止后续执行复用这些资源。
|
||||
|
||||
**通过权限预设协调清理。** 文件策略也有直接调用方。PTC 观察两类调用方提交的 Session 模式事件,无需依赖面向用户的预设选择。
|
||||
|
||||
**保留提权环境。** 单次审批不能授权未来程序或空闲期间的后台工作。省略状态所有者明确了审批生效的范围,同时保留原环境。
|
||||
|
||||
## 影响
|
||||
|
||||
普通调用保留变量,同时维持原文件权限。切换沙箱模式会丢弃变量。提权程序必须包含执行所需的完整代码。未发生 Session 模式变化时,运行时消费者在传入不同策略前必须释放保留状态的所有者;提供方拒绝策略不一致的复用。沙箱继承现有后端的文件约束范围与平台限制。
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
# Bilingual-pair consistency record for 2026-10-09-message-node-factories-and-delegates.md (docs/i18n/README.md): per heading
|
||||
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
|
||||
# After editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-10-09-message-node-factories-and-delegates.md
|
||||
/agent-note-domain-image-slots-and-a-shared-gallery-factory:
|
||||
en: bce6358fb4211261
|
||||
zh: bfebd3bbdb1affb1
|
||||
/agent-note-domain-image-slots-and-a-shared-gallery-factory/problem:
|
||||
en: ec6ffd7f737a1f56
|
||||
zh: 5e368f87ce52e28c
|
||||
/agent-note-domain-image-slots-and-a-shared-gallery-factory/decision:
|
||||
en: b807777fd48560eb
|
||||
zh: e7b1625bd4448ce1
|
||||
/agent-note-domain-image-slots-and-a-shared-gallery-factory/alternatives-considered:
|
||||
en: db14fd6d0e2ce78b
|
||||
zh: c29a19cde692a1dd
|
||||
/agent-note-domain-image-slots-and-a-shared-gallery-factory/verification:
|
||||
en: d82e036edb981e7c
|
||||
zh: 5a05e9a2e6489d21
|
||||
/agent-note-domain-image-slots-and-a-shared-gallery-factory/consequences:
|
||||
en: ac387ffb5a316920
|
||||
zh: ab584aaafe554278
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
# Agent Note: Domain image Slots and a shared gallery Factory
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-10-09-message-node-factories-and-delegates.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
User messages, assistant output, Trajectory records, and tool results need independently replaceable image presentation. Their official entries can share one gallery, but sharing that implementation must not merge their replacement positions or hide the caller's image-reading authority.
|
||||
|
||||
## Decision
|
||||
|
||||
Each domain declares its own image Slot and supplies the framework's `renderSlot` seat to the component that renders it. The official attachment entries use one reusable Content Factory, following the [Body Slot and Content Factory division](2026-10-09-reasoning-content-slot-factory.md).
|
||||
|
||||
| Entry | Declaration owner and renderer |
|
||||
| --- | --- |
|
||||
| `conversation.chat.user.images` Slot | The `conversation.chat.user.content` Factory declares it; `UserContent` renders it for user messages, pending steering, and local echoes. |
|
||||
| `conversation.chat.message.images` Slot | The Assistant node registration declares it; `AssistantMarkdown` renders consecutive output-image groups. |
|
||||
| `conversation.trajectory.images` Slot | The Trajectory view registration declares it; record attachments and tool-output blocks render it through the ledger's Slot seat. |
|
||||
| `tool.call.images` Slot | The `read_image` tool-view registration declares it; `ToolRow` renders the result image. |
|
||||
| `conversation.images.content` Factory | The attachment plugin registers this root-scoped gallery, implemented by `MessageImagesContent`. |
|
||||
|
||||
The attachment plugin fills all four image Slots with the same `MessageImages` React adapter at priority 100. A smaller priority replaces only the selected domain's entry; disposing that replacement restores the official entry. Each adapter invokes `conversation.images.content` with explicit image references, `loadImage`, and layout parameters. The shared Factory neither declares the domain Slots nor selects a domain from a scene parameter.
|
||||
|
||||
The image callers use their declared Slot directly; no image-entry Factory exists solely to forward one Slot call. User text remains a separate shared `conversation.user.text` Factory with its own keyed node Slot, as specified by the [user-text decision](2026-10-10-user-text-json-nodes.md).
|
||||
|
||||
`AssistantNodeView` passes its Slot seat and `loadImage` to `AssistantMarkdown`. User wrappers pass the loader through `UserStyleBubble` to `UserContent`. Trajectory passes its Slot seat and loader through the ledger. These paths preserve component responsibilities, image order, and keys without custom ReactNode-producing image callbacks.
|
||||
|
||||
Conversation owns read authorization, per-Session URL caching, and cleanup. The shared gallery never searches for a global current Session: persistent reads use the loader supplied by the caller, while submission-echo previews retain their existing browser URLs. Gallery layout, retries, thumbnails, and lightbox behavior remain in the attachment package.
|
||||
|
||||
`ChatView` keeps the existing `MarkdownDelegateProvider` with its wrapped file opener, external-link action, and file-image capabilities. Native-image composition does not replace Markdown image handling or its failure feedback.
|
||||
|
||||
`MessageImagesContentInput` accepts optional complete `MessageImageLabels`. Supplied labels are used unchanged; when omitted, the Content Factory's locale seat supplies the default copy.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**One shared image Slot for every caller.** A replacement intended for one domain would also affect the others; shared implementation does not require shared replacement identity.
|
||||
|
||||
**Import the gallery component across feature packages.** This bypasses standard Slot replacement and the reusable Factory entry point.
|
||||
|
||||
**Pass custom image-rendering functions or supply the loader through a private Context.** Framework Slot seats already express rendering authority, while explicit loader props preserve the caller's Session authority.
|
||||
|
||||
## Verification
|
||||
|
||||
- User and output images use separate Slots; replacing either leaves the other and its mounted gallery untouched.
|
||||
- Each official image entry has priority 100; a priority-0 replacement wins and disposal restores the official gallery.
|
||||
- All four defaults call the same Content Factory with their original image data, authorized loader, and layout choices.
|
||||
- Source order, keys, cached previews, failed-load retries, and lightbox interactions retain their existing behavior.
|
||||
- The Content Factory accepts complete custom labels and resolves default labels when the caller omits them.
|
||||
- Unloading the attachment implementation removes its entries without removing Slots declared by the owning domains.
|
||||
|
||||
## Consequences
|
||||
|
||||
Independent Slots allow domain-specific presentation without copying the gallery. Domain callers must forward the framework Slot seat to their plain React descendants and must supply an authorized loader at each image call. A Factory and an ordinary Slot have different lifetimes and ownership; a reusable implementation does not own every position where it appears.
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
# Agent Note: 领域图片 Slot 与共享画廊 Factory
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-10-09-message-node-factories-and-delegates.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
用户消息、助手输出、Trajectory 记录和工具结果需要可独立替换的图片展示。官方条目可以共享同一画廊,但共享实现不能合并各自的替换位置,也不能隐藏调用方的图片读取授权。
|
||||
|
||||
## 决策
|
||||
|
||||
各领域声明自己的图片 Slot,并把框架 `renderSlot` 席位交给实际渲染它的组件。官方附件条目复用一个 Content Factory,沿用 [Body Slot 与 Content Factory 的分工](2026-10-09-reasoning-content-slot-factory.zh.md)。
|
||||
|
||||
| 入口 | 声明所有者与渲染组件 |
|
||||
| --- | --- |
|
||||
| `conversation.chat.user.images` Slot | `conversation.chat.user.content` Factory 声明,由 `UserContent` 为用户消息、待处理 steering 和本地回显渲染。 |
|
||||
| `conversation.chat.message.images` Slot | Assistant 节点注册声明,由 `AssistantMarkdown` 渲染连续的输出图片组。 |
|
||||
| `conversation.trajectory.images` Slot | Trajectory 视图注册声明,记录附件及工具输出块通过账本的 Slot 席位渲染。 |
|
||||
| `tool.call.images` Slot | `read_image` 工具视图注册声明,由 `ToolRow` 渲染结果图片。 |
|
||||
| `conversation.images.content` Factory | 附件插件注册此 root 作用域画廊,由 `MessageImagesContent` 实现。 |
|
||||
|
||||
附件插件以优先级 100 为四个图片 Slot 填入同一个 `MessageImages` React 适配组件。较小的优先级只替换选定领域的条目;替换条目释放后恢复官方条目。适配组件显式向 `conversation.images.content` 传入图片引用、`loadImage` 和布局参数。共享 Factory 不声明各领域的 Slot,也不通过场景参数选择领域。
|
||||
|
||||
图片调用方直接使用其声明的 Slot,不设置仅转发一次 Slot 调用的图片入口 Factory。用户文本仍使用独立共享的 `conversation.user.text` Factory 及其 keyed 节点 Slot,详见[用户文本决策](2026-10-10-user-text-json-nodes.zh.md)。
|
||||
|
||||
`AssistantNodeView` 将 Slot 席位与 `loadImage` 传给 `AssistantMarkdown`。用户包装层通过 `UserStyleBubble` 将 loader 传给 `UserContent`。Trajectory 通过账本传递自己的 Slot 席位与 loader。这些路径保持组件职责、图片顺序和 key,不使用自制的 ReactNode 图片渲染回调。
|
||||
|
||||
Conversation 拥有读取授权、逐 Session URL 缓存与回收。共享画廊不查找全局当前 Session:持久图片使用调用方提供的 loader 读取,本地提交回显预览保留已有浏览器 URL。画廊布局、重试、缩略图和灯箱行为仍由附件包负责。
|
||||
|
||||
`ChatView` 保留既有 `MarkdownDelegateProvider` 及其包装后的文件打开动作、外链动作和文件图片能力。原生图片组合不替代 Markdown 图片处理及失败反馈。
|
||||
|
||||
`MessageImagesContentInput` 接受可选的完整 `MessageImageLabels`。提供 labels 时原样使用,省略时由 Content Factory 的 locale 席位提供默认文案。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
**所有调用方共用一个图片 Slot。** 面向某个领域的替换也会影响其他领域;共享实现不要求共享替换身份。
|
||||
|
||||
**跨功能包导入画廊组件。** 绕过标准 Slot 替换机制和可复用的 Factory 入口。
|
||||
|
||||
**传递自制图片渲染函数,或通过私有 Context 供应 loader。** 框架 Slot 席位已经表达渲染权限,显式 loader props 则保留调用方的 Session 授权。
|
||||
|
||||
## 验证
|
||||
|
||||
- 用户与输出图片使用独立 Slot;替换任意一方时,另一方及其已挂载画廊不受影响。
|
||||
- 各官方图片条目优先级为 100;优先级 0 的替换条目胜出,释放后恢复官方画廊。
|
||||
- 四个默认条目携带原有图片数据、授权 loader 和布局选项调用同一个 Content Factory。
|
||||
- 来源顺序、key、缓存预览、读取失败重试及灯箱交互保持既有行为。
|
||||
- Content Factory 接受完整自定义 labels,调用方省略时解析默认 labels。
|
||||
- 卸载附件实现会移除其条目,但不移除各领域声明的 Slot。
|
||||
|
||||
## 影响
|
||||
|
||||
独立 Slot 支持各领域定制展示而无需复制画廊。领域调用方需要将框架 Slot 席位传给普通 React 后代组件,并在每次图片调用中提供授权 loader。Factory 与普通 Slot 的生命周期和所有权不同;可复用实现不拥有它出现的所有位置。
|
||||
@@ -0,0 +1,19 @@
|
||||
# Bilingual-pair consistency record for 2026-10-09-ptc-owned-policy-cleanup.md (docs/i18n/README.md): per heading
|
||||
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
|
||||
# After editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-10-09-ptc-owned-policy-cleanup.md
|
||||
/agent-note-ptc-owns-policy-change-cleanup-waits:
|
||||
en: dfe28bee0ab79ff6
|
||||
zh: dd68c51b4cbfbc3f
|
||||
/agent-note-ptc-owns-policy-change-cleanup-waits/problem:
|
||||
en: 5620ff525d762114
|
||||
zh: ddef3a3624cab79b
|
||||
/agent-note-ptc-owns-policy-change-cleanup-waits/decision:
|
||||
en: 4389c330d95f1588
|
||||
zh: 97e4b9cf8b69f48e
|
||||
/agent-note-ptc-owns-policy-change-cleanup-waits/alternatives-considered:
|
||||
en: a51d7f5c5b8fd7dc
|
||||
zh: 4480107a4d8cff70
|
||||
/agent-note-ptc-owns-policy-change-cleanup-waits/consequences:
|
||||
en: f83009b6406bafa1
|
||||
zh: c774eb1895bf37e1
|
||||
@@ -0,0 +1,27 @@
|
||||
# Agent Note: PTC owns policy-change cleanup waits
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-10-09-ptc-owned-policy-cleanup.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
Stopping a retained Node process and its nested tools can take longer than recording a new file policy. Making permission setters await that cleanup spreads PTC lifecycle requirements into unrelated callers, including Webhook Session creation, and adds cancellation windows to those callers.
|
||||
|
||||
## Decision
|
||||
|
||||
Permission setters commit synchronously. Success means the selected policy is recorded; it does not guarantee that operations admitted under the old policy have completely stopped. This replaces the completion requirement and cleanup ownership in the [retained-process sandbox decision](2026-10-08-persistent-ptc-sandbox.md), whose confinement and one-time escalation rules still apply.
|
||||
|
||||
PTC observes committed Session mode changes. The tool bridge immediately cancels affected complete calls, including pending approvals and nested tools. A later PTC call waits for canceled predecessors to settle before resolving the latest standing policy. The Node provider immediately invalidates affected processes, including idle environments, starts cleanup, and retains the cleanup Promise internally. A later program waits for that cleanup; failed cleanup remains retryable and prevents process reuse. Unchanged modes preserve environments. Tool-registry and provider unload remove their observers and await owned work.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Wait in permission setters.** This provides a definite completion point for old-authority work, but no current permission consumer requires that guarantee. It introduces a general resource registry and asynchronous API migration for PTC-specific cleanup. The completion guarantee belongs to PTC execution and disposal instead.
|
||||
|
||||
**Clean up only on the next PTC call.** An idle environment can retain background work indefinitely when no further call occurs. Policy changes begin cancellation immediately; only waiting is deferred.
|
||||
|
||||
**Reconfigure an existing process.** The sandbox applies restrictions at launch. Open handles, retained objects and background work require releasing the old environment before another policy can execute there.
|
||||
|
||||
## Consequences
|
||||
|
||||
Permission changes return without process-cleanup latency. Old operations may remain active while cancellation finishes, and cleanup failure does not roll back the recorded policy. Subsequent PTC execution waits for cleanup and uses the latest policy. Webhook creation keeps its synchronous permission step, so that step adds no cancellation window before prompt admission. Regression coverage holds cleanup with explicit barriers, changes policy again while a call waits, checks idle cancellation without another call, and verifies cleanup retry and owner unload.
|
||||
@@ -0,0 +1,27 @@
|
||||
# Agent Note: PTC 自行等待策略变化后的清理
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-10-09-ptc-owned-policy-cleanup.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
停止保留的 Node 进程及其嵌套工具,可能比记录新文件策略耗时更长。让权限设置等待这些清理,会把 PTC 生命周期要求传给 Webhook Session 创建等无关调用方,并为这些调用方增加取消窗口。
|
||||
|
||||
## 决策
|
||||
|
||||
权限设置同步提交。成功表示所选策略已经记录,不保证按旧策略接纳的操作已完全停止。这替代[保留进程沙箱决策](2026-10-08-persistent-ptc-sandbox.zh.md)中的完成要求和清理归属;其中的沙箱限制与单次提权规则继续适用。
|
||||
|
||||
PTC 观察已提交的 Session 模式变化。工具桥接立即取消受影响的完整调用,包括等待中的审批和嵌套工具。后续 PTC 调用先等待已取消的调用结束,再解析最新常设策略。Node 提供方立即使受影响的进程失效,包括空闲环境,开始清理并在内部保留清理 Promise。后续程序等待清理完成;清理失败可重试,且阻止复用进程。模式不变时保留环境。工具注册表和提供方卸载时移除各自监听器,并等待所拥有的工作结束。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
**在权限设置中等待。** 这可以明确旧权限下的工作完成时点,但当前权限调用方不需要该保证。为此增加通用资源登记和异步 API 迁移,会扩大 PTC 清理的影响范围。完成保证由 PTC 执行和销毁负责。
|
||||
|
||||
**等下次 PTC 调用再清理。** 没有后续调用时,空闲环境的后台工作可能一直存在。策略变化立即开始取消,只有等待被推迟。
|
||||
|
||||
**重新配置已有进程。** 沙箱在启动时应用限制。打开的句柄、保留对象和后台工作要求先释放旧环境,才能在其中按另一策略执行。
|
||||
|
||||
## 影响
|
||||
|
||||
权限变化返回时无需等待进程清理。取消完成前,旧操作可能仍在运行;清理失败不会回滚已记录的策略。后续 PTC 执行等待清理并使用最新策略。Webhook 创建保留同步权限步骤,该步骤不会在提交任务前增加取消窗口。回归测试通过显式信号阻塞清理,覆盖等待期间再次改变策略、没有后续调用时取消空闲环境、清理重试及所有者卸载。
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
# Bilingual-pair consistency record for 2026-10-09-reasoning-content-slot-factory.md (docs/i18n/README.md): per heading
|
||||
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
|
||||
# After editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-10-09-reasoning-content-slot-factory.md
|
||||
/agent-note-standard-reasoning-body-slot-and-reusable-content-factory:
|
||||
en: 55862b96f6731c81
|
||||
zh: 425ab1ff7043a606
|
||||
/agent-note-standard-reasoning-body-slot-and-reusable-content-factory/problem:
|
||||
en: 91000d9dd4a47647
|
||||
zh: 6146424ac5c3987b
|
||||
/agent-note-standard-reasoning-body-slot-and-reusable-content-factory/decision:
|
||||
en: 0a45affd1b125113
|
||||
zh: 3af28a27b7721bc7
|
||||
/agent-note-standard-reasoning-body-slot-and-reusable-content-factory/decision/registration-and-rendering:
|
||||
en: 0af348d91eedc16e
|
||||
zh: a06d6f59cc271e94
|
||||
/agent-note-standard-reasoning-body-slot-and-reusable-content-factory/decision/data-state-and-lifecycle:
|
||||
en: e220ebf28a113e0b
|
||||
zh: dfedd85ac5d76ccb
|
||||
/agent-note-standard-reasoning-body-slot-and-reusable-content-factory/decision/translation-plugin-integration:
|
||||
en: 145a1ae617a49f43
|
||||
zh: b2eb8443bb1515a7
|
||||
/agent-note-standard-reasoning-body-slot-and-reusable-content-factory/alternatives-considered:
|
||||
en: 89adfc6fbad716f3
|
||||
zh: 098cca9e73c5eeaf
|
||||
/agent-note-standard-reasoning-body-slot-and-reusable-content-factory/verification:
|
||||
en: 23e6e98d176f08f4
|
||||
zh: 2b89720b8914552b
|
||||
/agent-note-standard-reasoning-body-slot-and-reusable-content-factory/consequences:
|
||||
en: 60a32f46ff947154
|
||||
zh: 6498e791d4c2c166
|
||||
@@ -0,0 +1,102 @@
|
||||
# Agent Note: Standard reasoning Body Slot and reusable Content Factory
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-10-09-reasoning-content-slot-factory.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
Reasoning bodies need to let third parties replace or wrap their presentation while reusing the official Markdown renderer, localized labels, and compact typography. An optional renderer parameter that makes the host choose between direct rendering and a chain fallback leaves the default body outside the registration system and forces plugins to maintain another Markdown call.
|
||||
|
||||
The render position and the base implementation serve different purposes: the position accepts plugin registrations and lets the framework select one, while any component can call the implementation directly. Calling the base implementation must not repeat plugin selection for that position or require a Session.
|
||||
|
||||
## Decision
|
||||
|
||||
The design uses existing [Component Factories](2026-09-10-component-factories-and-local-slots.md) and ordinary Slots without adding framework dispatch machinery.
|
||||
|
||||
| Name | Type | Owner and responsibility |
|
||||
| --- | --- | --- |
|
||||
| `conversation.chat.reasoning.body` | `single`, `session` Slot | Body position declared by the Assistant node registration; receives only original text and streaming state when expanded. |
|
||||
| `conversation.chat.reasoning.content` | `root` Factory | Standard reasoning content provided by `ui-chat`; accepts text, streaming state, and optional labels, supplies default labels, and fixes `variant="compact"`. |
|
||||
|
||||
The Factory receives no Session identity, reads no Session, and owns no translation state. The current Factory API requires `scope`; `root` expresses the absence of a Session dependency, while each render position still has an independent React instance. The Chat plugin registers the Factory independently of any Body occupant, so replacing a body does not remove the base implementation.
|
||||
|
||||
### Registration and rendering
|
||||
|
||||
1. `ui-chat` registers the Content Factory with component props derived through `FactoryComponentPropsOf`. When the caller omits labels, the Factory constructs stable defaults from Chat's locale seat.
|
||||
2. The Assistant node registration declares the Body Slot. An expanded `ReasoningRow` only calls `renderSlot`; it accepts no optional renderer, supplies no inline fallback, and does not check whether a plugin is enabled.
|
||||
3. The official default Body is an ordinary registration with `priority: 100`. It forwards only `text` and `running` to the Content Factory.
|
||||
4. Third parties wait for the Body declaration through `slots.inject` and register their Body with a smaller priority value. They may call the Content Factory directly and add their own controls around it without importing private official React components.
|
||||
5. When a third party unregisters, the framework selects the official default registration again. Multiple third-party registrations follow existing single-slot priorities; they do not form middleware or render multiple bodies together.
|
||||
|
||||
```text
|
||||
ReasoningRow
|
||||
└─ conversation.chat.reasoning.body
|
||||
├─ 官方默认 Body ───────────┐
|
||||
└─ 第三方 Body → 自有控件 ─┤
|
||||
└─ conversation.chat.reasoning.content
|
||||
└─ MarkdownText
|
||||
```
|
||||
|
||||
The body render position:
|
||||
|
||||
```tsx ignore-check
|
||||
<div className={css.thinkBody}>
|
||||
{renderSlot('conversation.chat.reasoning.body', { text, running })}
|
||||
</div>
|
||||
```
|
||||
|
||||
The default Body and third-party wrappers use the same reusable implementation; a third party passes labels when it needs custom copy:
|
||||
|
||||
```tsx ignore-check
|
||||
renderFactorySlot('conversation.chat.reasoning.content', {
|
||||
text: displayText,
|
||||
running,
|
||||
labels,
|
||||
})
|
||||
```
|
||||
|
||||
### Data, state, and lifecycle
|
||||
|
||||
- `ReasoningBodyOwnerProps` contains only readonly `text: string` and `running: boolean`. `ReasoningContentInput` also accepts `labels?: MarkdownLabels`; supplied labels are used unchanged without field-level merging, and omission selects the official defaults. Callers own the localization and reference stability of custom labels.
|
||||
- The Body's `text` is always the original reasoning. A third party selects original or derived display text and passes that value to the Factory without rewriting the Session or Chat projection.
|
||||
- `ReasoningRow` retains its existing disclosure state, summary, shimmer, and title behavior. Plugin-specific selection state, buttons, and styles stay inside the plugin's Body wrapper; the official implementation supplies no `setHeaderAction`, header-action type, or corresponding state.
|
||||
- Registrations follow Cordis effect lifetimes. Collapsing the Body declaration removes its contributions through `slots.inject`; unloading the Chat plugin removes the Content Factory.
|
||||
- Factory components and the default Body have stable module-level identities. Text changes update props instead of rebuilding component types or keys. Plugin replacement or HMR may remount components according to existing framework semantics; unmounted components have no state-retention guarantee.
|
||||
|
||||
### Translation plugin integration
|
||||
|
||||
The [anonymous translation design](2026-10-05-anonymous-reasoning-translation.md) retains its external-request, privacy, failure, and cancellation decisions; this design supersedes its body composition mechanism. The translation plugin registers in the standard Body Slot, renders original/translation selection, status, and retry controls in its own toolbar, and wraps the official Content Factory with its own state and optional custom labels rather than calling `MarkdownText` directly.
|
||||
|
||||
Translation requests, splitting, caching, persistence, and settings remain unchanged. The translation toggle belongs to the plugin body rather than the official Think title. The design adds no Slot outside the body, does not handle user-message masking, and does not change the general Markdown parser.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Translation-specific chain and host fallback.** The default content is not an ordinary registration, the host maintains a second render path, and third parties cannot directly reuse the complete official body implementation.
|
||||
|
||||
**Expose only the Body Slot.** This permits replacement, but wrapping plugins still have to copy the official Markdown call, labels, and variant.
|
||||
|
||||
**Provide only the Content Factory.** This permits implementation reuse but supplies no actual body position for third-party replacement registrations.
|
||||
|
||||
**Export React components across plugins.** This bypasses existing Factory loading, lifecycle, and type derivation and adds feature-plugin runtime dependencies.
|
||||
|
||||
**Name the Factory as general Markdown.** This implementation fixes reasoning-specific compact typography and supplies Chat defaults; it is not a general entry point for all Markdown uses.
|
||||
|
||||
**Make the official implementation maintain plugin header actions.** The default body has no such requirement; a dedicated setter makes the host own plugin state, cleanup, and button styles.
|
||||
|
||||
**Force official labels in the Factory.** Default copy must not prevent caller customization; an optional parameter provides an override without replacing the whole body implementation.
|
||||
|
||||
## Verification
|
||||
|
||||
- The body host only calls `conversation.chat.reasoning.body`, with no old `reasoning-body` chain, optional renderer, or inline Markdown fallback.
|
||||
- Both the default Body and translation Body render through `conversation.chat.reasoning.content`; omitted labels use official defaults, and custom labels reach Markdown unchanged.
|
||||
- The Factory works without a Session Provider, and two render positions do not share component-local state.
|
||||
- The official implementation has no header-action type, setter, state, or dedicated styles; the translation wrapper supplies its own selection and retry controls.
|
||||
- Enabling, disabling, or hot-reloading the translation plugin selects the corresponding Body; default output, original-text access, streaming, disclosure, and cancellation behavior remain consistent.
|
||||
- Unit and assembly tests cover registration disposal, a Session-independent Factory, default and custom labels, shared implementation reuse, and independent occurrences; existing browser replay covers default bodies and translation switching.
|
||||
|
||||
## Consequences
|
||||
|
||||
The old Slot name, chain-selector registrations, and header-action callback are unsupported, so third parties must migrate. Test fixtures and the Client inspect catalog must follow the new declarations rather than offer obsolete registration examples.
|
||||
|
||||
The Body remains an exclusive replacement position. This design does not automatically compose multiple text-transformation plugins or turn display transformations into redaction or model-input modification.
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
# Agent Note: 思考正文的标准 Slot 与可复用 Content Factory
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-10-09-reasoning-content-slot-factory.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
思考正文需要允许第三方替换或包装展示,同时复用官方的 Markdown、本地化文案和紧凑排版。将扩展做成可选的 renderer 参数,再由宿主选择直接渲染或 chain fallback,会让默认正文存在于注册体系之外,插件也只能另行维护 Markdown 调用。
|
||||
|
||||
渲染位置与基础实现是两种不同的能力:位置允许多个插件注册并由框架选中一个,实现则应允许任何组件直接调用。调用基础实现不应再次触发该位置的插件选择,也不应要求 Session。
|
||||
|
||||
## 决策
|
||||
|
||||
沿用现有 [Component Factory](2026-09-10-component-factories-and-local-slots.zh.md) 和普通 Slot,不增加框架分发机制。
|
||||
|
||||
| 名称 | 类型 | 所有者与职责 |
|
||||
| --- | --- | --- |
|
||||
| `conversation.chat.reasoning.body` | `single`、`session` Slot | Assistant 节点注册声明的正文位置;展开时只接收原始文本与流式状态。 |
|
||||
| `conversation.chat.reasoning.content` | `root` Factory | `ui-chat` 提供的标准思考正文;接收文本、流式状态及可选 labels,提供默认文案并固定 `variant="compact"`。 |
|
||||
|
||||
Factory 不接收 Session 身份,不读取 Session,不持有翻译状态。现有 Factory API 要求填写 `scope`,使用 `root` 表达无 Session 依赖;每个调用位置仍有独立的 React 实例。Factory 由 Chat 插件注册,而非依附于某个 Body occupant,第三方替换正文不会撤掉基础实现。
|
||||
|
||||
### 注册与调用
|
||||
|
||||
1. `ui-chat` 注册 Content Factory,组件 props 由 `FactoryComponentPropsOf` 推导。调用者未传 labels 时,Factory 使用 Chat 的 locale seat 构造稳定的默认文案。
|
||||
2. Assistant 节点注册声明 Body Slot。`ReasoningRow` 展开时只调用 `renderSlot`,不接收可选 renderer,不提供内联 fallback,也不判断插件是否启用。
|
||||
3. 官方以 `priority: 100` 注册普通默认 Body。该组件只将 `text`、`running` 传给 Content Factory。
|
||||
4. 第三方通过 `slots.inject` 等待 Body 声明,以较小的 priority 注册自己的 Body。第三方可直接调用 Content Factory,并在外层增加自己的控件;无需导入官方私有 React 组件。
|
||||
5. 第三方注销后,框架重新选中官方默认注册项。多个第三方注册遵循现有 single 优先级,不形成 middleware,也不同时渲染多份正文。
|
||||
|
||||
```text
|
||||
ReasoningRow
|
||||
└─ conversation.chat.reasoning.body
|
||||
├─ 官方默认 Body ───────────┐
|
||||
└─ 第三方 Body → 自有控件 ─┤
|
||||
└─ conversation.chat.reasoning.content
|
||||
└─ MarkdownText
|
||||
```
|
||||
|
||||
正文调用位置:
|
||||
|
||||
```tsx ignore-check
|
||||
<div className={css.thinkBody}>
|
||||
{renderSlot('conversation.chat.reasoning.body', { text, running })}
|
||||
</div>
|
||||
```
|
||||
|
||||
默认 Body 与第三方包装都使用同一个可复用实现;第三方需要自定义文案时传入 labels:
|
||||
|
||||
```tsx ignore-check
|
||||
renderFactorySlot('conversation.chat.reasoning.content', {
|
||||
text: displayText,
|
||||
running,
|
||||
labels,
|
||||
})
|
||||
```
|
||||
|
||||
### 数据、状态与生命周期
|
||||
|
||||
- `ReasoningBodyOwnerProps` 仅包含只读的 `text: string` 与 `running: boolean`。`ReasoningContentInput` 另外允许 `labels?: MarkdownLabels`;传入的文案原样使用,不做字段级合并,省略时才使用官方默认文案。调用者负责自定义文案的本地化与引用稳定性。
|
||||
- Body 的 `text` 始终是原始思考。第三方选择原文或派生显示文本后,只将该值交给 Factory,不改写 Session 或 Chat projection。
|
||||
- 折叠状态、摘要、流光和标题由 `ReasoningRow` 保持现有行为。插件自己的切换状态、按钮及样式留在其 Body 包装中;官方不提供 `setHeaderAction`、标题动作类型或相应状态。
|
||||
- 注册沿用 Cordis effect 生命周期。Body 声明撤销时,其贡献随 `slots.inject` 清理;Content Factory 在 Chat 插件卸载时撤销。
|
||||
- Factory 组件与默认 Body 使用模块级稳定身份,文本变化作为 props 更新,不以文本重建组件类型或 key。插件替换或 HMR 可按现有框架语义重新挂载,不承诺保留被卸载组件的本地状态。
|
||||
|
||||
### 翻译插件接入
|
||||
|
||||
[免登录翻译设计](2026-10-05-anonymous-reasoning-translation.zh.md)保留外部请求、隐私、失败与取消决策;其正文组合方式由本方案取代。翻译插件注册到标准 Body Slot,在自己的工具栏内渲染原文/译文切换、状态及重试按钮,使用自己的状态和可选自定义 labels 包装官方 Content Factory,不直接调用 `MarkdownText`。
|
||||
|
||||
翻译请求、分片、缓存、持久化和设置保持不变。翻译切换按钮属于插件正文,不再由官方思考标题承载;不增加正文外的 Slot,不处理用户消息遮挡,也不改动通用 Markdown 解析器。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
**翻译专用 chain 与宿主 fallback。** 默认内容不是普通注册项,宿主需要维护第二条渲染路径,第三方无法直接复用完整的官方正文实现。
|
||||
|
||||
**只开放 Body Slot。** 可以替换正文,但第三方包装时仍需复制官方 Markdown 调用、labels 与 variant。
|
||||
|
||||
**只提供 Content Factory。** 可以复用实现,但缺少允许第三方注册替换的实际正文位置。
|
||||
|
||||
**跨插件导出 React 组件。** 绕过现有 Factory 的加载、生命周期与类型推导,增加 feature-plugin 运行时依赖。
|
||||
|
||||
**将 Factory 命名为通用 Markdown。** 该实现固定思考正文的紧凑排版,并提供 Chat 默认文案,不是所有 Markdown 场景的通用入口。
|
||||
|
||||
**官方替插件维护标题动作。** 官方默认正文没有该需求;保留专用 setter 会让宿主承担插件的状态、清理与按钮样式。
|
||||
|
||||
**Factory 强制使用官方 labels。** 默认文案不应阻止调用者定制;可选参数即可表达覆盖,无需替换整套正文实现。
|
||||
|
||||
## 验证
|
||||
|
||||
- 正文宿主只调用 `conversation.chat.reasoning.body`,没有旧 `reasoning-body` chain、可选 renderer 或内联 Markdown fallback。
|
||||
- 官方默认 Body 和翻译 Body 都通过 `conversation.chat.reasoning.content` 渲染;未传 labels 使用官方默认文案,传入自定义 labels 则原样交给 Markdown。
|
||||
- Factory 在没有 Session Provider 的调用位置也能使用;两个调用位置不共享组件本地状态。
|
||||
- 官方不存在标题动作类型、setter、状态或专用样式;翻译包装自行提供切换与重试控件。
|
||||
- 翻译插件启用、停用及 HMR 后,框架选中对应 Body;默认输出、原文访问、流式展示、折叠和取消行为保持一致。
|
||||
- 单元与装配测试覆盖注册撤销、无 Session Factory、labels 缺省与覆盖、默认与第三方共用实现及双实例;已有浏览器回放覆盖默认正文和翻译切换。
|
||||
|
||||
## 后果
|
||||
|
||||
旧 Slot 名称、chain selector 注册及标题动作回调不再受支持,第三方需要迁移。测试夹具与 Client inspect catalog 必须同步到新声明,避免仍向插件开发者提供旧注册方式。
|
||||
|
||||
Body 仍是独占替换位置。此方案不解决多个正文转换插件的自动组合,也不将显示变换提升为脱敏或模型输入修改能力。
|
||||
@@ -0,0 +1,40 @@
|
||||
# Bilingual-pair consistency record for 2026-10-10-user-text-json-nodes.md (docs/i18n/README.md): per heading
|
||||
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
|
||||
# After editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-10-10-user-text-json-nodes.md
|
||||
/agent-note-user-text-candidate-scheduling-and-facts-driven-refresh:
|
||||
en: 6185ac67a459beba
|
||||
zh: 994bf90f05c0fd5a
|
||||
/agent-note-user-text-candidate-scheduling-and-facts-driven-refresh/problem:
|
||||
en: 42e3752efebedde5
|
||||
zh: 75e23c5762efbb1f
|
||||
/agent-note-user-text-candidate-scheduling-and-facts-driven-refresh/decision:
|
||||
en: 71f5b33f58cd2066
|
||||
zh: 8365506795c0b158
|
||||
/agent-note-user-text-candidate-scheduling-and-facts-driven-refresh/decision/rule-roles-and-original-coordinates:
|
||||
en: ed0fa9306f0605cb
|
||||
zh: 7240e135f6d1b46b
|
||||
/agent-note-user-text-candidate-scheduling-and-facts-driven-refresh/decision/one-reconciliation-loop:
|
||||
en: acaeb2dccfac3f07
|
||||
zh: 2170ec2b858627fc
|
||||
/agent-note-user-text-candidate-scheduling-and-facts-driven-refresh/decision/query-cache-and-traversal:
|
||||
en: 55dee64de32bef9a
|
||||
zh: 69eb625451f55268
|
||||
/agent-note-user-text-candidate-scheduling-and-facts-driven-refresh/decision/actual-facts-dependencies:
|
||||
en: 11bc718324d22c03
|
||||
zh: 371576c1b2a90d62
|
||||
/agent-note-user-text-candidate-scheduling-and-facts-driven-refresh/decision/facts-text-and-registration-refresh:
|
||||
en: 47bc3f32b9aa22de
|
||||
zh: 59ffd06c800c00af
|
||||
/agent-note-user-text-candidate-scheduling-and-facts-driven-refresh/decision/output-ownership-and-publication:
|
||||
en: e076cb57a1fdcfc1
|
||||
zh: 988c02c6cf751f16
|
||||
/agent-note-user-text-candidate-scheduling-and-facts-driven-refresh/verification-requirements:
|
||||
en: 78127c1f18eacf8a
|
||||
zh: fcc2ad5db1279356
|
||||
/agent-note-user-text-candidate-scheduling-and-facts-driven-refresh/alternatives-considered:
|
||||
en: 439e5ecff6701938
|
||||
zh: 9861805f2a19439c
|
||||
/agent-note-user-text-candidate-scheduling-and-facts-driven-refresh/consequences:
|
||||
en: 13bd26532d0353fa
|
||||
zh: d6a4997b51af09d5
|
||||
@@ -0,0 +1,91 @@
|
||||
# Agent Note: User-text candidate scheduling and Facts-driven refresh
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](<2026-10-10-user-text-json-nodes.zh.md>)
|
||||
|
||||
## Problem
|
||||
|
||||
User text contains references owned by independent businesses. Recognition must follow original-text positions, resolve collisions deterministically, and reuse unaffected matching work when Facts change. The evaluator must expose its scheduling behavior to pure logic tests without depending on React, browser timing, or elapsed-time benchmarks.
|
||||
|
||||
## Decision
|
||||
|
||||
The composition is business registration, lazy candidate matching, a single reconciliation loop, readonly JSON nodes, and business-owned keyed renderers. Initialization and Facts refresh use the same algorithm; they differ only in which query caches remain valid.
|
||||
|
||||
### Rule roles and original coordinates
|
||||
|
||||
Every rule provides an ID, finite order, and synchronous `match(text, from, context)`. Each call returns its nearest eligible node with `start >= from`, or no candidate anywhere in the remaining original text. It returns one node, not all matches or a replacement document. The original string and UTF-16 offsets remain intact: a search cursor does not create a new word, line, or quoted-token boundary. Matching depends only on original text and Facts, not previously selected node payloads.
|
||||
|
||||
Each matcher resolves its own alternative spellings by earliest start and then its documented same-start choice; Session labels choose the longer match. A matcher cannot replace a consumed prefix with the remainder of an overlapping candidate. Each business owns its grammar, payload, and renderer; the generic evaluator has no Session, file, skill, or command branches.
|
||||
|
||||
There is one rule type and one candidate protocol. A candidate contains either a business node using the rule ID or a Text node preserving the original substring with an optional `data.display`. Text-producing rules compete by the same start, order, and stable ID as business rules. Order only breaks same-start ties; no rule gets precedence over an earlier position.
|
||||
|
||||
### One reconciliation loop
|
||||
|
||||
The evaluator keeps original text, the previous successful segmentation, registered definitions, per-rule Facts reads, and query results. Nonempty text initially has one Text covering `[0, text.length)`; empty text has no nodes. Initialization makes every rule invalid, including rules that read no Facts.
|
||||
|
||||
Each reconciliation starts its result cursor at zero. Every matcher obtains one candidate through the same cache-aware query operation. The evaluator selects the smallest candidate start, breaks same-start ties by ascending rule order and stable ID, emits the preceding raw Text gap, emits the winner, and advances the cursor to the winner's end. No candidate means the remaining raw text is emitted and recognition ends.
|
||||
|
||||
Only a consumed candidate or a candidate whose start is before the new cursor needs another query. A cached candidate beginning at or after the cursor remains eligible. A no-candidate result remains eligible as the cursor advances. All ranges are nonempty half-open original-text ranges; candidates cannot move the cursor backward or exceed the original string.
|
||||
|
||||
Previously accepted nodes are reusable output, not a complete list of possible matches: a losing candidate can become visible when an earlier winner disappears. The evaluator retains queries for this purpose instead of treating an old Text node or the absence of a business node as unconditional evidence of no match.
|
||||
|
||||
### Query cache and traversal
|
||||
|
||||
A query record contains its original `from` offset and either the next candidate, no candidate, or a failure. If a query from `q` returned a candidate starting at `s`, its result is valid for any requested cursor `c` with `q <= c <= s`. A no-candidate result is valid for every `c >= q`. Neither result proves anything before `q`. A failed query is reusable only at the same requested offset while its dependencies remain valid.
|
||||
|
||||
During a reconciliation, each matcher has one active candidate. The evaluator also retains the sorted query records already produced for that matcher across reconciliations; it does not enumerate all possible matches eagerly. Forward cursors walk these records monotonically. Queries generated in newly exposed gaps are merged with retained records in source order, with one record per query offset. Unsearched intervals require matching rather than an invented negative result.
|
||||
|
||||
The result cursor and matcher search cursors are distinct. Starting result reconciliation at zero does not require unchanged matchers to scan from zero again: their recorded queries answer valid requests. Each matcher must implement a forward search rather than repeatedly materializing every match in the remaining text. Internal scans, candidate selection, and result comparison still have costs; the scheduling guarantees concern actual matcher calls, not a claim that arbitrary regular expressions are linear.
|
||||
|
||||
### Actual Facts dependencies
|
||||
|
||||
`context.read(key)` returns the borrowed readonly value from the current Facts snapshot and records the read, including `undefined`. The reader is valid only during its owning synchronous call and cannot be retained. Businesses receive neither a whole Facts object, a manual `dependsOn` list, nor previous-output/cache objects.
|
||||
|
||||
A matcher's dependency set is the union of reads for the queries retained in its current cache generation, including losing candidates, no-match queries, and failed calls. Changed fields are detected with `Object.is` at field granularity. A new outer Facts object alone does not invalidate matching. When a rule is invalidated, its old queries and read set are discarded; subsequent calls establish the replacement read set, so obsolete conditional dependencies do not remain permanently attached.
|
||||
|
||||
Text-producing rules use the same query cache and dependency generation as every other rule. Updating a whitespace switch invalidates only its readers; it does not trigger an unconditional pass over the completed nodes.
|
||||
|
||||
### Facts, text, and registration refresh
|
||||
|
||||
A Facts update invalidates only rules that read changed fields. Their first query starts at original offset zero; all other rules retain their query records. The same reconciliation loop then compares fresh candidates with reusable ones. Refreshing a rule does not give it priority, and a new first candidate does not hide the removal of an earlier old winner.
|
||||
|
||||
A fresh candidate confined to previously plain text can split that Text without invoking unrelated matchers whose cached queries still cover the new cursors. A fresh candidate that loses to an earlier unchanged winner is discarded without invalidating that winner. When a fresh winner covers another candidate, only expired heads advance. When a winner disappears or shrinks, cached losing candidates can compete again; any previously unsearched interval causes a query at the newly exposed cursor. All changes use the same cursor loop and query-coverage rules.
|
||||
|
||||
Several changed Facts fields form one snapshot and one invalidation set. All affected matchers participate in the same reconciliation; one update is not committed before processing the next. Equivalent refreshed results reuse previous nodes and, when all elements remain identical, the entire result array. Reconciliation settles the complete document before publishing its result.
|
||||
|
||||
Original-text changes discard all query caches and supply one full-range Text as the previous segmentation. New or replaced rule definitions start with empty caches; unchanged definitions retain their independent queries. Removed rules lose their dependencies and candidates. Reconciliation after additions, removals, or order changes uses the same selection logic and can expose formerly blocked candidates.
|
||||
|
||||
### Output, ownership, and publication
|
||||
|
||||
Nodes retain `{ id, start, end, data }`. Business nodes use their matcher's ID; Text retains the exact source substring in `data.text` and optional display text in `data.display`. Output is ordered, nonoverlapping, and completely covers the original text. JSON compatibility and readonly ownership are TypeScript obligations: the evaluator neither freezes borrowed objects nor deep-copies Facts or payloads for isolation.
|
||||
|
||||
The document evaluator owns all caches; the registry retains neither evaluators nor document subscriptions. Separate text instances do not share state through a global cache. Only fully successful evaluation publishes a new result. A matcher failure carries the rule ID and cause and publishes no partial segmentation; its actual reads remain available for dependency-driven retry. Reentrant evaluation and escaped synchronous read contexts are rejected.
|
||||
|
||||
`conversation.user.text` remains a session-scoped Factory owning `conversation.user.text.node`, a keyed inline Slot. React consumes final nodes and forwards `raw` and `interactive` to the business renderer. Session, skill, file/folder, and command matching remain owned by their respective plugins, and actions remain in each renderer's Injection. No matching work or cache subscription moves into React.
|
||||
|
||||
`collapseWhitespace` and `trimWhitespace` each control one ordinary matcher and default to off. Collapse offers whitespace runs as Text candidates with display `" "`; trim offers original leading/trailing whitespace runs with display `""`. Trim has lower order than collapse, so it wins their same-start edge collisions. An earlier reference candidate consumes its whole range and naturally excludes internal whitespace candidates. Every Text candidate retains its exact raw substring; emoji and spaces between references remain intact. `interactive` and CSS `ellipsis` remain React Props, not matching Facts. Queue uses full original text with CSS ellipsis; native attachments remain in their existing cards and image Slots, outside text recognition.
|
||||
|
||||
## Verification requirements
|
||||
|
||||
Pure logic tests assert both the final nodes and each matcher's ordered calls with their `from` offsets. They use deterministic input and spies rather than clocks or performance thresholds.
|
||||
|
||||
- Initialization calls every matcher, including Facts-free and no-match rules; empty input has explicit terminal-query behavior.
|
||||
- Earlier starts beat stronger order values; same-start ties use order and stable ID, and each matcher preserves its own longest-match choice.
|
||||
- A future candidate and a no-match answer are not requested again during forward traversal; only the winner and overlapping heads advance.
|
||||
- Unread Facts changes and new outer Facts objects with unchanged field values cause no matcher calls.
|
||||
- One or several changed fields rerun exactly their readers; missing fields, losing candidates, no-match results, failures, and conditional read-set replacement remain covered.
|
||||
- Equivalent refreshes preserve node/array identity and do not invoke unrelated matchers; payload-only changes preserve unaffected nodes.
|
||||
- Insertions in plain text, losing new candidates, same-start winner changes, extending/shrinking matches, and removal of an early winner assert the exact affected query offsets and counts.
|
||||
- Removing or shrinking a winner exposes cached losers and unsearched gaps; an old tail no-match result cannot answer a query before its recorded origin.
|
||||
- Batched Facts updates equal a fresh evaluation under the same snapshot and do not give the first refreshed matcher precedence.
|
||||
- Text and rule-definition changes, registration/disposal, range validation, synchronous failure containment, Context lifetime, and independent evaluator state have deterministic tests.
|
||||
- Real Session/file/skill/command combinations preserve full quoted references, original word boundaries, and same-start precedence; whitespace candidates use exactly the same scheduling and cache rules.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Ordered whole-document business transforms.** A business can consume later text before an earlier reference from another business is considered, so fixed type order does not express leftmost recognition.
|
||||
|
||||
## Consequences
|
||||
|
||||
Field-level invalidation can require a matcher to traverse the document again; it does not promise per-element Facts invalidation. Query history consumes memory proportional to the distinct recorded search positions for the current text and definitions. Reconciliation and JSON identity alignment can still traverse output without invoking matchers. The performance evidence is the exact scheduling count and query offsets, while semantic evidence compares refreshed output with a fresh evaluator. This mechanism requires no browser end-to-end or elapsed-time benchmark tests.
|
||||
@@ -0,0 +1,91 @@
|
||||
# Agent Note: 用户文本候选调度与 Facts 驱动刷新
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](<2026-10-10-user-text-json-nodes.md>) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
用户文本包含独立业务拥有的引用。识别必须按原文位置推进,确定性地处理冲突,并在 Facts 变化时复用无关匹配工作。计算器必须能通过纯逻辑测试展示调度行为,不依赖 React、浏览器时序或耗时基准。
|
||||
|
||||
## 决策
|
||||
|
||||
采用业务注册、按需候选匹配、单一协调循环、只读 JSON 节点和业务拥有的 keyed renderer。初始化与 Facts 刷新使用同一个算法,只在查询缓存的有效性上存在差别。
|
||||
|
||||
### 规则职责与原始坐标
|
||||
|
||||
所有规则都提供 ID、有限的 order 和同步 `match(text, from, context)`。每次调用返回 `start >= from` 的最近有效节点,或确认剩余原文中没有候选。它返回一个节点,不返回全部匹配或整份替换文档。原始字符串及 UTF-16 坐标保持不变:搜索游标不创造新的词首、行首或带引号词元边界。匹配只依赖原文和 Facts,不依赖先前选中节点的 payload。
|
||||
|
||||
每个匹配器先按最早起点处理自身不同写法,再按该语法规定处理同起点选择;Session 标签选择较长匹配。匹配器不能把被消费前缀之后的半截重叠候选变成新匹配。语法、payload 和 renderer 均归对应业务所有,通用计算器不包含 Session、文件、skill 或命令分支。
|
||||
|
||||
只有一种规则类型和一个候选协议。候选既可以包含使用规则 ID 的业务节点,也可以包含保留原文子串、携带可选 `data.display` 的 Text 节点。产生 Text 的规则与业务规则按相同的起点、order 和稳定 ID 竞争。Order 仅裁定同起点冲突,任何规则都不优先于更早的位置。
|
||||
|
||||
### 单一协调循环
|
||||
|
||||
计算器保留原文、上一次成功分段、注册定义、逐规则 Facts 读取记录和查询结果。非空原文最初对应一个覆盖 `[0, text.length)` 的 Text,空原文没有节点。初始化使所有规则失效,包括不读取 Facts 的规则。
|
||||
|
||||
每次协调都从结果游标零开始,各匹配器通过同一个带缓存的查询操作获取一个候选。计算器选择起点最小的候选,同起点按规则 order 升序和稳定 ID 裁定,输出前面的原文 Text 空隙,输出赢家,再将游标推进到赢家末尾。全部没有候选时,输出剩余原文并结束识别。
|
||||
|
||||
只有被消费的候选或起点早于新游标的候选需要再次查询。缓存候选从新游标或其后开始时仍然有效;无候选结论在游标前进时保持有效。所有范围均是原文中的非空半开区间;候选不能使游标倒退或超出原文。
|
||||
|
||||
先前采纳的节点是可复用输出,不是所有可能匹配的完整清单;更早的赢家消失后,落败候选可能重新可见。计算器为此保留查询,而不能将旧 Text 节点或缺少某类业务节点无条件解释为没有匹配。
|
||||
|
||||
### 查询缓存与遍历
|
||||
|
||||
查询记录包含原始 `from` 偏移,以及下一个候选、无候选或失败。若从 `q` 查询得到起点为 `s` 的候选,则在请求游标 `c` 满足 `q <= c <= s` 时可以复用;无候选结论可用于所有 `c >= q`。两者都不能证明 `q` 之前的情况。失败查询只在请求偏移相同且依赖仍有效时复用。
|
||||
|
||||
一次协调过程中,每个匹配器只有一个活动候选。计算器同时保留该匹配器跨次协调已经产生的有序查询记录,不预先枚举所有可能匹配。前进游标单调遍历这些记录;新暴露空隙中的查询按原文顺序与旧记录合并,每个查询偏移只保留一条记录。未搜索区间必须执行匹配,不能虚构无匹配结论。
|
||||
|
||||
结果游标与匹配器搜索游标不同。结果协调从零开始,不代表无变化的匹配器需要从零重新扫描,它们的查询记录可以回答有效请求。各匹配器必须实现向前搜索,不能每次调用都重新收集剩余文本中的全部匹配。内部扫描、候选选择及结果比较仍有成本;调度保证针对实际匹配器调用,不宣称任意正则表达式都是线性的。
|
||||
|
||||
### 实际 Facts 依赖
|
||||
|
||||
`context.read(key)` 返回本次 Facts 快照中借用的只读值,并记录读取,包括 `undefined`。读取器仅在所属同步调用期间有效,不得保留。业务既不获得完整 Facts 对象,也不维护手工 `dependsOn` 列表或接收上一轮输出/缓存对象。
|
||||
|
||||
匹配器依赖集合是当前缓存代次所保留查询读取字段的并集,包括落败候选、无匹配查询和失败调用。字段变化按 `Object.is` 比较,外层 Facts 对象换引用不单独使匹配失效。规则失效时丢弃旧查询及读取集合,后续调用建立新的集合,因此已经不再读取的条件依赖不会永久残留。
|
||||
|
||||
产生 Text 的规则与其他规则共用查询缓存和依赖代次。空白开关更新只使它的读取者失效,不触发对已完成节点的无条件遍历。
|
||||
|
||||
### Facts、原文与注册刷新
|
||||
|
||||
Facts 更新只使实际读取变化字段的规则失效。它们的首次查询从原文零开始,其他规则保留查询记录;随后由同一个协调循环比较新候选和可复用候选。规则刷新不赋予额外优先权,新首个候选也不能掩盖更早旧赢家的消失。
|
||||
|
||||
新候选完全位于原有普通文字内时,可以拆分该 Text;无关匹配器的缓存仍覆盖新游标时,不调用它们。新候选输给更早的有效旧赢家时,丢弃新候选,不使旧赢家失效。新赢家覆盖其他候选时,只推进过期的候选头。赢家消失或缩短时,缓存的落败候选可以重新竞争;此前未搜索的区间在新暴露游标处触发查询。所有变化都使用同一个游标循环和查询覆盖规则。
|
||||
|
||||
多个 Facts 字段变化形成同一份快照和一个失效集合,所有受影响匹配器共同参与一次协调,不先提交某个更新再处理另一个。刷新结果等价时复用旧节点,所有元素相同则复用整个结果数组。协调在整份文档确定后发布结果。
|
||||
|
||||
原文变化会丢弃全部查询缓存,并以覆盖全文的 Text 作为上一版分段。新增或替换的规则定义从空缓存开始,无变化定义保留各自独立查询。规则移除时删除其依赖和候选。新增、移除或顺序变化后的协调仍使用相同选择逻辑,并可暴露此前被遮挡的候选。
|
||||
|
||||
### 输出、所有权与发布
|
||||
|
||||
节点保留 `{ id, start, end, data }`。业务节点使用所属匹配器 ID;Text 的 `data.text` 保留准确原文子串,`data.display` 可保存显示文字。输出有序、非重叠并完整覆盖原文。JSON 兼容性与只读所有权由 TypeScript 协议约束;计算器既不冻结借用对象,也不为隔离而深拷贝 Facts 或 payload。
|
||||
|
||||
文档计算器拥有全部缓存;注册表不持有计算器或文档订阅,不同文本实例不通过全局缓存共享状态。只有完整成功的求值才能发布新结果。匹配器失败时保留规则 ID 与 cause,不发布部分分段;实际读取仍参与依赖驱动的重试。禁止重入求值以及逃逸的同步读取 Context。
|
||||
|
||||
`conversation.user.text` 保持 session 作用域 Factory,拥有 keyed inline Slot `conversation.user.text.node`。React 消费最终节点,将 `raw` 和 `interactive` 交给业务 renderer。Session、skill、文件/目录和命令匹配仍归各插件所有,动作仍由各 renderer 的 Injection 提供,匹配与缓存订阅不进入 React。
|
||||
|
||||
`collapseWhitespace` 与 `trimWhitespace` 分别控制一个普通匹配器,缺省均关闭。Collapse 将空白串作为显示值为 `" "` 的 Text 候选;trim 将原文开头/末尾的空白串作为显示值为 `""` 的候选。Trim 的 order 小于 collapse,因此在同起点的边缘空白冲突中胜出。更早的引用候选消费自身完整范围,自然排除内部空白候选。每个 Text 候选保留准确的原文子串,emoji 及引用之间的空格保持不变。`interactive` 和 CSS `ellipsis` 仍为 React Props,不是匹配 Facts。Queue 使用完整原文和 CSS 省略,原生附件保留既有卡片及图片 Slot,不进入文本识别。
|
||||
|
||||
## 验证要求
|
||||
|
||||
纯逻辑测试同时断言最终节点,以及每个匹配器有序的调用和 `from` 偏移。使用确定性输入与 spy,不依赖时钟或性能阈值。
|
||||
|
||||
- 初始化调用所有匹配器,包括不读 Facts 和始终无匹配的规则;空输入具有明确的终止查询行为。
|
||||
- 较早起点胜过更强 order;同起点按 order 和稳定 ID 裁定,各匹配器保留自身最长匹配选择。
|
||||
- 向前遍历不重复查询未来候选及无匹配结论,只推进赢家和重叠候选头。
|
||||
- 未读取 Facts 字段变化,或字段值不变而外层 Facts 对象换引用,不调用匹配器。
|
||||
- 一个或多个字段变化只重跑实际读取者;覆盖缺失字段、落败候选、无匹配结果、失败和条件依赖集合替换。
|
||||
- 等价刷新保留节点/数组引用且不调用无关匹配器;仅 payload 变化保留无关节点。
|
||||
- 普通文字中插入、落败的新候选、同起点换赢家、匹配扩张/缩短、较早赢家移除,均断言准确的受影响查询偏移与次数。
|
||||
- 赢家移除或缩短后暴露缓存落败者及未搜索空隙;旧尾部的无匹配结论不能回答其查询起点之前的问题。
|
||||
- 批量 Facts 更新等于相同快照下的新建计算器结果,不赋予先刷新匹配器额外优先权。
|
||||
- 原文及规则定义变化、注册/注销、范围校验、同步失败隔离、Context 生命周期和计算器状态独立均有确定性测试。
|
||||
- 真实 Session/文件/skill/命令组合保留完整带引号引用、原始词边界和同起点优先关系;空白候选使用完全相同的调度和缓存规则。
|
||||
|
||||
## 备选方案
|
||||
|
||||
**按顺序处理整篇文档的业务变换。** 某业务可能在另一业务更早的引用获得考虑前消费后面的文本,因此固定类型顺序不能表达最左优先识别。
|
||||
|
||||
## 影响
|
||||
|
||||
字段级失效仍可能要求某个匹配器重新遍历文档,不承诺 Facts 字段内部逐元素失效。查询历史的内存量与当前原文、定义下记录的不同搜索位置成正比。协调及 JSON 引用对齐可能遍历输出而不调用匹配器。性能证据是准确的调度次数和查询偏移,语义证据是刷新结果与全新计算器的等价性。本机制不需要浏览器端到端测试或耗时基准测试。
|
||||
@@ -0,0 +1,25 @@
|
||||
# Bilingual-pair consistency record for 2026-10-11-chat-group-tree-exchanges.md (docs/i18n/README.md): per heading
|
||||
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
|
||||
# After editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-10-11-chat-group-tree-exchanges.md
|
||||
/agent-note-chat-group-tree-positions-and-exchanges:
|
||||
en: b26212eda152d6c6
|
||||
zh: a801a75ee0c07911
|
||||
/agent-note-chat-group-tree-positions-and-exchanges/problem:
|
||||
en: 84858bc0e1e268ab
|
||||
zh: 39ebb2979968d6cc
|
||||
/agent-note-chat-group-tree-positions-and-exchanges/decision:
|
||||
en: c8c4091863dec269
|
||||
zh: 57a0f26683867350
|
||||
/agent-note-chat-group-tree-positions-and-exchanges/why-mark-the-opening-rather-than-the-notice:
|
||||
en: db8d77cd773ad646
|
||||
zh: ab2f03b8fc58e952
|
||||
/agent-note-chat-group-tree-positions-and-exchanges/alternatives-considered:
|
||||
en: 635ddcb04fefe8e2
|
||||
zh: 360c171ccb613ed5
|
||||
/agent-note-chat-group-tree-positions-and-exchanges/consequences:
|
||||
en: c8cfdc3009b19b12
|
||||
zh: 36c541cd9692057d
|
||||
/agent-note-chat-group-tree-positions-and-exchanges/testing:
|
||||
en: 0d7f5281e66b0c39
|
||||
zh: 5be221de46e9e0f7
|
||||
Loaded 100 of 2839 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user