mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-08-29 04:26:38 +00:00
Merge remote-tracking branch 'origin/master' into worktree/2986-trajectory-image-attachments
This commit is contained in:
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-08-tool-output-spill-files.md
|
||||
2026-07-08-tool-output-spill-files.md: 14667b74ca877622d05196e9bf83945a842fe366
|
||||
2026-07-08-tool-output-spill-files.zh.md: db297fa6bee707a1d5a10d20260ce6b8a660d207
|
||||
2026-07-08-tool-output-spill-files.md: e14607e388c634c4e2679c993c1b720be0a3a9f3
|
||||
2026-07-08-tool-output-spill-files.zh.md: 372c9c6cadf3cd64c3de97a8c305b8909f03caab
|
||||
|
||||
@@ -57,7 +57,7 @@ interface SpillRef {
|
||||
|
||||
`SpillLocator` is a [branded](../../../../packages/util/brand) model-facing handle returned by the backend. The local backend renders it as a filesystem path; a remote or database backend can render a URI, key, or command token. Consumers treat it as opaque and render it with `retrievalHint` instead of assuming `read` is always the right retrieval mechanism. `SpillOwner.sessionId` is the save-time storage namespace: forked sessions inherit existing spill locators from the seeded log without copying or re-owning them, and new spills after the fork use the child session id. A retention-period cleanup may expire old locators with other old session artifacts; the spill seam does not define a per-session cleanup policy.
|
||||
|
||||
`dsh-spill-local` owns only storage details: session-scoped directory selection, safe names, path-traversal protection, the write, and returning `{ locator, bytes, retrievalHint }`. It does not own retention policy, tool-result replacement, search, or file inspection. Files land at `<root>/session-<hash>/<random>-<safeName>`, where `root` is a configured path or a lazily-created private (0700) per-process temp dir, the session subdir is a short `sha256(sessionId)` prefix, and the leaf is a random hex prefix plus the caller's `suggestedName` sanitized to one path segment (mirrors the JSONL backend's `encodeSegment`). The write is `open(path, 'wx', 0o600)` — exclusive and owner-only, so a planted symlink cannot redirect it. The locator is the path, and the retrieval hint tells the model it can use `read` or `grep` on that path.
|
||||
`dsh-spill-local` owns storage details: session-scoped directory selection, safe names, path-traversal protection, the write, local artifact lifetime, and returning `{ locator, bytes, retrievalHint }`. It does not own tool-result replacement, model-facing preview policy, search, file inspection, or a seam-wide/per-session retention policy. Files land at `<root>/session-<hash>/<random>-<safeName>`, where `root` is a configured path or a lazily-created private (0700) per-process temp dir, the session subdir is a short `sha256(sessionId)` prefix, and the leaf is a random hex prefix plus the caller's `suggestedName` sanitized to one path segment (mirrors the JSONL backend's `encodeSegment`). The write is `open(path, 'wx', 0o600)` — exclusive and owner-only, so a planted symlink cannot redirect it. The locator is the path, and the retrieval hint tells the model it can use `read` or `grep` on that path. Its one-shot startup cleanup applies the backend-specific artifact lifetime described in the [local spill cleanup note](./2026-07-17-local-spill-startup-cleanup.md).
|
||||
|
||||
### Spill policy
|
||||
|
||||
@@ -160,7 +160,8 @@ Those cases can consume `ctx.spillStore` directly in later work. They are not pa
|
||||
- Tool-owned spill for subagent rollouts (`await run.result`, read in-process child session before `run.dispose()`, save JSONL).
|
||||
- Per-tool opt-out or per-tool policy declarations if the built-in `read` skip is insufficient.
|
||||
- Remote or database storage backends for ACP or remote environments where a local path is not meaningful.
|
||||
- Cleanup and retention policy for old spill files, likely tied to session cleanup.
|
||||
|
||||
Cleanup shipped for the local backend as a one-shot startup sweep, not tied to session deletion — see the [startup-cleanup Agent Note](./2026-07-17-local-spill-startup-cleanup.md). The seam still defines no per-session cleanup policy; retention is a backend concern.
|
||||
|
||||
## Testing
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ interface SpillRef {
|
||||
|
||||
`SpillLocator` 是一个[品牌化的](../../../../packages/util/brand)模型可见句柄,由后端返回。本地后端将其渲染为文件系统路径;远程或数据库后端可以渲染 URI、键或命令 token。消费方把它视为不透明值,并使用 `retrievalHint` 渲染,而不是假定 `read` 始终是正确的检索机制。`SpillOwner.sessionId` 是保存时的存储命名空间:fork 后的会话会从种子日志继承已有的 spill 定位符,无需复制它们或重新取得所有权;fork 后的新 spill 使用子会话 id。保留期清理可以连同其他旧会话产物一起使旧定位符失效;spill seam 不定义逐会话的清理策略。
|
||||
|
||||
`dsh-spill-local` 只负责存储细节:选择会话作用域的目录、安全名称、防止路径遍历、执行写入,以及返回 `{ locator, bytes, retrievalHint }`。它不负责保留策略、工具结果替换、搜索或文件检查。文件写入 `<root>/session-<hash>/<random>-<safeName>`:`root` 是配置路径,或延迟创建的私有(0700)进程级临时目录;会话子目录是 `sha256(sessionId)` 的短前缀;叶节点由随机十六进制前缀与调用方的 `suggestedName` 组成,后者会被清理成单一路径段(与 JSONL 后端的 `encodeSegment` 一致)。系统使用 `open(path, 'wx', 0o600)` 写入,确保独占且仅所有者可访问,因此预先植入的符号链接无法重定向写入。定位符就是该路径,检索提示则告知模型可以在该路径上使用 `read` 或 `grep`。
|
||||
`dsh-spill-local` 负责存储细节:选择会话作用域的目录、安全名称、防止路径遍历、执行写入、本地产物生命周期,以及返回 `{ locator, bytes, retrievalHint }`。它不负责工具结果替换、模型可见的预览策略、搜索、文件检查,也不定义 seam 级或逐会话保留策略。文件写入 `<root>/session-<hash>/<random>-<safeName>`:`root` 是配置路径,或延迟创建的私有(0700)进程级临时目录;会话子目录是 `sha256(sessionId)` 的短前缀;叶节点由随机十六进制前缀与调用方的 `suggestedName` 组成,后者会被清理成单一路径段(与 JSONL 后端的 `encodeSegment` 一致)。系统使用 `open(path, 'wx', 0o600)` 写入,确保独占且仅所有者可访问,因此预先植入的符号链接无法重定向写入。定位符就是该路径,检索提示则告知模型可以在该路径上使用 `read` 或 `grep`。它的一次性启动清理会应用[本地 spill 清理说明](./2026-07-17-local-spill-startup-cleanup.zh.md)所述的后端专属产物生命周期。
|
||||
|
||||
### spill 策略
|
||||
|
||||
@@ -160,7 +160,8 @@ ctx.tools.register(defineTool({
|
||||
- 由工具负责的 subagent 执行轨迹 spill(`await run.result`,在 `run.dispose()` 前读取进程内子会话,保存 JSONL)。
|
||||
- 如果内置的 `read` 跳过规则不足,再增加逐工具选择退出或逐工具策略声明。
|
||||
- 面向 ACP(Agent Client Protocol)或远程环境的远程/数据库存储后端,因为本地路径在这些环境中没有意义。
|
||||
- 旧 spill 文件的清理和保留策略,很可能与会话清理绑定。
|
||||
|
||||
本地后端通过一次性启动扫描清理旧文件,而不是绑定到会话删除——参见[启动清理 Agent Note](./2026-07-17-local-spill-startup-cleanup.zh.md)。seam 仍未定义逐会话清理策略;保留策略属于后端。
|
||||
|
||||
## 测试
|
||||
|
||||
|
||||
+2
-2
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md
|
||||
2026-07-10-single-file-executable-sdk-runtime-distribution.md: e46dbbc119e2078e44632d81b333c8be5ab9d6d7
|
||||
2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md: d7a1e3f445ea1c1f03df2b349a4391534a5502c3
|
||||
2026-07-10-single-file-executable-sdk-runtime-distribution.md: 8731528b9ae600bb8bfe12738669f3a84c11a06b
|
||||
2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md: 755b4bd7ddbe9b88b4f40b8a3ae7419f746b8dde
|
||||
|
||||
+2
-2
@@ -44,13 +44,13 @@ The deploy root includes `@deepseek-ai/dsh-mcp-client` as an explicitly supporte
|
||||
|
||||
[`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.node-linker=hoisted --config.auto-install-peers=false --config.link-workspace-packages=true` **directly into** `python/sdk-runtime/src/deepseek_harness_runtime/runtime/node/` → 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 → inject pkg configuration whose bin is `node_modules/@deepseek-ai/dsh/lib/bin.js` and whose assets cover 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 into the runtime directory. 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`. All four deploy flags are grounded in measurement: `--legacy` is the mandatory path with inject-workspace-packages off; hoisted gives pkg a stable single-instance layout that the explicit materialization pass makes symlink-free; 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.
|
||||
|
||||
CI: [`.github/workflows/build-exe-for-python-sdk.yml`](../../../../.github/workflows/build-exe-for-python-sdk.yml) is called for all three targets by the [installed-wheel Python runtime pull-request validation](../testing/2026-08-23-installed-python-wheel-black-box-ci.md) and the [public publication workflow](../process/2026-08-11-python-publication-workflow.md); `workflow_dispatch` and the `build-exe` label can still select a subset. Native builds run on linux-x64 / linux-arm64 (`ubuntu-24.04-arm`) / macos-arm64, with `~/.pkg-cache` cached, and pkg handles macOS ad-hoc signing. Each leg installs the release-shaped SDK and runtime wheels into a clean venv outside the checkout, proves their package and executable provenance, then drives the complete keyless scenario set through the public SDK and direct NDJSON JSON-RPC. Trusted pull requests additionally run a real DeepSeek two-turn tool smoke on every target; 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 verifies that the executable's deployment target fits the wheel tag. A full three-target run retains four artifacts, each containing one release file: the platform-independent SDK wheel and three 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 three native runtime wheels, then a single serialized job checks and publishes all four to the project PyPI registry. Windows is a non-goal.
|
||||
CI: [`.github/workflows/build-exe-for-python-sdk.yml`](../../../../.github/workflows/build-exe-for-python-sdk.yml) is called for all four targets by the [installed-wheel Python runtime pull-request validation](../testing/2026-08-23-installed-python-wheel-black-box-ci.md) and the [public publication workflow](../process/2026-08-11-python-publication-workflow.md); `workflow_dispatch` and the `build-exe` label can still select a subset. Native builds run on linux-x64 / linux-arm64 (`ubuntu-24.04-arm`) / macos-arm64 / 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, proves their package and executable provenance, then drives the complete keyless scenario set through the public SDK and direct NDJSON JSON-RPC. Trusted pull requests additionally run a real DeepSeek two-turn tool smoke on every target; 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 verifies that the executable's deployment target fits the wheel tag. A full four-target run retains five artifacts, each containing one release file: the platform-independent SDK wheel and four 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 four native runtime wheels, then a single serialized job checks and publishes all five to the project PyPI registry. The [Windows x64 runtime decision](2026-08-23-python-sdk-windows-x64-runtime.md) owns the fourth 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 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`.
|
||||
|
||||
[`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 and its architecture-matched `-rg` sidecar, and the macOS wheel also contains its architecture-matched spawn helper. Runtime wheels use one of `py3-none-manylinux_2_28_x86_64`, `py3-none-manylinux_2_28_aarch64`, or the conservative `py3-none-macosx_14_0_arm64` tag for the Node 24 executable's macOS 13.5 deployment target; the Hatch hook rejects sdists, universal tags, mixed-platform payloads, missing or extra sidecars, and unsupported platforms.
|
||||
[`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 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`, the conservative `py3-none-macosx_14_0_arm64` tag for the Node 24 executable's macOS 13.5 deployment target, or `py3-none-win_amd64`; the Hatch hook rejects sdists, universal tags, mixed-platform payloads, missing or extra sidecars, and unsupported platforms.
|
||||
|
||||
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.
|
||||
|
||||
|
||||
+2
-2
@@ -44,13 +44,13 @@ exe 的 VFS 内是**构建产物形态的真实包树**(各包的 `lib/` + 真
|
||||
|
||||
[`scripts/build-exe-for-python-sdk.ts`](../../../../scripts/build-exe-for-python-sdk.ts):运行时闭包校验 → `pnpm run build` →(清空后)`pnpm --filter dsh-python-runtime-closure deploy --legacy --prod --config.node-linker=hoisted --config.auto-install-peers=false --config.link-workspace-packages=true` **直接写入** `python/sdk-runtime/src/deepseek_harness_runtime/runtime/node/` → 恢复 legacy deploy 遗漏的直接工作区包,并拒绝剩余的 manifest 缺口 → 将暂存依赖中的符号链接替换为目标文件内容,删除包管理器的 `.bin` 链接,并在仍有任何符号链接时失败 → 注入 pkg 配置,其中 bin 为 `node_modules/@deepseek-ai/dsh/lib/bin.js`,assets 覆盖动态读取的 profile、bundle、前端、preset、原生库与配置文件 → 暂存目标平台的 `node-pty` addon → 每个构建目标调用一次 `pkg --sea` → 将 `deepseek-harness-sdk-runtime-<platform>-<arch>` 写入 `dist-exe/` 并拷回运行时目录。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` 为 pkg 提供稳定的单实例布局,再由显式物化步骤消除符号链接;关闭对等依赖自动安装可防止未声明的对等依赖扩大闭包;`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 Python 运行时拉取请求验证](../testing/2026-08-23-installed-python-wheel-black-box-ci.zh.md)与[公开发布工作流](../process/2026-08-11-python-publication-workflow.zh.md)都会调用它构建全部三个目标;`workflow_dispatch` 与 `build-exe` 标签仍可选择部分目标。linux-x64、linux-arm64(`ubuntu-24.04-arm`)和 macos-arm64 三个平台分别进行原生构建,并缓存 `~/.pkg-cache`;macOS 的 ad-hoc 签名由 pkg 处理。每个平台都把发布形态的 SDK wheel 包与运行时 wheel 包安装到 checkout 外的干净 venv,证明包与可执行文件来源,再通过公开 SDK 与直接 NDJSON JSON-RPC 运行完整 keyless 场景。可信拉取请求还会在每个目标上运行真实 DeepSeek 双轮工具冒烟测试;fork 与 Dependabot head 不会获得密钥。Linux 会检查可执行文件和原生 addon 各自的 GLIBC 依赖,并额外运行 manylinux 2.28 冒烟测试;macOS 则验证可执行文件的部署目标符合 wheel 包标签。完整构建三个目标时保留 4 个产物,每个产物只含一个发布文件:平台无关的 SDK wheel 包与 3 个原生运行时 wheel 包;手动选择部分目标时保留 SDK wheel 与所选运行时 wheel。裸 exe 与源码包只作为测试中间输入。[`.gitlab-ci.yml`](../../../../.gitlab-ci.yml) 只接受版本与根目录 `package.json` 匹配的 `python-v<repository-version>` 标签流水线,构建一个 SDK wheel 包和 3 个原生运行时 wheel 包,再由单个串行任务校验并将这 4 个文件发布到项目的 PyPI 注册表。Windows 不在目标范围内。
|
||||
CI 使用 [`.github/workflows/build-exe-for-python-sdk.yml`](../../../../.github/workflows/build-exe-for-python-sdk.yml):[安装后 wheel Python 运行时拉取请求验证](../testing/2026-08-23-installed-python-wheel-black-box-ci.zh.md)与[公开发布工作流](../process/2026-08-11-python-publication-workflow.zh.md)都会调用它构建全部四个目标;`workflow_dispatch` 与 `build-exe` 标签仍可选择部分目标。linux-x64、linux-arm64(`ubuntu-24.04-arm`)、macos-arm64 与 win-x64(`windows-2025`)分别进行原生构建,并在适用平台缓存 `~/.pkg-cache`;macOS 的 ad-hoc 签名由 pkg 处理。每个平台都把发布形态的 SDK wheel 包与运行时 wheel 包安装到 checkout 外的干净 venv,证明包与可执行文件来源,再通过公开 SDK 与直接 NDJSON JSON-RPC 运行完整 keyless 场景。可信拉取请求还会在每个目标上运行真实 DeepSeek 双轮工具冒烟测试;fork 与 Dependabot head 不会获得密钥。Linux 会检查可执行文件和原生 addon 各自的 GLIBC 依赖,并额外运行 manylinux 2.28 冒烟测试;macOS 则验证可执行文件的部署目标符合 wheel 包标签。完整构建四个目标时保留 5 个产物,每个产物只含一个发布文件:平台无关的 SDK wheel 包与 4 个原生运行时 wheel 包;手动选择部分目标时保留 SDK wheel 与所选运行时 wheel。裸 exe 与源码包只作为测试中间输入。[`.gitlab-ci.yml`](../../../../.gitlab-ci.yml) 只接受版本与根目录 `package.json` 匹配的 `python-v<repository-version>` 标签流水线,构建一个 SDK wheel 包和 4 个原生运行时 wheel 包,再由单个串行任务校验并将这 5 个文件发布到项目的 PyPI 注册表。[Windows x64 运行时决策](2026-08-23-python-sdk-windows-x64-runtime.zh.md)负责第四个目标及对 Windows arm64 的明确排除。
|
||||
|
||||
### Python SDK 分发:双载体,exe 用于生产,`node` 用于开发
|
||||
|
||||
Python SDK 位于 [`python/`](../../../../python/README.zh.md):`python/sdk` 是客户端,`python/sdk-runtime` 是运行时载体包。运行时包的数据目录包含构建注入的平台可执行文件及其必需的 `-rg` 伴随文件和可选的 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`。
|
||||
|
||||
[`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 及其架构匹配的 `-rg` 伴随文件,macOS wheel 包还包含与其架构匹配的 spawn helper。运行时 wheel 包使用 `py3-none-manylinux_2_28_x86_64`、`py3-none-manylinux_2_28_aarch64`,或针对 Node 24 可执行文件 macOS 13.5 部署目标而保守选择的 `py3-none-macosx_14_0_arm64` 标签;Hatch 钩子拒绝 sdist、通用标签、混合平台载荷、伴随文件缺失或多余,以及不支持的平台。
|
||||
[`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 及其架构匹配的 ripgrep 伴随文件,macOS wheel 包还包含与其架构匹配的 spawn helper。运行时 wheel 包使用 `py3-none-manylinux_2_28_x86_64`、`py3-none-manylinux_2_28_aarch64`、针对 Node 24 可执行文件 macOS 13.5 部署目标而保守选择的 `py3-none-macosx_14_0_arm64` 标签,或 `py3-none-win_amd64`;Hatch 钩子拒绝 sdist、通用标签、混合平台载荷、伴随文件缺失或多余,以及不支持的平台。
|
||||
|
||||
Python 客户端使用所选 profile(默认 `sdk`)、有序 patch 文件和显式 Harness home 启动打包后的 `dsh` 命令。Profile 负责 JSON-RPC 服务和应用组合;缺失 home、profile、bundle、patch 或 server 配置项都会失败,不存在外部完整配置回退。
|
||||
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-17-local-spill-startup-cleanup.md
|
||||
2026-07-17-local-spill-startup-cleanup.md: fc64938c1af07d9dd0d7ecec379115d22d1e2464
|
||||
2026-07-17-local-spill-startup-cleanup.zh.md: 583a33ead84f552c67e2e770a8b3fabc3ce88120
|
||||
@@ -0,0 +1,37 @@
|
||||
# Agent Note: One-shot startup cleanup for local spill files
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-07-17-local-spill-startup-cleanup.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
The local spill backend never deleted the full tool results it wrote. Every oversized result added another file, so configured roots grew without bound and default per-process `dsh-spill-*` roots accumulated across runs. Immediate deletion is wrong because persisted, resumed, and forked sessions may still reference a locator. The [tool output spill policy](./2026-07-08-tool-output-spill-files.md) needs a bounded local-storage lifetime.
|
||||
|
||||
## Decision
|
||||
|
||||
`dsh-spill-local` runs one best-effort cleanup sweep after activation. It does not delay service availability, is owned by the plugin fiber (a single `ctx.effect` whose generator launches the sweep and yields an async disposer that awaits it), and is awaited during disposal so no sweep I/O outlives the fiber. There is no recurring timer and no separate process.
|
||||
|
||||
A `cleanupPeriodDays` config defaults to `30`; `0` disables cleanup. Schemastery rejects a negative or fractional value at load. The sweep scans the configured/active root plus any prior default `dsh-spill-*` temp roots discovered under the OS temp dir and deletes regular files whose `mtime` is strictly older than `now − cleanupPeriodDays`. It prunes every empty session directory but removes the root itself only for a discovered prior-default root; writes recreate a session directory if pruning races them. Root aliases are de-duplicated by device/inode identity, with the configured identity overriding a discovered match as active and non-prunable. It uses `lstat`, so a symlink is never followed or deleted; unrelated entries (non-`session-` directories, special files) are skipped. Every filesystem failure is caught and logged through `ctx.logger.warn`, and a warning-sink exception is also contained — the sweep never throws, so it cannot reject activation or a concurrent spill write.
|
||||
|
||||
Path-based deletion is restricted to directories an untrusted local OS user cannot replace during the scan. On POSIX, every root and session directory must be owned by the current user and not writable by group or others; the root's ancestor path must also be non-writable or protected by a sticky directory such as `/tmp`. Discovery rejects symlinks, while a configured symlink may resolve to a trusted target and participates in identity de-duplication. An unsafe path is skipped with a warning. The same-user account remains the trust boundary, consistent with the backend's private local-storage model.
|
||||
|
||||
The ctx-free sweep mechanics live in `packages/spill/spill-local/src/cleanup.ts` (`sweepSpillRoots`, `discoverDefaultRoots`), unit-testable without a `ctx`; `store.ts` owns root naming, path derivation, and writes, while the service in `src/index.ts` owns the config, cutoff, and fiber-owned launch/await.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Run a periodic timer.** Rejected because it adds timer lifecycle, overlap control, and another interval knob. A long-lived process may retain files until restart.
|
||||
|
||||
**Delete spills on session disposal.** Rejected because durable sessions, resumes, and forks retain locators.
|
||||
|
||||
**Delete old session directories recursively.** Rejected because a concurrent process may create a fresh spill after the age check. Per-file expiry preserves fresh writes.
|
||||
|
||||
**Tie cleanup to session-persistence deletion.** Rejected because the persistence seam has no common deletion lifecycle, while the local backend also owns independent temporary roots.
|
||||
|
||||
## Consequences
|
||||
|
||||
Cleanup cost the backend a startup sweep and a config knob, and bought a bounded local-storage lifetime without a timer, a daemon, or a session-lifecycle coupling. Concurrent processes may duplicate startup I/O; strict filtering and idempotent file deletion keep this safe. A long-lived process is not cleaned again until restart, and retention deliberately makes old model-visible locators stale only once they age past the cutoff. The seam itself still defines no retention policy — this is a local-backend concern.
|
||||
|
||||
## Testing
|
||||
|
||||
`dsh-spill-local` unit tests cover the exact age boundary, `cleanupPeriodDays: 0` disabling, empty-session and discovered-root pruning, symlink/unrelated-entry skipping, configured-plus-discovered-root coverage, filesystem-identity de-duplication through a configured symlink, unsafe POSIX root/session rejection, load-time config validation, filesystem- and warning-sink-failure containment, and the quiescence contract. A separate test boots the plugin through the real Loader and a cordis.yml, then observes configured expiry and directory pruning after disposal.
|
||||
@@ -0,0 +1,37 @@
|
||||
# Agent Note: 本地 spill 文件的一次性启动清理
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-07-17-local-spill-startup-cleanup.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
本地 spill 后端从不删除它写下的完整工具结果。每个超限结果都会新增一个文件,因此配置的根目录会无限增长,而每进程默认的 `dsh-spill-*` 根目录也会跨多次运行不断累积。立即删除是错误的,因为已持久化、已恢复和已 fork 的会话仍可能引用某个 locator。[工具输出 spill 策略](./2026-07-08-tool-output-spill-files.zh.md)需要一个有界的本地存储生命周期。
|
||||
|
||||
## 决策
|
||||
|
||||
`dsh-spill-local` 在激活后运行一次尽力而为的清理扫描。它不延迟服务可用性,由插件 fiber 拥有(一个 `ctx.effect`,其生成器启动该扫描并让出一个等待它的异步 disposer),并在 dispose 期间被等待,因此没有扫描 I/O 会存活到 fiber 之后。既没有周期性定时器,也没有独立进程。
|
||||
|
||||
`cleanupPeriodDays` 配置默认为 `30`;`0` 会禁用清理。Schemastery 会在加载时拒绝负数或小数。扫描会遍历配置的/活动的根目录,以及在 OS 临时目录下发现的任何先前默认 `dsh-spill-*` 临时根目录,并删除 `mtime` 严格早于 `now − cleanupPeriodDays` 的常规文件。它会修剪所有空会话目录,但只删除发现的先前默认根目录本身;如果修剪与写入发生竞争,写入操作会重新创建会话目录。根目录别名按设备/inode 身份去重,配置目录的身份会覆盖发现的匹配项,并标记为活动且不可删除。扫描使用 `lstat`,因此符号链接绝不会被跟随或删除;无关条目(非 `session-` 目录、特殊文件)会被跳过。每一次文件系统失败都会被捕获并通过 `ctx.logger.warn` 记录,警告接收方抛出的异常也会被兜底——扫描绝不抛出,因此它无法让激活失败,也无法影响并发的 spill 写入。
|
||||
|
||||
基于路径的删除仅限于不受信任的本地 OS 用户无法在扫描期间替换的目录。在 POSIX 上,每个根目录和会话目录都必须由当前用户拥有,且组用户和其他用户不可写;根目录的祖先路径也必须不可写,或由 `/tmp` 这类 sticky 目录保护。发现过程拒绝符号链接,而配置的符号链接可以解析到可信目标并参与身份去重。不安全路径会被跳过并记录警告。与后端的私有本地存储模型一致,同一用户账号仍是信任边界。
|
||||
|
||||
无 ctx 依赖的扫描机制位于 `packages/spill/spill-local/src/cleanup.ts`(`sweepSpillRoots`、`discoverDefaultRoots`),无需 `ctx` 即可做单元测试;`store.ts` 负责根目录命名、路径推导与写入,而 `src/index.ts` 中的服务负责配置、截止时间以及 fiber 拥有的启动/等待。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
**运行周期性定时器。** 已否决,因为它引入了定时器生命周期、重叠控制以及又一个间隔旋钮。长期运行的进程可能会保留文件直到重启。
|
||||
|
||||
**在会话 dispose 时删除 spill。** 已否决,因为持久会话、恢复和 fork 都会保留 locator。
|
||||
|
||||
**递归删除旧的会话目录。** 已否决,因为并发进程可能在年龄检查之后创建一个新的 spill。按文件过期可保留新写入。
|
||||
|
||||
**将清理绑定到会话持久化删除。** 已否决,因为持久化 seam 没有共同的删除生命周期,而本地后端还独立拥有临时根目录。
|
||||
|
||||
## 后果
|
||||
|
||||
清理让后端付出了一次启动扫描和一个配置旋钮的代价,换来了无需定时器、守护进程或会话生命周期耦合的有界本地存储生命周期。并发进程可能重复启动 I/O;严格的过滤与幂等的文件删除保证了这一点的安全。长期运行的进程在重启前不会再次被清理,而这种保留是刻意的——旧的模型可见 locator 只有在超过截止时间后才会失效。seam 本身仍不定义任何保留策略——这是本地后端的关切。
|
||||
|
||||
## 验证
|
||||
|
||||
`dsh-spill-local` 单元测试覆盖了精确年龄边界、`cleanupPeriodDays: 0` 的禁用、空会话目录与发现根目录的修剪、符号链接/无关条目的跳过、配置根加发现根的覆盖、经配置符号链接验证的文件系统身份去重、不安全 POSIX 根目录/会话目录拒绝、加载期配置校验、文件系统与警告接收方故障兜底,以及静止契约。另一个测试会通过真实 Loader 和 cordis.yml 启动插件,并在 dispose 后观察按配置执行的过期与目录修剪。
|
||||
+2
-2
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-23-client-plugin-loading-model.md
|
||||
2026-07-23-client-plugin-loading-model.md: 02dadf6e1dc1f2c4fd99907446bc6d07b35ba471
|
||||
2026-07-23-client-plugin-loading-model.zh.md: eaf10d32a6b51189867d2a52f76dc190380cbca0
|
||||
2026-07-23-client-plugin-loading-model.md: bd6f6e58c571102afc789ef57085db1e302158cc
|
||||
2026-07-23-client-plugin-loading-model.zh.md: 256b57102bbec6f793d48d0bdaf60445b194ecdf
|
||||
|
||||
@@ -14,7 +14,7 @@ The browser client runs the same cordis plugin mechanism, so it needs the same s
|
||||
|
||||
Conventional frontend engineering digests all dependencies at build time: one bundle, externals resolved by the bundler, nothing left to manage at runtime. Runtime module management on top of that is the unusual requirement here. The client therefore splits into two layers: the upper layer is cordis plugin loading through the same vendored Loader, and the lower layer is module-granular dependency management — `dsh-client-modules`.
|
||||
|
||||
The lower layer supplies four capabilities: externals (the platform list), remote arrival (same-origin external classic scripts plus lazy factory registration), versioning (content-hash revs), and hot update (invalidate/prefetch).
|
||||
The lower layer supplies four capabilities: externals (the platform list), remote arrival (same-origin external classic scripts plus lazy factory registration), immutable revisioned delivery, and hot update (invalidate/prefetch).
|
||||
|
||||
Plugin bundles are built independently outside Vite's module graph. Feeding response text into an inline script leaves the browser with a dynamic source execution: no standard source-map chain connects the network resource, generated bundle, and TypeScript/TSX source, so performance profiles and stacks stop at generated `client.js`; the module system must also buffer the complete source and split one arrival responsibility across fetch and execute transport boundaries.
|
||||
|
||||
@@ -28,7 +28,7 @@ The first-generation client loader (`createClientLoader`) hand-wrote both layers
|
||||
|
||||
The [client shell layering note](2026-08-15-client-shells-and-dynamic-packages.md) defines the current static and dynamic package sets and the import rules between them. The loading machinery treats every `dsh.client` package as a host-graph row with one ordinary `lib/client.js` factory bundle. Its declaration carries Cordis `inject` edges, synchronous module-table `external` requests, and the optional `immediately` prefetch mark; the composing app owns only the mounted roster.
|
||||
|
||||
The web kernel remains framework-free and imports no dynamic package value. Modules is itself a dynamic row, but the host parser delivers its ordinary factory before the Vite main module. The HTML-installed `__ModuleLoader__` facade uses that factory to construct the module system when the kernel calls `create()`. Runtime arrives through the same pending queue; static React, Cordis, and UI library identities come from the shell seed.
|
||||
The web kernel remains framework-free and imports no dynamic package value. Modules is itself a dynamic row, but the host parser delivers its factory before the Vite main module. The HTML-installed `__ModuleLoader__` facade uses that factory to construct the module system when the kernel calls `create()`. Every other dynamic row belongs to an application combo script; static React, Cordis, and UI library identities come from the shell seed.
|
||||
|
||||
### One module system, one plugin governor
|
||||
|
||||
@@ -38,13 +38,13 @@ The browser mirrors the host's division of labor. `dsh-client-modules` (`ClientM
|
||||
|
||||
The vendored Loader consumes the module system through its `internal` contract — the only call site is `tree.import` — and owns everything entry-shaped: entry creation, fiber activation through cordis service waiting (PENDING until injected services exist, cascading when a service is provided), update/refresh, teardown. The governance code is byte-identical to the host side, per vendor policy. Browserization is compile-time mapping in the shell's vite config: a `node:module` stub alias plus `process.*` defines make `ModuleLoader.fromInternal()` return undefined — exactly the empty slot the shell fills. The module system mounts as `ctx.modules`.
|
||||
|
||||
### External-script arrival and source maps
|
||||
### Combo external-script arrival and source maps
|
||||
|
||||
Each graph row's `url` goes to a same-origin external classic `<script src>` with `async` set. The browser owns the network request and script execution; the node is removed as soon as `load` or `error` settles so HMR cannot accumulate dead nodes. Successful settlement also requires the graph row's factory id to exist in the module table, or arrival fails; registration still does not run the factory, so the side-effect boundary remains first materialization.
|
||||
The Host snapshots every built plugin artifact and partitions each scheduling phase's ordered rows into one or more same-origin classic scripts. It greedily fills each group while the longer map-form request URL remains within 3 KiB, preserving graph order and allowing another request instead of emitting an oversized URL. Each script is addressed by its package resources, for example `/plugins/??<package-a>/client.js,<package-b>/client.js&rev=<rev>`. The `bootstrap` and `application` values are scheduling phases in the graph, not URL components: HTML preloads every application URL before executing every parser-blocking bootstrap URL. The module system keys in-flight transport by combo URL, so concurrent row arrivals within one group execute one script. Successful settlement still requires each requested row's factory id to exist in the module table, and registration does not run the factory, so the side-effect boundary remains first materialization.
|
||||
|
||||
The shared tsdown preset emits `client.js.map` for every plugin and rewrites first-party source paths into the browser-resolvable repository shape `/packages/<group>/<package>/src/...`. Other workspace sources inlined into a bundle likewise resolve to their `packages/` owner, while dependency paths remain unchanged; `sourcesContent` carries the source, so the host only serves the map at `/plugins/<id>/client.js.map` and exposes no source route. The Vite shell also emits source maps, letting both shell code and out-of-graph plugins map stacks and performance profiles back to TypeScript/TSX.
|
||||
The shared tsdown preset emits `client.js.map` for every plugin and rewrites first-party source paths into the browser-resolvable repository form `/packages/<group>/<package>/src/...`. The production Client pass consumes `lib/types`; the preset supplies each tsc map to Rolldown and fills `sourcesContent` from the original files, so the final map reaches TypeScript/TSX instead of stopping at emitted JavaScript. Other workspace sources inlined into a bundle likewise resolve to their `packages/` owner, while dependency paths remain unchanged. Combo generation strips each local debug directive, records its generated-line offset, resolves every authored source against the original per-plugin map URL, and emits an Indexed Source Map v3. An authored map supplies its section; otherwise an identity section embeds the generated bundle and uses the packer's `sourceURL` as its source name when present. The absolute map URL mirrors the script resource list by changing every `client.js` suffix to `client.js.map`, so `/plugins/??<package-a>/client.js,<package-b>/client.js&rev=<rev>` points to `/plugins/??<package-a>/client.js.map,<package-b>/client.js.map&rev=<rev>`. One resource follows the same rule and still produces an indexed map with one section. The Vite shell also emits source maps, letting shell code and combo-loaded plugins map stacks and performance profiles back to TypeScript/TSX.
|
||||
|
||||
`rev` remains the script URL's query parameter and content-consistency anchor, and the bundle and map are both served with `no-cache`. An external script's `error` event exposes neither response status nor body, so failure diagnostics name only the URL; the same-origin host and build-stamped registration id form the identity boundary, while the post-`load` factory-presence check rejects an artifact that did not register the expected id.
|
||||
The graph retains each row's revisioned one-resource combo URL for HMR and adds a content-addressed descriptor for every startup combo request; several descriptors may carry the same scheduling phase. Initial row revisions are opaque process nonces rather than content hashes; they keep the snapshotted one-resource response immutable without hashing every plugin at startup. After the watcher observes one artifact change, `rebuilt(id)` hashes only that bundle and map and publishes the resulting revision. Startup combo revisions cover the combined script inputs and indexed map. Versioned scripts and maps use immutable caching. The Host serves only exact generated URLs; stale revisions and unadvertised resource lists return 404 instead of aliasing different bytes. An external script's `error` event exposes neither response status nor body, so failure diagnostics name only the URL; the same-origin Host and build-stamped registration id form the identity boundary, while the post-`load` factory-presence check rejects an artifact that did not register the expected id.
|
||||
|
||||
### The loading flow, end to end
|
||||
|
||||
@@ -53,12 +53,12 @@ What happens between `dsh web` starting and the UI appearing? Three stages: the
|
||||
**Host side — compose the graph.**
|
||||
|
||||
1. The composing app (`apps/cli`) ships the roster as ordinary rows in its `cordis.yml` config tree — client plugin packages are entry rows like every host plugin, including the always-mounted `client-hmr` row. A roster row that fails to import is caught by `assertEntriesLoaded`; a row whose fiber rejects is reported with its original stack by `assertEntriesActivated` ([host boot decision](2026-07-24-web-config-tree-boot-and-transport-layering.md)).
|
||||
2. The `dsh-client-modules` node half (the package is dual-face: its browser half is the module table) scans loader entries' package.json `dsh.client` declarations and composes `window.__DSH_BOOT__`: `{ rev, entries: [{ id, url, rev, inject?, immediately?, external? }] }`. The three optional fields come from manifests, never hand-copied. Composition orders requested dynamic rows before their consumers and rejects synchronous request cycles. It refuses declared plugins without built `./client` bundles and groups their package/path rows under one required source-build instruction; malformed declaration fields also fail activation, and the host audit reports either error from the FAILED fiber.
|
||||
3. Scanning is incremental per package — there is no full-rescan code path. Each cordis `internal/plugin` emission marks the fiber's entry name dirty (entry-less fibers drop O(1)); a microtask flush reconciles each dirty name against live loader entries, with package metadata (including the negative "not a client package" verdict) cached per name forever and bundle re-hashing reachable only through `rebuilt(id)`. The activation pass seeds the same dirty set from current entries and flushes synchronously, so first scan and steady state share one implementation. Each bundle's content hash is its `rev` (cache busting + HMR diff anchor), the row set hashes into `graph.rev`, and every row is served as a script resource at `/plugins/<id>/client.js?rev=…`, with its source map at the same path plus `.map`. The graph types are single-sourced in the modules package's `./client` export — the webserver knows nothing about the graph (it is a plain route-registration plugin; modules registers the bundle route and taps the index render itself).
|
||||
2. The `dsh-client-modules` node half (the package is dual-face: its browser half is the module table) scans loader entries' package.json `dsh.client` declarations and composes `window.__DSH_BOOT__`: `{ rev, entries: [{ id, url, rev, inject?, immediately?, external? }], batches: [{ phase, url, rev, entries }] }`. The row's three optional fields come from manifests, never hand-copied. Composition orders requested dynamic rows before their consumers, rejects synchronous request cycles, and assigns every row to exactly one initial batch. It refuses declared plugins without built `./client` bundles and groups their package/path rows under one required source-build instruction; malformed declaration fields also fail activation, and the Host audit reports either error from the FAILED fiber.
|
||||
3. Scanning is incremental per package — there is no full-rescan code path. Each cordis `internal/plugin` emission marks the fiber's entry name dirty (entry-less fibers drop O(1)); a microtask flush reconciles each dirty name against live loader entries, with package metadata (including the negative "not a client package" verdict) cached per name forever and bundle re-hashing reachable only through `rebuilt(id)`. The activation pass seeds the same dirty set from current entries and flushes synchronously, so first scan and steady state share one implementation. Initial rows receive an opaque process nonce plus sequence without hashing their artifacts; startup combo revisions hash the combined script inputs plus indexed map, and the rows plus batch descriptors hash into `graph.rev`. The graph types are single-sourced in the modules package's `./client` export — the webserver knows nothing about the graph, while modules registers the combo route and contributes structured index-injection rows.
|
||||
|
||||
Why is the roster yml rows and not a scan? Because which plugins compose into a deployment is a composition decision, not a package property — a package declaring `dsh.client` in the repo does not mean this deployment mounts it, so discovery-by-scan cannot make that call; the node half scans only what the tree actually mounted.
|
||||
|
||||
**Phase one — the module face.** The injected HTML installs `window.__ModuleLoader__` in queue mode, executes the modules and runtime graph rows as blocking classic scripts, assigns `window.__DSH_BOOT__`, and then starts the Vite main module. The kernel calls the facade's `create()` with the raw graph and shell seeds. The facade removes and materializes the modules registration with a bootstrap `require` that rejects every external, then calls its `createClientModuleSystem` export. The modules bundle parses the graph, constructs the system, memoizes its own exports, and retains the instance in its module closure; construction switches the same facade to live registration before draining runtime's pending factory. The kernel then prefetches every `immediately` row in parallel; prefetch recursively registers declared dynamic requests and the row itself without materializing either. A row's prefetch failure is swallowed here because phase two's import retries and owns the loud failure. `immediately` remains an arrival mark, not a lifecycle barrier or package identity.
|
||||
**Phase one — the module face.** The injected HTML installs `window.__ModuleLoader__` in queue mode, starts preloading every application combo URL, executes every bootstrap combo URL as a blocking classic script, assigns `window.__DSH_BOOT__`, and then starts the Vite main module. The kernel calls the facade's `create()` with the raw graph and shell seeds. The facade removes and materializes the modules registration with a bootstrap `require` that rejects every external, then calls its `createClientModuleSystem` export. The modules bundle parses the graph, constructs the system, memoizes its own exports, retains the instance in its module closure, and switches the same facade to live registration. The kernel then prefetches every `immediately` row in parallel. Rows in the same application combo share its execution; separate combos load independently when an immediate row, a requested dependency, or ordinary entry import reaches them. A prefetch failure is swallowed here because phase two's import retries and owns the loud failure. `immediately` remains a registration barrier, not a package identity.
|
||||
|
||||
**Phase two — the plugin face.**
|
||||
|
||||
@@ -72,19 +72,19 @@ Why is the roster yml rows and not a scan? Because which plugins compose into a
|
||||
|
||||
Hot reload is a composition decision: the web bundle mounts the `client-hmr` row (a normal plugin package) unconditionally; its node half brings the bundle watch and the SSE channel, and the chain stays idle until a rebuild watcher rewrites client bundles. A composition that must not expose it disables the row.
|
||||
|
||||
How does a rebuilt bundle become a reload signal? The hmr node half observes it itself — no builder tells it. It reads bundle paths from `ctx.clientModules.clientPath(id)`, and one HMR-owned interval stat-polls every current graph row. Adding a row is ordered as synchronous stat baseline, then immediate `clientModuleHost.rebuilt(id)`: a write after the module host's graph hash but before that baseline is caught by the immediate re-hash, while a write after the baseline leaves a stat delta for the next poll. This avoids `fs.watchFile`, whose asynchronous first baseline can silently absorb a construction-time rebuild. Watch membership follows `onGraphChanged`; vanished rows drop out, and a bundle missing at poll time keeps its row dirty so reappearance forces a re-hash even with identical metadata. On a mtime/size delta or dirty row, `clientModuleHost.rebuilt(id)` is the single re-hash entry point; when the `rev` actually changed, the node half broadcasts a `rebuilt` frame on `GET /plugins/events` — a system SSE channel that sends the full graph on connect and `rebuilt` frames on change, presentation-only wire that never enters the session log. Polling is deliberate because inotify does not fire on the weka network mount, the same reason the build-side watcher needs `--poll`; the interval is a validated config field (default 500ms), and disposal clears the one timer. Rebuilding bundles is any tsdown watch process's business — `scripts/dev-web.ts` remains the watch-build entry point, discovering its package list through `dsh.client` while scanning `packages/*/*/package.json` at startup — and builder and host share zero protocol. A torn read self-heals: stats keep changing while the write completes, so the next poll re-hashes and broadcasts the final rev.
|
||||
How does a rebuilt bundle become a reload signal? The hmr node half observes it itself — no builder tells it. Before reading each startup snapshot, the module host captures the bundle and optional-map stat baseline and exposes it through `ctx.clientModules.artifactBaseline(id)`. One HMR-owned interval compares every current graph row with that baseline. An unchanged row starts watching without a content read or hash; a write after baseline capture is already a stat delta and only that row enters `rebuilt(id)`. This avoids both an initial all-row re-hash and `fs.watchFile`, whose asynchronous first baseline can silently absorb a construction-time rebuild. Watch membership follows `onGraphChanged`; vanished rows drop out, and a bundle missing at poll time keeps its row dirty so reappearance forces a re-hash even with identical metadata. On a script/map mtime or size delta, or a dirty row, `rebuilt(id)` is the single re-hash entry point; when the `rev` actually changed, the node half broadcasts a `rebuilt` frame on `GET /plugins/events` — a system SSE channel that sends the full graph on connect and `rebuilt` frames on change, presentation-only wire that never enters the session log. Polling is deliberate because inotify does not fire on the weka network mount, the same reason the build-side watcher needs `--poll`; the interval is a validated config field (default 500ms), and disposal clears the one timer. Rebuilding artifacts is any tsdown watch process's business — `scripts/dev-web.ts` remains the watch-build entry point, discovering its package list through `dsh.client` while scanning `packages/*/*/package.json` at startup — and builder and host share zero protocol. A torn read self-heals: stats keep changing while the write completes, so the next poll re-hashes and broadcasts the final rev.
|
||||
|
||||
On the browser side, the driver reloads one plugin per frame, serialized:
|
||||
|
||||
1. `invalidate` — drop the stale factory and record. A live factory would make the next step a no-op.
|
||||
2. `prefetch` — load the external script and register the fresh factory, while the old fiber still serves.
|
||||
1. `invalidate` — drop the stale factory and record, and bind the rebuilt frame's revision to that row's one-resource combo URL. A live factory would make the next step a no-op.
|
||||
2. `prefetch` — load that one-resource external script and register the fresh factory while the old fiber still serves. The initial multi-resource script never executes again.
|
||||
3. `registry.delete` — before touching the fiber. A bare fiber dispose trips the vendored Loader's self-dispose branch, which would disable the entry permanently.
|
||||
4. Drain the old fiber's disposers.
|
||||
5. Remove owned `<style data-plugin>` tags.
|
||||
6. `entry.refresh()` — re-imports, materializing the fresh factory. CSS re-injects here, under the same stable tag ids.
|
||||
7. `fiber.await()` — rethrows loud.
|
||||
|
||||
Every plugin shares this one semantics; an `immediately` row reloads exactly like a lazy one. Dependency cascade costs zero client code: a fiber's activation epoch strings its service providers' uids, so replacing a provider's fiber re-loads every dependent through cordis itself. Reloading connection or runtime cascades the whole UI — correct, if heavy.
|
||||
Every plugin shares this one semantics; an `immediately` row reloads exactly like a lazy one. Dependency cascade costs zero client code: a fiber's activation epoch strings its service providers' uids, so replacing a foundational provider such as connection re-loads every dependent through cordis itself — correct, if heavy.
|
||||
|
||||
The support boundary, stated honestly. Reload is coarse by design: fresh fiber, fresh components, React state lost, data layer untouched — react-refresh-grade state preservation conflicts with "re-executing the bundle re-runs the factory" and is deliberately out. Static assembly packages and the shell kernel are not entries: changing them means a shell rebuild and a full page reload. Reload has no rollback: an import failure leaves the entry fiberless and the next rebuilt frame retries from scratch; an apply failure leaves a FAILED fiber for the status projection; both log loudly. Self-reload works — the in-flight reload finishes in the old bundle's closure and the new apply opens a fresh SSE channel — but frames arriving in the gap are lost, and the next rebuild renotifies. One known dev-only race: a rebuilt frame overlapping a still-in-flight boot arrival shares that arrival's task and may materialize the pre-rebuild bytes; the next frame self-heals.
|
||||
|
||||
@@ -96,7 +96,7 @@ The current package inventory and build forms live in the [client shell layering
|
||||
|
||||
One governance implementation runs on both sides of the wire; the browser-specific layer is one module system plus one reload plugin. Dynamic packages have one artifact form, so the purity check covers them all. Cordis dependencies, module requests, and the boot tier live with their owners — the manifests — while the composing app holds only the roster. Host graph validation and recursive request arrival keep synchronous factory dependencies explicit. Browser-native script loading preserves the standard mapping among plugin network resources, generated bundles, and TypeScript/TSX sources, while the module system keeps only one replaceable `loadBundle` hook.
|
||||
|
||||
Costs accepted: the vendored Loader carries idle machinery in the browser (EntryTree persistence is a no-op, groups/isolation unused); every plugin edit in dev pays a bundle rebuild plus fiber remount; graph `inject` rows are informational — activation truth is service-level — so a mismatch appears at the settled sweep, not at graph validation; the static UI libraries keep direct value exports; every bundle gains a source-map artifact; and external-script failures provide only coarse URL diagnostics instead of the HTTP status available to an explicit fetch.
|
||||
Costs accepted: the vendored Loader carries idle machinery in the browser (EntryTree persistence is a no-op, groups/isolation unused); every plugin edit in dev pays a bundle rebuild plus fiber remount; graph `inject` rows guide factory arrival but service availability remains the activation authority, so a mismatch appears at the settled sweep; the static UI libraries keep direct value exports; every bundle gains a source-map artifact; and external-script failures provide only coarse URL diagnostics instead of the HTTP status available to an explicit fetch. The Host retains per-plugin bundle/map snapshots, generated one-resource responses, current startup combo responses, and one previous startup generation, so memory scales as several copies of the composed client artifacts. This retained state keeps URLs immutable and lets an in-flight request finish across one HMR recomposition.
|
||||
|
||||
Roster: it lives in the web bundle's config tree (`packages/bundle/web-app/cordis.patch.yml`); `mountWebPlugins` and the `CLIENT_PACKAGES` constant are gone, and recomposing a deployment means swapping the yml/overlay. The graph composer lives in the `dsh-client-modules` node half, while the parser-preloaded client face bootstraps the browser module table. The webserver remains a plain route-registration plugin; `/api/*` binding belongs to the connection node half over `api-gateway` (`dsh-host-apiproxy` providing `ctx.apiProxy`), and the dev bundle watch plus SSE channel belongs to the hmr node half.
|
||||
|
||||
|
||||
+14
-14
@@ -14,7 +14,7 @@ host 侧,cordis 插件装载站在 Node 的模块机制之上——require cac
|
||||
|
||||
常规前端工程在构建期消化全部依赖:单一 bundle,external 由打包器解决,运行时无物可管。在此之上再做运行时模块管理,正是这里的特殊需求。client 因此拆成两层:上层是经同一份 vendored Loader 的 cordis 插件装载,下层是模块粒度的依赖管理——`dsh-client-modules`。
|
||||
|
||||
下层供给四项能力:external(平台清单)、远程到达(同源外部 classic script 加惰性工厂登记)、版本化(内容哈希 rev)、热更新(invalidate/prefetch)。
|
||||
下层供给四项能力:external(平台清单)、远程到达(同源外部 classic script 加惰性工厂登记)、不可变的版本化交付、热更新(invalidate/prefetch)。
|
||||
|
||||
插件 bundle 独立构建在 Vite 模块图之外。若把响应文本塞进内联 script,浏览器只能看到一次动态源码执行:网络资源、生成 bundle、TypeScript/TSX 源码之间没有标准 sourcemap 链,性能 profile 与 stack 只能落到生成后的 `client.js`;模块系统还要持有整份源码文本,并把同一项到达职责拆成 fetch 与 execute 两道传输边界。
|
||||
|
||||
@@ -28,7 +28,7 @@ host 侧,cordis 插件装载站在 Node 的模块机制之上——require cac
|
||||
|
||||
[Client 外壳分层 Note](2026-08-15-client-shells-and-dynamic-packages.zh.md)定义当前的静态、动态包集合及其 import 规则。装载机件把每个 `dsh.client` 包视为一个 host graph row,且每个包只有一个普通 `lib/client.js` factory bundle。包声明携带 Cordis `inject` 边、同步模块表 `external` 请求,以及可选的 `immediately` 预取标记;负责组合的 app 只拥有挂载名册。
|
||||
|
||||
Web 内核保持不依赖框架,也不 import 任何动态包实体。Modules 本身是动态图 row,但 host parser 会在 Vite 主模块前送达其普通 factory。内核调用 `create()` 时,由 HTML 安装的 `__ModuleLoader__` facade 使用该 factory 构造模块系统。Runtime 经同一个 pending queue 到达;React、Cordis 与静态 UI 库的身份由外壳 seed 提供。
|
||||
Web 内核保持不依赖框架,也不 import 任何动态包实体。Modules 本身是动态图 row,但 host parser 会在 Vite 主模块前送达其 factory。内核调用 `create()` 时,由 HTML 安装的 `__ModuleLoader__` facade 使用该 factory 构造模块系统。其他每个动态图 row 都归属一个 application combo 脚本;React、Cordis 与静态 UI 库的身份由外壳 seed 提供。
|
||||
|
||||
### 一套模块系统,一个插件治理器
|
||||
|
||||
@@ -38,13 +38,13 @@ Web 内核保持不依赖框架,也不 import 任何动态包实体。Modules
|
||||
|
||||
vendored Loader 经其 `internal` 约定消费模块系统——唯一调用点是 `tree.import`——并拥有一切 entry 形状的事务:entry 创建、fiber 经 cordis 服务等待的激活(注入的服务未就位即保持 PENDING,服务 provide 时级联激活)、update/refresh、拆除。治理代码按 vendor 政策与 host 侧逐字节相同。浏览器化是壳 vite 配置里的编译期映射:一个 `node:module` stub 别名加若干 `process.*` define,使 `ModuleLoader.fromInternal()` 返回 undefined——这正是留给壳来填的空槽。模块系统挂载为 `ctx.modules`。
|
||||
|
||||
### 外部脚本到达与源码映射
|
||||
### Combo 外部脚本到达与源码映射
|
||||
|
||||
每个图行的 `url` 交给一个带 `async` 的同源外部 classic `<script src>`。浏览器拥有网络请求与脚本执行;`load` 或 `error` 结算后节点立即移除,避免 HMR 累积失效节点。成功结算还要求图行对应的工厂 id 已出现在模块表中,否则到达失败;登记仍不运行工厂,副作用边界继续落在首次物化。
|
||||
Host 会快照每个已构建插件产物,并把每个调度阶段的有序 row 划入一个或多个同源 classic script。它在更长的 map 形式请求 URL 保持在 3 KiB 以内时贪心填充每组,既保留 graph 顺序,也以增加请求代替超长 URL。每个脚本都由其中的 package 资源寻址,例如 `/plugins/??<package-a>/client.js,<package-b>/client.js&rev=<rev>`。`bootstrap` 与 `application` 是图中的调度阶段,不是 URL 组成部分:HTML 先预加载所有 application URL,再执行所有阻塞 parser 的 bootstrap URL。模块系统按 combo URL 复用进行中的传输,因此同组 row 的并发到达只执行一个脚本。成功结算仍要求模块表中已经存在被请求 row 的 factory id;登记不会运行 factory,所以副作用边界依然是首次物化。
|
||||
|
||||
共享 tsdown 预设为每个插件产出 `client.js.map`,并把第一方源码路径重写成浏览器可识别的仓库形状 `/packages/<group>/<package>/src/...`。内联进 bundle 的其他 workspace 源码同样回到其 `packages/` 归属,依赖包路径保持原样;`sourcesContent` 承载源码,因此 host 只需在 `/plugins/<id>/client.js.map` 供给 map,无需开放源码路由。Vite 壳也产出 sourcemap,使壳代码与图外插件都能从 stack 和性能 profile 回到 TypeScript/TSX。
|
||||
共享 tsdown 预设为每个插件产出 `client.js.map`,并把第一方源码路径重写成浏览器可识别的仓库形式 `/packages/<group>/<package>/src/...`。生产 Client 构建会消费 `lib/types`;预设把每份 tsc map 交给 Rolldown,并从原文件补齐 `sourcesContent`,使最终 map 回到 TypeScript/TSX,而不是停在编译后的 JavaScript。内联进 bundle 的其他 workspace 源码同样回到其 `packages/` 归属,依赖包路径保持原样。Combo 生成会移除每个局部调试指令、记录其生成行偏移、以原插件 map URL 解析每个自带 source,再产出 Indexed Source Map v3。插件有自带 map 时直接用于对应 section;没有时则生成 identity section,内嵌构建后 bundle,并在存在时把 packer 写入的 `sourceURL` 用作 source 名。绝对 map URL 会平行改写脚本资源列表中的每个 `client.js` 后缀,因此 `/plugins/??<package-a>/client.js,<package-b>/client.js&rev=<rev>` 指向 `/plugins/??<package-a>/client.js.map,<package-b>/client.js.map&rev=<rev>`。单资源也采用相同规则,仍产出只有一个 section 的 indexed map。Vite 壳同样产出 sourcemap,使壳代码与经 combo 加载的插件都能从 stack 和性能 profile 回到 TypeScript/TSX。
|
||||
|
||||
`rev` 继续作为脚本 URL 的查询参数和内容一致性锚点,bundle 与 map 都以 `no-cache` 供给。外部脚本的 `error` 事件不给响应状态与正文,因此失败诊断只报告 URL;同源 host 供给与构建期写入的 registration id 是身份边界,`load` 后的工厂存在性检查负责拒绝未登记预期 id 的产物。
|
||||
图为 HMR 保留每个 row 带 revision 的单资源 combo URL,并为每个启动 combo 请求增加按内容寻址的描述;多条描述可以使用同一调度阶段。初始 row revision 是进程级不透明 nonce,而不是内容哈希;它无需在启动时哈希每个插件,也能保证已快照的单资源响应不可变。watcher 观察到某个产物变化后,`rebuilt(id)` 只哈希该 bundle 与 map,并发布所得 revision。启动 combo revision 覆盖合并脚本输入与 indexed map。版本化脚本与 map 使用 immutable 缓存。Host 只提供精确生成的 URL;陈旧 revision 与未发布资源列表返回 404,不会别名到其他字节。外部脚本的 `error` 事件不给响应状态与正文,因此失败诊断只报告 URL;同源 Host 与构建期写入的 registration id 是身份边界,`load` 后的 factory 存在性检查负责拒绝未登记预期 id 的产物。
|
||||
|
||||
### 装载流程,端到端
|
||||
|
||||
@@ -53,12 +53,12 @@ vendored Loader 经其 `internal` 约定消费模块系统——唯一调用点
|
||||
**host 侧——组合这张图。**
|
||||
|
||||
1. 负责组合的 app(`apps/cli`)把名册作为普通行放进它的 `cordis.yml` 配置树——client 插件包与每个 host 插件一样是 entry 行,包括无条件挂载的 `client-hmr` 行。名册行 import 失败由 `assertEntriesLoaded` 捕获;fiber reject 的行则由 `assertEntriesActivated` 报告原始 stack([host boot 决策](2026-07-24-web-config-tree-boot-and-transport-layering.zh.md))。
|
||||
2. `dsh-client-modules` 的 node 半(该包是双面的:浏览器半就是模块表)扫描 loader entry 的 package.json `dsh.client` 声明,组合出 `window.__DSH_BOOT__`:`{ rev, entries: [{ id, url, rev, inject?, immediately?, external? }] }`。三个可选字段都来自 manifest,永不人肉抄写。组合会把被请求的动态图 row 排到消费者之前,并拒绝同步请求环。它会拒绝没有已构建 `./client` bundle 的已声明插件,并把它们的 package/path 行归到一条源码构建要求下;畸形声明字段同样会让激活失败,host 检查会从 FAILED fiber 报告这两类错误。
|
||||
3. 扫描是单包增量——不存在全量重扫代码路径。每次 cordis `internal/plugin` 发射把该 fiber 的 entry 名标脏(无 entry 的 fiber O(1) 丢弃);微任务 flush 把每个脏名对账 live loader entries,包元数据(含「非 client 包」的否定结论)按名永久缓存,bundle 重哈希只经 `rebuilt(id)` 可达。激活趟从当前 entries 灌同一脏集合并同步 flush,初扫与稳态共享一条实现。每个 bundle 的内容哈希是其 `rev`(缓存失效 + HMR diff 锚点),行集合哈希进 `graph.rev`,每一行都作为脚本资源供给:`/plugins/<id>/client.js?rev=…`,对应 sourcemap 位于同一路径加 `.map`。图类型单源在 modules 包的 `./client` 出口——webserver 对图一无所知(它是朴素路由注册插件;bundle 路由和 index 渲染 tap 都由 modules 自己注册)。
|
||||
2. `dsh-client-modules` 的 node 半(该包是双面的:浏览器半就是模块表)扫描 loader entry 的 package.json `dsh.client` 声明,组合出 `window.__DSH_BOOT__`:`{ rev, entries: [{ id, url, rev, inject?, immediately?, external? }], batches: [{ phase, url, rev, entries }] }`。Row 的三个可选字段都来自 manifest,永不人肉抄写。组合会把被请求的动态图 row 排到消费者之前、拒绝同步请求环,并把每个 row 恰好分配给一个初始批次。它会拒绝没有已构建 `./client` bundle 的已声明插件,并把它们的 package/path 行归到一条源码构建要求下;畸形声明字段同样会让激活失败,Host 检查会从 FAILED fiber 报告这两类错误。
|
||||
3. 扫描是单包增量——不存在全量重扫代码路径。每次 cordis `internal/plugin` 发射把该 fiber 的 entry 名标脏(无 entry 的 fiber O(1) 丢弃);微任务 flush 把每个脏名对账 live loader entries,包元数据(含「非 client 包」的否定结论)按名永久缓存,bundle 重哈希只经 `rebuilt(id)` 可达。激活趟从当前 entries 灌同一脏集合并同步 flush,初扫与稳态共享一条实现。初始 row 使用不透明的进程 nonce 加序号,不对其产物求哈希;启动 combo revision 对合并脚本输入及 indexed map 求哈希,row 与批次描述再共同哈希进 `graph.rev`。图类型单源在 modules 包的 `./client` 出口——webserver 对图一无所知;modules 会注册 combo 路由并贡献结构化 index 注入行。
|
||||
|
||||
为什么名册是 yml 行而不是扫描?因为哪些插件组合进一次部署是组合决策,不是包属性——一个在仓库中声明了 dsh.client 的包,不代表这次部署要挂载它,扫描发现无从替人做这个决定;node 半只扫描配置树实际挂载了的东西。
|
||||
|
||||
**第一阶段——模块面。**注入的 HTML 以 queue 模式安装 `window.__ModuleLoader__`,以阻塞式 classic script 执行 modules 与 runtime graph row,赋值 `window.__DSH_BOOT__`,然后启动 Vite 主模块。内核把原始图和外壳 seed 传给 facade 的 `create()`。Facade 移除 modules registration,用拒绝全部 external 的 bootstrap `require` 将其物化,再调用其 `createClientModuleSystem` 导出。Modules bundle 解析图、构造系统、记忆化自身 exports,并在模块闭包中保留该实例;构造过程先把同一 facade 切换到 live registration,再排空 runtime 的 pending factory。随后内核并行预取每个 `immediately` row;prefetch 会递归登记已声明的动态请求和 row 自身,但不物化任一项。单行预取失败在这里被吞下,因为第二阶段 import 会重试并拥有那次大声失败。`immediately` 仍是到达标记,不是生命周期屏障或包身份。
|
||||
**第一阶段——模块面。**注入的 HTML 以 queue 模式安装 `window.__ModuleLoader__`,开始预加载所有 application combo URL,以阻塞式 classic script 依次执行所有 bootstrap combo URL,赋值 `window.__DSH_BOOT__`,然后启动 Vite 主模块。内核把原始图和外壳 seed 传给 facade 的 `create()`。Facade 移除 modules registration,用拒绝全部 external 的 bootstrap `require` 将其物化,再调用其 `createClientModuleSystem` 导出。Modules bundle 解析图、构造系统、记忆化自身 exports、在模块闭包中保留该实例,并把同一 facade 切换到 live registration。随后内核并行预取每个 `immediately` row;同一 application combo 中的 row 共享一次执行,不同 combo 会在 immediate row、被请求依赖或普通 entry import 首次触及时独立加载。预取失败在这里被吞下,因为第二阶段 import 会重试并拥有那次大声失败。`immediately` 仍是 registration barrier,不是包身份。
|
||||
|
||||
**第二阶段——插件面。**
|
||||
|
||||
@@ -72,19 +72,19 @@ vendored Loader 经其 `internal` 约定消费模块系统——唯一调用点
|
||||
|
||||
热重载是一项组合决策:web 组合包无条件挂载 `client-hmr` 行(一个常规的插件包),其 node 半带来 bundle 监视与 SSE(Server-Sent Events)通道;没有重建 watcher 改写客户端 bundle 时链路保持空闲。不应暴露它的组合可以禁用该行。
|
||||
|
||||
重建好的 bundle 怎么变成重载信号?hmr 的 node 半自己观察——没有构建器来通知它。它从 `ctx.clientModules.clientPath(id)` 读取图上各行的 bundle 路径,由 HMR 自持的单个定时器对当前图上的每一行做 stat 轮询。新增图行时,顺序固定为先同步取得 stat 基线,再立即调用 `clientModuleHost.rebuilt(id)`:在模块 host 算出图哈希之后、取得基线之前发生的写入会被这次立即重哈希捕获;取得基线之后发生的写入则会留下 stat 差异,供下一次轮询捕获。这避开了 `fs.watchFile`:它以异步首次 stat 建立基线,可能把构造期间的重建静默吸收进基线。监视集合的成员随 `onGraphChanged` 更新;消失的行撤下监视,轮询时缺失的 bundle 则让对应行保持标脏状态,文件重现时即使元数据相同也强制重哈希。mtime/size 变化或行处于标脏状态时,`clientModuleHost.rebuilt(id)` 是重哈希的唯一入口;当 `rev` 真的变了,node 半才在 `GET /plugins/events` 上广播 `rebuilt` 帧——这是一条系统级 SSE 通道,连接即发全量图,变更时发 `rebuilt` 帧,仅供呈现的 wire,永不进会话日志。轮询是刻意选择:inotify 在 weka 网络挂载上不触发,构建侧监视器需要 `--poll` 也是同一原因;轮询间隔是一个经校验的配置字段(默认 500ms),dispose(资源释放)会清掉那一个定时器。重建 bundle 则是任意一个 tsdown watch 进程的事——`scripts/dev-web.ts` 仍作为 watch 构建入口保留,其包清单在启动时扫描 `packages/*/*/package.json` 按 dsh.client 发现——构建器与 host 共享零协议。写一半的 bundle 被撕裂读取会自愈:写入完成期间 stat 持续变化,下一个轮询节拍会再次重哈希并广播最终的 rev。
|
||||
重建好的 bundle 怎么变成重载信号?hmr 的 node 半自己观察——没有构建器来通知它。模块 host 在读取每份启动快照前捕获 bundle 与可选 map 的 stat 基线,并通过 `ctx.clientModules.artifactBaseline(id)` 暴露它。HMR 自持的单个定时器把当前图的每个 row 与这份基线比较:未变化的 row 直接开始监视,不读取内容也不求哈希;基线捕获后的写入已经形成 stat 差异,只有该 row 会进入 `rebuilt(id)`。这同时消除了启动期的全量重哈希,并避开 `fs.watchFile` 以异步首次 stat 建立基线、可能静默吸收构造期重建的问题。监视集合的成员随 `onGraphChanged` 更新;消失的 row 撤下监视,轮询时缺失的 bundle 则让对应 row 保持标脏状态,文件重现时即使元数据相同也强制重哈希。脚本/map 的 mtime 或 size 变化,或 row 处于标脏状态时,`rebuilt(id)` 是重哈希的唯一入口;当 `rev` 真的变了,node 半才在 `GET /plugins/events` 上广播 `rebuilt` 帧——这是一条系统级 SSE 通道,连接即发全量图,变更时发 `rebuilt` 帧,仅供呈现的 wire,永不进会话日志。轮询是刻意选择:inotify 在 weka 网络挂载上不触发,构建侧监视器需要 `--poll` 也是同一原因;轮询间隔是一个经校验的配置字段(默认 500ms),dispose(资源释放)会清掉那一个定时器。重建产物是任意一个 tsdown watch 进程的事——`scripts/dev-web.ts` 仍作为 watch 构建入口保留,其包清单在启动时扫描 `packages/*/*/package.json` 按 dsh.client 发现——构建器与 host 共享零协议。写一半的 bundle 被撕裂读取会自愈:写入完成期间 stat 持续变化,下一个轮询节拍会再次重哈希并广播最终的 rev。
|
||||
|
||||
浏览器侧,驱动插件每帧重载一个插件,串行执行:
|
||||
|
||||
1. `invalidate`——丢弃陈旧的工厂与记录。工厂还活着会让下一步变成 no-op。
|
||||
2. `prefetch`——加载外部脚本并登记新工厂,旧 fiber 此刻仍在服役。
|
||||
1. `invalidate`——丢弃陈旧的 factory 与记录,并把 rebuilt 帧的 revision 绑定到该 row 的单资源 combo URL。Factory 还活着会让下一步变成 no-op。
|
||||
2. `prefetch`——加载该单资源外部脚本并登记新 factory,旧 fiber 此刻仍在服役。初始多资源脚本不会再次执行。
|
||||
3. `registry.delete`——先于任何 fiber 操作。裸做 fiber dispose 会触发 vendored Loader 的自 dispose 分支,把 entry 永久停用。
|
||||
4. 排空旧 fiber 的各 disposer。
|
||||
5. 移除名下的 `<style data-plugin>` 标签。
|
||||
6. `entry.refresh()`——重新 import,物化新工厂。CSS 在这里重新注入,沿用同一批稳定标签 id。
|
||||
7. `fiber.await()`——让失败大声重抛。
|
||||
|
||||
每个插件都共享同一套语义;`immediately` 行的重载与 lazy 行分毫不差。依赖级联不花一行 client 代码:fiber 的激活纪元串接着它各服务提供方的 uid,因此换掉提供方的 fiber,每个依赖方都会经 cordis 本身重新装载。重载 connection 或 runtime 会级联整个 UI——正确,虽然重。
|
||||
每个插件都共享同一套语义;`immediately` 行的重载与 lazy 行分毫不差。依赖级联不花一行 client 代码:fiber 的激活纪元串接着它各服务提供方的 uid,因此替换 connection 等基础 provider 的 fiber 时,每个依赖方都会经 cordis 本身重新装载——行为正确,但代价较高。
|
||||
|
||||
支持边界,如实陈述。重载粒度刻意做粗:全新 fiber、全新组件、React 状态丢失、数据层不动——react-refresh 级的状态保留与「重执行 bundle 即重跑 factory」相冲突,属刻意不做。静态装配包与外壳内核不是 entry:改动它们意味着外壳重建加整页刷新。重载不做回滚:import 失败让 entry 失去 fiber,下一个 rebuilt 帧从头重试;apply 失败留下 FAILED fiber 交给状态投影;两者都大声记录。自我重载可行——在途的重载在旧 bundle 的闭包里跑完,新的 apply 再开一条新 SSE 通道——但空窗期到达的帧会丢失,下次重建会再次通知。一处已知的仅限 dev 竞态:rebuilt 帧与仍在途的 boot 到达重叠时共享那次到达的任务,可能物化重建前的字节;下一帧自愈。
|
||||
|
||||
@@ -96,7 +96,7 @@ vendored Loader 经其 `internal` 约定消费模块系统——唯一调用点
|
||||
|
||||
Wire 两侧运行同一份治理实现;浏览器特有层只包含一套模块系统和一个重载插件。动态包只有一种产物形态,因此纯度检查覆盖全部动态包。Cordis 依赖、模块请求与启动档位都与其所有者——manifest——同住,负责组合的 app 只握名册。Host graph 校验与递归请求到达使同步 factory 依赖保持显式。浏览器原生 script 装载保留插件网络资源、生成 bundle 与 TypeScript/TSX 源码之间的标准映射,模块系统也只保留一个可替换的 `loadBundle` 钩子。
|
||||
|
||||
接受的代价:vendored Loader 在浏览器里背着闲置机件(EntryTree 持久化是 no-op,分组/隔离未用);开发期每次修改插件都要付一次 bundle 重建加 fiber 重挂;graph `inject` row 仅是信息性说明——激活的真相在服务层——因此不匹配会在 settled 扫描时浮出,而不是在 graph 校验时被拦下;静态 UI 库保留直接实体导出;每个 bundle 多出一份 sourcemap 产物,外部 script 失败也只能给出粗粒度 URL 诊断,不能像显式 fetch 那样报告 HTTP 状态。
|
||||
接受的代价:vendored Loader 在浏览器里背着闲置机件(EntryTree 持久化是 no-op,分组/隔离未用);开发期每次修改插件都要付一次 bundle 重建加 fiber 重挂;graph `inject` row 指导 factory 到达,但服务可用性仍是激活权威,因此不匹配会在 settled 扫描时浮出;静态 UI 库保留直接实体导出;每个 bundle 多出一份 sourcemap 产物,外部 script 失败也只能给出粗粒度 URL 诊断,不能像显式 fetch 那样报告 HTTP 状态。Host 会保留逐插件 bundle/map 快照、生成的单资源响应、当前启动 combo 响应及上一代启动响应,因此内存会随组合出的客户端产物增长为数份副本。这组保留状态使 URL 保持不可变,并让进行中的请求跨越一次 HMR 重组后仍能完成。
|
||||
|
||||
名册位于 web 组合包的配置树(`packages/bundle/web-app/cordis.patch.yml`);`mountWebPlugins` 与 `CLIENT_PACKAGES` 常量已消失,重组一次部署等于替换 yml/overlay。Graph 组合器位于 `dsh-client-modules` node 半,由 parser 预载的 client face 则自举浏览器模块表。Webserver 继续作为朴素路由注册插件;`/api/*` 绑定属于 connection node 半,并经 `api-gateway`(由 `dsh-host-apiproxy` 提供 `ctx.apiProxy`);开发期 bundle 监视与 SSE 通道属于 hmr node 半。
|
||||
|
||||
|
||||
+2
-2
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-24-web-config-tree-boot-and-transport-layering.md
|
||||
2026-07-24-web-config-tree-boot-and-transport-layering.md: eb30ba84ef293a169931ef6519a9d6d2ea98af7f
|
||||
2026-07-24-web-config-tree-boot-and-transport-layering.zh.md: a3e310a4a5ab8bc6a40ad8d0336cb94e29c1744f
|
||||
2026-07-24-web-config-tree-boot-and-transport-layering.md: 3d1ccc2a0f71411d496466288934d9038425e7cf
|
||||
2026-07-24-web-config-tree-boot-and-transport-layering.zh.md: c2409e128dfdbd8550bb7052a7e0f67a40fe1d1f
|
||||
|
||||
+3
-1
@@ -18,7 +18,7 @@ English | [中文](2026-07-24-web-config-tree-boot-and-transport-layering.zh.md)
|
||||
|
||||
**Config sources have one declaration place each.** Bundle yml values are engineering defaults, Settings sections are writable user preferences, CLI flags address their owning launcher rows, and env values enter through yml `!!js` expressions. Patches replace a row's config wholesale. The resolved frontend `distIndex` uses that patch channel as an assembly fact. The transport-independent provider/model default belongs to `ctx.agentDefaultModel`; the [direct headless entry point](2026-08-09-headless-direct-core-entry-point.md) and the Web gateway consume the same state.
|
||||
|
||||
**The transport splits five ways.** `dsh-host-apiproxy` is the gateway plugin (`api-gateway` row): it default-exports `ApiProxyService`, configures only `{nativeOpen?}`, consumes the base layer's entry-point-neutral `ctx.agentDefaultModel`, provides `ctx.apiProxy`, remains transport-agnostic, and registers no routes. `dsh-host-webserver` is a plain route-registration plugin: `WebServer` provides `ctx.webServer` (`register(route) → disposer` with duplicate-pattern throw, `renderIndex` rendering — structured `webserver/index-inject` rows, then raw `tapIndex` transforms in registration order — and `port`), listens on activation, answers per-request failures with 400 and logging, and knows no harness concepts. The connection node half owns the `/api` binding from `ctx.apiProxy` through `toFetchHandler`. The modules node half (`ClientModuleRegistry`, providing `ctx.clientModules`) owns incremental package scanning, the bundle route, the boot injection rows, and `onRebuilt`/`onGraphChanged` notification. The hmr node half owns dev reload through `fs.watchFile` membership and the `/plugins/events` SSE route.
|
||||
**The transport splits five ways.** `dsh-host-apiproxy` is the gateway plugin (`api-gateway` row): it default-exports `ApiProxyService`, configures only `{nativeOpen?}`, consumes the base layer's entry-point-neutral `ctx.agentDefaultModel`, provides `ctx.apiProxy`, remains transport-agnostic, and registers no routes. `dsh-host-webserver` is a plain route-registration plugin: `WebServer` provides `ctx.webServer` (`register(route) → disposer` with duplicate-pattern throw, `renderIndex` rendering — structured `webserver/index-inject` rows, then raw `tapIndex` transforms in registration order — and `port`), listens on activation, answers per-request failures with 400 and logging, and knows no harness concepts. Its socket-backed Node HTTP entry may apply configured gzip through maintained middleware without adding a response-writing service method or changing route owners; the Web Worker tunnel carries identity bytes. The connection node half owns the `/api` binding from `ctx.apiProxy` through `toFetchHandler`. The modules node half (`ClientModuleRegistry`, providing `ctx.clientModules`) owns incremental package scanning, the bundle route, the boot injection rows, and `onRebuilt`/`onGraphChanged` notification. The hmr node half owns dev reload through `fs.watchFile` membership and the `/plugins/events` SSE route.
|
||||
|
||||
**Package export discipline.** The modules package exposes exactly `.` (node half) and `./client` (the complete browser half: `ClientModuleSystem`, `parseBootManifest`, the adoption plugin face) — no bespoke subpaths; wire types re-export through the root for host-side consumers. The adoption handshake: the kernel writes the constructed instance to `window.__DSH_MODULES__` before cordis exists; the `./client` apply reads the slot (missing = loud throw) and provides `ctx.modules`.
|
||||
|
||||
@@ -40,3 +40,5 @@ English | [中文](2026-07-24-web-config-tree-boot-and-transport-layering.zh.md)
|
||||
| env vars in the mapping table | The same field would gain env/json double sourcing and need an invented precedence |
|
||||
| Unbarriered create-after-prefetch (`arrive()` dedup as safety) | Disproved by a 10–25% boot race: in-flight dedup covers same-package double-fetch, not cross-package synchronous require edges |
|
||||
| json file used directly as loader patches | json keys would couple to yml row structure; profile writers would need cordis knowledge |
|
||||
| Public response writer plus per-route opt-in | Response coding is Node HTTP policy; exposing it through `ctx.webServer` would make every route owner and test double depend on that policy |
|
||||
| Hand-written gzip negotiation and stream lifecycle | Maintained middleware already owns negotiation, media-type filtering, header rewriting, backpressure, and threshold behavior |
|
||||
|
||||
+3
-1
@@ -18,7 +18,7 @@ Status: implemented
|
||||
|
||||
**每个配置源有唯一声明位置。** 组合包 yml 值是工程默认,Settings 分节是可写的用户偏好,CLI(命令行界面)flags 面向其归属的启动器配置行,env 值则通过 yml `!!js` 表达式进入。patch 会整体替换一行的 config。解析后的前端 `distIndex` 通过同一条 patch 通道作为组装事实传递。与传输无关的提供方/模型默认值归 `ctx.agentDefaultModel` 所有;[直接 headless 入口](2026-08-09-headless-direct-core-entry-point.zh.md)与 Web 网关消费同一份状态。
|
||||
|
||||
**传输五分。** `dsh-host-apiproxy` 是网关插件(`api-gateway` 行):默认导出 `ApiProxyService`,只配置 `{nativeOpen?}`,消费 base 层不偏向特定入口的 `ctx.agentDefaultModel`,provide `ctx.apiProxy`,保持传输无关且不注册路由。`dsh-host-webserver` 是朴素的路由注册插件:`WebServer` provide `ctx.webServer`(`register(route) → disposer`、重复 pattern 即抛、`renderIndex` 渲染——先结构化 `webserver/index-inject` 行、后原始 `tapIndex` 按注册序应用——与 `port`),激活即 listen,单请求失败时答 400 并记日志,且不认识任何 harness 概念。connection node 半拥有从 `ctx.apiProxy` 经 `toFetchHandler` 绑定到 `/api` 的逻辑。modules node 半(`ClientModuleRegistry`,provide `ctx.clientModules`)拥有单包增量扫描、bundle 路由、启动注入行与 `onRebuilt`/`onGraphChanged` 通知。HMR(热模块替换) node 半通过 `fs.watchFile` membership 与 `/plugins/events` SSE 路由拥有开发期重载。
|
||||
**传输五分。** `dsh-host-apiproxy` 是网关插件(`api-gateway` 行):默认导出 `ApiProxyService`,只配置 `{nativeOpen?}`,消费 base 层不偏向特定入口的 `ctx.agentDefaultModel`,provide `ctx.apiProxy`,保持传输无关且不注册路由。`dsh-host-webserver` 是朴素的路由注册插件:`WebServer` provide `ctx.webServer`(`register(route) → disposer`、重复 pattern 即抛、`renderIndex` 渲染——先结构化 `webserver/index-inject` 行、后原始 `tapIndex` 按注册序应用——与 `port`),激活即 listen,单请求失败时答 400 并记日志,且不认识任何 harness 概念。其基于 socket 的 Node HTTP 入口可以通过受维护的中间件应用已配置的 gzip,无需新增响应写出服务方法或改变 route 所有者;Web Worker 隧道传递 identity 字节。connection node 半拥有从 `ctx.apiProxy` 经 `toFetchHandler` 绑定到 `/api` 的逻辑。modules node 半(`ClientModuleRegistry`,provide `ctx.clientModules`)拥有单包增量扫描、bundle 路由、启动注入行与 `onRebuilt`/`onGraphChanged` 通知。HMR(热模块替换) node 半通过 `fs.watchFile` membership 与 `/plugins/events` SSE 路由拥有开发期重载。
|
||||
|
||||
**包出口纪律。** modules 包只暴露 `.`(node 半)与 `./client`(完整浏览器半:`ClientModuleSystem`、`parseBootManifest`、收编插件面)——不设专用子路径;wire 类型经根出口 re-export 给 host 侧消费方。收编握手:内核在 cordis 之前把建好的实例写入 `window.__DSH_MODULES__`;`./client` 的 apply 读取该槽位(缺少时显式抛错)并 provide `ctx.modules`。
|
||||
|
||||
@@ -40,3 +40,5 @@ Status: implemented
|
||||
| env 进映射表 | 同一字段将出现 env/json 双源,需再发明优先级 |
|
||||
| create 不等预取(以 `arrive()` 去重为安全依据) | 被 10–25% boot 竞态证伪:在途去重只覆盖同包双拉,不覆盖跨包同步 require 边 |
|
||||
| json 直接当 loader patches 文件 | json 键名将耦合 yml 行结构,profile 编写者要懂 cordis |
|
||||
| 公开响应写出方法并让每条 route 选择接入 | 响应编码属于 Node HTTP 策略;经 `ctx.webServer` 暴露会让每个 route 所有者与测试替身依赖这项策略 |
|
||||
| 手写 gzip 协商与流生命周期 | 受维护的中间件已经处理协商、媒体类型筛选、响应头改写、背压与阈值行为 |
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-29-package-regrouping.md
|
||||
2026-07-29-package-regrouping.md: 52a1fa28e4826daa7b0bb84a37be3c094fddb68c
|
||||
2026-07-29-package-regrouping.zh.md: bfba4c6bfc6190956b40b295b650d0bed78eb3f4
|
||||
2026-07-29-package-regrouping.md: 8d7c84434bf45568a7a78006759e002edbfc02d6
|
||||
2026-07-29-package-regrouping.zh.md: fce454181061a1af51e46a6a2395a47cf6cfdec1
|
||||
|
||||
@@ -29,7 +29,7 @@ Five regrouping decisions remain current; every other group keeps its prior boun
|
||||
|
||||
- **`session/`** is the durable session data plane: the persistence seam with its backends and checkpoint policy, the projection fold that serves whole values from that log, log-backed titles, and OTel reporting. The title fold is itself load-bearing for the read side (`session-query` peer-depends on `dsh-session-title`), so titles belong with the data plane, not in a derived-services annex. The plain name is deliberate (prefer names a human would say); the nearby `core/session` package remains the live in-memory service, while this group is the durable family around it. `session-query/` stays a standalone group — the read/tool surface has its own model tools and SQLite FTS backend and is consumed independently of persistence internals.
|
||||
- **`interaction/`** is the human-collaboration plane plus the terminal channel that answers it: the question/approval seams, the permission preset, the model-facing `ask_user_question` tool, the human-command registry (`plan-mode` and `command-goal` already consume `commands` together with the interaction seams), and `tui` — the interactive channel is the plane's richest provider and consumer (peer edges to `commands` and `user-questions`), and a one-package `tui/` group would spend a top-level name on one plugin.
|
||||
- **`boot/`** is a role-complete single-package group: the shared bin boot glue that belongs to no channel and no assembly (consumed by `apps/cli` and the `examples/` demo bins).
|
||||
- **`boot/`** is a role-complete single-package group: the shared boot glue that belongs to no channel and no assembly (consumed by `apps/cli` and test-only Loader drivers).
|
||||
- **`guard/`** keeps its documented role, loop-hygiene guards, and gains the tool-call timeout enforcer, dissolving the one-package `timeout/` group whose name collided with `util/timeout`.
|
||||
- **`extensions/`** names the role `cordis/` obscured: the toolset with which the agent inspects and mounts plugins in its own live runtime, and the landing zone for future self-modification packages.
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ Status: implemented
|
||||
|
||||
- **`session/`** 是持久会话数据平面:持久化 seam 连同其各后端与检查点策略、从该日志折叠(fold)出全量值并对外提供的投影、基于日志的标题,以及 OTel 上报。标题折叠本身就是读取侧的承重构件(`session-query` 对 `dsh-session-title` 声明对等依赖),所以标题属于数据平面,而非某个「派生服务」附属区。用这个朴素的名字是有意为之(名字要像人起的);旁边的 `core/session` 包仍是常驻内存的实时服务,本组则是围绕它的持久家族。`session-query/` 保持独立成组:这个读取/工具面自带模型工具和 SQLite FTS 后端,其消费不依赖持久化内部实现。
|
||||
- **`interaction/`** 是人机协作平面加上应答它的终端通道:提问/批准 seam、权限预设、面向模型的 `ask_user_question` 工具、人类命令注册表(`plan-mode` 与 `command-goal` 已经把 `commands` 和各交互 seam 放在一起消费),以及 `tui`——这个交互通道是该平面功能最丰富的提供方与消费方(对 `commands` 与 `user-questions` 均有对等依赖边),而一个单包 `tui/` 组会把一个顶层名字花在一个插件上。
|
||||
- **`boot/`** 是角色完备的单包组:不归属任何通道也不归属任何组装的共享 bin boot 胶水(被 `apps/cli` 与 `examples/` 各演示 bin 消费)。
|
||||
- **`boot/`** 是角色完备的单包组:不归属任何通道也不归属任何组装的共享 boot 胶水(被 `apps/cli` 与仅限测试的 Loader driver 消费)。
|
||||
- **`guard/`** 保留其文档记载的角色(循环卫生守卫),并新纳入强制执行工具调用超时的包;那个与 `util/timeout` 撞名的单包组 `timeout/` 随之解散。
|
||||
- **`extensions/`** 把 `cordis/` 遮蔽掉的角色说了出来:它是供 agent(智能体)在自身当前运行时中检查和挂载插件的工具集,也是未来自我修改类包的落点。
|
||||
|
||||
|
||||
+2
-2
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-30-command-row-copy-contract.md
|
||||
2026-07-30-command-row-copy-contract.md: f6d5199389b3907780c501894e2861e6add85e77
|
||||
2026-07-30-command-row-copy-contract.zh.md: 86f0c044e841a6a0148b1f785582868bba5bb3d0
|
||||
2026-07-30-command-row-copy-contract.md: d410adf62dbbc4445936c70309d5902bc184b16f
|
||||
2026-07-30-command-row-copy-contract.zh.md: 0f680f9beb8b335dd6311cadc6c7e022809fb8f2
|
||||
|
||||
@@ -32,4 +32,4 @@ The log is unchanged: `command/run` keeps the structured `name`/`args` split, so
|
||||
|
||||
## Consequences
|
||||
|
||||
Every command row gets shorter, and the rule scales: a new command's author writes its outcome without knowing which surface renders it, and no surface has to de-duplicate. The cost is that the dispatched arguments leave the collapsed row — while a command is still executing the row shows only its name and `执行中…` — and that the no-caption rule is a convention the reviewer enforces, not a gate. The `/permission` texts are pinned by the permission package's command tests, and the assembled row copy by the [seeded-history](../../../../apps/web/tests/snapshots/seeded-history/command-row.expected.md) web golden, which reaches a real settled command row keylessly because `/permission` runs entirely on the host.
|
||||
Every command row gets shorter, and the rule scales: a new command's author writes its outcome without knowing which surface renders it, and no surface has to de-duplicate. The cost is that the dispatched arguments leave the collapsed row — while a command is still executing the row shows only its name and `执行中…` — and that the no-caption rule is a convention the reviewer enforces, not a gate. The `/permission` texts are pinned by the permission package's command tests, and the assembled row copy by the [seeded-history](../../../../snapshots/web/seeded-history/command-row.expected.md) web golden, which reaches a real settled command row keylessly because `/permission` runs entirely on the host.
|
||||
|
||||
@@ -32,4 +32,4 @@ Web 命令条目由一对落库的[命令生命周期事件](../../proposed/arch
|
||||
|
||||
## 后果
|
||||
|
||||
每一条命令条目都变短了,而且这条规则可扩展:新命令的作者写结果时无需知道由哪个界面渲染,任何界面也都不必再去重。代价是分派参数离开了折叠行——命令仍在执行时,行上只有名字和 `执行中…`——以及「不加题头」这条规则是靠评审执行的约定,而非门禁。`/permission` 的文案由 permission 包的命令测试钉住,装配后的行文案由 [seeded-history](../../../../apps/web/tests/snapshots/seeded-history/command-row.expected.md) web 预期输出钉住:由于 `/permission` 完全在 host 上执行,该预期输出无需密钥即可覆盖一条真实的已落定命令条目。
|
||||
每一条命令条目都变短了,而且这条规则可扩展:新命令的作者写结果时无需知道由哪个界面渲染,任何界面也都不必再去重。代价是分派参数离开了折叠行——命令仍在执行时,行上只有名字和 `执行中…`——以及「不加题头」这条规则是靠评审执行的约定,而非门禁。`/permission` 的文案由 permission 包的命令测试钉住,装配后的行文案由 [seeded-history](../../../../snapshots/web/seeded-history/command-row.expected.md) web 预期输出钉住:由于 `/permission` 完全在 host 上执行,该预期输出无需密钥即可覆盖一条真实的已落定命令条目。
|
||||
|
||||
+2
-2
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-06-subagent-list-identity-projection.md
|
||||
2026-08-06-subagent-list-identity-projection.md: 2e88639eade2b5a83ef491bcb93693d432bd676b
|
||||
2026-08-06-subagent-list-identity-projection.zh.md: 56fa5bad853a5c089bb19e58cbc180c3e830c1b7
|
||||
2026-08-06-subagent-list-identity-projection.md: d48d6f869b49861ac54d19c9efc9bda42cf8e575
|
||||
2026-08-06-subagent-list-identity-projection.zh.md: a5765a98bbd6aa5aa8552fa80a0e253d488b790e
|
||||
|
||||
+1
-1
@@ -165,7 +165,7 @@ Consuming surfaces: diagnostic handling across wire, tool, and GUI **stays entir
|
||||
|
||||
## Verification
|
||||
|
||||
`packages/subagent/subagent/tests/list-children.spec.ts` is rewritten to this contract: live-only listing without persistence, query services, or the continuation runtime; with the registry absent, even zero children loudly report `SUBAGENT_CONTROL_PROJECTIONS_UNAVAILABLE`; a live child incurs zero `inspect` throughout while a cold child incurs exactly one per listing; multiple descriptors resolve last-wins to the final one; corrupt payloads and unknown versions fold to `corrupt`; a cold-read failure maps to `unavailable` and retries on the next listing; the ancestor descriptor in a fork seed forms a row under that identity (pinning deviation one); ordinary forks and descendants without a subagent origin neither enter the list nor count toward `hasChildren`; `createdAt`-then-id ordering; an unmounted provider does not affect listing; compacted and uncompacted twins list identically; the three cases of pre-abort, persistence listing, and cold-read cancellation all normalize to `CANCELLED`; the empty list and stable error codes. A hostile-unit dual-path probe (`apply` lazily poisons, `view` detonates) proves that any registered unit's fold/schema throw on this child's log is contained as that child's `corrupt` row on both the live and the cold retrieval paths, with siblings and the listing itself unaffected. Second-rung cases: an own-seq identity used directly with zero `inspect`, a fork seed's ancestor identity (seq inside the seed range) rejected by the gate and falling through, an in-row identity absence (null sentinel or absent key) falling through, an absent cache service falling through, and a poisoned cache row silently falling through to the refold; cold-path lifecycle tampering degrades to `corrupt` field by witness field (`it.each` over the seven). The `tool-subagent-control` list-agents tests are updated for the narrowed load requirement; `optional-session-query.spec.ts` is deleted with the dependency it guarded; the existing keyless snapshots (`subagent-list-agents` among others) are unchanged, pinning that the healthy path's wire and model-visible surfaces did not move; a new keyless snapshot, `subagent-diagnostic` (examples/headless-agent), pins the four-state mapping's diagnostic classification — the model-visible changes such as descriptor-less settled debris becoming a `corrupt` row.
|
||||
`packages/subagent/subagent/tests/list-children.spec.ts` is rewritten to this contract: live-only listing without persistence, query services, or the continuation runtime; with the registry absent, even zero children loudly report `SUBAGENT_CONTROL_PROJECTIONS_UNAVAILABLE`; a live child incurs zero `inspect` throughout while a cold child incurs exactly one per listing; multiple descriptors resolve last-wins to the final one; corrupt payloads and unknown versions fold to `corrupt`; a cold-read failure maps to `unavailable` and retries on the next listing; the ancestor descriptor in a fork seed forms a row under that identity (pinning deviation one); ordinary forks and descendants without a subagent origin neither enter the list nor count toward `hasChildren`; `createdAt`-then-id ordering; an unmounted provider does not affect listing; compacted and uncompacted twins list identically; the three cases of pre-abort, persistence listing, and cold-read cancellation all normalize to `CANCELLED`; the empty list and stable error codes. A hostile-unit dual-path probe (`apply` lazily poisons, `view` detonates) proves that any registered unit's fold/schema throw on this child's log is contained as that child's `corrupt` row on both the live and the cold retrieval paths, with siblings and the listing itself unaffected. Second-rung cases: an own-seq identity used directly with zero `inspect`, a fork seed's ancestor identity (seq inside the seed range) rejected by the gate and falling through, an in-row identity absence (null sentinel or absent key) falling through, an absent cache service falling through, and a poisoned cache row silently falling through to the refold; cold-path lifecycle tampering degrades to `corrupt` field by witness field (`it.each` over the seven). The `tool-subagent-control` list-agents tests are updated for the narrowed load requirement; `optional-session-query.spec.ts` is deleted with the dependency it guarded; the existing keyless snapshots (`subagent-list-agents` among others) are unchanged, pinning that the healthy path's wire and model-visible surfaces did not move; the owner-local `apps/cli/tests/profiles/headless/tests/subagent-diagnostic.expected.e2e.ts` pins the four-state mapping's diagnostic classification — the model-visible changes such as descriptor-less settled debris becoming a `corrupt` row.
|
||||
|
||||
## Consequences
|
||||
|
||||
|
||||
+1
-1
@@ -165,7 +165,7 @@ export type SubagentListEntry =
|
||||
|
||||
## 验证
|
||||
|
||||
`packages/subagent/subagent/tests/list-children.spec.ts` 重写为本约定:无 persistence、query 服务与继续运行时的 live-only 列表;registry 缺席时零 children 也响亮报 `SUBAGENT_CONTROL_PROJECTIONS_UNAVAILABLE`;live child 全程零 `inspect`、cold child 每次列表恰一次;多描述符 last-wins 取末者;损坏载荷与未知版本折为 `corrupt`;冷读失败映射 `unavailable` 且下次列表重试;fork seed 里的祖先描述符按该身份成行(偏差一钉住);普通 fork 与无 subagent origin 的后代不入列也不计入 `hasChildren`;`createdAt`→id 排序;提供方未挂载不影响列表;压缩与未压缩孪生一致;预中止、持久化列表与冷读取消三例归一 `CANCELLED`;空列表与稳定错误码。敌意 unit 双路探针(`apply` 惰性置毒、`view` 引爆)证明任一注册 unit 在该 child 日志上的 fold/schema 抛错,在 live 与 cold 两条取值路径上都收纳为该 child 的 `corrupt` 行,sibling 与列表本身不受影响。第二级例:own-seq 身份直用零 `inspect`、fork 种子祖先身份(seq 落在 seed 区间)被门拒绝落底、行内无身份(null 哨兵或 key 缺席)落底、cache 服务缺席落底、缓存行中毒静默落底重折;冷路径 lifecycle 篡改按见证七字段逐一(`it.each`)降级为 `corrupt`。`tool-subagent-control` 的 list-agents 测试随加载要求收窄更新;`optional-session-query.spec.ts` 随依赖消失删除;既有无密钥快照(`subagent-list-agents` 等)零变化,钉住健康路径的 wire 与 model-visible 面不变;新增无密钥快照 `subagent-diagnostic`(examples/headless-agent)钉住四态映射的诊断分类——descriptor-less 定局残骸成 `corrupt` 行等模型可见变化。
|
||||
`packages/subagent/subagent/tests/list-children.spec.ts` 重写为本约定:无 persistence、query 服务与继续运行时的 live-only 列表;registry 缺席时零 children 也响亮报 `SUBAGENT_CONTROL_PROJECTIONS_UNAVAILABLE`;live child 全程零 `inspect`、cold child 每次列表恰一次;多描述符 last-wins 取末者;损坏载荷与未知版本折为 `corrupt`;冷读失败映射 `unavailable` 且下次列表重试;fork seed 里的祖先描述符按该身份成行(偏差一钉住);普通 fork 与无 subagent origin 的后代不入列也不计入 `hasChildren`;`createdAt`→id 排序;提供方未挂载不影响列表;压缩与未压缩孪生一致;预中止、持久化列表与冷读取消三例归一 `CANCELLED`;空列表与稳定错误码。敌意 unit 双路探针(`apply` 惰性置毒、`view` 引爆)证明任一注册 unit 在该 child 日志上的 fold/schema 抛错,在 live 与 cold 两条取值路径上都收纳为该 child 的 `corrupt` 行,sibling 与列表本身不受影响。第二级例:own-seq 身份直用零 `inspect`、fork 种子祖先身份(seq 落在 seed 区间)被门拒绝落底、行内无身份(null 哨兵或 key 缺席)落底、cache 服务缺席落底、缓存行中毒静默落底重折;冷路径 lifecycle 篡改按见证七字段逐一(`it.each`)降级为 `corrupt`。`tool-subagent-control` 的 list-agents 测试随加载要求收窄更新;`optional-session-query.spec.ts` 随依赖消失删除;既有无密钥快照(`subagent-list-agents` 等)零变化,钉住健康路径的 wire 与 model-visible 面不变;归属方本地的 `apps/cli/tests/profiles/headless/tests/subagent-diagnostic.expected.e2e.ts` 钉住四态映射的诊断分类——descriptor-less 定局残骸成 `corrupt` 行等模型可见变化。
|
||||
|
||||
## 后果
|
||||
|
||||
|
||||
+2
-2
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-08-bounded-session-persistence-write-batching.md
|
||||
2026-08-08-bounded-session-persistence-write-batching.md: 6d77ef90276dcf143bb63019f801379b702b43b2
|
||||
2026-08-08-bounded-session-persistence-write-batching.zh.md: e773f20ade706e97a5f84d4a4cc15cc9d3b2d836
|
||||
2026-08-08-bounded-session-persistence-write-batching.md: fc22a10537ebb9009ab1ae1ec21ca625c9025651
|
||||
2026-08-08-bounded-session-persistence-write-batching.zh.md: c2763e157fcfc2e004b14116694054c604fbfb9c
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ Dropping chunk events or replacing them with assembled messages would reduce log
|
||||
|
||||
### Quantified baseline
|
||||
|
||||
Repository fixtures make the logical volume concrete. Decoding the current packed rows in [`goal-multi-turn-actions`](../../../../apps/web/tests/snapshots/goal-multi-turn-actions/session.jsonl) yields 2,098 events: 2,017 chunks (96.1%). Their unpacked JSONL lines occupy 332,647 of 379,225 event bytes (87.7%), while chunk packing reduces the committed file to 89,176 bytes and 182 storage rows, including 23 packed chunk rows. [`permission-policy-context`](../../../../apps/web/tests/snapshots/permission-policy-context/session.jsonl) yields 813 events: 746 chunks (91.8%) and 118,935 of 184,821 unpacked event bytes (64.4%); its packed file is 84,917 bytes and 123 storage rows, including 14 packed rows. These are tracked deterministic fixtures, not a production workload distribution, but they demonstrate why deleting chunks would reduce logical volume and why the existing packed-row layout already removes much of their JSON envelope cost.
|
||||
Repository fixtures make the logical volume concrete. Decoding the current packed rows in [`goal-multi-turn-actions`](../../../../snapshots/web/goal-multi-turn-actions/session.jsonl) yields 2,098 events: 2,017 chunks (96.1%). Their unpacked JSONL lines occupy 332,647 of 379,225 event bytes (87.7%), while chunk packing reduces the committed file to 89,176 bytes and 182 storage rows, including 23 packed chunk rows. [`permission-policy-context`](../../../../snapshots/web/permission-policy-context/session.jsonl) yields 813 events: 746 chunks (91.8%) and 118,935 of 184,821 unpacked event bytes (64.4%); its packed file is 84,917 bytes and 123 storage rows, including 14 packed rows. These are tracked deterministic fixtures, not a production workload distribution, but they demonstrate why deleting chunks would reduce logical volume and why the existing packed-row layout already removes much of their JSON envelope cost.
|
||||
|
||||
SQLite stores one row per logical event, so those same logical logs would retain 2,098 and 813 event rows respectively; batching does not change those counts. JSONL writes one Zstandard frame and fsync per durable append batch, while SQLite performs one transaction and one session-revision increment per batch. Runtime files do not record former append boundaries, so fixture row counts cannot honestly be presented as fsync or transaction counts.
|
||||
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ Status: implemented
|
||||
|
||||
### 量化基线
|
||||
|
||||
仓库 fixture(测试前置数据)让逻辑数据量有了具体依据。对当前 [`goal-multi-turn-actions`](../../../../apps/web/tests/snapshots/goal-multi-turn-actions/session.jsonl) 中的打包行进行解码,可得到 2,098 个事件,其中 2,017 个是分片(96.1%)。这些分片解包后的 JSONL 行共 332,647 字节,占全部事件 379,225 字节的 87.7%;分片打包则把仓库中的已提交文件缩小到 89,176 字节和 182 个存储行,其中包括 23 个打包分片行。[`permission-policy-context`](../../../../apps/web/tests/snapshots/permission-policy-context/session.jsonl) 可得到 813 个事件,其中 746 个是分片(91.8%);这些分片解包后的 JSONL 行共 118,935 字节,占全部事件 184,821 字节的 64.4%。其打包文件为 84,917 字节,共 123 个存储行,其中包括 14 个打包行。这些是纳入版本控制的确定性 fixture,不代表生产工作负载分布;但它们说明了删除分片为何会降低逻辑数据量,也说明现有打包行布局已经消除了大量 JSON 包装开销。
|
||||
仓库 fixture(测试前置数据)让逻辑数据量有了具体依据。对当前 [`goal-multi-turn-actions`](../../../../snapshots/web/goal-multi-turn-actions/session.jsonl) 中的打包行进行解码,可得到 2,098 个事件,其中 2,017 个是分片(96.1%)。这些分片解包后的 JSONL 行共 332,647 字节,占全部事件 379,225 字节的 87.7%;分片打包则把仓库中的已提交文件缩小到 89,176 字节和 182 个存储行,其中包括 23 个打包分片行。[`permission-policy-context`](../../../../snapshots/web/permission-policy-context/session.jsonl) 可得到 813 个事件,其中 746 个是分片(91.8%);这些分片解包后的 JSONL 行共 118,935 字节,占全部事件 184,821 字节的 64.4%。其打包文件为 84,917 字节,共 123 个存储行,其中包括 14 个打包行。这些是纳入版本控制的确定性 fixture,不代表生产工作负载分布;但它们说明了删除分片为何会降低逻辑数据量,也说明现有打包行布局已经消除了大量 JSON 包装开销。
|
||||
|
||||
SQLite 每个逻辑事件存储一行,因此同样的逻辑日志会分别保留 2,098 和 813 个事件行;批处理不会改变这些数量。JSONL 每个持久化追加批次会写入一个 Zstandard 帧并执行一次 fsync,SQLite 每个批次会执行一次事务并递增一次会话修订版本。运行时文件不记录原有追加边界,因此不能把 fixture 的存储行数当作 fsync 或事务次数。
|
||||
|
||||
|
||||
+2
-2
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.md
|
||||
2026-08-10-fork-children-stay-one-shot.md: b2d9a77d7cc9969e517a4f5d6973aa2aee1134f5
|
||||
2026-08-10-fork-children-stay-one-shot.zh.md: b5dc1a7fda4e2b4152baf63e9c49b89bcebdeef0
|
||||
2026-08-10-fork-children-stay-one-shot.md: ba1e99c4d78d14230a2199cd7fb3c3eb9d3ad754
|
||||
2026-08-10-fork-children-stay-one-shot.zh.md: 0d4e214be952f5a4d96c296f38a5b0dbfbd85c86
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Agent Note: Cache-preserving forked children stay one-shot
|
||||
# Agent Note: Forked children stay one-shot
|
||||
|
||||
Status: implemented
|
||||
|
||||
@@ -12,7 +12,7 @@ The child-scoped `report` return channel is now the largest such addition, and s
|
||||
|
||||
## Decision
|
||||
|
||||
The cache-preserving compositions bind the fork delegation tool to `backgroundMode: one-shot`: [the base bundle](../../../../packages/bundle/base/cordis.patch.yml), [the ACP example](../../../../examples/acp-agent/cordis.yml), and [the headless example](../../../../examples/headless-agent/cordis.yml). The base bundle leaves `run_in_background` available, because it mounts a task service; the two examples set `enableRunInBackground: false`, because they mount none and a one-shot background start would otherwise fail at call time on a missing `tasks` service. The standard, code, and Cordis CLI presets instead bind fork to `continuable`; their child-scoped `report` additions invalidate the inherited prefix and accept the recomputation cost described here.
|
||||
Every shipped composition inherits the fork delegation tool's `backgroundMode: one-shot` from the [base bundle](../../../../packages/bundle/base/cordis.patch.yml). The base bundle leaves `run_in_background` available because it also mounts the task service needed to settle background work.
|
||||
|
||||
One-shot children — foreground and background alike — are created through `SubagentRuntime.start()`, which never enters the continuable activation-setup registry, so neither `report` nor its prompt section is installed. A forked one-shot child's system prompt and tool schemas therefore equal its parent's, apart from the `persona` and `toolFilter` deltas a deployment opts into per delegation tool.
|
||||
|
||||
@@ -20,9 +20,9 @@ One-shot children — foreground and background alike — are created through `S
|
||||
|
||||
### The restriction is composition, not code
|
||||
|
||||
`ForkInProcessProvider.prepareContinuable` stays implemented and `ctx.subagents.startContinuable()` accepts `fork`; composition chooses whether the fork tool is one-shot or continuable. `tool-subagent` knows both the provider's `inheritsParentContext` and its own `backgroundMode` at mount, so a load-time rejection of the pair is available and deliberately absent: the pair is not wrong in general. It is costly only while a child-scope delta precedes inherited history, and the package that creates that delta — [`dsh-tool-subagent-report`](../../../../packages/subagent/tool-subagent-report/README.md) — is separately installable and, by its own design, invisible to `tool-subagent`. A deployment that omits the report package can run continuable forked children with the prefix intact. Encoding one roster's consequence as a delegation-tool invariant would make the tool assert something it cannot observe.
|
||||
`ForkInProcessProvider.prepareContinuable` stays implemented and `ctx.subagents.startContinuable()` still accepts `fork`; only the shipped `cordis.yml` rows changed. `tool-subagent` knows both the provider's `inheritsParentContext` and its own `backgroundMode` at mount, so a load-time rejection of the pair was available and is deliberately not added: the pair is not wrong in general. It is wrong only while a child-scope delta precedes inherited history, and the package that creates that delta — [`dsh-tool-subagent-report`](../../../../packages/subagent/tool-subagent-report/README.md) — is separately installable and, by its own design, invisible to `tool-subagent`. A deployment that omits the report package can run continuable forked children with the prefix intact. Encoding one roster's consequence as a delegation-tool invariant would make the tool assert something it cannot observe.
|
||||
|
||||
The cache-preserving condition is recorded as a `TODO(fork-continuable-prefix-reuse)` marker on `prepareContinuable` and tracked as issue #2124: continuable fork preserves its inherited prefix when the child's system prompt and tool schemas can match the parent's byte for byte.
|
||||
The reintroduction condition is recorded as a `TODO(fork-continuable-prefix-reuse)` marker on `prepareContinuable` itself, the one method the shipped compositions do not call, and tracked as issue #2124: continuable fork reopens when a child's system prompt and tool schemas can match its parent's byte for byte.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
@@ -30,7 +30,7 @@ The cache-preserving condition is recorded as a `TODO(fork-continuable-prefix-re
|
||||
|
||||
**Stop mounting the fork provider at all.** This was the broader form of the restriction. Rejected because foreground fork *is* the prefix-reusing case and is untouched by the report channel, so a full ban gives up the capability without buying anything the one-shot binding does not already buy — and would leave no shipped composition exercising session seeding.
|
||||
|
||||
**Use continuable forked children in cache-preserving compositions and accept the loss.** Rejected for the base bundle and ACP/headless examples because the loss is total rather than marginal: reuse breaks ahead of the inherited history, so the child pays full prefill on a transcript it duplicated for the sole purpose of not paying it. The CLI presets make the other tradeoff and retain continuable fork. A deployment that wants a long-lived child with no inherited context already has `spawn`.
|
||||
**Ship continuable forked children and accept the loss.** Rejected because the loss is total rather than marginal: reuse breaks ahead of the inherited history, so the child pays full prefill on a transcript it duplicated for the sole purpose of not paying it. A deployment that wants a long-lived child with no inherited context already has `spawn`.
|
||||
|
||||
**Make `report` visible to every Agent.** A global registration would restore byte-identical prefixes by giving parent and child the same schema and section. Rejected because roots, one-shot children, remote children, and agentless callers would advertise a tool with no derivable recipient, and execution-time rejection would make schema visibility disagree with authority — the scope-local decision the [report tool Agent Note](../feature/2026-07-30-continuable-subagent-report-tool.md) already settled.
|
||||
|
||||
@@ -38,12 +38,12 @@ The cache-preserving condition is recorded as a `TODO(fork-continuable-prefix-re
|
||||
|
||||
## Consequences
|
||||
|
||||
- The base bundle and ACP/headless examples create only one-shot forked children; their `subagent_fork` returns a result to the caller's turn, and `send_message` addresses only spawned children there. The three CLI presets create continuable forked children.
|
||||
- A one-shot forked child's request prefix stays byte-identical to its parent's unless the deployment configures `persona`, `toolFilter`, or a different LLM route on the fork delegation tool, so the token cost of seeding can buy provider-side reuse. Continuable fork adds `report` before the inherited history and forfeits that reuse.
|
||||
- The fork provider's continuable path has CLI production callers and package-level tests. The same seam accepts one-shot composition, so a bundle or `--patch` overlay can choose either lifecycle without a code change or warning.
|
||||
- No shipped composition creates a continuable forked child; `subagent_fork` returns a result to its caller's turn, and `send_message` addresses only spawned children.
|
||||
- A forked child's request prefix stays byte-identical to its parent's unless the deployment configures `persona` or `toolFilter` on the fork delegation tool, so the token cost of seeding buys provider-side reuse again.
|
||||
- The fork provider's continuable path has no production caller and no assembled-composition coverage. It keeps its package-level tests, and the seam still accepts it, so a bundle or `--patch` overlay can reintroduce it with no code change and no warning.
|
||||
- `subagent_fork`'s model-visible schema changes: the continuable background wording is replaced by the one-shot task wording in the base bundle, and disappears entirely from the two examples. The affected keyless snapshot tool-schema sidecars are re-recorded in the same change.
|
||||
- The report obligation reaches spawned children in every continuable composition and forked children in the CLI presets. Its default `next-step` scheduling, authority model, and coverage remain independent of fork composition.
|
||||
- The report obligation's reach narrows to spawned children in shipped deployments. Its default `next-step` scheduling, authority model, and coverage remain independent of fork composition.
|
||||
|
||||
### Accepted risks
|
||||
|
||||
The one-shot constraint lives in three configuration files and a code comment, not in a gate; the CLI preset rows already choose `backgroundMode: continuable` and incur the prefix loss. Any bundle or profile patch can make either choice without a warning. That is the accepted cost of not encoding one roster's consequence into `tool-subagent`.
|
||||
The constraint lives in three configuration files and a code comment, not in a gate. A future bundle row or profile patch can set `backgroundMode: continuable` on a fork tool and silently reintroduce the prefix loss; nothing fails loud. That is the accepted cost of not encoding one roster's consequence into `tool-subagent`.
|
||||
|
||||
+10
-10
@@ -1,4 +1,4 @@
|
||||
# Agent Note: 保留缓存的 fork child 保持 one-shot
|
||||
# Agent Note: fork 出的 child 保持 one-shot
|
||||
|
||||
Status: implemented
|
||||
|
||||
@@ -12,7 +12,7 @@ fork 与 spawn 的唯一区别是 child 的 Session 会以 parent 已完成轮
|
||||
|
||||
## 决策
|
||||
|
||||
保留缓存的组合会把 fork 委派工具绑定为 `backgroundMode: one-shot`:[base 组合包](../../../../packages/bundle/base/cordis.patch.yml)、[ACP 示例](../../../../examples/acp-agent/cordis.yml)与[headless 示例](../../../../examples/headless-agent/cordis.yml)。base 组合包保留 `run_in_background`,因为它挂载了 task 服务;两个示例设置 `enableRunInBackground: false`,因为它们都不挂载 task 服务,否则一次 one-shot 后台启动会在调用时因缺少 `tasks` 服务而失败。standard、code 与 Cordis CLI preset 则把 fork 绑定为 `continuable`;其子级作用域的 `report` 增量会使继承前缀失效,并接受这里说明的重算成本。
|
||||
所有交付组合都从 [base bundle](../../../../packages/bundle/base/cordis.patch.yml)继承 fork 委派工具的 `backgroundMode: one-shot`。base bundle 保留 `run_in_background`,因为它也挂载了结算后台工作所需的 task 服务。
|
||||
|
||||
one-shot child——前台与后台皆然——经由 `SubagentRuntime.start()` 创建,该路径从不进入可继续的 activation setup 注册表,因此 `report` 与它的提示词 section 都不会被安装。于是一个 fork 出的 one-shot child 的系统提示词与工具 schema 与其 parent 相同,只差部署逐个委派工具主动选择的 `persona` 与 `toolFilter` 增量。
|
||||
|
||||
@@ -20,9 +20,9 @@ one-shot child——前台与后台皆然——经由 `SubagentRuntime.start()`
|
||||
|
||||
### 该限制在于组合,不在于代码
|
||||
|
||||
`ForkInProcessProvider.prepareContinuable` 仍然实现完好,`ctx.subagents.startContinuable()` 也接受 `fork`;组合会选择 fork 工具采用 one-shot 还是 continuable。`tool-subagent` 在挂载时同时知道提供方的 `inheritsParentContext` 与自身的 `backgroundMode`,因此一个加载期拒绝该组合的检查是可行的,而这里刻意不加:该组合并非普遍错误。只有在某个 child 作用域增量位于继承历史之前时,它才会产生高昂成本,而产生该增量的包——[`dsh-tool-subagent-report`](../../../../packages/subagent/tool-subagent-report/README.zh.md)——是独立安装的,并且按其自身设计对 `tool-subagent` 不可见。一个不安装 report 包的部署可以在前缀完好的前提下运行可继续的 fork child。把某一份插件清单的后果写成委派工具的不变量,会让该工具断言它无法观察到的事实。
|
||||
`ForkInProcessProvider.prepareContinuable` 仍然实现完好,`ctx.subagents.startContinuable()` 也仍接受 `fork`;改动的只有随附的 `cordis.yml` 行。`tool-subagent` 在挂载时同时知道提供方的 `inheritsParentContext` 与自身的 `backgroundMode`,因此一个加载期拒绝该组合的检查是可行的,而这里刻意不加:该组合并非普遍错误。它只在某个 child 作用域增量位于继承历史之前时才是错的,而产生该增量的包——[`dsh-tool-subagent-report`](../../../../packages/subagent/tool-subagent-report/README.zh.md)——是独立安装的,并且按其自身设计对 `tool-subagent` 不可见。一个不安装 report 包的部署可以在前缀完好的前提下运行可继续的 fork child。把某一份插件清单的后果写成委派工具的不变量,会让该工具断言它无法观察到的事实。
|
||||
|
||||
保留缓存的条件记录为 `prepareContinuable` 方法上的 `TODO(fork-continuable-prefix-reuse)` 标记,并由 issue #2124 跟踪:当 child 的系统提示词与工具 schema 能与其 parent 逐字节一致时,可继续 fork 就能保留继承前缀。
|
||||
重新开放的条件记录为 `prepareContinuable` 方法上的 `TODO(fork-continuable-prefix-reuse)` 标记——随附组合不调用这个方法——并由 issue #2124 跟踪:当 child 的系统提示词与工具 schema 能与其 parent 逐字节一致时,可继续 fork 即可重新开放。
|
||||
|
||||
## 备选方案
|
||||
|
||||
@@ -30,7 +30,7 @@ one-shot child——前台与后台皆然——经由 `SubagentRuntime.start()`
|
||||
|
||||
**干脆不挂载 fork 提供方。** 这是该限制更彻底的形式。否决的原因是前台 fork *正是*复用前缀的那种情形,且不受 report 通道影响,因此全面禁用会在不换来任何 one-shot 绑定尚未换来的东西的同时放弃该能力——并且随附组合将没有任何一个演练 session 初始内容。
|
||||
|
||||
**在保留缓存的组合中随附可继续的 fork child 并接受这份损失。** base 组合包与 ACP/headless 示例不采用,因为这份损失是全额而非边际的:复用在继承历史之前就已中断,于是 child 为一份自己复制过来、目的恰恰是不必付费的 transcript 付了全额预填充。CLI preset 选择了另一项取舍并保留可继续 fork。想要一个没有继承上下文的长期 child 的部署,本来就有 `spawn`。
|
||||
**照常随附可继续的 fork child 并接受这份损失。** 否决的原因是这份损失是全额而非边际的:复用在继承历史之前就已中断,于是 child 为一份自己复制过来、目的恰恰是不必付费的 transcript 付了全额预填充。想要一个没有继承上下文的长期 child 的部署,本来就有 `spawn`。
|
||||
|
||||
**让 `report` 对每个 Agent 可见。** 全局注册会通过让 parent 与 child 拥有相同的 schema 与 section 来恢复逐字节相同的前缀。否决的原因是根 agent、one-shot child、远端 child 与无 agent 调用方都会宣告一件推导不出收件方的工具,而执行期拒绝会让 schema 可见性与权限彼此矛盾——这正是[report 工具 Agent Note](../feature/2026-07-30-continuable-subagent-report-tool.zh.md)已经定下的作用域局部决策。
|
||||
|
||||
@@ -38,12 +38,12 @@ one-shot child——前台与后台皆然——经由 `SubagentRuntime.start()`
|
||||
|
||||
## 后果
|
||||
|
||||
- base 组合包与 ACP/headless 示例只创建 one-shot fork child;其中的 `subagent_fork` 会把结果返回给调用方的轮次,`send_message` 也只寻址 spawn 出的 child。三个 CLI preset 会创建可继续的 fork child。
|
||||
- 除非部署在 fork 委派工具上配置了 `persona`、`toolFilter` 或不同的 LLM 路由,one-shot fork child 的请求前缀会与其 parent 逐字节相同,因此初始内容的 token 成本可以换来提供方侧的复用。可继续 fork 会在继承历史之前增加 `report`,从而失去该复用。
|
||||
- fork 提供方的可继续路径有 CLI 生产调用方与包内测试。同一条 seam 也接受 one-shot 组合,因此某个组合包或 `--patch` 覆盖层可以无需改动代码、也不会有任何警告地选择任一生命周期。
|
||||
- 没有任何随附组合会创建可继续的 fork child;`subagent_fork` 把结果返回给调用方的轮次,而 `send_message` 只寻址 spawn 出的 child。
|
||||
- 除非部署在 fork 委派工具上配置了 `persona` 或 `toolFilter`,fork child 的请求前缀与其 parent 逐字节相同,因此初始内容的 token 成本重新换来了提供方侧的复用。
|
||||
- fork 提供方的可继续路径没有生产调用方,也没有整体组装层面的覆盖。它保留自己的包内测试,seam 也仍然接受它,因此某个组合包或 `--patch` 覆盖层可以无需改动代码、也不会有任何警告地把它重新引入。
|
||||
- `subagent_fork` 面向模型的 schema 发生变化:base 组合包中可继续的后台措辞被 one-shot 的 task 措辞取代,在两个示例中则完全消失。受影响的无密钥快照工具 schema 伴随文件在同一次改动中重新记录。
|
||||
- 在每个可继续组合中,report 义务都会覆盖 spawn 出的 child;在 CLI preset 中,它也覆盖 fork 出的 child。它的 `next-step` 默认调度、权限模型与覆盖仍独立于 fork 组合。
|
||||
- 在随附部署中,report 义务的覆盖范围收窄到 spawn 出的 child。它的 `next-step` 默认调度、权限模型与覆盖仍独立于 fork 组合。
|
||||
|
||||
### 已接受的风险
|
||||
|
||||
one-shot 限制存在于三个配置文件与一处代码注释中,而不在门禁里;CLI preset 行已经选择 `backgroundMode: continuable` 并承担前缀损失。任何组合包或 profile 补丁都能选择任一方式,且不会收到警告。这就是不把某一份插件清单的后果写入 `tool-subagent` 所接受的代价。
|
||||
该限制存在于三个配置文件与一处代码注释中,而不在门禁里。未来某个组合包行或 profile 补丁可以在 fork 工具上设置 `backgroundMode: continuable`,从而悄然重新引入前缀损失;没有任何东西会失败得很响亮。这就是不把某一份插件清单的后果写入 `tool-subagent` 所接受的代价。
|
||||
|
||||
+2
-2
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-11-repository-naming-contract-and-rename-ledger.md
|
||||
2026-08-11-repository-naming-contract-and-rename-ledger.md: 403a7288bb3af0eff09b229e7d399b94492ae6c0
|
||||
2026-08-11-repository-naming-contract-and-rename-ledger.zh.md: 644e1c0bebfc43414e9e67def8d6c42acddf4c72
|
||||
2026-08-11-repository-naming-contract-and-rename-ledger.md: 269f18c494a17a1af5ab4252ad812a5ded03d672
|
||||
2026-08-11-repository-naming-contract-and-rename-ledger.zh.md: 614332795d46d810bdef32c0d0f7e1010fc7f8a0
|
||||
|
||||
+1
-1
@@ -276,7 +276,7 @@ Keep MCP, Todo, and the Plan Mode package, key, events, and tool names. This dec
|
||||
| `PluginInventoryService` | `PluginInventoryGateway` | The class is a Remote-only adapter from the live Loader tree to the `pluginInventory/list` RPC. It owns no same-process service, cache, history, or mutation path. `Gateway` states the role that exists. |
|
||||
| `@deepseek-ai/dsh-jsonrpc-demo`, `@deepseek-ai/dsh-sdk-jsonrpc-demo`, `@deepseek-ai/dsh-sdk-python-runtime` | removed | The Python runtime packages the existing `@deepseek-ai/dsh` CLI and its `sdk` profile; a private application package would recreate a second launcher. |
|
||||
| `packages/examples/jsonrpc-demo/`, `packages/sdk/python-runtime/` | removed | The Python runtime wheel's closure manifest owns packaging without a separate application package. |
|
||||
| `examples/jsonrpc-agent/` | `examples/python-sdk-agent/` | The example demonstrates Python use of the `sdk` profile and ordered patches. |
|
||||
| `examples/jsonrpc-agent/` | `python/sdk/examples/` | The example demonstrates Python use of the `sdk` profile and ordered patches. |
|
||||
| `@deepseek-ai/dsh-acp-demo` | `@deepseek-ai/dsh-acp-app` | The package is the ACP profile's application bundle, not a standalone demo bin. |
|
||||
| Deploy-root manifests `dsh-jsonrpc-agent-pkg`, `dsh-sdk-python-runtime-closure` | `dsh-python-runtime-closure` | The zero-code manifest defines the Python runtime wheel's complete `dsh` dependency closure without naming a separate SDK application. |
|
||||
| `@deepseek-ai/dsh-frontend` | `@deepseek-ai/dsh-web-frontend` | The application is the web frontend. Keep its physical `apps/web/` folder. |
|
||||
|
||||
+1
-1
@@ -276,7 +276,7 @@ PascalCase 标识符中的首字母缩略词使用首字母大写格式:`Ui`
|
||||
| `PluginInventoryService` | `PluginInventoryGateway` | 该类只负责把实时 Loader 树适配到 `pluginInventory/list` RPC。它不拥有同进程服务、缓存、历史或修改路径。`Gateway` 准确说明现有角色。 |
|
||||
| `@deepseek-ai/dsh-jsonrpc-demo`、`@deepseek-ai/dsh-sdk-jsonrpc-demo`、`@deepseek-ai/dsh-sdk-python-runtime` | 已删除 | Python 运行时打包现有 `@deepseek-ai/dsh` CLI 与其 `sdk` profile;私有应用包会重新产生第二个启动器。 |
|
||||
| `packages/examples/jsonrpc-demo/`、`packages/sdk/python-runtime/` | 已删除 | Python 运行时 wheel 的闭包 manifest 负责打包,无需独立应用包。 |
|
||||
| `examples/jsonrpc-agent/` | `examples/python-sdk-agent/` | 该示例演示 Python 使用 `sdk` profile 与有序 patch。 |
|
||||
| `examples/jsonrpc-agent/` | `python/sdk/examples/` | 该示例演示 Python 使用 `sdk` profile 与有序 patch。 |
|
||||
| `@deepseek-ai/dsh-acp-demo` | `@deepseek-ai/dsh-acp-app` | 该包是 ACP profile 的应用组合包,不是独立 demo bin。 |
|
||||
| 部署根 manifest `dsh-jsonrpc-agent-pkg`、`dsh-sdk-python-runtime-closure` | `dsh-python-runtime-closure` | 该零代码 manifest 定义 Python 运行时 wheel 的完整 `dsh` 依赖闭包,不再命名独立 SDK 应用。 |
|
||||
| `@deepseek-ai/dsh-frontend` | `@deepseek-ai/dsh-web-frontend` | 该应用是 Web 前端。保留其物理目录 `apps/web/`。 |
|
||||
|
||||
+2
-2
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-12-pi-ai-route-default-input-modalities.md
|
||||
2026-08-12-pi-ai-route-default-input-modalities.md: eb03d5330a1283d439e16262325965cf2e7e8087
|
||||
2026-08-12-pi-ai-route-default-input-modalities.zh.md: dfbbd2ae6db7a78e82955db66fe506d3506209fd
|
||||
2026-08-12-pi-ai-route-default-input-modalities.md: 83158d686b028a085fc8801f83a7cea1a369471b
|
||||
2026-08-12-pi-ai-route-default-input-modalities.zh.md: 18ee56db7bfe5cba63c125341b5ecc5fe3905c5b
|
||||
|
||||
+1
-1
@@ -50,4 +50,4 @@ A model that declares image input its endpoint does not serve is not caught loca
|
||||
|
||||
`config.spec.ts` holds the schema boundary: an unknown modality refused at both levels, the empty route list accepted by the schema and refused by the namespace validator that the settings seam actually runs, and the `[]` materialization for an absent array that the inheritance rule depends on.
|
||||
|
||||
No keyless snapshot lane exercises a pi-ai route: the snapshot examples drive `dsh-llm-replay`, which declares modalities directly in its configuration, and a pi-ai route needs a live endpoint whose port a static `cordis.yml` cannot name. The admission points this change feeds are already covered there through that provider (`examples/acp-agent/image.cordis.snapshot.yml` and `image-text-route.cordis.snapshot.yml`) and are unaffected — what changed is what one adapter reports, not how a gate reads it.
|
||||
No keyless snapshot lane exercises a pi-ai route: the recorded-session corpus drives `dsh-llm-replay`, which declares modalities directly in its configuration, and a pi-ai route needs a live endpoint whose port a static `cordis.yml` cannot name. The admission points this change feeds are covered by [`read-image`](../../../../snapshots/session/read-image/) and [`read-image-text-route`](../../../../snapshots/session/read-image-text-route/) and are unaffected — what changed is what one adapter reports, not how a gate reads it.
|
||||
|
||||
+1
-1
@@ -50,4 +50,4 @@ DeepSeek 直接适配器拥有独立的精确模型目录。支持视觉的条
|
||||
|
||||
`config.spec.ts` 负责 schema 边界:两个层级上的未知模态拒绝、路由空列表被 schema 接受而由 settings seam 真正运行的命名空间校验器拒绝,以及继承规则所倚赖的「缺省数组物化为 `[]`」这一事实。
|
||||
|
||||
没有任何无密钥 snapshot 通道会跑 pi-ai 路由:snapshot 示例驱动的是 `dsh-llm-replay`,它在自己的配置里直接声明模态,而 pi-ai 路由需要一个真实端点,其端口是静态 `cordis.yml` 无法写出的。本次变更所供给的那些准入点已经通过该提供方在那里得到覆盖(`examples/acp-agent/image.cordis.snapshot.yml` 与 `image-text-route.cordis.snapshot.yml`)且不受影响——改变的是某个适配器报告什么,而非门禁如何读取它。
|
||||
没有任何无密钥 snapshot 通道会跑 pi-ai 路由:录制会话语料驱动的是 `dsh-llm-replay`,它在自己的配置里直接声明模态,而 pi-ai 路由需要一个真实端点,其端口是静态 `cordis.yml` 无法写出的。本次变更所供给的准入点由 [`read-image`](../../../../snapshots/session/read-image/) 与 [`read-image-text-route`](../../../../snapshots/session/read-image-text-route/)覆盖且不受影响——改变的是某个适配器报告什么,而非门禁如何读取它。
|
||||
|
||||
+2
-2
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-15-client-shells-and-dynamic-packages.md
|
||||
2026-08-15-client-shells-and-dynamic-packages.md: a92300663bac3bfe04768cf2a4f0c354c4c0c66f
|
||||
2026-08-15-client-shells-and-dynamic-packages.zh.md: 5bd55558708f8e949ed3700f145c0725554f4d4b
|
||||
2026-08-15-client-shells-and-dynamic-packages.md: 016314d10f55e0b590e98944ca417bae658ab56a
|
||||
2026-08-15-client-shells-and-dynamic-packages.zh.md: 4e0277d1becab8467521dc21d0e5b7509d1ee993
|
||||
|
||||
+9
-9
@@ -10,7 +10,7 @@ English | [中文](2026-08-15-client-shells-and-dynamic-packages.zh.md)
|
||||
|
||||
Client npm dependency sections describe installation and development relationships, but they do not reliably describe bundle contents. Treating `dependencies`, `peerDependencies`, or `devDependencies` as implicit bundler instructions can inline a shared React or workspace identity, or leave a built library carrying unresolved child imports without the host that is meant to assemble them.
|
||||
|
||||
The browser application also contains distinct roles: the HTML/Vite compilation entry, the framework-free Cordis startup kernel, static assembly libraries, and Loader-governed plugins. Early execution from HTML is an arrival policy, not a package kind. Runtime and modules need to arrive before the Vite main module while retaining ordinary `lib/client.js` artifacts and dynamic graph rows.
|
||||
The browser application also contains distinct roles: the HTML/Vite compilation entry, the framework-free Cordis startup kernel, static assembly libraries, and Loader-governed plugins. Early execution from HTML is an arrival policy, not a package kind. Modules must arrive before the Vite main module while retaining its ordinary `lib/client.js` artifact and dynamic graph row.
|
||||
|
||||
Shared UI libraries still expose synchronous TypeScript and React values to many consumers. Until those values move behind services or slots, making the libraries formal dynamic entries would preserve the value coupling while obscuring which module identity the shell must share.
|
||||
|
||||
@@ -24,7 +24,7 @@ Shared UI libraries still expose synchronous TypeScript and React values to many
|
||||
| Startup kernel | `packages/client/web` | Owns the plain-DOM boot page, module-system wiring, Cordis settlement, and renderer handoff | `staticLinked` `lib/index.js`; no `dsh.client` row |
|
||||
| Static assembly libraries | Cordis, `ui-primitives`, `ui-slots` | Supply shared module identities and direct value APIs | ESM `lib/index.js`, merged and chunked by Vite; not Loader entries |
|
||||
| Module bootstrap | `packages/client/modules` | Supplies the client module table and its Cordis wrapper | Dynamic package with one ordinary `lib/client.js`; the host delivers its factory early |
|
||||
| Dynamic client packages | runtime, `ui-renderer`, theme, and feature plugins | Participate through Cordis services, slots, and effects | Declare `dsh.client`, emit self-registering `lib/client.js`, and remain host-graph entries |
|
||||
| Dynamic client packages | connection, `ui-renderer`, theme, and feature plugins | Participate through Cordis services, slots, and effects | Declare `dsh.client`, emit self-registering `lib/client.js`, and remain host-graph entries |
|
||||
|
||||
`packages/client/web` keeps Cordis as matching peer and development dependencies and uses modules and static UI packages as development compilation inputs. `apps/web` consumes built package exports rather than aliases into workspace source.
|
||||
|
||||
@@ -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` names runtime's parser-preloaded dynamic identity. 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, 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.
|
||||
|
||||
A request has exactly two suppliers:
|
||||
|
||||
@@ -46,12 +46,12 @@ There is no general `dsh.client.provide` alias mechanism. Dynamic rows and stati
|
||||
The modules Node half injects the startup protocol into the served HTML in this order:
|
||||
|
||||
1. Install `window.__ModuleLoader__` in queue mode with `pendingQueue`, `load()`, and `create()`.
|
||||
2. Execute the modules graph row's ordinary `lib/client.js` as a blocking classic script.
|
||||
3. Execute runtime's ordinary `lib/client.js` the same way.
|
||||
4. Assign `window.__DSH_BOOT__`.
|
||||
2. Start preloading every content-addressed application combo URL containing the rows other than modules.
|
||||
3. Execute every blocking bootstrap combo URL; these currently contain the ordinary modules factory registration.
|
||||
4. Assign `window.__DSH_BOOT__`, including all scheduling descriptors and every row's one-resource HMR combo URL.
|
||||
5. Execute the Vite main module.
|
||||
|
||||
Both early scripts only register factories. The startup kernel passes the raw graph and shell seeds to `__ModuleLoader__.create()`. The facade removes the modules registration, materializes it with a `require` function that rejects every external, and invokes its `createClientModuleSystem` export. The modules bundle parses the graph, constructs `ClientModuleSystem`, caches its own exports as the modules row, and retains the system in a module closure. Construction switches the same facade to live mode before draining runtime's pending factory. The modules client face consequently has a zero-runtime-external bootstrap requirement.
|
||||
The bootstrap combo currently registers only the modules factory. The startup kernel passes the raw graph and shell seeds to `__ModuleLoader__.create()`. The facade removes the modules registration, materializes it with a `require` function that rejects every external, and invokes its `createClientModuleSystem` export. The modules bundle parses the graph, constructs `ClientModuleSystem`, caches its own exports as the modules row, retains the system in a module closure, and switches the same facade to live mode. The modules client face consequently has a zero-external bootstrap requirement.
|
||||
|
||||
After the `immediately` tier has registered its factories, the kernel creates all Loader entries, awaits Cordis quiescence, and requires every fiber to be ACTIVE. It then calls `ctx.uiRenderer.mount(container)`. The dynamic `ui-renderer` package owns React, slot rendering, hydration of the existing boot DOM, and the React root lifecycle; the startup kernel and failure page remain React-free.
|
||||
|
||||
@@ -67,7 +67,7 @@ Ordinary installed libraries remain `dependencies`: a dynamic build may bundle a
|
||||
|
||||
**Convert every client package into a dynamic plugin immediately.** `ui-primitives` and `ui-slots` still provide synchronous values without independent service or slot lifecycles; a manifest declaration alone would not remove those imports.
|
||||
|
||||
**Generate a separate `client-static.js` for modules or runtime.** Both packages remain dynamic graph rows and Cordis plugins; only their factory arrival is early. A second artifact would encode host policy in a filename and create two runtime products from one source.
|
||||
**Generate a separate `client-static.js` for modules.** The package remains a dynamic graph row and Cordis plugin; only its factory arrival is early. A second artifact would encode host policy in a filename and create two runtime products from one source.
|
||||
|
||||
**Compile all shared modules into the Vite entry.** This would remove deployment composition and plugin-level replacement from business plugins, including the renderer and theme.
|
||||
|
||||
@@ -79,7 +79,7 @@ Ordinary installed libraries remain `dependencies`: a dynamic build may bundle a
|
||||
|
||||
Bundle contents stay stable when an npm dependency moves between peer and development sections, because each build face declares externality directly. Static libraries remain host-assembled, while dynamic packages retain uniform artifacts and lifecycle governance.
|
||||
|
||||
The startup protocol depends on the modules and runtime package ids, and modules must remain self-contained at runtime. A missing bootstrap registration fails before Cordis starts; later plugin import, apply, and service-wait failures remain visible through the boot page's ACTIVE scan.
|
||||
The startup protocol depends on the modules package id, and modules must remain self-contained at runtime. Combo generation preserves its ordinary package artifact and gives every other row one shared initial transport; HMR uses the same route with that row as its sole resource. A missing bootstrap registration fails before Cordis starts; later plugin import, apply, and service-wait failures remain visible through the boot page's ACTIVE scan.
|
||||
|
||||
The shell consumes built `lib/` products, so source and browser artifacts can drift until the relevant build or watcher runs. Typechecking source alone does not prove the served application uses the same code.
|
||||
|
||||
|
||||
+9
-9
@@ -10,7 +10,7 @@ Status: implemented
|
||||
|
||||
Client npm 依赖区段描述安装和开发关系,但不能可靠描述 bundle 内容。把 `dependencies`、`peerDependencies` 或 `devDependencies` 当作隐式 bundler 指令,可能内联本应共享的 React 或 workspace 身份,也可能让构建后的库携带未解析子 import,却没有交给预期的宿主组装。
|
||||
|
||||
浏览器应用还包含不同角色:HTML/Vite 编译入口、不依赖框架的 Cordis 启动内核、静态装配库,以及由 Loader 治理的插件。HTML 提前执行属于到达策略,不定义包类别。Runtime 和 modules 需要先于 Vite 主模块到达,同时继续使用普通 `lib/client.js` 产物和动态图 row。
|
||||
浏览器应用还包含不同角色:HTML/Vite 编译入口、不依赖框架的 Cordis 启动内核、静态装配库,以及由 Loader 治理的插件。HTML 提前执行属于到达策略,不定义包类别。Modules 必须先于 Vite 主模块到达,同时继续使用普通 `lib/client.js` 产物和动态图 row。
|
||||
|
||||
共享 UI 库仍向大量消费者暴露同步 TypeScript 与 React 实体。在这些实体进入 service 或 slot 前,形式上把库改为动态 entry 只会保留实体耦合,并模糊外壳必须共享的模块身份。
|
||||
|
||||
@@ -24,7 +24,7 @@ Client npm 依赖区段描述安装和开发关系,但不能可靠描述 bundl
|
||||
| 启动内核 | `packages/client/web` | 拥有纯 DOM 启动页、模块系统接线、Cordis settle 和 renderer handoff | `staticLinked` `lib/index.js`;无 `dsh.client` row |
|
||||
| 静态装配库 | Cordis、`ui-primitives`、`ui-slots` | 提供共享模块身份和直接实体 API | ESM `lib/index.js`,由 Vite 合并拆分;不是 Loader entry |
|
||||
| 模块自举包 | `packages/client/modules` | 提供 client 模块表及其 Cordis wrapper | 带一个普通 `lib/client.js` 的动态包;host 提前送达其 factory |
|
||||
| 动态 client 包 | runtime、`ui-renderer`、主题和功能插件 | 通过 Cordis service、slot 和 effect 参与应用 | 声明 `dsh.client`,产出自注册 `lib/client.js`,并保留 host graph entry |
|
||||
| 动态 client 包 | connection、`ui-renderer`、主题和功能插件 | 通过 Cordis service、slot 和 effect 参与应用 | 声明 `dsh.client`,产出自注册 `lib/client.js`,并保留 host graph entry |
|
||||
|
||||
`packages/client/web` 把 Cordis 保持为 matching peer 与开发依赖,并把 modules 和静态 UI 包作为开发期编译输入。`apps/web` 消费已构建 package export,不通过 alias 读取 workspace 源码。
|
||||
|
||||
@@ -32,7 +32,7 @@ Client npm 依赖区段描述安装和开发关系,但不能可靠描述 bundl
|
||||
|
||||
### 共享模块请求
|
||||
|
||||
动态浏览器 bundle 会隐式 external 统一基座:`PLATFORM_MODULES` 命名由外壳播种的 React、Cordis 和静态 UI 身份,`PRELOADED_CLIENT_EXTERNALS` 命名由 HTML parser 预载的 runtime 动态身份。包只在精确请求基座外实体时使用 `dsh.client.external`。纯类型 import 会被擦除,不产生请求;允许的第三方实现库保留为 bundle 私有内容。
|
||||
动态浏览器 bundle 会隐式 external 统一基座:`PLATFORM_MODULES` 命名由外壳播种的 React、Cordis 和静态 UI 身份,`PRELOADED_CLIENT_EXTERNALS` 则为必须先于 shell 启动到达的动态身份预留,当前为空。包只在精确请求基座外实体时使用 `dsh.client.external`。纯类型 import 会被擦除,不产生请求;允许的第三方实现库保留为 bundle 私有内容。
|
||||
|
||||
请求只有两种提供方:
|
||||
|
||||
@@ -46,12 +46,12 @@ Client npm 依赖区段描述安装和开发关系,但不能可靠描述 bundl
|
||||
Modules Node 半按以下顺序向实际返回的 HTML 注入启动协议:
|
||||
|
||||
1. 以 queue 模式安装 `window.__ModuleLoader__`,包含 `pendingQueue`、`load()` 与 `create()`。
|
||||
2. 以阻塞式 classic script 执行 modules graph row 的普通 `lib/client.js`。
|
||||
3. 以相同方式执行 runtime 的普通 `lib/client.js`。
|
||||
4. 赋值 `window.__DSH_BOOT__`。
|
||||
2. 开始预加载所有按内容寻址的 application combo URL,其中包含 modules 之外的 row。
|
||||
3. 执行所有阻塞式 bootstrap combo URL;当前其中包含普通的 modules factory registration。
|
||||
4. 赋值 `window.__DSH_BOOT__`,其中包含全部调度描述及每个 row 的单资源 HMR combo URL。
|
||||
5. 执行 Vite 主模块。
|
||||
|
||||
两个提前执行的脚本都只注册 factory。启动内核把原始图与外壳 seed 传给 `__ModuleLoader__.create()`。Facade 移除 modules registration,用拒绝全部 external 的 `require` 函数将其物化,再调用其 `createClientModuleSystem` 导出。Modules bundle 解析图、构造 `ClientModuleSystem`、把自身 exports 缓存为 modules row,并在模块闭包中保留该系统。构造过程先把同一 facade 切换到 live 模式,再排空 runtime 的 pending factory。因此 modules client face 必须满足零 runtime external 的自举要求。
|
||||
Bootstrap combo 当前只登记 modules factory。启动内核把原始图与外壳 seed 传给 `__ModuleLoader__.create()`。Facade 移除 modules registration,用拒绝全部 external 的 `require` 函数将其物化,再调用其 `createClientModuleSystem` 导出。Modules bundle 解析图、构造 `ClientModuleSystem`、把自身 exports 缓存为 modules row、在模块闭包中保留该系统,并把同一 facade 切换到 live 模式。因此 modules client face 必须满足零 external 的自举要求。
|
||||
|
||||
`immediately` 层级完成 factory 注册后,内核创建全部 Loader entry,等待 Cordis 静止,并要求每个 fiber 都进入 ACTIVE。随后调用 `ctx.uiRenderer.mount(container)`。动态 `ui-renderer` 包拥有 React、slot 渲染、已有启动 DOM 的 hydrate 和 React root 生命周期;启动内核与失败页保持 React-free。
|
||||
|
||||
@@ -67,7 +67,7 @@ Modules Node 半按以下顺序向实际返回的 HTML 注入启动协议:
|
||||
|
||||
**立即把所有 client 包改为动态插件。** `ui-primitives` 与 `ui-slots` 仍提供同步实体,且没有独立 service 或 slot 生命周期;只加 manifest 声明不会移除这些 import。
|
||||
|
||||
**为 modules 或 runtime 生成单独的 `client-static.js`。** 两个包仍是动态图 row 和 Cordis 插件,只有 factory 提前到达。第二份产物会把宿主策略编码进文件名,并让同一源码产生两个运行期产品。
|
||||
**为 modules 生成单独的 `client-static.js`。** 该包仍是动态图 row 和 Cordis 插件,只有 factory 提前到达。第二份产物会把宿主策略编码进文件名,并让同一源码产生两个运行期产品。
|
||||
|
||||
**把全部共享模块编进 Vite entry。** 这会让业务插件失去部署组合与插件级替换能力,包括 renderer 和主题。
|
||||
|
||||
@@ -79,7 +79,7 @@ Modules Node 半按以下顺序向实际返回的 HTML 注入启动协议:
|
||||
|
||||
Npm 依赖在 peer 与开发区段间移动时,bundle 内容保持稳定,因为每个构建 face 都直接声明 external。静态库继续由宿主装配,动态包则保留统一产物与生命周期治理。
|
||||
|
||||
启动协议依赖 modules 和 runtime 的 package id,modules 还必须保持运行期自包含。缺少 bootstrap registration 会在 Cordis 启动前失败;后续插件 import、apply 与 service 等待失败仍由启动页的 ACTIVE 扫描呈现。
|
||||
启动协议依赖 modules 的 package id,modules 还必须保持运行期自包含。Combo 生成保留其普通 package 产物,并为其他全部 row 提供一条共享初始传输;HMR 使用同一条路由,并只把该 row 作为资源。缺少 bootstrap registration 会在 Cordis 启动前失败;后续插件 import、apply 与 service 等待失败仍由启动页的 ACTIVE 扫描呈现。
|
||||
|
||||
外壳消费已构建 `lib/` 产品,因此在相关 build 或 watcher 运行前,源码与浏览器产物可能漂移。仅源码 typecheck 通过不能证明实际服务的应用使用同一份代码。
|
||||
|
||||
|
||||
+2
-2
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-18-client-build-environment.md
|
||||
2026-08-18-client-build-environment.md: 45ed6c8bc68e0f08157fb56a91ae4f6165e6e431
|
||||
2026-08-18-client-build-environment.zh.md: bb9633721401f66b443a65253dcbc0241f45d328
|
||||
2026-08-18-client-build-environment.md: c23a73e9520392e41949f95d04029dbf34213dd0
|
||||
2026-08-18-client-build-environment.zh.md: 5dbb5deeee75eb0f1b2cadf7e38ad8bfc5661f5c
|
||||
|
||||
@@ -18,7 +18,9 @@ The Vite config and the shared tsdown preset for dynamic client bundles use one
|
||||
|
||||
The `DSH_CLIENT_*` prefix itself declares that a value is public. Credentials, paths, and other Host- or CI-only values must not use it.
|
||||
|
||||
The root build wrapper supplies one exact public environment to both bundlers. It derives `DSH_CLIENT_COMMIT_HASH` as the seven-character prefix of the source Git HEAD for every complete build; an explicit value supports build environments without repository metadata. `pnpm run build` otherwise inherits the caller's `DSH_CLIENT_*` values, while `pnpm run build:official` selects the repository's official artifact profile without shell-specific environment syntax and sets `DSH_CLIENT_BUILD_PROFILE=official` for deployment-specific business registrations. A successful complete build writes the exact public environment and a digest covering the Vite output and every dynamic client bundle. Partial build commands do not replace that record.
|
||||
The root build wrapper supplies one exact public environment to both bundlers. Every complete build carries the root package version as `DSH_CLIENT_VERSION` and the seven-character source Git HEAD prefix as `DSH_CLIENT_COMMIT_HASH`; an explicit commit supports build environments without repository metadata. A default local build also samples Git status before building and sets `DSH_CLIENT_GIT_DIRTY=true` for any staged, unstaged, untracked, or submodule change. Clean checkouts and sources without Git metadata omit the dirty field. These repository-owned fields replace inherited values, while `pnpm run build` otherwise inherits the caller's remaining `DSH_CLIENT_*` values.
|
||||
|
||||
`pnpm run build:official` selects the repository's official artifact profile without shell-specific environment syntax. Its exact environment carries the version and commit, sets `DSH_CLIENT_BUILD_PROFILE=official` for deployment-specific business registrations, and omits local dirty metadata. A successful complete build writes the exact public environment and a digest covering the Vite output and every dynamic client bundle. Partial build commands do not replace that record. `pnpm run dev:web` instead samples the default local environment once at startup and passes that environment to every watcher stage for the session. It does not validate the complete-build record because the watcher stages rewrite every recorded artifact.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
@@ -30,8 +32,12 @@ The root build wrapper supplies one exact public environment to both bundlers. I
|
||||
|
||||
**Standardize on `import.meta.env`.** Dynamic plugins are emitted as independent CommonJS factories and cannot retain `import.meta`. Business code would still need two interfaces depending on the artifact path.
|
||||
|
||||
**Reuse the last complete-build record for watchers.** Watcher stages rewrite every recorded client artifact, so the artifact digest becomes stale during normal development. An official-build record would also make edited local source retain the official profile and title. Startup sampling gives every stage one local metadata snapshot without coupling watcher restarts to the recorded artifact digest.
|
||||
|
||||
**Resample Git state during every watcher rebuild.** Vite and tsdown fix define substitutions when their long-lived watchers start. Restarting the build pipeline on repository status changes would make an incremental source edit rebuild unrelated artifacts; sampling once at startup keeps the stages consistent without rebuilding for later status changes.
|
||||
|
||||
## Consequences
|
||||
|
||||
The Vite static shell and shared tsdown dynamic bundles receive the same string for a given `DSH_CLIENT_*` build-process variable. An unset static property read evaluates to `undefined`; non-`DSH_CLIENT_*` values cannot enter browser artifacts through this mechanism, and business code cannot enumerate the build process environment. Every complete build carries its short source revision as public display metadata. CI build gates select the official profile without exposing its public values to source tests or unrelated workflow steps. npm packing and built Web tests verify the recorded environment and current artifact digest, so a default build followed by an official pack request, a partial rebuild, or modified output fails before consumption.
|
||||
The Vite static shell and shared tsdown dynamic bundles receive the same string for a given `DSH_CLIENT_*` build-process variable. An unset static property read evaluates to `undefined`; non-`DSH_CLIENT_*` values cannot enter browser artifacts through this mechanism, and business code cannot enumerate the build process environment. Every complete build carries its package version and short source revision as public display metadata; a dirty default build additionally identifies its uncommitted source state. CI build gates select the official profile without exposing its public values to source tests or unrelated workflow steps. npm packing and built Web tests verify the recorded environment and current artifact digest, so a default build followed by an official pack request, a partial rebuild, or modified output fails before consumption. Watch builds retain their startup snapshot until `pnpm run dev:web` restarts.
|
||||
|
||||
Every `DSH_CLIENT_*` value referenced by business code becomes public artifact content, so a misnamed value can disclose information. Build choices are fixed when the artifact is generated; a setting that must change after deployment requires a validated, transported, and documented runtime configuration mechanism.
|
||||
|
||||
@@ -18,7 +18,9 @@ Vite 配置与动态 client bundle 的共享 tsdown preset 使用同一 define
|
||||
|
||||
`DSH_CLIENT_*` 的名称本身表示公开性。凭据、路径和其他仅供 Host 或 CI 使用的值不得使用该前缀。
|
||||
|
||||
根构建包装脚本向两个 bundler 提供同一份精确的公开环境。每次完整构建都会把源码 Git HEAD 的七位前缀派生为 `DSH_CLIENT_COMMIT_HASH`;没有仓库元数据的构建环境可显式提供该值。除此之外,`pnpm run build` 继承调用方的 `DSH_CLIENT_*` 值,`pnpm run build:official` 则不依赖特定 shell 的环境变量语法,直接选择仓库的官方产物 profile,并设置 `DSH_CLIENT_BUILD_PROFILE=official` 供部署专属业务注册使用。完整构建成功后会写入精确的公开环境,以及覆盖 Vite 输出和所有动态 client bundle 的摘要;局部构建命令不会替换该记录。
|
||||
根构建包装脚本向两个 bundler 提供同一份精确的公开环境。每次完整构建都以 `DSH_CLIENT_VERSION` 携带根包版本,并以 `DSH_CLIENT_COMMIT_HASH` 携带源码 Git HEAD 的七位前缀;没有仓库元数据的构建环境可显式提供 commit。默认本地构建还会在构建前读取 Git 状态;存在任何暂存、未暂存、未跟踪或子模块变化时设置 `DSH_CLIENT_GIT_DIRTY=true`。没有变化的 worktree 和没有 Git 元数据的源码不携带 dirty 字段。这些由仓库持有的字段会替换继承值,除此之外,`pnpm run build` 继续继承调用方剩余的 `DSH_CLIENT_*` 值。
|
||||
|
||||
`pnpm run build:official` 不依赖特定 shell 的环境变量语法,直接选择仓库的官方产物 profile。它的精确环境携带版本和 commit,设置 `DSH_CLIENT_BUILD_PROFILE=official` 供部署专属业务注册使用,并省略本地 dirty 元数据。完整构建成功后会写入精确的公开环境,以及覆盖 Vite 输出和所有动态 client bundle 的摘要;局部构建命令不会替换该记录。`pnpm run dev:web` 则会在启动时读取一次默认本地环境,并在本次会话中把该环境传给所有 watcher stage。它不会校验完整构建记录,因为 watcher stage 会重写记录覆盖的全部产物。
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
@@ -30,8 +32,12 @@ Vite 配置与动态 client bundle 的共享 tsdown preset 使用同一 define
|
||||
|
||||
**统一改用 `import.meta.env`。** 动态插件输出为独立 CJS factory,不能保留 `import.meta`;业务代码仍会因产物路径不同而使用两套接口。
|
||||
|
||||
**让 watcher 复用上次完整构建记录。** watcher stage 会重写记录覆盖的全部 client 产物,因此正常开发期间产物摘要就会变为陈旧。官方构建记录还会让经过编辑的本地源码继续携带官方 profile 和标题。启动时读取一次可以让所有 stage 共用同一份本地元数据快照,同时避免 watcher 重启依赖记录的产物摘要。
|
||||
|
||||
**每次 watcher 重建都重新读取 Git 状态。** Vite 和 tsdown 在长驻 watcher 启动时固定 define 替换。仓库状态变化时重启构建流水线,会使一次增量源码修改重建无关产物;启动时只读取一次可以保持各 stage 一致,同时避免因后续状态变化而重新构建。
|
||||
|
||||
## Consequences
|
||||
|
||||
Vite 静态壳和共享 tsdown 动态 bundle 对同一 `DSH_CLIENT_*` 构建进程变量产生相同字符串值。未设置的静态点访问得到 `undefined`,非 `DSH_CLIENT_*` 值不会通过该机制进入浏览器产物,业务代码也无法枚举构建进程环境。每次完整构建都携带可公开展示的短源码 revision。CI 构建门禁选择官方 profile,而不把其中的公开值暴露给源码测试或无关 workflow 步骤。npm 打包与 built Web 测试会校验记录中的环境及当前产物摘要,因此默认构建后请求官方打包、局部重建或修改输出都会在消费产物前失败。
|
||||
Vite 静态壳和共享 tsdown 动态 bundle 对同一 `DSH_CLIENT_*` 构建进程变量产生相同字符串值。未设置的静态点访问得到 `undefined`,非 `DSH_CLIENT_*` 值不会通过该机制进入浏览器产物,业务代码也无法枚举构建进程环境。每次完整构建都携带可公开展示的包版本和短源码 revision;dirty 的默认构建还会标明其源码存在未提交变化。CI 构建门禁选择官方 profile,而不把其中的公开值暴露给源码测试或无关 workflow 步骤。npm 打包与 built Web 测试会校验记录中的环境及当前产物摘要,因此默认构建后请求官方打包、局部重建或修改输出都会在消费产物前失败。watch build 会保留启动时的快照,直到 `pnpm run dev:web` 重启。
|
||||
|
||||
任何被业务代码引用的 `DSH_CLIENT_*` 值都会成为公开产物内容,命名错误可能泄露信息。构建选择在产物生成时固定;需要部署后变化的设置必须使用拥有校验、传输和文档的运行时配置机制。
|
||||
|
||||
+2
-2
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-18-session-history-and-event-transport.md
|
||||
2026-08-18-session-history-and-event-transport.md: 206d3d13d1f183b97b02b89644b022367be2ebfd
|
||||
2026-08-18-session-history-and-event-transport.zh.md: 3d0b5d4ab768ecd3877bfde86822245d0b63ac49
|
||||
2026-08-18-session-history-and-event-transport.md: 808565ff7df60b8aa6aa3f18820c1139b8bf5362
|
||||
2026-08-18-session-history-and-event-transport.zh.md: 8bd00def4531afa9cdf77ae7f689f2e77908545e
|
||||
|
||||
+20
-14
@@ -134,7 +134,7 @@ If a page request is canceled with its physical carrier generation, the journal
|
||||
|
||||
`packages/api/session-controller` provides Host `ctx.sessionController` and the generated `ctx.remote.session` namespace.
|
||||
|
||||
It owns Session list, search, create, models, selectModel, rename, fork, prompt, attachment, updateQueue, cancel, page, follow, and control.
|
||||
It owns Session list, search, create, selectModel, rename, fork, prompt, attachment, updateQueue, cancel, page, follow, and control. The Host-generation model catalog is exposed separately through `llm.models` because it is not Session-specific.
|
||||
|
||||
The package separates agent, commands, control, history, and list controllers internally, but Session identity resolution, activation policy, subagent ownership, and Remote error projection have one public owner.
|
||||
|
||||
@@ -148,9 +148,9 @@ Each method explicitly selects a cold inspection, live-only lookup, or resume-ca
|
||||
|
||||
| Operation | Source or result without a live Agent | Activation rule |
|
||||
|---|---|---|
|
||||
| `session.list`, `search` | persistence, projection cache, or cold log | Never resumes an Agent |
|
||||
| `session.list`, `search` | headers and projection cache; a bounded small-log read can resolve uncertain blankness | Never resumes an Agent |
|
||||
| `session.page(address)` | attached Session or persistence log | Never resumes an Agent |
|
||||
| `session.follow(address)` | cold-read current cursor, then wait for future appends | Neither opening nor waiting resumes an Agent |
|
||||
| `session.follow(address)` | one live or prepared observation carrying the opening page and projections | Publishes the snapshot first, then promotes an ordinary cold Session once in the background |
|
||||
| `session.control()` | current attached Agents, pending registry, and process-local registries | Baseline and reconnect do not resume an Agent |
|
||||
| `session.attachment`, fork source read | authorized durable Session data | A read does not resume an Agent |
|
||||
| `session.updateQueue`, `cancel` | only the current live Agent | Does not resume vanished state |
|
||||
@@ -159,6 +159,12 @@ Each method explicitly selects a cold inspection, live-only lookup, or resume-ca
|
||||
|
||||
Reading titles, lists, and projections does not require an Agent. An observation operation cannot inherit resume authority merely because another Remote endpoint uses Agent lookup.
|
||||
|
||||
`SessionQuery.observeSession()` chooses an attached Session or borrows one prepared source from `SessionPersistence.borrowSession()`. The persistence preparation cache shares concurrent cold reads and pins the exact unpublished Session until every observation lease is released. An observation computes either all registered projections or none; callers may expose a subset, but no caller creates a partial projection state.
|
||||
|
||||
`session.list` never performs an unbounded cold-log scan. It uses cached projection hints when available and may fully observe only an individually stored artifact within the configured small-log byte limit to distinguish an abandoned blank Session. Missing or unreadable hints keep the row visible with unknown metadata.
|
||||
|
||||
`model/selection` is a required-on-read durable event because it changes the model route used by the next request. Its projection records both the last request selection and a later pending selection; prompt assembly consumes the pending value when the matching `request/header` is committed.
|
||||
|
||||
#### Session journal
|
||||
|
||||
`session.page` returns a history window clipped on message boundaries with contiguous internal sequence numbers. Every request must carry an explicit `throughSeq`; this value comes from the corresponding `session.follow` generation's opening cursor and fixes the read at the same log cut. A tail page without `beforeSeq` must end exactly at `throughSeq`, where `-1` denotes an empty log. `beforeSeq` only selects an older page before that cut and cannot replace the synchronization cursor. `maxMessages` limits user/assistant message count without dropping chunks, tools, or state events between those messages.
|
||||
@@ -167,20 +173,20 @@ The tail page also carries a projection baseline no later than `throughSeq`; old
|
||||
|
||||
Ordinary Sessions and direct subagents use one `SessionAddress` protocol. A direct-subagent address carries parent Session, child Session, and mode; a cold Host read verifies durable ownership and descriptor rather than authorizing access from the child id alone.
|
||||
|
||||
`session.follow` installs `session/event` and `session/created` listeners before checking an attached Session or persistence, then reads the current cursor.
|
||||
`session.follow` installs `session/event` and `session/created` listeners before observing an attached or prepared Session.
|
||||
|
||||
The first follow response is `{ type: 'opened', cursor }`. A generation with `afterSeq` first replays the missing suffix from the authoritative log, then emits commits buffered during the read in sequence order.
|
||||
The first follow response is a complete `{ type: 'snapshot', header, cursor, events, hasMore, projections }` frame. Every reconnect sends another complete snapshot replacement; the protocol has no `afterSeq`. Events committed during observation remain buffered and are emitted after the snapshot in sequence order.
|
||||
|
||||
A cold Session can open history immediately and keep follow waiting. Future events appear only after another explicit command resumes the Agent.
|
||||
A cold ordinary Session can publish its prepared snapshot immediately. After that first frame, the Controller transfers a retained observation to one background promotion; follow does not wait for activation. Direct-subagent addresses never use this promotion path.
|
||||
|
||||
Client `SessionEventStream` extends `RemoteJournalStream` and supplies only `session.follow`, `session.page`, the Session sequence algorithm, and repair requests. The general layer first obtains opening cursor `C`, then calls `session.page({ throughSeq: C })`; entries `C + 1...` received during the read remain in the follow queue, and the page must cover exactly through `C` before the layer merges and publishes a continuous sequence.
|
||||
Client `SessionEventStream` extends `RemoteJournalStream` and supplies only `session.follow`, `session.page`, the Session sequence algorithm, and repair requests. The general layer validates and publishes the opening snapshot directly. It calls `session.page({ throughSeq })` only for older history or when a later event reveals a sequence gap.
|
||||
|
||||
```text
|
||||
ctx.remote.session.follow(address, afterSeq?) --------|
|
||||
|[]> SessionEventStream
|
||||
ctx.remote.session.page(address, throughSeq, pageArgs) -| |-- replace(window)
|
||||
|-- prepend(history)
|
||||
`-- append(live entry)
|
||||
ctx.remote.session.follow(address, pageArgs) ----------------|
|
||||
snapshot(header, cursor, page, projections), event* |[]> SessionEventStream
|
||||
ctx.remote.session.page(address, throughSeq, pageArgs) -------| |-- replace(window)
|
||||
|-- prepend(history)
|
||||
`-- append(live entry)
|
||||
```
|
||||
|
||||
Each Client Session owns only one current `events: SessionEventStream | undefined`. The read-only `SessionEventSource` gives the materialized event window to Conversation consumers.
|
||||
@@ -328,7 +334,7 @@ Connection tests pin missing, duplicate, and withdrawn generation sources; the r
|
||||
|
||||
`RemoteSnapshotStream` tests pin exactly one opening snapshot per generation, rejection of an update before a snapshot, rejection of duplicate snapshots, and reconnect replacement.
|
||||
|
||||
`RemoteJournalStream` tests pin follow-before-page, opening-overlap removal, contiguous append, historical prepend, reconnect catch-up, gap repair, and one atomic replacement.
|
||||
`RemoteJournalStream` tests pin snapshot-first opening, contiguous append, historical prepend, reconnect replacement, gap repair, and one atomic replacement.
|
||||
|
||||
Session Host tests pin cold page/follow without increasing attached Agents, contiguous events reaching a cold follow after an explicit prompt, direct-subagent ownership, message-aligned pagination, and terminal-error projection.
|
||||
|
||||
@@ -352,7 +358,7 @@ Static checks pin that API Proxy exports no Session/Workspace Host-frame carrier
|
||||
|
||||
## Consequences
|
||||
|
||||
The browser can read and follow a durable Session while its Agent is stopped. Observation does not implicitly resume execution; only explicitly authorized Session commands create or resume Agents according to their own rules.
|
||||
The browser can read a durable Session while its Agent is stopped. Opening an ordinary Session publishes the prepared snapshot before one background promotion begins; list, search, page, and other observation-only reads never activate it.
|
||||
|
||||
Durable logs repair a missing suffix by sequence number and page; Session control and Workspace state converge through opening snapshots; ordinary Remote Events promise no replay. Recovery semantics follow the data kind instead of imitating one another.
|
||||
|
||||
|
||||
+20
-14
@@ -134,7 +134,7 @@ repair 期间旧 window 保持可读;page 与期间积累的 live entries 拼
|
||||
|
||||
`packages/api/session-controller` 提供 Host `ctx.sessionController` 与生成的 `ctx.remote.session` namespace。
|
||||
|
||||
它拥有 Session list、search、create、models、selectModel、rename、fork、prompt、attachment、updateQueue、cancel、page、follow 与 control。
|
||||
它拥有 Session list、search、create、selectModel、rename、fork、prompt、attachment、updateQueue、cancel、page、follow 与 control。Host generation 的 model catalog 通过独立的 `llm.models` 公开,因为它不属于特定 Session。
|
||||
|
||||
包内的 agent、commands、control、history 与 list controller 分开实现,但 Session 身份解析、激活策略、subagent ownership 和 Remote 错误投影只有一个公开 owner。
|
||||
|
||||
@@ -148,9 +148,9 @@ Session Remote 方法传递 `SessionId` 或 `SessionAddress`,不靠参数类
|
||||
|
||||
| 操作 | 无 live Agent 时的数据来源或结果 | 激活规则 |
|
||||
|---|---|---|
|
||||
| `session.list`、`search` | persistence、投影缓存或冷日志 | 永不恢复 Agent |
|
||||
| `session.list`、`search` | header 与投影缓存;可通过有界的小日志读取判断不确定的 blank 状态 | 永不恢复 Agent |
|
||||
| `session.page(address)` | attached Session 或 persistence 日志 | 永不恢复 Agent |
|
||||
| `session.follow(address)` | 冷读当前 cursor,等待将来的 append | 建联和等待都不恢复 Agent |
|
||||
| `session.follow(address)` | 一份携带 opening page 与 projection 的 live 或 prepared observation | 先发布 snapshot,再在后台把普通冷 Session 提升一次 |
|
||||
| `session.control()` | 当前 attached Agent、pending registry 与进程内 registry | baseline 与重连不恢复 Agent |
|
||||
| `session.attachment`、fork 源读取 | 已授权的持久 Session 数据 | 读取不恢复 Agent |
|
||||
| `session.updateQueue`、`cancel` | 仅命中当前 live Agent | 不为已消失状态恢复 Agent |
|
||||
@@ -159,6 +159,12 @@ Session Remote 方法传递 `SessionId` 或 `SessionAddress`,不靠参数类
|
||||
|
||||
读取 title、列表和投影不要求 Agent。观察操作不能因为另一个 Remote endpoint 使用了 Agent lookup 而继承其恢复权限。
|
||||
|
||||
`SessionQuery.observeSession()` 选择 attached Session,或从 `SessionPersistence.borrowSession()` 借用 prepared source。Persistence preparation cache 共享并发冷读取,并在所有 observation lease 释放前固定同一个未发布 Session。一次 observation 要么计算所有已注册 projection,要么完全不计算;调用方可以只公开其中一部分,但不会建立只计算部分 projection 的中间状态。
|
||||
|
||||
`session.list` 不会无界扫描冷日志。它优先使用缓存的 projection hint,仅在独立存储 artifact 不超过配置的小日志字节上限时,才可能完整观察日志以判断不确定的 blank 状态。hint 缺失或不可读时,列表仍保留该行,并把 metadata 视为未知。
|
||||
|
||||
`model/selection` 是 required-on-read 的持久 event,因为它改变下一次请求使用的 model route。对应 projection 同时记录最近一次 request selection 与之后的 pending selection;prompt assembly 在提交匹配的 `request/header` 时消费 pending value。
|
||||
|
||||
#### Session 日志
|
||||
|
||||
`session.page` 返回一段按消息边界裁剪、内部 seq 连续的历史窗口。每个请求必须显式携带 `throughSeq`;该值来自对应 `session.follow` generation 的 opening cursor,并把本次读取固定在同一个日志切点。无 `beforeSeq` 的 tail page 必须精确结束于 `throughSeq`,其中 `-1` 表示空日志;`beforeSeq` 只选择该切点之前的更早页面,不能替代同步 cursor。`maxMessages` 限制 user/assistant 消息数,不丢弃这些消息之间的 chunk、tool 或状态事件。
|
||||
@@ -167,20 +173,20 @@ tail page 同时携带不晚于 `throughSeq` 的 projection baseline;旧页只
|
||||
|
||||
普通 Session 与 direct subagent 使用同一个 `SessionAddress` 协议。direct subagent 地址同时携带父 Session、子 Session 与 mode,Host 冷读时验证持久 ownership 和 descriptor,不能只凭 child id 越权读取。
|
||||
|
||||
`session.follow` 在检查 attached Session 或 persistence 前先安装 `session/event` 与 `session/created` listener,再读取当前 cursor。
|
||||
`session.follow` 在观察 attached 或 prepared Session 前先安装 `session/event` 与 `session/created` listener。
|
||||
|
||||
首次 follow 返回 `{ type: 'opened', cursor }`。带 `afterSeq` 的 generation 先从权威日志重放缺失后缀,再按 seq 排出读取期间缓存的 commit。
|
||||
首次 follow 返回完整的 `{ type: 'snapshot', header, cursor, events, hasMore, projections }` frame。每次重连都发送另一份完整 snapshot replacement;协议不含 `afterSeq`。观察期间提交的 event 会保留在缓冲区,并在 snapshot 之后按 seq 发出。
|
||||
|
||||
冷 Session 可以立即打开历史并保持 follow 等待。只有另一条显式命令恢复 Agent 后,后续事件才会出现。
|
||||
普通冷 Session 可以立即发布 prepared snapshot。首帧之后,Controller 把 retained observation 交给一次后台 promotion;follow 不等待激活。Direct-subagent 地址不会进入该 promotion 路径。
|
||||
|
||||
Client 的 `SessionEventStream` 继承 `RemoteJournalStream`,只提供 `session.follow`、`session.page`、Session seq 算法与 repair request。通用层先取得 opening cursor `C`,再调用 `session.page({ throughSeq: C })`;读取期间收到的 `C + 1...` entries 留在 follow 队列中,page 精确覆盖至 `C` 后才按连续 seq 合并并发布。
|
||||
Client 的 `SessionEventStream` 继承 `RemoteJournalStream`,只提供 `session.follow`、`session.page`、Session seq 算法与 repair request。通用层直接校验并发布 opening snapshot;仅在读取更早历史或后续 event 暴露 seq gap 时调用 `session.page({ throughSeq })`。
|
||||
|
||||
```text
|
||||
ctx.remote.session.follow(address, afterSeq?) --------|
|
||||
|[]> SessionEventStream
|
||||
ctx.remote.session.page(address, throughSeq, pageArgs) -| |-- replace(window)
|
||||
|-- prepend(history)
|
||||
`-- append(live entry)
|
||||
ctx.remote.session.follow(address, pageArgs) ----------------|
|
||||
snapshot(header, cursor, page, projections), event* |[]> SessionEventStream
|
||||
ctx.remote.session.page(address, throughSeq, pageArgs) -------| |-- replace(window)
|
||||
|-- prepend(history)
|
||||
`-- append(live entry)
|
||||
```
|
||||
|
||||
每个 Client Session 只持有一个当前 `events: SessionEventStream | undefined`。只读 `SessionEventSource` 把已物化 event window 交给 Conversation consumer。
|
||||
@@ -328,7 +334,7 @@ Connection 测试固定 generation source 缺失、重复注册、撤回、`$eve
|
||||
|
||||
`RemoteSnapshotStream` 测试固定每 generation 恰好一份 opening snapshot、update-before-snapshot 拒绝、重复 snapshot 拒绝和重连 replacement。
|
||||
|
||||
`RemoteJournalStream` 测试固定 follow-before-page、opening overlap 去重、连续 append、历史 prepend、重连 catch-up、gap repair 与一次性 replacement。
|
||||
`RemoteJournalStream` 测试固定 snapshot-first opening、连续 append、历史 prepend、重连 replacement、gap repair 与一次性 replacement。
|
||||
|
||||
Session Host 测试固定 cold page/follow 不增加 attached Agent、显式 prompt 后 cold follow 收到连续事件、direct subagent ownership、message-aligned pagination 和终止错误投影。
|
||||
|
||||
@@ -352,7 +358,7 @@ Remote Event Client 测试固定实例私有 key、Cordis 注册顺序、Agent C
|
||||
|
||||
## 后果
|
||||
|
||||
浏览器可以在 Agent 停止时读取并跟随持久 Session。观察不隐式恢复执行,只有明确获得授权的 Session 命令按各自约定创建或恢复 Agent。
|
||||
浏览器可以在 Agent 停止时读取持久 Session。打开普通 Session 时先发布 prepared snapshot,再开始一次后台 promotion;list、search、page 及其他只读 observation 不会激活 Agent。
|
||||
|
||||
持久日志用 seq 与 page 修复缺失后缀;Session control 和 Workspace state 用 opening snapshot 收敛;普通 Remote Event 不承诺重放。恢复语义由数据类型决定,不再互相模拟。
|
||||
|
||||
|
||||
+2
-2
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-19-web-index-injection-table.md
|
||||
2026-08-19-web-index-injection-table.md: 9ed02aa94cd318d107a32802d8723652e6b10ea2
|
||||
2026-08-19-web-index-injection-table.zh.md: 8ad036766faa14071b20da12ef907ab012cae23f
|
||||
2026-08-19-web-index-injection-table.md: 050690946a73ce453946f6d1152c9e86e1ba3aaf
|
||||
2026-08-19-web-index-injection-table.zh.md: 27d1cbce1dc39ebe22e0b79e52f9ce935279ee69
|
||||
|
||||
@@ -10,16 +10,16 @@ The web shell's boot HTML needs three kinds of injection: client-modules' boot p
|
||||
|
||||
## Decision
|
||||
|
||||
Make the injection surface an event over pure data: the webserver declares the `webserver/index-inject` event and the `IndexInjection` row union (`global`/`script`/`script-src`/`style`/`html`, `head|body` placement). A plugin that wants to inject subscribes and pushes rows; every collection (`collectIndexInjections()`) is a fresh emit, so subscribers read live state at emit time (module graph, theme preference — no re-registration staleness), and a subscription dies with its fiber.
|
||||
Make the injection surface an event over pure data: the webserver declares the `webserver/index-inject` event and the `IndexInjection` row union (`global`/`script`/`script-src`/`script-preload`/`style`/`html`, with placement where applicable). A plugin that wants to inject subscribes and pushes rows; every collection (`collectIndexInjections()`) is a fresh emit, so subscribers read live state at emit time (module graph, theme preference — no re-registration staleness), and a subscription dies with its fiber.
|
||||
|
||||
One table, two renderers: the served form's `webServer.renderIndex(html)` renders rows into index.html deterministically (head rows after the opening head tag, body rows after the opening body tag; `<` JSON-escaped in global values, attribute-escaped `src`); the worker form's `/__boot__` payload is `{ injections }`, executed row by row by a small page-side interpreter (set global / create script element / load external through the tunnel's `loadBundle` / mount style and markup). Rows are pure JSON data — that is the both-ends-equivalent discipline.
|
||||
One table, two renderers: the served form's `webServer.renderIndex(html)` renders rows into index.html deterministically (head rows after the opening head tag, body rows after the opening body tag; `<` JSON-escaped in global values, attribute-escaped `src`); the worker form's `/__boot__` payload is `{ injections }`, executed row by row by a small page-side interpreter (set global / create script element / load external through the tunnel's `loadBundle` / mount style and markup). A `script-preload` row renders a browser preload hint in served HTML and is ignored by the worker interpreter, whose `/plugins` resources exist only behind the tunnel and load on demand. Rows are pure JSON data — that is the both-ends-equivalent discipline.
|
||||
|
||||
`tapIndex`/`applyIndexTaps` survive as the raw-HTML escape hatch, applied after row rendering; every internal consumer moved to the event.
|
||||
|
||||
## Consequences
|
||||
|
||||
- client-modules and ui-theme no longer regex-edit HTML; the worker's `readBootPayload` service-poking (`clientModules`, `settings`, theme constants through `loader.load`) is deleted; the page-side `installModuleLoaderFacade`, `applyBootTheme`, and `PARSER_PRELOAD_IDS` re-implementations retire.
|
||||
- Ordering: across subscribers, subscription order (same as the old tap order); within one subscriber, push order — modules itself guarantees queue → preloads → global.
|
||||
- Ordering: across subscribers, subscription order (same as the old tap order); within one subscriber, push order — modules itself guarantees queue → application preload → bootstrap script → global.
|
||||
- The served rendering of the manifest global changed from `window.__DSH_BOOT__ =` to `globalThis["__DSH_BOOT__"] =`; no committed snapshot expectation carries that text, so none needed re-recording.
|
||||
- New model-visible or page-visible boot inputs extend the row union; no new tap consumers.
|
||||
|
||||
|
||||
@@ -10,16 +10,16 @@ Web 壳的启动 HTML 需要三类注入:client-modules 的引导协议(`__M
|
||||
|
||||
## Decision
|
||||
|
||||
注入面事件化、数据化:webserver 声明 `webserver/index-inject` 事件与纯数据行类型 `IndexInjection`(`global`/`script`/`script-src`/`style`/`html`,`head|body` 定位)。想注入的插件订阅事件、往表里 push 行;每次收集(`collectIndexInjections()`)都是一次全新 emit,订阅方现读现填(模块图、主题偏好天然新鲜,无重注册问题),订阅随 fiber 销毁自动摘除。
|
||||
注入面事件化、数据化:webserver 声明 `webserver/index-inject` 事件与纯数据行类型 `IndexInjection`(`global`/`script`/`script-src`/`script-preload`/`style`/`html`,在适用的行上携带定位)。想注入的插件订阅事件、往表里 push 行;每次收集(`collectIndexInjections()`)都是一次全新 emit,订阅方现读现填(模块图、主题偏好天然新鲜,无重注册问题),订阅随 fiber 销毁自动摘除。
|
||||
|
||||
一张表两个渲染器:served 形态 `webServer.renderIndex(html)` 确定性把行渲染进 index.html(head 行插 head 首、body 行插 body 首,全局值 JSON `<` 转义、src 属性转义);worker 形态 `/__boot__` 载荷就是 `{ injections }`,页面侧小解释器逐行执行(设全局 / 建脚本元素 / 经 tunnel loadBundle 载外链 / 挂样式与 DOM)。行是纯 JSON 数据,这是双端等价的纪律。
|
||||
一张表两个渲染器:served 形态 `webServer.renderIndex(html)` 确定性把行渲染进 index.html(head 行插 head 首、body 行插 body 首,全局值 JSON `<` 转义、src 属性转义);worker 形态 `/__boot__` 载荷就是 `{ injections }`,页面侧小解释器逐行执行(设全局 / 建脚本元素 / 经 tunnel loadBundle 载外链 / 挂样式与 DOM)。`script-preload` 行在 served HTML 中渲染为浏览器预加载提示;worker 解释器忽略它,因为 `/plugins` 资源只存在于 tunnel 后方,并在实际需要时加载。行是纯 JSON 数据,这是双端等价的纪律。
|
||||
|
||||
`tapIndex`/`applyIndexTaps` 保留为原始 HTML 变换的逃生口,在行渲染之后执行;内部消费者全部迁走。
|
||||
|
||||
## Consequences
|
||||
|
||||
- client-modules 与 ui-theme 不再各自正则改 HTML;worker 侧 `readBootPayload` 的 `ctx.get` 手掏(clientModules、settings、theme 常量 loader.load)删除;页面侧 `installModuleLoaderFacade`、`applyBootTheme`、`PARSER_PRELOAD_IDS` 三份重抄退役。
|
||||
- 顺序语义:跨订阅方按订阅注册顺序(与旧 tap 顺序一致),单订阅方内按 push 顺序;modules 自己保证 队列→preload→全局 三行有序。
|
||||
- 顺序语义:跨订阅方按订阅注册顺序(与旧 tap 顺序一致),单订阅方内按 push 顺序;modules 自己保证队列→application preload→bootstrap script→全局的顺序。
|
||||
- `__DSH_BOOT__` 的 served 渲染文本从 `window.__DSH_BOOT__ =` 变为 `globalThis["__DSH_BOOT__"] =`;已核实无已提交快照期望含此文本,无需重录。
|
||||
- 新的模型可见/页面可见注入一律走行类型扩展,不再新增 tap 消费者。
|
||||
|
||||
|
||||
+2
-2
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-20-webworker-pack-lowering-and-preview.md
|
||||
2026-08-20-webworker-pack-lowering-and-preview.md: 72a6ccf856c95f835e103bd355223bf3cf42f692
|
||||
2026-08-20-webworker-pack-lowering-and-preview.zh.md: 074d44833c34a2999a5c47da809533065201b580
|
||||
2026-08-20-webworker-pack-lowering-and-preview.md: 1ec8fb050445b0a90d8fbf0d97c9ef10cb28b287
|
||||
2026-08-20-webworker-pack-lowering-and-preview.zh.md: 86da66560b509b38ba3ffa49d58035f3e5a173f1
|
||||
|
||||
+2
-2
@@ -10,9 +10,9 @@ The browser worker can neither compile modules at load nor be served by the prod
|
||||
|
||||
## Decision
|
||||
|
||||
**Lowering happens at pack time only.** `@deepseek-ai/dsh-experimental-webworker-packer` composes the profile, materializes the closure, and lowers every JavaScript body; `LOWERING_VERSION` and `WRAPPER_PARAMS` are the pack↔worker contract and live in `src/image-layout.ts` beside the rest of the image layout. The loader wraps bodies exactly as the image holds them: a body still carrying module syntax is a refusal naming the image, and `startWorkerHost` requires the manifest's `lowered` to equal this build's contract before it mounts a single module. `lowerModuleSource` is the transform's only face and the packer its only caller; inside the worker graph, imports name the module that owns the value — never the package barrel, which is the edge that smuggled the parser in.
|
||||
**Lowering happens at pack time only.** `@deepseek-ai/dsh-experimental-webworker-packer` composes the profile, materializes the closure, and lowers every JavaScript body; `LOWERING_VERSION` and `WRAPPER_PARAMS` are the pack↔worker contract and live in `src/image-layout.ts` beside the rest of the image layout. The loader wraps bodies exactly as the image holds them: a body still carrying module syntax is a refusal naming the image, and `startWorkerHost` requires the manifest's `lowered` to equal this build's contract before it mounts a single module. `lowerModuleSource` is the transform's only face and the packer its only caller; inside the worker graph, imports name the module that owns the value — never the package barrel, which is the edge that smuggled the parser in. Source-directory exclusion applies only to workspace and vendored packages whose runtime plane is built `lib/`; installed third-party packages retain JavaScript under `src/` and `dist/` because their published entrypoints may resolve there.
|
||||
|
||||
**The preview is the served page plus one tag.** One Vite build emits `dist/index.html` and `dist/preview.html` sharing every chunk; the only difference is a prepended bootstrap entry whose module connects the worker host. Startup then converges on one protocol: whichever side applies the injection table settles the `__DSH_BOOT_READY__` deferred — the served renderer resolves it in a tail script after the rendered rows, the worker bootstrap installs it before its first await and settles it after the last row — and the client entry awaits it before reading any injected state, so the chain from the stock entry onward is the served chain verbatim. The build uses a relative base so the output mounts under any static directory; the served form anchors deep SPA-fallback paths by rendering `<base href="/">` at serve time, keeping the on-disk pages byte-shared.
|
||||
**The preview is the served page plus one tag.** One Vite build emits `dist/index.html` and `dist/preview.html` sharing every chunk; the only difference is a prepended bootstrap entry whose module connects the worker host. Startup then converges on one protocol: whichever side applies the injection table settles the `__DSH_BOOT_READY__` deferred — the served renderer resolves it in a tail script after the rendered rows, the worker bootstrap installs it before its first await and settles it after the last row — and the client entry awaits it before reading any injected state, so the chain from the stock entry onward is the served chain verbatim. Plugin combo scripts and maps travel through the tunnel; the page-side loader embeds each tunnel-only map as a Base64 data URL before executing its script Blob, preserving indexed-map component names in DevTools without another object-URL lifetime. The build uses a relative base so the output mounts under any static directory; the served form anchors deep SPA-fallback paths by rendering `<base href="/">` at serve time, keeping the on-disk pages byte-shared.
|
||||
|
||||
**The repository preview carries selectable filesystem sources.** The packer emits one base image and a small overlay archive for each named built-in fixture. Without a source query, `preview.html` waits at a chooser for an empty filesystem, the built-in fixtures, or the separately owned WebFS provider. A valid `preview-fixture=none|<built-in-id>` query selects directly and skips the chooser for deterministic browser runs; its distinct name avoids the Client's existing `fixture` transport switch. The Worker mounts the base and then applies the selected overlays in order, restricted to `home/` and `workspace/`, before it validates the base manifest or boots Cordis. `packages/experimental/webworker-runtime/tests/fixtures/vfs-example/` supplies one built-in overlay without giving the packer Session or Workspace knowledge. Its plaintext JSONL logs use the persistence backend's real project/session directory layout, so Session Persistence reads them cold and Workspace Registry derives the Workspace from their `/dsh/workspace` headers. The main Session exceeds the Client's 50-message page and keeps representative tool results at its tail; persisted one-shot and continuable children exercise the subagent catalog. WebFS authorization and user data remain a separate provider and never share this fixture tree.
|
||||
|
||||
|
||||
+2
-2
@@ -10,9 +10,9 @@
|
||||
|
||||
## 决定
|
||||
|
||||
**Lowering 只发生在 pack 期。** `@deepseek-ai/dsh-experimental-webworker-packer` 组合 profile、物化闭包、lower 每个 JavaScript 模块体;`LOWERING_VERSION` 与 `WRAPPER_PARAMS` 是 pack↔worker 的契约,与镜像布局的其余部分一起放在 `src/image-layout.ts`。装载器完全按镜像持有的形态包装模块体:仍带模块语法的模块体是一次点名镜像的拒绝,且 `startWorkerHost` 在挂载任何模块之前要求 manifest 的 `lowered` 等于本构建的契约。`lowerModuleSource` 是转换器唯一的面、packer 是它唯一的调用方;worker 图内部的 import 一律指向拥有该值的模块——绝不指向包 barrel,那正是把解析器偷运进来的那条边。
|
||||
**Lowering 只发生在 pack 期。** `@deepseek-ai/dsh-experimental-webworker-packer` 组合 profile、物化闭包、lower 每个 JavaScript 模块体;`LOWERING_VERSION` 与 `WRAPPER_PARAMS` 是 pack↔worker 的契约,与镜像布局的其余部分一起放在 `src/image-layout.ts`。装载器完全按镜像持有的形态包装模块体:仍带模块语法的模块体是一次点名镜像的拒绝,且 `startWorkerHost` 在挂载任何模块之前要求 manifest 的 `lowered` 等于本构建的契约。`lowerModuleSource` 是转换器唯一的面、packer 是它唯一的调用方;worker 图内部的 import 一律指向拥有该值的模块——绝不指向包 barrel,那正是把解析器偷运进来的那条边。源码目录排除只用于运行期使用已构建 `lib/` 的 workspace 与 vendored 包;已安装第三方包会保留 `src/` 和 `dist/` 下的 JavaScript,因为其发布入口可能解析到这些位置。
|
||||
|
||||
**preview 就是服务页面加一个标签。** 一次 Vite 构建产出共享全部 chunk 的 `dist/index.html` 与 `dist/preview.html`;唯一差异是前插的一个引导入口,其模块负责连接 worker host。启动随之汇于一个协议:应用注入表的一方 settle `__DSH_BOOT_READY__` deferred——served 渲染器在渲染完的行之后用尾部脚本 resolve,worker 引导段在首个 await 之前安装、末行生效后 settle——client 入口在读取任何注入状态前 await 它,因此从标准入口起的链路逐字就是 served 链路。构建使用相对 base,产物可挂载于任意静态目录;served 形态在 serve 期渲染 `<base href="/">` 锚定深层 SPA fallback 路径,磁盘上的两个页面保持字节共享。
|
||||
**preview 就是服务页面加一个标签。** 一次 Vite 构建产出共享全部 chunk 的 `dist/index.html` 与 `dist/preview.html`;唯一差异是前插的一个引导入口,其模块负责连接 worker host。启动随之汇于一个协议:应用注入表的一方 settle `__DSH_BOOT_READY__` deferred——served 渲染器在渲染完的行之后用尾部脚本 resolve,worker 引导段在首个 await 之前安装、末行生效后 settle——client 入口在读取任何注入状态前 await 它,因此从标准入口起的链路逐字就是 served 链路。插件 combo 脚本与 map 都通过 tunnel;页面侧 loader 会在执行脚本 Blob 前,把每个仅 tunnel 可达的 map 内嵌为 Base64 data URL,从而不依赖另一条 object URL 的生命周期,并在 DevTools 中保留 indexed map 的组件名称。构建使用相对 base,产物可挂载于任意静态目录;served 形态在 serve 期渲染 `<base href="/">` 锚定深层 SPA fallback 路径,磁盘上的两个页面保持字节共享。
|
||||
|
||||
**仓库 preview 携带可选择的文件系统来源。** Packer 产出一份基础镜像,并为每套具名内置 fixture 产出一份小型 overlay 归档。没有来源 query 时,`preview.html` 会停在选择面板,可选择空文件系统、内置 fixtures,或归另一实现所有的 WebFS provider。合法的 `preview-fixture=none|<built-in-id>` query 会直接选择并跳过面板,供确定性的浏览器流程使用;该独立名称避开 Client 既有的 `fixture` transport 开关。Worker 先挂载基础镜像,再按顺序把所选 overlays 应用到仅限 `home/` 和 `workspace/` 的路径,随后才校验基础 manifest 并启动 Cordis。`packages/experimental/webworker-runtime/tests/fixtures/vfs-example/` 提供其中一套内置 overlay,Packer 无需理解 Session 或 Workspace。明文 JSONL 日志使用 persistence backend 的真实 project/session 目录布局,因此 Session Persistence 会冷读取它们,Workspace Registry 则根据其 `/dsh/workspace` header 派生 Workspace。主 Session 超过 Client 的 50-message page,并把代表性工具结果留在尾页;持久化的 one-shot 与 continuable child 用于验证 subagent catalog。WebFS 授权与用户数据仍属于独立 provider,绝不与该 fixture 共用目录。
|
||||
|
||||
|
||||
+2
-2
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-22-single-dsh-application-launcher.md
|
||||
2026-08-22-single-dsh-application-launcher.md: 48a45cb2454b5532a78474203b6d88aef3dd0697
|
||||
2026-08-22-single-dsh-application-launcher.zh.md: dbf2fb3a5bdc16208b0435482d8d0851bd7c44d7
|
||||
2026-08-22-single-dsh-application-launcher.md: feac31b3eafced8158a8d79a0e5967a8de5e2f87
|
||||
2026-08-22-single-dsh-application-launcher.zh.md: 88fe4ef4d4e0436ecb450e3f5319be7acee6882b
|
||||
|
||||
+4
-4
@@ -8,7 +8,7 @@ English | [中文](2026-08-22-single-dsh-application-launcher.zh.md)
|
||||
|
||||
DeepSeek Harness application processes need one owner for composition, plugin resolution, environment discovery, shutdown, and user customization. A dedicated app bin with a complete `cordis.yml` creates a second lifecycle beside profile launch: plugins installed into a profile do not reach it, behavior drifts from `dsh-base`, and SDK callers learn arbitrary process argv instead of the product's composition model.
|
||||
|
||||
The Python SDK distributes a native executable and three platform wheels. Its packaged process must use the same profile launcher while preserving the closed VFS dependency tree, native sidecars, and installed-wheel evidence.
|
||||
The Python SDK distributes a native executable through four platform wheels. Its packaged process uses the same profile launcher while preserving the closed VFS dependency tree, native sidecars, and installed-wheel evidence.
|
||||
|
||||
## Decision
|
||||
|
||||
@@ -46,9 +46,9 @@ Direct SDK use follows normal Harness-home resolution: explicit `dshHome`, inher
|
||||
|
||||
### Python runtime
|
||||
|
||||
The Python runtime wheel packages the ordinary `@deepseek-ai/dsh` CLI from `node_modules/@deepseek-ai/dsh/lib/bin.js` through the private `dsh-python-runtime-closure` deploy manifest. The Python client selects `dsh --profile sdk` by default, ordered patch files, and an explicit Harness home; the runnable Python example selects `sdk-minimal`. The installed `dsh` console command exposes the same profile grammar and the separately packaged `web` application.
|
||||
The Python runtime wheel packages the ordinary `@deepseek-ai/dsh` CLI from `node_modules/@deepseek-ai/dsh/lib/bin.js` through the private `dsh-python-runtime-closure` deploy manifest. 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 and the separately packaged `web` application.
|
||||
|
||||
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.
|
||||
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. The [Python profile-runtime decision](2026-08-23-python-sdk-dsh-profile-runtime.md) owns this launch, and the [Windows x64 runtime decision](2026-08-23-python-sdk-windows-x64-runtime.md) owns the fourth carrier.
|
||||
|
||||
### Enforcement
|
||||
|
||||
@@ -76,7 +76,7 @@ The [ACP automation-only protocol](../simplification/2026-07-23-acp-automation-o
|
||||
|
||||
**Hot-reload protocol profiles.** Rejected: replacing a protocol server or its dependencies can invalidate pending frames and SDK-owned agents. Process restart is the adoption boundary for SDK and ACP configuration changes.
|
||||
|
||||
**Move the Python executable through profiles without a separate packaging proof.** Rejected: the native VFS closure, three platform wheels, ripgrep and spawn-helper sidecars, default config discovery, and clean-install behavior require their own migration evidence.
|
||||
**Move the Python executable through profiles without a separate packaging proof.** Rejected: the native VFS closure, four platform wheels, profile assets, ripgrep and spawn-helper sidecars, and clean-install behavior require their own migration evidence.
|
||||
|
||||
## Verification
|
||||
|
||||
|
||||
+4
-4
@@ -8,7 +8,7 @@ Status: implemented
|
||||
|
||||
DeepSeek Harness 应用进程需要由同一个机制负责组合、插件解析、环境发现、关闭和用户自定义。带完整 `cordis.yml` 的专用应用 bin 会在 profile 启动之外形成第二套生命周期:安装到 profile 的插件无法到达它,行为会与 `dsh-base` 偏离,SDK 调用方还需要学习任意进程 argv,而不是产品的组合模型。
|
||||
|
||||
Python SDK 分发一个原生可执行文件和三个平台 wheel 包。其打包进程必须使用同一 profile 启动器,同时保留封闭的 VFS 依赖树、原生伴随文件与 installed-wheel 证据。
|
||||
Python SDK 通过四个平台 wheel 包分发原生可执行文件。其打包进程使用同一 profile 启动器,同时保留封闭的 VFS 依赖树、原生伴随文件与 installed-wheel 证据。
|
||||
|
||||
## Decision
|
||||
|
||||
@@ -46,9 +46,9 @@ SDK 用户通过 profile 自定义插件。`dsh plugin --profile <name> ...` 管
|
||||
|
||||
### Python 运行时
|
||||
|
||||
Python 运行时 wheel 通过私有 `dsh-python-runtime-closure` 部署 manifest,打包来自 `node_modules/@deepseek-ai/dsh/lib/bin.js` 的普通 `@deepseek-ai/dsh` CLI。Python 客户端默认选择 `dsh --profile sdk`、有序 patch 文件与显式 Harness home;可运行 Python 示例选择 `sdk-minimal`。安装的 `dsh` 控制台命令暴露相同 profile 语法与单独打包的 `web` 应用。
|
||||
Python 运行时 wheel 通过私有 `dsh-python-runtime-closure` 部署 manifest,打包来自 `node_modules/@deepseek-ai/dsh/lib/bin.js` 的普通 `@deepseek-ai/dsh` CLI。Python 客户端默认选择 `dsh --profile sdk`、有序 patch 文件与显式 Harness home;`python/sdk/examples` 下的可运行示例选择 `sdk-minimal`。安装的 `dsh` 控制台命令暴露相同 profile 语法与单独打包的 `web` 应用。
|
||||
|
||||
可执行文件族是 `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 启动别名。
|
||||
可执行文件族是 `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 启动别名。[Python profile 运行时决策](2026-08-23-python-sdk-dsh-profile-runtime.zh.md)负责该启动方式,[Windows x64 运行时决策](2026-08-23-python-sdk-windows-x64-runtime.zh.md)负责第四个载体。
|
||||
|
||||
### 强制校验
|
||||
|
||||
@@ -76,7 +76,7 @@ Python 运行时 wheel 通过私有 `dsh-python-runtime-closure` 部署 manifest
|
||||
|
||||
**热重载协议 profile。** 拒绝:替换协议服务器或其依赖可能破坏待处理协议帧与 SDK 自有 agent。进程重启是 SDK 与 ACP 配置变更的采用边界。
|
||||
|
||||
**不做独立打包证明就把 Python 可执行文件迁移到 profile。** 拒绝:原生 VFS 闭包、三个平台 wheel 包、ripgrep 与 spawn-helper 伴随文件、默认配置发现和干净安装行为都需要自己的迁移证据。
|
||||
**不做独立打包证明就把 Python 可执行文件迁移到 profile。** 拒绝:原生 VFS 闭包、四个平台 wheel 包、profile 资源、ripgrep 与 spawn-helper 伴随文件和干净安装行为都需要自己的迁移证据。
|
||||
|
||||
## 验证
|
||||
|
||||
|
||||
+2
-2
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.md
|
||||
2026-08-23-python-sdk-dsh-profile-runtime.md: 3298224688e8f0cd4216f8ed68d9e4364014d2a9
|
||||
2026-08-23-python-sdk-dsh-profile-runtime.zh.md: 400e70113e1112aa6e4979c64a5046199c07e8a0
|
||||
2026-08-23-python-sdk-dsh-profile-runtime.md: dcb4f77048d516f0e187b611dc09c224fba47b58
|
||||
2026-08-23-python-sdk-dsh-profile-runtime.zh.md: 505b49506ae4c4be4809d588448b2c593d3ebaf5
|
||||
|
||||
@@ -34,7 +34,7 @@ The zero-code deployment manifest is `dsh-python-runtime-closure`. It packages `
|
||||
|
||||
Plain Node profiles use symlinks in `$DSH_HOME/profiles/node_modules` to share installation packages with external plugins. An operating-system symlink cannot traverse pkg's `/snapshot` filesystem, so the packaged CLI writes small real ESM proxy packages instead. Each proxy resolves the source package's explicit ESM export map directly under Node import conditions, exposes targets that exist in the installation, and re-exports their virtual module URLs. Export rows without an ESM runtime target and executable-only or declaration-only packages produce no unusable proxy entry; malformed export maps fail startup. A complete matching generation returns without acquiring the cross-process writer lock. A missing or stale entry acquires the lock, rechecks the generation, and repairs it without exposing partial proxies; either carrier can replace the other carrier's managed entry. Loader rows and external plugin peers therefore resolve through the normal profile parent walk while retaining one Cordis and one instance of each bundled module.
|
||||
|
||||
The published target set is Linux x64, Linux arm64, and macOS arm64. Installed-wheel black-box CI owns artifact provenance, default and patched profiles, external bundle installation, native tools, MCP, direct JSON-RPC, snapshots, and trusted real-provider turns on every target.
|
||||
The published target set is Linux x64, Linux arm64, macOS arm64, and Windows x64. Installed-wheel black-box CI owns artifact provenance, default and patched profiles, external bundle installation, native tools, MCP, direct JSON-RPC, snapshots, and trusted real-provider turns on every target. The [Windows x64 runtime decision](2026-08-23-python-sdk-windows-x64-runtime.md) owns the fourth artifact and its platform-specific shell surface.
|
||||
|
||||
## Existing decisions and supersession
|
||||
|
||||
|
||||
+1
-1
@@ -34,7 +34,7 @@ Python SDK 分发一个私有 Node 应用,直接启动完整外部 `cordis.yml
|
||||
|
||||
普通 Node profile 在 `$DSH_HOME/profiles/node_modules` 中使用符号链接,让外部插件共享安装包。操作系统符号链接无法进入 pkg 的 `/snapshot` 文件系统,因此打包 CLI 改为写入小型真实 ESM 代理包。每个代理直接按 Node import 条件解析源包的显式 ESM exports map,公开安装中实际存在的目标,并重新导出其虚拟模块 URL。没有 ESM 运行时目标的 export 项以及仅含可执行入口或类型声明入口的包不会产生不可用的代理条目;格式错误的 exports map 会导致启动失败。完整且匹配的 generation 不会获取跨进程写入锁。缺失或过期的配置项会获取该锁、重新检查 generation,并在不暴露半成品代理的前提下修复;任一载体都可以替换另一载体留下的受管配置项。Loader 配置项和外部插件 peer 因而可以通过普通 profile 逐级向上查找解析,同时保留一个 Cordis 和每个内置模块的单一实例。
|
||||
|
||||
已发布目标集合是 Linux x64、Linux arm64 与 macOS arm64。Installed-wheel 黑盒 CI 在每个目标上负责产物来源、默认及 patched profile、外部 bundle 安装、原生工具、MCP、直接 JSON-RPC、快照,以及可信真实提供方轮次。
|
||||
已发布目标集合是 Linux x64、Linux arm64、macOS arm64 与 Windows x64。Installed-wheel 黑盒 CI 在每个目标上负责产物来源、默认及 patched profile、外部 bundle 安装、原生工具、MCP、直接 JSON-RPC、快照,以及可信真实提供方轮次。[Windows x64 运行时决策](2026-08-23-python-sdk-windows-x64-runtime.zh.md)负责第四个产物及其平台专属 shell surface。
|
||||
|
||||
## 既有决策与取代关系
|
||||
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-23-python-sdk-windows-x64-runtime.md
|
||||
2026-08-23-python-sdk-windows-x64-runtime.md: 59a46d99f9e7ed411aeffbb541bbe3bb0c752078
|
||||
2026-08-23-python-sdk-windows-x64-runtime.zh.md: 3ab972aabb8135c8bc6285d129ba7bc9335eb11f
|
||||
@@ -0,0 +1,51 @@
|
||||
# Agent Note: Python SDK Windows x64 runtime
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-08-23-python-sdk-windows-x64-runtime.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
The Python SDK runtime distribution needs a Windows carrier without creating another application entrypoint or weakening the installed-wheel evidence used by the existing native targets. Windows executable names, Python wheel tags, ConPTY addons, ripgrep sidecars, shell composition, virtual environments, and process launch rules differ from Linux and macOS. Claiming Windows from cross-platform unit tests or from a non-Windows executable would leave the artifact selected by `pip` unproved.
|
||||
|
||||
## Decision
|
||||
|
||||
### One x64 product
|
||||
|
||||
`python/sdk-runtime/platforms.json` declares one Windows target, `win-x64`. Its pkg target is `node24-win-x64`, its runtime wheel tag is `py3-none-win_amd64`, and its payload is `deepseek-harness-sdk-runtime-win-x64.exe` with `deepseek-harness-sdk-runtime-win-x64-rg.exe`. The packaged `node-pty` tree must contain both x64 ConPTY addons. Runtime lookup rejects Windows arm64 rather than selecting or relabeling the x64 wheel.
|
||||
|
||||
The Python process still launches the ordinary `dsh --profile sdk` application and requires an explicit Harness home under the [Python profile-runtime decision](2026-08-23-python-sdk-dsh-profile-runtime.md). Windows adds no Python-specific Node application, complete-config entrypoint, implicit `~/.dsh`, or system Node requirement.
|
||||
|
||||
### Native build and publication
|
||||
|
||||
The executable builder accepts `win` as a pkg platform only with x64, requires the Windows build to run under x64 Node on a Windows host, preserves `.exe` names, and copies `@vscode/ripgrep-win32-x64` as the conventional `-rg.exe` sidecar. Pnpm subprocesses use a caller-supplied JavaScript entry through `process.execPath`. When the caller exposes a `.cmd` shim, the builder resolves the installed `pnpm.mjs` or `pnpm.cjs` through `PNPM_HOME`; it fails if no JavaScript entry exists instead of spawning the shim or enabling a command shell.
|
||||
|
||||
The required GitHub matrix builds `node24-win-x64` on `windows-2025` beside the three existing targets. The public GitHub release and GitLab tag pipeline each publish the same four runtime wheels plus the pure SDK wheel. Windows arm64 is absent from target parsing, manifests, matrices, release contents, and documentation.
|
||||
|
||||
### Installed-wheel behavior
|
||||
|
||||
The Windows lane creates a clean Windows virtual environment, installs the exact SDK and `win_amd64` runtime wheels, changes to a directory outside the checkout, unsets `PYTHONPATH` and `DSH_RUNTIME_MODE`, and runs the same `--scenario all --installed-wheel` blackbox as every other target. Trusted pull requests also run the same two-turn `sdk-live` provider scenario. Fork and Dependabot heads receive no key.
|
||||
|
||||
The public Python client gives the initial profile handshake an independent 30-second default through `initialize_timeout_seconds`. The bound accommodates cold Windows x64 executable startup and profile materialization while still failing a stuck runtime; callers may configure it separately from ordinary request timeouts.
|
||||
|
||||
After a successful shutdown response, the Python client closes stdin and waits within the configured shutdown timeout for the `dsh` context to exit and flush durable session state before terminating it. A failed shutdown retains immediate bounded termination. `shutdown_timeout_seconds` bounds each of the shutdown request, EOF grace, and termination-confirmation phases, so a pathological close can approach three times that value before the final kill. This distinction preserves the final accepted turn on Windows, where `terminate()` force-kills the process rather than delivering a catchable signal.
|
||||
|
||||
The minimal blackbox uses persistent `pwsh` plus `str_replace_editor` on Windows and owns `minimal/win-x64/model-visible.json`; Linux and macOS retain persistent Bash and the shared `minimal/model-visible.json`. The advanced process/subagent snapshot and restart/durable-log snapshot remain shared across all targets. The shipped [`sdk-minimal` bundle](../../../../packages/bundle/sdk-minimal/README.md) selects the same platform shell pair for the runnable Python tutorial.
|
||||
|
||||
## Existing decisions and supersession
|
||||
|
||||
This decision partially supersedes the Windows non-goal in the [single-file runtime distribution](2026-07-10-single-file-executable-sdk-runtime-distribution.md) and extends the required target set in the [installed Python wheel blackbox decision](../testing/2026-08-23-installed-python-wheel-black-box-ci.md). Those notes remain authoritative for SEA packaging, the two Python distributions, provenance checks, key handling, and the common blackbox scenarios.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Add Windows before the dsh profile runtime.** Rejected because tests for the retired private direct-config carrier would not prove the Windows form users receive. Windows is defined only for the sole `dsh` launch architecture.
|
||||
|
||||
**Publish Windows arm64 too.** Rejected because the accepted product scope is x64 only; adding a second architecture would require its own native builder, wheel tag, ConPTY and ripgrep payload checks, installed-wheel matrix leg, and release artifact.
|
||||
|
||||
**Give Windows a smaller smoke suite.** Rejected because a platform wheel cannot borrow protocol, persistence, worker, MCP, plugin, native-tool, or real-provider evidence from another executable. Platform-specific expected output is limited to the persistent shell surface; the remaining snapshots stay shared.
|
||||
|
||||
**Run the Windows leg through Git Bash.** Rejected because the repository requires native `pwsh` on Windows runners and MSYS path conversion would not prove native command behavior. Portable one-line steps use each runner's default shell; path, virtual-environment, and blackbox steps have explicit POSIX and PowerShell forms.
|
||||
|
||||
## Consequences
|
||||
|
||||
Python installation now selects a Node-free Windows x64 runtime with the same explicit-home and profile customization model as Linux and macOS. Every pull request pays for a fourth executable, runtime wheel, full keyless blackbox, and—on trusted heads—real provider task. Release validation retains five wheels instead of four. Windows arm64 users receive an explicit unsupported-platform failure until a separate native product decision supplies and proves that carrier.
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
# Agent Note: Python SDK Windows x64 运行时
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-08-23-python-sdk-windows-x64-runtime.md) | 中文
|
||||
|
||||
## Problem
|
||||
|
||||
Python SDK 运行时分发需要 Windows 载体,同时不能创建另一个应用入口,也不能削弱现有原生目标所使用的 installed-wheel 证据。Windows 的可执行文件名、Python wheel 标签、ConPTY addon、ripgrep sidecar、shell 组合、虚拟环境与进程启动规则均不同于 Linux 和 macOS。仅凭跨平台单元测试或非 Windows 可执行文件声称支持 Windows,会使 `pip` 实际选择的产物未经证明。
|
||||
|
||||
## Decision
|
||||
|
||||
### 唯一 x64 产品
|
||||
|
||||
`python/sdk-runtime/platforms.json` 声明唯一的 Windows 目标 `win-x64`。其 pkg 目标是 `node24-win-x64`,运行时 wheel 标签是 `py3-none-win_amd64`,载荷包含 `deepseek-harness-sdk-runtime-win-x64.exe` 与 `deepseek-harness-sdk-runtime-win-x64-rg.exe`。打包后的 `node-pty` 文件树必须包含两个 x64 ConPTY addon。运行时查找会拒绝 Windows arm64,不会选择 x64 wheel 或把它重新标记为 arm64。
|
||||
|
||||
Python 进程仍按 [Python profile 运行时决策](2026-08-23-python-sdk-dsh-profile-runtime.zh.md)启动普通 `dsh --profile sdk` 应用,并要求显式 Harness home。Windows 不会增加 Python 专用 Node 应用、完整配置入口、隐式 `~/.dsh` 或系统 Node 要求。
|
||||
|
||||
### 原生构建与发布
|
||||
|
||||
可执行文件构建器仅允许 x64 使用 pkg 的 `win` 平台,并要求 Windows 构建在 Windows 宿主的 x64 Node 下运行;构建器保留 `.exe` 文件名,并把 `@vscode/ripgrep-win32-x64` 复制为常规 `-rg.exe` sidecar。Pnpm 子进程通过 `process.execPath` 执行调用方提供的 JavaScript 入口。当调用方暴露 `.cmd` shim 时,构建器会通过 `PNPM_HOME` 解析已安装的 `pnpm.mjs` 或 `pnpm.cjs`;如果不存在 JavaScript 入口,构建会失败,而不会启动 shim 或启用命令 shell。
|
||||
|
||||
必需 GitHub 矩阵会在 `windows-2025` 上构建 `node24-win-x64`,与现有三个目标并列。公开 GitHub 发布与 GitLab 标签流水线都会发布同一组四个运行时 wheel 加纯 SDK wheel。目标解析、manifest、矩阵、发布内容与文档均不包含 Windows arm64。
|
||||
|
||||
### Installed-wheel 行为
|
||||
|
||||
Windows lane 会创建干净的 Windows 虚拟环境,安装版本精确匹配的 SDK 与 `win_amd64` 运行时 wheel,切换到 checkout 外的目录,清除 `PYTHONPATH` 与 `DSH_RUNTIME_MODE`,再运行与其他目标相同的 `--scenario all --installed-wheel` 黑盒测试。可信拉取请求还会运行相同的双轮 `sdk-live` 真实提供方场景。Fork 与 Dependabot head 不会获得密钥。
|
||||
|
||||
公开 Python 客户端通过 `initialize_timeout_seconds` 为首次 profile 握手提供独立的 30 秒默认上限。该上限可容纳 Windows x64 可执行文件冷启动与 profile 物化,同时仍会使卡死的运行时失败;调用方可将其与普通请求超时分开配置。
|
||||
|
||||
成功收到 shutdown 响应后,Python 客户端会关闭 stdin,并在已配置的 shutdown 超时内等待 `dsh` 上下文退出及刷写持久 session 状态,然后才回退到终止进程。Shutdown 失败时仍立即执行有界终止。`shutdown_timeout_seconds` 会分别限制 shutdown 请求、EOF 宽限与终止确认阶段,因此异常关闭在最终 kill 前可能接近该值的三倍。该区别会保留 Windows 上最后一个已接受轮次;该平台的 `terminate()` 会强制结束进程,而不是发送可捕获信号。
|
||||
|
||||
极简黑盒测试在 Windows 上使用持久 `pwsh` 与 `str_replace_editor`,并由 `minimal/win-x64/model-visible.json` 固定预期;Linux 与 macOS 保留持久 Bash 和共享的 `minimal/model-visible.json`。高级进程/subagent 快照与重启/持久日志快照继续由所有目标共享。随附的 [`sdk-minimal` 组合包](../../../../packages/bundle/sdk-minimal/README.zh.md)为可运行 Python 教程选择同一组平台 shell。
|
||||
|
||||
## Existing decisions and supersession
|
||||
|
||||
本决策部分取代[单文件运行时分发](2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md)中的 Windows 非目标声明,并扩展[安装后 Python wheel 黑盒决策](../testing/2026-08-23-installed-python-wheel-black-box-ci.zh.md)中的必需目标集合。上述 Note 继续负责 SEA 打包、两个 Python distribution、来源校验、密钥处理与通用黑盒场景。
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**在 dsh profile 运行时之前增加 Windows。** 否决:针对已退役私有直启载体的测试无法证明 Windows 用户实际获得的形态。Windows 仅定义于唯一的 `dsh` 启动架构。
|
||||
|
||||
**同时发布 Windows arm64。** 否决:已接受的产品范围只有 x64;增加第二种架构需要独立的原生构建器、wheel 标签、ConPTY 与 ripgrep 载荷校验、installed-wheel 矩阵 lane 及发布产物。
|
||||
|
||||
**为 Windows 提供较小的冒烟测试套件。** 否决:一个平台 wheel 不能借用其他可执行文件的协议、持久化、worker、MCP、插件、原生工具或真实提供方证据。只有持久 shell surface 使用平台专属预期,其余快照继续共享。
|
||||
|
||||
**通过 Git Bash 运行 Windows lane。** 否决:仓库要求 Windows runner 使用原生 `pwsh`,而 MSYS 路径转换无法证明原生命令行为。可移植的单行步骤使用各 runner 的默认 shell;路径、虚拟环境与黑盒步骤分别提供显式 POSIX 和 PowerShell 形式。
|
||||
|
||||
## Consequences
|
||||
|
||||
Python 安装现在会选择无需 Node 的 Windows x64 运行时,并与 Linux、macOS 使用同一套显式 home 与 profile 自定义模型。每个拉取请求都要承担第四个可执行文件、运行时 wheel 与完整 keyless 黑盒测试;可信 head 还要承担真实提供方任务。候选发行版验证会保留五个而不是四个 wheel。Windows arm64 用户会收到明确的不支持平台错误,直到另一项原生产品决策提供并证明该载体。
|
||||
+2
-2
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-24-standalone-sdk-minimal-profile.md
|
||||
2026-08-24-standalone-sdk-minimal-profile.md: bc1a177dc4a232201004e6869caa452a7d55deb9
|
||||
2026-08-24-standalone-sdk-minimal-profile.zh.md: ae6fdb95079f748f26758a30c968c27548e9a87f
|
||||
2026-08-24-standalone-sdk-minimal-profile.md: 692bf9763f4ad710ff5cc819a7480b2f3e8d9b5f
|
||||
2026-08-24-standalone-sdk-minimal-profile.zh.md: f39baad1b4429b73f13d601716dadd8376c71bfb
|
||||
|
||||
@@ -22,9 +22,9 @@ The bundle reuses `@deepseek-ai/dsh-sdk-app` for command help, stdin EOF, and bo
|
||||
|
||||
### Explicit composition
|
||||
|
||||
The bundle owns one DeepSeek adapter, SDK JSON-RPC serving, the executor-less agent spine, local subprocess and unrestricted filesystem providers, persistent Bash, the string-replace editor, and uncompressed JSONL sessions under `$DSH_HOME/sessions`. The SDK initialization request owns the model id; `DSH_CONTEXT_WINDOW` supplies fallback capacity for models outside the adapter's advisory catalog. The persona comes from `DSH_SYSTEM_PROMPT`, and the credential from `DEEPSEEK_API_KEY`.
|
||||
The bundle owns one DeepSeek adapter, SDK JSON-RPC serving, the executor-less agent spine, local subprocess and unrestricted filesystem providers, a platform-selected persistent shell, the string-replace editor, and uncompressed JSONL sessions under `$DSH_HOME/sessions`. Linux and macOS mount Bash; Windows mounts PowerShell. The SDK initialization request owns the model id; `DSH_CONTEXT_WINDOW` supplies fallback capacity for models outside the adapter's advisory catalog. The persona comes from `DSH_SYSTEM_PROMPT`, and the credential from `DEEPSEEK_API_KEY`.
|
||||
|
||||
Harness identity, runtime context, workspace instructions, skills, model-facing job controls, compaction, settings, managed credentials, telemetry, Web tools, subagents, and every other base row are absent rather than hidden. The profile pins `danger-full-access`, `maxTokensAsSuccess: false`, and startup-only patch loading. This layer is POSIX-only because its persistent terminal uses Bash.
|
||||
Harness identity, runtime context, workspace instructions, skills, model-facing job controls, compaction, settings, managed credentials, telemetry, Web tools, subagents, and every other base row are absent rather than hidden. The profile pins `danger-full-access`, `maxTokensAsSuccess: false`, and startup-only patch loading.
|
||||
|
||||
### Customization and Web
|
||||
|
||||
|
||||
+2
-2
@@ -22,9 +22,9 @@ Status: implemented
|
||||
|
||||
### 显式组合
|
||||
|
||||
该组合包拥有一个 DeepSeek 适配器、SDK JSON-RPC 服务、无执行器的 agent 主干、本地子进程与不受限文件系统提供方、持久 Bash、字符串替换 editor,以及位于 `$DSH_HOME/sessions` 的未压缩 JSONL 会话。SDK 初始化请求拥有模型 id;`DSH_CONTEXT_WINDOW` 为不在适配器建议目录中的模型提供后备容量。Persona 来自 `DSH_SYSTEM_PROMPT`,凭据来自 `DEEPSEEK_API_KEY`。
|
||||
该组合包拥有一个 DeepSeek 适配器、SDK JSON-RPC 服务、无执行器的 agent 主干、本地子进程与不受限文件系统提供方、按平台选择的持久 shell、字符串替换 editor,以及位于 `$DSH_HOME/sessions` 的未压缩 JSONL 会话。Linux 与 macOS 挂载 Bash,Windows 挂载 PowerShell。SDK 初始化请求拥有模型 id;`DSH_CONTEXT_WINDOW` 为不在适配器建议目录中的模型提供后备容量。Persona 来自 `DSH_SYSTEM_PROMPT`,凭据来自 `DEEPSEEK_API_KEY`。
|
||||
|
||||
Harness 身份、运行时上下文、workspace 指令、skills、面向模型的 job 控制、compaction、settings、托管凭据、遥测、Web 工具、subagent 与其他所有 base 配置项均不存在,而不是被隐藏。该 profile 固定使用 `danger-full-access`、`maxTokensAsSuccess: false` 与仅启动时 patch 加载。由于持久终端使用 Bash,此层只支持 POSIX。
|
||||
Harness 身份、运行时上下文、workspace 指令、skills、面向模型的 job 控制、compaction、settings、托管凭据、遥测、Web 工具、subagent 与其他所有 base 配置项均不存在,而不是被隐藏。该 profile 固定使用 `danger-full-access`、`maxTokensAsSuccess: false` 与仅启动时 patch 加载。
|
||||
|
||||
### 自定义与 Web
|
||||
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-25-session-observations-and-projection-owned-client-state.md
|
||||
2026-08-25-session-observations-and-projection-owned-client-state.md: e47f2fc75ecbca51d01af077f6c6ab98f4e275f9
|
||||
2026-08-25-session-observations-and-projection-owned-client-state.zh.md: 527a4eb6b6765cba95d6067f2be60bff8f31a559
|
||||
+201
@@ -0,0 +1,201 @@
|
||||
# Agent Note: Session observations and projection-owned client state
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-08-25-session-observations-and-projection-owned-client-state.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
Session-facing consumers needed the same logical data but resolved it independently. List, follow, page, attachment and fork reads, and subagent inspection each chose between an attached Session, persisted metadata, a prepared Session, and projection cache entries. One page visit could therefore materialize the same cold log more than once, and independently assembled header, event, cursor, and projection values could describe different cuts.
|
||||
|
||||
Client features also kept Session-derived facts in several forms. Title had dedicated list and update handling; model selection mixed a Session-specific catalog request with local state; agent preset display could infer a global default before the current Session arrived; and subagent listing scanned or reconstructed identity separately. These mirrors introduced intermediate states in which the UI showed a guessed default, a raw id, or an unavailable state even though the durable Session already determined the answer.
|
||||
|
||||
Unifying only the persistence read would leave those Client mirrors as competing authorities. Unifying only the Client fields would leave each Host endpoint free to obtain a different source cut. The read unit and the derived-state unit therefore need one coordinated ownership rule.
|
||||
|
||||
## Decision
|
||||
|
||||
Exact Session reads use a retained `SessionObservation`, and replayable Session-derived values exposed to the Client use registered projections. Observation owns source selection and one immutable read cut; projection owns derivation from that cut. API layers select what to publish, while Client code consumes finished values and does not reconstruct Session facts from events or duplicate them in domain-specific mirrors.
|
||||
|
||||
### Data flow
|
||||
|
||||
The two ownership rules meet at the observation's projection snapshot. Lightweight listing may stop at cached hints; every exact opening reaches the same observation path and gives the Client a complete replacement baseline.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
List["list / search"] --> Corpus["SessionQuery corpus"]
|
||||
Follow["follow"] --> Observe["observeSession"]
|
||||
Page["page / attachment / fork"] --> Observe
|
||||
Subagent["subagent list / continuation"] --> Corpus
|
||||
Subagent --> Observe
|
||||
Corpus --> Cache["projection cache hints"]
|
||||
Cache --> ClientList["Client Session list"]
|
||||
Cache -->|"small miss"| Observe
|
||||
Observe --> Source{"live or cold"}
|
||||
Source --> Live["attached Session cut"]
|
||||
Source --> Borrow["borrowSession"]
|
||||
Borrow --> Prepared["SessionPreparations.borrow"]
|
||||
Live --> Mode{"all or none"}
|
||||
Prepared --> Mode
|
||||
Mode --> Snapshot["SessionObservation"]
|
||||
Snapshot --> Opening["follow opening snapshot"]
|
||||
Snapshot --> Read["page / inspection"]
|
||||
Opening --> Store["Client projection store"]
|
||||
Store --> Domain["title / model / preset / subagent"]
|
||||
```
|
||||
|
||||
### Observation is the point-read unit
|
||||
|
||||
`SessionQueryEngine.observeSession(sessionId, options)` returns a disposable `SessionObservation` containing one source kind, header, contiguous event prefix, cursor, optional projection snapshot, and the durable revision for a prepared source. An attached Session wins. Otherwise `SessionPersistence.borrowSession()` and `SessionPreparations.borrow()` share and pin one prepared Session, including an in-flight cold load.
|
||||
|
||||
Every owner disposes its observation. `retain()` creates another lease over the same cut, which lets `session.follow` publish a snapshot and then transfer that exact prepared source to background Agent promotion without rereading the log. A live Session that appears during cold resolution wins before publication; a disappeared live source is retried as cold.
|
||||
|
||||
### Source resolution and lifetime
|
||||
|
||||
An observation binds all returned fields to one lifecycle witness. Callers do not combine a header from corpus listing, events from persistence, and projections from a later live Session. The selected header and event prefix produce the cursor and projection snapshot together.
|
||||
|
||||
Live preference is checked both before and after a cold borrow. The second check closes the race in which an Agent attaches while persistence is loading. If persistence itself reports that a live source won but that source has already detached by the time SessionQuery examines it, resolution restarts instead of publishing an unowned reference.
|
||||
|
||||
Persistence absence maps to Session-not-found only after no attached Session exists. Durable corruption, source-identity conflict, cancellation, and operational persistence failure remain distinct `SessionQueryError` outcomes so API owners can preserve their own public error vocabulary without duplicating source detection.
|
||||
|
||||
The observation owns no mutation authority. Its event array is an immutable prefix, and its prepared Session remains unpublished. Promotion is an explicit ownership transfer performed by the Session Controller after it has emitted the opening snapshot; other readers cannot turn an observation into a live Agent.
|
||||
|
||||
Projection work is deliberately `all | none`. `all` computes every registered projection at the observation's event cursor; `none` leaves projection state untouched. There is no per-key preparation state, `projectionKeys` mode, or cached `viewedState`/`viewedValue` layer. A publisher may filter the completed values for an audience, but the underlying observation is never partly projected.
|
||||
|
||||
### Projection execution boundary
|
||||
|
||||
For a live source, `all` reads one synchronous registry snapshot. For a prepared source, the projection cache may seed valid state rows, after which every registered unit advances over the exact remaining event prefix. The resulting client values share one `asOfSeq`.
|
||||
|
||||
Filtering belongs after computation because it changes disclosure, not state. A page authorization check may consume only `subagent`, and a list row may publish only list-relevant values, while both still rely on a complete projected cut when they request projection work.
|
||||
|
||||
The registry owns fold state; each domain owns its `init`, `apply`, `view`, schemas, and `stateVersion`. SessionQuery knows only whether projection work is required. It does not know title, model, preset, subagent, token, image, plan, todo, or goal semantics.
|
||||
|
||||
`view` remains an uncached synchronous conversion over folded state. Its cost is bounded by the registered projection units and is paid at snapshot publication; introducing a second cache would add invalidation states without reducing event replay.
|
||||
|
||||
Corpus listing remains a separate lightweight operation. `listSessions()` returns live-preferred headers without materializing every log. Session list and subagent list first use live projection state or durable projection-cache rows. Session list may take one complete observation for an individually stored artifact within its configured small-log limit when cached metadata cannot establish whether it is blank; a large or unreadable cache miss remains visible with unknown hints.
|
||||
|
||||
`session.follow` publishes a required opening snapshot containing header, cursor, the initial event window, and a complete projection baseline. Reconnect replaces the previous generation from another complete snapshot. `session.page` is reserved for older-history reads and gap repair. Observation-only reads never activate an Agent; only an ordinary follow may retain its prepared observation and request promotion after the opening snapshot has been delivered.
|
||||
|
||||
### Read audiences
|
||||
|
||||
Each public operation chooses one query and projection policy. The choice is part of that operation's behavior rather than a heuristic inside persistence or transport.
|
||||
|
||||
| Operation | Read path | Projection policy | Agent activation |
|
||||
|---|---|---|---|
|
||||
| `session.list` | Corpus headers, live state, and cached rows; bounded small-log fallback | Partial hints, or one full small-log observation | Never |
|
||||
| `session.search` | Corpus authorization plus the configured search provider | None for result listing | Never |
|
||||
| `session.follow` | One exact observation | All, carried in the opening snapshot | Ordinary cold Session only, after snapshot delivery |
|
||||
| `session.page` | One exact observation | None, except projection-backed subagent authorization | Never |
|
||||
| Attachment and fork source | One exact observation | None unless authorization requires it | Never for the source |
|
||||
| Subagent list and continuation | Corpus plus live/cache/observation resolution | All on a cold fallback; audience consumes identity or inherited values | Never for listing; continuation follows its explicit command semantics |
|
||||
|
||||
### Replayable Client facts are projection-owned
|
||||
|
||||
A Client-visible fact belongs to `SessionProjectionMap` when its value is determined by the Session header or event log and must survive reload, cold access, or reconnect. The rule covers title, list metadata, model selection, agent preset selection, subagent identity, and subagent timing. Their domain packages own pure projection definitions; the Session transport and Client value store remain domain-neutral.
|
||||
|
||||
The three projection delivery states have different meanings:
|
||||
|
||||
- A Session-list hint is optional, partial, and possibly stale. A missing key means unknown, so a list consumer must not invent an empty value or deployment default.
|
||||
- A follow opening baseline is the complete set of client-visible projection capabilities registered at its cursor. A missing key there means the capability is absent for that Host composition.
|
||||
- An explicit `null` is a domain-computed no-value result. It is distinct from a missing list hint and survives JSON transport.
|
||||
|
||||
These distinctions prevent one overloaded `undefined` from representing cache miss, unloaded plugin, and a real domain answer. API types name list data as hints and opening data as a baseline so a consumer cannot assume equivalent completeness merely because both carry projection values.
|
||||
|
||||
### Client merge rules
|
||||
|
||||
| Input | Completeness | Freshness | Meaning of missing key |
|
||||
|---|---|---|---|
|
||||
| Session list hints | Partial | Last durable checkpoint or bounded fallback cut | Unknown |
|
||||
| Follow opening baseline | Complete for the Host composition | Exact opening cursor | Capability absent |
|
||||
| Projection frame | One whole key | Event sequence carried by the frame | Not applicable |
|
||||
|
||||
The Client stores one row per key with its sequence number. A newer hint, baseline, or frame replaces a row; an equal or older input is ignored. Reconnect can therefore replace the event window without rolling back a projection frame that was already accepted at a later sequence.
|
||||
|
||||
The list view reads the same per-Session store as the opened Session. Hints can populate title, preset, and other list presentation before follow completes; the opening baseline then converges that state without creating a second summary-only authority.
|
||||
|
||||
The per-Session Client projection store accepts list hints, the follow baseline, and later whole-value frames under one higher-sequence-wins rule. It never folds Session events. A baseline or frame may advance a hinted value, while an older cut cannot overwrite a newer row.
|
||||
|
||||
Data that is not derived from one Session remains outside projections. `llm.models` owns the Host-generation model catalog, and `agentPreset.list` owns the configurable preset roster. A selector combines the relevant catalog with the Session's `modelSelection` or `agentPreset` projection only when both inputs are ready. During refresh it may retain the last complete catalog; before the first complete pair it reports loading instead of rendering a guessed name or availability verdict.
|
||||
|
||||
Client-local interaction state also remains local: loading and error status, an open menu, an in-flight selection, and a staged choice for a not-yet-created Session are not replayable Session facts. Once a choice applies to a Session, its durable event and projection become authoritative.
|
||||
|
||||
### Domain applications
|
||||
|
||||
- **Title and list metadata.** Cached projection hints may render an existing title and determine blankness or recency. Missing hints leave those facts unknown; only the bounded small-log policy may resolve them during listing.
|
||||
- **Model selection.** `model/selection` records a complete provider, model, and optional reasoning effort. `modelSelection` distinguishes the last request's route from a later selection pending consumption by a request header.
|
||||
- **Agent preset.** The projection initializes from immutable Session metadata and advances on preset-selection events. A missing or `null` value is not replaced with the deployment default for an existing Session.
|
||||
- **Subagent identity.** The `subagent` unit remains the sole descriptor interpreter. Listing obtains candidates from the shared corpus and resolves values through live state, projection cache, or an observation rather than scanning events itself.
|
||||
- **Subagent presentation.** Opening projection values establish timing and identity before the Client declares the child interactive or offline, so transport loading does not masquerade as a durable state.
|
||||
|
||||
These migrations remove special-case Client state without making projection own provider catalogs or interaction mechanics. A domain still owns mutations and commands; projection owns only their replayable Session result.
|
||||
|
||||
### Failure and readiness boundaries
|
||||
|
||||
- A list cache miss is not an error and does not hide the row. Unknown hints remain absent until a bounded fallback or exact opening supplies them.
|
||||
- A projection failure during an exact cold observation makes that observation fail as corrupt Session data; callers do not publish a mixture of successful keys and failed keys.
|
||||
- A subagent candidate's failed cold observation is isolated to that candidate's diagnostic row; sibling candidates remain usable.
|
||||
- A catalog load failure is Client-visible catalog state. It does not erase a previously complete catalog during refresh and does not synthesize a Session selection.
|
||||
- A follow carrier generation is not accepted until its opening snapshot is validated and applied. The previous generation remains visible during reconnect.
|
||||
|
||||
Cancellation stops queued or in-flight cold resolution at documented checkpoints and releases every acquired lease. Cancellation does not convert into not-found, nor may it leave a prepared entry pinned.
|
||||
|
||||
### Ownership matrix
|
||||
|
||||
| Concern | Owner | Non-owner |
|
||||
|---|---|---|
|
||||
| Cold materialization and revision checks | Session persistence | API Controller and Client |
|
||||
| Exact live-preferred read cut | SessionQuery observation | Individual endpoint helpers |
|
||||
| Fold state and client-value computation | Projection registry and domain unit | SessionQuery and Client |
|
||||
| Partial list acceleration | Projection cache and list policy | Follow protocol |
|
||||
| Opening and reconnect replacement | Session follow and journal stream | Session page |
|
||||
| Per-key value ordering | Client projection store | Domain UI components |
|
||||
| Provider or preset catalog lifecycle | Its catalog directory | Session projection |
|
||||
| Rendering and transient interaction state | Domain UI package | Host projection units |
|
||||
|
||||
### Extension rules
|
||||
|
||||
1. Determine whether a new value is a replayable fact of one Session. If it is, define or reuse its durable header/event input before adding a Client field.
|
||||
2. Register one pure projection unit in the owning domain. Keep fold state and Client view types distinct when their representations differ.
|
||||
3. Let exact readers request `projectionMode: 'all'`; filter only when constructing an audience-specific response.
|
||||
4. Let list consumers accept an optional hint. Do not force full corpus hydration merely to avoid an explicit unknown state.
|
||||
5. Feed the generic Client projection store. Do not add a dedicated reconnect fetch, event reducer, or Session summary mirror for the same fact.
|
||||
6. Keep non-Session catalogs and ephemeral UI state in their own owners, and define readiness before combining them with a projection value.
|
||||
|
||||
These rules apply to new Session-derived Client state even when a direct event scan appears cheap. Complexity is measured across cold reads, reconnect, multiple tabs, plugin lifetime, and future consumers rather than at the first call site.
|
||||
|
||||
### Relationship to existing decisions
|
||||
|
||||
- [Reusable Session preparation](2026-08-05-session-preparation.md) owns cold materialization, repair, reservation, and publication. Observation adds a shared read lease over that prepared object; it does not move preparation into SessionQuery.
|
||||
- [Session history and Remote event transport](2026-08-18-session-history-and-event-transport.md) owns stream generations and replacement semantics. This decision supplies the exact snapshot that opens each journal generation.
|
||||
- [Projection state and Client views](2026-08-19-session-projection-state-and-client-views.md) owns the distinction between Host fold state and Client values. This decision governs where those values are consumed and how partial list hints differ from a complete baseline.
|
||||
- [Subagent identity projection](2026-08-06-subagent-list-identity-projection.md) continues to own descriptor folding, the serializable `null` sentinel, and the own-suffix sequence check. This decision supersedes only its independent corpus merge and direct cold-inspection path: listing now uses SessionQuery's corpus and observation.
|
||||
- The broader [session projection and command-log proposal](../../proposed/architecture/2026-07-27-session-projection-and-command-log.md) remains proposed for the portions not represented by shipped code. This decision records the shipped observation and Client-ownership subset.
|
||||
|
||||
## Verification
|
||||
|
||||
Persistence and SessionQuery tests pin shared cold loading, cancellation, live-source races, retained observations, disposal, and all-or-none projection calculation. Session Controller and Gateway tests pin snapshot-first opening, replacement reconnect, older-page reads, gap repair, list-cache hints, bounded small-log fallback, and promotion after snapshot delivery.
|
||||
|
||||
Client tests pin higher-sequence-wins projection storage, title updates, model catalog and selection readiness, preset roster refresh and Session-specific selection, and subagent loading without transient offline presentation. Subagent tests pin corpus enumeration, cache and observation fallback, lifecycle witnesses, bounded cold reads, and no Agent activation during listing.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Keep source resolution in each consumer.** Rejected because every caller would continue to implement its own live race, persistence error mapping, preparation lifetime, cancellation, and projection cut, allowing both duplicate work and inconsistent results.
|
||||
|
||||
**Activate an Agent for every exact read.** Rejected because list, history, attachment, search, and subagent inspection are read operations. Activation loads plugins and changes process state, and it has no natural retirement point for pagination or catalog reads.
|
||||
|
||||
**Prepare only requested projection keys.** Rejected because a partially projected Session creates another lifecycle state that every cache, restore, plugin-registration, and caller path must track. Projection units are pure and few; computing all registered units for an exact observation is simpler than maintaining `O(E*k)` partial state instead of `O(E*P)` complete state.
|
||||
|
||||
**Cache each projection's viewed value separately.** Rejected because `view` is a pure synchronous conversion over already folded state. A second `viewReady`/`viewedState`/`viewedValue` cache adds invalidation and plugin-lifetime states without avoiding event folding.
|
||||
|
||||
**Keep dedicated summary fields, RPCs, or Client reducers.** Rejected because each creates a second authority beside the event log and projection registry. It also requires separate baseline, reconnect, and race handling for every domain value.
|
||||
|
||||
**Require complete projections on every list row.** Rejected because listing a large cold corpus would force full-log work before navigation can render. Partial cache hints preserve a cheap list path; consumers already have an explicit unknown state until opening supplies the exact baseline.
|
||||
|
||||
**Render guessed defaults while catalog or projection input is missing.** Rejected because the guess can visibly disagree with the Session and then change after loading. Initial uncertainty renders as loading; refresh retains the last complete value until the replacement is ready.
|
||||
|
||||
## Consequences
|
||||
|
||||
Session consumers share one live-preferred read model and one prepared cold object. Header, events, cursor, and projections belong to the same observation, and ordinary page opening can reuse that object for later promotion. New point-read consumers use SessionQuery instead of composing persistence and registry calls themselves.
|
||||
|
||||
Session-derived Client state has one extension path: record or identify the durable input, register a pure projection unit, and consume its finished value through the generic store. Domain-specific catalogs may remain separate when they are not Session-derived, but they cannot substitute a default for an unknown Session projection.
|
||||
|
||||
The simpler state model accepts bounded extra computation. An exact projected cold observation evaluates every registered unit, and a small cache-missing list artifact may be read in full. Large list rows can remain partially described until opened, so every list consumer must preserve the distinction among unknown, absent capability, and explicit no value. Observation leases also make disposal part of the caller contract; retaining a prepared source without releasing it prevents normal cache retirement.
|
||||
+201
@@ -0,0 +1,201 @@
|
||||
# Agent Note: Session observation 与 projection 所有的客户端状态
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-08-25-session-observations-and-projection-owned-client-state.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
面向 Session 的多个消费方需要相同的逻辑数据,却各自完成解析。list、follow、page、附件/fork 读取和 subagent 检查分别在已挂载 Session、持久化元数据、prepared Session 与 projection cache 之间作选择。因此一次页面访问可能多次物化同一份冷日志,各自拼装的 header、事件、cursor 和 projection 值也可能来自不同的读取切面。
|
||||
|
||||
客户端功能还以多种形式保存 Session 派生事实。title 有专门的 list 和更新逻辑;模型选择把 Session 专用 catalog 请求与本地状态混在一起;agent preset 展示可能在当前 Session 到达前猜测全局默认值;subagent list 则单独扫描或重建 identity。这些镜像产生了中间状态:即使持久化 Session 已经决定答案,UI 仍会短暂显示猜测出的默认值、原始 id 或不可用状态。
|
||||
|
||||
仅统一持久化读取,Client 镜像仍会成为互相竞争的真源。仅统一 Client 字段,各 Host 入口仍可能获得不同的数据切面。因此读取单元与派生状态单元需要一条配套的 ownership 规则。
|
||||
|
||||
## 决策
|
||||
|
||||
Session 精确读取使用可保留的 `SessionObservation`,向 Client 暴露的可回放 Session 派生值使用已注册 projection。Observation 负责选择数据源和提供一份不可变读取切面;projection 负责从该切面派生状态。API 层只选择要发布的内容,Client 只消费成品值,不从事件重建 Session 事实,也不在各领域镜像中重复保存这些事实。
|
||||
|
||||
### 数据动线
|
||||
|
||||
两条 ownership 规则在 observation 的 projection snapshot 处汇合。轻量 list 可以止于 cache hints;每次精确 opening 都进入同一 observation 路径,并向 Client 提供完整 replacement baseline。
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
List["list / search"] --> Corpus["SessionQuery corpus"]
|
||||
Follow["follow"] --> Observe["observeSession"]
|
||||
Page["page / attachment / fork"] --> Observe
|
||||
Subagent["subagent list / continuation"] --> Corpus
|
||||
Subagent --> Observe
|
||||
Corpus --> Cache["projection cache hints"]
|
||||
Cache --> ClientList["Client Session list"]
|
||||
Cache -->|"small miss"| Observe
|
||||
Observe --> Source{"live or cold"}
|
||||
Source --> Live["attached Session cut"]
|
||||
Source --> Borrow["borrowSession"]
|
||||
Borrow --> Prepared["SessionPreparations.borrow"]
|
||||
Live --> Mode{"all or none"}
|
||||
Prepared --> Mode
|
||||
Mode --> Snapshot["SessionObservation"]
|
||||
Snapshot --> Opening["follow opening snapshot"]
|
||||
Snapshot --> Read["page / inspection"]
|
||||
Opening --> Store["Client projection store"]
|
||||
Store --> Domain["title / model / preset / subagent"]
|
||||
```
|
||||
|
||||
### Observation 是 point read 单元
|
||||
|
||||
`SessionQueryEngine.observeSession(sessionId, options)` 返回可 dispose(资源释放)的 `SessionObservation`,其中包含同一份 source kind、header、连续事件前缀、cursor、可选 projection snapshot,以及 prepared source 的持久化 revision。已挂载 Session 优先;否则 `SessionPersistence.borrowSession()` 与 `SessionPreparations.borrow()` 共享并固定一份 prepared Session,包括尚未完成的冷加载。
|
||||
|
||||
每个 owner 都会 dispose 自己的 observation。`retain()` 为同一切面创建另一份 lease,使 `session.follow` 能够先发布 snapshot,再把完全相同的 prepared source 转交给后台 Agent promotion,而无需重读日志。冷解析期间出现的 live Session 会在发布前胜出;已经消失的 live source 会按 cold source 重试。
|
||||
|
||||
### 数据源解析与生命周期
|
||||
|
||||
一份 observation 把所有返回字段绑定到同一 lifecycle witness。调用方不会把 corpus list 的 header、persistence 的 events 和稍后 live Session 的 projections 拼在一起。选中的 header 与事件前缀共同产生 cursor 和 projection snapshot。
|
||||
|
||||
系统在 cold borrow 前后都检查 live 优先级。第二次检查封住 persistence 加载期间 Agent 完成 attach 的竞态。如果 persistence 报告由 live source 胜出,但 SessionQuery 检查时该 source 已经 detach,解析会重新开始,而不是发布一份无人持有的引用。
|
||||
|
||||
只有在不存在已挂载 Session 后,persistence absence 才映射为 Session-not-found。持久数据损坏、source identity 冲突、取消和 persistence 操作失败分别保留不同的 `SessionQueryError`,API owner 因而可以维持自身公开错误词汇,而不用重复数据源判定。
|
||||
|
||||
Observation 不拥有任何 mutation 权限。其事件数组是不可变前缀,prepared Session 保持未发布。Promotion 是 Session Controller 在 opening snapshot 发出后执行的显式 ownership transfer;其他读方不能把 observation 变成 live Agent。
|
||||
|
||||
Projection 工作明确只有 `all | none` 两种模式。`all` 在 observation 的事件 cursor 上计算所有已注册 projection;`none` 完全不触碰 projection 状态。系统不存在按 key preparation 的状态、`projectionKeys` 模式或额外的 `viewedState`/`viewedValue` cache。发布方可以按 audience 筛选已完成的值,但底层 observation 不会处于只算完部分 projection 的状态。
|
||||
|
||||
### Projection 执行边界
|
||||
|
||||
对于 live source,`all` 读取一份同步 registry snapshot。对于 prepared source,projection cache 可以播种有效 state row,随后每个已注册 unit 在精确的剩余事件前缀上推进。得到的 Client value 共用一个 `asOfSeq`。
|
||||
|
||||
筛选发生在计算完成之后,因为它改变的是披露内容,而不是状态。Page 鉴权可以只消费 `subagent`,list row 可以只发布 list 相关值;只要它们请求 projection 工作,仍然依赖一份完整 projected cut。
|
||||
|
||||
Registry 拥有 fold state;各领域拥有自己的 `init`、`apply`、`view`、schema 和 `stateVersion`。SessionQuery 只知道是否需要 projection 工作,不理解 title、model、preset、subagent、token、image、plan、todo 或 goal 的语义。
|
||||
|
||||
`view` 保持为 folded state 上无 cache 的同步转换。其成本由已注册 projection unit 数量界定,并在 snapshot 发布时支付;引入第二层 cache 只会增加 invalidation 状态,无法减少 event replay。
|
||||
|
||||
语料库 list 仍是独立的轻量操作。`listSessions()` 返回 live-preferred header,而不物化每份日志。Session list 与 subagent list 先读取 live projection 状态或持久 projection-cache row。当 cache 无法判断 Session 是否为空,且该 Session 拥有的独立产物未超过配置的小日志限制时,Session list 可以执行一次完整 observation;大型或不可读的 cache miss 仍以 hints 未知但 row 可见的方式返回。
|
||||
|
||||
`session.follow` 发布必需的 opening snapshot,其中包含 header、cursor、首个事件窗口和完整 projection baseline。重连使用另一份完整 snapshot 替换上一 generation。`session.page` 仅用于旧历史读取与 gap repair。只读 observation 不激活 Agent;只有普通 follow 可以保留 prepared observation,并在 opening snapshot 已交付后请求 promotion。
|
||||
|
||||
### 读取 audience
|
||||
|
||||
每个公开操作选择一组查询与 projection 策略。该选择属于操作本身的行为,而不是 persistence 或 transport 内部的启发式判断。
|
||||
|
||||
| 操作 | 读取路径 | Projection 策略 | Agent 激活 |
|
||||
|---|---|---|---|
|
||||
| `session.list` | Corpus header、live state 和 cached row;有界小日志 fallback | 部分 hints,或一次完整小日志 observation | 从不 |
|
||||
| `session.search` | Corpus 鉴权加已配置 search provider | 结果列表不计算 | 从不 |
|
||||
| `session.follow` | 一份精确 observation | 全算,并由 opening snapshot 携带 | 仅普通 cold Session,且在 snapshot 交付后 |
|
||||
| `session.page` | 一份精确 observation | 不计算,但 projection-backed subagent 鉴权除外 | 从不 |
|
||||
| Attachment 与 fork source | 一份精确 observation | 鉴权不要求时不计算 | source 从不激活 |
|
||||
| Subagent list 与 continuation | Corpus 加 live/cache/observation 解析 | cold fallback 全算;audience 只消费 identity 或继承值 | Listing 从不;continuation 遵循显式命令语义 |
|
||||
|
||||
### 可回放的 Client 事实归 projection 所有
|
||||
|
||||
当一个 Client 可见值由 Session header 或事件日志决定,并且必须在刷新、冷访问或重连后恢复时,它属于 `SessionProjectionMap`。这条规则覆盖 title、list metadata、model selection、agent preset selection、subagent identity 和 subagent timing。各领域包拥有纯 projection definition;Session transport 与 Client value store 不理解具体领域。
|
||||
|
||||
Projection 的三种交付状态含义不同:
|
||||
|
||||
- Session-list hint 是可选、部分且可能陈旧的数据。key 缺失表示未知,因此 list 消费方不得自行补成空值或部署默认值。
|
||||
- Follow opening baseline 是其 cursor 上所有已注册 Client 可见 projection capability 的完整集合。此处缺少 key 表示当前 Host composition 不具备该 capability。
|
||||
- 显式 `null` 是领域计算出的无值结果。它不同于 list hint 缺失,并且能够完整通过 JSON transport。
|
||||
|
||||
这些区别避免由一个重载的 `undefined` 同时表示 cache miss、plugin 未加载和真实领域答案。API 类型把 list 数据命名为 hints,把 opening 数据命名为 baseline,因此消费方不能只因两者都携带 projection value 就假定其完整性相同。
|
||||
|
||||
### Client 合并规则
|
||||
|
||||
| 输入 | 完整性 | 新鲜度 | key 缺失的含义 |
|
||||
|---|---|---|---|
|
||||
| Session list hints | 部分 | 上次持久 checkpoint 或有界 fallback cut | 未知 |
|
||||
| Follow opening baseline | 对当前 Host composition 完整 | 精确 opening cursor | Capability 不存在 |
|
||||
| Projection frame | 单个完整 key | Frame 携带的 event sequence | 不适用 |
|
||||
|
||||
Client 为每个 key 保存带 sequence number 的一行。更新的 hint、baseline 或 frame 会替换 row;相同或更旧的输入被忽略。因此 reconnect 可以替换 event window,而不会回退已经在更晚 sequence 接受的 projection frame。
|
||||
|
||||
List view 与已打开 Session 读取同一个 per-Session store。Hints 可以在 follow 完成前填充 title、preset 和其他 list presentation;opening baseline 随后收敛这份状态,而不会建立第二套 summary-only authority。
|
||||
|
||||
每个 Session 的 Client projection store 按一条 higher-sequence-wins 规则接收 list hints、follow baseline 和后续 whole-value frame。它从不折叠 Session event。Baseline 或 frame 可以推进 hinted value,较旧切面不能覆盖较新的 row。
|
||||
|
||||
不由单个 Session 派生的数据不进入 projection。`llm.models` 拥有当前 Host generation 的 model catalog,`agentPreset.list` 拥有可配置 preset roster。Selector 只在相应 catalog 与 Session 的 `modelSelection` 或 `agentPreset` projection 均就绪后组合两者。刷新时可以保留上一份完整 catalog;第一次获得完整输入前显示 loading,而不是展示猜测的名称或可用性结论。
|
||||
|
||||
Client 本地交互状态也继续留在本地:loading 和 error 状态、打开的菜单、进行中的选择,以及为尚未创建 Session 暂存的选择都不是可回放 Session 事实。选择一旦应用到 Session,其持久事件与 projection 就成为权威。
|
||||
|
||||
### 领域应用
|
||||
|
||||
- **Title 与 list metadata。** Cached projection hints 可以渲染已有 title,并判断 blankness 或 recency。Hints 缺失时这些事实保持未知;listing 期间只有有界小日志策略可以解析它们。
|
||||
- **Model selection。** `model/selection` 记录完整 provider、model 和可选 reasoning effort。`modelSelection` 区分上一请求使用的 route,以及等待 request header 消费的较晚 selection。
|
||||
- **Agent preset。** Projection 从不可变 Session metadata 初始化,并随 preset-selection event 推进。对于现有 Session,缺失或 `null` 值不会替换成部署默认值。
|
||||
- **Subagent identity。** `subagent` unit 仍是唯一 descriptor interpreter。Listing 从共享 corpus 获得 candidate,并通过 live state、projection cache 或 observation 解析值,不自行扫描 event。
|
||||
- **Subagent presentation。** Opening projection value 在 Client 宣布 child 可交互或离线前建立 timing 与 identity,因此 transport loading 不会伪装成 durable state。
|
||||
|
||||
这些迁移删除特殊 Client state,但不会让 projection 接管 provider catalog 或交互机制。领域仍拥有 mutation 和 command;projection 只拥有其可回放 Session 结果。
|
||||
|
||||
### 失败与 readiness 边界
|
||||
|
||||
- List cache miss 不是错误,也不会隐藏 row。未知 hints 保持缺失,直到有界 fallback 或精确 opening 提供值。
|
||||
- 精确 cold observation 中的 projection failure 使整份 observation 按损坏的 Session data 失败;调用方不会发布成功 key 与失败 key 的混合结果。
|
||||
- 一个 subagent candidate 的 cold observation 失败只影响该 candidate 的 diagnostic row;sibling candidate 继续可用。
|
||||
- Catalog load failure 是 Client 可见的 catalog state。它不会在 refresh 期间清除上一份完整 catalog,也不会合成 Session selection。
|
||||
- Follow carrier generation 只有在 opening snapshot 完成校验与应用后才被接受。Reconnect 期间继续显示上一 generation。
|
||||
|
||||
取消会在文档规定的检查点终止排队中或进行中的 cold resolution,并释放每一份已获得 lease。取消不会变成 not-found,也不能让 prepared entry 保持 pinned。
|
||||
|
||||
### Ownership 矩阵
|
||||
|
||||
| 事项 | Owner | 非 owner |
|
||||
|---|---|---|
|
||||
| Cold materialization 与 revision 检查 | Session persistence | API Controller 与 Client |
|
||||
| 精确 live-preferred read cut | SessionQuery observation | 各 endpoint helper |
|
||||
| Fold state 与 Client-value 计算 | Projection registry 与 domain unit | SessionQuery 与 Client |
|
||||
| 部分 list acceleration | Projection cache 与 list policy | Follow protocol |
|
||||
| Opening 与 reconnect replacement | Session follow 与 journal stream | Session page |
|
||||
| Per-key value ordering | Client projection store | Domain UI component |
|
||||
| Provider 或 preset catalog lifecycle | 对应 catalog directory | Session projection |
|
||||
| Rendering 与瞬时 interaction state | Domain UI package | Host projection unit |
|
||||
|
||||
### 扩展规则
|
||||
|
||||
1. 判断新值是否属于单个 Session 的可回放事实;如果属于,先定义或复用其持久 header/event 输入,再添加 Client 字段。
|
||||
2. 在 owning domain 注册一个 pure projection unit。Fold state 与 Client view 表示不同时,分别定义其类型。
|
||||
3. 让精确读方请求 `projectionMode: 'all'`;仅在构造 audience-specific response 时筛选。
|
||||
4. 让 list 消费方接受 optional hint。不能只为消除显式 unknown state 而强制 hydrate 整个 corpus。
|
||||
5. 把值送入通用 Client projection store。不能为同一事实再增加 dedicated reconnect fetch、event reducer 或 Session summary mirror。
|
||||
6. 非 Session catalog 与 ephemeral UI state 保留各自 owner,并在和 projection value 组合前定义 readiness。
|
||||
|
||||
这些规则适用于新的 Session-derived Client state,即使在第一个调用点直接扫描 event 看似廉价。复杂度需要覆盖 cold read、reconnect、多 tab、plugin lifetime 和未来消费方,而不是只看首次实现。
|
||||
|
||||
### 与既有决策的关系
|
||||
|
||||
- [可复用 Session preparation](2026-08-05-session-preparation.zh.md)拥有冷物化、修复、reservation 和发布。Observation 在该 prepared object 之上增加共享读取 lease,并未把 preparation 移入 SessionQuery。
|
||||
- [Session 历史与 Remote event transport](2026-08-18-session-history-and-event-transport.zh.md)拥有 stream generation 与 replacement 语义。本决策提供每个日志 generation 的精确 opening snapshot。
|
||||
- [Projection state 与 Client view](2026-08-19-session-projection-state-and-client-views.zh.md)拥有 Host fold state 和 Client value 的区分。本决策规定这些值在哪里消费,以及部分 list hints 与完整 baseline 的差别。
|
||||
- [Subagent identity projection](2026-08-06-subagent-list-identity-projection.zh.md)继续拥有 descriptor folding、可序列化 `null` sentinel 和 own-suffix sequence 检查。本决策只取代其中独立 corpus merge 和直接 cold inspection 路径:listing 改为使用 SessionQuery corpus 和 observation。
|
||||
- 更广泛的 [session projection 与 command-log 提案](../../proposed/architecture/2026-07-27-session-projection-and-command-log.zh.md)仍为 proposed,其中尚未由已交付代码体现的部分不受影响。本决策记录已经交付的 observation 与 Client ownership 子集。
|
||||
|
||||
## 验证
|
||||
|
||||
Persistence 与 SessionQuery 测试固定共享冷加载、取消、live-source race、retained observation、dispose 和 all-or-none projection 计算。Session Controller 与 Gateway 测试固定 snapshot-first opening、replacement reconnect、旧分页读取、gap repair、list-cache hints、小日志有界 fallback,以及 snapshot 交付后的 promotion。
|
||||
|
||||
Client 测试固定 higher-sequence-wins projection store、title 更新、model catalog 与 selection readiness、preset roster refresh 与 Session 专属选择,以及不会短暂展示离线状态的 subagent loading。Subagent 测试固定 corpus 枚举、cache 与 observation fallback、lifecycle witness、有界冷读,以及 listing 期间不激活 Agent。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
**由各消费方继续解析数据源。** 否决,因为每个调用方都需要重复实现 live race、persistence error mapping、preparation lifetime、cancellation 和 projection cut,既会重复工作,也会产生不一致结果。
|
||||
|
||||
**每次精确读取都激活 Agent。** 否决,因为 list、history、attachment、search 与 subagent inspection 都是读取操作。Activation 会加载插件并改变进程状态,也没有适合分页或 catalog 读取的自然退出点。
|
||||
|
||||
**只 prepare 被请求的 projection key。** 否决,因为只完成部分 projection 的 Session 会增加一种生命周期状态,所有 cache、restore、plugin registration 和调用路径都必须追踪它。Projection unit 数量少且为纯函数;精确 observation 计算全部已注册 unit,比为了 `O(E*k)` 而维护部分状态、取代 `O(E*P)` 完整状态更简单。
|
||||
|
||||
**单独缓存每个 projection 的 viewed value。** 否决,因为 `view` 只是已折叠 state 上的纯同步转换。第二层 `viewReady`/`viewedState`/`viewedValue` cache 会增加 invalidation 和 plugin lifetime 状态,却不能减少 event folding。
|
||||
|
||||
**保留专用 summary 字段、RPC 或 Client reducer。** 否决,因为每一项都会在 event log 与 projection registry 之外建立第二个真源,还要求每个领域分别实现 baseline、reconnect 和 race handling。
|
||||
|
||||
**要求每个 list row 都携带完整 projection。** 否决,因为列出大型 cold corpus 时必须先读取完整日志,navigation 才能渲染。部分 cache hints 保留了轻量 list 路径;在 opening 给出精确 baseline 前,消费方已经拥有明确的 unknown 状态。
|
||||
|
||||
**在 catalog 或 projection 输入缺失时渲染猜测默认值。** 否决,因为猜测可能明显违背 Session,并在加载后发生跳变。初次不确定时显示 loading;刷新时保留上一份完整值,直到替代值就绪。
|
||||
|
||||
## 后果
|
||||
|
||||
Session 消费方共享一份 live-preferred read model 和一个 prepared cold object。Header、events、cursor 与 projections 属于同一 observation,普通页面打开还可以为后续 promotion 复用该对象。新的 point-read 消费方使用 SessionQuery,而不再自行拼接 persistence 与 registry 调用。
|
||||
|
||||
Session 派生 Client 状态只有一条扩展路径:记录或识别持久输入、注册纯 projection unit,再通过通用 store 消费其成品值。不由 Session 派生的领域 catalog 可以独立存在,但不能用默认值替代未知的 Session projection。
|
||||
|
||||
更简单的状态模型接受有界的额外计算。精确的 projected cold observation 会计算所有已注册 unit,小型且 cache miss 的 list artifact 可能完整读取。大型 list row 在打开前可以保持部分描述,因此每个 list 消费方必须保留 unknown、capability absent 和 explicit no value 的区别。Observation lease 还使 dispose 成为调用方约定的一部分;保留 prepared source 而不释放会阻止正常 cache retirement。
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-07-20-config-hot-reload-resilience.md
|
||||
2026-07-20-config-hot-reload-resilience.md: 2b98ee73e1c7734da9c0ae6df5e3de1af6a5cbeb
|
||||
2026-07-20-config-hot-reload-resilience.zh.md: 0a3652a7cac589b601d7fd30d0c713d4585be830
|
||||
2026-07-20-config-hot-reload-resilience.md: 6f8503c93bcbf55b1bd0543cf49e41cfe32fbe2f
|
||||
2026-07-20-config-hot-reload-resilience.zh.md: 359a34a2252b2999f896245a4bd07f7da32d98f5
|
||||
|
||||
@@ -38,4 +38,4 @@ HMR contains live refresh rejection. Its `registerConfig(filename, refresh)` met
|
||||
|
||||
## Testing
|
||||
|
||||
`packages/boot/app-boot/tests/config-reload.spec.ts` boots real temporary Loader/Include trees and covers parse and shape rejection, import-before-dispose, plugin/config restoration, multi-entry rollback, ancestor disablement, overlay convergence, option identity, failed direct-update persistence, and failed programmatic moves. `packages/boot/app-boot/tests/hmr-config.spec.ts` covers existing and missing exact paths, add/change/removal, serialized coalescing, disposal drainage, non-`Error` normalization, failure broadcast, and rejecting-observer containment. `packages/host/webserver/tests/webserver.spec.ts` proves a service-gated startup failure rejects Loader composition with its bind diagnostic, `packages/typert/loader/tests/loader.spec.ts` exercises awaited programmatic removal through a real Loader consumer, and the ACP `pty-tools` snapshot guards concurrent composition from reordering equal-priority prompt sections.
|
||||
`packages/boot/app-boot/tests/config-reload.spec.ts` boots real temporary Loader/Include trees and covers parse and shape rejection, import-before-dispose, plugin/config restoration, multi-entry rollback, ancestor disablement, overlay convergence, option identity, failed direct-update persistence, and failed programmatic moves. `packages/boot/app-boot/tests/hmr-config.spec.ts` covers existing and missing exact paths, add/change/removal, serialized coalescing, disposal drainage, non-`Error` normalization, failure broadcast, and rejecting-observer containment. `packages/host/webserver/tests/webserver.spec.ts` proves a service-gated startup failure rejects Loader composition with its bind diagnostic, and `packages/typert/loader/tests/loader.spec.ts` exercises awaited programmatic removal through a real Loader consumer.
|
||||
|
||||
@@ -38,4 +38,4 @@ HMR 收容实时刷新 rejection。其 `registerConfig(filename, refresh)` 方
|
||||
|
||||
## Testing
|
||||
|
||||
`packages/boot/app-boot/tests/config-reload.spec.ts` 启动真实的临时 Loader/Include 树,并覆盖对解析和形状错误的拒绝、先导入再 dispose、插件/配置恢复、多配置项回滚、祖先禁用、overlay 收敛、option 对象身份、失败的直接更新不持久化以及失败的程序化移动。`packages/boot/app-boot/tests/hmr-config.spec.ts` 覆盖现有和缺失的确切路径、添加/变更/移除、串行化合并、dispose 排空、非 `Error` 值的规范化、失败广播以及对发生 rejection 的观察者的收容。`packages/host/webserver/tests/webserver.spec.ts` 证明受服务门控的启动失败会让 Loader 组合以其 bind 诊断 reject;`packages/typert/loader/tests/loader.spec.ts` 则通过真实 Loader 消费方演练可等待的程序化移除;ACP(Agent Client Protocol)的 `pty-tools` 快照会防止并发组合改变同优先级提示词段的顺序。
|
||||
`packages/boot/app-boot/tests/config-reload.spec.ts` 启动真实的临时 Loader/Include 树,并覆盖对解析和形状错误的拒绝、先导入再 dispose、插件/配置恢复、多配置项回滚、祖先禁用、overlay 收敛、option 对象身份、失败的直接更新不持久化以及失败的程序化移动。`packages/boot/app-boot/tests/hmr-config.spec.ts` 覆盖现有和缺失的确切路径、添加/变更/移除、串行化合并、dispose 排空、非 `Error` 值的规范化、失败广播以及对发生 rejection 的观察者的收容。`packages/host/webserver/tests/webserver.spec.ts` 证明受服务门控的启动失败会让 Loader 组合以其 bind 诊断 reject;`packages/typert/loader/tests/loader.spec.ts` 则通过真实 Loader 消费方演练可等待的程序化移除。
|
||||
|
||||
+2
-2
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-07-24-empty-model-response-is-retryable.md
|
||||
2026-07-24-empty-model-response-is-retryable.md: 021c368a25ce8dd277ba431fa905c0621b752fd0
|
||||
2026-07-24-empty-model-response-is-retryable.zh.md: 67809931f959086a4525cec4c879af190e653d4a
|
||||
2026-07-24-empty-model-response-is-retryable.md: 62e45385fbcf1cdebd1b2c19abcbfe6888abdfe7
|
||||
2026-07-24-empty-model-response-is-retryable.zh.md: 459a5def0bbd5a82c83af67ea1c035259ec4f0a5
|
||||
|
||||
@@ -33,4 +33,4 @@ The classification uses the existing loop machinery — `finishError` → `agent
|
||||
|
||||
- A transiently misbehaving provider consumes a bounded retry instead of a turn with no output; a persistently empty model produces an actionable `EMPTY_RESPONSE` turn failure.
|
||||
- A model that genuinely intends to say nothing (rare, but possible after a tool result) is retried and, if consistently empty, fails the turn. This trade was accepted deliberately: an empty assistant message is indistinguishable from the provider defect and has no value to the user.
|
||||
- The `empty-response-retry` ACP snapshot (an authored keyless scenario with a deterministic 1 ms zero-jitter retry overlay, `examples/acp-agent/retry.cordis.yml`) pins the product-visible behavior: a durable `llm/retry` event, no ACP output for the discarded attempt, the recovered reply, and a clean completed turn.
|
||||
- The [`empty-response-retry` headless snapshot](../../../../snapshots/session/empty-response-retry/) is an authored keyless scenario with a deterministic 1 ms zero-jitter retry overlay. It pins a durable `llm/retry` event, no output for the discarded attempt, the recovered reply, and a clean completed turn.
|
||||
|
||||
+1
-1
@@ -33,4 +33,4 @@ Status: implemented
|
||||
|
||||
- 一个偶发异常的提供方会消耗一次有界重试,而不是一个没有输出的轮次;一个持续返回空内容的模型则会产生用户可据以行动的 `EMPTY_RESPONSE` 轮次失败。
|
||||
- 一个确实打算什么都不说的模型(罕见,但在一次工具结果之后有可能出现)会被重试,若始终为空,则该轮次失败。这个取舍是经过审慎权衡后接受的:一条空的 assistant 消息与提供方缺陷无法区分,且对用户毫无价值。
|
||||
- `empty-response-retry` ACP(Agent Client Protocol)快照(一个人工编写的无密钥场景,配有确定性的 1 ms 零抖动重试 overlay,`examples/acp-agent/retry.cordis.yml`)钉住了产品可见的行为:持久的 `llm/retry` 事件、被丢弃的尝试不产生任何 ACP 输出、恢复后的回复,以及一次正常完成的轮次。
|
||||
- [`empty-response-retry` headless 快照](../../../../snapshots/session/empty-response-retry/)是配有确定性 1 ms 零抖动重试 overlay 的人工无密钥场景。它钉住持久的 `llm/retry` 事件、被丢弃的尝试不产生输出、恢复后的回复,以及一次正常完成的轮次。
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-08-06-api-key-format-validation.md
|
||||
2026-08-06-api-key-format-validation.md: e2dcb91cba703cf95994426c67c4203effad290a
|
||||
2026-08-06-api-key-format-validation.zh.md: e74cb58ee0e0be9ec9ec30123e7eeffa3f3db657
|
||||
2026-08-06-api-key-format-validation.md: 853ae9a4e2f21a6b2cfa92b5059181b2c2869623
|
||||
2026-08-06-api-key-format-validation.zh.md: 923798e5264a29fe95e6ee7616fdefeabea766b4
|
||||
|
||||
@@ -98,4 +98,4 @@ The costliest way to get this wrong would have been to treat absence as invalidi
|
||||
|
||||
`packages/client/ui-settings-models/tests/` pins `apiKeyFailure` over the same table plus the paste-shape cases, and drives both cards: a blank field submits without writing a credential, a whitespace-only field fails on the field, an illegal or wrapped key blocks submit and the interrogation alike, a padded key is trimmed before `credentials.set` and before an interrogation, and a hand-declared route can be created with no key at all.
|
||||
|
||||
The user-visible terminal state is pinned where it is actually assembled: `examples/headless-agent/tests/headless.snapshot.ts` runs the one-shot app against a stored key no header can carry, over the same keyless composition its missing-credential sibling uses, and records that the turn ends on `INVALID_CREDENTIAL` with an actionable message carrying neither the key nor the word `ByteString`. A package test could not have shown that, and the web e2e covers only the browser half.
|
||||
The user-visible terminal state is pinned where it is actually assembled: `apps/cli/tests/profiles/headless/tests/headless.expected.e2e.ts` runs the one-shot app against a stored key no header can carry, over the same keyless composition its missing-credential sibling uses, and records that the turn ends on `INVALID_CREDENTIAL` with an actionable message carrying neither the key nor the word `ByteString`. A package test could not have shown that, and the web e2e covers only the browser half.
|
||||
|
||||
@@ -98,4 +98,4 @@ Status: implemented
|
||||
|
||||
`packages/client/ui-settings-models/tests/` 以同一张表加上形状用例钉住 `apiKeyFailure`,并驱动两张卡片:留空的输入框可提交且不写入凭据、只含空白的输入框在字段上失败、非法或被包裹的 Key 同时拦截提交与探测、带首尾空白的 Key 在 `credentials.set` 与探测之前被 trim,以及手工声明的路由可以完全不带 Key 创建。
|
||||
|
||||
用户可见的终态则钉在它真正被组装的位置:`examples/headless-agent/tests/headless.snapshot.ts` 让 one-shot 应用在一个 HTTP 标头无法承载的已存密钥下运行,复用其 missing-credential 兄弟场景的同一套无密钥 composition,并记录该轮次以 `INVALID_CREDENTIAL` 结束、消息可操作且既不含密钥也不含 `ByteString` 字样。包级测试无法证明这一点,而 web e2e 只覆盖了浏览器那一半。
|
||||
用户可见的终态则钉在它真正被组装的位置:`apps/cli/tests/profiles/headless/tests/headless.expected.e2e.ts` 让 one-shot 应用在一个 HTTP 标头无法承载的已存密钥下运行,复用其 missing-credential 兄弟场景的同一套无密钥 composition,并记录该轮次以 `INVALID_CREDENTIAL` 结束、消息可操作且既不含密钥也不含 `ByteString` 字样。包级测试无法证明这一点,而 web e2e 只覆盖了浏览器那一半。
|
||||
|
||||
+2
-2
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-08-10-minimal-preset-owns-rl-composition.md
|
||||
2026-08-10-minimal-preset-owns-rl-composition.md: 2e9a3e56252f8e91008a5559ad738a7ca678446b
|
||||
2026-08-10-minimal-preset-owns-rl-composition.zh.md: 31df6ebfbc15f35208bc73b391725b2039fe7819
|
||||
2026-08-10-minimal-preset-owns-rl-composition.md: 4c296ed4af5df7a48bdfba6ff1972321cc2e54cf
|
||||
2026-08-10-minimal-preset-owns-rl-composition.zh.md: 545f0a32fe9c7726d6fc910d0598174e7d7a3ec1
|
||||
|
||||
@@ -36,4 +36,4 @@ The standalone [`sdk-minimal` bundle](../../../../packages/bundle/sdk-minimal/RE
|
||||
|
||||
## Consequences
|
||||
|
||||
The Web RL prompt is fixed rather than environment-overridable; the standalone JSON-RPC prompt is deployment-selected. The Web preset and `sdk-minimal` profile state the same two-tool behavior for their respective launch paths. The model sees only persistent `bash` and `str_replace_editor`; shell state is per agent and disappears with that agent. The Web preset pays for its own PTY and bare filesystem service instances, while other presets pay nothing for them. The local persistent-shell backend requires the supported POSIX terminal substrate, so this preset does not support Windows agents.
|
||||
The Web RL prompt is fixed rather than environment-overridable; the standalone JSON-RPC prompt is deployment-selected. The Web preset and `sdk-minimal` profile share persistent-shell-plus-editor behavior for their respective launch paths; `sdk-minimal` selects PowerShell on Windows. Shell state is per agent and disappears with that agent. The Web preset pays for its own PTY and bare filesystem service instances, while other presets pay nothing for them. The Web preset's Bash backend requires the supported POSIX terminal substrate, so that preset does not support Windows agents.
|
||||
|
||||
+1
-1
@@ -36,4 +36,4 @@ preset persona 恰好是 `You are a helpful software engineer assistant.`,它
|
||||
|
||||
## 后果
|
||||
|
||||
Web RL 提示词固定不变,不能通过环境覆盖;独立 JSON-RPC 提示词由部署选择。Web preset 与 `sdk-minimal` profile 分别为各自启动路径声明相同的双工具行为。模型只看到持久 `bash` 与 `str_replace_editor`;shell 状态按 agent 隔离,并随该 agent 一并消失。Web preset 为自身的 PTY 与裸文件系统服务实例承担开销,其他 preset 无需承担。持久 shell 的本地后端需要受支持的 POSIX 终端基础环境,因此该 preset 不支持 Windows agent。
|
||||
Web RL 提示词固定不变,不能通过环境覆盖;独立 JSON-RPC 提示词由部署选择。Web preset 与 `sdk-minimal` profile 在各自启动路径共享持久 shell 加 editor 的行为;`sdk-minimal` 在 Windows 上选择 PowerShell。Shell 状态按 agent 隔离,并随该 agent 一并消失。Web preset 为自身的 PTY 与裸文件系统服务实例承担开销,其他 preset 无需承担。Web preset 的 Bash 后端需要受支持的 POSIX 终端基础环境,因此该 preset 不支持 Windows agent。
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-08-24-system-prompt-section-order-ties.md
|
||||
2026-08-24-system-prompt-section-order-ties.md: 673c3b3cd668115ead9f9b5478c2bc432b78f930
|
||||
2026-08-24-system-prompt-section-order-ties.zh.md: 96a6843a0db48e52a2132ad9f8caf6243dfbbcd2
|
||||
@@ -0,0 +1,27 @@
|
||||
# Agent Note: Equal-order system-prompt sections render in activation order
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-08-24-system-prompt-section-order-ties.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
`SystemPromptRegistry` sorts sections by `order` with a stable sort, so equal orders render in plugin-activation order. `tool:cordis` and `tool:workflow` both declared `order: 115`, while their activation order varies between clean platform compositions. ACP and SDK snapshot replays could therefore assemble the same sections in a different order from their committed `system-prompt.expected.md` files.
|
||||
|
||||
## Decision
|
||||
|
||||
Give the affected sequence distinct values without changing its established relative order: `tool:cordis` stays at 115, `tool:workflow` uses 115.5, `tool:ralph` stays at 116, continuable subagent guidance stays at 116.5, and child-report guidance stays at 117. Prompt text and tool schemas remain unchanged.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Normalize section order in the snapshot harness.** Rejected because the runtime, request header, and model prompt would remain sensitive to activation timing while only the fixture comparison hid the difference.
|
||||
|
||||
**Tie-break equal orders by section name in the registry.** Rejected because it would silently reorder every existing tie. Explicit orders keep each model-visible placement local to the contributing plugin.
|
||||
|
||||
## Consequences
|
||||
|
||||
The Cordis and workflow guidance has a platform-independent order while Ralph remains before continuable subagent and child-report guidance. Prompt-section placements that require a stable relative position need distinct `order` values; other equal-order sections retain activation-order semantics and are outside this decision.
|
||||
|
||||
## Testing
|
||||
|
||||
The keyless ACP and SDK snapshot replays pin Cordis before workflow and preserve the workflow, Ralph, continuable-subagent, and child-report sequence. The full snapshot suite verifies the refreshed fixtures.
|
||||
@@ -0,0 +1,27 @@
|
||||
# Agent Note: 等序系统提示词分段按激活顺序渲染
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-08-24-system-prompt-section-order-ties.md) | 中文
|
||||
|
||||
## Problem
|
||||
|
||||
`SystemPromptRegistry` 使用稳定排序按 `order` 排列分段,因此相同 order 的分段会按插件激活顺序渲染。`tool:cordis` 与 `tool:workflow` 都声明了 `order: 115`,但两者在不同平台的全新组合中激活顺序不同。因此,ACP(Agent Client Protocol)与 SDK 的快照回放可能把相同分段组装成不同于已提交 `system-prompt.expected.md` 文件的顺序。
|
||||
|
||||
## Decision
|
||||
|
||||
在不改变既有相对顺序的前提下,为受影响的分段序列指定互不相同的 order:`tool:cordis` 保持 115,`tool:workflow` 使用 115.5,`tool:ralph` 保持 116,可继续运行的子代理指引保持 116.5,子代理报告指引保持 117。提示词文本与工具 schema 保持不变。
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**在快照 harness 中规范化分段顺序。** 已否决,因为运行时、请求标头和模型提示词仍然受激活时序影响,只有 fixture 比较会隐藏差异。
|
||||
|
||||
**在注册表中用分段名称打破并列。** 已否决,因为这会静默重排每一组现有并列。显式 order 让每个模型可见位置都由贡献该分段的插件就地决定。
|
||||
|
||||
## Consequences
|
||||
|
||||
Cordis 与 workflow 指引具有不依赖平台的顺序,同时 Ralph 仍排在可继续运行的子代理指引和子代理报告指引之前。需要稳定相对位置的提示词分段必须使用互不相同的 `order`;其他等序分段仍采用激活顺序,不属于本决策的范围。
|
||||
|
||||
## Testing
|
||||
|
||||
无密钥 ACP 与 SDK 快照回放会固定 Cordis 排在 workflow 之前,并保留 workflow、Ralph、可继续运行的子代理和子代理报告指引的顺序。完整快照套件验证刷新的 fixture。
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-08-24-token-meter-surface-fold-plan-commit.md
|
||||
2026-08-24-token-meter-surface-fold-plan-commit.md: 878b2634c18e6bfbf5c341260659028f92399e96
|
||||
2026-08-24-token-meter-surface-fold-plan-commit.zh.md: 7707bf645ecc26098835e6922f48911927ce36f1
|
||||
@@ -0,0 +1,29 @@
|
||||
# Agent Note: Token-meter surface fold commits in place through a plan/commit pair
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-08-24-token-meter-surface-fold-plan-commit.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
`foldSurfaceTokens` rebuilt the meter's priced surface on every surface event: an append allocated `[...nodes, node]` and a replacement copied the whole array before splicing. The copy existed for one property — a throw must leave the caller's `ReplayState` untouched so a malformed event fails identically on every retry — but it charged every WELL-FORMED event O(surface) for it. Benchmarks on this fold showed the copy was ~99.9% of an append's cost (100µs at a 50k-node surface versus 0.1µs for the pricing itself), and successive appends accumulate O(S²) over a session's life, concentrated in exactly the long sessions users report as sluggish. The token meter folds inside the synchronous `session/event` publication path, so this cost lands on the agent loop's streaming appends.
|
||||
|
||||
## Decision
|
||||
|
||||
Split the fold into the session core's existing `planSurfaceEvent`/`applySurfacePlan` shape: `planSurfaceTokens` performs every fallible step (message pricing, replacement-range resolution) against the read-only surface and returns a `SurfaceTokenPlan`; `commitSurfaceTokens` applies a plan in place — `push` for an append, one `splice` for a replacement — and is infallible by construction. `TokenMeter._foldEvent` plans first, runs the remaining fallible anchor validation (step pairing, provider-chunk provenance), and only then commits, so retry identity is preserved by ordering instead of by allocation. Appends drop from O(surface) to amortized O(1); replacements keep their O(surface) `findIndex` but stop paying the extra full copy.
|
||||
|
||||
`measure()` still detaches its result with `structuredClone` + `deepFreeze`, so in-place mutation of the meter-owned array never escapes to callers.
|
||||
|
||||
## Testing
|
||||
|
||||
The existing malformed-replay suite already pins retry identity (`expectRepeatedFailure` asserts the same throw twice for out-of-range replacements, missing step boundaries, and bad provenance). A new regression test covers the hazard this change introduces: an event whose surface plan is valid but whose later anchor validation throws must leave the priced surface and running total uncommitted across repeated failures — under a mis-ordered in-place commit the throw pattern would still match while the surface silently double-counted. The full token-meter and compaction suites exercise both commit arms through real prune and summary replacements.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**A seq→index map to make replacements O(1) too.** Rejected for now: index shifts on every splice force an O(surface) rebuild per replacement anyway, and replacements are orders of magnitude rarer than appends (compaction summaries and prune passes only). The append path was the quadratic term.
|
||||
|
||||
**Keeping the allocation and sharing structurally (persistent vector).** Rejected: a dependency or hand-rolled structure for a single internal array is not justified when the plan/commit ordering already provides the atomicity the copy existed for.
|
||||
|
||||
## Consequences
|
||||
|
||||
The fold no longer contributes a quadratic term to long-session append cost; the meter's remaining per-event costs are the `Session.events` snapshot read in `_sync` (addressed independently by the indexed log-read work, PR #1724/#2907) and O(content) pricing, which is inherent. `SurfaceTokenFold` (the old detached-result type) is gone; `surface-fold.ts` is package-internal, so no external consumer changes. The [composer context-meter note](../feature/2026-08-05-composer-context-meter-breakdown.md) records the projection design around this fold.
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
# Agent Note:token-meter surface fold 改为 plan/commit 两段式原地提交
|
||||
|
||||
状态:已实现
|
||||
|
||||
[English](2026-08-24-token-meter-surface-fold-plan-commit.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
`foldSurfaceTokens` 在每个 surface 事件上重建计价 surface:append 分配 `[...nodes, node]`,replacement 先整表复制再 splice。这次复制只为一个性质而存在——抛错必须让调用方的 `ReplayState` 保持原样,使同一条畸形事件在每次重试时以完全相同的方式失败——但它让每条**合法**事件都为此付出 O(surface)。针对该 fold 的基准显示复制占 append 成本的约 99.9%(surface 为 5 万节点时每次 100µs,而估价本身仅 0.1µs),且连续 append 在会话生命周期内累计 O(S²),恰好集中在用户反馈卡顿的长会话上。token meter 在同步的 `session/event` 发布路径内折叠,这笔成本直接落在 agent loop 的流式 append 上。
|
||||
|
||||
## 决定
|
||||
|
||||
按 session 核心既有的 `planSurfaceEvent`/`applySurfacePlan` 形态拆分 fold:`planSurfaceTokens` 针对只读 surface 执行所有可失败步骤(消息估价、替换区间解析)并返回 `SurfaceTokenPlan`;`commitSurfaceTokens` 原地应用 plan——append 用 `push`,replacement 用一次 `splice`——并且构造上不可失败。`TokenMeter._foldEvent` 先 plan,再执行剩余的可失败 anchor 校验(step 配对、provider chunk 溯源),最后才 commit,因此重试一致性由执行顺序保证而不再依赖分配。append 从 O(surface) 降为均摊 O(1);replacement 保留 O(surface) 的 `findIndex`,但不再额外整表复制。
|
||||
|
||||
`measure()` 仍以 `structuredClone` + `deepFreeze` 分离结果,所以对 meter 私有数组的原地修改永远不会泄漏给调用方。
|
||||
|
||||
## 测试
|
||||
|
||||
既有的畸形回放套件已钉住重试一致性(`expectRepeatedFailure` 对越界替换、缺失 step 边界、坏溯源各断言两次相同抛错)。新增一个回归测试覆盖本次改动引入的风险点:surface plan 合法但后续 anchor 校验抛错的事件,必须在反复失败后保持计价 surface 与累计总量未提交——若原地提交顺序错误,抛错模式依然匹配而 surface 会悄悄重复计数。完整的 token-meter 与 compaction 套件通过真实的 prune 与 summary 替换覆盖两个 commit 分支。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
**用 seq→index 映射把 replacement 也做成 O(1)。** 暂缓:每次 splice 引起的下标移动本就迫使映射按替换做 O(surface) 重建,而 replacement 比 append 少几个数量级(仅 compaction 摘要与 prune 批次)。二次项在 append 路径上。
|
||||
|
||||
**保留分配并用结构共享(持久化向量)。** 否决:为单个内部数组引入依赖或手搓结构并不划算,plan/commit 的顺序已提供复制原本换取的原子性。
|
||||
|
||||
## 后果
|
||||
|
||||
该 fold 不再为长会话 append 成本贡献二次项;meter 剩余的每事件成本是 `_sync` 中的 `Session.events` 快照读取(由索引化日志读取工作独立解决,PR #1724/#2907)与固有的 O(内容) 估价。旧的分离结果类型 `SurfaceTokenFold` 已移除;`surface-fold.ts` 为包内部模块,无外部消费者需要变更。[composer 上下文仪表笔记](../feature/2026-08-05-composer-context-meter-breakdown.zh.md)记录了该 fold 周边的投影设计。
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-06-approval-seam.md
|
||||
2026-07-06-approval-seam.md: d7f8d90d408bb85c20f6a4dd0373de1aff9b180b
|
||||
2026-07-06-approval-seam.zh.md: 8f2481ff0e25193e91118d5b8181cb4dd3cf40e5
|
||||
2026-07-06-approval-seam.md: cbb1b1cb1fef0f6ed1db8e3e0eb1ea01a5834f53
|
||||
2026-07-06-approval-seam.zh.md: 29e23f81827200cbbbe0d6a78216ff1f1d4b52ae
|
||||
|
||||
@@ -25,7 +25,7 @@ One `cordis.yml` entry mounts the seam. Not loading it is the fail-closed opt-ou
|
||||
# policy: never # deployment default for sessions without an override; 'ask' when omitted
|
||||
```
|
||||
|
||||
The entry alone provides mechanism, not a channel: with no answerer composed, every ask resolves `unavailable` and the asking tool call denies — fail-closed needs no configuration. Composing the ACP profile app (`@deepseek-ai/dsh-acp-app`, as in [the acp-agent example](../../../../examples/acp-agent/README.md)) completes the loop: its [automation-only bridge](../simplification/2026-07-23-acp-automation-only-protocol.md) registers an answerer that sends `session/request_permission` to the owning client with the exact tool-call id and one-shot allow/reject options. `policy: never` is the unattended stance — every ask auto-rejects deterministically, and the current value joins the runtime-context snapshot. `policy` is validated against the closed list at plugin load; anything else throws.
|
||||
The entry alone provides mechanism, not a channel: with no answerer composed, every ask resolves `unavailable` and the asking tool call denies — fail-closed needs no configuration. Composing the [ACP profile app](../../../../packages/bundle/acp-app/README.md) completes the loop: its [automation-only bridge](../simplification/2026-07-23-acp-automation-only-protocol.md) registers an answerer that sends `session/request_permission` to the owning client with the exact tool-call id and one-shot allow/reject options. `policy: never` is the unattended stance — every ask auto-rejects deterministically, and the current value joins the runtime-context snapshot. `policy` is validated against the closed list at plugin load; anything else throws.
|
||||
|
||||
What a composed deployment observes: `allowed-once` lets exactly that call proceed; rejection, dismissal, and channel absence deny with three distinct reasons the model can tell apart; a successful in-turn request lands a durable `approval/asked`/`approval/decided` pair on the asking agent's session log; nothing about a grant persists past the call that asked. An idle request or audit append failure rejects instead of returning an unaudited decision.
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ Status: implemented
|
||||
# policy: never # deployment default for sessions without an override; 'ask' when omitted
|
||||
```
|
||||
|
||||
仅有这条条目只提供机制,不提供通道:没有组合应答者时,每次 ask 都解析为 `unavailable`,发起请求的工具调用会被拒绝——无需配置即可做到故障时默认拒绝。组合 ACP profile 应用(`@deepseek-ai/dsh-acp-app`,如 [acp-agent 示例](../../../../examples/acp-agent/README.zh.md))即可闭环:其[仅面向自动化的桥接层](../simplification/2026-07-23-acp-automation-only-protocol.zh.md)注册一个应答者,向拥有该会话的客户端发送 `session/request_permission`,携带精确的工具调用 id 和一次性 allow/reject 选项。`policy: never` 是无人值守姿态:每次 ask 都会被确定性地自动拒绝,当前值也会加入运行时上下文快照。`policy` 在插件加载时对照封闭列表校验;非法值直接抛异常。
|
||||
仅有这条条目只提供机制,不提供通道:没有组合应答者时,每次 ask 都解析为 `unavailable`,发起请求的工具调用会被拒绝——无需配置即可做到故障时默认拒绝。组合 [ACP profile 应用](../../../../packages/bundle/acp-app/README.zh.md)即可闭环:其[仅面向自动化的桥接层](../simplification/2026-07-23-acp-automation-only-protocol.zh.md)注册一个应答者,向拥有该会话的客户端发送 `session/request_permission`,携带精确的工具调用 id 和一次性 allow/reject 选项。`policy: never` 是无人值守姿态:每次 ask 都会被确定性地自动拒绝,当前值也会加入运行时上下文快照。`policy` 在插件加载时对照封闭列表校验;非法值直接抛异常。
|
||||
|
||||
组合部署的可观测行为:`allowed-once` 仅允许该次调用继续;拒绝、关闭和通道缺失以三种不同原因拒绝,模型可以区分;轮次内成功的请求会在发起请求的 agent 的会话日志上落一对持久化的 `approval/asked`/`approval/decided` 事件;授权不会在发起请求的调用结束后继续存在。空闲时的请求或审计追加失败会拒绝,而不会返回未经审计的决策。
|
||||
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-06-sandbox.md
|
||||
2026-07-06-sandbox.md: 660406167b757a0c7161184dd98e4ab6327f12f8
|
||||
2026-07-06-sandbox.zh.md: e4fcb37a4cb56b7aa90eebe365c3ec5ddc0a9ed1
|
||||
2026-07-06-sandbox.md: 9a3f61cf853ef2866180611ae12d8dbd1ad71989
|
||||
2026-07-06-sandbox.zh.md: 2b6d17322bbd22d65d986c0a80dc23d8ebf6cee3
|
||||
|
||||
@@ -18,7 +18,7 @@ One seam, one per-platform chain of local backends, one consumer, and two levers
|
||||
|
||||
### How a deployment uses it
|
||||
|
||||
Four `cordis.yml` entries turn an unconfined coding agent into the sandboxed product path; [`examples/acp-agent`](../../../../examples/acp-agent/README.md) uses this composition by default:
|
||||
Four `cordis.yml` entries turn an unconfined coding agent into the sandboxed product path; the [base profile bundle](../../../../packages/bundle/base/cordis.patch.yml) uses this composition by default:
|
||||
|
||||
```yaml
|
||||
- id: sandbox
|
||||
@@ -40,7 +40,7 @@ The swap is invisible to every consumer of `ctx.shell`: the bash tools, hook com
|
||||
|
||||
Misconfiguration fails loud: `mode` outside the closed vocabulary is rejected at plugin load, and a host with no usable backend throws the structured `SANDBOX_UNAVAILABLE` at `confine()` rather than degrading to unconfined execution. If the selected runner rejects with attributable `ENOENT` or `EACCES`, the consumer reports the same infrastructure error from the spawn channel before any command starts; other spawn errors retain local command-start semantics while still running nothing. `runnerCommand` on `dsh-sandbox-local` is the operator's explicit assertion of a bwrap-compatible runner (chain and probes skipped); it doubles as the deterministic fake-runner hook for keyless tests.
|
||||
|
||||
Denied file effects return a `[sandbox: file access denied under <mode> mode]` marker and instructions not to work around the denial. A confining executor adds paired `sandbox_permissions` and `justification` fields for one approved retry that must be strictly wider than the session's effective mode. A grant widens only that retry; rejection executes nothing, returns `the user rejected escalating this command to "<mode>"`, and permits no re-ask. The owner-derived pending policy context states the current file policy without replacing those enforcement boundaries. When `dsh-permission-presets` is composed with a UI adapter, one preset selects both knob values; unmatched values fold to `custom`. The [ACP automation composition](../../../../examples/acp-agent/README.md) does not mount that UI service and selects its deployment mode explicitly.
|
||||
Denied file effects return a `[sandbox: file access denied under <mode> mode]` marker and instructions not to work around the denial. A confining executor adds paired `sandbox_permissions` and `justification` fields for one approved retry that must be strictly wider than the session's effective mode. A grant widens only that retry; rejection executes nothing, returns `the user rejected escalating this command to "<mode>"`, and permits no re-ask. The owner-derived pending policy context states the current file policy without replacing those enforcement boundaries. When `dsh-permission-presets` is composed with a UI adapter, one preset selects both knob values; unmatched values fold to `custom`. The [ACP application bundle](../../../../packages/bundle/acp-app/README.md) does not mount that UI service and selects its deployment mode explicitly.
|
||||
|
||||
### Design detail
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ harness 是一个 SDK,因此约束必须是开发者可组合的能力:是
|
||||
|
||||
### 部署方式
|
||||
|
||||
四条 `cordis.yml` 条目即可将一个无约束的编码 agent 转变为沙箱产品路径;[`examples/acp-agent`](../../../../examples/acp-agent/README.zh.md) 默认使用此组合:
|
||||
四条 `cordis.yml` 条目即可将一个无约束的编码 agent 转变为沙箱产品路径;[base profile bundle](../../../../packages/bundle/base/cordis.patch.yml)默认使用此组合:
|
||||
|
||||
```yaml
|
||||
- id: sandbox
|
||||
@@ -40,7 +40,7 @@ harness 是一个 SDK,因此约束必须是开发者可组合的能力:是
|
||||
|
||||
配置错误会显式导致失败:`mode` 不在封闭词汇中时在插件加载时被拒绝;主机上没有可用后端时在 `confine()` 阶段抛出结构化的 `SANDBOX_UNAVAILABLE`,而非降级为无约束执行。如果所选 runner 以可归因的 `ENOENT` 或 `EACCES` 拒绝,消费方会在任何命令开始前通过 spawn 通道报告同一基础设施错误;其他 spawn 错误仍保留本地命令启动语义,同时也不会运行任何内容。`dsh-sandbox-local` 上的 `runnerCommand` 是运维人员对一个 bwrap 兼容 runner 的显式断言(跳过链和探测);它同时充当 keyless 测试的确定性 fake-runner 钩子。
|
||||
|
||||
被拒绝的文件操作返回 `[sandbox: file access denied under <mode> mode]` 标记,并附带不要绕过拒绝的指令。约束执行器添加配对的 `sandbox_permissions` 和 `justification` 字段,用于一次经批准的重试,该重试必须严格宽于会话的有效模式。授权仅放宽该次重试;拒绝则不执行任何内容,返回 `the user rejected escalating this command to "<mode>"`,且不允许再次请求。由归属方派生的待处理策略上下文会说明当前文件策略,但不会取代这些强制执行边界。当 `dsh-permission-presets` 与某个 UI 适配器一起组合时,一个 preset 同时选定两个旋钮值;不匹配的组合折叠为 `custom`。[ACP(Agent Client Protocol)自动化组合](../../../../examples/acp-agent/README.zh.md)不挂载该 UI 服务,而是显式选定其部署模式。
|
||||
被拒绝的文件操作返回 `[sandbox: file access denied under <mode> mode]` 标记,并附带不要绕过拒绝的指令。约束执行器添加配对的 `sandbox_permissions` 和 `justification` 字段,用于一次经批准的重试,该重试必须严格宽于会话的有效模式。授权仅放宽该次重试;拒绝则不执行任何内容,返回 `the user rejected escalating this command to "<mode>"`,且不允许再次请求。由归属方派生的待处理策略上下文会说明当前文件策略,但不会取代这些强制执行边界。当 `dsh-permission-presets` 与某个 UI 适配器一起组合时,一个 preset 同时选定两个旋钮值;不匹配的组合折叠为 `custom`。[ACP 应用 bundle](../../../../packages/bundle/acp-app/README.zh.md)不挂载该 UI 服务,而是显式选定其部署模式。
|
||||
|
||||
### 设计细节
|
||||
|
||||
|
||||
+2
-2
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.md
|
||||
2026-07-08-self-referential-cordis-toolset.md: feb613d74c6be65f13aa9005b9f0d90c57beb14b
|
||||
2026-07-08-self-referential-cordis-toolset.zh.md: 8840e548cac6dba2d0b5a9e724b747e46d187097
|
||||
2026-07-08-self-referential-cordis-toolset.md: d8ae7d26666964c24245021249473d4160b35220
|
||||
2026-07-08-self-referential-cordis-toolset.zh.md: d6fac5b59d1d057ea11b607cba115ceb1c37664a
|
||||
|
||||
@@ -12,7 +12,7 @@ First, model-written registration must be validated where it happens: a malforme
|
||||
|
||||
## Decision
|
||||
|
||||
The toolset ships as [`@deepseek-ai/dsh-tool-cordis`](../../../../packages/extensions/tool-cordis/README.md) and is demoed by `examples/web-cordis`. It gives the model three tools over the live Cordis runtime in the current DSH process: inspect it, mount an in-memory temporary Plugin, and unmount that Plugin to quiescence.
|
||||
The toolset ships as [`@deepseek-ai/dsh-tool-cordis`](../../../../packages/extensions/tool-cordis/README.md), with its runnable overlay and usage in the [runtime Cordis guide](../../../../docs/user/develop/practice/dynamic-cordis.md). It gives the model three tools over the live Cordis runtime in the current DSH process: inspect it, mount an in-memory temporary Plugin, and unmount that Plugin to quiescence.
|
||||
|
||||
The vm isolates accidental global pollution, and the context façade hides framework internals. Neither restricts the authority of exposed services: a temporary Plugin can call `ctx.shell` with the host executor's privileges and reach the real filesystem and web services. It runs in the shared DSH runtime and may affect other sessions in that process. This is an opt-in development tool with bash-equivalent trust, not a security boundary or product default.
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ Status: implemented
|
||||
|
||||
## 决策
|
||||
|
||||
该工具集以 [`@deepseek-ai/dsh-tool-cordis`](../../../../packages/extensions/tool-cordis/README.zh.md) 发布,并由 `examples/web-cordis` 演示。它为模型提供三个工具,用于操作当前 DSH 进程中的活跃 Cordis 运行时:检查该运行时、挂载一个仅存于内存的临时插件,再将该插件卸载至完全停稳。
|
||||
该工具集以 [`@deepseek-ai/dsh-tool-cordis`](../../../../packages/extensions/tool-cordis/README.zh.md) 发布,其可运行 overlay 与用法位于[运行时 Cordis 指南](../../../../docs/user/develop/practice/dynamic-cordis.zh.md)。它为模型提供三个工具,用于操作当前 DSH 进程中的活跃 Cordis 运行时:检查该运行时、挂载一个仅存于内存的临时插件,再将该插件卸载至完全停稳。
|
||||
|
||||
vm 隔离了意外的全局污染,上下文门面隐藏了框架内部细节。但二者都不限制已暴露服务的权限:临时插件可以调用 `ctx.shell` 以宿主执行器的权限运行命令,也能访问真实的文件系统和网络服务。它运行在共享 DSH 运行时中,可能影响同一进程的其他会话。这是一个需要显式启用的开发工具,信任等级与 bash 相当,不是安全边界,也不是产品默认配置。
|
||||
|
||||
|
||||
+2
-2
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-12-subagent-persona-tool-filter-and-depth.md
|
||||
2026-07-12-subagent-persona-tool-filter-and-depth.md: 26349e87c008ff89fa8f7ebfd7fce30e446180d4
|
||||
2026-07-12-subagent-persona-tool-filter-and-depth.zh.md: 6628cd84a008c59c991549668c76a2ce118edcf6
|
||||
2026-07-12-subagent-persona-tool-filter-and-depth.md: 4e4fa3c0039004c50f9c803a951253a7f603f909
|
||||
2026-07-12-subagent-persona-tool-filter-and-depth.zh.md: e2fb1508d780d39f587d279ab097f86ed7602c80
|
||||
|
||||
+1
-1
@@ -53,7 +53,7 @@ The depth limit bounds recursive delegation independently of tool visibility. A
|
||||
|
||||
The effective parent depth is the greater of durable `SessionHeader.delegationDepth` and runtime `AgentOptions.subagentDepth`. An in-process child records its derived depth in the session header, and resume restores that header, so a restart cannot lower the recursion count.
|
||||
|
||||
Every public entry validates the domain rather than relying on one model-facing configuration path. Negative values, fractions, negative zero, non-finite values, unsafe integers, malformed stored parent depth, and derived overflow all reject. A direct `SubagentStartRequest` may omit the cap to leave depth unbounded; loader-resolved `dsh-tool-subagent` configuration instead defaults to `3`, accepts a numeric override, and uses explicit `'provider-managed'` to omit the cap for an out-of-process provider whose deployment owns its recursion budget. Three is a small finite default that still permits a root plus three descendant generations: the [JSON-RPC example](../../../../examples/python-sdk-agent/cordis.yml) uses that general policy, while the ACP and headless examples pin one. A numeric tool cap fails at provider mount when the provider lacks `depthLimit`.
|
||||
Every public entry validates the domain rather than relying on one model-facing configuration path. Negative values, fractions, negative zero, non-finite values, unsafe integers, malformed stored parent depth, and derived overflow all reject. A direct `SubagentStartRequest` may omit the cap to leave depth unbounded; loader-resolved `dsh-tool-subagent` configuration instead defaults to `3`, accepts a numeric override, and uses explicit `'provider-managed'` to omit the cap for an out-of-process provider whose deployment owns its recursion budget. Three is a small finite default that still permits a root plus three descendant generations, and the [base profile](../../../../packages/bundle/base/cordis.patch.yml) uses that general policy. A numeric tool cap fails at provider mount when the provider lacks `depthLimit`.
|
||||
|
||||
A deployment can combine depth and filtering, but the numeric cap does not synthesize a filter. The delegation tool stays visible at the cap because authorization may depend on runtime state; every attempted start checks the calling agent's current durable and runtime depth, and a rejected start returns an errored tool result without publishing a child. A deployment may separately deny delegation tools in children when its visibility policy is static. Neither choice changes the provider's conversation-history behavior.
|
||||
|
||||
|
||||
+1
-1
@@ -55,7 +55,7 @@ subagent 启动有三个独立的组合控制:`persona`、`toolFilter` 和 `ma
|
||||
|
||||
有效父级深度取持久 `SessionHeader.delegationDepth` 与运行时 `AgentOptions.subagentDepth` 中的较大值。进程内子 agent 把推导出的深度记录在会话 header 中,恢复时会重新载入该 header,因此重启无法降低递归计数。
|
||||
|
||||
每个公开入口都自行验证值域,而非依赖单一的面向模型配置路径。负值、小数、负零、非有限值、不安全整数、格式错误的存储父级深度以及推导溢出均被拒绝。直接的 `SubagentStartRequest` 可以省略上限,让此机制不约束深度;经 loader 解析的 `dsh-tool-subagent` 配置则默认值为 `3`、接受数值覆盖,并使用显式的 `'provider-managed'` 来省略由进程外提供方部署拥有递归预算时的上限。三是一个较小的有限默认值,仍允许 root 加三代后代:[JSON-RPC 示例](../../../../examples/python-sdk-agent/cordis.yml)采用这项通用策略,而 ACP 与 headless 示例固定为一。提供方缺少 `depthLimit` 时,数值工具上限会在提供方挂载阶段失败。
|
||||
每个公开入口都自行验证值域,而非依赖单一的面向模型配置路径。负值、小数、负零、非有限值、不安全整数、格式错误的存储父级深度以及推导溢出均被拒绝。直接的 `SubagentStartRequest` 可以省略上限,让此机制不约束深度;经 loader 解析的 `dsh-tool-subagent` 配置则默认值为 `3`、接受数值覆盖,并使用显式的 `'provider-managed'` 来省略由进程外提供方部署拥有递归预算时的上限。三是一个较小的有限默认值,仍允许 root 加三代后代,[base profile](../../../../packages/bundle/base/cordis.patch.yml)采用这项通用策略。提供方缺少 `depthLimit` 时,数值工具上限会在提供方挂载阶段失败。
|
||||
|
||||
部署可以组合深度与过滤,但数值上限不会合成过滤器。委派工具在上限处仍然可见,因为授权可能依赖运行时状态;每次尝试启动都会检查调用方 agent 当前的持久与运行时深度,被拒绝的启动返回错误工具结果,且不发布子 agent。可见性策略固定的部署可以另外在子 agent 中 deny 委派工具。两种选择都不改变提供方的对话历史行为。
|
||||
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-14-cross-family-fs-sandbox.md
|
||||
2026-07-14-cross-family-fs-sandbox.md: fca19cd30a958cb035c69d104dae249e0b70ef82
|
||||
2026-07-14-cross-family-fs-sandbox.zh.md: 42b082ff72649d1bea80839c89474f479cfe3af0
|
||||
2026-07-14-cross-family-fs-sandbox.md: 9bd62b5ad7048f05d1cc613305662c0a36a409df
|
||||
2026-07-14-cross-family-fs-sandbox.zh.md: d1dba9da9ed7e367504143ae26d05e7e9a8eb696
|
||||
|
||||
@@ -45,7 +45,7 @@ The threat model is stated in the package README: a policy fence in trusted code
|
||||
|
||||
The shared pieces live in `dsh-sandbox`, which owns the mode types: `WIDER_MODES`, the escalation-target enum, the argument-pairing validation, the denial/hint marker builders, and `approveEscalation` — the ordered fail-closed choreography. `approveEscalation` takes a minimal STRUCTURAL approver (`EscalationApprover`, generic over the agent and call-id types), not the approval service type, so `dsh-sandbox` gains no dependency on the approval or agent packages: each tool passes its own `ctx.approval`, agent, call id, and tool name as ingredients. `dsh-tool-bash` and `dsh-tool-fs` both use these; the cross-file duplication gate holds the single-sourcing honest.
|
||||
|
||||
The [`examples/acp-agent`](../../../../examples/acp-agent/cordis.yml) composition loads `dsh-sandbox-policy` and `dsh-fs-sandbox`, moves the `mode`/`workspaceRoot` config to the policy entry, and drops the old gating that disabled the fs stack under confined modes; `fs-observation-policy` (read-before-edit) composes orthogonally on top. The system prompt still states no sandbox mode — the marker teaches the boundary at the moment it matters, per the sandbox Agent Note's live evidence.
|
||||
The [base profile composition](../../../../packages/bundle/base/cordis.patch.yml) loads `dsh-sandbox-policy` and `dsh-fs-sandbox`, keeps the `mode`/`workspaceRoot` config on the policy entry, and leaves `fs-observation-policy` (read-before-edit) orthogonally composed on top. The system prompt still states no sandbox mode — the marker teaches the boundary at the moment it matters, per the sandbox Agent Note's live evidence.
|
||||
|
||||
### The enforcement point: provider, not intent gate
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ Status: implemented
|
||||
|
||||
共享部分住在 `dsh-sandbox`,它拥有模式类型:`WIDER_MODES`、升级目标枚举、参数配对校验、拒绝/提示标记构造器,以及 `approveEscalation`——有序的 fail-closed 编排。`approveEscalation` 接收一个最小的结构式 approver(`EscalationApprover`,对 agent 与 call-id 类型泛型化),而非审批服务类型,所以 `dsh-sandbox` 不获得对 approval 或 agent 包的依赖:每个工具把自己的 `ctx.approval`、agent、call id 与工具名作为原料传入。`dsh-tool-bash` 与 `dsh-tool-fs` 都使用它们;跨文件重复检测门禁确保单一来源不走样。
|
||||
|
||||
[`examples/acp-agent`](../../../../examples/acp-agent/cordis.yml) 组合加载 `dsh-sandbox-policy` 与 `dsh-fs-sandbox`,把 `mode`/`workspaceRoot` 配置移到策略条目,并去掉在受限模式下禁用整个 fs 栈的旧门控;`fs-observation-policy`(read-before-edit)正交地叠加其上。系统提示仍然不陈述沙箱模式——标记会在真正重要的那一刻教会模型边界,遵循沙箱 Agent Note 所述的实时证据原则。
|
||||
[base profile 组合](../../../../packages/bundle/base/cordis.patch.yml)加载 `dsh-sandbox-policy` 与 `dsh-fs-sandbox`,把 `mode`/`workspaceRoot` 配置留在策略条目,并让 `fs-observation-policy`(read-before-edit)正交地叠加其上。系统提示仍然不陈述沙箱模式——标记会在真正重要的那一刻教会模型边界,遵循沙箱 Agent Note 所述的实时证据原则。
|
||||
|
||||
### 强制执行点:提供方,而非 intent gate
|
||||
|
||||
|
||||
+2
-2
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-19-fresh-agent-ralph-workflow-tool.md
|
||||
2026-07-19-fresh-agent-ralph-workflow-tool.md: 4ec2a4d804ef4869c7d12f4b5a1c1b5ae63bd36d
|
||||
2026-07-19-fresh-agent-ralph-workflow-tool.zh.md: 5d6a5d00feb285bfadda9b49368c15b672ea70f4
|
||||
2026-07-19-fresh-agent-ralph-workflow-tool.md: f374fcee7abc89c61c3ff6654093d1026cc8a715
|
||||
2026-07-19-fresh-agent-ralph-workflow-tool.zh.md: 1362e9ba528230b9247bb65e3bbfa2c61a72c568
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user