Merge branch 'master' into fix/web-favicon-dark-mode

This commit is contained in:
_Kerman
2026-08-11 11:29:25 +08:00
committed by GitHub
2196 changed files with 30363 additions and 15942 deletions

View File

@@ -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/archived/feature/2026-08-08-dsh-run-headless-command.md
2026-08-08-dsh-run-headless-command.md: ce9cff965192357022c49655983fe6ff8d554b9f
2026-08-08-dsh-run-headless-command.zh.md: 0484c069ed6365235e4616542a4fb3d5ceb2d880

View File

@@ -1,9 +1,12 @@
# Agent Note: `dsh run` owns one-shot headless execution
Status: implemented
Archived: 2026-08-10
English | [中文](2026-08-08-dsh-run-headless-command.zh.md)
> **Superseded command grammar.** [Apps now own their command lines](../architecture/2026-08-06-app-owned-command-line.md): the headless startup row parses the task from `dsh --profile headless <task...>`, and the launcher no longer has a `run` invocation or patches task text into rows. This note remains the rejected launcher-owned design context; the direct execution and completion contract it selected remains current in [headless is a direct core entry point](../architecture/2026-08-09-headless-direct-core-entry-point.md).
## Problem
Generic profile boot and one-shot task execution have different lifecycle contracts. A root grammar that accepts optional task text makes one argv shape mean either a long-lived process or a terminating task according to a plugin row discovered only after composition. It also exposes a profile implementation detail as the primary user command and gives custom profiles no explicit one-shot entry.

View File

@@ -1,9 +1,12 @@
# Agent Note: `dsh run` 负责一次性 headless 执行
Status: implemented
Archived: 2026-08-10
[English](2026-08-08-dsh-run-headless-command.md) | 中文
> **命令语法已被取代。** [应用现在持有自己的命令行](../architecture/2026-08-06-app-owned-command-line.md)headless 启动行从 `dsh --profile headless <task...>` 解析任务,启动器不再包含 `run` 调用,也不再把任务文本 patch 进配置行。本笔记保留被否决的启动器持有设计背景;它选定的直接执行与完成约定仍由 [headless 是直接 core 入口](../architecture/2026-08-09-headless-direct-core-entry-point.md)持有。
## 问题
通用 profile 启动与一次性任务执行具有不同的生命周期约定。若根语法接受可选任务文本,同一种 argv 形态会表示常驻进程或终止式任务,具体含义取决于组合完成后才发现的插件配置行。它还会把 profile 实现细节暴露成主要用户命令,并使自定义 profile 缺少明确的一次性入口。

View File

@@ -256,6 +256,9 @@
"feature/2026-07-31-web-cards-toolrow.i18n.yaml": "sha256:f9a6ab72a77934cdcc02167c7313f08d7e9925362017b34bed7ad56c8c70fbaa",
"feature/2026-07-31-web-cards-toolrow.md": "sha256:5058f7cec4497d1cb0a5c8e77b88fddacac6eead034f3edec88e8514919b8a3e",
"feature/2026-07-31-web-cards-toolrow.zh.md": "sha256:ba84ef2e1be61211ab5ba6950b78ede3d3a979f252bc068d3e04e2c025f7bc03",
"feature/2026-08-08-dsh-run-headless-command.i18n.yaml": "sha256:1c2b4c5b61b9263b6267275d6fc69faeaad3cc887f0728a7ed4172d817af812b",
"feature/2026-08-08-dsh-run-headless-command.md": "sha256:7695fe7fd322377d5986f14e35f13337f4cd376405c758218a81230f6d182d1c",
"feature/2026-08-08-dsh-run-headless-command.zh.md": "sha256:113c14a36c64d2facc8ae46f37c7aa76359d8cacb9c18fcba26a723f15d036fb",
"process/2026-06-11-doc-sync-enforcement.i18n.yaml": "sha256:33b6d5874427bd7a2bd82e7e2f4f482b12448b2464aef15a9c57975edb48554d",
"process/2026-06-11-doc-sync-enforcement.md": "sha256:aa2fe83d519fc30d48dff19e596e83c8922aacc9e063e14fe2cc35b769b9100e",
"process/2026-06-11-doc-sync-enforcement.zh.md": "sha256:698017bd35f030fdea3eac51df9e43138c48140f504739d687b7251d13fced2b",
@@ -304,6 +307,12 @@
"process/2026-07-27-wine-windows-gates-experiment.i18n.yaml": "sha256:6f4cbc12ee9cddbb297bf7e138ccabcd204f66898a0f7411b1633f03d5a9eab5",
"process/2026-07-27-wine-windows-gates-experiment.md": "sha256:8d37dcdab058098c7de3da1de00ce61bef92bbc8d6ee71add959474c6fb3e936",
"process/2026-07-27-wine-windows-gates-experiment.zh.md": "sha256:77fbf04df36af09e55007a93bd6b22d08ff99869efe8de3e97dac5b4701e0a9e",
"process/2026-08-04-forward-only-pr-issue-status.i18n.yaml": "sha256:af23e203a66a95674154899410e2f420d1d0685dbf856c24cfccdaa547a17925",
"process/2026-08-04-forward-only-pr-issue-status.md": "sha256:2d31077da47d95ab3ddf64d5efc6b1b8fb7c7709d39aca4a825ef9e9d382d501",
"process/2026-08-04-forward-only-pr-issue-status.zh.md": "sha256:b61f865b7a8a0ac901250a3edbb92ea73177067c4c25448c7088925c2caeccd7",
"process/2026-08-08-review-driven-issue-lifecycle-triggers.i18n.yaml": "sha256:4c28c59d3fc323e7cd01eff31f1fe759834719c5bede1e82b39f868970bf856d",
"process/2026-08-08-review-driven-issue-lifecycle-triggers.md": "sha256:1b0514de5d030170e91e12e4d6ba788a9247f840e82700faa385a1c0c76ab857",
"process/2026-08-08-review-driven-issue-lifecycle-triggers.zh.md": "sha256:028d78d61f603d8bac64c4cce20b393a78f8e029d3bb4976e79a47ecaefa6032",
"simplification/2026-06-20-drop-unconsumed-llm-adapter-change-event.i18n.yaml": "sha256:ad3d1263cb0051b885173bf064de62065e2c646ccaae2d7250723da3b4eab90c",
"simplification/2026-06-20-drop-unconsumed-llm-adapter-change-event.md": "sha256:8fb061d51c8c23b47d2367814bab3623c6d5b972f38d207a273caa9030b579bd",
"simplification/2026-06-20-drop-unconsumed-llm-adapter-change-event.zh.md": "sha256:2ffeaca91f82844a5616d6dcce6b4af514bb8a7c46f78e47f668b204ac6edc04",

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-08-04-forward-only-pr-issue-status.md
2026-08-04-forward-only-pr-issue-status.md: dd567707bc7fccd0a631943ab3ffd2838a7f2f76
2026-08-04-forward-only-pr-issue-status.zh.md: f7fee58d6afb812f97569ae4d86c3d6504f35752
2026-08-04-forward-only-pr-issue-status.md: 56004a39ce52c77429574f481d9945cdc4936d30
2026-08-04-forward-only-pr-issue-status.zh.md: ee85319842d3245bdfab9668de0a42ab29597fac

View File

@@ -1,6 +1,7 @@
# Agent Note: Forward-only PR-to-Issue status projection
Status: implemented
Archived: 2026-08-10
English | [中文](2026-08-04-forward-only-pr-issue-status.zh.md)

View File

@@ -1,6 +1,7 @@
# Agent Note: PR 到 Issue 的状态仅向前投射
Status: implemented
Archived: 2026-08-10
[English](2026-08-04-forward-only-pr-issue-status.md) | 中文

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-08-08-review-driven-issue-lifecycle-triggers.md
2026-08-08-review-driven-issue-lifecycle-triggers.md: 8a2d48ee23da4c20bb832ae0109e2ea9912dac83
2026-08-08-review-driven-issue-lifecycle-triggers.zh.md: 004739ff471815b0fe12e111eba0ec7aaaef9507
2026-08-08-review-driven-issue-lifecycle-triggers.md: 444927968912d93f473e27ae8576e8371b9c287c
2026-08-08-review-driven-issue-lifecycle-triggers.zh.md: 6e00e2a936b6421824743e779756011fcd4a1c9e

View File

@@ -1,6 +1,7 @@
# Agent Note: Review-driven Issue lifecycle triggers
Status: implemented
Archived: 2026-08-10
English | [中文](2026-08-08-review-driven-issue-lifecycle-triggers.zh.md)

View File

@@ -1,6 +1,7 @@
# Agent Note: 由评审驱动的 Issue 生命周期触发器
Status: implemented
Archived: 2026-08-10
[English](2026-08-08-review-driven-issue-lifecycle-triggers.md) | 中文

View File

@@ -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: a45678c9bb5fcae340ff7134687890879f56c630
2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md: f1fccc508471356dd6434da0e126ed38f15ed3ba
2026-07-10-single-file-executable-sdk-runtime-distribution.md: c2b6d9ff1825915e39738bf8f782c302ecfc1d0d
2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md: d7758a77083e07b1d2cac99ae2be3f15e6edd2dc

View File

@@ -34,27 +34,27 @@ Config discovery has two channels and fails loudly when both are missing: the `D
### Plugin resolution: the VFS holds a real package tree, the closure manifest IS the deploy root
Inside the exe's VFS sits a **real package tree in build-artifact form** (each package's `lib/` plus a real `node_modules`); the Loader resolves plugin names through standard dynamic `import()`: bare specifiers resolve upward along `node_modules` from the Loader's position inside the VFS, and land inside the VFS naturally. The closed set needs no allowlist code — the set is whatever the VFS has installed, and importing a name outside the set fails.
Inside the exe's VFS sits a **real package tree in build-artifact form** (each package's `lib/` plus a real `node_modules`). The packaged JSON-RPC entry supplies its installed harness base to app-boot's root Include: relative plugin specifiers resolve from the external configuration directory, while bare package names resolve from the VFS, so a configuration inside another Node project cannot shadow the packaged plugin set. The ordinary development bin leaves bare packages configuration-owned. Bare specifiers in the packaged entry resolve upward along `node_modules` from the entry's position inside the VFS and land inside the VFS naturally. The closed set needs no allowlist code — the set is whatever the VFS has installed, and importing a name outside the set fails.
The deploy root is [`python/sdk-runtime/package.json`](../../../../python/sdk-runtime/package.json) (`dsh-jsonrpc-agent-pkg`, a pnpm workspace member and a zero-code pure dependency manifest) — the unified source of truth for "which plugins the exe ships" and "what the Python runtime distributes". Adding a plugin to the exe = adding one dependency line to the manifest and repackaging. [`scripts/verify-runtime-closure.ts`](../../../../scripts/verify-runtime-closure.ts) traverses every workspace package covered by that manifest and requires every non-optional workspace peer at the runtime root, reporting the complete referencing-package → missing-peer chain; `pnpm run hygiene`, CI static, and the single-exe build run it before packaging. Deploy also packs by each package's `files`, so the shared chunks tsdown splits out must be covered by `files`.
### Build pipeline and artifacts
[`scripts/build-exe-for-python-sdk.ts`](../../../../scripts/build-exe-for-python-sdk.ts): runtime closure verification → `pnpm run build` → (after clearing) `pnpm --filter dsh-jsonrpc-agent-pkg 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/` → inject the pkg configuration (`bin` points at `node_modules/@deepseek-ai/dsh-jsonrpc-demo/lib/bin.js` inside the closure, `assets` is a full glob — dynamic import is invisible to pkg's static analysis, so everything must be packed in explicitly) → stage the target `node-pty` addon → one `pkg --sea` per target → the executables `dsh-jsonrpc-agent-pkg-<platform>-<arch>` land in `dist-exe/` and are copied back into the runtime directory. Linux installs build `pty.node` from source, so the builder copies it from the root install into the staged closure because legacy deploy omits that side-effect directory; macOS uses its target prebuild and emits the required `-spawn-helper` beside the executable. CI treats these products as intermediate test inputs and retains their platform wheels. All four deploy flags are grounded in measurement: `--legacy` is the mandatory path with inject-workspace-packages off; hoisted yields a zero-symlink file tree (most stable for the pkg VFS, physically guaranteeing a single cordis instance); disabling automatic peer installation keeps unpublished package names from triggering registry resolution; link-workspace-packages points the closure at workspace/vendor sources.
[`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-jsonrpc-agent-pkg 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 any direct workspace package that legacy deploy hoisted back under the source manifest's `node_modules`, omitting its package-local dependency tree and rejecting any remaining manifest gap → replace every staged dependency symlink with its target bytes, remove package-manager `.bin` links, and fail if any symlink remains → inject the pkg configuration (`bin` points at `node_modules/@deepseek-ai/dsh-jsonrpc-demo/lib/packaged-bin.js` inside the closure, `assets` is a full glob — dynamic import is invisible to pkg's static analysis, so everything must be packed in explicitly) → stage the target `node-pty` addon → one `pkg --sea` per target → the executables `dsh-jsonrpc-agent-pkg-<platform>-<arch>` land in `dist-exe/` and are copied back into the runtime directory. Linux installs build `pty.node` from source, so the builder copies it from the root install into the staged closure because legacy deploy omits that side-effect directory; macOS uses its target prebuild and emits the required `-spawn-helper` beside the executable. CI treats these products as intermediate test inputs and retains their platform wheels. 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), triggered explicitly only — `workflow_dispatch`, or the `build-exe` label on a pull request; native builds on the three platforms linux-x64 / linux-arm64 (`ubuntu-24.04-arm`) / macos-arm64, with `~/.pkg-cache` cached; macOS ad-hoc signing is handled by pkg. Each leg drives a mock SSE model through the SDK with the default config and a custom `cordis.yml`, drives the exe directly over NDJSON JSON-RPC, verifies the JSONL and final response, and installs release-shaped wheels into a clean venv without `runtime_bin`; Linux additionally inspects GLIBC requirements and runs in a manylinux 2.28 container. 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 only `python-vX.Y.Z` 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.
### Python SDK distribution: two carriers, exe for production, node for development
The Python SDK lives at [`python/`](../../../../python/README.md): `python/sdk` (the client) + `python/sdk-runtime` (the runtime carrier package). The runtime package's data directory holds the checked-in default `runtime/cordis.yml`, the build-injected platform exe and optional helper, and the build-injected `runtime/node/` closure tree. `resolve_bundled_launch_args()` automatic resolution **finds the exe only**; the node carrier is enabled only by an explicit `DSH_RUNTIME_MODE=node` (running `runtime/node/node_modules/@deepseek-ai/dsh-jsonrpc-demo/lib/bin.js`, requiring a system node ≥22.19), positioned as the development-verification channel for members of this repo, and does not enter wheel distributions.
The Python SDK lives at [`python/`](../../../../python/README.md): `python/sdk` (the client) + `python/sdk-runtime` (the runtime carrier package). The runtime package's data directory holds the checked-in default `runtime/cordis.yml`, the build-injected platform exe and optional helper, and the build-injected `runtime/node/` closure tree. `resolve_bundled_launch_args()` automatic resolution **finds the exe only**; the node carrier is enabled only by an explicit `DSH_RUNTIME_MODE=node` (running `runtime/node/node_modules/@deepseek-ai/dsh-jsonrpc-demo/lib/packaged-bin.js`, requiring a system node ≥22.19), positioned as the development-verification channel for members of this repo, and does not enter wheel distributions.
[`scripts/build-python-release.py`](../../../../scripts/build-python-release.py) reads the authoritative stable `X.Y.Z` from the repository root `package.json` and stages both packages at that version, with the SDK depending exactly on `deepseek-harness-runtime-bin==X.Y.Z`. An optional `python-vX.Y.Z` 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. The SDK is a `py3-none-any` wheel; each wheel-only runtime package contains one exe, and the macOS wheel also contains its architecture-matched helper. Runtime wheels use one of `py3-none-manylinux_2_28_x86_64`, `py3-none-manylinux_2_28_aarch64`, or `py3-none-macosx_11_0_arm64`; the Hatch hook rejects sdists, universal tags, mixed-platform payloads, missing or extra helpers, and unsupported platforms.
[`scripts/build-python-release.py`](../../../../scripts/build-python-release.py) reads the authoritative stable `X.Y.Z` from the repository root `package.json` and stages both packages at that version, with `deepseek-harness-sdk` depending exactly on `deepseek-harness-runtime-bin==X.Y.Z`. An optional `python-vX.Y.Z` 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. The SDK is a `py3-none-any` wheel; each wheel-only runtime package contains one exe, and the macOS wheel also contains its architecture-matched helper. Runtime wheels use one of `py3-none-manylinux_2_28_x86_64`, `py3-none-manylinux_2_28_aarch64`, or `py3-none-macosx_11_0_arm64`; the Hatch hook rejects sdists, universal tags, mixed-platform payloads, missing or extra helpers, and unsupported platforms.
The exe's "must be explicitly configured" hard semantic is unchanged; the zero-config experience is restored by the wrapper: when the caller gave no `cordis`, named no explicit runtime, and the environment has no `DSH_CORDIS_CONFIG`, the client explicitly injects the checked-in default `cordis.yml` (agent-core + preloaded llm-deepseek + JSONL persistence + bash-local + the `dsh-jsonrpc` serving entry, with `!!js` environment-variable fallbacks) via `DSH_CORDIS_CONFIG`.
### Naming lineage
`@deepseek-ai/dsh-jsonrpc-demo` (the package) → `dsh-jsonrpc-agent` (the bin) → `dsh-jsonrpc-agent-pkg` (the closure manifest; no scope prefix, deliberately sidestepping the constraints' package-shape rules for `@deepseek-ai/dsh-*`) → `dsh-jsonrpc-agent-pkg-<platform>-<arch>` (the exe artifacts). The wire `serverInfo.name` stays `deepseek-harness-sdk-runtime` (a protocol-stable value); the Python dist names are `deepseek-harness` / `deepseek-harness-runtime-bin`.
`@deepseek-ai/dsh-jsonrpc-demo` (the package) → `dsh-jsonrpc-agent` (the bin) → `dsh-jsonrpc-agent-pkg` (the closure manifest; no scope prefix, deliberately sidestepping the constraints' package-shape rules for `@deepseek-ai/dsh-*`) → `dsh-jsonrpc-agent-pkg-<platform>-<arch>` (the exe artifacts). The wire `serverInfo.name` stays `deepseek-harness-sdk-runtime` (a protocol-stable value); the Python distribution names are `deepseek-harness-sdk` / `deepseek-harness-runtime-bin`, while the import modules remain `deepseek_harness` / `deepseek_harness_runtime`.
## Disposition of worker-style plugins
@@ -62,7 +62,7 @@ The exe's "must be explicitly configured" hard semantic is unchanged; the zero-c
## Testing
The verification surface has three tiers. Mechanism tier: the measured conclusions for the `--sea` chain are embedded in the Decision sections (ESM dynamic import inside the VFS, single cordis instance, fail-loud config chain, `node:sqlite`, macOS ad-hoc signing runs). SDK tier: the complete keyless pytest suite covers the client protocol against a fake runtime peer, subprocess cleanup, absolute cwd propagation, dual-carrier launch, and carrier resolution; root CI runs it on Python 3.10. End-to-end tier: every platform build completes a turn against a mock endpoint through the default SDK path, a custom config, and the direct binary protocol, with final text and JSONL checked. The custom config additionally drives `run_code` and a zero-agent `workflow` through their real worker files inside the packaged VFS. The same build leg runs a committed executable-specific snapshot through the Python SDK: a keyless scripted model mounts a Cordis plugin that registers a tool, invokes that tool from `run_code`, runs a direct spawn subagent and a workflow that starts a second spawn child, then unmounts the plugin. The fixture explicitly disables its unused bundled Bash and local skill discovery so its tool set does not depend on repository-external state, and the comparison normalizes opaque message IDs in the SDK result and notification stream plus the parent and two child JSONL logs. This harness stays separate from ACP's `pnpm run test:snapshot` because the protocols and build artifacts differ. The platform wheel is then installed in a clean venv and run without `runtime_bin`.
The verification surface has three tiers. Mechanism tier: the measured conclusions for the `--sea` chain are embedded in the Decision sections (ESM dynamic import inside the VFS, single cordis instance, fail-loud config chain, `node:sqlite`, macOS ad-hoc signing runs). SDK tier: the complete keyless pytest suite covers the client protocol against a fake runtime peer, subprocess cleanup, absolute cwd propagation, dual-carrier launch, and carrier resolution; root CI runs it on Python 3.10. End-to-end tier: every platform build completes a turn against a mock endpoint through the default SDK path, a custom config, the checked-in standalone minimal composition, and the direct binary protocol, with final text and JSONL checked. The minimal run asserts its exact system prompt and two-tool catalog, retains Bash state across calls, and invokes the editor. The custom config additionally drives `run_code` and a zero-agent `workflow` through their real worker files inside the packaged VFS. The same build leg runs a committed executable-specific snapshot through the Python SDK: a keyless scripted model mounts a Cordis plugin that registers a tool, invokes that tool from `run_code`, runs a direct spawn subagent and a workflow that starts a second spawn child, then unmounts the plugin. The fixture explicitly disables its unused bundled Bash and local skill discovery so its tool set does not depend on repository-external state, and the comparison normalizes opaque message IDs in the SDK result and notification stream plus the parent and two child JSONL logs. This harness stays separate from ACP's `pnpm run test:snapshot` because the protocols and build artifacts differ. The platform wheel is then installed in a clean venv and run without `runtime_bin`.
Manual-driving caveat: the bin treats stdin EOF as "the client is gone" and disposes immediately, so a short-lived pipe aborts an in-flight turn — pipe-driven runs must keep stdin open until the turn ends.

View File

@@ -34,27 +34,27 @@ exe 使用 [@yao-pkg/pkg](https://github.com/yao-pkg/pkg)vercel/pkg 归档后
### 插件解析VFS 装载真实包树,闭包 manifest元数据清单就是部署根目录
exe 的 VFS 内是**构建产物形态的真实包树**(各包的 `lib/` + 真实 `node_modules`)。loader 通过标准动态 `import()` 解析插件名:裸包名从 VFS 内 loader 所在位置沿 `node_modules` 向上解析,自然落在 VFS 内。封闭集不需要白名单代码——VFS 中安装了什么,集合中就有什么;`import()` 集合外的名称会失败。
exe 的 VFS 内是**构建产物形态的真实包树**(各包的 `lib/` + 真实 `node_modules`)。打包专用 JSON-RPC 入口会向 app-boot 的根 Include 提供自身已安装 harness 的基准位置:相对插件说明符从外部配置目录解析,裸包名从 VFS 解析,因此位于另一个 Node 项目内的配置无法遮蔽已打包的插件集合。普通开发 bin 仍由配置项目提供裸包。打包入口中的裸包名从该入口在 VFS 内的位置沿 `node_modules` 向上解析,自然落在 VFS 内。封闭集不需要白名单代码——VFS 中安装了什么,集合中就有什么;`import()` 集合外的名称会失败。
部署根目录是 [`python/sdk-runtime/package.json`](../../../../python/sdk-runtime/package.json)`dsh-jsonrpc-agent-pkg`pnpm 工作区成员、零代码纯依赖 manifest也是「exe 安装哪些插件」与「Python 运行时分发什么」的统一真源。向 exe 添加插件,就是在 manifest 中增加一行依赖后重新打包。[`scripts/verify-runtime-closure.ts`](../../../../scripts/verify-runtime-closure.ts) 遍历该 manifest 覆盖的全部工作区包要求每个非可选的工作区对等依赖peer dependency都显式列在运行时根目录并报告“引用包 → 缺失对等依赖”的完整链路;`pnpm run hygiene`、CI 静态检查与 single-exe 构建都会在打包前运行该门禁。部署还会依据各包的 `files` 字段打包,因此 tsdown 拆出的共享分片必须被 `files` 覆盖。
### 构建管线与产物
[`scripts/build-exe-for-python-sdk.ts`](../../../../scripts/build-exe-for-python-sdk.ts):运行时闭包校验 → `pnpm run build` →(清空后)`pnpm --filter dsh-jsonrpc-agent-pkg 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/` → 注入 pkg 配置(`bin` 指向闭包内的 `node_modules/@deepseek-ai/dsh-jsonrpc-demo/lib/bin.js``assets` 使用全量 glob因为动态 `import()` 对 pkg 静态分析不可见,必须显式打入全部内容)→ 暂存目标平台的 `node-pty` addon → 每个构建目标调用一次 `pkg --sea` → 可执行文件 `dsh-jsonrpc-agent-pkg-<platform>-<arch>` 写入 `dist-exe/`并拷回运行时目录。Linux 安装会从源码构建 `pty.node`,而 `--legacy` 部署会省略该副作用目录因此构建器会把它从根安装目录复制到暂存闭包macOS 使用对应目标的预构建产物,并在可执行文件旁生成所需的 `-spawn-helper`。CI 将这些产物作为测试中间输入,只保留对应平台的 wheel 包。四个部署标志都有实测依据:未启用 `inject-workspace-packages` 时必须使用 `--legacy``hoisted` 产出无符号链接的文件树(对 pkg VFS 最稳定,并从物理上保证只有一个 Cordis 实例);关闭对等依赖自动安装可避免未发布包名触发注册表解析;`link-workspace-packages` 让闭包指向工作区/vendor 源码
[`scripts/build-exe-for-python-sdk.ts`](../../../../scripts/build-exe-for-python-sdk.ts):运行时闭包校验 → `pnpm run build` →(清空后)`pnpm --filter dsh-jsonrpc-agent-pkg 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 的 `node_modules` 下的任何直接工作区包,同时省略其包内依赖树,并拒绝剩余的 manifest 缺口 → 将暂存依赖中的每个符号链接替换为目标文件内容,删除包管理器的 `.bin` 链接,并在仍有任何符号链接时失败 → 注入 pkg 配置(`bin` 指向闭包内的 `node_modules/@deepseek-ai/dsh-jsonrpc-demo/lib/packaged-bin.js``assets` 使用全量 glob因为动态 `import()` 对 pkg 静态分析不可见,必须显式打入全部内容)→ 暂存目标平台的 `node-pty` addon → 每个构建目标调用一次 `pkg --sea` → 可执行文件 `dsh-jsonrpc-agent-pkg-<platform>-<arch>` 写入 `dist-exe/`并拷回运行时目录。Linux 安装会从源码构建 `pty.node`,而 `--legacy` 部署会省略该副作用目录因此构建器会把它从根安装目录复制到暂存闭包macOS 使用对应目标的预构建产物,并在可执行文件旁生成所需的 `-spawn-helper`。CI 将这些产物作为测试中间输入,只保留对应平台的 wheel 包。四个部署标志都有实测依据:未启用 `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),且只允许显式触发:手动派发 `workflow_dispatch`,或给 PRPull Request添加 `build-exe` 标签。linux-x64、linux-arm64`ubuntu-24.04-arm`)和 macos-arm64 三个平台分别进行原生构建,并缓存 `~/.pkg-cache`macOS 的 ad-hoc 签名由 pkg 处理。每个平台都使用 mock SSEServer-Sent Events模型分别通过默认配置和自定义 `cordis.yml` 驱动 SDK再通过 NDJSON JSON-RPC 直接驱动 exe校验 JSONL 与最终响应;最后把发布形态的 wheel 包安装到干净的 venv 中,并在不传 `runtime_bin` 的情况下运行。Linux 还会检查 GLIBC 依赖,并在 manylinux 2.28 容器中运行。完整构建三个目标时保留 4 个产物,每个产物只含一个发布文件:平台无关的 SDK wheel 包与 3 个原生运行时 wheel 包;手动选择部分目标时保留 SDK wheel 与所选运行时 wheel。裸 exe 与源码包只作为测试中间输入。[`.gitlab-ci.yml`](../../../../.gitlab-ci.yml) 只接受版本与根目录 `package.json` 匹配的 `python-vX.Y.Z` 标签流水线,构建一个 SDK wheel 包和 3 个原生运行时 wheel 包,再由单个串行任务校验并将这 4 个文件发布到项目的 PyPI 注册表。Windows 不在目标范围内。
### Python SDK 分发双载体exe 用于生产,`node` 用于开发
Python SDK 位于 [`python/`](../../../../python/README.md)`python/sdk` 是客户端,`python/sdk-runtime` 是运行时载体包。运行时包的数据目录包含检入的默认 `runtime/cordis.yml`、构建注入的平台 exe 与可选 helper以及构建注入的 `runtime/node/` 闭包树。`resolve_bundled_launch_args()` 的自动解析**只查找 exe**`node` 载体仅在显式设置 `DSH_RUNTIME_MODE=node` 时启用(运行 `runtime/node/node_modules/@deepseek-ai/dsh-jsonrpc-demo/lib/bin.js`,需要系统 Node ≥22.19),定位为本仓库成员的开发验证通道,不随 wheel 包分发。
Python SDK 位于 [`python/`](../../../../python/README.md)`python/sdk` 是客户端,`python/sdk-runtime` 是运行时载体包。运行时包的数据目录包含检入的默认 `runtime/cordis.yml`、构建注入的平台 exe 与可选 helper以及构建注入的 `runtime/node/` 闭包树。`resolve_bundled_launch_args()` 的自动解析**只查找 exe**`node` 载体仅在显式设置 `DSH_RUNTIME_MODE=node` 时启用(运行 `runtime/node/node_modules/@deepseek-ai/dsh-jsonrpc-demo/lib/packaged-bin.js`,需要系统 Node ≥22.19),定位为本仓库成员的开发验证通道,不随 wheel 包分发。
[`scripts/build-python-release.py`](../../../../scripts/build-python-release.py) 从仓库根目录的 `package.json` 读取权威的稳定版本 `X.Y.Z`,以该版本暂存两个包,并让 SDK 精确依赖 `deepseek-harness-runtime-bin==X.Y.Z`。可选的 `python-vX.Y.Z` 发布标签只是一项一致性断言,与仓库版本不同时会被拒绝;源码 `pyproject.toml` 中的开发占位版本从不决定发布版本。SDK 是 `py3-none-any` wheel 包;每个只提供 wheel 包的运行时包都包含一个 exemacOS wheel 包还包含与其架构匹配的 helper。运行时 wheel 包使用 `py3-none-manylinux_2_28_x86_64``py3-none-manylinux_2_28_aarch64``py3-none-macosx_11_0_arm64` 三种标签之一Hatch 钩子拒绝 sdist、通用标签、混合平台载荷、helper 缺失或多余,以及不支持的平台。
[`scripts/build-python-release.py`](../../../../scripts/build-python-release.py) 从仓库根目录的 `package.json` 读取权威的稳定版本 `X.Y.Z`,以该版本暂存两个包,并让 `deepseek-harness-sdk` 精确依赖 `deepseek-harness-runtime-bin==X.Y.Z`。可选的 `python-vX.Y.Z` 发布标签只是一项一致性断言,与仓库版本不同时会被拒绝;源码 `pyproject.toml` 中的开发占位版本从不决定发布版本。SDK 是 `py3-none-any` wheel 包;每个只提供 wheel 包的运行时包都包含一个 exemacOS wheel 包还包含与其架构匹配的 helper。运行时 wheel 包使用 `py3-none-manylinux_2_28_x86_64``py3-none-manylinux_2_28_aarch64``py3-none-macosx_11_0_arm64` 三种标签之一Hatch 钩子拒绝 sdist、通用标签、混合平台载荷、helper 缺失或多余,以及不支持的平台。
exe「必须显式配置」的硬语义不变零配置体验由包装层恢复调用方没有提供 `cordis`、没有显式指定运行时,且环境中没有 `DSH_CORDIS_CONFIG` 时,客户端将检入的默认 `cordis.yml``agent-core` + 预载的 `llm-deepseek` + JSONL 持久化 + `bash-local` + `dsh-jsonrpc` 对外服务条目,并通过 `!!js` 使用环境变量兜底)显式注入 `DSH_CORDIS_CONFIG`
### 命名血统
`@deepseek-ai/dsh-jsonrpc-demo`(包)→ `dsh-jsonrpc-agent``bin`)→ `dsh-jsonrpc-agent-pkg`(闭包 manifest没有作用域前缀刻意避开 `constraints``@deepseek-ai/dsh-*` 的包形状规则)→ `dsh-jsonrpc-agent-pkg-<platform>-<arch>`exe 产物)。协议字段 `serverInfo.name` 保持为 `deepseek-harness-sdk-runtime`协议稳定值Python 分发名为 `deepseek-harness` / `deepseek-harness-runtime-bin`
`@deepseek-ai/dsh-jsonrpc-demo`(包)→ `dsh-jsonrpc-agent``bin`)→ `dsh-jsonrpc-agent-pkg`(闭包 manifest没有作用域前缀刻意避开 `constraints``@deepseek-ai/dsh-*` 的包形状规则)→ `dsh-jsonrpc-agent-pkg-<platform>-<arch>`exe 产物)。协议字段 `serverInfo.name` 保持为 `deepseek-harness-sdk-runtime`协议稳定值Python 分发名为 `deepseek-harness-sdk` / `deepseek-harness-runtime-bin`,导入模块名仍为 `deepseek_harness` / `deepseek_harness_runtime`
## 工作线程插件
@@ -62,7 +62,7 @@ exe 内支持 `dsh-workflow-workerthread` 与 `dsh-code-runtime-worker`。两个
## 测试
验证面分三层。机制层:`--sea` 链路的实测结论内嵌在「决策」各节VFS 内 ESM 动态 `import()`、单一 Cordis 实例、明确报错的配置链路、`node:sqlite`、macOS ad-hoc 签名可运行。SDK 层:完整的无密钥 pytest 套件以 mock 运行时对端覆盖客户端协议、子进程清理、绝对 `cwd` 传递、双载体启动与载体解析;根 CI 在 Python 3.10 上运行全部用例。端到端层:每个平台构建都通过默认 SDK 路径、自定义配置和直接二进制协议,对 mock 端点完成一个轮次,并校验最终文本与 JSONL。自定义配置还会通过打包进 VFS 的真实工作线程文件执行 `run_code` 和不启动 agent 的 `workflow`。同一构建任务还会经 Python SDK 运行一组检入的 exe 专用快照:无密钥脚本化模型挂载一个会注册工具的 Cordis 插件,从 `run_code` 调用该工具,运行一个直接 spawn 的 subagent 和一个会通过 spawn 启动第二个 subagent 的工作流,随后卸载该插件。该 fixture测试前置数据会显式禁用组合包中未使用的 Bash 和本地 skill技能发现使其工具集不依赖仓库外部状态比较时会规范化以下各处的不透明消息 IDSDK 结果与通知流,以及父会话和两个子会话的 JSONL 日志。该 harness 与 ACP 的 `pnpm run test:snapshot` 保持独立,因为二者的协议和构建产物不同。随后把平台 wheel 包安装进干净的 venv并在不传 `runtime_bin` 的情况下运行。
验证面分三层。机制层:`--sea` 链路的实测结论内嵌在「决策」各节VFS 内 ESM 动态 `import()`、单一 Cordis 实例、明确报错的配置链路、`node:sqlite`、macOS ad-hoc 签名可运行。SDK 层:完整的无密钥 pytest 套件以 mock 运行时对端覆盖客户端协议、子进程清理、绝对 `cwd` 传递、双载体启动与载体解析;根 CI 在 Python 3.10 上运行全部用例。端到端层:每个平台构建都通过默认 SDK 路径、自定义配置、仓库内置的独立 minimal 组合和直接二进制协议,对 mock 端点完成一个轮次,并校验最终文本与 JSONL。minimal 运行会断言其精确系统提示词与双工具目录,跨调用保留 Bash 状态,并调用编辑器。自定义配置还会通过打包进 VFS 的真实工作线程文件执行 `run_code` 和不启动 agent 的 `workflow`。同一构建任务还会经 Python SDK 运行一组检入的 exe 专用快照:无密钥脚本化模型挂载一个会注册工具的 Cordis 插件,从 `run_code` 调用该工具,运行一个直接 spawn 的 subagent 和一个会通过 spawn 启动第二个 subagent 的工作流,随后卸载该插件。该 fixture测试前置数据会显式禁用组合包中未使用的 Bash 和本地 skill技能发现使其工具集不依赖仓库外部状态比较时会规范化以下各处的不透明消息 IDSDK 结果与通知流,以及父会话和两个子会话的 JSONL 日志。该 harness 与 ACP 的 `pnpm run test:snapshot` 保持独立,因为二者的协议和构建产物不同。随后把平台 wheel 包安装进干净的 venv并在不传 `runtime_bin` 的情况下运行。
手工驱动注意:`bin` 将 stdin EOF 视为「客户端已离开」并立即 dispose短命管道会中止进行中的轮次——管道驱动必须保持 stdin 打开,直到轮次结束。

View File

@@ -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-19-gui-layering-and-rpc-protocol.md
2026-07-19-gui-layering-and-rpc-protocol.md: f9c95176321496e965a95b6358d6feaa8466fe89
2026-07-19-gui-layering-and-rpc-protocol.zh.md: 7d20c5a2662c9036382b30a96bc9973c8f0349bd
2026-07-19-gui-layering-and-rpc-protocol.md: da96ae97f2a2d64aeef7794bd82ccbd86602b1ad
2026-07-19-gui-layering-and-rpc-protocol.zh.md: 36dc7391bc3f9bb0d5105fea14a2763d0b7159a1

View File

@@ -10,7 +10,7 @@ English | [中文](2026-07-19-gui-layering-and-rpc-protocol.zh.md)
We need a UI integration layer. Beyond the existing ACP/stdio baseline, more product clients are coming — Web (server), Electron, and others. We call them Clients and want the following capabilities:
- One `dsh` process supporting both `dsh web` (serve) and `dsh run` (headless) — one process, two modes (a design reservation)
- One `dsh` process supporting both `dsh web` (serve) and `dsh --profile headless` (headless) — one process, two modes (a design reservation)
- Launching inside Electron with the same Web technologies as `dsh web`
That demands a stable layered responsibility model in the engineering codebase, so future clients plug in cleanly.
@@ -27,11 +27,11 @@ Directories layer as follows:
- the unified backend protocol (fetch, HTTP, streaming interfaces…) — definitions and support, see the "Message protocol" sections below
- `packages/client/*`: packages provide client-side capability only; every package stays single-sided. Three kinds live here (the axes are owned by the [client plugin loading note](2026-07-23-client-plugin-loading-model.md)):
- **Pure libraries** (`ui-slots`, `web-react`, `ui-primitives`, plus the `loader` kernel package): ordinary root-index packages, statically bundled into the shell; the first three are seeded into the module table.
- **Static-arrival entry packages** (`connection`, `runtime`, `ui-theme`, `i18n`, `hmr`): no `dshClient` key and no browser bundle — the shell bundles their `src/client/` half and registers it with `ctx.modules`; they are governed as entries of the host-authored graph like everything else.
- **Fetch-arrival plugin packages** (`ui-layout`, `ui-sidebar`, `ui-conversation`, `ui-trajectory`): dual-entry — the root index is the node half (an empty `apply`, existing so the host Loader governs lifecycle and the web plugin registry discovers the package.json `dshClient` declaration); the implementation lives under `src/client/`, shipped as the `./client` subpath (a tsdown closure-factory bundle). Cross-plugin consumption of `/client` is type-only; value cooperation goes through cordis services.
- **Static-arrival entry packages** (`connection`, `runtime`, `ui-theme`, `i18n`, `hmr`): no `dsh.client` key and no browser bundle — the shell bundles their `src/client/` half and registers it with `ctx.modules`; they are governed as entries of the host-authored graph like everything else.
- **Fetch-arrival plugin packages** (`ui-layout`, `ui-sidebar`, `ui-conversation`, `ui-trajectory`): dual-entry — the root index is the node half (an empty `apply`, existing so the host Loader governs lifecycle and the web plugin registry discovers the package.json `dsh.client` declaration); the implementation lives under `src/client/`, shipped as the `./client` subpath (a tsdown closure-factory bundle). Cross-plugin consumption of `/client` is type-only; value cooperation goes through cordis services.
- `apps/` holds the externally exported applications, assembled from Client / Host mixtures.
- `apps/web` (`dsh-frontend`) is the vite application: a thin `main.ts` over the shell surface exported by `dsh-client-web`.
- `apps/cli` (`@deepseek-ai/dsh`) dispatches commands: `dsh web` = Host + webserver + the built `dsh-frontend` dist; `dsh run` = [a direct core Agent/Session entry point](2026-08-09-headless-direct-core-entry-point.md), with zero Host, HTTP, or browser layer.
- `apps/cli` (`@deepseek-ai/dsh`) dispatches commands: `dsh web` = Host + webserver + the built `dsh-frontend` dist; `dsh --profile headless` = [a direct core Agent/Session entry point](2026-08-09-headless-direct-core-entry-point.md), with zero Host, HTTP, or browser layer.
- A future Electron application reuses the same web client packages over an IPC fetch carrier.
```
@@ -40,7 +40,7 @@ apps/* (applications: apps/web = vite app, apps/cli = bin dispatch)
packages/host/* packages/client/*
apiproxy front layer: protocol pure libs: ui-slots / web-react / ui-primitives
runtime assembly / host entity dshClient plugins ×8 (node half = empty apply,
runtime assembly / host entity dsh.client plugins ×8 (node half = empty apply,
webserver Web HTTP carriage client half = src/client/)
│ ctx.plugin(...) ▲ import only apiproxy's /api /client subpaths
▼ │ (type-only + the client base class)
@@ -63,7 +63,7 @@ On the protocol side: TS interfaces (`packages/host/apiproxy/src/api/`, zero Nod
| Layer | Package | Responsibility | Key discipline |
|---|---|---|---|
| Front layer | `dsh-host-apiproxy` | TS/zod definitions (api/) + the fetch abstraction (fetch/: handler + client base class) | Keep it simple — every consumer needs it; importable from Node and browser alike; protocol content in the "Message protocol" sections below; clients must not bypass api through ctx |
| Assembly layer | `dsh-host-runtime` | Plugin composition + ApiProxy integration + the web UI plugin mount (in-memory Loader tree over the eight dshClient packages); home of host-level configuration (defaults/persistenceRoot, future user profile) | Which plugins mount and with what defaults is decided only here; shells must not alter the assembly |
| Assembly layer | `dsh-host-runtime` | Plugin composition + ApiProxy integration + the web UI plugin mount (in-memory Loader tree over the eight dsh.client packages); home of host-level configuration (defaults/persistenceRoot, future user profile) | Which plugins mount and with what defaults is decided only here; shells must not alter the assembly |
| Carrier layer | `dsh-host-webserver` | Web HTTP and upgrade: static serving + `/api/*`→handler forwarding + WebSocket upgrade route + close semantics; plugin bundle endpoint + `__DSH_BOOT__` manifest injection (fed by the web plugin registry) | Web (browser access) only; zero workspace dependencies (the registry arrives by structural injection); Electron does not reuse it |
| Client libraries | `dsh-client-ui-slots` / `dsh-client-web-react` / `dsh-client-ui-primitives` | Slot registry core / ctx↔React glue / pure React atoms | Zero cordis runtime dependency in components; seeded into the loader module table by the shell |
| Client plugins | `dsh-client-connection` / `dsh-client-runtime` / `dsh-client-ui-theme` / `dsh-client-i18n` / `dsh-client-ui-layout` / `dsh-client-ui-sidebar` / `dsh-client-ui-conversation` / `dsh-client-ui-trajectory` | Browser-side cordis plugin tree (wire consumer, core services, theme, i18n, layout, sidebar, conversation, trajectory) — see the web client architecture note | Dual entry (node half = empty apply; implementation in `src/client/`); the consumption face goes exclusively through ApiProxy |
@@ -79,7 +79,7 @@ Packages under `packages/host/*` and `packages/client/*` **must carry the direct
2. **Write an assembly module under `apps/`**: `startHost()` + a client subclass + the application's private signal/print/exit semantics; a mixture never becomes a package — assembly is written in the app.
3. **Import `dsh-host-webserver` only if you need HTTP carriage**, otherwise zero ports.
The two existing applications preserve the division: the Web application mounts Host, carrier, and browser composition, while `dsh run` mounts a direct core runner with zero Host, HTTP, or ports. ACP-class protocol bridges do not follow the client-carrier checklist: they expose core to the external ecosystem and mount directly via `ctx.plugin(entry-point plugin)` without fetch.
The two existing applications preserve the division: the Web application mounts Host, carrier, and browser composition, while `dsh --profile headless` mounts a direct core runner with zero Host, HTTP, or ports. ACP-class protocol bridges do not follow the client-carrier checklist: they expose core to the external ecosystem and mount directly via `ctx.plugin(entry-point plugin)` without fetch.
## Message protocol
@@ -215,7 +215,7 @@ All four quadrant full forms pass through `onEnvelope`; the base implementation
| Subclass | Package | doFetch | Purpose |
|---|---|---|---|
| `InProcessApiClient` | apiproxy itself | the injected `{ fetch }` handler | **The isomorphic point**: `new InProcessApiClient(toFetchHandler(api))` never touches the network yet runs the real wire serialization/zod/SSE framing; carrier tests and callers can exercise the protocol without opening a port, while product `dsh run` drives core directly |
| `InProcessApiClient` | apiproxy itself | the injected `{ fetch }` handler | **The isomorphic point**: `new InProcessApiClient(toFetchHandler(api))` never touches the network yet runs the real wire serialization/zod/SSE framing; carrier tests and callers can exercise the protocol without opening a port, while product `dsh --profile headless` drives core directly |
| `WebApiClient` | dsh-client-connection | `globalThis.fetch` uplink + one same-origin WebSocket downlink per logical stream | the browser client; physical boundary in the [WebSocket downlink carrier](2026-08-04-websocket-downlink-carrier.md) |
| `FixtureApiClient` | dsh-client-connection | unused (protocol-layer override) | serverless UI development (`?fixture`): overrides the `callUnary`/`openMux`/`openHost`/`respond` virtuals and is itself the fake server (frame rpcIds minted by it, semantics self-consistent) |
| IPC bridge subclass (hypothetical example — no such shell exists) | an Electron shell | IPC serialization round trip | would swap only doFetch; contract and base class unchanged |

View File

@@ -9,7 +9,7 @@ Status: implemented
## Problem
需要提供 UI 对接层,除已有 ACPAgent Client Protocol/stdio 基线外,还需要 Webserver、Electron 等其他产品客户端。我们把它们统一称为 Client。希望具备以下能力
- 一个 `dsh` 进程同时支持 `dsh web`(启动)和 `dsh run`headless一个进程两种模式设计预留
- 一个 `dsh` 进程同时支持 `dsh web`(启动)和 `dsh --profile headless`headless一个进程两种模式设计预留
- 在 Electron 中使用与 `dsh web` 相同的 Web 技术启动
那么当前的工程代码需要稳定的分层职责模型,便于以后接入各类 client。
@@ -25,11 +25,11 @@ Status: implemented
- 统一后端协议fetch、HTTP、流式接口等定义和支持见本篇「消息协议」起各节
- `packages/client/*`:包只提供 Client 侧能力,每包单边不混。这里住三类包(两条轴归 [client 插件装载笔记](2026-07-23-client-plugin-loading-model.md) 所有):
- **纯库**`ui-slots``web-react``ui-primitives`,外加内核包 `loader`):普通根入口包,静态打包进壳;前三者播种进模块表。
- **静态到达 entry 包**`connection``runtime``ui-theme``i18n``hmr`):无 `dshClient` 键、无浏览器 bundle——壳把它们的 `src/client/` 半边打进自己的 bundle 并向 `ctx.modules` 登记;它们与其余单元一样,作为 host 独家撰写的图里的 entry 受治理。
- **fetch 到达插件包**`ui-layout``ui-sidebar``ui-conversation``ui-trajectory`):双入口——根入口是 node 半边(空 `apply`,其存在是为了让 host Loader 管辖生命周期、让 web 插件注册表发现 package.json 的 `dshClient` 声明);实现住在 `src/client/` 下,经 `./client` 子路径发布tsdown 闭包工厂 bundle。跨插件消费 `/client` 只限类型;值层面的协作走 cordis 服务。
- **静态到达 entry 包**`connection``runtime``ui-theme``i18n``hmr`):无 `dsh.client` 键、无浏览器 bundle——壳把它们的 `src/client/` 半边打进自己的 bundle 并向 `ctx.modules` 登记;它们与其余单元一样,作为 host 独家撰写的图里的 entry 受治理。
- **fetch 到达插件包**`ui-layout``ui-sidebar``ui-conversation``ui-trajectory`):双入口——根入口是 node 半边(空 `apply`,其存在是为了让 host Loader 管辖生命周期、让 web 插件注册表发现 package.json 的 `dsh.client` 声明);实现住在 `src/client/` 下,经 `./client` 子路径发布tsdown 闭包工厂 bundle。跨插件消费 `/client` 只限类型;值层面的协作走 cordis 服务。
- `apps/` 作为对外导出的应用入口,可以由 Client / Host 混合组装。
- `apps/web``dsh-frontend`)是 vite 应用:`dsh-client-web` 导出的壳表面之上的一层薄 `main.ts`
- `apps/cli``@deepseek-ai/dsh`)分发命令:`dsh web` = Host + webserver + 构建出的 `dsh-frontend` dist`dsh run` = [直接使用核心 AgentSession 的入口](2026-08-09-headless-direct-core-entry-point.md),不含 Host、HTTP 或浏览器层。
- `apps/cli``@deepseek-ai/dsh`)分发命令:`dsh web` = Host + webserver + 构建出的 `dsh-frontend` dist`dsh --profile headless` = [直接使用核心 AgentSession 的入口](2026-08-09-headless-direct-core-entry-point.md),不含 Host、HTTP 或浏览器层。
- 将来的 Electron 应用经由 IPC fetch 载体复用同一套 web client 包。
```
@@ -38,7 +38,7 @@ apps/* (applications: apps/web = vite app, apps/cli = bin dispatch)
packages/host/* packages/client/*
apiproxy front layer: protocol pure libs: ui-slots / web-react / ui-primitives
runtime assembly / host entity dshClient plugins ×8 (node half = empty apply,
runtime assembly / host entity dsh.client plugins ×8 (node half = empty apply,
webserver Web HTTP carriage client half = src/client/)
│ ctx.plugin(...) ▲ import only apiproxy's /api /client subpaths
▼ │ (type-only + the client base class)
@@ -61,7 +61,7 @@ TypeScript 以 solution 根引用的**两个聚合 program** 检查(`tsconfig.
| 层 | 包 | 职责 | 关键纪律 |
|---|---|---|---|
| 前置层 | `dsh-host-apiproxy` | TS/zod 定义 (api/)+ fetch 抽象 (fetch/handler + 客户端基类) | 做简单、所有接入方都要Node/浏览器皆可 import协议内容见下文「消息协议」起各节client 不得经 ctx 绕开 api |
| 装配层 | `dsh-host-runtime` | 插件组合 + ApiProxy 集成 + web UI 插件挂载(覆盖八个 dshClient 包的内存 Loader 树host 级配置归属地defaults/persistenceRoot将来用户 profile | 装什么插件、给什么默认值只在这里定;壳不得改装配 |
| 装配层 | `dsh-host-runtime` | 插件组合 + ApiProxy 集成 + web UI 插件挂载(覆盖八个 dsh.client 包的内存 Loader 树host 级配置归属地defaults/persistenceRoot将来用户 profile | 装什么插件、给什么默认值只在这里定;壳不得改装配 |
| 承载层 | `dsh-host-webserver` | Web HTTP 与 upgrade静态服务 + `/api/*`→handler 转发 + WebSocket upgrade route + close 语义;插件 bundle 端点 + `__DSH_BOOT__` manifest元数据清单注入由 web 插件注册表供给) | Web浏览器访问专用零 workspace 依赖注册表经结构注入到达Electron 不复用它 |
| client 库 | `dsh-client-ui-slots` / `dsh-client-web-react` / `dsh-client-ui-primitives` | slot 注册表核心 / ctx↔React 胶合 / 纯 React 原子组件 | 组件零 cordis 运行时依赖;由壳播种进 loader 模块表 |
| client 插件 | `dsh-client-connection` / `dsh-client-runtime` / `dsh-client-ui-theme` / `dsh-client-i18n` / `dsh-client-ui-layout` / `dsh-client-ui-sidebar` / `dsh-client-ui-conversation` / `dsh-client-ui-trajectory` | 浏览器侧 cordis 插件树wire 消费者、核心服务、主题、i18n、布局、侧栏、对话、轨迹——见 Web 客户端架构笔记 | 双入口node 半边=空 apply实现在 `src/client/`);消费面唯一经 ApiProxy |
@@ -77,7 +77,7 @@ TypeScript 以 solution 根引用的**两个聚合 program** 检查(`tsconfig.
2. **在 `apps/` 下写拼装模块**`startHost()` + 客户端子类 + 该应用私有的信号/打印/退出语义;混合体不建包,拼装写在 app 里。
3. **需要 HTTP 承载才 import `dsh-host-webserver`**,否则零端口。
现有两个应用保持这一区分Web 应用挂载 Host、载体与浏览器组合`dsh run` 挂载直接使用核心服务的 runner不包含 Host、HTTP 或端口。ACP 类协议桥不遵循 client 载体清单:它把 core 暴露给外部生态,直接通过 `ctx.plugin(入口插件)` 挂载,不使用 fetch。
现有两个应用保持这一区分Web 应用挂载 Host、载体与浏览器组合`dsh --profile headless` 挂载直接使用核心服务的 runner不包含 Host、HTTP 或端口。ACP 类协议桥不遵循 client 载体清单:它把 core 暴露给外部生态,直接通过 `ctx.plugin(入口插件)` 挂载,不使用 fetch。
## 消息协议
@@ -213,7 +213,7 @@ export type ResponseValue<K> =
| 子类 | 所在包 | doFetch | 用途 |
|---|---|---|---|
| `InProcessApiClient` | apiproxy 本包 | 注入的 `{ fetch }` handler | **同构点**`new InProcessApiClient(toFetchHandler(api))` 全程不过网络但真跑 wire 序列化/zod/SSE 帧;载体测试与调用方可以在不打开端口的情况下运行这套协议,而产品 `dsh run` 直接驱动 core |
| `InProcessApiClient` | apiproxy 本包 | 注入的 `{ fetch }` handler | **同构点**`new InProcessApiClient(toFetchHandler(api))` 全程不过网络但真跑 wire 序列化/zod/SSE 帧;载体测试与调用方可以在不打开端口的情况下运行这套协议,而产品 `dsh --profile headless` 直接驱动 core |
| `WebApiClient` | dsh-client-connection | `globalThis.fetch` 上行 + 每逻辑流一条同源 WebSocket 下行 | 浏览器客户端;物理边界见 [WebSocket 下行载体](2026-08-04-websocket-downlink-carrier.md) |
| `FixtureApiClient` | dsh-client-connection | 不用(协议层覆写) | 无 server 的 UI 开发(`?fixture`):覆写 `callUnary`/`openMux`/`openHost`/`respond` 虚方法,自己就是假 server帧 rpcId 由它 mint语义自洽 |
| IPC 桥子类(假想示例——尚无此形态) | Electron 壳 | IPC 序列化往返 | 只需换 doFetch约定/基类零改 |

View File

@@ -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-19-gui-web-client-architecture.md
2026-07-19-gui-web-client-architecture.md: 82b2f85708c423748954644d4991e2d54d42874a
2026-07-19-gui-web-client-architecture.zh.md: c37252d1db291cae11db2a615c9e4005ece717da
2026-07-19-gui-web-client-architecture.md: bc61aab894d587820ef4cb568b6439993a27d30d
2026-07-19-gui-web-client-architecture.zh.md: 1f5bafe1dff878b5ca5ffcbdb9ed8ca38a863c9f

View File

@@ -30,7 +30,7 @@ Both ends run cordis. The host is a cordis plugin tree; the browser runs a secon
## The client cordis tree and the loading chain
The loading chain — the two package kinds (plain vs dshClient plugin), the module-system/plugin-governor split, the two-phase boot over the host-authored entry graph with revisions, and hot reload — is owned by the [client plugin loading note](2026-07-23-client-plugin-loading-model.md). The load-bearing facts for this document: the browser boots the same vendored `@cordisjs/plugin-loader` as the host with a client module system (`ctx.modules`, `packages/client/modules`) filling its `internal` contract; every unit with product behavior is an entry in the host-authored `__DSH_BOOT__` graph — every production plugin package (infrastructure included) carries the `dshClient` declaration and arrives as a fetched `./client` tsdown closure bundle, `immediately` rows differing only in boot phase-one prefetch, while plain packages (react family, cordis, the not-yet-promoted libraries) stay shell-bundled, seeded, and invisible to the graph; bundles execute `window.__ModuleLoader__.load({ id, factory })` and their `require` is answered from the lazy CJS module table (seed words + registered factories, materialized and memoized on first require — cross-plugin value imports are a build error, cooperation goes through cordis services); plugin CSS is inlined in the bundle and injected as `<style data-plugin="<id>">` at materialization (CSS Modules hashing + ownership tag = isolation, removal on reload); hot reload is live in dev graphs — the webserver stat-polls the bundles it serves and broadcasts `rebuilt` SSE frames, and the `client-hmr` plugin swaps one fiber per frame. The settled flip (`loader.await()` + an all-ACTIVE sweep) still switches the shell from the loading page to the real UI in one pass — settled means every entry is created and every fiber reached ACTIVE, with FAILED/PENDING fibers listed loud; there is no partial-availability mode (progressive rendering is deferred work).
The loading chain — the two package kinds (plain vs dsh.client plugin), the module-system/plugin-governor split, the two-phase boot over the host-authored entry graph with revisions, and hot reload — is owned by the [client plugin loading note](2026-07-23-client-plugin-loading-model.md). The load-bearing facts for this document: the browser boots the same vendored `@cordisjs/plugin-loader` as the host with a client module system (`ctx.modules`, `packages/client/modules`) filling its `internal` contract; every unit with product behavior is an entry in the host-authored `__DSH_BOOT__` graph — every production plugin package (infrastructure included) carries the `dsh.client` declaration and arrives as a fetched `./client` tsdown closure bundle, `immediately` rows differing only in boot phase-one prefetch, while plain packages (react family, cordis, the not-yet-promoted libraries) stay shell-bundled, seeded, and invisible to the graph; bundles execute `window.__ModuleLoader__.load({ id, factory })` and their `require` is answered from the lazy CJS module table (seed words + registered factories, materialized and memoized on first require — cross-plugin value imports are a build error, cooperation goes through cordis services); plugin CSS is inlined in the bundle and injected as `<style data-plugin="<id>">` at materialization (CSS Modules hashing + ownership tag = isolation, removal on reload); hot reload is live in dev graphs — the webserver stat-polls the bundles it serves and broadcasts `rebuilt` SSE frames, and the `client-hmr` plugin swaps one fiber per frame. The settled flip (`loader.await()` + an all-ACTIVE sweep) still switches the shell from the loading page to the real UI in one pass — settled means every entry is created and every fiber reached ACTIVE, with FAILED/PENDING fibers listed loud; there is no partial-availability mode (progressive rendering is deferred work).
Type universes stay split at the aggregate level — `tsconfig.host.json` is the host program and `tsconfig.client.json` the client program, both referenced by the solution root `tsconfig.json` — because both sides merge cordis `Context` under the same keys (`sessions`, `loader`) with different services; client packages consume the wire vocabulary through pure type subpaths (`@deepseek-ai/dsh-session/types` and kin) so no host augmentation rides into the client program.
@@ -108,7 +108,7 @@ Domain implementation files never import a sibling domain; shared surfaces route
## How to develop
- **A new UI feature** = a new plugin package: declare `dshClient` (+ `inject` topology) in package.json, write the browser half under `src/client/` (apply mounts services/stores and registers slots), keep the node half an empty apply unless there is host logic, build with the shared preset. Add the plugin to the host config; the manifest and loading follow automatically.
- **A new UI feature** = a new plugin package: declare `dsh.client` (+ `inject` topology) in package.json, write the browser half under `src/client/` (apply mounts services/stores and registers slots), keep the node half an empty apply unless there is host logic, build with the shared preset. Add the plugin to the host config; the manifest and loading follow automatically.
- **A new slot**: see the [slot system standard note](2026-07-22-slot-type-chain-implementation.md) — merge the contract into `SlotMap`, declare it in the parent entry's `children`, render through the auto-injected `renderSlot` prop. Never export components globally.
- **Consuming a new frame type**: transport-only session frames → Session's dispatch switch; host-level frames → the Manager routing table; logged conversation business events → a Definition plus a keyed view renderer, without a Session business branch.
- **Where does this state live**: business data (events, streaming, pending) → always the object layer; what the parent knows → owner props at the renderSlot site; private to one component (scroll, search text, expansion) → component state; shared across entries or surviving remounts (selection, drafts, panel widths) → an entry-declared store ([slot system standard](2026-07-22-slot-type-chain-implementation.md)).

View File

@@ -30,7 +30,7 @@ Status: implemented
## client cordis 树与装载链
装载链——两类包(普通包 vs dshClient 插件)、模块系统/插件治理器之分、host 独家撰写的带修订号 entry 图之上的双层 boot、热重载——归 [client 插件装载笔记](2026-07-23-client-plugin-loading-model.md) 所有。本篇赖以立足的事实:浏览器启动与 host 相同的 vendored `@cordisjs/plugin-loader`,由 client 模块系统(`ctx.modules``packages/client/modules`)填上其 `internal` 约定;凡带产品行为的单元都是 host 独家撰写的 `__DSH_BOOT__` 图里的 entry——每个生产插件包含基础设施都携带 `dshClient` 声明、以 fetch 到达的 `./client` tsdown 闭包 bundle 供给,`immediately` 行的差别仅在 boot 第一层预取而普通包react 家族、cordis、尚未升格的库保持打进壳、已播种、对图不可见bundle 执行 `window.__ModuleLoader__.load({ id, factory })`,其 `require` 由 lazy CJS 模块表应答(种子词条 + 已登记工厂,首次 require 时物化并记忆化——跨插件值 import 是构建错误,协作走 cordis 服务);插件 CSS 内联在 bundle 里、物化时注入为 `<style data-plugin="<id>">`CSS Modules 哈希 + 归属标记 = 隔离,重载时移除);热重载已在 dev 图落地——webserver 对自己供给的 bundle 做 stat 轮询并广播 `rebuilt` SSE 帧,`client-hmr` 插件每帧换掉一个 fiber。settled 翻转(`loader.await()` + 一次全 ACTIVE 扫描)依旧让壳从 loading 页一次切换到真 UI——settled 意味着每个 entry 已创建、每个 fiber 都到达 ACTIVEFAILED/PENDING 的 fiber 被大声列出;不存在部分可用模式(渐进渲染为后置工作)。
装载链——两类包(普通包 vs dsh.client 插件)、模块系统/插件治理器之分、host 独家撰写的带修订号 entry 图之上的双层 boot、热重载——归 [client 插件装载笔记](2026-07-23-client-plugin-loading-model.md) 所有。本篇赖以立足的事实:浏览器启动与 host 相同的 vendored `@cordisjs/plugin-loader`,由 client 模块系统(`ctx.modules``packages/client/modules`)填上其 `internal` 约定;凡带产品行为的单元都是 host 独家撰写的 `__DSH_BOOT__` 图里的 entry——每个生产插件包含基础设施都携带 `dsh.client` 声明、以 fetch 到达的 `./client` tsdown 闭包 bundle 供给,`immediately` 行的差别仅在 boot 第一层预取而普通包react 家族、cordis、尚未升格的库保持打进壳、已播种、对图不可见bundle 执行 `window.__ModuleLoader__.load({ id, factory })`,其 `require` 由 lazy CJS 模块表应答(种子词条 + 已登记工厂,首次 require 时物化并记忆化——跨插件值 import 是构建错误,协作走 cordis 服务);插件 CSS 内联在 bundle 里、物化时注入为 `<style data-plugin="<id>">`CSS Modules 哈希 + 归属标记 = 隔离,重载时移除);热重载已在 dev 图落地——webserver 对自己供给的 bundle 做 stat 轮询并广播 `rebuilt` SSE 帧,`client-hmr` 插件每帧换掉一个 fiber。settled 翻转(`loader.await()` + 一次全 ACTIVE 扫描)依旧让壳从 loading 页一次切换到真 UI——settled 意味着每个 entry 已创建、每个 fiber 都到达 ACTIVEFAILED/PENDING 的 fiber 被大声列出;不存在部分可用模式(渐进渲染为后置工作)。
类型宇宙在聚合层拆分——`tsconfig.host.json` 是 host program、`tsconfig.client.json` 是 client program二者由 solution 根 `tsconfig.json` 引用,因为两侧都在相同键(`sessions``loader`)上对 cordis `Context` 做声明合并且服务不同client 包经纯类型子路径(`@deepseek-ai/dsh-session/types`消费协议词汇host 侧的声明合并不会搭车进入 client program。
@@ -108,7 +108,7 @@ src/client/
## 怎么开发
- **新 UI 功能** = 新插件包package.json 声明 `dshClient`+ `inject` 拓扑),浏览器半边写在 `src/client/`apply 挂服务/建 store、注册 slot无 host 逻辑时 node 半边保持空 apply用共享预设构建。把插件加进 host 配置manifest 与装载随之自动跟上。
- **新 UI 功能** = 新插件包package.json 声明 `dsh.client`+ `inject` 拓扑),浏览器半边写在 `src/client/`apply 挂服务/建 store、注册 slot无 host 逻辑时 node 半边保持空 apply用共享预设构建。把插件加进 host 配置manifest 与装载随之自动跟上。
- **新 slot**:见 [slot 体系标准笔记](2026-07-22-slot-type-chain-implementation.md)——约定合并进 `SlotMap`,在父 entry 的 `children` 里声明,经自动注入的 `renderSlot` prop 渲染。永不全局导出组件。
- **消费新帧类型**:纯传输 session frame → Session 分发 switchhost 级 frame → Manager 路由表;已记录的 conversation 业务事件 → Definition 加 keyed view renderer不增加 Session 业务分支。
- **状态住哪**:业务数据(事件、流式、待答)→ 永远对象层;父知道的 → renderSlot 现场的 owner props单组件私有滚动、搜索词、展开集→ 组件状态;跨 entry 共享或跨重挂载存活(选中、草稿、面板宽)→ entry 声明的 store[slot 体系标准](2026-07-22-slot-type-chain-implementation.md))。

View File

@@ -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: fff96f65a21d9527c8fa49589b178c490bacdd5a
2026-07-23-client-plugin-loading-model.zh.md: 0c0c95ba7ebffca33c2c2d1dec13f745c4316f43
2026-07-23-client-plugin-loading-model.md: 21289c5dcebc7244e98c602e9f10bac7eb365bc3
2026-07-23-client-plugin-loading-model.zh.md: c3c4ef598c1d92d4ebec7b9691d31cf33c7c62a2

View File

@@ -1,4 +1,4 @@
# Agent Note: Client plugin loading — plain packages, dshClient plugins, and the two-phase boot
# Agent Note: Client plugin loading — plain packages, dsh.client plugins, and the two-phase boot
Status: implemented
@@ -18,22 +18,22 @@ The lower layer supplies four capabilities: externals (the platform list), remot
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.
On top of that, client and host plugins register and load consistently: a package declares `dshClient` once, the host scans the declaration into the boot graph, and the same Loader semantics govern entries on both sides.
On top of that, client and host plugins register and load consistently: a package declares `dsh.client` once, the host scans the declaration into the boot graph, and the same Loader semantics govern entries on both sides.
The first-generation client loader (`createClientLoader`) hand-wrote both layers in one function. The fusion left no unload/reload path (loads were one-shot, style tags never removed), hand-copied dependency lists that had already drifted across three files, and a module-table backdoor for cross-plugin imports that duplicated cordis's service mechanism while making load order a correctness constraint. The structure below replaced it.
## Decision
### Two package kinds; `dshClient` means plugin, period
### Two package kinds; `dsh.client` means plugin, period
What makes a package a plugin? One rule: **a package is a plugin package once its consumption is cordis dependency injection; until then it is a plain package.** How code reaches the page is not part of the taxonomy — arrival follows from the kind instead of defining it.
- **Plain packages** are the absolute base the module system itself needs, plus libraries not yet converted to DI: the react family, cordis, `@deepseek-ai/dsh-client-modules` (the module system itself — it can never be a plugin, because modules precede all modules), the web shell kernel, and — for now — ui-slots, web-react, ui-primitives. Plain packages are shell-bundled, seeded into the module table, and invisible to the host graph.
- **Plugin packages** are everything else. Each one carries a `dshClient` manifest declaration (`{ platform, inject, immediately? }`) and one uniform shape: the shared tsdown preset emits `lib/client.js`, and `exports["./client"]` points at that bundle. Each is a governed entry of the host-authored graph. The current set is connection, runtime, ui-theme, i18n, hmr (dev graphs only), ui-layout, ui-sidebar, ui-conversation, ui-model-selector, ui-question, and ui-trajectory.
- **Plugin packages** are everything else. Each one carries a `dsh.client` manifest declaration (`{ platform, inject, immediately? }`) and one uniform shape: the shared tsdown preset emits `lib/client.js`, and `exports["./client"]` points at that bundle. Each is a governed entry of the host-authored graph. The current set is connection, runtime, ui-theme, i18n, hmr (dev graphs only), ui-layout, ui-sidebar, ui-conversation, ui-model-selector, ui-question, and ui-trajectory.
The manifest owns the package's loading contract: its `inject` dependency edges, plus the optional `immediately` prefetch mark (absent means lazy). The composing app owns only the roster and the `--dev` switch.
To add a plugin package: declare `dshClient`, emit the `./client` bundle through the shared preset, add the name to the composing app's roster. Nothing else changes hands.
To add a plugin package: declare `dsh.client`, emit the `./client` bundle through the shared preset, add the name to the composing app's roster. Nothing else changes hands.
When does a plain package become a plugin? The upgrade law, recorded so the migration path stays honest: **a plain package becomes a plugin package when its consumers switch to cordis DI, not before.** Three promotions are queued: ui-slots (the slots machinery now living in runtime — SlotsService, the renderer contract, the root slot), web-react (the renderer install moving into its own `apply`), and ui-primitives (once components are served through slots/services). Until then they stay plain, and their symbol exports stay ordinary static imports.
@@ -67,10 +67,10 @@ 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, and `--dev` appends the `client-hmr` row in code (`AppCLIEntry`) before the host activation audit so the same check covers it. 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 `dshClient` declarations and composes `window.__DSH_BOOT__`: `{ rev, entries: [{ id, url, rev, inject?, immediately? }] }`. The `inject` edges and the `immediately` mark come from manifests, never hand-copied. 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.
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? }] }`. The `inject` edges and the `immediately` mark come from manifests, never hand-copied. 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).
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 dshClient package existing 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.
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 shell builds the module system over the graph, then prefetches every `immediately` row in parallel. Prefetch loads the external script and registers its factory only. A single row's prefetch failure is swallowed here: phase two's import retries the load and owns the loud failure, so one bad row cannot mask the others. `immediately` is a prefetch mark — not a barrier, not an identity. The package declares it, the registry carries it into the row. The infrastructure plugins (connection, runtime, ui-theme, i18n, plus hmr) declare it; UI plugins simply arrive on demand.
@@ -86,7 +86,7 @@ Why is the roster yml rows and not a scan? Because which plugins compose into a
Whether hot reload is active is a composition decision: dev compositions mount the `client-hmr` row (a normal plugin package, appended by `--dev`) whose node half brings the bundle watch and the SSE channel; prod compositions mount nothing and have neither.
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.clientModuleHost.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, its package list dshClient-discovered by 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. It reads bundle paths from `ctx.clientModuleHost.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.
On the browser side, the driver reloads one plugin per frame, serialized:
@@ -113,7 +113,7 @@ The support boundary, stated honestly. Reload is coarse by design: fresh fiber,
| `dsh-client-ui-slots` | slot registry core | plain, seeded | promote to plugin; receive runtime's slots machinery |
| `dsh-client-web-react` | ctx↔React glue | plain, seeded | promote to plugin; renderer install moves into its apply |
| `dsh-client-ui-primitives` | base components | plain, seeded | promote to plugin (components via slots/services) |
| `dsh-client-connection` | wire layer | plugin (dshClient + bundle), declares `immediately` | transport swap (Electron IPC carrier) |
| `dsh-client-connection` | wire layer | plugin (`dsh.client` + bundle), declares `immediately` | transport swap (Electron IPC carrier) |
| `dsh-client-runtime` | session object layer + slots service + store engine | plugin, declares `immediately` | keeps shrinking toward a pure session object layer |
| `dsh-client-ui-theme` | theme tokens/service | plugin, declares `immediately`, plus the `./styles/*` source channel | Theme Registry (separate ruling) |
| `dsh-client-i18n` | I18nService | plugin, declares `immediately` | per-deployment locale composition |
@@ -132,7 +132,7 @@ Roster: it lives in the web bundle's config tree (`packages/bundle/web-app/cordi
| Rejected | One-line reason |
|---|---|
| Two-axis taxonomy (entry × arrival) with non-dshClient infrastructure packages | Erased manifest dependency edges (inject leaked to the composer), split the plugin shape in two, blinded the purity gate to half the plugins |
| Two-axis taxonomy (entry × arrival) with infrastructure packages lacking `dsh.client` | Erased manifest dependency edges (inject leaked to the composer), split the plugin shape in two, blinded the purity gate to half the plugins |
| Keep evolving the hand-written loader into a governor | Re-implements entry/fiber lifecycle the vendored Loader owns; HMR would have no shared skeleton with the host side |
| Reuse `@cordisjs/plugin-hmr` in the browser | ~80% solves problems the browser doesn't have (fs watching, deep graph coloring, Node's dual caches); the reload skeleton is copied as a shape |
| Module federation | Independently built remote bundles are exactly the form vite federation does not support |

View File

@@ -1,4 +1,4 @@
# Agent Note: client 插件装载——普通包、dshClient 插件与双阶段 boot
# Agent Note: client 插件装载——普通包、dsh.client 插件与双阶段 boot
Status: implemented
@@ -18,22 +18,22 @@ host 侧cordis 插件装载站在 Node 的模块机制之上——require cac
插件 bundle 独立构建在 Vite 模块图之外。若把响应文本塞进内联 script浏览器只能看到一次动态源码执行网络资源、生成 bundle、TypeScript/TSX 源码之间没有标准 sourcemap 链,性能 profile 与 stack 只能落到生成后的 `client.js`;模块系统还要持有整份源码文本,并把同一项到达职责拆成 fetch 与 execute 两道传输边界。
在此之上client 与 host 插件以一致的方式注册与装载:包声明一次 `dshClient`host 把声明扫描进 boot 图,同一套 Loader 语义在两侧治理 entry。
在此之上client 与 host 插件以一致的方式注册与装载:包声明一次 `dsh.client`host 把声明扫描进 boot 图,同一套 Loader 语义在两侧治理 entry。
第一代 client loader`createClientLoader`)把这两层手写进了同一个函数。这一融合留下的是:没有卸载/重载路径装载一次性style 标签从不移除)、在三个文件间人肉抄写且早已漂移的依赖清单、一条供跨插件 import 走的模块表后门——既复制了 cordis 的服务机制,又把装载顺序变成正确性约束。下文的结构取代了它。
## Decision
### 两类包;`dshClient` 即插件,别无他义
### 两类包;`dsh.client` 即插件,别无他义
什么让一个包成为插件?只有一条规则:**一个包的消费方式一旦是 cordis 依赖注入,它就是插件包;在此之前它是普通包。**代码怎么到达页面不属于分类体系——到达方式由包的类别推得,而不是反过来定义类别。
- **普通包**是模块系统自身所需的绝对基座,加上尚未转成 DI 的库react 家族、cordis、`@deepseek-ai/dsh-client-modules`模块系统本身——它永远不可能是插件因为模块先于一切模块、web 壳内核以及——暂时——ui-slots、web-react、ui-primitives。普通包打进壳 bundle、播种进模块表、对 host 图不可见。
- **插件包**是其余一切。每个都携带 `dshClient` manifest元数据清单声明`{ platform, inject, immediately? }`)和同一种统一形态:共享 tsdown 预设产出 `lib/client.js``exports["./client"]` 指向该 bundle。每个都是 host 独家撰写的图里受治理的 entry。当前包括connection、runtime、ui-theme、i18n、hmr仅进 dev 图、ui-layout、ui-sidebar、ui-conversation、ui-model-selector、ui-question、ui-trajectory。
- **插件包**是其余一切。每个都携带 `dsh.client` manifest元数据清单声明`{ platform, inject, immediately? }`)和同一种统一形态:共享 tsdown 预设产出 `lib/client.js``exports["./client"]` 指向该 bundle。每个都是 host 独家撰写的图里受治理的 entry。当前包括connection、runtime、ui-theme、i18n、hmr仅进 dev 图、ui-layout、ui-sidebar、ui-conversation、ui-model-selector、ui-question、ui-trajectory。
manifest 拥有包的装载约定:它的 `inject` 依赖边,加可选的 `immediately` 预取标记(缺省即 lazy。负责组合的 app 只拥有名册与 `--dev` 开关。
新增一个插件包:声明 `dshClient`,经共享预设产出 `./client` bundle把包名加进负责组合的 app 的名册。除此之外无需任何交接。
新增一个插件包:声明 `dsh.client`,经共享预设产出 `./client` bundle把包名加进负责组合的 app 的名册。除此之外无需任何交接。
普通包何时升格为插件?升级法则,记录在案让迁移路径保持诚实:**普通包在其消费方改用 cordis DI 之时升格为插件包,绝不提前。**三项升格在排队ui-slots现居 runtime 的 slots 机件——SlotsService、渲染器约定、root slot、web-react渲染器安装移入自己的 `apply`、ui-primitives组件经 slot/服务供给之时)。在那之前它们保持普通包身份,符号导出保持普通的静态 import。
@@ -67,10 +67,10 @@ vendored Loader 经其 `internal` 约定消费模块系统——唯一调用点
**host 侧——组合这张图。**
1. 负责组合的 app`apps/cli`)把名册作为普通行放进它的 `cordis.yml` 配置树——client 插件包与每个 host 插件一样是 entry 行,`--dev` 由代码(`AppCLIEntry`)在 host 激活检查之前追加 `client-hmr` 行,使同一项检查覆盖它。名册行 import 失败由 `assertEntriesLoaded` 捕获fiber reject 的行则由 `assertEntriesActivated` 报告原始 stack[host boot 决策](2026-07-24-web-config-tree-boot-and-transport-layering.md))。
2. `dsh-client-modules` 的 node 半(该包是双面的:浏览器半就是模块表)扫描 loader entry 的 package.json `dshClient` 声明,组合出 `window.__DSH_BOOT__``{ rev, entries: [{ id, url, rev, inject?, immediately? }] }``inject` 边与 `immediately` 标记都来自 manifest永不人肉抄写。它会拒绝没有已构建 `./client` bundle 的已声明插件,并把它们的 package/path 行归到一条源码构建要求下畸形声明字段同样会让激活失败host 检查会从 FAILED fiber 报告这两类错误。
2. `dsh-client-modules` 的 node 半(该包是双面的:浏览器半就是模块表)扫描 loader entry 的 package.json `dsh.client` 声明,组合出 `window.__DSH_BOOT__``{ rev, entries: [{ id, url, rev, inject?, immediately? }] }``inject` 边与 `immediately` 标记都来自 manifest永不人肉抄写。它会拒绝没有已构建 `./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 自己注册)。
为什么名册是 yml 行而不是扫描?因为哪些插件组合进一次部署是组合决策,不是包属性——一个 dshClient 包存在于仓库里不代表这次部署要挂载它扫描发现无从替人做这个决定node 半只扫描配置树实际挂载了的东西。
为什么名册是 yml 行而不是扫描?因为哪些插件组合进一次部署是组合决策,不是包属性——一个 dsh.client 包存在于仓库里不代表这次部署要挂载它扫描发现无从替人做这个决定node 半只扫描配置树实际挂载了的东西。
**第一阶段——模块面。**壳在图之上建起模块系统,然后并行预取每个 `immediately` 行。预取即加载外部脚本,只登记工厂。单行预取失败在这里被吞下:第二阶段 import 时会重试加载并拥有那次大声失败,因此一个坏行藏不住其他行。`immediately` 是预取标记——不是屏障不是身份。包声明它注册表把它带进图行。基础设施插件connection、runtime、ui-theme、i18n外加 hmr声明它UI 插件则径直按需到达。
@@ -86,7 +86,7 @@ vendored Loader 经其 `internal` 约定消费模块系统——唯一调用点
热重载是否启用是一项组合决策dev 组合挂载 `client-hmr` 行(一个常规的插件包,由 `--dev` 追加),其 node 半带来 bundle 监视与 SSEServer-Sent Events通道prod 组合不挂载,两者皆无。
重建好的 bundle 怎么变成重载信号hmr 的 node 半自己观察——没有构建器来通知它。它从 `ctx.clientModuleHost.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` 也是同一原因;轮询间隔是一个经校验的配置字段(默认 500msdispose资源释放会清掉那一个定时器。重建 bundle 则是任意一个 tsdown watch 进程的事——`scripts/dev-web.ts` 仍作为 watch 构建入口保留,其包清单在启动时扫描 `packages/*/*/package.json` 按 dshClient 发现——构建器与 host 共享零协议。写一半的 bundle 被撕裂读取会自愈:写入完成期间 stat 持续变化,下一个轮询节拍会再次重哈希并广播最终的 rev。
重建好的 bundle 怎么变成重载信号hmr 的 node 半自己观察——没有构建器来通知它。它从 `ctx.clientModuleHost.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` 也是同一原因;轮询间隔是一个经校验的配置字段(默认 500msdispose资源释放会清掉那一个定时器。重建 bundle 则是任意一个 tsdown watch 进程的事——`scripts/dev-web.ts` 仍作为 watch 构建入口保留,其包清单在启动时扫描 `packages/*/*/package.json` 按 dsh.client 发现——构建器与 host 共享零协议。写一半的 bundle 被撕裂读取会自愈:写入完成期间 stat 持续变化,下一个轮询节拍会再次重哈希并广播最终的 rev。
浏览器侧,驱动插件每帧重载一个插件,串行执行:
@@ -113,7 +113,7 @@ vendored Loader 经其 `internal` 约定消费模块系统——唯一调用点
| `dsh-client-ui-slots` | slot 注册表核心 | 普通包,已播种 | 升格为插件;接收 runtime 的 slots 机件 |
| `dsh-client-web-react` | ctx↔React 胶水 | 普通包,已播种 | 升格为插件;渲染器安装移入其 apply |
| `dsh-client-ui-primitives` | 基础组件 | 普通包,已播种 | 升格为插件(组件经 slot/服务供给) |
| `dsh-client-connection` | wire 层 | 插件dshClient + bundle声明 `immediately` | 传输替换Electron IPC 载体) |
| `dsh-client-connection` | wire 层 | 插件dsh.client + bundle声明 `immediately` | 传输替换Electron IPC 载体) |
| `dsh-client-runtime` | 会话对象层 + slots 服务 + store 引擎 | 插件,声明 `immediately` | 持续缩向纯会话对象层 |
| `dsh-client-ui-theme` | 主题 token/服务 | 插件,声明 `immediately`,外加 `./styles/*` 源码通道 | Theme Registry另行裁定 |
| `dsh-client-i18n` | I18nService | 插件,声明 `immediately` | 按部署组合语言包 |
@@ -132,7 +132,7 @@ wire 两侧跑着同一份治理实现;浏览器特有的表面只是一套模
| Rejected | One-line reason |
|---|---|
| 两轴分类体系entry × 到达),基础设施包不带 dshClient | 抹掉了 manifest 依赖边inject 泄漏给组合方)、把插件形态拆成两种、让纯度门禁对一半插件失明 |
| 两轴分类体系entry × 到达),基础设施包不带 dsh.client | 抹掉了 manifest 依赖边inject 泄漏给组合方)、把插件形态拆成两种、让纯度门禁对一半插件失明 |
| 继续把手写 loader 演化成治理器 | 重新实现 vendored Loader 已拥有的 entry/fiber 生命周期HMR 将与 host 侧毫无共享骨架 |
| 在浏览器复用 `@cordisjs/plugin-hmr` | 约 80% 在解决浏览器没有的问题fs 监听、深度图着色、Node 的双缓存);只按形状抄用其重载骨架 |
| 模块联邦module federation | 独立构建的远端 bundle 恰是 vite 联邦不支持的形态 |

View File

@@ -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: 9bf44e398da66ee286fc9bbc1496c002606d1606
2026-07-24-web-config-tree-boot-and-transport-layering.zh.md: 23d9bc790c792438cb699952ee802e1ffa89de88
2026-07-24-web-config-tree-boot-and-transport-layering.md: c00d0c544cfd04927d23eac53720cb969a44e044
2026-07-24-web-config-tree-boot-and-transport-layering.zh.md: 4fe315bb7673ba219286b176123ccbbe08f02f0d

View File

@@ -12,7 +12,7 @@ English | [中文](2026-07-24-web-config-tree-boot-and-transport-layering.zh.md)
## Decision
**Composition is one flat assembled tree.** `apps/cli/config/base.cordis.yml` plus `apps/cli/config/web.cordis.yml` holds every row — the host runtime (32 rows), the `api-gateway` row, the `webserver` row, and the `dshClient` rows (the browser roster; the modules row is simultaneously a host row). No spine bundle: every plugin is one row and every config field is yml-editable. That stance later became repository-wide, with the rows both surfaces share factored into `apps/cli/config/base.cordis.yml` and each surface reduced to an overlay ([shared-base overlays](../simplification/2026-07-29-shared-base-config-overlays.md)). `--dev` appends the `dsh-client-hmr` row in code before the settle audit — prod and dev differ by exactly that row. Row order carries no load semantics; activation is service-availability driven. The shared audit rejects imports with no fiber, awaits only failed fibers to recover original activation errors, and reports services that leave a fiber `PENDING`; before throwing, it marks those exact rejection reasons through one process checkpoint so `installFailLoud` coalesces Loader's duplicate notification while unrelated unhandled rejections remain fatal. The Node app-boot artifact embeds `@cordisjs/plugin-include` while leaving `@cordisjs/plugin-loader` external, so the include's `EntryTree` and the host bind to one Loader peer instead of splitting a config tree across two Loader implementations.
**Composition is one flat assembled tree.** `apps/cli/config/base.cordis.yml` plus `apps/cli/config/web.cordis.yml` holds every row — the host runtime (32 rows), the `api-gateway` row, the `webserver` row, and the `dsh.client` rows (the browser roster; the modules row is simultaneously a host row). No spine bundle: every plugin is one row and every config field is yml-editable. That stance later became repository-wide, with the rows both surfaces share factored into `apps/cli/config/base.cordis.yml` and each surface reduced to an overlay ([shared-base overlays](../simplification/2026-07-29-shared-base-config-overlays.md)). `--dev` appends the `dsh-client-hmr` row in code before the settle audit — prod and dev differ by exactly that row. Row order carries no load semantics; activation is service-availability driven. The shared audit rejects imports with no fiber, awaits only failed fibers to recover original activation errors, and reports services that leave a fiber `PENDING`; before throwing, it marks those exact rejection reasons through one process checkpoint so `installFailLoud` coalesces Loader's duplicate notification while unrelated unhandled rejections remain fatal. The Node app-boot artifact embeds `@cordisjs/plugin-include` while leaving `@cordisjs/plugin-loader` external, so the include's `EntryTree` and the host bind to one Loader peer instead of splitting a config tree across two Loader implementations.
**Boot glue is a class pair.** `AppCLIEntry` (apps/cli) and `AppWebEntry` (the shell kernel) hold only what must exist independently of cordis: argv facts, the composed patch set, the parsed boot manifest, the module system instance, loading-page handles — everything else lives in plugins. `AppCLIEntry.run()` is three stages: layered env (ambient > cwd `.env` > `$DSH_HOME/.env`, closing the defect above) → patch composition → Loader include boot plus the activation audit. `AppWebEntry.run()` mirrors it browser-side: parse `window.__DSH_BOOT__` into a `BootManifest` (two views: npm-package rows for the module table, cordis-plugin rows for entry composition; malformed wire throws), build the module system, render the loading page, prefetch the `immediately` tier in parallel with Context/Loader setup, **await the prefetch before creating entries** (materialization is `tree.import`'s synchronous require, unprotected by fiber inject waiting; cross-package require edges such as i18n → runtime/client need every immediately-tier factory registered first — an empirically found 1025% boot race otherwise), adopt the modules entry, create the graph rows, settle, sweep.

View File

@@ -12,7 +12,7 @@ Status: implemented
## 决策
**组合结果是一棵平铺配置树。** `apps/cli/config/base.cordis.yml``apps/cli/config/web.cordis.yml` 共同持有全部行——host 运行时32 行)、`api-gateway` 行、`webserver` 行、`dshClient` 行(浏览器 rostermodules 行同时是 host 行)。不做 spine bundle每插件一行、每个 config 字段 yml 可改。这一立场后来推广到全仓:两个 surface 共享的配置项被抽取进 `apps/cli/config/base.cordis.yml`,各 surface 则收敛为一份 overlay[共享 base overlay](../simplification/2026-07-29-shared-base-config-overlays.md))。`--dev` 在 settle audit 之前由代码追加 `dsh-client-hmr` 行——prod 与 dev 的全部差异就是这一行。行序无装载语义;激活由服务可用性驱动。共享 audit 会拒绝没有 fiber 的 import、仅等待失败的 fiber 以恢复原始激活错误,并报告让 fiber 停在 `PENDING` 的服务;抛出错误前,审计会通过一个进程级检查点标记这些 rejection 的确切原因,从而让 `installFailLoud` 将 Loader 的重复通知合并为一次,而无关的未处理 rejection 仍然致命。Node app-boot 产物内嵌 `@cordisjs/plugin-include`,但将 `@cordisjs/plugin-loader` 保持为外部依赖,因此 include 的 `EntryTree` 与 host 会绑定到同一个 Loader peer而不会让一棵配置树横跨两个 Loader 实现。
**组合结果是一棵平铺配置树。** `apps/cli/config/base.cordis.yml``apps/cli/config/web.cordis.yml` 共同持有全部行——host 运行时32 行)、`api-gateway` 行、`webserver` 行、`dsh.client` 行(浏览器 rostermodules 行同时是 host 行)。不做 spine bundle每插件一行、每个 config 字段 yml 可改。这一立场后来推广到全仓:两个 surface 共享的配置项被抽取进 `apps/cli/config/base.cordis.yml`,各 surface 则收敛为一份 overlay[共享 base overlay](../simplification/2026-07-29-shared-base-config-overlays.md))。`--dev` 在 settle audit 之前由代码追加 `dsh-client-hmr` 行——prod 与 dev 的全部差异就是这一行。行序无装载语义;激活由服务可用性驱动。共享 audit 会拒绝没有 fiber 的 import、仅等待失败的 fiber 以恢复原始激活错误,并报告让 fiber 停在 `PENDING` 的服务;抛出错误前,审计会通过一个进程级检查点标记这些 rejection 的确切原因,从而让 `installFailLoud` 将 Loader 的重复通知合并为一次,而无关的未处理 rejection 仍然致命。Node app-boot 产物内嵌 `@cordisjs/plugin-include`,但将 `@cordisjs/plugin-loader` 保持为外部依赖,因此 include 的 `EntryTree` 与 host 会绑定到同一个 Loader peer而不会让一棵配置树横跨两个 Loader 实现。
**boot 胶水由两个类组成。** `AppCLIEntry`apps/cli`AppWebEntry`(壳内核)只持有那些必须独立于 cordis、提前存在的东西argv 事实、合成的 patch 集、解析出的 boot manifest元数据清单、模块系统实例、loading 页句柄——其余一律进插件。`AppCLIEntry.run()` 三段:分层 envambient > cwd `.env` > `$DSH_HOME/.env`,顺手关掉上述缺陷)→ patch 合成 → Loader include boot 加 activation audit。`AppWebEntry.run()` 在浏览器侧镜像它:把 `window.__DSH_BOOT__` 解析成 `BootManifest`双视角npm 包行给模块表、cordis 插件行给 entry 组合;畸形 wire 大声抛)、建模块系统、渲染 loading 页、immediately 层预取与 Context/Loader 准备并行、**create entry 之前等预取齐**(物化是 `tree.import` 的同步 require不受 fiber inject 等待保护i18n → runtime/client 这类跨包 require 边要求 immediately 层工厂全部注册完——否则有实测 1025% 的 boot 竞态)、收编 modules entry、逐一创建图行、settle、sweep。

View File

@@ -1,33 +0,0 @@
# Agent Note: Experimental and internal package group
Status: implemented
English | [中文](2026-07-28-experimental-plugin-package-group.zh.md)
## Problem
The [package hierarchy](../../../../packages/README.md) groups plugins by product role, but it cannot distinguish release packages from prototypes or internal-only packages. The team needs an obvious shared place for useful work that is not part of the official release.
## Decision
The subtree rules in [`packages/experimental/AGENTS.md`](../../../../packages/experimental/AGENTS.md) make `packages/experimental/<pkg>/` the required home for Cordis plugin packages whose whole public contract is experimental or internal-only. Package names remain `@deepseek-ai/dsh-<pkg>`.
The group is the team's in-repository place to share engineering and product-manager prototypes: members can discover, run, review, and extend one another's work against the real plugin graph without implying product support.
Official releases exclude this directory. A package enters a release only after moving to its product-role group; release packages cannot take runtime dependencies on packages here. Examples may use them, while any other runtime dependent also belongs here. Tests may use them as development dependencies.
Experimental packages carry no stability, compatibility, migration, or support promise: they may change APIs, configuration, or data, or disappear without deprecation or migration. Internal-only packages may define narrower internal contracts but make no public release promise. Neither status relaxes engineering, security, documentation, lifecycle, testing, or snapshot requirements.
The pending `@deepseek-ai/dsh-tui-session-changes` `/diff` viewer and `/btw` plugin are examples governed by this rule. Promotion into an official release requires explicit review of the public contract, limitations, test evidence, and a named owner accepting stable-package obligations.
## Alternatives considered
**Keep experimental and internal-only packages in product-role groups with README labels.** Labels are easy to miss and cannot enforce dependency boundaries.
**Treat every package as experimental until the first tagged release.** This provides no durable incubation boundary.
**Develop prototypes and internal packages elsewhere.** This loses the real plugin graph, examples, snapshots, and lifecycle checks needed to evaluate them.
## Consequences
The path makes release exclusion and dependency blast radius visible while retaining the real plugin graph for team sharing. It gives up product-role colocation and creates path churn on promotion, while the npm name remains stable. The subtree rules, repository [current-owner/current-need rule](../../../../packages/AGENTS.md), and unchanged engineering gates limit junk-drawer growth. Because official release tooling does not yet exist, contributor policy enforces the exclusion; when such tooling is added, the directory is its required exclusion boundary.

View File

@@ -1,33 +0,0 @@
# Agent Note: 实验性与内部专用包分组
Status: implemented
[English](2026-07-28-experimental-plugin-package-group.md) | 中文
## 问题
[包层级结构](../../../../packages/README.md)按产品角色对插件分组,但无法区分发布包、原型和内部专用包。团队需要一个明确的共享位置,存放不属于官方发布版本的有价值成果。
## 决策
[`packages/experimental/AGENTS.md`](../../../../packages/experimental/AGENTS.md) 中的子树规则要求所有公开约定整体处于实验状态或仅限内部使用的 Cordis 插件包位于 `packages/experimental/<pkg>/`。包名仍为 `@deepseek-ai/dsh-<pkg>`
该分组供团队在仓库内共享工程人员和产品经理制作的原型:成员可以基于真实插件图发现、运行、评审并扩展彼此的原型,但这不代表产品会提供支持。
官方发布版本不包含此目录。包只有移入对应的产品角色分组后才会纳入发布版本;发布包不得在运行时依赖此处的包。示例可以使用这些包;其他任何运行时依赖方也必须位于此处。测试可以将它们用作开发依赖。
实验性包不提供稳定性、兼容性、迁移或支持保证:其 API、配置或数据可以变更包也可以移除均不提供弃用期或迁移路径。内部专用包可以定义范围更窄的内部约定但不作公开发布承诺。无论哪种状态都不降低仓库对工程、安全、文档、生命周期、测试或快照的要求。
尚待完成的 `@deepseek-ai/dsh-tui-session-changes` `/diff` 查看器和 `/btw` 插件都受这项规则约束。将包提升为稳定包并纳入官方发布版本,需要明确评审其公开约定、限制和测试证据,并指定一名愿意承担稳定包义务的负责人。
## 考虑过的替代方案
**将实验性和内部专用包留在产品角色分组中,并用 README 标注。** 标注容易被忽略,也无法强制执行依赖边界。
**首个带标签的版本发布前,将所有包都视为实验性。** 这无法提供持久的孵化边界。
**在其他位置开发原型和内部专用包。** 这会失去评估它们所需的真实插件图、示例、快照和生命周期检查。
## 后果
该路径明确标示不纳入发布版本的包及其依赖影响范围,同时保留供团队共享成果的真实插件图。代价是这些包无法与同产品角色的包共置,提升并纳入发布版本时还会产生路径变动,但 npm 包名保持稳定。子树规则、仓库已有的[「必须有当前负责人和实际需求」规则](../../../../packages/AGENTS.md)以及保持不变的工程门禁,可限制该分组无序膨胀。由于官方发布工具尚不存在,目前由贡献者政策执行这项排除规则;添加发布工具后,必须以该目录为排除边界。

View File

@@ -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-dsh-source-launch-tsx-esm.md
2026-07-29-dsh-source-launch-tsx-esm.md: ed22e51d59a25db130b3760ce484c116bade4348
2026-07-29-dsh-source-launch-tsx-esm.zh.md: bdd549092eb30f7749c8f7561068daafe3548b28
2026-07-29-dsh-source-launch-tsx-esm.md: 5cf4a227f388a1ac8315594af4e0256864ef17f5
2026-07-29-dsh-source-launch-tsx-esm.zh.md: b5a52b3d01840337c0091310e50d8fac34245519

View File

@@ -35,4 +35,4 @@ The node-compat CI matrix (Node 22.19 and 26) gains `dsh-source-launch-smoke` (`
- One launch vector across the whole engines range, including future Node lines that change native TypeScript support; the smoke gate enforces it per matrix line.
- TypeScript transformation is delegated to tsx/esbuild again, reversing the prior note's goal of proving Node-native transformation; that goal is unreachable while vendored sources use non-erasable syntax and Node ships no transform mode.
- The runtime declared-dependency enforcement in source launches is gone; undeclared workspace imports now surface only through static gates or built-mode resolution failures.
- Startup improves ~0.4s over the full tsx default (`demo:headless` now aliases the same `dsh run` source launch; ACP keeps `--import tsx` because its graph was not audited for CJS-hook dependence and its launch latency is not on the interactive path).
- Startup improves ~0.4s over the full tsx default (`demo:headless` now aliases the same `dsh --profile headless` source launch; ACP keeps `--import tsx` because its graph was not audited for CJS-hook dependence and its launch latency is not on the interactive path).

View File

@@ -35,4 +35,4 @@ node-compat CI 矩阵Node 22.19 与 26新增 `dsh-source-launch-smoke``
- 整个 engines 范围(包括未来改变原生 TypeScript 支持的 Node 版本线)只有一个启动向量;冒烟门禁按矩阵行强制执行。
- TypeScript 转换重新委托给 tsx/esbuild逆转了前一篇 Agent Note「证明 Node 原生转换可用」的目标;在 vendor 源码使用不可擦除语法且 Node 不再提供 transform 模式的情况下,该目标不可达。
- 源码启动中的运行时依赖声明强制不复存在;未声明的 workspace import 现在只能通过静态门禁或构建模式的解析失败暴露。
- 启动相比完整 tsx 默认形态快约 0.4s`demo:headless` 现为同一条 `dsh run` 源码启动命令的别名ACP 保留 `--import tsx`,因为它的依赖图尚未就 CJS 钩子依赖性做审计,且其启动延迟不在交互路径上)。
- 启动相比完整 tsx 默认形态快约 0.4s`demo:headless` 现为同一条 `dsh --profile headless` 源码启动命令的别名ACP 保留 `--import tsx`,因为它的依赖图尚未就 CJS 钩子依赖性做审计,且其启动延迟不在交互路径上)。

View File

@@ -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: 3c37bce05bacd6af800a76ac93fb691b896a6772
2026-07-29-package-regrouping.zh.md: 68903ff1fad6a975c4445fe8971c8fe0dd40117f
2026-07-29-package-regrouping.md: 30fc45a122263350b4a2ad1998850f631c20f9b8
2026-07-29-package-regrouping.zh.md: a3a9a11ec71b7f894dcea7c733eb39a80b71ac50

View File

@@ -58,7 +58,7 @@ The moves landed as pure `git mv` moves, so rename detection carries the history
A group move did not touch: npm names, imports, `cordis.yml` configs, snapshot fixtures, the `pnpm-workspace.yaml`/`tsdown` globs (both `packages/*/*`), or the Python runtime manifest — all reference packages by npm name.
`client/` and `host/` were out of scope and are unchanged. The `experimental/` group proposal (PR #844) is orthogonal — a release-boundary container, not a clustering decision.
`client/` and `host/` were out of scope and are unchanged.
## Alternatives considered

View File

@@ -58,7 +58,7 @@ Status: implemented
组移动未触及npm 包名、import、`cordis.yml` 配置、快照 fixture测试前置数据`pnpm-workspace.yaml``tsdown` 的 glob都是 `packages/*/*`),以及 Python 运行时 manifest元数据清单——它们全部按 npm 包名引用包。
`client/``host/` 不在本次范围内,保持不变。`experimental/` 组提案PR #844)与本案正交:它是发布边界容器,不是聚类决策。
`client/``host/` 不在本次范围内,保持不变。
## Alternatives considered

View File

@@ -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-client-locale-full-rollout.md
2026-07-30-client-locale-full-rollout.md: 09baf5876029295f7a80b6a0fe6a6395d98f406c
2026-07-30-client-locale-full-rollout.zh.md: a2f9b619be9928a713d5dbf527d7c2278e84bd1f
2026-07-30-client-locale-full-rollout.md: 0faf4e0424e037b59b24d32f7fa987ac36497691
2026-07-30-client-locale-full-rollout.zh.md: 5c26c2d5e7b75b89675b0b0d9ca3f147d2152bc8

View File

@@ -25,7 +25,7 @@ After the typed locale standard seat landed (`locale:` on register → framework
**Derivation layers stay pure; localization happens at render.** ui-workspace's `relativeTime` returns structured `{unit, n}` composed with dictionary templates by the renderer; blank sessions and the Ungrouped bucket keep their stored titles, with the renderer substituting localized copy off the `blank` flag / absent `workspaceId`; **blank rows are excluded from search entirely** (a bilingual display title cannot match a single-language query stably). Dates use no Intl: format templates live in the dictionaries (message clock `clock.md`/`clock.ymd`, workspace hover `date.ymd`) and the formatters take `t` as a parameter, staying pure.
**Test and e2e doctrine**: `makeTranslate(...dicts)` (dsh-client-test-runtime) mirrors the service lookup chain (first-dict-wins, key fallback, `{name}` interpolation); component specs stub the `t` seat with it, typed against real props seats. Web e2e uniformly opens through `newEnglishPage` (pins `dsh.locale=en` before boot) and the built-boot snapshot pins the same — goldens are immune to localization migrations; the settings language-switch scenario bypasses the helper and opens a `zh-CN` browser, since the initial locale follows `navigator` ([browser-derived initial locale](../feature/2026-07-31-browser-derived-initial-locale.md)).
**Test and e2e doctrine**: `makeTranslate(...dicts)` (dsh-client-test-runtime) mirrors the service lookup chain (first-dict-wins, key fallback, `{name}` interpolation); component specs stub the `t` seat with it, typed against real props seats. Web e2e uniformly opens through `newEnglishPage` (an `en-US` browser) and the built-boot snapshot pins the same navigator language—goldens are immune to localization migrations; the settings language-switch scenario bypasses the helper and opens a `zh-CN` browser, since the provisional locale follows `navigator` before an explicit Host preference arrives ([browser-derived initial locale](../feature/2026-07-31-browser-derived-initial-locale.md)).
The "apply layer subscribes to `locale/change` and re-registers for fresh labels" mechanism in the [settings/locale/theme layering note](../../proposed/architecture/2026-07-25-client-settings-locale-theme.md) is superseded by this decision (thunk + revision lifecycle).

View File

@@ -25,7 +25,7 @@ typed locale 标准席位(`locale:` 注册声明 → 框架注入强类型 `t`
**派生层保持纯函数,本地化只在渲染层**ui-workspace 的 `relativeTime` 返回结构化 `{unit, n}` 由渲染组合字典模板blank 会话/未分组桶的存储标题不变,渲染按 `blank` 标志/`workspaceId` 缺席替换本地化文案;**搜索态 blank 行一律排除**(双语标题无法与单语查询稳定匹配)。日期不引 Intl格式模板进字典消息时钟 `clock.md`/`clock.ymd`workspace hover `date.ymd`),格式化函数吃 `t` 参数保持纯。
**测试与 e2e 口径**`makeTranslate(...dicts)`dsh-client-test-runtime镜像服务查找链首个命中字典胜出、key 兜底、`{name}` 插值),组件测试的 `t` 桩统一用它并以真实 props 席位定型。web e2e 统一通过 `newEnglishPage` 打开boot 前钉 `dsh.locale=en`built-boot 快照同样钉 en——预期输出不受本地化迁移影响settings 语言切换用例绕开该 helper 并开启 `zh-CN` 浏览器,因为初始 locale 跟随 `navigator`[由浏览器推导初始 locale](../feature/2026-07-31-browser-derived-initial-locale.md))。
**测试与 e2e 口径**`makeTranslate(...dicts)`dsh-client-test-runtime镜像服务查找链首个命中字典胜出、key 兜底、`{name}` 插值),组件测试的 `t` 桩统一用它并以真实 props 席位定型。web e2e 统一通过 `newEnglishPage``en-US` 浏览器)打开built-boot 快照 同样固定 navigator 语言golden 因而不受语言迁移影响settings 语言切换用例绕开该 helper 并开启 `zh-CN` 浏览器,因为在显式 Host 偏好到达前,暂定 locale 跟随 `navigator`[由浏览器推导初始 locale](../feature/2026-07-31-browser-derived-initial-locale.md))。
[settings/locale/theme 分层 Note](../../proposed/architecture/2026-07-25-client-settings-locale-theme.md) 中「apply 层订阅 `locale/change` 重注册刷新 label」的机制已被本决定取代thunk + revision 生命周期)。

View File

@@ -1,47 +0,0 @@
# Agent Note: Package-manager-native repository cache
Status: implemented
English | [中文](2026-07-30-package-manager-native-repository-cache.zh.md)
## Problem
A standalone Harness app cannot rely on a developer-owned SDK project to declare and install repository dependencies. Loading a configured GitHub repository therefore needs a persistent fetch, preparation, and cache boundary, but implementing Git transport, hosted-source syntax, package preparation, and a content store inside DSH would duplicate a package manager. Requiring a separately installed package manager would make a config-only feature depend on host setup.
The cache also needs an update identity. A mutable branch name cannot both remain permanently cached and reflect later commits without an independent refresh protocol.
## Decision
Vendored `@cordisjs/plugin-loader/repository` exports `RepositoryCache`, a generic Node-only package helper with no DSH plugin-format knowledge. Keeping it on a subpath prevents browser consumers of the Loader's main entry from traversing Node filesystem and child-process imports. The caller supplies a package-manager-native source specifier and a cache root. DSH-specific callers own accepted source syntax, path selection, and the cache-root location; the [SDK project dependency workflow](../../proposed/feature/2026-07-17-sdk-follow-up-capabilities.md#external-cordis-plugin-installation) remains a separate path owned by the developer project's selected package manager.
The Loader carries an exact runtime dependency on `pnpm@11.7.0` and invokes that package's JavaScript entry with the current Node executable. It never discovers a global executable or delegates through Corepack. Each cache miss creates an isolated project with one dependency named `repository`; pnpm owns Git/GitHub resolution, fetching, its content-addressed store, dependency installation, and lifecycle scripts in the repository's dependency graph.
The isolated workspace sets `dangerouslyAllowAllBuilds: true`. A configured repository and its dependency graph are trusted executable code: lifecycle scripts may run before DSH reads any declared assets. The child receives ordinary host process state needed by Git and pnpm, but ambient credential-shaped (`KEY`, `PASSWORD`, `SECRET`, `TOKEN`) variables are removed. No OAuth, token forwarding, or private-repository authentication contract is added.
The SHA-256 of the exact specifier names the cache entry. Concurrent same-process requests share one task. Installation occurs in a sibling temporary directory; only a successful install with a package directory and marker is atomically renamed into the final key. Failed staging is removed, and a competing process's already-published valid entry wins. A later process validates the marker and package directory before returning the stable `node_modules/repository` path.
An identical specifier permanently reuses its published entry. The caller changes the ref or another part of the specifier to request a new generation; the cache does not poll remotes, reinterpret mutable refs, expire entries, or garbage-collect old generations.
## Alternatives considered
**Implement GitHub download, archive extraction, preparation, and caching directly.** Rejected under the [dependency policy](../process/2026-07-26-dependencies-over-hand-rolling.md): pnpm already owns hosted Git syntax, Git execution, lifecycle policy, and a shared content store. A second resolver would add more code while still needing package semantics.
**Require `pnpm` on `PATH` or invoke Corepack.** Rejected because changing one app config must be sufficient on every supported installation. Pinning and shipping the CLI also makes the preparation policy reviewable and independent of the host's package-manager version.
**Resolve a branch or tag again on every startup.** Rejected because it turns startup into a network refresh, changes code without a config diff, and makes rollback depend on remote state. Explicit ref changes preserve auditability even when a user deliberately chooses a mutable ref.
**Disable repository lifecycle scripts.** Rejected because common plugin repositories need a declarative `prepare` step to validate and package their plugin subdirectory. The trust boundary is explicit configuration of executable source, not an incomplete illusion that only static files can run.
**Introduce a Cordis repository service.** Rejected because cache lookup has no runtime contribution registry or provider variation. A small helper lets the later host own Cordis lifecycle and HMR without adding a service contract prematurely.
## Consequences
- Standalone apps carry pnpm's approximately 18.6 MB unpacked runtime instead of requiring a global tool or owning a Git/package implementation.
- A repository author may use ordinary package preparation, and a malicious configured repository or dependency can execute code with the scrubbed child environment and the user's filesystem authority.
- Exact specifiers make startup deterministic after the first successful install; changing cached code requires a config/ref change.
- Failed installs leave no published cache entry and may be retried. Published corruption fails loud instead of silently reinstalling under the same identity.
- Cache generations consume disk until a future explicit cache-management policy removes them.
## Testing
`packages/boot/app-boot/tests/repository-cache.spec.ts` covers same-process single-flight, cross-instance cache reuse, exact-specifier separation, failed-stage cleanup and retry, and boundary validation. Its real local-Git case invokes the bundled pnpm, runs the fixture repository's `prepare` script, and reads the prepared file from the installed cache entry without network access.

View File

@@ -1,47 +0,0 @@
# Agent Note: 包管理器原生仓库缓存
Status: implemented
[English](2026-07-30-package-manager-native-repository-cache.md) | 中文
## 问题
独立运行的 Harness 应用不能依赖开发者自有的 SDK 工程来声明并安装仓库依赖。因此,加载配置中的 GitHub 仓库需要一道持久的获取、准备与缓存边界;但如果在 DSH 内实现 Git 传输、托管来源语法、包准备流程和内容存储,就会重复实现包管理器。若要求用户另行安装包管理器,则只需修改配置即可使用的功能还会依赖宿主环境的额外配置。
缓存还需要明确更新标识。若没有独立的刷新协议,可变分支名无法既永久缓存,又反映后续 commit。
## 决策
vendor 中的 `@cordisjs/plugin-loader/repository` 导出 `RepositoryCache`:一个不包含 DSH 插件格式知识、仅限 Node 使用的通用包辅助工具。把它保留在子路径上,可以避免 Loader 主入口的浏览器消费方在解析依赖时遍历到 Node 文件系统和子进程 import。调用方提供包管理器原生的来源 specifier 和缓存根目录。DSH 专属调用方负责规定可接受的来源语法、路径选择与缓存根目录位置;[SDK 工程依赖工作流](../../proposed/feature/2026-07-17-sdk-follow-up-capabilities.md#external-cordis-plugin-installation)仍是另一条路径,由开发者工程选定的包管理器负责。
Loader 将 `pnpm@11.7.0` 作为固定版本的运行时依赖,并使用当前 Node 可执行文件调用该包的 JavaScript 入口。它绝不探测全局可执行文件,也不经 Corepack 调用。每次缓存未命中都会创建一个隔离工程,其中只有一个名为 `repository` 的依赖Git 与 GitHub 来源的解析和获取、pnpm 自身的内容寻址 store、依赖安装以及仓库依赖图中的生命周期脚本均由 pnpm 负责。
隔离工作区设置 `dangerouslyAllowAllBuilds: true`。用户配置的仓库及其依赖图都属于受信任的可执行代码DSH 读取任何已声明资产之前,生命周期脚本就可能运行。子进程会收到 Git 与 pnpm 所需的常规宿主进程状态,但会移除环境中名称形似凭据(`KEY``PASSWORD``SECRET``TOKEN`)的变量。该机制不新增 OAuth、token 转发或私有仓库认证约定。
缓存项以精确 specifier 的 SHA-256 命名。同一进程内针对相同 specifier 的并发请求共享一项任务。安装在同级临时目录中进行;只有安装成功且存在包目录和标记时,系统才会把暂存目录原子重命名为最终键对应的目录。失败的暂存目录会被删除;如果另一进程已发布有效项,则以该项为准。后续进程会先校验标记与包目录,再返回稳定的 `node_modules/repository` 路径。
相同的 specifier 会永久复用已发布项。调用方通过修改 ref 或 specifier 的其他部分来请求新的缓存代次;缓存不会轮询远端、重新解释可变 ref、让条目过期也不会垃圾回收旧代次。
## 曾考虑的替代方案
**直接实现 GitHub 下载、归档解压、准备与缓存。** 根据[依赖政策](../process/2026-07-26-dependencies-over-hand-rolling.md)不予采纳pnpm 已负责托管 Git 语法、Git 执行、生命周期政策和共享内容存储。第二套解析器会增加更多代码,却仍需实现包语义。
**要求 `pnpm` 位于 `PATH` 上,或调用 Corepack。** 不予采纳:在每种受支持的安装形态中,只修改一份应用配置就必须足以启用该功能。固定并随应用分发 CLI命令行界面还能使准备政策可供评审并与宿主的包管理器版本无关。
**每次启动都重新解析分支或 tag。** 不予采纳:这会把启动变成网络刷新,在配置 diff 未变化时更改代码,并让回滚依赖远端状态。即使用户有意选择可变 ref显式修改 ref 仍能保持可审计性。
**禁用仓库生命周期脚本。** 不予采纳:常见插件仓库需要声明式 `prepare` 步骤来校验并打包插件子目录。信任边界是显式配置可执行来源,而不是营造一种不完整的假象,仿佛只有静态文件能够运行。
**引入 Cordis 仓库服务。** 不予采纳:缓存查找没有运行时贡献注册表,也不存在提供方变体。小型 helper 让后续宿主负责 Cordis 生命周期与 HMR热模块替换无需过早新增服务约定。
## 后果
- 独立应用随附 pnpm 约 18.6 MB 的解压后运行时,不要求全局工具,也无需自行实现 Git 与包处理。
- 仓库作者可以使用常规包准备流程;恶意的已配置仓库或依赖可以在经过上述清理的子进程环境中,以用户的文件系统权限执行代码。
- 精确 specifier 使首次安装成功后的启动具有确定性;更改缓存代码必须修改配置或 ref。
- 安装失败不会留下已发布缓存项,可以再次重试。已发布缓存损坏时会明确报错,而不会在同一标识下静默重装。
- 缓存代次会持续占用磁盘,直到未来有明确的缓存管理政策将其移除。
## 测试
`packages/boot/app-boot/tests/repository-cache.spec.ts` 覆盖同进程 single-flight、跨实例缓存复用、精确 specifier 隔离、失败暂存清理与重试,以及边界校验。其真实本地 Git 用例会调用随附的 pnpm运行 fixture测试前置数据仓库的 `prepare` 脚本,并在不访问网络的情况下,从已安装缓存项中读取准备后的文件。

View File

@@ -1,49 +0,0 @@
# Agent Note: Static repository Plugin format
Status: implemented
English | [中文](2026-07-30-static-repository-plugin-format.zh.md)
## Problem
A repository that already contains reusable skills or an MCP server declaration should be usable by standalone Harness applications without becoming a Harness SDK project or rewriting its existing layout. Popular repositories must be able to add one `.dsh-plugin` directory while keeping their current skills and `.mcp.json` elsewhere in the tree. These portable static contributions still need to reuse the existing skill and MCP lifecycle owners when the same trusted package also carries native Cordis code.
The [package-manager-native repository cache](2026-07-30-package-manager-native-repository-cache.md) prepares an exact package source but intentionally knows nothing about DSH formats. This layer therefore needs a package-manager-compatible authoring format, a deterministic prepared artifact, and a Cordis composition that stays transactional under Loader disposal and replacement.
## Decision
`@deepseek-ai/dsh-repository-plugin` owns the static contribution subformat inside a `.dsh-plugin` package: skill roots and one common `.mcp.json`. Its package metadata uses `package.json#dsh.skills` for relative skill-root paths and `package.json#dsh.mcpServers` for the relative MCP document path. Each path may leave `.dsh-plugin` to reuse repository content but must remain beneath the directory containing that `.dsh-plugin`; a nested selectable Plugin therefore owns the adjacent subtree above its package without gaining access to unrelated host paths. The package may additionally declare the explicit code entry owned by the [trusted repository package decision](2026-08-08-trusted-repository-package-code.md), and at least one code or static contribution is required.
The `.dsh-plugin` package declares the published `@deepseek-ai/dsh-repository-plugin` package as a development dependency and a non-empty `scripts.prepack` that invokes its `dsh-plugin-prepare` executable. During Git installation, pnpm installs that dependency from the selected package's own manifest; `prepack` runs after dependency installation and before pnpm packs a selected subdirectory, including a Plugin nested inside another package-manager workspace. The package may build its code first. The helper validates metadata and source types, strictly parses `.mcp.json`, copies static assets into `dsh-plugin-assets`, and writes `dsh-plugin.mjs`; the source loader revalidates the installed package's helper-bearing lifecycle metadata before importing that wrapper. A static-only package still receives an import-free wrapper containing its normalized manifest, service-derived `inject` list, and delegation to the `dsh-repository-plugin` Loader builtin. The dependency and workspace-isolation rationale is in the [Git source preparation repair](../bug-fix/2026-08-08-npm-backed-git-repository-plugin-preparation.md).
Loading the DSH package registers that builtin as an effect. A generated wrapper mounts the builtin as its child with `import.meta.url`, so all contributions belong to the wrapper fiber and disappear on Loader removal or rollback. The builtin revalidates the prepared manifest and path containment before reading assets. It composes the existing implementations rather than registering skills or MCP tools itself.
Each prepared skill set mounts `dsh-skill-local` with a unique `repository:<package-name>` provider name, only the copied custom roots, and watching disabled. `dsh-skill-local` therefore gains two general configuration fields: `providerName` and `includeDefaultRoots`. Their defaults preserve its existing single local provider; repository instances set a distinct name and exclude project/user roots so multiple instances neither collide nor duplicate host-local discovery.
Each `.mcp.json` server becomes one existing `dsh-mcp-client` child. The adapter accepts the common root `{ "mcpServers": ... }`; stdio definitions allow only optional `type: "stdio"`, `command`, `args`, and `env`, while HTTP definitions allow only `type: "http"`, `url`, and `headers`. Exact `${NAME}` process-environment references expand at runtime, after cache preparation; missing names fail Plugin load. HTTP maps to the client's Streamable HTTP transport, and stdio uses the prepared package directory as `cwd`. The existing client alone owns connection attempts, failure logging, remote tool synchronization, tool calls, and disconnects. Repository instances enable strict startup, so an initial connection, discovery, or tool-registration failure rejects the repository Loader generation; non-strict standalone clients retain the logged successful-plugin/no-tools behavior.
Unknown MCP fields reject. This intentionally excludes OAuth, `auth` objects, `CLAUDE_PLUGIN_ROOT`, and a broader Claude compatibility contract. Commands, hooks, agents, rules, and other foreign manifest conventions are not inferred from static repository layout; DSH-native behavior uses the explicit trusted Cordis entry. Repository subdirectory selection and GitHub source configuration belong to the [standalone app integration](../feature/2026-07-30-config-only-repository-plugins.md), not this static adapter.
## Alternatives considered
**Discover an entry from `main`, `exports`, or repository layout.** Rejected because static assets do not imply that a package's ordinary entry is a Cordis Plugin. Trusted code loading is explicit through `dsh.entry` and remains outside this static adapter's ownership.
**Teach generated wrappers to implement skills and MCP directly.** Rejected because copied runtime code would drift from `dsh-skill-local` and `dsh-mcp-client`, especially their provider invalidation, tool synchronization, failure, and teardown contracts.
**Import Harness packages from each generated wrapper.** Rejected because repository packages should not resolve or version the application's internal dependency graph. A Loader builtin supplies one app-owned implementation and keeps generated wrappers import-free.
**Watch prepared repository assets.** Rejected because an exact repository cache generation is immutable. Ref, subdirectory, or configuration changes select a new generation; a second watcher would create an unowned refresh identity.
**Make every MCP connect failure a Loader update failure.** Rejected because optional standalone MCP clients deliberately contain startup failures and expose no tools. The MCP client instead owns an explicit strict-startup option, which repository adapters enable for their declared servers.
## Consequences
- Existing skill/MCP repositories can add a small `.dsh-plugin/package.json` without relocating their assets or adopting an SDK project.
- Prepared static output is deterministic glue, while an optional `dsh.entry` and the configured repository lifecycle remain trusted executable package-manager input rather than a sandbox.
- Multiple repository Plugins coexist through provider names and ordinary MCP server-name uniqueness; duplicate names fail through their existing registries and participate in Loader rollback.
- Cached source edits do not appear live. Another exact source/ref/path/config selection is required.
- Adding another portable static contribution kind requires an explicit format and DSH-owned runtime consumer; DSH-native behavior uses the separate explicit code entry.
## Testing
Focused tests prepare skills and MCP metadata, prove a static-only wrapper contains no imports, reject Work IQ-style OAuth fields, map Expo-style HTTP and DataJunction-style stdio plus environment values, and exercise missing variables. A real Loader test mounts a generated wrapper through the registered builtin, reads its skill through `ctx.skills`, removes the Loader entry, and observes provider cleanup. The CI built-entry acceptance invokes `dsh run` with a GitHub source pinned to the pull request head and observes the copied skill alongside the trusted code and MCP proofs owned by the superseding decision.

View File

@@ -1,49 +0,0 @@
# Agent Note: 静态 repository Plugin 格式
状态:已实现
[English](2026-07-30-static-repository-plugin-format.md) | 中文
## 问题
一个已经包含可复用 skills 或 MCP server 声明的仓库,应当能被独立 Harness 应用使用,而不必先变成 Harness SDK 项目,也不应被迫改写现有布局。常见仓库只需新增一个 `.dsh-plugin` 目录,同时仍可把原有 skills 与 `.mcp.json` 放在仓库其他位置。当同一个受信任包还携带原生 Cordis 代码时,这些可移植静态贡献仍需复用现有的 skill 与 MCP 生命周期所有者。
[Package-manager-native repository cache](2026-07-30-package-manager-native-repository-cache.md) 会准备一个精确 package source但有意不了解任何 DSH 格式。因此本层需要一种兼容 package manager 的创作格式、确定性的已准备产物,以及在 Loader dispose 和替换期间仍保持事务性的 Cordis 组合。
## 决策
`@deepseek-ai/dsh-repository-plugin` 负责 `.dsh-plugin` 包内的静态贡献子格式skill 根和一个通用 `.mcp.json`。其包元数据使用 `package.json#dsh.skills` 声明相对 skill 根路径,使用 `package.json#dsh.mcpServers` 声明相对 MCP 文档路径。每条路径都可以离开 `.dsh-plugin` 以复用仓库内容,但必须留在包含该 `.dsh-plugin` 的目录之下;因此,一个嵌套且可选择的插件可以拥有其包上方相邻的子树,却不能访问无关宿主路径。该包还可以声明由[受信任 repository 包决策](2026-08-08-trusted-repository-package-code.md)负责的显式代码入口,并且至少需要一种代码或静态贡献。
`.dsh-plugin` 包将已发布的 `@deepseek-ai/dsh-repository-plugin` 包声明为开发依赖,并声明非空 `scripts.prepack` 来调用其 `dsh-plugin-prepare` 可执行文件。在 Git 安装期间pnpm 会按所选包自身的 manifest元数据清单安装该依赖`prepack` 会在依赖安装后、pnpm 打包选定子目录前运行,即使插件嵌套在另一个包管理器工作区内也不例外。包可以先构建其代码。该辅助程序会校验元数据与源码类型,严格解析 `.mcp.json`,把静态资源复制到 `dsh-plugin-assets`,并写入 `dsh-plugin.mjs`;源码 loader 会在导入该包装层前重新校验已安装包的生命周期元数据是否包含辅助命令。仅含静态贡献的包仍会获得无 import 包装层,其中包含规范化 manifest、由服务派生的 `inject` 列表,以及对 `dsh-repository-plugin` Loader builtin 的委托。依赖与 workspace 隔离的设计依据见[Git 源准备修复](../bug-fix/2026-08-08-npm-backed-git-repository-plugin-preparation.md)。
加载 DSH package 会以 effect 方式注册该 builtin。生成的包装模块使用 `import.meta.url` 把 builtin 挂载为自己的子级,因此所有贡献都归属于包装 fiber并在 Loader 移除或回滚时消失。Builtin 会在读取资源前重新校验已准备 manifest 与路径包含关系。它只组合现有实现,而不自行注册 skills 或 MCP 工具。
每份已准备 skill 集合都会挂载 `dsh-skill-local`,使用唯一的 `repository:<package-name>` 提供方名称、仅包含复制后的自定义根,并禁用监视。因此 `dsh-skill-local` 新增两个通用配置字段:`providerName``includeDefaultRoots`。默认值保持原有单一本地提供方行为repository 实例设置不同名称并排除项目/用户根,使多个实例既不冲突,也不会重复宿主本地发现。
`.mcp.json` 中的每个 server 都变成一个现有 `dsh-mcp-client` 子级。适配层接受通用根对象 `{ "mcpServers": ... }`stdio 定义只允许可选的 `type: "stdio"``command``args``env`HTTP 定义只允许 `type: "http"``url``headers`。严格的 `${NAME}` 进程环境变量引用在运行时、cache 准备之后展开;缺失变量会使 Plugin 加载失败。HTTP 映射到 client 的 Streamable HTTP transportstdio 使用已准备 package 目录作为 `cwd`。只有现有 client 负责连接尝试、失败日志、远端工具同步、工具调用和断开。Repository 实例会启用严格启动,因此初始连接、发现或工具注册失败会拒绝 repository Loader generation非严格的独立 client 则保留“记录日志、Plugin 成功但不注册工具”的行为。
未知 MCP 字段会被拒绝。这里有意排除 OAuth、`auth` 对象、`CLAUDE_PLUGIN_ROOT` 和更广泛的 Claude 兼容约定。命令、hook、agent智能体、规则和其他外来 manifest 约定不会从静态 repository 布局中推断出来DSH 原生行为使用显式的受信任 Cordis 入口。Repository 子目录选择与 GitHub 源配置属于[独立应用集成](../feature/2026-07-30-config-only-repository-plugins.md),而不是本静态适配器。
## 考虑过的替代方案
**从 `main`、`exports` 或 repository 布局中发现入口。** 拒绝,因为静态资源并不表示包的普通入口就是 Cordis 插件。受信任代码通过 `dsh.entry` 显式加载,不属于该静态适配器的职责。
**让生成包装模块直接实现 skills 和 MCP。** 拒绝,因为复制的运行时代码会与 `dsh-skill-local``dsh-mcp-client` 漂移,尤其是提供方失效、工具同步、失败和 teardown 约定。
**让每个生成包装模块 import Harness package。** 拒绝,因为 repository package 不应解析或锁定应用的内部依赖图。Loader builtin 提供一份由 app 所有的实现,并让生成包装模块保持无 import。
**监视已准备 repository 资源。** 拒绝,因为一个精确 repository cache generation 是不可变的。Ref、子目录或配置变化会选择新 generation第二套 watcher 会创造一套没有所有者的刷新身份。
**把每次 MCP 连接失败都当作 Loader 更新失败。** 拒绝,因为可选的独立 MCP client 会有意收束启动失败并且不暴露工具。MCP client 改为自行提供显式的严格启动选项,由 repository 适配器为其声明的 server 启用。
## 后果
- 现有 skillMCP 仓库可以新增一个很小的 `.dsh-plugin/package.json`,无需移动资源或采用 SDK 项目。
- 已准备的静态输出是确定性胶水;可选的 `dsh.entry` 和已配置的 repository 生命周期仍是受信任的可执行包管理器输入,而非沙箱。
- 多个 repository Plugin 通过提供方名称和普通 MCP server-name 唯一性共存;重复名称经现有 registry 失败,并参与 Loader 回滚。
- Cache 内的源码编辑不会实时出现;必须选择另一个精确 sourcerefpathconfig。
- 新增可移植静态贡献类型必须提供显式格式和 DSH 自有运行时消费方DSH 原生行为使用独立的显式代码入口。
## 测试
聚焦测试会准备 skill 与 MCP 元数据,证明仅含静态贡献的包装模块不含 import拒绝 Work IQ 风格的 OAuth 字段,映射 Expo 风格 HTTP 与 DataJunction 风格 stdio 及环境变量,并覆盖缺失变量。真实 Loader 测试通过已注册 builtin 挂载生成包装模块,经 `ctx.skills` 读取其 skill移除 Loader 条目并观察提供方清理。CI 构建入口验收会使用锁定到 PRPull Requesthead 的 GitHub 源调用 `dsh run`,并观察已复制的 skill以及由取代本决策的新决策所负责的受信任代码与 MCP 验证证据。

View File

@@ -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-03-per-session-agent-presets.md
2026-08-03-per-session-agent-presets.md: 6f1643c25008c3363cb10adb7fbff7afeea31cbe
2026-08-03-per-session-agent-presets.zh.md: 7afe9ade5c98fadb96384a7e0acd47531c370e0c
2026-08-03-per-session-agent-presets.md: 5a82f0220058c10892b819a83499c817aa9be6ad
2026-08-03-per-session-agent-presets.zh.md: 0fb3330014eb703c854f70c9e5ed552256651c31

View File

@@ -18,7 +18,7 @@ Composition splits into two planes, decided by what must be shared rather than b
| Plane | Instances | Contents |
|---|---|---|
| Host | one | The registries themselves (`tools`, `systemPrompt`, `agents`, `agent-loop`, `sessions`), cross-session facilities (persistence, query, projections, storage, settings, credentials, telemetry), and the web host |
| Host | one | The registries themselves (`tools`, `systemPrompt`, `agents`, `agent-loop`, `sessions`), cross-session facilities (persistence, query, projections, storage, settings, credentials, telemetry), the subagent providers those facilities resolve, and the web host |
| Agent | one per session | What a single agent contributes to those registries: tool plugins, persona and prompt sections, compaction policy |
Model routing stays out of presets. `installAgentLlmTarget` is already the per-agent seam for provider, model, and reasoning effort, and an LLM adapter mounted inside a preset would never be resolved by `agent-loop`, which lives in the host plane.
@@ -31,7 +31,7 @@ Which preset an unnamed session gets is a user setting (`agent-presets.default`)
## Consequences
**The effective default is read per resolution, never snapshotted.** A cached value would need a `watch` subscription and a reload path to stay honest, and the resolved scope already re-reads a hot-reloaded document. Reading through is also what makes the boundary correct rather than merely cheap: the new value applies to the next session created, and every running session keeps the composition it was built from. That invariant is the same one the session header enforces from the other side — the header records the id a session actually runs, so a resume rebuilds that composition rather than today's default, and the gateway rejects an attempt to adopt a live session under a different one. A snapshot would make the two disagree at exactly the moment the setting changes.
**The effective default is read per resolution, never snapshotted.** A cached value would need a `watch` subscription and a reload path to stay honest, and the resolved scope already re-reads a hot-reloaded document. Reading through is also what makes the boundary correct rather than merely cheap: the new value applies to the next session created, and every running session keeps the composition it was built from. That invariant is the same one the session log enforces from the other side — the header records the id a session was CREATED with and an `agent-preset/selected` event records any later blank-session switch, so a reader resolves the pair (`resolveSessionPreset`) and never the header alone: a resume rebuilds the composition its history was produced under rather than today's default, a cold transcript's presenters resolve in that composition's layer, and the gateway rejects an attempt to adopt a live session under a preset other than the one it currently runs. A snapshot would make the two disagree at exactly the moment the setting changes.
**A directly-plugged subtree is invisible to the boot audit.** It never links itself to an `Entry`, so it is absent from `ctx.loader.entries()` and `assertEntriesActivated` cannot see it. The mount audits its own rows instead, reading the tree through an `Include` subclass that publishes it.
@@ -55,7 +55,7 @@ Which preset an unnamed session gets is a user setting (`agent-presets.default`)
**Authoring a preset is an RPC, and a privileged one.** A composition is a file, but "edit it on the filesystem" is not a browser affordance, so the roster gained `read`/`write`/`remove` beside `select`. Those three are loopback-pinned: a composition names the plugins a session runs, so reading one is reconnaissance and writing one is arbitrary capability. `list` and `select` deliberately stay ordinary. The roster carries ids and trust only, and a LAN client's picker needs it; and choosing a preset looked like escalation — one of them mounts the toolset that edits the live runtime — but `session.create` already takes an `agentPreset`, so pinning only the switch would have left the same capability one method over. The capability is not the preset's to grant either: the deployment's own default already carries `bash` and the filesystem tools, so any caller that may start a session at all can already run commands as this process. Containment is a property of the id (`[a-z0-9][a-z0-9-]*`), checked before it becomes a directory name rather than by inspecting the joined path afterwards; the text is parsed with the loader's own schema and dialect, so a save cannot leave a file no session could load. Shipped presets are refused for writes and deletes, because the deployment's copy is what a broken local preset is compared against — which also makes "duplicate, then edit" the authoring path rather than an afterthought.
**A service with a consumer outside the agent plane cannot move into a preset.** The aggressive split moved the `subagents` registry and its spawn/fork backends into the delegation group's entry-local realm, and `dsh web` then failed to boot: `dsh-host-apiproxy` is a HOST row that injects `subagents` to answer the browser's cross-session queries (`listChildren`, `followup`), so it waited forever for a service only sessions now provided. A per-session copy is wrong twice over — a provider name registers once, so the second session would have collided anyway. The registry and its backends are host-plane; the preset contributes the delegation TOOLS, which resolve the host registry. `workflows` stays entry-local because nothing outside an agent reads it. Grepping injectors is what should have caught this and did not: the search has to include the host packages, not just the agent-plane ones.
**A service with a consumer outside the agent plane cannot move into a preset.** The aggressive split moved the `subagents` registry and its spawn/fork backends into the delegation group's entry-local realm, and `dsh web` then failed to boot: `dsh-host-apiproxy` is a HOST row that injects `subagents` to answer the browser's cross-session queries (`listChildren`, `followup`), so it waited forever for a service only sessions now provided. A per-session copy is wrong twice over — a provider name registers once, so the second session would have collided anyway. The registry and every shared backend, including the [fixed Codex and Claude Code product providers](2026-08-10-product-subagent-providers-in-shared-host.md), are host-plane; a preset contributes whichever delegation TOOLS its agent should see, and those tools resolve the host registry. `workflows` stays entry-local because nothing outside an agent reads it. Grepping injectors is what should have caught this and did not: the search has to include the host packages, not just the agent-plane ones.
**A real-composition test that disables a host row cannot audit that row.** The web composition test disabled `api-gateway` — the api-proxy itself — as a row with side effects, which is exactly the row whose pending injection would have named the break. It now boots with the api-proxy enabled and the browse directory picker substituted, so the boot audit covers the whole host-plane injection graph; only the port, the asset tree, and the telemetry exporter stay off.
@@ -78,3 +78,7 @@ Which preset an unnamed session gets is a user setting (`agent-presets.default`)
**Make the agent's scope key the preset.** Sessions on one preset would share a layer for free, but per-agent registrations — `installAgentLlmTarget`, per-agent tool restrictions — would then collide across sessions.
**Run each preset as a child process.** [`subagent-dsh-sdk`](../../../../packages/subagent/subagent-dsh-sdk/README.md) already proves a full child harness works, and isolation would be absolute. It also means proxying streaming, approvals, and projections per session, which is a transport project rather than a composition one.
**Give product subagents global enable settings and a separate settings page.** The process-wide value would compete with the preset as owner of model-visible tools and could not express two sessions using different compositions. Product providers stay host-side, while ordinary preset rows independently expose Codex and Claude Code tools.
**Ship one preset for every Codex and Claude Code combination.** Four identities duplicate the full preset composition to represent two independent rows. A copied preset can enable either row directly, so combination presets add roster and maintenance cost without adding a user result.

View File

@@ -18,7 +18,7 @@ Status: implemented
| 平面 | 实例数 | 内容 |
|---|---|---|
| 宿主 | 一份 | 注册表本身(`tools``systemPrompt``agents``agent-loop``sessions`)、跨会话设施(持久化、查询、投影、存储、设置、凭据、遥测),以及 web 宿主 |
| 宿主 | 一份 | 注册表本身(`tools``systemPrompt``agents``agent-loop``sessions`)、跨会话设施(持久化、查询、投影、存储、设置、凭据、遥测)、这些设施所解析的 subagent provider,以及 web 宿主 |
| agent | 每会话一份 | 单个 agent 对这些注册表的贡献:工具插件、人设与提示词段落、压缩策略 |
模型路由不进 preset。`installAgentLlmTarget` 已经是 provider、model 与 reasoning effort 的按 agent 可替换点;而挂在 preset 内部的 LLM 适配器永远不会被 `agent-loop` 解析到,因为后者位于宿主平面。
@@ -31,7 +31,7 @@ Status: implemented
## 后果
**有效默认值在每次解析时读取,从不快照。** 缓存下来就需要一个 `watch` 订阅和一条重载路径才能保持诚实,而解析后的 scope 本来就会重读热重载过的文档。读穿也不只是省事,它让边界本身是对的:新值作用于**下一个新建的会话**,每个运行中的会话保持它被构建时的那份组装。这条不变量正是 session header 从另一侧执行的同一条——header 记录会话实际运行的 id因此恢复重建的是那份组装而不是当下的默认值网关也会拒绝把一个活着的会话收编到另一个 preset 之下。快照会让两者恰好在设置改变的那一刻各说各话。
**有效默认值在每次解析时读取,从不快照。** 缓存下来就需要一个 `watch` 订阅和一条重载路径才能保持诚实,而解析后的 scope 本来就会重读热重载过的文档。读穿也不只是省事,它让边界本身是对的:新值作用于**下一个新建的会话**,每个运行中的会话保持它被构建时的那份组装。这条不变量正是 session 日志从另一侧执行的同一条——header 记录会话**创建时**的 id此后空白期的任何切换由 `agent-preset/selected` 事件记录,因此读取方解析的是两者之和(`resolveSessionPreset`)、绝不单看 header恢复重建的是其历史所产出的那份组装而不是当下的默认值冷读记录的 presenter 在那份组装的层里解析,网关也会拒绝把一个活着的会话收编到它当前运行的 preset 以外的 preset 之下。快照会让两者恰好在设置改变的那一刻各说各话。
**直接挂载的子树对启动审计不可见。** 它不会把自己关联到 `Entry`,因此不在 `ctx.loader.entries()` 中,`assertEntriesActivated` 也看不到它。改由挂载过程自行校验各行,通过一个会公开自身 tree 的 `Include` 子类读取。
@@ -56,7 +56,7 @@ Status: implemented
**创作 preset 是一次 RPC而且是特权 RPC。** 组装是一个文件,但“去文件系统里改它”并不是浏览器能提供的操作,因此名单在 `select` 之外新增了 `read`/`write`/`remove`。这三者被固定在环回地址:组装指明了一个会话所运行的插件,因此读取它是侦察,写入它是任意能力。`list``select` 刻意保持为普通方法。名单只携带 id 与信任级别,而局域网客户端的选择器需要它;至于选择本身,它看起来像提权——其中一个 preset 会挂载可编辑活动运行时的工具集——但 `session.create` 本就接受 `agentPreset`,只固定切换会把同一能力留在隔壁一个方法上。这份能力也不由 preset 授予:部署自带的默认 preset 本就带着 `bash` 与文件系统工具,因此任何被允许开启会话的调用方,早已能以本进程的身份执行命令。约束是 id 自身的性质(`[a-z0-9][a-z0-9-]*`),在它成为目录名之前就检查,而不是事后再去审视拼接出的路径;文本使用 loader 自身的 schema 与方言解析,因此保存不会留下任何会话都无法加载的文件。随部署提供的 preset 拒绝写入与删除,因为部署自带的那一份正是用来对照有问题的本地 preset 的——这也让“先复制、再编辑”成为创作路径本身,而非事后补充。
**在 agent 平面之外还有消费方的服务,不能搬进 preset。** 激进拆分把 `subagents` 注册表连同 spawn/fork 后端一起搬进了 delegation 组的 entry-local realm于是 `dsh web` 直接起不来:`dsh-host-apiproxy` 是宿主行,它注入 `subagents` 来回答浏览器的跨会话查询(`listChildren``followup`因而永远等待一个此刻只有会话才提供的服务。按会话各一份在两个层面上都是错的——provider 名只能注册一次,第二个会话本来也会相撞。注册表与后端属于宿主平面preset 贡献的委派**工具**它们解析宿主注册表。`workflows` 保持 entry-local因为 agent 之外没有任何东西读它。本该拦下它的是「检索注入方」这一步,而它没拦住:检索必须覆盖宿主包,而不只是 agent 平面的包。
**在 agent 平面之外还有消费方的服务,不能搬进 preset。** 激进拆分把 `subagents` 注册表连同 spawn/fork 后端一起搬进了 delegation 组的 entry-local realm于是 `dsh web` 直接起不来:`dsh-host-apiproxy` 是宿主行,它注入 `subagents` 来回答浏览器的跨会话查询(`listChildren``followup`因而永远等待一个此刻只有会话才提供的服务。按会话各一份在两个层面上都是错的——provider 名只能注册一次,第二个会话本来也会相撞。注册表与所有共享后端,包括[固定的 Codex 与 Claude Code 产品 provider](2026-08-10-product-subagent-providers-in-shared-host.md),都属于宿主平面preset 贡献自己的 agent 应看见的委派**工具**这些工具解析宿主注册表。`workflows` 保持 entry-local因为 agent 之外没有任何东西读它。本该拦下它的是「检索注入方」这一步,而它没拦住:检索必须覆盖宿主包,而不只是 agent 平面的包。
**真实组装测试若禁用了某个宿主行,就无法审计该行。** web 组装测试把 `api-gateway`——也就是 api-proxy 本身——当作「有外部副作用的行」禁用了,而它恰恰是那个会以 pending 注入点名此次断裂的行。现在它在启用 api-proxy、并替换为 browse 目录选择器的前提下引导,启动审计因此覆盖整个宿主平面的注入图;只有端口、资源目录与遥测导出器仍然关闭。
@@ -79,3 +79,7 @@ Status: implemented
**把 agent 的 scope 键设为 preset。** 同一 preset 上的会话就能免费共享一层,但按 agent 的注册——`installAgentLlmTarget`、按 agent 的工具限制——会跨会话相撞。
**把每个 preset 作为子进程运行。** [`subagent-dsh-sdk`](../../../../packages/subagent/subagent-dsh-sdk/README.md) 已经证明完整的子 harness 可行,隔离性也会是绝对的。但这同时意味着要按会话代理流式输出、审批与投影,那是一个传输层项目,而非组装问题。
**给产品 subagent 增加全局启用设置与独立设置页。** 进程级值会与 preset 争夺模型可见工具的所有权,也无法表达两个会话使用不同组装。产品 provider 留在宿主,普通 preset 行分别暴露 Codex 与 Claude Code 工具。
**为 Codex 与 Claude Code 的每种组合交付一份 preset。** 四个身份会复制完整 preset 组装,只为表示两条独立行。复制后的 preset 已能直接启用任一行,因此组合 preset 只增加名单与维护成本,不增加用户结果。

View File

@@ -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-05-profile-plugin-bundles.md
2026-08-05-profile-plugin-bundles.md: 2924b3cb445064fd47d82bcc94ec8d77ded5721b
2026-08-05-profile-plugin-bundles.zh.md: b2287034010bcac1048bb385b2266f1bc75921da
2026-08-05-profile-plugin-bundles.md: 54626e3f48a2ba7db19813e6e883f0e77499d0e2
2026-08-05-profile-plugin-bundles.zh.md: 357e0f63d4eba0f0985c9e14aad54595c7b41c77

View File

@@ -10,11 +10,9 @@ The `dsh` launcher hardcoded its compositions: `base.cordis.yml` + `web.cordis.y
## Decision
Everything becomes a **profile**: a directory `$DSH_HOME/profiles/<name>` with a `package.json` (pnpm-managed out-of-tree plugin `dependencies` plus the profile manifest `dsh.profile` with its ordered `bundles` layer list) and a user `cordis.patch.yml`. A **bundle** is an npm package declaring `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }`; the two manifest kinds live under distinct `dsh.profile` / `dsh.bundle` keys so a package.json states which role it plays. The tree composes over an empty root by applying each bundle's patch in `dsh.profile.bundles` order, then the user layer, then `--patch` overlays, then flag patches — one `applyEntryPatches` call, identical for boot, flag derivation, and `--dump-config`.
Everything becomes a **profile**: a directory `$DSH_HOME/profiles/<name>` with a `package.json` (pnpm-managed out-of-tree plugin `dependencies` plus the profile manifest `dsh.profile` with its ordered `bundles` layer list) and a user `cordis.patch.yml`. A **bundle** is an npm package declaring `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }`; the two manifest kinds live under distinct `dsh.profile` / `dsh.bundle` keys so a package.json states which role it plays. The tree composes over an empty root by applying each bundle's patch in `dsh.profile.bundles` order, then the user layer and `--patch` overlays — one `applyEntryPatches` call shared by boot and `--dump-config`. App invocation values later moved from launcher-derived patches to startup services in the [app-owned command-line decision](2026-08-06-app-owned-command-line.md).
The shipped bundles are `@deepseek-ai/dsh-base` (shared core rows), `@deepseek-ai/dsh-web-app` (browser Host rows and Web runtime glue), and `@deepseek-ai/dsh-headless` (a direct one-shot runner over base, without web-app). `dsh web` is the Web-flag alias for `--profile web`; `dsh run [--profile <name>] "task"` owns one-shot execution and defaults to the headless profile; generic `dsh --profile <name>` boots without a task. Patch overlays use `--patch`. `dsh plugin --profile <name> <args...>` is a thin pnpm forwarder that initializes the profile and reconciles `dsh.profile.bundles` with installed bundle declarations; a package without a bundle declaration remains a plain dependency. [Headless as a direct core entry point](2026-08-09-headless-direct-core-entry-point.md) owns the headless composition contract.
The [`dsh run` command decision](../feature/2026-08-08-dsh-run-headless-command.md) owns the one-shot grammar; this note owns the profile composition it selects.
The shipped bundles are `@deepseek-ai/dsh-base` (shared core rows), `@deepseek-ai/dsh-web-app` (browser Host rows and Web runtime glue), and `@deepseek-ai/dsh-headless` (a direct one-shot runner over base, without web-app). Generic `dsh --profile <name>` hands its remaining arguments to that profile's command-line startup row: Web owns its flag family, while headless owns its task positional. Patch overlays use launcher-owned `--patch`. `dsh plugin --profile <name> <args...>` is a thin pnpm forwarder that initializes the profile and reconciles `dsh.profile.bundles` with installed bundle declarations; a package without a bundle declaration remains a plain dependency. [Headless as a direct core entry point](2026-08-09-headless-direct-core-entry-point.md) owns the headless composition contract.
Resolution is two-anchored by construction: `dsh.profile.bundles` names resolve from the dsh installation first, then the profile directory — so in-box bundles always come from the same installation as the running `dsh` and pnpm never manages them — while bare plugin names in patch rows resolve through the profile directory's Node parent-walk into the maintained flat fallback `$DSH_HOME/profiles/node_modules` (one symlink per package the installation's app and bundles depend on, healed on every launch).
@@ -24,7 +22,7 @@ Two supporting refactors: the webserver's built-in static dist serving became th
- **Dependency-scan plus partial `patchOrder`** (the original sketch): scanning `dependencies` for bundles and ordering unlisted ones alphabetically has two sources of truth and an implicit tie-break; one explicit ordered `dsh.profile.bundles` list is smaller and fully deterministic. A raw `pnpm add` inside the profile installs a library without activating any patch — explicit, no spooky scan.
- **`link:` entries for in-box bundles**: pnpm cannot version, install, or update a `link:` into the installation, it embeds a machine path in a user file, and it breaks when the installation moves. The two-anchor resolution plus healed symlink fallback gives the same guarantee ("bundles come from the installation") without ceremony.
- **A pre-boot `context` module in the bundle manifest** for boot-time values (dist path, flag facts): rejected in favor of pure plugins — the glue is ordinary rows the launcher patches, so the composition stays fully dumpable and the manifest stays data-only. The launcher-owned `ctx.headlessIo` host hook is the one host-provided slot, and it is provided in `boot()`'s `prepare` hook, before any config-tree entry mounts.
- **A pre-boot `context` module in the bundle manifest** for boot-time values (dist path, flag facts): rejected in favor of pure plugins — the glue is ordinary rows and app-owned startup services, so the composition stays fully dumpable and the manifest stays data-only. The launcher-owned `ctx.headlessIo` host hook is the one host-provided slot, and it is provided in `boot()`'s `prepare` hook, before any config-tree entry mounts.
- **Transitive bundle auto-application**: only direct `dsh.profile.bundles` entries contribute layers; a meta-bundle wanting to re-export another bundle's patch must do so explicitly in its own patch file.
## Consequences

View File

@@ -10,11 +10,9 @@ Status: implemented
## Decision
一切都变成 **profile**:即目录 `$DSH_HOME/profiles/<name>`,其中包含一个 `package.json`pnpm 管理的树外插件 `dependencies`,加上 profile manifest(元数据清单)`dsh.profile` 及其有序的 `bundles` 层列表)和一份用户 `cordis.patch.yml`。**组合包**bundle是声明了 `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }` 的 npm 包;两种 manifest 分别位于互不相同的 `dsh.profile` / `dsh.bundle` 键下,因此一份 package.json 能说明自己扮演哪种角色。配置树在空的根之上组合:按 `dsh.profile.bundles` 顺序应用每个组合包的 patch然后是用户层,然后是 `--patch` overlay,最后是 flag patch——全部收敛为一次 `applyEntryPatches` 调用启动、flag 派生与 `--dump-config` 使用完全相同的路径
一切都变成 **profile**:即目录 `$DSH_HOME/profiles/<name>`,其中包含一个 `package.json`pnpm 管理的树外插件 `dependencies`,加上 profile manifest `dsh.profile` 及其有序的 `bundles` 层列表)和一份用户 `cordis.patch.yml`。**组合包**bundle是声明了 `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }` 的 npm 包;两种 manifest 分别位于互不相同的 `dsh.profile` / `dsh.bundle` 键下,因此一份 package.json 能说明自己扮演哪种角色。配置树在空的根之上组合:按 `dsh.profile.bundles` 顺序应用每个组合包的 patch然后是用户层 `--patch` overlay——启动与 `--dump-config` 共享同一条 `applyEntryPatches` 路径。随后,[应用持有命令行的决策](2026-08-06-app-owned-command-line.md)又把调用期取值从启动器派生的 patch 迁移到了启动服务
随附的组合包是 `@deepseek-ai/dsh-base`(共享核心配置行)、`@deepseek-ai/dsh-web-app`(浏览器 Host 配置行与 Web 运行时粘合层)和 `@deepseek-ai/dsh-headless`(直接叠加在 base 上且不含 web-app 的一次性 runner`dsh web` 是携带 Web flag 家族的 `--profile web` 别名;`dsh run [--profile <name>] "task"` 负责一次性执行,默认使用 headless profile通用的 `dsh --profile <name>` 启动 profile 而不携带任务。patch overlay 使用 `--patch``dsh plugin --profile <name> <args...>` 是一层薄薄的 pnpm 转发器,负责初始化 profile并依据已安装包的组合包声明调和 `dsh.profile.bundles`;没有组合包声明的包保持为普通依赖。[Headless 作为直接 core 入口](2026-08-09-headless-direct-core-entry-point.md)负责 headless 组合约定。
[`dsh run` 命令决策](../feature/2026-08-08-dsh-run-headless-command.md)负责一次性语法;本 Agent Note 负责该语法所选择的 profile 组合。
随附的组合包是 `@deepseek-ai/dsh-base`(共享核心配置行)、`@deepseek-ai/dsh-web-app`(浏览器 Host 配置行与 Web 运行时粘合层)和 `@deepseek-ai/dsh-headless`(直接叠加在 base 上且不含 web-app 的一次性 runner通用的 `dsh --profile <name>` 把剩余参数交给该 profile 的命令行启动行Web 持有自己的 flag 家族headless 则持有任务位置参数。patch overlay 使用启动器持有的 `--patch``dsh plugin --profile <name> <args...>` 是一层薄薄的 pnpm 转发器,负责初始化 profile并依据已安装包的组合包声明调和 `dsh.profile.bundles`;没有组合包声明的包保持为普通依赖。[Headless 作为直接 core 入口](2026-08-09-headless-direct-core-entry-point.md)负责 headless 组合约定。
解析在构造上就是双锚点的:`dsh.profile.bundles` 中的名称先从 dsh 安装目录解析,再从 profile 目录解析——因此内置组合包始终来自与运行中 `dsh` 相同的安装pnpm 从不管理它们——而 patch 行中的裸插件名称经 profile 目录的 Node 父目录逐级查找,落到受维护的扁平回退目录 `$DSH_HOME/profiles/node_modules`(安装目录的应用与各组合包所依赖的每个包各一个符号链接,每次启动时修复)。
@@ -24,7 +22,7 @@ Status: implemented
- **依赖扫描加部分 `patchOrder`**(最初的草案):扫描 `dependencies` 找出组合包、未列出者按字母序排列,会产生两个真源和一条隐式决胜规则;一份显式有序的 `dsh.profile.bundles` 列表更小、完全确定。在 profile 内直接 `pnpm add` 只会安装一个库,不激活任何 patch——行为显式没有暗中扫描。
- **内置组合包使用 `link:` 条目**pnpm 无法对指向安装目录的 `link:` 做版本管理、安装或更新,它会把机器路径嵌进用户文件,并且在安装目录移动后失效。双锚点解析加上每次启动修复的符号链接回退提供了同样的保证(「组合包来自安装目录」),且没有这些繁文缛节。
- **在组合包 manifest 中放一个启动前 `context` 模块**承载启动期取值dist 路径、flag 事实):否决,改用纯插件——粘合逻辑就是启动器 patch 的普通配置行,因此组合始终可完整 dumpmanifest 保持纯数据。启动器持有的 `ctx.headlessIo` 宿主钩子是唯一由宿主提供的 slot且在任何配置树条目挂载之前`boot()``prepare` 钩子中提供。
- **在组合包 manifest 中放一个启动前 `context` 模块**承载启动期取值dist 路径、flag 事实):否决,改用纯插件——粘合逻辑就是普通配置行和由应用持有的启动服务,因此组合始终可完整 dumpmanifest 保持纯数据。启动器持有的 `ctx.headlessIo` 宿主钩子是唯一由宿主提供的 slot且在任何配置树条目挂载之前`boot()``prepare` 钩子中提供。
- **组合包的传递式自动应用**:只有直接列在 `dsh.profile.bundles` 中的条目才贡献层;想重新导出另一个组合包 patch 的元组合包,必须在自己的 patch 文件中显式完成。
## Consequences

View File

@@ -1,6 +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/feature/2026-08-08-dsh-run-headless-command.md
2026-08-08-dsh-run-headless-command.md: ed095f4077a23e51bffb647d24eed19ba09e11ed
2026-08-08-dsh-run-headless-command.zh.md: 89d54e35573f14786e05d648f2b42891ca27a043
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-06-app-owned-command-line.md
2026-08-06-app-owned-command-line.md: 4a05cac5ed7f44fb55c2d4498bf28a43befdb073
2026-08-06-app-owned-command-line.zh.md: 86a37f416d17c4615152b29d73f171803f24c4c3

View File

@@ -0,0 +1,51 @@
# Agent Note: Apps own their command line through `ctx.cmdlineArgs`
Status: implemented
English | [中文](2026-08-06-app-owned-command-line.zh.md)
## Problem
After profiles, compositions were installable but their command lines were not. `apps/cli` still declared the Web flag family (`--host`, `--port`, `--dev`, `--workspace-root`, `--trusted-host`) and the one-shot task positional, then derived patches for row ids it hardcoded (`webserver`, `api-gateway`, `connection`, `web-runtime`). An out-of-tree app such as [turtle-ui](https://github.com/deepseek-harness/turtle-ui) could contribute rows but had no way to accept a flag: `dsh --profile tui --resume <session>` had nowhere to be parsed, and `dsh --profile web --help` printed the launcher's help rather than the web app's.
## Decision
The launcher parses only what it owns — `--profile`, `--patch`, the config dumps — and hands **everything after its own flags** to the booted tree verbatim. The split is positional: the first token the launcher does not recognize starts the app's arguments (commander's `passThroughOptions` + `allowUnknownOption` + `helpOption(false)`). A bare `dsh -h`, which has no app to hand the flag to, still prints the launcher's own help.
The new `@deepseek-ai/dsh-cmdline` package owns the handoff. A launcher calls `provideCmdline(ctx, host)` before any entry mounts, providing `ctx.cmdlineArgs` (whose whole interface is `get(): readonly string[]`) and `ctx.appExit`. Any ordinary app plugin may inject `cmdlineArgs`, call `parseCmdline(ctx, program, plan)` with its own commander program, and provide the returned value as an app-owned service. Its Loader row carries no launcher marker or special kind, and the launcher does not inspect the composition for an owner. Multiple plugins may read the same immutable snapshot; a profile with no reader ignores its app arguments. Rows configured from a provider inject its service and read direct lazy config expressions (`port: !!js ctx.webStartup.port ?? 3080`), so a flag beats the value written beside it and nothing is written back into any row.
The boot mounts the composition once. Cordis holds each row until its injections are active; Loader then interpolates that row's `!!js` against the injection-ready plugin context immediately before activation. Include keeps nested row expressions raw until their target row reaches this point. `--help` leaves the provider's service absent, so dependent rows never activate, and a live patch reload interpolates again against the service that remains active, so a served port cannot be silently reset.
The shipped apps moved their flags into their bundles: `dsh-web-app` owns the Web family (and enables the `client-hmr` row it now ships disabled, for `--dev`), and `dsh-headless` owns the task positional and rejects a missing task as a usage error. `apps/cli/src/web.ts` is gone; `runProfile` no longer knows any flag-target row id. Out of tree, turtle-ui gained `--resume <session>` / `--session <id>` the same way, which is the design's real validation: an installed plugin added a flag with no launcher change.
Two further consequences. Loader mounts sibling rows concurrently, so one row can activate while another still mounts or while the whole boot is rolling back; the Web bundle therefore publishes its URL only after its own Loader tree settles. The Web bundle's runtime plugin owns the harness-source prompt section too, so `dsh web` and `dsh --profile web` boot identically without Web-specific launcher setup.
## Why Loader owns the ordering
Four framework facts shape the mechanism:
- **A profile's rows arrive inside the root include's `patches` option.** Include is an entry-tree owner, so its static entry-config resolver interpolates Include's own options while preserving nested `!!js` nodes for their target rows instead of recursively evaluating them in the Include context.
- **Cordis activates a fiber only after all declared injections are active.** Immediately before each activation, Cordis runs the `internal/config` waterfall against the fiber's own context; Loader's listener interpolates the raw config after Cordis snapshots its injected services.
- **Provider replacement and HMR must preserve the same contract.** Fiber reactivation re-runs the waterfall, HMR carries the raw config to the replacement fiber, and a pending row accepts option changes without prematurely evaluating expressions against absent services.
- **A row cannot be inserted from inside a mounting plugin** — `tree.create` returns a prefixed id it then fails to resolve — so a conditional row ships `disabled: true` and an active row enables it (`dsh web --dev` and its reload chain). Enablement is an in-memory Loader override rather than an options rewrite, so Include reapplication cannot silently disable it. The Web bundle also starts client discovery only after enabling the optional row, ensuring the first browser graph already contains its HMR receiver.
This leaves dependency ordering in Cordis activation and Loader interpolation, which own it. Rows keep their `inject` and config, Loader mounts the composition once, and the launcher only provides argv and process-lifecycle services.
## Alternatives considered
- **Writing the resolved values into each row** (a config update per row, plus a patch layer handed back to the launcher so a reload could not undo it): it worked, but it meant patches travelling from an app to the launcher and back, two mechanisms for one fact, and a recycle whose correctness depended on Loader restart internals. The maintainer rejected the round trip; the service the rows read replaced all of it.
- **Releasing rows by clearing their `inject`**: it worked in isolation and failed on the real web tree, because clearing `inject` is exactly what loses the plugin's static injections. The failure is silent until a plugin reads a service it declared.
- **Launcher-managed two-pass mounting**: it can make a provider active before readers are applied, but duplicates the composition, makes ordering a launcher concern, and conceals the Loader defect that nested expressions were evaluated in the include context rather than the target row's injected context.
- **The launcher running each bundle's command function before boot** (no Cordis involvement): strictly earlier than "boot, then help", but it makes app startup a second plugin protocol outside the tree. An ordinary `cmdlineArgs`-injected provider keeps one protocol and remains dumpable and patchable.
- **A launcher-enforced command-line owner**: rejecting zero or multiple readers would arbitrate overlaps such as `-h`, but `get()` is an immutable read and normal composition may need several app-owned services. Plugins therefore share the snapshot and own any parser interaction through ordinary composition.
- **`instanceof CommanderError`**: an out-of-tree plugin brings its own commander copy, so the class identity differs and a printed `--help` was rethrown as a fatal load failure. Commander's control-flow errors are detected structurally instead.
## Consequences
- An app's flags, help text, and usage errors live with the rows they configure; adding a flag to an installed plugin needs no launcher change.
- The launcher still recognizes the headless runner for one-shot process lifetime and the telemetry row for its environment switch; neither path interprets app arguments.
- `--help` leaves every row that depends on the provider's service pending and requests bounded exit; unrelated rows may activate concurrently before teardown.
- An app-owned service has no statically declared provider: a bundle shipping consumer rows without that provider fails at settlement with pending entries naming the service, not at load.
- A user patch that replaces a row's whole `config` drops its expressions, and with them the flag's precedence for that row.
- Launcher flags must precede app arguments; a first app argument equal to `web` or `plugin` selects that subcommand instead, `-V`/`--version` remains launcher-owned before that boundary, and the launcher's parser consumes one `--`, so a literal `--` for the app needs `-- --`.
- `--dump-config` never runs app command-line providers, so it prints the composition before any app argument is resolved and rejects an invocation that carries app arguments.

View File

@@ -0,0 +1,51 @@
# Agent Note: 应用通过 `ctx.cmdlineArgs` 持有自己的命令行
Status: implemented
[English](2026-08-06-app-owned-command-line.md) | 中文
## 问题
profile 落地之后,组合可以安装,命令行却不能。`apps/cli` 仍然声明着 Web flag 家族(`--host``--port``--dev``--workspace-root``--trusted-host`)和一次性任务位置参数,再为自己硬编码的行 id`webserver``api-gateway``connection``web-runtime`)派生 patch。像 [turtle-ui](https://github.com/deepseek-harness/turtle-ui) 这样的树外应用能贡献行,却无处接受一个 flag`dsh --profile tui --resume <session>` 没有地方可供解析,而 `dsh --profile web --help` 打印的是启动器的 help而不是 web 应用的 help。
## 决策
启动器只解析属于自己的部分(`--profile``--patch`、配置 dump并把**自己 flag 之后的一切**原样交给引导起来的配置树。切分按位置进行:启动器不认识的第一个 token 就是应用参数的起点(依靠 commander 的 `passThroughOptions` + `allowUnknownOption` + `helpOption(false)`)。裸的 `dsh -h` 没有可交付的应用,仍然打印启动器自己的 help。
新包 `@deepseek-ai/dsh-cmdline` 持有这次交接。启动器在任何条目挂载之前调用 `provideCmdline(ctx, host)`,提供 `ctx.cmdlineArgs`(其全部接口就是 `get(): readonly string[]`)与 `ctx.appExit`。任何普通应用插件都可以注入 `cmdlineArgs`,用自己的 commander program 调用 `parseCmdline(ctx, program, plan)`,再把返回值作为应用自有服务提供出去。它的 Loader 行不携带启动器标记或特殊类型,启动器也不会检查组合中的所有者。多个插件可以读取同一份不可变快照;没有读取方的 profile 会忽略自己的应用参数。由提供方配置的行注入其服务,并在惰性配置表达式中直接读取它(`port: !!js ctx.webStartup.port ?? 3080`),因此 flag 胜过写在它旁边的值,也没有任何东西被写回任何一行。
boot 只挂载一次整套组合。Cordis 让每一行等待其注入激活Loader 随后在激活前一刻,基于已注入就绪的插件上下文插值该行的 `!!js`。Include 会保留嵌套的行表达式,直到目标行到达这一时点。`--help` 会让提供方服务保持缺失,因此依赖行永不激活;活动 patch 重载会针对仍然在线的服务再次插值,所以已经服务中的端口不会被悄悄重置。
已交付的各应用把自己的 flag 搬进了组合包:`dsh-web-app` 持有 Web 家族(并为 `--dev` 启用它如今以禁用状态交付的 `client-hmr` 行),`dsh-headless` 持有任务位置参数,缺少任务时按用法错误拒绝。`apps/cli/src/web.ts` 已删除;`runProfile` 不再知道任何 flag 目标行 id。在树外turtle-ui 以同样的方式获得了 `--resume <session>` / `--session <id>`,这才是这套设计的真正验证:一个已安装的插件加上了一个 flag启动器毫无改动。
还有两条后果。Loader 会并发挂载兄弟行,因此一行可能已经激活,而另一行仍在挂载,或整次 boot 正在回滚;所以 Web 组合包只会在自身的 Loader 配置树结算后公布 URL。另外Web 组合包的运行时插件也持有 harness 源码提示词段,因此 `dsh web``dsh --profile web` 无需 Web 专用启动器设置即可按完全相同的方式启动。
## 为什么由 Loader 持有顺序
四条框架事实塑造了这套机制:
- **profile 的各行位于根 include 的 `patches` 选项内部。** Include 是条目树所有者,因此它的静态条目配置解析器会插值 Include 自身的选项,同时为目标行保留嵌套的 `!!js` 节点,而不是在 Include 上下文中递归求值。
- **Cordis 只在所有声明的注入都已激活后才激活 fiber。** 每次激活前一刻Cordis 会基于 fiber 自身上下文运行 `internal/config` waterfallCordis 快照注入服务之后Loader 的监听器再插值原始配置。
- **提供方替换与 HMR 必须保持相同契约。** fiber 重新激活时会重跑 waterfallHMR 会把原始配置带给替换 fiber而待处理行可以接受选项变更不会针对缺失服务提前求值表达式。
- **不能从正在挂载的插件内部插入一行**——`tree.create` 返回一个带前缀的 id随后它自己解析不出来——因此条件性的行以 `disabled: true` 交付,再由活跃行启用(`dsh web --dev` 及其重载链路)。启用采用 Loader 的内存覆盖而非改写选项,因此 Include 重新应用配置时不会悄然将其禁用。Web 组合包还会在启用可选行之后才启动客户端发现,确保首份浏览器图中已经包含 HMR 接收端。
这样,依赖顺序仍由负责它的 Cordis 激活与 Loader 插值流程处理。各行保留自己的 `inject` 和配置Loader 只挂载一次组合,启动器只提供 argv 与进程生命周期服务。
## 曾考虑的替代方案
- **把解析出的取值写进每一行**(逐行一次配置更新,外加交还给启动器的一层 patch使重载无法撤销它它能工作但这意味着 patch 在应用与启动器之间来回传递、同一件事有两套机制,以及一套其正确性依赖 Loader 重启内部细节的回收重建。维护者否决了这次往返;供各行读取的服务取代了这一切。
- **通过清空行的 `inject` 来放行**:孤立测试可行,在真实 web 树上失败,因为清空 `inject` 恰恰会丢失插件的静态注入。在插件真的去读它声明过的服务之前,这个失败是静默的。
- **由启动器管理两趟挂载**:它可以让提供方先于读取行激活,但会重复组合、把顺序变成启动器职责,还掩盖了 Loader 的缺陷——嵌套表达式在 include 上下文而不是目标行的注入上下文中求值。
- **由启动器在 boot 之前运行每个组合包的命令函数**(完全不经过 Cordis严格早于「先 boot 再 help」但这会让应用启动成为配置树之外的第二套插件协议。使用注入 `cmdlineArgs` 的普通提供方只保留一套协议,并且仍可 dump、可 patch。
- **由启动器强制指定命令行所有者**:拒绝零个或多个读取方可以裁决 `-h` 等重叠项,但 `get()` 是不可变读取,普通组合也可能需要多个应用自有服务。因此插件共享该快照,并通过普通组合持有各自解析器的交互。
- **`instanceof CommanderError`**:树外插件会带来自己的一份 commander 副本,类身份因此不同,已经打印出来的 `--help` 会被重新抛成致命的加载失败。改为按结构识别 commander 的控制流错误。
## 后果
- 应用的 flag、help 文本和用法错误与它们所配置的行放在一起;给已安装的插件加一个 flag 不需要改动启动器。
- 启动器仍会识别 headless runner 以管理一次性进程生命周期,并识别 telemetry 行以应用环境开关;两条路径都不解析应用参数。
- `--help` 会让所有依赖提供方服务的行保持待处理并请求有边界的退出;无关行可能在拆除前并发激活。
- 应用自有服务没有静态声明的提供方:交付了消费行却缺少对应提供方的组合包会在结算时失败,报出指向该服务的待处理条目,而不是在加载时失败。
- 用户 patch 若整体替换某行的 `config`,会连同其中的表达式一起丢掉,该行上 flag 的优先级也随之消失。
- 启动器的 flag 必须写在应用参数之前;如果应用的第一个参数恰好等于 `web``plugin`,会选择对应的子命令;`-V``--version` 在该边界之前仍归启动器持有;而且启动器的解析器会消耗掉一个 `--`,因此要给应用传一个字面量 `--` 需要写成 `-- --`
- `--dump-config` 从不运行应用命令行提供方,因此它在任何应用参数被解析之前打印组合,并拒绝携带应用参数的调用。

View File

@@ -1,51 +0,0 @@
# Agent Note: Trusted repository packages load Cordis code
Status: implemented
English | [中文](2026-08-08-trusted-repository-package-code.zh.md)
## Problem
The standalone repository format already installs a selected Git package and runs its dependency and lifecycle code with host authority, but it exposed only copied skills and MCP metadata to DSH. Forbidding a Cordis entry did not create a security boundary: package installation remained trusted executable code while the restriction prevented the package from contributing the Plugin behavior that the Harness architecture is designed to compose.
A repository author also needs to keep an ordinary TypeScript npm package shape. Requiring publication to npm, pre-generated JavaScript in Git, or a DSH-owned TypeScript compiler would make a Git source less capable than the same package installed through a developer-owned SDK project. The first model request must observe any MCP tools that this package starts; background-only initial discovery makes a successful installation nondeterministic at the application boundary.
## Decision
A configured repository package is trusted code. Its `.dsh-plugin/package.json` may declare `dsh.entry` as a relative path to a compiled ESM Cordis Plugin inside that package, alongside or instead of `dsh.skills` and `dsh.mcpServers`. At least one contribution is required. The entry may use namespace exports or a default export and retains ordinary Cordis semantics for `name`, `inject`, `Config`, registrations, startup failure, and effect-scoped teardown.
The package owns its npm dependencies and build toolchain. It declares the published `@deepseek-ai/dsh-repository-plugin` package to obtain the `dsh-plugin-prepare` executable. `scripts.prepack` is a non-empty package-authored command that must invoke that dependency-provided helper, but it may first run `tsc`, `tsdown`, or any other build. DSH neither injects the helper, parses the shell program, nor compiles repository source. The helper validates the metadata after the preceding build, requires the configured entry to resolve to a file within `.dsh-plugin`, validates and copies declared static assets, and writes the prepared `dsh-plugin.mjs` wrapper. The installed package must retain a `prepack` declaration containing that helper command; a missing dependency, wrapper, or build output fails before a cache generation becomes usable.
The generated wrapper first mounts the DSH-owned static runtime for skills and MCP definitions, then dynamically imports and unwraps the explicit entry and mounts it as a child. The wrapper statically declares dependencies implied by the prepared manifest; an entry module's additional `inject` is discovered only when mounted and must already be available in the host composition. Both children must reach Cordis `ACTIVE`; an unsatisfied `inject` or startup exception rejects the repository Loader transaction instead of committing an inert generation. Loader removal, failed replacement, and parent disposal unwind the entry, skill providers, MCP clients, and their effects together.
`dsh-mcp-client` resolves its initial connection and tool synchronization promise as part of Plugin application. Its entry is an `async function`, not an ordinary function returning a Promise: Cordis identifies prototype-bearing ordinary functions as constructors and does not treat a constructor's returned Promise as startup work. A valid server's tools therefore exist before its parent repository wrapper activates and before a one-shot application starts its first model request. Its `failOnStartupError` config preserves optional standalone servers by default while letting repository adapters require their declared servers. Repository-translated MCP clients enable that mode, so initial connection, discovery, or tool-registration failure rejects the candidate generation and rollback still closes the transport.
## Trust boundary
Exact refs, source containment, credential-shaped environment scrubbing, prepared manifests, and immutable cache keys protect identity and composition integrity; they do not sandbox executable package input. Repository lifecycle scripts, transitive npm dependencies, the compiled entry, and spawned MCP servers can exercise the authority available to the DSH process and the Cordis services they receive. Users must therefore trust the selected repository and should pin immutable refs and grant Git only the narrow read credential needed for acquisition.
Model-visible behavior remains governed by the owning DSH seam. A repository entry may register tools, prompt sections, policies, commands, agents, or other effects, but anything reaching a model request still needs the corresponding logged DSH representation and lifecycle cleanup. The repository format grants code loading; it does not weaken those service contracts.
## Alternatives considered
**Keep code forbidden while allowing arbitrary package lifecycles.** Rejected because installation already executes trusted repository code, so the restriction added no isolation and forced Plugin authors to publish or maintain a second integration path.
**Have DSH compile repository TypeScript.** Rejected because compiler choice, module layout, generated chunks, native dependencies, and package metadata belong to the npm package. Running the package's declared build preserves the same boundary as other Git dependencies.
**Import `main`, `exports`, or another discovered entry implicitly.** Rejected because an npm package may contain utilities or an MCP executable that is not a Cordis Plugin. The explicit `dsh.entry` field makes code activation reviewable and lets preparation validate the packed path.
**Add a closed manifest field for every future DSH contribution.** Rejected as the universal extension mechanism. Skills and common MCP files retain useful portable static adapters, while DSH-native behavior composes through the existing Cordis Plugin and service contracts.
## Consequences
- A TypeScript DSH Plugin can live in a GitHub repository, install ordinary npm dependencies, compile during `prepack`, and run without publishing the Plugin package to npm.
- Static-only repository packages remain valid and retain import-free wrappers; adding `dsh.entry` opts that package into runtime code import.
- A package build, dependency install, entry import, unmet service, or Plugin startup failure prevents the candidate generation from replacing the last good configuration.
- Initial MCP synchronization can lengthen application startup by the MCP SDK's per-request timeout, and a repository-declared server that is unavailable or cannot publish its complete tool generation prevents that candidate generation from activating.
- Repository code receives host authority, so source review and immutable pinning are operational security requirements rather than optional hardening.
## Testing
Repository-format tests prepare and mount default-export code entries through the real Loader, observe an entry-owned service, remove the Loader row, and observe cleanup; they also retain skill/MCP preparation, containment, damaged-package, pending-service, and rollback coverage. MCP lifecycle tests require `apply` to settle only after initial tool publication, preserve opt-in contained startup failure, and prove strict connection or tool-registration rejection still closes the client.
The Node 24 consumer acceptance uses the actual built `dsh run` command with a fresh DSH home and an authenticated private GitHub source pinned to the pull request's exact head SHA. The test packs the current repository Plugin build with the same private-field removal and workspace-dependency pinning used for publication, serves its packument and tarball from a job-local npm registry, and directs the Git package's ordinary scoped npm resolution there. That repository package obtains `dsh-plugin-prepare` from the simulated published dependency, installs its other pinned runtime and development dependencies, type-checks and bundles TypeScript during `prepack`, prepares a skill plus a stdio MCP server and `dsh.entry`, exposes the skill and MCP schema in the first real model request, executes the MCP tool, and lets the compiled Cordis entry append a second marker to the result observed in the following request. Registry and cache assertions require npm resolution to reach the simulated publication, source files to be absent from the packed installation, and both built modules, their installed dependency, copied assets, and generated wrapper to be present.

View File

@@ -1,51 +0,0 @@
# Agent Note: 受信任 repository 包加载 Cordis 代码
状态:已实现
[English](2026-08-08-trusted-repository-package-code.md) | 中文
## 问题
独立 repository 格式已经会安装选定的 Git 包,并以宿主权限运行其依赖和生命周期代码,但它向 DSH 暴露的只有复制后的 skill技能和 MCP 元数据。禁止 Cordis 入口并未建立安全边界:包安装过程仍会执行受信任代码,而这项限制却阻止包贡献 Harness 架构本就用于组合的插件行为。
仓库作者还需要保持普通 TypeScript NPM 包的结构。如果要求发布到 NPM、把预生成的 JavaScript 签入 Git或使用 DSH 自有的 TypeScript 编译器Git 源的能力就会弱于通过开发者自有 SDK 项目安装的同一个包。首个模型请求必须看到该包启动的所有 MCP 工具;仅在后台进行初始发现,会让一次成功安装在应用边界上具有不确定性。
## 决策
已配置的 repository 包是受信任代码。其 `.dsh-plugin/package.json` 可以连同 `dsh.skills``dsh.mcpServers` 声明 `dsh.entry`,也可以用它取代二者;`dsh.entry` 是指向该包内已编译 ESM Cordis 插件的相对路径。至少需要一种贡献。入口可以使用 namespace 导出或 default export并沿用 Cordis 对 `name``inject``Config`、注册、启动失败和 effect 作用域清理的常规语义。
包自行负责其 NPM 依赖和构建工具链。它声明已发布的 `@deepseek-ai/dsh-repository-plugin` 包以取得 `dsh-plugin-prepare` 可执行文件。`scripts.prepack` 是由包作者编写的非空命令,必须调用该依赖提供的辅助程序,但可以先运行 `tsc``tsdown` 或其他任意构建。DSH 不会注入辅助程序,也不会解析该 shell 程序或编译 repository 源码。辅助程序会在前序构建之后校验元数据,要求已配置入口解析到 `.dsh-plugin` 内的文件,校验并复制已声明的静态资源,再写入已准备的 `dsh-plugin.mjs` 包装层。已安装包必须保留包含该辅助命令的 `prepack` 声明;依赖、包装层或构建输出缺失会在缓存 generation 可用前导致失败。
生成的包装层先挂载 DSH 自有的静态运行时来处理 skill 和 MCP 定义,再动态导入显式入口、解包其导出并将其挂载为子级。包装层会静态声明已准备 manifest元数据清单所隐含的依赖入口模块的额外 `inject` 只有在挂载时才会被发现,并且此时必须已存在于宿主组合中。两个子级都必须进入 Cordis `ACTIVE`;无法满足的 `inject` 或启动异常会拒绝 repository Loader 事务,而不会提交未激活的 generation。Loader 移除、替换失败和父级 dispose资源释放会一并撤销入口、skill 提供方、MCP client 及其 effect。
`dsh-mcp-client` 会在插件应用期间完成其初始连接和工具同步 promise。其入口必须是 `async function`,而不是返回 Promise 的普通函数Cordis 会把带 prototype 的普通函数识别为 constructor不会把 constructor 返回的 Promise 当作启动工作。因此,有效 server 的工具会在父级 repository 包装层激活前、一次性应用发起首个模型请求前就已存在。其 `failOnStartupError` 配置默认保留独立可选 server 的行为,同时允许 repository adapter 要求已声明 server 必须可用。Repository 转换出的 MCP client 会启用该模式,因此初始连接、发现或工具注册失败会拒绝候选 generation回滚仍会关闭 transport。
## 信任边界
精确 ref、源路径包含约束、清除名称符合凭据模式的环境变量、已准备的 manifest 和不可变缓存键可以保护身份与组合完整性它们不会为可执行包输入提供沙箱隔离。Repository 生命周期脚本、传递性 NPM 依赖、已编译入口和 spawn 的 MCP server 可以行使 DSH 进程可用的权限,以及它们所获 Cordis 服务授予的权限。因此,用户必须信任所选仓库,应当固定不可变 ref并只授予 Git 获取源码所需的最小只读凭据。
模型可见行为仍由所属 DSH seam 管理。repository 入口可以注册工具、提示词段落、策略、命令、agent智能体或其他 effect但任何进入模型请求的内容仍须具有对应的 DSH 日志表示和生命周期清理。repository 格式授予代码加载能力;它不会削弱这些服务约定。
## 考虑过的替代方案
**继续禁止代码,但允许任意包生命周期。** 拒绝,因为安装过程本就执行受信任的 repository 代码,所以该限制没有提供隔离,反而迫使插件作者发布或维护第二条集成路径。
**由 DSH 编译 repository TypeScript。** 拒绝,因为编译器选择、模块布局、生成分片、原生依赖和包元数据属于 NPM 包。运行包所声明的构建,可以保持与其他 Git 依赖相同的边界。
**隐式导入 `main`、`exports` 或其他发现的入口。** 拒绝,因为 NPM 包可能包含并非 Cordis 插件的实用工具或 MCP 可执行文件。显式 `dsh.entry` 字段使代码激活可供评审,并让准备阶段校验打包后的路径。
**为未来每种 DSH 贡献添加封闭 manifest 字段。** 不采用它作为通用扩展机制。skill 和通用 MCP 文件仍保留有用的可移植静态适配器DSH 原生行为则通过现有 Cordis 插件与服务约定组合。
## 后果
- TypeScript DSH 插件可以存放在 GitHub 仓库中,安装普通 NPM 依赖,在 `prepack` 期间完成编译,并在无需把插件包发布到 NPM 的情况下运行。
- 仅含静态贡献的 repository 包仍然有效,并保留无 import 包装层;添加 `dsh.entry` 会使该包选择启用运行时代码导入。
- 包构建、依赖安装、入口导入、所需服务未满足或插件启动失败,都会阻止候选 generation 替换最后一个可用配置。
- 初始 MCP 同步可能因 MCP SDK 的单次请求超时而延长应用启动时间repository 声明的 server 不可用或无法发布完整工具 generation 时,该候选 generation 无法激活。
- Repository 代码获得宿主权限,因此源码评审和锁定不可变 ref 是运行安全要求,而不是可选加固措施。
## 测试
repository 格式测试通过真实 Loader 准备并挂载使用 default export 的代码入口,观察入口自有服务,移除 Loader 配置项,再观察清理;测试还保留针对 skillMCP 准备、路径包含约束、包损坏、等待服务和回滚的覆盖。MCP 生命周期测试要求 `apply` 只在初始工具发布后完成,保留可选择启用的启动失败收束行为,并证明严格连接拒绝或工具注册拒绝仍会关闭 client。
Node 24 消费方验收使用实际构建的 `dsh run` 命令、全新 DSH 主目录,以及锁定到 PRPull Request的精确 head SHA 且经过认证的私有 GitHub 源。测试会采用发布时相同的移除 `private` 字段和固定 workspace 依赖版本流程,对当前 repository 插件构建进行打包;再由作业本地 NPM 注册表提供其 `packument` 与 tarball并把 Git 包的常规 scoped NPM 解析指向该注册表。该 repository 包从模拟发布的依赖取得 `dsh-plugin-prepare`,安装其他固定版本的运行时依赖与开发依赖,在 `prepack` 期间对 TypeScript 进行类型检查和打包,准备一个 skill、一个 stdio MCP server 及 `dsh.entry`,在首个真实模型请求中暴露 skill 与 MCP schema执行 MCP 工具,并让已编译 Cordis 入口向结果追加第二个标记,供后续请求观察。注册表与缓存断言要求 NPM 解析必须命中模拟发布,打包安装中不存在源码文件,同时必须存在两个已构建模块、其已安装依赖、复制资源和生成包装层。

View File

@@ -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-09-client-conversation-node-assembly.md
2026-08-09-client-conversation-node-assembly.md: 16a39539064644e5467f701789a7e2ef1f7ff172
2026-08-09-client-conversation-node-assembly.zh.md: 0e0fbdf8f3320393022528e6e3fe2cf0d492a1d3
2026-08-09-client-conversation-node-assembly.md: f6cd7ea94d485d92fd4ea178751c08bd01ecb4b2
2026-08-09-client-conversation-node-assembly.zh.md: e550b7870a611ec625d7c2a738bf71947825ea58

View File

@@ -148,7 +148,7 @@ The Assembler verifies `node.key === context.key` and `node.target === target`.
`current` lets a Definition distinguish "never materialized" from "already materialized and now hidden." Assistant retry and Turn Error suppression use it to avoid illegal Node withdrawal.
A Definition may branch by target to construct different data, while matching, Context identity, and State remain target-neutral. This change registers only the `chat` builder; Trajectory remains on its independent `session-history` fold until it gains a registered target.
A Definition owns at most one view target; state-only Definitions omit both `target` and `buildViewNode()`. Chat and Trajectory register separate business Definitions even when they recognize the same durable Event family, while the shared Assembler supplies the same matching, replay, Location, and publication mechanics to both targets.
#### No generic `end()`
@@ -328,7 +328,9 @@ When business logic deliberately changes a materialized Node to hidden, it leave
The concrete Tool renderer remains governed by the [`ui-tool ownership decision`](2026-08-08-client-tool-presentation-ownership.md). Tool Definition supplies recursive root/subcall data, and `ui-tool` dispatches concrete presentation by the Tool-name keyed slot.
Trajectory has no registered target and does not consume the Chat Builder's legacy slice. Its activated `SessionHistoryInspection` keeps an independent history fold, while the ordinary Session snapshot no longer runs a second transcript fold. The Chat Builder retains its legacy slice for StatsLine and the top-level public compatibility fields; a future Trajectory migration does not change the Event Definition, Context, Reader, or Location contracts.
Trajectory registers its own target and business Definitions against the same Assembler and Session event window as Chat. Its target builder preserves the stage-oriented read model without consuming the Chat Builder's legacy slice or running an independent history fold. The Chat Builder retains its legacy slice for StatsLine and the top-level public compatibility fields; target-specific Definitions do not change the shared Context, Reader, or Location contracts.
The target-specific Trajectory Definitions, retained stage model, Steering adaptation, complexity bounds, and presentation hot paths are owned by the [Trajectory Context assembly decision](2026-08-11-trajectory-conversation-context-assembly.md).
## Runtime and render path
@@ -339,20 +341,17 @@ Session Event window
-> Context matches + State + Location
-> Definition.buildLocationData(step -> turn)
-> StepLocation.data / TurnLocation.data
-> Definition.buildViewNode(target = chat)
-> ChatSnapshotBuilder
-> order[] + keyed Node store + Location index + timeline
-> ChatView
-> ChatNodeSeat(key)
-> conversation.chat.node(entryKey = node.kind, hookContext = key)
-> slot-level useTurnData(businessKey)
-> Definition.buildViewNode() for its declared target
-> target View Builder
-> chat: ChatSnapshotBuilder -> ChatView -> keyed ChatNodeSeat
-> trajectory: TrajectorySnapshotBuilder -> stages/layout/table
```
## Verification
Runtime tests pin Definition lifecycle registration, exact-ID append, update-before-start collection followed by forward replay after start, prepend identity, Reader window-gap repair, transitive dependencies, Location closure, Step→Turn data phase order, Location data replacement, publication cadence, illegal withdrawal, and per-target Builders.
Conversation tests cover every built-in Definition, Assistant Step data, Turn Tail and Deliverables Turn data, Chat ordering and structural sharing, selector isolation, Assistant and Tool running-to-settled identity, nested Code Dispatch, steering, Compaction, Retry, interruption, load-older anchoring, and slot dispatch.
Conversation tests cover every built-in Chat Definition, Assistant Step data, Turn Tail and Deliverables Turn data, Chat ordering and structural sharing, selector isolation, Assistant and Tool running-to-settled identity, nested Code Dispatch, steering, Compaction, Retry, interruption, load-older anchoring, and slot dispatch. Trajectory tests cover its independently registered Message, Assistant, Tool, Compaction, Request-header, and boundary Definitions together with the preserved stage-oriented view model.
Slot type/runtime tests pin required parent-provided common inject, the `hookContext` type, Hook isolation across Node contexts, stable factory/Hook identity, and the absence of business-renderer rerenders for unrelated Session publications. Existing entry-owned Observable Hook tests continue to pin the path that does not use a contextual factory.
@@ -382,7 +381,7 @@ History-path tests cover complete replace, non-overlapping prepend, overlapping-
**Add generic `end()`, prepared, or window-reset lifecycles.** Rejected: businesses have different completion conditions, and a pagination gap is not a business lifecycle. Business Events update State, Location close triggers replay/build, and Reader dependencies own pagination invalidation.
**Register separate Event Definitions for Chat and Trajectory.** Rejected: identity, State, and Location are target-neutral. `buildViewNode(target)` and each Builder express view differences; Trajectory's independent history fold remains until it registers its own Builder.
**Reuse one Event Definition across Chat and Trajectory by branching in `buildViewNode(target)`.** Rejected: the views require different business State and intermediate records, so a shared Definition would make each package carry the other's conditions and payloads. Separate target-owned Definitions keep those choices local while sharing the Assembler's ingestion and lifecycle contracts.
**Add a generic layout model above final business Nodes.** Rejected: activity, tail candidacy, and layout enums would centralize current Chat business semantics in the engine again. Final Nodes carry renderer-required data directly and share only identity, ordering, and Location facts.
@@ -406,4 +405,4 @@ Steps and Turns become stable homes for cross-business aggregates. Turn Tail and
The cost is new Runtime contracts for Registry, Assembler, Location data, dependency replay, and per-target Builders, plus parent-owned common inject and per-occurrence `hookContext` in UI Slots. Definition authors must understand stable IDs, unique starts, forward replay, Step→Turn publication order, read-only Reader access, and the prohibition on Node withdrawal.
`useTurnData()` does not revoke the standard `useSession` capability from session-scoped renderers, so this boundary relies on API guidance and tests rather than capability isolation. Registry changes remain low-frequency full rebuilds; the Chat Builder still maintains a legacy slice for StatsLine and the top-level public fields, Trajectory still owns an independent history fold, and built-in Definitions currently remain centralized in `ui-conversation`. These compatibility boundaries do not return business interpretation to Session.
`useTurnData()` does not revoke the standard `useSession` capability from session-scoped renderers, so this boundary relies on API guidance and tests rather than capability isolation. Registry changes remain low-frequency full rebuilds; the Chat Builder still maintains a legacy slice for StatsLine and the top-level public fields, while Trajectory owns target-specific Definitions and a Builder over the shared Session window. Built-in Definitions remain in their respective UI packages, and these compatibility boundaries do not return business interpretation to Session.

View File

@@ -148,7 +148,7 @@ Assembler 校验 Node `key === context.key` 且 Node `target === target`。业
`current` 让 Definition 区分“从未生成”与“已经生成后需要隐藏”。Assistant retry 和 Turn Error suppression 使用它避免非法的 Node 撤回。
Definition 可以针对 target 分支构造不同 data但匹配、Context identity 和 State 保持 target-neutral。本次只注册 `chat` builder在拥有注册 target 之前Trajectory 继续使用独立的 `session-history` fold
一个 Definition 最多拥有一个 view target仅维护状态的 Definition 同时省略 `target``buildViewNode()`。即使 Chat 与 Trajectory 识别同一持久 Event 族,它们也分别注册自己的业务 Definition共享 Assembler 则为两个 target 提供相同的匹配、replay、Location 与发布机制
#### 不提供通用 `end()`
@@ -328,7 +328,9 @@ Assistant streaming 到 final、Tool running 到 settled 只更新同一个 Seat
具体 Tool renderer 仍由 [`ui-tool ownership decision`](2026-08-08-client-tool-presentation-ownership.md) 约束。Tool Definition 只交付递归 root/subcall data`ui-tool` 再按 Tool name keyed slot 分发具体表现。
Trajectory 尚未注册 target也不消费 Chat Builder 的 legacy slice。它已激活的 `SessionHistoryInspection` 继续维护独立 history fold而普通 Session snapshot 不再运行第二套 transcript fold。Chat Builder 为 StatsLine 和顶层公共兼容字段保留 legacy slice未来迁移 Trajectory 不改变 Event DefinitionContext、Reader 或 Location 契约。
Trajectory 针对与 Chat 相同的 Assembler 和 Session 事件窗口注册自己的 target 与业务 Definition。它的 target builder 保留 stage-oriented read model既不消费 Chat Builder 的 legacy slice也不运行独立 history fold。Chat Builder 为 StatsLine 和顶层公共兼容字段保留 legacy slicetarget 专属 Definition 不改变共享的 Context、Reader 或 Location 契约。
target 专属 Trajectory Definition、保留的 stage model、Steering 适配、复杂度上界与表现层热点由 [Trajectory Context 组装决策](2026-08-11-trajectory-conversation-context-assembly.md)负责。
## Runtime and render path
@@ -339,20 +341,17 @@ Session Event window
-> Context matches + State + Location
-> Definition.buildLocationData(step -> turn)
-> StepLocation.data / TurnLocation.data
-> Definition.buildViewNode(target = chat)
-> ChatSnapshotBuilder
-> order[] + keyed Node store + Location index + timeline
-> ChatView
-> ChatNodeSeat(key)
-> conversation.chat.node(entryKey = node.kind, hookContext = key)
-> slot-level useTurnData(businessKey)
-> Definition.buildViewNode() for its declared target
-> target View Builder
-> chat: ChatSnapshotBuilder -> ChatView -> keyed ChatNodeSeat
-> trajectory: TrajectorySnapshotBuilder -> stages/layout/table
```
## Verification
Runtime tests 固定 Definition 生命周期注册、exact-ID append、update-before-start 收集与 start 后正序 replay、prepend identity、Reader window-gap 修复、传递依赖、Location closure、Step→Turn data phase order、Location data replacement、publication cadence、非法撤回和 per-target Builder。
Conversation tests 覆盖全部内建 Definition、Assistant Step data、Turn Tail 与 Deliverables Turn data、Chat 排序和结构共享、selector isolation、Assistant/Tool running-to-settled identity、nested Code Dispatch、steering、Compaction、Retry、interruption、load-older anchoring 和 slot dispatch。
Conversation tests 覆盖全部内建 Chat Definition、Assistant Step data、Turn Tail 与 Deliverables Turn data、Chat 排序和结构共享、selector isolation、Assistant/Tool running-to-settled identity、nested Code Dispatch、steering、Compaction、Retry、interruption、load-older anchoring 和 slot dispatch。Trajectory tests 则覆盖它独立注册的 Message、Assistant、Tool、Compaction、Request-header 与 boundary Definition以及继续保留的 stage-oriented view model。
Slot type/runtime tests 固定父注册必须提供声明的 common inject、`hookContext` 类型、不同 Node context 的 Hook 隔离、factory/Hook identity 稳定,以及无关 Session publication 不重渲染业务 renderer。原 entry-owned Observable Hook 测试继续固定未使用 contextual factory 的路径。
@@ -382,7 +381,7 @@ Assembled Web snapshot、GUI 和浏览器场景覆盖真实 plugin graph。浏
**增加通用 `end()`、prepared 或 window reset 生命周期。** 拒绝:不同业务完成条件不同,分页缺口也不是业务生命周期。业务 Event 更新 StateLocation close 触发 replay/buildReader dependency 负责补页失效。
**为 Chat 与 Trajectory 注册两套 Event Definition。** 拒绝identity、State 和 Location 与 target 无关。视图差异由 `buildViewNode(target)` 和各自 Builder 表达Trajectory 在注册自己的 Builder 之前继续使用独立 history fold
**在同一个 Event Definition 内通过 `buildViewNode(target)` 为 Chat 与 Trajectory 分支。** 拒绝:两种视图需要不同的业务 State 与中间记录,共用 Definition 会迫使每个 package 携带另一边的条件与 payload。target 自有的 Definition 把这些选择留在本地,同时复用 Assembler 的摄入与生命周期契约
**在最终业务 Node 上再叠一层通用 layout model。** 拒绝activity、tail candidacy 和 layout enum 会把当前 Chat 的业务语义重新集中到引擎。最终 Node 直接携带 renderer 所需 data只共享 identity、排序和 Location 事实。
@@ -406,4 +405,4 @@ Step/Turn 成为业务间共享聚合的稳定宿主。Turn Tail 和 Deliverable
代价是 Runtime 新增 Registry、Assembler、Location data、依赖重放和 per-target Builder 契约UI Slots 也新增 parent-owned common inject 与 per-occurrence `hookContext`。Definition 作者必须理解稳定 ID、唯一 start、正序 replay、Step→Turn 发布顺序、只读 Reader 和 Node 不撤回规则。
`useTurnData()` 不撤销 session-scoped renderer 的标准 `useSession`,因此该边界依靠 API 引导和测试而不是能力隔离。Registry 变化仍是低频完整 rebuildChat Builder 继续为 StatsLine 和顶层公共字段维护 legacy sliceTrajectory 继续拥有独立 history fold内建 Definitions 暂时集中在 `ui-conversation`这些兼容边界不把业务解释权交还给 Session。
`useTurnData()` 不撤销 session-scoped renderer 的标准 `useSession`,因此该边界依靠 API 引导和测试而不是能力隔离。Registry 变化仍是低频完整 rebuildChat Builder 继续为 StatsLine 和顶层公共字段维护 legacy sliceTrajectory 则在共享 Session 窗口上拥有 target 专属 Definition 与 Builder。内建 Definition 分别留在所属 UI package这些兼容边界不把业务解释权交还给 Session。

View File

@@ -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-09-headless-direct-core-entry-point.md
2026-08-09-headless-direct-core-entry-point.md: 49afe2993de7302adbedcdf9e8e2347d6424ee2a
2026-08-09-headless-direct-core-entry-point.zh.md: 73c1cbe5ac777025f63f46751b1d5ccebbfe9676
2026-08-09-headless-direct-core-entry-point.md: b705df2e6d88e096ee3ba50a6156b815dbd98b98
2026-08-09-headless-direct-core-entry-point.zh.md: 439f4c21a2ce1741e8d483bc307508550da1bec7

View File

@@ -20,11 +20,11 @@ The shipped `headless` profile contains `dsh-base` and `dsh-headless`. The headl
`loadProfile` recognizes the exact installation-owned headless tuple (`dsh-base`, `dsh-web-app`, `dsh-headless`) and normalizes it to the shipped headless template while preserving every other manifest field. Extra, missing, or reordered bundle lists are user-owned and remain untouched.
This note owns the headless transport and completion contracts. [`dsh run` owns one-shot headless execution](../feature/2026-08-08-dsh-run-headless-command.md) owns the command grammar, [GUI layering and RPC protocol](2026-07-19-gui-layering-and-rpc-protocol.md) owns browser gateway boundaries, [web config-tree boot and transport layering](2026-07-24-web-config-tree-boot-and-transport-layering.md) owns the Web tree, and [the default model follows the picker](../feature/2026-08-07-default-model-follows-the-picker.md) owns persistence of the shared Agent default.
This note owns the headless transport and completion contracts. [Apps own their command lines](2026-08-06-app-owned-command-line.md) owns the current `dsh --profile headless` grammar; the former [`dsh run` decision](../../archived/feature/2026-08-08-dsh-run-headless-command.md) records the superseded launcher-owned grammar, [GUI layering and RPC protocol](2026-07-19-gui-layering-and-rpc-protocol.md) owns browser gateway boundaries, [web config-tree boot and transport layering](2026-07-24-web-config-tree-boot-and-transport-layering.md) owns the Web tree, and [the default model follows the picker](../feature/2026-08-07-default-model-follows-the-picker.md) owns persistence of the shared Agent default.
## Verification
Package tests use the real Session store and Agent registry around a scripted Agent factory to pin idle-to-idle aggregation, late asynchronous completion, terminal model diagnostics, other non-completed exits, direct failures, Loader-time disposal, and flush-before-exit ordering. The keyless assembled snapshots drive `dsh run` through a replayed tool round trip, record a `user/message` with `source.kind: 'user'`, and expose a terminal model failure on stderr. Built-bin acceptance reaches a mock provider through the published entry and requires final text on stdout, exit 0, and empty stderr. Config-dump acceptance excludes every Host, Web, and Client package from the shipped headless tree; PTY shutdown coverage requires no observation line and bounded disposal.
Package tests use the real Session store and Agent registry around a scripted Agent factory to pin idle-to-idle aggregation, late asynchronous completion, terminal model diagnostics, other non-completed exits, direct failures, Loader-time disposal, and flush-before-exit ordering. The keyless assembled snapshots drive `dsh --profile headless` through a replayed tool round trip, record a `user/message` with `source.kind: 'user'`, and expose a terminal model failure on stderr. Built-bin acceptance reaches a mock provider through the published entry and requires final text on stdout, exit 0, and empty stderr. Config-dump acceptance excludes every Host, Web, and Client package from the shipped headless tree; PTY shutdown coverage requires no observation line and bounded disposal.
## Alternatives considered
@@ -39,6 +39,6 @@ Package tests use the real Session store and Agent registry around a scripted Ag
## Consequences
`dsh run` provides a local Agent task rather than browser observation, Host APIs, or HTTP. Users who need those capabilities choose `dsh web`. Successful stderr is empty, completion follows durable flush, and the persisted Session remains available to later tooling. Its initial user message records `source.kind: 'user'` and therefore carries no ApiProxy `rpcId`.
`dsh --profile headless` provides a local Agent task rather than browser observation, Host APIs, or HTTP. Users who need those capabilities choose `dsh web`. Successful stderr is empty, completion follows durable flush, and the persisted Session remains available to later tooling. Its initial user message records `source.kind: 'user'` and therefore carries no ApiProxy `rpcId`.
ApiProxy carrier coverage stays in the ApiProxy package. Custom one-shot profiles may include Host or Web bundles explicitly, while the shipped profile and the recognized installation-owned tuple are Web-free.

View File

@@ -20,11 +20,11 @@ Status: implemented
`loadProfile` 识别安装过程拥有的精确 headless 元组(`dsh-base``dsh-web-app``dsh-headless`),将其规范化为随附的 headless 模板,并保留 manifest元数据清单的其他所有字段。带额外项、缺少项或顺序不同的组合包列表归用户所有保持不变。
本 Agent Note 负责 headless 的传输与完成约定。[`dsh run` 负责一次性 headless 执行](../feature/2026-08-08-dsh-run-headless-command.md)负责命令语法,[GUI 分层与 RPC 协议](2026-07-19-gui-layering-and-rpc-protocol.md)负责浏览器网关边界,[Web 配置树启动与传输分层](2026-07-24-web-config-tree-boot-and-transport-layering.md)负责 Web 插件树,[默认模型跟随选择器](../feature/2026-08-07-default-model-follows-the-picker.md)负责共享 Agent 默认值的持久化。
本 Agent Note 负责 headless 的传输与完成约定。[应用持有自己的命令行](2026-08-06-app-owned-command-line.md)负责当前的 `dsh --profile headless` 语法;原 [`dsh run` 决策](../../archived/feature/2026-08-08-dsh-run-headless-command.md)记录已被取代的启动器持有语法,[GUI 分层与 RPC 协议](2026-07-19-gui-layering-and-rpc-protocol.md)负责浏览器网关边界,[Web 配置树启动与传输分层](2026-07-24-web-config-tree-boot-and-transport-layering.md)负责 Web 插件树,[默认模型跟随选择器](../feature/2026-08-07-default-model-follows-the-picker.md)负责共享 Agent 默认值的持久化。
## 验证
包测试围绕脚本化 Agent 工厂使用真实的会话存储与 Agent 注册表固定空闲态到空闲态的聚合、延迟异步完成、终止态模型诊断、其他未完成退出、直接失败、Loader 加载期间的 dispose资源释放以及退出前 flush 的顺序。组装后的无密钥快照通过回放的工具往返驱动 `dsh run`,记录一条带 `source.kind: 'user'``user/message`,并在 stderr 暴露终止态模型失败。构建后二进制验收通过已发布入口访问 mock 提供方,并要求最终文本出现在 stdout、退出状态为 0 且 stderr 为空。配置转储验收排除随附 headless 树中的所有 Host、Web 与 Client 包PTY 关闭覆盖要求不出现观察行,并在有界时间内完成 dispose。
包测试围绕脚本化 Agent 工厂使用真实的会话存储与 Agent 注册表固定空闲态到空闲态的聚合、延迟异步完成、终止态模型诊断、其他未完成退出、直接失败、Loader 加载期间的 dispose资源释放以及退出前 flush 的顺序。组装后的无密钥快照通过回放的工具往返驱动 `dsh --profile headless`,记录一条带 `source.kind: 'user'``user/message`,并在 stderr 暴露终止态模型失败。构建后二进制验收通过已发布入口访问 mock 提供方,并要求最终文本出现在 stdout、退出状态为 0 且 stderr 为空。配置转储验收排除随附 headless 树中的所有 Host、Web 与 Client 包PTY 关闭覆盖要求不出现观察行,并在有界时间内完成 dispose。
## 考虑过的替代方案
@@ -39,6 +39,6 @@ Status: implemented
## 后果
`dsh run` 提供本地 Agent 任务而不是浏览器观察、Host API 或 HTTP。需要这些能力的用户选择 `dsh web`。成功时 stderr 为空,完成结果在持久化 flush 后推导,持久化会话仍可供后续工具使用。初始用户消息记录 `source.kind: 'user'`,因此不携带 ApiProxy `rpcId`
`dsh --profile headless` 提供本地 Agent 任务而不是浏览器观察、Host API 或 HTTP。需要这些能力的用户选择 `dsh web`。成功时 stderr 为空,完成结果在持久化 flush 后推导,持久化会话仍可供后续工具使用。初始用户消息记录 `source.kind: 'user'`,因此不携带 ApiProxy `rpcId`
ApiProxy 载体覆盖保留在 ApiProxy 包中。自定义一次性 profile 可以显式包含 Host 或 Web 组合包;随附 profile 与可识别的安装过程所属元组均不含 Web。

View File

@@ -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-09-layered-skill-registry.md
2026-08-09-layered-skill-registry.md: 3f092cfb4b722e3dd51fa4dc46c620259eaffa39
2026-08-09-layered-skill-registry.zh.md: 38b17329c8d46ee9bbd0863f3fae7cf6be39aa75
2026-08-09-layered-skill-registry.md: 73897c3cb7e0055ff59221b7ea47c5d6ced06991
2026-08-09-layered-skill-registry.zh.md: 655780d4ef154434d6debf478134d3293d6c564f

View File

@@ -24,7 +24,7 @@ The composition moves with it: the web-app bundle re-enables the base `skill` re
**A deployment-level skill reaches every preset-composed session that mounts `tool-skill`.** The repository-plugin e2e's skill root and assertions are restored; the shipped-Web e2e proves the badge row (the same host-registration shape) merges into a standard-preset agent's catalog while the host view stays global-only.
**Layer visibility and consumption stay separate choices.** A core-web agent can read the global layer in principle, but composes no `skill` tool — whether an agent has skills at all remains the preset's decision, made by mounting or omitting `tool-skill`.
**Layer visibility and consumption stay separate choices.** A `minimal` agent can read the global layer in principle, but composes no `skill` tool — whether an agent has skills at all remains the preset's decision, made by mounting or omitting `tool-skill`.
**Provider options are still the borrowed caller object.** `SkillViewOptions` extends `SkillLookupOptions`; the registry consumes `scope` and providers read only their own contract from the same readonly object, preserving the existing borrow-identity guarantee.

View File

@@ -24,7 +24,7 @@ agent-preset stack 曾把整个 skill 能力——注册表、本地提供方和
**部署级 skill 会到达每个挂载 `tool-skill` 的 preset 会话。**repository-plugin e2e 的 skill 根目录与断言已恢复shipped-Web e2e 证明 badge 行(同一种宿主注册形态)汇入 standard preset agent 的目录,而宿主视图保持仅全局。
**层可见性与消费仍是两个独立选择。**core-web agent 原则上可读全局层,但不组合 `skill` 工具——agent 是否拥有 skill 依旧由 preset 通过挂载或省略 `tool-skill` 决定。
**层可见性与消费仍是两个独立选择。** `minimal` agent 原则上可读全局层,但不组合 `skill` 工具——agent 是否拥有 skill 依旧由 preset 通过挂载或省略 `tool-skill` 决定。
**提供方选项仍是借用的调用方对象。**`SkillViewOptions` 扩展 `SkillLookupOptions`;注册表消费 `scope`,提供方只从同一个只读对象中读取自己的契约,保持既有的借用恒等保证。

View File

@@ -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-10-product-subagent-providers-in-shared-host.md
2026-08-10-product-subagent-providers-in-shared-host.md: 33b6eb6cf7a6c19e9ea71cdb7dc8881e8052ef24
2026-08-10-product-subagent-providers-in-shared-host.zh.md: fd78c7a3fee4e4ee30d27d87c752e1a23576fd85

View File

@@ -0,0 +1,41 @@
# Agent Note: Product subagent providers live in the shared profile host
Status: implemented
English | [中文](2026-08-10-product-subagent-providers-in-shared-host.zh.md)
## Problem
The [Codex and Claude Code provider contracts](../feature/2026-08-04-claude-code-and-codex-subagent-backends.md) were first shipped as independently installable packages that a deployment loaded beside the common subagent tool. Agent Presets later became the ordinary owner of one agent's model-visible tools, but a preset cannot safely own these product providers: `ctx.subagents` is a process registry, provider names are unique, and host consumers resolve the same registry across sessions. Requiring a person to edit both a Profile and a Preset would also make a generic preset row incomplete by itself.
The placement decision must preserve two independent facts. Loading a provider must not start or authenticate a product, while enabling a tool must remain per preset so two sessions can expose different products. A global product switch, a provider instance per agent, or pre-enumerated combination presets would each create a second owner for one of those facts.
## Decision
Every shipped Profile loads the fixed `codex` and `claude-code` providers once through the base bundle's host plane. Loading either plugin only registers a dormant backend; the corresponding Codex or Claude process starts on the first actual delegation call. Agent Presets independently contribute ordinary `dsh-tool-subagent` rows for `subagent_codex` and `subagent_claude_code`, so a preset can expose neither tool, either one, or both without changing the provider registry.
This decision supersedes only the opt-in composition placement recorded by the provider-contract note. That note continues to own each product protocol, result mapping, cancellation, process-tree lifecycle, and evidence tiers. The [Agent Preset architecture](2026-08-03-per-session-agent-presets.md) continues to own the Host/Agent split, preset authoring, and the rule that edits affect only newly composed sessions.
The providers use products already selected by the host environment. Codex starts `codex` from `PATH`; Claude Code resolves `claude` through the shared subprocess execution world and passes the exact path to the official SDK. Profile loading does not install a product, create product state, probe a version, test authentication, or add product-specific settings. Missing commands and product failures remain local to the attempted delegation.
The current base dependency closure still includes the Claude Agent SDK's optional platform CLI payload even though production resolves the host `claude`. Removing that unused payload belongs to the separate product installation-closure follow-up; this placement decision neither installs it dynamically nor treats it as the production executable.
## Verification
The base Loader test proves both provider names register exactly once and no product process starts during Profile boot. Real Agent Preset composition covers none, Codex-only, Claude-only, and both tool sets, including generation isolation after an authored preset changes. Keyless ACP snapshots pin the model-visible tool schemas for one and both products, while provider tests separately prove native executable resolution, failure, cancellation, and process-tree quiescence.
## Alternatives considered
**Keep product providers opt-in at the Profile layer.** This preserves a smaller default dependency closure, but a copied or agent-authored Preset row is not usable unless the person also discovers and edits a second composition layer. It leaves the general Preset entry incomplete for these otherwise ordinary tools.
**Store global or per-Profile product enable switches.** A process switch competes with the Preset as owner of model-visible tools and cannot express two sessions using different combinations. Availability and authentication are deployment facts, not another persisted product state.
**Mount a provider inside every Agent Preset.** Provider names belong to a process registry, so the second session would collide with the first. Host consumers also need the registry independently of any one agent's lifetime.
**Ship four product-combination presets.** Four identities duplicate complete compositions to represent two independent tool rows. Ordinary rows already express the full matrix without adding roster or maintenance state.
## Consequences
A user manages both products through the same Agent Preset authoring path as other plugins, and each new session receives exactly the tools its chosen preset contributes. Every Profile carries two dormant provider registrations, so unused products consume package and module-loading footprint but no product process, login, model call, or product home.
The Host registry remains the single provider authority and each Preset remains the single model-tool authority. The trade-off is the current Claude SDK optional-payload installation cost, which stays explicitly deferred rather than being hidden behind another enable state or installer lifecycle.

View File

@@ -0,0 +1,41 @@
# Agent Note: 产品 subagent 提供方位于共享 profile 宿主
Status: implemented
[English](2026-08-10-product-subagent-providers-in-shared-host.md) | 中文
## 问题
[Codex 与 Claude Code 提供方约定](../feature/2026-08-04-claude-code-and-codex-subagent-backends.md)最初以可独立安装的包交付,由部署环境在通用 subagent 工具旁加载。Agent Preset 后来成为单个 agent智能体的模型可见工具的常规责任方但 preset 不能安全地拥有这些产品提供方:`ctx.subagents` 是进程级注册表,提供方名称唯一,而宿主消费方会跨会话解析同一个注册表。如果要求用户同时编辑 Profile 和 Preset也会使通用 preset 行本身不完整。
归属决策必须同时保留两个彼此独立的事实:加载提供方不得启动产品,也不得对产品执行身份验证;而工具是否启用仍须按 preset 决定,这样两个会话才能暴露不同的产品。全局产品开关、按 agent 创建提供方实例或预先枚举的组合 preset都会为其中一个事实另设第二责任方。
## 决策
每个随发行版交付的 Profile 都会通过 base 组合包的宿主平面,把固定的 `codex``claude-code` 提供方各加载一次。加载任一插件只会注册一个休眠后端;对应的 Codex 或 Claude 进程直到第一次实际委派调用时才启动。Agent Preset 分别通过普通的 `dsh-tool-subagent` 行贡献 `subagent_codex``subagent_claude_code`,因此一个 preset 可以不暴露任何工具、只暴露其中一个或同时暴露两者,而无需更改提供方注册表。
本决策仅取代提供方约定说明所记录的、原先由用户选择启用的组装位置。该说明仍负责每个产品的协议、结果映射、取消、进程树生命周期与证据层级。[Agent Preset 架构](2026-08-03-per-session-agent-presets.md)仍负责宿主与 agent 的划分、preset 创作,以及改动只影响新组装会话的规则。
这些提供方使用宿主环境已经选定的产品。Codex 启动 `codex`,该命令从 `PATH` 解析Claude Code 通过共享的子进程执行世界解析 `claude`,并把确切路径交给官方 SDK。加载 Profile 不会安装产品、创建产品状态、探测版本、测试身份验证,也不会新增产品专属设置。命令缺失和产品故障仍局限于发生问题的那次委派。
当前 base 依赖闭包仍包含 Claude Agent SDK 的可选平台 CLI命令行界面载荷尽管生产环境解析的是宿主提供的 `claude`。移除这份未使用载荷属于独立的产品安装闭包后续项;本归属决策既不会动态安装它,也不会将它当作生产可执行文件。
## 验证
base Loader 测试证明两个提供方名称都恰好注册一次,而且 Profile 启动期间不会启动产品进程。真实 Agent Preset 组装覆盖不暴露任何工具、仅暴露 Codex、仅暴露 Claude 和同时暴露两者这四种工具集合,也覆盖自行创作的 preset 发生改动后的代际隔离。无密钥 ACPAgent Client Protocol快照固定单个产品与两个产品同时启用时的模型可见工具 schema提供方测试则另行证明原生可执行文件解析、失败、取消和进程树完全停稳。
## 考虑过的替代方案
**将产品提供方保留为 Profile 层的按需启用项。** 这样可缩小默认依赖闭包,但复制或由 agent 创作的 Preset 行无法直接使用,除非用户还发现并编辑第二个组装层。对于这些本来与其他工具无异的工具,通用 Preset 入口仍不完整。
**存储全局或按 Profile 配置的产品启用开关。** 进程级开关会与 Preset 争夺模型可见工具的责任归属,也无法表示两个会话使用不同组合。可用性与身份验证属于部署事实,并非另一份需要持久化的产品状态。
**在每个 Agent Preset 内挂载一个提供方。** 提供方名称属于进程级注册表,因此第二个会话会与第一个冲突。宿主消费方也需要独立于任何单个 agent 的生命周期使用该注册表。
**交付四个产品组合 preset。** 四个身份会复制完整组装,只为表示两条独立的工具行。普通行已经能表达完整矩阵,无需新增名单或维护状态。
## 后果
用户通过与其他插件相同的 Agent Preset 创作路径管理两个产品,每个新会话只会获得其所选 preset 所贡献的工具。每个 Profile 都携带两个休眠的提供方注册,因此未使用的产品会产生包和模块加载开销,但不会启动产品进程、登录、调用模型或创建产品主目录。
宿主注册表仍是提供方的唯一权威,每个 Preset 仍是模型工具的唯一权威。代价是当前 Claude SDK 可选载荷的安装成本继续被明确延期处理,而不会隐藏在另一种启用状态或安装程序生命周期之后。

View File

@@ -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-11-trajectory-conversation-context-assembly.md
2026-08-11-trajectory-conversation-context-assembly.md: 7d0aea2fc09f0f04bd5de923bee15c42a773489a
2026-08-11-trajectory-conversation-context-assembly.zh.md: be3903bc62a8b1cc21bd9ddac450541ad4679ff0

View File

@@ -0,0 +1,105 @@
# Agent Note: Trajectory assembly from registered Conversation Contexts
Status: implemented
English | [中文](2026-08-11-trajectory-conversation-context-assembly.zh.md)
## Problem
Trajectory maintained an independent Session History source and folded the complete loaded Event window into Assistant, Tool, message, Request-header, and Compaction state. Chat already assembled the same Event families through registered Conversation Definitions. The two paths duplicated business correlation and pagination behavior, and a Trajectory structural update copied or rescanned work proportional to the raw Event count even when one business object changed.
Reusing Chat's final Nodes would not solve the ownership problem. Trajectory needs request lifecycles, running Assistant state, prompt inheritance, Tool schemas, timing records, and a stage-oriented read model that Chat does not consume. Sharing final Node payloads would couple both views to the union of their requirements.
The migration also had to preserve durable steering classification. A `user/message` does not say whether it opened a Turn or was claimed from the `next-step` inbox, and an older page can supply the missing inbox predecessor or Location after the message has already materialized.
## Decision
Trajectory registers target-owned Conversation Definitions and a `trajectory` View Builder against the shared [`ConversationNodeAssembler`](2026-08-09-client-conversation-node-assembly.md). Session owns one contiguous Event window and publishes both Chat and Trajectory snapshots through `Session.views`; it does not run a second Trajectory history source or business fold.
Each Definition belongs to one target. Chat and Trajectory may recognize the same durable Event family, but they keep separate State and final Node payloads. They share only the Assembler's exact-ID matching, ordered Matches, Location facts, Reader dependencies, publication scheduling, and replace/prepend/append lifecycle.
The existing [Trajectory inspection ledger](../feature/2026-07-27-trajectory-inspection-ledger.md) remains the view model. The Trajectory Builder converts materialized target Nodes into its established `eventNodes`, Requests, Tool schemas, running calls, and Location map; layout, table virtualization, selection, Overview, and inspector behavior do not become generic Conversation contracts.
### Business Definitions
| Business | Context identity | State assembly | Trajectory contribution |
|---|---|---|---|
| `next-step` inbox | splice Event seq | Apply the splice to the nearest preceding inbox Context | State only; no visible Node |
| User, steering, or injected message | message Event seq | Read the preceding inbox State and classify the durable message | Input or context Node |
| Assistant and ordinary Request | `turn:step` | Fold `step/start`, chunks, final message, retry, and `step/end` | Final Assistant, partial Assistant, and Request |
| Root Tool call | root call ID | Fold root call/result and nested Code Dispatch events into one call tree | Final or running Tool tree |
| Compaction | compaction ID | Fold start, summary, end, and replacement checkpoint | Compaction Request |
| Request header | header Event seq | Read the preceding header and retain effective prompt plus the actual change | Prompt and Tool-schema source |
| Session and Turn boundaries | boundary Event seq | Retain closure time and error facts | Interrupted Compaction or failed ordinary Request |
Every correlating Event must expose the same business ID directly. Code Dispatch uses `rootCallId`, Compaction uses its compaction ID, and ordinary Tool and retry events retain their protocol identities even when a specific Definition correlates by `turn:step`. Legacy records that lack the required correlation ID are ignored by that Definition rather than merged into an `undefined` Context or crashing the Session.
Assistant chunks update only their `turn:step` Context. Content-bearing chunks request animation-frame publication; usage and finish chunks update State without forcing their own frame. A final message, retry, or boundary publishes immediately. Completed Assistant State retains assembled blocks, timing, usage, and retry facts rather than copying the raw chunk ledger into the target snapshot.
### Steering from predecessor Contexts
Trajectory reconstructs steering from durable inbox history, using the same identity rule as the [Chat steering decision](../feature/2026-08-04-web-context-source-and-steer-marks.md) without sharing Chat's final Node.
Each `agent/inbox/spliced` Event targeting `next-step` starts an invisible Context identified by its Event seq. Its `start()` reads the nearest earlier inbox Context, applies the splice, and stores the pending identities plus the cumulative set of claimed message IDs. A later user-origin `user/message` reads the nearest earlier inbox Context: a claimed ID produces a Steering Node, while every other user-origin message produces an ordinary User Node.
A Reader miss while older history remains records a window-gap dependency. When prepend supplies the missing predecessor, the Assembler replays the affected inbox chain and message Contexts in forward Event order. Historical page direction therefore cannot permanently misclassify a message.
The message Event's Location places steering in the owning Step. If the loaded history window lacks enough boundary Events to resolve that Location, layout uses the following Assistant step as the positional fallback. A running Request marker follows leading steering input in the same Step, so the marker denotes the model Request caused by that input rather than appearing before it.
### Window paths and complexity
Let `E` be the loaded raw Event count, `P` one newly prepended page, `D` the number of Trajectory Definitions, `C` the number of materialized Trajectory Context contributions, and `Mᵣ` the total Matches held by Contexts invalidated by a prepend. `D` is a small registered set; streaming chunks aggregate into one Assistant Context, so `C` is normally much smaller than `E`.
| Path | Context work | Target snapshot work | Result |
|---|---|---|---|
| Initial tail or reconnect replace | Match the loaded window in `O(E × D)` and build State in forward Event order | Build and order `C` contributions | A full replace remains proportional to the loaded window |
| Older-page prepend | Match only fresh Events and replay only Contexts whose Match, Location, or Reader answer changed, in `O(P × D + Mᵣ)` | Rebuild the stage snapshot from `C` contributions | Business folding does not restart over all `E` Events |
| Live append | Match in `O(D)`, locate the keyed Context in `O(1)`, and update only that State | Replace a same-anchor contribution in `O(1)` before snapshot assembly | Business correlation is independent of loaded Event history |
The Builder stores contributions by Context key and keeps a key-to-position index. A content update with the same anchor replaces one contribution in place; a new contribution or anchor change rebuilds and sorts contribution order. Snapshot assembly then walks `C` contributions, indexes Request headers and Tool schemas with Maps, and handles Compaction boundaries and Turn errors with linear cursors or indexes.
Final Event and Request ordering keeps a publication's current upper bound at `O(C log C)`. The migration removes repeated reverse lookups and the old raw-history refold, but it does not claim end-to-end `O(1)` publication. Chat retains its existing keyed snapshot behavior and complexity; adding the Trajectory target does not make Chat scan Trajectory Contexts or Nodes.
### Independent presentation hot paths
The Context migration and the following presentation optimizations solve different costs. These reductions preserve the existing view model and are theoretical from call counts and asymptotic behavior; this decision does not claim benchmark measurements.
| Hot path | Retained behavior | Expected reduction |
|---|---|---|
| Markdown summaries | Layout retains source Markdown; each stable Table record memoizes its displayed summary by content, while Detail parses only the selected record | A one-record append reparses the changed visible record instead of every Markdown record |
| Search text | `TrajectorySearchIndex` linearly checks stable Record IDs and source signatures, but normalizes Markdown only for changed records and commits updates in three-second batches | Signature comparison remains `O(C)`; expensive normalization follows the changed-record count, and continuous frame updates collapse into one batch per interval |
| Timeline tooltip | Timing text is computed after the delayed tooltip opens | A render with no open tooltip performs no per-span label formatting |
| Following Assistant lookup | One reverse pass records the next Assistant for every input position | The former repeated forward lookup falls from worst-case `O(C²)` to `O(C)` |
| Group duration | Fixed decimal grouping replaces `toLocaleString('en-US')` for the invariant English numeric shape | Complexity remains linear in Groups, but the Intl formatter leaves the repeated render path |
Display memoization and search indexing stay separate. Search must include off-screen records and may lag live changes by the throttle interval; Table rendering must update the visible changed record immediately and must not inherit the index's commit cadence.
## Alternatives considered
**Keep the independent Session History fold and optimize it locally.** Rejected: caches could reduce selected hot paths, but Trajectory would still own a second Event window, pagination repair, request inspection fold, and business-correlation implementation beside Chat.
**Reuse Chat Definitions and branch on a `target` argument in `buildViewNode()`.** Rejected: Trajectory needs different State and intermediate records, not only another React renderer. One Definition would carry both views' payloads and conditionals and would invalidate unrelated target data when either view changed.
**Create a Trajectory-specific Assembler.** Rejected: exact-ID routing, update-before-start collection, prepend replay, Location repair, Reader dependencies, and publication cadence are not Trajectory-specific. A second engine would recreate the lifecycle duplication this change removes.
**Add generic Surface, rewind, fanout, or settled lifecycle concepts.** Rejected: the current durable Event stream does not require a generic Surface branch, and Session or Turn boundaries are target business inputs rather than a reason to fan out one Event over every historical Context. Completion remains business State interpreted with Location closure.
**Replace the Trajectory stages with generic Conversation Nodes.** Rejected: stages organize requests, timing, schemas, and table layout for one view. Making them engine contracts would constrain a future plain Session-log view and return view-specific composition to Client Runtime.
**Share one Markdown cache between display and search.** Rejected: display is immediate and viewport-bound, while search covers the complete loaded record set and intentionally batches updates. A shared cache would couple correctness and scheduling across unrelated consumers.
## Verification
Runtime tests pin target registration, exact-ID append, update-before-start replay, prepend identity, Reader window-gap repair, Location replay, and isolation between Chat and Trajectory snapshots.
Trajectory Definition and Builder tests pin Assistant streaming and interruption, nested Tool calls and parallel interruption, Compaction and prompt inheritance, Steering classification and Step placement, Request marker order, stable contribution replacement, and prepend expansion. Table, layout, Timeline, and search tests pin deferred Markdown work, throttled index updates, tooltip-time formatting, and stable search results across append and prepend.
## Consequences
Trajectory business assembly now scales with the changed page or keyed Context instead of restarting from the complete raw Event window. Target-owned Definitions can evolve independently from Chat while retaining one Session window and one set of lifecycle rules. Steering becomes a first-class Trajectory record at its actual Step position without adding steering-specific state to Session.
The retained stage-oriented Builder still performs work proportional to materialized Trajectory contributions and may sort on publication. The search index still performs a light linear signature pass when its input layout changes. These costs are explicit target-view work, not hidden full Event refolding.
Definition authors must provide stable protocol identities. Old Events without a required ID can disappear from the affected Trajectory business view, which is preferable to joining unrelated records or failing history load; producers that require faithful display must log the identity.
The [Conversation assembly decision](2026-08-09-client-conversation-node-assembly.md) remains the authority for the generic Context, Reader, Location, and publication contracts. The [Trajectory ledger decision](../feature/2026-07-27-trajectory-inspection-ledger.md) remains the authority for table hierarchy, virtualization, inspector, and interaction behavior. This Note owns how Trajectory adapts those two decisions and why the adaptation does not share final Nodes with Chat.

View File

@@ -0,0 +1,105 @@
# Agent Note: Trajectory 基于注册式 Conversation Context 组装数据
Status: implemented
[English](2026-08-11-trajectory-conversation-context-assembly.md) | 中文
## 问题
Trajectory 曾维护独立的 Session History 数据源,并把完整的已加载 Event 窗口折叠为 Assistant、Tool、消息、Request header 和 Compaction 状态。Chat 已经通过注册式 Conversation Definition 组装相同的 Event 族。两条链路重复实现业务关联与分页行为即使只改变一个业务对象Trajectory 的结构更新仍会复制或重新扫描与原始 Event 数量成正比的数据。
复用 Chat 的最终 Node 无法解决职责问题。Trajectory 需要请求生命周期、运行中 Assistant 状态、提示词继承、Tool schema、计时记录和 stage-oriented read model而 Chat 不消费这些数据。共享最终 Node payload 会让两个视图都依赖双方需求的并集。
本次迁移还必须保留持久 steering中途引导分类。`user/message` 本身不说明它是开启了一个 Turn还是从 `next-step` inbox 被领取;更早页面还可能在消息已经物化后,才补齐缺失的 inbox 前驱或 Location。
## 决策
Trajectory 针对共享的 [`ConversationNodeAssembler`](2026-08-09-client-conversation-node-assembly.md) 注册 target 自有的 Conversation Definition 和 `trajectory` View Builder。Session 只维护一份连续 Event 窗口,并通过 `Session.views` 发布 Chat 与 Trajectory 快照;它不再运行第二套 Trajectory history source 或业务 fold。
每个 Definition 只属于一个 target。Chat 与 Trajectory 可以识别同一持久 Event 族,但分别维护自己的 State 和最终 Node payload。它们只共享 Assembler 的精确 ID 匹配、有序 Match、Location 事实、Reader 依赖、发布调度,以及 replace/prepend/append 生命周期。
既有的 [Trajectory 检查记录表](../feature/2026-07-27-trajectory-inspection-ledger.md)继续作为视图模型。Trajectory Builder 把已物化的 target Node 转换为原有的 `eventNodes`、Requests、Tool schema、运行中调用和 Location maplayout、表格虚拟化、选择、Overview 与检查器行为不会成为通用 Conversation 约定。
### 业务 Definition
| 业务 | Context 标识 | State 组装方式 | Trajectory contribution |
|---|---|---|---|
| `next-step` inbox | splice Event seq | 把 splice 应用到最近的前序 inbox Context | 只维护状态,不产生可见 Node |
| 用户、steering 或注入消息 | message Event seq | 读取前序 inbox State并对持久消息分类 | Input 或 context Node |
| Assistant 与普通 Request | `turn:step` | 折叠 `step/start`、chunk、最终消息、retry 和 `step/end` | 最终 Assistant、partial Assistant 与 Request |
| 根 Tool call | root call ID | 把根 call/result 与嵌套 Code Dispatch Event 折叠为一棵调用树 | 最终或运行中的 Tool tree |
| Compaction | compaction ID | 折叠 start、summary、end 和 replacement checkpoint | Compaction Request |
| Request header | header Event seq | 读取前一个 header保留生效提示词及真实变化 | Prompt 与 Tool-schema 来源 |
| Session 与 Turn 边界 | boundary Event seq | 保留关闭时间和错误事实 | 被中断的 Compaction 或失败的普通 Request |
每个关联 Event 都必须直接提供相同的业务 ID。Code Dispatch 使用 `rootCallId`Compaction 使用 compaction ID即使某个 Definition 按 `turn:step` 关联,普通 Tool 与 retry Event 仍保留各自的协议标识。缺少必要关联 ID 的旧记录由该 Definition 忽略,不会合入 `undefined` Context也不会导致 Session 崩溃。
Assistant chunk 只更新对应的 `turn:step` Context。带内容的 chunk 请求 animation-frame 发布usage 与 finish chunk 更新 State但不单独强制刷新一帧。最终消息、retry 或边界立即发布。已完成 Assistant State 只保留组装后的 block、计时、usage 与 retry 事实,不会把原始 chunk ledger 复制进 target snapshot。
### 通过前序 Context 恢复 steering
Trajectory 从持久 inbox 历史恢复 steering使用与 [Chat steering 决策](../feature/2026-08-04-web-context-source-and-steer-marks.md)相同的标识规则,但不共享 Chat 的最终 Node。
每条目标为 `next-step``agent/inbox/spliced` Event 都会启动一个以 Event seq 标识的不可见 Context。它的 `start()` 读取最近的前序 inbox Context应用 splice并存储待处理标识以及累计的已领取 message ID 集合。后续用户来源的 `user/message` 读取最近的前序 inbox Context已领取的 ID 生成 Steering Node其余用户来源消息生成普通 User Node。
仍有更早历史时Reader miss 会记录 window-gap 依赖。prepend 补齐缺失的前驱后Assembler 按 Event 正序重放受影响的 inbox chain 与 message Context。因此历史分页方向不会永久错误分类消息。
消息 Event 的 Location 会把 steering 放进所属 Step。如果已加载历史窗口缺少足够的边界 Event无法解析该 Locationlayout 就以后续 Assistant step 作为位置回退。同一个 Step 中,运行中 Request 标记排在前置 steering 输入之后,因此该标记表示由这条输入触发的模型 Request而不会出现在输入前面。
### 窗口链路与复杂度
`E` 为已加载原始 Event 数,`P` 为一次新 prepend 的页面,`D` 为 Trajectory Definition 数,`C` 为已物化的 Trajectory Context contribution 数,`Mᵣ` 为一次 prepend 使其失效的 Context 所持有的 Match 总数。`D` 是较小的注册集合;流式 chunk 会聚合到同一个 Assistant Context因此通常 `C` 明显小于 `E`
| 链路 | Context 工作量 | Target snapshot 工作量 | 结果 |
|---|---|---|---|
| 初始尾页或重连 replace | 以 `O(E × D)` 匹配已加载窗口,并按 Event 正序构造 State | 构造并排序 `C` 个 contribution | 完整 replace 仍与已加载窗口成正比 |
| 更早页面 prepend | 只匹配新 Event并只重放 Match、Location 或 Reader 答案发生变化的 Context成本为 `O(P × D + Mᵣ)` | 从 `C` 个 contribution 重建 stage snapshot | 业务 fold 不会从头重跑全部 `E` 个 Event |
| 实时 append | 以 `O(D)` 匹配,以 `O(1)` 找到 keyed Context并只更新对应 State | snapshot 组装前,以 `O(1)` 替换 anchor 未变的 contribution | 业务关联成本与已加载 Event 历史无关 |
Builder 按 Context key 保存 contribution并维护 key-to-position index。anchor 相同的内容更新会原位替换一个 contribution新增 contribution 或 anchor 变化才会重建并排序 contribution 顺序。随后snapshot assembly 遍历 `C` 个 contribution用 Map 索引 Request header 与 Tool schema并以线性游标或索引处理 Compaction boundary 与 Turn error。
最终 Event 和 Request 排序使单次发布的当前上界保持为 `O(C log C)`。本次迁移移除了重复反向查找和旧的原始历史 refold但不声称端到端发布达到 `O(1)`。Chat 保持既有 keyed snapshot 行为与复杂度;增加 Trajectory target 不会让 Chat 扫描 Trajectory Context 或 Node。
### 独立的表现层热点优化
Context 迁移与下列表现层优化解决的是不同成本。这些优化保留既有视图模型;收益来自调用次数和渐进复杂度推算,本决策不声称存在 benchmark 实测结果。
| 热点 | 保留的行为 | 预期减少的工作 |
|---|---|---|
| Markdown 摘要 | Layout 只保留源 Markdown每个稳定 Table record 按内容 memo 展示摘要Detail 只解析当前选中记录 | 单条 record append 只重解析发生变化的可见记录,而非全部 Markdown record |
| 搜索文本 | `TrajectorySearchIndex` 仍线性核对稳定 Record ID 与来源签名,但只为变化的 record 标准化 Markdown并以三秒批次提交更新 | 签名比较仍为 `O(C)`;昂贵标准化只随变化 record 数量增长,持续 frame update 每个时间窗合并成一个批次 |
| Timeline tooltip | 延迟 Tooltip 打开后才计算计时文案 | 没有打开 Tooltip 的 render 不执行逐 span label 格式化 |
| 后继 Assistant 查找 | 一次反向遍历为每个输入位置记录后续 Assistant | 原先重复向前查找的最坏复杂度从 `O(C²)` 降为 `O(C)` |
| Group duration | 以固定十进制分组替代固定英文数字形态下的 `toLocaleString('en-US')` | 复杂度仍与 Group 数线性相关,但重复 render 路径不再调用 Intl formatter |
展示 memo 与搜索索引彼此独立。搜索必须覆盖屏幕外 record并允许实时变化延迟一个 throttle 周期Table 必须立即更新发生变化的可见 record不能继承索引的提交节奏。
## 考虑过的替代方案
**保留独立 Session History fold只做局部优化。** 不予采纳:缓存可以降低部分热点,但 Trajectory 仍会在 Chat 之外拥有第二套 Event 窗口、分页修复、request inspection fold 与业务关联实现。
**复用 Chat Definition并在 `buildViewNode()` 中按 `target` 分支。** 不予采纳Trajectory 需要不同的 State 与中间 record不只是另一套 React renderer。单一 Definition 会携带两个视图的 payload 与条件,并在任一视图变化时让无关 target 数据失效。
**创建 Trajectory 专属 Assembler。** 不予采纳:精确 ID 路由、先 update 后 start 的收集、prepend replay、Location 修复、Reader 依赖与发布节奏都不是 Trajectory 特有行为。第二套引擎会重新制造本次改造要消除的生命周期重复。
**增加通用 Surface、rewind、fanout 或 settled 生命周期。** 不予采纳:当前持久 Event stream 不需要通用 Surface branchSession 或 Turn boundary 是 target 业务输入,不构成把一个 Event fanout 到全部历史 Context 的理由。完成条件仍由业务 State 结合 Location closure 判断。
**用通用 Conversation Node 替换 Trajectory stage。** 不予采纳stage 为单一视图组织 Request、计时、schema 和表格 layout。把它变成引擎约定会限制未来的朴素 Session-log 视图,并把视图专属组合重新放回 Client Runtime。
**在展示与搜索之间共享一套 Markdown cache。** 不予采纳:展示要求立即更新且受 viewport 约束,搜索则覆盖全部已加载 record并有意批量提交更新。共享 cache 会把两个无关消费方的正确性与调度节奏耦合起来。
## 验证
Runtime 测试固定 target 注册、精确 ID append、先 update 后 start 的 replay、prepend identity、Reader window-gap 修复、Location replay以及 Chat 与 Trajectory snapshot 隔离。
Trajectory Definition 与 Builder 测试固定 Assistant streaming 与 interruption、嵌套 Tool call 和并行 interruption、Compaction 与 prompt 继承、Steering 分类和 Step 位置、Request 标记顺序、稳定 contribution 替换与 prepend 扩展。Table、layout、Timeline 与搜索测试固定延迟 Markdown 工作、节流索引更新、Tooltip 展示时格式化,以及 append/prepend 期间稳定的搜索结果。
## 后果
Trajectory 业务组装的成本随变化页面或 keyed Context 增长,不再从完整原始 Event 窗口重新开始。target 自有 Definition 可以独立于 Chat 演进,同时继续共享一份 Session 窗口和一套生命周期规则。steering 会在实际所属 Step 位置成为一等 Trajectory record不需要向 Session 增加 steering 专属状态。
保留的 stage-oriented Builder 仍会执行与已物化 Trajectory contribution 数量成正比的工作,并可能在发布时排序。输入 layout 变化时,搜索索引仍会执行一次轻量线性签名检查。这些成本是显式的 target view 工作,不是隐藏的完整 Event refold。
Definition 作者必须提供稳定的协议标识。缺少必要 ID 的旧 Event 可能不会出现在受影响的 Trajectory 业务视图中;与合并无关记录或让历史加载失败相比,这是更安全的退化方式。要求完整展示的生产方必须记录该标识。
[Conversation assembly 决策](2026-08-09-client-conversation-node-assembly.md)继续作为通用 Context、Reader、Location 与发布约定的真源。[Trajectory ledger 决策](../feature/2026-07-27-trajectory-inspection-ledger.md)继续负责表格层级、虚拟化、检查器和交互行为。本 Note 负责说明 Trajectory 如何适配这两项决策,以及为何该适配不与 Chat 共享最终 Node。

View File

@@ -1,6 +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/feature/2026-07-30-config-only-repository-plugins.md
2026-07-30-config-only-repository-plugins.md: 35327a30e03c51311f634e05ade209ab93ae0155
2026-07-30-config-only-repository-plugins.zh.md: 5755045560da761b59f7c65e99d551f599c2b5b3
# pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-07-30-bounded-overwrite-diff-basis.md
2026-07-30-bounded-overwrite-diff-basis.md: 7a09934bd1798059de43a092f338d37aa9ccbd9a
2026-07-30-bounded-overwrite-diff-basis.zh.md: 1d6bdd1068d119aae859132d9f8216ca29d0dc11

View File

@@ -0,0 +1,31 @@
# Agent Note: Bound overwrite contextual-diff bases at the provider
Status: implemented
English | [中文](2026-07-30-bounded-overwrite-diff-basis.zh.md)
## Problem
`dsh-fs-local` returned the complete prior file in `FsWriteOutcome.before` so consumers could build a contextual overwrite diff. That presentation-only pre-read was unbounded: a large overwrite could allocate the entire prior file, and checking an earlier path stat alone could not enforce a limit because an external process could replace or grow the file between the stat and the read. A large replacement also made the contextual hunk approach the replacement size even when the prior file was small. This closes the deferred bound recorded by [result-time applied-hunk diffs](../../archived/architecture/2026-07-02-result-time-applied-hunk-diffs.md).
## Decision
`LocalFileSystem.Config.diffBasisMaxBytes` is a positive safe-integer deployment setting no greater than the runtime's Buffer-allocation and string-decoding limits, with a 10 MiB default. An overwrite supplies `before` only when the UTF-8 replacement is strictly below that limit and the prior file opened for the basis also ends below it. The prior read opens a descriptor, checks that descriptor, and reads at most the configured byte count in cancellation-aware chunks; reaching the boundary returns `null`. A size change after descriptor stat also returns `null`, even if the final size remains below the limit, because a partial prefix would be an incorrect diff basis. Binary or invalid UTF-8 prior content likewise returns `null`, as does any descriptor-phase errno — a prior file deleted or made unreadable between the caller's preflight and the basis open cannot fail a write the caller already committed to; only cancellation and non-errno faults propagate. These outcomes do not block the atomic write.
The local provider owns this decision because `before` is its optional, best-effort basis: it can avoid acquiring prior content that the configured pair limit has already made ineligible. `tool-fs` continues to own diff computation, retention, and presentation. The setting is independent of `tool-fs.readStreamMinSize`; read routing and overwrite presentation are different policies and need not share a value.
`before: null` asks consumers to use their existing whole-file fallback. The limit bounds only the extra prior-content acquisition and eligibility for a contextual pair. It does not bound the caller-owned replacement, the returned `after` value, or a consumer's fallback rendering.
## Alternatives considered
**Keep a hardcoded threshold equal to the read tool's streaming threshold.** Rejected because the read threshold is deployment-configurable and consumer-owned. Two same-valued constants would create an unenforced cross-package coupling, while the overwrite basis is itself a deployment memory/presentation choice.
**Gate only the prior side in the provider and cap new-content diffing in `tool-fs`.** Rejected because it would acquire prior text even when the provider's configured pair limit already excludes the replacement, and it would split one `before` eligibility rule across two plugins. Consumers remain free to impose additional output limits.
**Trust the initial `probe()` size before using an ordinary whole-file read.** Rejected because that size can become stale before the read. The descriptor reader must enforce the bound on the object it actually reads.
**Stream a contextual diff for arbitrarily large pairs.** Rejected for this bug fix because the current filesystem seam returns complete `before`/`after` strings and the current diff implementation consumes them. A streaming diff would require a separate cross-package protocol and presentation design.
## Consequences
Deployments can tune the extra overwrite-basis cost without changing read routing. At or above the exclusive limit, overwrites still succeed and remain visible through the whole-file fallback, but lose contextual hunks. Below the limit, the provider can still hold almost `diffBasisMaxBytes` of prior text in addition to the caller's replacement. The bounded descriptor read adds an open/stat/read sequence for eligible overwrites, while preventing a stale path probe from turning that sequence into an unbounded allocation.

View File

@@ -0,0 +1,31 @@
# Agent Note: 在提供方限制覆写上下文 diff 基础
Status: implemented
[English](2026-07-30-bounded-overwrite-diff-basis.md) | 中文
## Problem
`dsh-fs-local` 会在 `FsWriteOutcome.before` 中返回完整旧文件,供消费方生成覆写上下文 diff。这个仅用于展示的预读没有上限大文件覆写可能分配整个旧文件而仅检查较早的路径 stat 也无法真正实施上限,因为外部进程可以在 stat 与读取之间替换文件或扩大文件。即使旧文件很小,大替换内容也会使上下文 hunk 接近替换内容本身的大小。本改动关闭了 [result-time applied-hunk diff](../../archived/architecture/2026-07-02-result-time-applied-hunk-diffs.md) 中记录的暂缓上限事项。
## Decision
`LocalFileSystem.Config.diffBasisMaxBytes` 是一个不超过运行时 Buffer 分配和字符串解码上限的正安全整数部署配置,默认 10 MiB。只有当 UTF-8 替换内容严格低于该上限,且为生成基础而打开的旧文件最终也低于该上限时,覆写才提供 `before`。旧文件读取会打开文件描述符、检查该描述符,并按可响应取消的分块最多读取配置的字节数;一旦到达边界便返回 `null`。描述符 stat 后发生大小变化时同样返回 `null`,即使最终大小仍低于上限,因为部分前缀会成为错误的 diff 基础。旧内容为二进制或无效 UTF-8 时也返回 `null`;描述符阶段的任何 errno 同样如此——旧文件在调用方预检之后、基础读取打开之前被删除或变得不可读,不能让调用方已经提交的写入失败;只有取消和非 errno 故障会继续向上传播。这些结果都不会阻止原子写入。
本地提供方拥有该决策,因为 `before` 是它提供的可选、尽力而为的基础:当配置的成对上限已使替换内容不合格时,它可以避免获取旧内容。`tool-fs` 继续拥有 diff 计算、保留与展示。该配置独立于 `tool-fs.readStreamMinSize`;读取路由与覆写展示是不同策略,无需共享数值。
`before: null` 要求消费方使用既有的整文件回退。该上限只限制额外获取旧内容的成本,以及上下文内容对是否合格;它不限制调用方持有的替换内容、返回的 `after` 值或消费方的回退渲染。
## Alternatives considered
**保留一个与读取工具流式阈值相等的硬编码阈值。** 否决,因为读取阈值可由部署配置,且归消费方所有。两个同值常量会形成无法强制的一致性耦合,而覆写基础本身也是部署层面的内存与展示选择。
**提供方只限制旧内容一侧,并在 `tool-fs` 中限制新内容 diff。** 否决,因为当提供方配置的成对上限已经排除替换内容时,这仍会获取旧文本;同时会把同一条 `before` 合格规则拆到两个插件中。消费方仍可自由施加额外的输出限制。
**信任初次 `probe()` 的大小,再执行普通整文件读取。** 否决,因为该大小可能在读取前变旧;描述符读取必须对它真正读取的对象实施上限。
**为任意大的内容对流式生成上下文 diff。** 本次缺陷修复不采用,因为当前文件系统 seam 返回完整的 `before`/`after` 字符串,当前 diff 实现也消费这两个字符串。流式 diff 需要独立的跨包协议与展示设计。
## Consequences
部署可以调整额外的覆写基础成本,而不改变读取路由。达到或超过排他上限时,覆写仍会成功,并通过整文件回退保持可见,但不再提供上下文 hunk。低于上限时除调用方的替换内容外提供方仍可能持有接近 `diffBasisMaxBytes` 的旧文本。对于合格覆写,有上限的描述符读取会增加一次 open/stat/read 序列,同时防止陈旧路径探测把该序列变成无上限分配。

View File

@@ -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-03-cli-signal-shutdown-escalation.md
2026-08-03-cli-signal-shutdown-escalation.md: 55917400fac2728d13dc2cdd799a7e234b6ed661
2026-08-03-cli-signal-shutdown-escalation.zh.md: c7897a8d77e8c2ebad43cec4e12170b04c837350
2026-08-03-cli-signal-shutdown-escalation.md: 173d06482cd8a1fcbb985763cc313e3f9b170bc6
2026-08-03-cli-signal-shutdown-escalation.zh.md: efa52524199906cf636cb2b55cb4249857dc1348

View File

@@ -6,7 +6,7 @@ English | [中文](2026-08-03-cli-signal-shutdown-escalation.zh.md)
## Problem
The default telemetry mount added SIGINT/SIGTERM handlers to `dsh web` and the headless command (now `dsh run`) so process exit could drain the Cordis tree instead of dropping queued telemetry. Each handler used a one-way boolean latch and exited only after `ctx.fiber.dispose()` settled. Headless normal completion also awaited that disposal without a bound.
The default telemetry mount added SIGINT/SIGTERM handlers to `dsh web` and the headless command (now `dsh --profile headless`) so process exit could drain the Cordis tree instead of dropping queued telemetry. Each handler used a one-way boolean latch and exited only after `ctx.fiber.dispose()` settled. Headless normal completion also awaited that disposal without a bound.
A user then reproduced the headless command hanging immediately after the observation URL and ignoring repeated `Ctrl+C`; `DSH_TELEMETRY_DISABLED=1` removed the hang, while a standalone Node handler in the same Linux sandbox received SIGINT. This isolated the pending disposer to telemetry rather than terminal signal forwarding. OTel's `BatchLogRecordProcessor.shutdown()` awaits `exporter.forceFlush()` before the `exportTimeoutMillis`-bounded completion promise, and the OTLP exporter's `forceFlush()` waits directly on its in-flight HTTP Promise. A proxy/sandbox connection that never obtains a socket can therefore leave provider shutdown pending despite both configured SDK timeouts.

View File

@@ -6,7 +6,7 @@ Status: implemented
## 问题
默认挂载遥测后,`dsh web` 与 headless 命令(现为 `dsh run`)新增了 SIGINT/SIGTERM 处理器,使进程退出时可以排空 Cordis 插件树而不是丢弃排队中的遥测数据。每个处理器都使用单向布尔闩锁latch并且只有在 `ctx.fiber.dispose()` 结算后才退出。headless 正常完成时同样会无界等待整棵树执行 dispose资源释放
默认挂载遥测后,`dsh web` 与 headless 命令(现为 `dsh --profile headless`)新增了 SIGINT/SIGTERM 处理器,使进程退出时可以排空 Cordis 插件树而不是丢弃排队中的遥测数据。每个处理器都使用单向布尔闩锁latch并且只有在 `ctx.fiber.dispose()` 结算后才退出。headless 正常完成时同样会无界等待整棵树执行 dispose资源释放
随后有用户复现headless 命令在打印观察 URL 后立即卡死,重复按 `Ctrl+C` 也没有反应;设置 `DSH_TELEMETRY_DISABLED=1` 后不再卡死,而同一 Linux 沙箱中的独立 Node 信号处理器能够收到 SIGINT。这将待结算的 disposer 定位到遥测而非终端信号转发。OTel 的 `BatchLogRecordProcessor.shutdown()` 会先等待 `exporter.forceFlush()`,再进入受 `exportTimeoutMillis` 限制的完成 promiseOTLP 导出器的 `forceFlush()` 则直接等待正在进行的 HTTP Promise。因此代理沙箱连接始终无法取得 socket 时,即使已经配置两项 SDK 超时,也会让提供方关闭一直待结算。

View File

@@ -1,6 +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-08-trusted-repository-package-code.md
2026-08-08-trusted-repository-package-code.md: 387479b3b36a8bc5e145641ae40802b3090ced70
2026-08-08-trusted-repository-package-code.zh.md: ecc325c3dd0a9e823f1411c3a809c30ada486289
# pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-08-06-host-backed-web-preferences.md
2026-08-06-host-backed-web-preferences.md: d56a8d2e330b214a1922997e3cc7165fd0fb31e4
2026-08-06-host-backed-web-preferences.zh.md: 593646fe0845c20fb09cb7d115e6fa558226506e

View File

@@ -0,0 +1,43 @@
# Agent Note: Persist Web user preferences through Host settings
Status: implemented
English | [中文](2026-08-06-host-backed-web-preferences.zh.md)
## Problem
The Web Appearance, Language, and busy-Enter preferences lived in browser `localStorage`. Browser storage is scoped to an origin, so reopening `dsh web` on another port selected a different partition and lost choices even though both processes used the same DSH home. These are user-level product preferences; session selection, drafts, disclosure state, and other transient browser state remain page-local.
The first theme implementation moved only Appearance to Host settings but awaited its initial RPC before providing `ThemeService`. A slow or unavailable settings request therefore suspended the assembled page. It also subscribed after the read, could miss an invalidation in that window, did not carry namespace revisions on writes, and allowed queued writes from a disposed plugin to reach the Host.
## Decision
The owning Host halves register three schemas: optional `locale.preference` (`zh` or `en`, where absence delegates to the browser), `ui-theme.preference` (`light`, `dark`, or `system`, default `system`), and `ui-conversation.busyEnter` (`queue` or `steer`, default `queue`). The local settings provider stores explicit choices in `$DSH_HOME/settings.yaml`, which resolves to `~/.dsh/settings.yaml` under the default home. The API proxy explicitly exposes all three namespaces beside the other Web settings; registration alone never crosses that configuration boundary.
The client runtime provides one `bindSettingsScope` lifecycle per namespace — the browser mirror of the Host-side settings owner seam. It installs `settings/changed` and `connection/reset` listeners before starting a background initial read, so no settings transport can block plugin activation and an invalidation cannot fall into a read-before-subscribe gap, and it publishes a snapshot store (status, section value, revision, writability, host/memory mode) the domain service subscribes to. The default decoder validates each incoming section against the namespace's own serialized wire schema, rehydrated through dsh-client-schema-form, so domains carry no hand-written wire guards. Domain services take the scope as an ordinary constructor collaborator, publish their provisional defaults immediately—browser-derived locale, system theme, and Queue—then adopt an accepted Host section without writing it back; a service constructed without a scope (standalone dictionary or policy fixtures) simply stays process-local.
User changes update the live service synchronously and queue a `settings.mutate` path operation through `scope.set`. The scope serializes gestures, sends the latest known namespace revision as `expectedRevision`, records every successful revision, and lets only the latest write settlement republish live state. A rejected or failed latest write reloads Host state. Disposal rejects new work, skips queued operations, suppresses publication by the in-flight operation, and waits for that operation to settle before the plugin reaches quiescence.
Remote browsers cannot call the loopback-only configuration API, so their preferences remain process-local. Dynamic third-party theme ids remain in-process extensions outside the built-in Host schema; removing one resets the live registry without replacing the last durable built-in preference.
## Alternatives considered
**Keep `localStorage` and copy values between ports.** One origin cannot enumerate another origin's storage, and a Host relay would recreate the settings service around a browser-specific format.
**Mirror Host settings into `localStorage`.** A second authority requires boot and invalidation conflict rules while retaining the partition that caused the defect. The Host document is the sole durable source.
**Await the initial read to avoid a provisional render.** Configuration availability is not a prerequisite for drawing the page. A background read may cause one live convergence, but it keeps failure isolated and preserves the existing browser/system/default fallbacks.
**Give every domain its own settings controller.** The concurrency, revision, failure, invalidation, and disposal rules are identical; copying them already produced lifecycle drift in the theme implementation. Domain-owned schemas keep product policy out of the shared runtime.
**A per-field preference controller with paired sync/persist callbacks.** The first shared lifecycle synchronized one scalar field through a domain `sync` callback while the service wrote back through an injected `persist` callback. The mutual callbacks forced two-phase construction — a defaulted no-op writer later replaced via `bindPersistence` — every additional field of a namespace would have carried its own controller and whole-document read, and each domain re-declared a hand-written guard the registered wire schema already expresses. The namespace scope publishes a snapshot the service subscribes to and accepts writes directly, so the callback pair and the second construction phase do not exist.
**Move every `localStorage` entry into settings.** Current session, drafts, panel disclosure, trajectory display state, and similar entries are browser-instance state rather than user configuration. Promoting them would synchronize transient navigation state across tabs and ports without a product contract.
## Consequences
Appearance, Language, and busy-Enter choices follow the DSH user home across reloads, ports, and loopback origins. Direct edits to `settings.yaml` converge through the existing invalidation stream, while legacy `dsh.theme`, `dsh.locale`, and `dsh.conversation.busyEnter` entries are neither read nor written.
Boot may briefly show the domain default before the background read settles. A transient read failure keeps that default or the last good in-process value; reconnect retries. A write rejection can visibly restore the durable preference after the immediate local change.
Focused unit coverage pins schema registration, listener-before-read ordering, nonblocking activation, schema-validated section acceptance, revisioned ordered writes, stale-response containment, failure recovery, disposal quiescence, and remote memory mode. The namespace-granular scope also carries multi-field sections, so later configuration surfaces can ride the same lifecycle instead of hand-rolling describe/mutate synchronization. The keyless Web settings scenario writes all three preferences through the UI, verifies the YAML document and empty legacy storage, reloads, and boots another Host on a distinct port against the same DSH home.

View File

@@ -0,0 +1,43 @@
# Agent Note: 通过 Host settings 持久化 Web 用户偏好
Status: implemented
[English](2026-08-06-host-backed-web-preferences.md) | 中文
## 问题
Web 的 Appearance、Language 和繁忙态 Enter 偏好原本存在浏览器 `localStorage` 中。浏览器存储以 origin 为作用域,因此换一个端口重新打开 `dsh web` 会选中另一个存储分区并丢失选择,即使两个进程使用同一个 DSH home。这些是用户级产品偏好会话选择、草稿、折叠展开状态和其他瞬态浏览器状态仍保留在页面内。
第一版主题实现只把 Appearance 移入 Host settings但会在提供 `ThemeService` 之前等待初始 RPC。缓慢或不可用的 settings 请求因而会挂起组装后的页面。该实现还在读取后才建立订阅,可能错过此窗口内的失效通知;它写入时不携带 namespace revision并且允许已释放插件所排队的写入到达 Host。
## 决策
各领域所属的 Host half 注册三份 schema可选的 `locale.preference``zh``en`,缺失时交由浏览器决定)、`ui-theme.preference``light``dark``system`,默认为 `system`),以及 `ui-conversation.busyEnter``queue``steer`,默认为 `queue`)。本地 settings 提供方将显式选择存入 `$DSH_HOME/settings.yaml`,在使用默认 home 时,该路径解析为 `~/.dsh/settings.yaml`。API 代理会显式暴露这三个 namespace与其他 Web settings 并列;仅注册它们,绝不会跨越该配置边界。
客户端运行时为每个 namespace 提供一份 `bindSettingsScope` 生命周期——即 Host 侧 settings owner seam 的浏览器镜像。它在开始后台初始读取之前安装 `settings/changed``connection/reset` 监听器,因此任何 settings 传输都不会阻塞插件激活,失效通知也不会掉入先读取、后订阅的空档;它还会发布一个供领域服务订阅的快照 store状态、分节值、revision、可写性、host内存模式。默认解码器会对照该 namespace 自身的序列化 wire schema经 dsh-client-schema-form 还原)校验每个传入分节,因此各领域无需携带手写的 wire 校验器。领域服务把 scope 当作普通的构造函数协作者接收,立即发布各自的暂定默认值:由浏览器派生的 locale、系统主题和 Queue随后采纳已获接受的 Host 分节,但不将其写回;不带 scope 构造的服务——独立词典或政策 fixture测试前置数据——则仅停留在进程本地。
用户变更会同步更新实时服务,并经 `scope.set` 将一项 `settings.mutate` 路径操作排入队列。scope 会串行处理手势,以最新已知 namespace revision 作为 `expectedRevision` 发送,记录每次成功写入的 revision并且只允许最新写入的结算结果重新发布实时状态。最新写入被拒或失败时scope 会重新加载 Host 状态。插件释放会拒绝新工作、跳过已排队操作、抑制运行中操作发布状态,并等待该操作结算后才让插件达到完全停稳。
远程浏览器无法调用仅限回环请求的配置 API因此其偏好仅保留在进程内。动态第三方主题 id 仍是内置 Host schema 之外的进程内扩展;移除其中一个会重置实时注册表,但不会替换上一个持久化的内置偏好。
## 曾考虑的替代方案
**保留 `localStorage`,并在不同端口间复制值。** 一个 origin 无法枚举另一个 origin 的存储,而 Host 中继会围绕浏览器特有格式重新实现一套 settings 服务。
**将 Host settings 镜像到 `localStorage`。** 第二个权威来源会要求另外定义启动与失效时的冲突规则同时依然保留造成该缺陷的分区。Host settings 文档是唯一的持久化真源。
**等待初始读取,以避免暂定渲染。** 绘制页面不以配置可用为前置条件。后台读取可能引发一次实时收敛,但它会隔离失败,并保留既有的浏览器/系统/默认回落路径。
**让每个领域拥有自己的 settings 控制器。** 并发、revision、失败、失效与释放规则完全一致此前的主题实现已因复制这些规则产生生命周期漂移。由领域持有 schema可以避免把产品政策放入共享运行时。
**带成对 sync/persist 回调的逐字段偏好控制器。** 第一版共享生命周期经领域提供的 `sync` 回调同步单个标量字段,服务则经注入的 `persist` 回调写回。这对相互依赖的回调迫使构造分两阶段完成——写入器先默认为无操作,稍后经 `bindPersistence` 替换——namespace 每新增一个字段,本都得再携带一个自己的控制器和一次全文档读取,且每个领域都重新声明了一个已注册 wire schema 本已表达的手写校验器。namespace scope 发布一份供服务订阅的快照并直接接受写入,因此这对回调与第二个构造阶段都不存在。
**把每个 `localStorage` 条目都移入 settings。** 当前会话、草稿、面板展开状态、trajectory 显示状态和类似条目属于浏览器实例状态,而非用户配置。将它们提升为设置,会在没有产品契约的情况下,跨标签页和端口同步短暂导航状态。
## 后果
Appearance、Language 和繁忙态 Enter 选择会跟随 DSH 用户 home跨越重新加载、端口与回环 origin。直接编辑 `settings.yaml` 所产生的变更会通过现有失效流收敛,而旧的 `dsh.theme``dsh.locale``dsh.conversation.busyEnter` 条目既不会被读取,也不会被写入。
启动时可能会在后台读取结算前短暂显示领域默认值。短暂的读取失败会保留该默认值或上一个正确的进程内值;重连时会重试。写入被拒时,界面可能会在本地值立即变化后明显恢复为持久化偏好。
聚焦的单元测试覆盖 schema 注册、先监听后读取的顺序、非阻塞激活、经 schema 校验的分节接受、携带 revision 的有序写入、陈旧响应隔离、故障恢复、释放时完全停稳,以及远程端仅内存模式。以 namespace 为粒度的 scope 也承载多字段分节,因此后续的配置表面可以沿用同一份生命周期,而不必手搭 describe/mutate 同步。无密钥 Web settings 场景通过 UI 写入全部三项偏好,校验 YAML 文档并确认旧 `localStorage` 为空,重新加载,再使用同一个 DSH home 在不同端口上启动另一个 Host。

View File

@@ -1,6 +0,0 @@
# 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-08-npm-backed-git-repository-plugin-preparation.md
2026-08-08-npm-backed-git-repository-plugin-preparation.md: 958b932f82f4da3cf63aa911260411855e514409
2026-08-08-npm-backed-git-repository-plugin-preparation.zh.md: d2256e0eae303c371371b9b5ba1967105aa61834

View File

@@ -1,48 +0,0 @@
# Agent Note: npm-backed preparation makes GitHub repository Plugins self-contained
Status: implemented
English | [中文](2026-08-08-npm-backed-git-repository-plugin-preparation.zh.md)
## Problem
The repository Plugin authoring contract requires `scripts.prepack` to invoke `dsh-plugin-prepare`. Supplying that executable from the running DSH installation made a source package appear valid even when its own manifest could not obtain the helper. It therefore did not prove the behavior users need after `@deepseek-ai/dsh-repository-plugin` is published: an ordinary Git-hosted npm package must be installable and preparable from only its declared dependencies.
A selectable `.dsh-plugin` inside a pnpm workspace has a second isolation requirement. pnpm prepares a Git-hosted package by running the repository's preferred package manager before packing the selected subdirectory. A nested `pnpm install` can join the containing workspace; when the root lockfile does not list `.dsh-plugin` as an importer, pnpm can report success without installing dependencies declared only by that package. Its TypeScript build or prepare command then fails, or a pre-generated artifact hides the missing dependency.
The checked-in headless fixture mounts an already prepared wrapper. It proves runtime composition, not GitHub acquisition, npm resolution, or package-owned preparation.
## Decision
The `.dsh-plugin` package declares `@deepseek-ai/dsh-repository-plugin` as an ordinary development dependency and invokes its published `dsh-plugin-prepare` executable from `scripts.prepack`. The package may declare any other build and runtime dependencies and run arbitrary compilation before the helper. The repository Plugin package marks its Cordis and DSH peers optional so a helper-only development install resolves only the helper's actual `zod` runtime dependency; an application composition still supplies the peers used by the package's Cordis entry.
DSH does not materialize or prepend a prepare executable. `RepositoryCache` supplies only a transaction-owned `pnpm` wrapper: the outer install runs the pinned pnpm entry directly, while pnpm's hard-coded Git-package `pnpm install` reinvokes the same entry with `--ignore-workspace`. The selected package therefore owns dependency resolution even beneath another pnpm lockfile, and normal package-manager lifecycle `PATH` construction exposes `node_modules/.bin/dsh-plugin-prepare`. The temporary pnpm wrapper disappears after the child settles. The repository remains trusted package-manager input: all dependency and lifecycle code executes under the existing trust contract.
The Node 24 consumer lane passes an exact source derived from the pull request head repository and SHA. It uses the existing private DeepSeek Harness repository rather than creating another repository per run. A job-scoped Git configuration gives the read-only job token access to that exact private source and rewrites pnpm's SSH fallback to authenticated HTTPS.
The built-entry acceptance also creates an in-process npm registry. It stages the current built `@deepseek-ai/dsh-repository-plugin` as a publication artifact by removing `private`, replacing workspace protocols with the release version, and packing the declared files. The registry serves the resulting packument and tarball, while a job-local npm config directs only the `@deepseek-ai` scope to it. The real built `dsh run` child then fetches the exact Git source; that package resolves the helper through npm, type-checks and bundles a TypeScript Cordis entry and MCP server, prepares the adjacent skill, and loads all three contributions. A deliberately failing host `PATH` command proves the lifecycle selected the dependency-local executable. The acceptance also requires registry resolution and inspects the immutable prepared cache, so restoring a host-injected helper cannot satisfy it.
## Alternatives considered
**Inject `dsh-plugin-prepare` from the running DSH installation.** Rejected because it lets an incomplete repository manifest pass and tests a host-only path that npm consumers cannot reproduce.
**Publish the source fixture itself to npm.** Rejected because the product contract is specifically that the DSH Plugin remains Git-hosted; only the reusable preparation helper is an npm dependency.
**Create a new private GitHub repository in every CI run.** Rejected because the pull request repository at its exact head SHA is already a real authenticated private Git remote. Per-run repository mutation would add credentials, cleanup, and eventual-consistency failure modes without changing the acquisition path.
**Prepare after `RepositoryCache` installs the selected package.** Rejected because pnpm's packed subdirectory no longer contains sibling source assets referenced by paths such as `../skills`; preparation must happen before packlist.
**Clone GitHub repositories in DSH and bypass pnpm's Git fetcher.** Rejected because it would duplicate ref resolution, subdirectory selection, dependency installation, packlist behavior, and cache integrity already owned by the pinned package manager.
## Consequences
- A repository author can commit a `.dsh-plugin` package, TypeScript source, skills, and MCP definitions to GitHub without publishing that Plugin package to npm. The package must declare the published preparation dependency.
- Private GitHub sources use the host's standard Git authentication. CI proves that path with a temporary read-only configuration rather than persistent runner credentials.
- `prepack`, not `prepare`, is part of the authoring format. It may contain arbitrary package-owned build steps but must invoke the dependency-provided helper; missing dependency or lifecycle metadata fails before a cache generation is usable.
- A selected package in a pnpm repository installs from its own manifest rather than an enclosing workspace. It cannot rely on workspace-only hoisting; ordinary registry and relative `file:` dependencies remain package-owned inputs.
- Exact source strings identify immutable cache generations; a changed ref or source configuration selects another generation.
- Package dependencies, compilation, preparation, and the trusted `dsh.entry` contribution remain owned by the repository package and the [trusted-code decision](../architecture/2026-08-08-trusted-repository-package-code.md).
## Testing
`packages/boot/app-boot/tests/repository-cache.spec.ts` runs a package excluded from its source repository's root pnpm lockfile through a local Git subpath and requires relative `file:` dependencies to provide both its build command and `dsh-plugin-prepare`; it also proves that visible environment survives while credential-shaped variables are scrubbed. `packages/self-modification/repository-plugin/tests/repository-plugin.spec.ts` pins helper-bearing `prepack` metadata and preparation output. `examples/headless-agent/tests/keyless-smoke.e2e.ts` keeps the checked-in prepared fixture on that source contract. `apps/cli/tests/github-repository-plugin.built.e2e.ts` is the product acceptance: simulated published helper package, job-local npm registry, fresh DSH home, exact authenticated private GitHub source, actual built `dsh run`, package-owned TypeScript build, real MCP execution, code-entry transformation, mock LLM request observation, and prepared cache inspection.

View File

@@ -1,48 +0,0 @@
# Agent Note: 基于 NPM 的准备机制使 GitHub repository 插件自包含
状态:已实现
[English](2026-08-08-npm-backed-git-repository-plugin-preparation.md) | 中文
## 问题
repository 插件创作约定要求 `scripts.prepack` 调用 `dsh-plugin-prepare`。如果由正在运行的 DSH 安装提供该可执行文件,即使源包自身的 manifest元数据清单无法取得辅助程序它也会显得有效。因此这并未证明 `@deepseek-ai/dsh-repository-plugin` 发布后用户所需的行为:普通 Git 托管 NPM 包必须只依靠自身声明的依赖即可安装和准备。
pnpm workspace 内可选择的 `.dsh-plugin` 还有另一项隔离要求。pnpm 会在打包所选子目录前运行仓库首选的包管理器,以准备 Git 托管包。嵌套的 `pnpm install` 可能加入外层 workspace当根 lockfile 未把 `.dsh-plugin` 列为 importer 时pnpm 可能报告成功,却未安装仅由该包声明的依赖。随后,其 TypeScript 构建或准备命令会失败;也可能因为存在预生成产物,依赖缺失被掩盖。
签入仓库的 headless fixture测试前置数据挂载的是已准备好的包装层。它证明运行时组合而不证明 GitHub 获取、NPM 解析或包自有准备。
## 决策
`.dsh-plugin` 包将已发布的 `@deepseek-ai/dsh-repository-plugin` 声明为普通开发依赖,并在 `scripts.prepack` 中调用其已发布的 `dsh-plugin-prepare` 可执行文件。该包可以声明其他任意构建依赖与运行时依赖并在辅助程序前执行任意编译。repository 插件包把 Cordis 与 DSH 对等依赖peer dependency标为可选因此仅为使用辅助程序而进行的开发安装只会解析辅助程序实际依赖的 `zod` 运行时依赖;应用组合仍会提供该包 Cordis 入口所使用的对等依赖。
DSH 不会生成准备阶段可执行文件,也不会将其前置到 `PATH``RepositoryCache` 只提供一个由事务持有的 `pnpm` 包装脚本:外层安装直接运行锁定的 pnpm 入口,而 pnpm 为 Git 包硬编码的 `pnpm install` 会以 `--ignore-workspace` 重新调用同一入口。因此,即使位于另一个 pnpm lockfile 之下,所选包仍自行负责依赖解析,正常的包管理器生命周期 `PATH` 构造会暴露 `node_modules/.bin/dsh-plugin-prepare`。临时 pnpm 包装脚本会在子进程结算后消失。repository 仍是受信任的包管理器输入:所有依赖与生命周期代码都按既有信任约定执行。
Node 24 消费方 CI 任务会传入从 PRPull Requesthead 仓库与 SHA 派生的精确源。它复用现有私有 DeepSeek Harness 仓库,而不会为每次运行新建仓库。作业作用域的 Git 配置允许只读作业 token 访问该精确私有源,并把 pnpm 的 SSH 回退改写为已认证 HTTPS。
构建入口验收还会创建一个进程内 NPM 注册表。它通过移除 `private`、将 workspace protocol 替换为发布版本并打包声明的文件,把当前已构建的 `@deepseek-ai/dsh-repository-plugin` 暂存为发布产物。注册表会提供由此生成的 `packument` 与 tarball作业本地 NPM 配置则只把 `@deepseek-ai` scope 指向它。实际构建的 `dsh run` 子进程随后获取精确 Git 源;该包通过 NPM 解析辅助程序,对 TypeScript Cordis 入口和 MCP server 进行类型检查与打包,准备相邻的 skill技能并加载全部三类贡献。一个刻意设为失败的宿主 `PATH` 命令可以证明,该生命周期选中的是依赖内的可执行文件。验收还要求经过注册表解析并检查不可变的已准备缓存,因此恢复宿主注入的辅助程序也无法通过。
## 考虑过的替代方案
**从正在运行的 DSH 安装注入 `dsh-plugin-prepare`。** 拒绝,因为这会让 manifest 不完整的 repository 包通过,并测试 NPM 消费方无法复现的纯宿主路径。
**把源 fixture 本身发布到 NPM。** 拒绝,因为产品约定明确要求 DSH 插件仍托管在 Git只有可复用的准备辅助程序是 NPM 依赖。
**在每次 CI 运行中创建新的私有 GitHub 仓库。** 拒绝,因为 PR 仓库的精确 head SHA 已是经过认证的真实私有 Git remote。每次运行的仓库变更会增加凭据、清理和最终一致性失败模式却不改变获取路径。
**在 `RepositoryCache` 安装所选包后再准备。** 拒绝,因为 pnpm 打包后的子目录不再包含 `../skills` 等路径所引用的同仓库相邻资源;准备必须在生成 packlist 前完成。
**在 DSH 中克隆 GitHub 仓库并绕过 pnpm 的 Git 获取器。** 拒绝,因为这会重复实现已由锁定包管理器负责的 ref 解析、子目录选择、依赖安装、packlist 行为和缓存完整性。
## 后果
- 仓库作者可以把 `.dsh-plugin` 包、TypeScript 源码、skill 与 MCP 定义提交到 GitHub而无需把该插件包发布到 NPM。该包必须声明已发布的准备依赖。
- 私有 GitHub 源使用宿主的标准 Git 认证。CI 使用临时的只读配置而非运行器上的持久凭据来验证该路径。
- 创作格式使用 `prepack` 而不是 `prepare`。其中可以包含任意包自有构建步骤,但必须调用依赖提供的辅助程序;依赖或生命周期元数据缺失时,会在缓存 generation 可用前失败。
- pnpm 仓库中的所选包按自身 manifest 安装,而不继承外层 workspace。它不能依赖仅由 workspace 提升而可见的包;普通注册表依赖和相对 `file:` 依赖仍是包自有输入。
- 精确源字符串标识不可变缓存 generation改变 ref 或源配置会选择另一个 generation。
- 包依赖、编译、准备和受信任的 `dsh.entry` 贡献仍由 repository 包和[受信任代码决策](../architecture/2026-08-08-trusted-repository-package-code.md)负责。
## 测试
`packages/boot/app-boot/tests/repository-cache.spec.ts` 会通过本地 Git 子路径运行一个未列入源仓库根 pnpm lockfile 的包,并要求相对 `file:` 依赖同时提供构建命令与 `dsh-plugin-prepare`;该测试还证明可见环境变量得以保留,而名称符合凭据模式的变量会被清除。`packages/self-modification/repository-plugin/tests/repository-plugin.spec.ts` 锁定包含辅助命令的 `prepack` 元数据与准备输出。`examples/headless-agent/tests/keyless-smoke.e2e.ts` 使签入仓库的已准备 fixture 继续符合该源格式约定。`apps/cli/tests/github-repository-plugin.built.e2e.ts` 是产品验收测试:模拟发布的辅助程序包、作业本地 NPM 注册表、全新 DSH 主目录、精确且经过认证的私有 GitHub 源、实际构建的 `dsh run`、包自有 TypeScript 构建、真实 MCP 执行、代码入口转换、mock LLM大语言模型请求观测以及已准备缓存检查。

View File

@@ -1,6 +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-28-experimental-plugin-package-group.md
2026-07-28-experimental-plugin-package-group.md: 1ebae5dbb16d4c966f94ffde69fb0cb9bc163d80
2026-07-28-experimental-plugin-package-group.zh.md: 2d09451c5069a775906e5bc8748c334c29008164
# pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-08-10-child-agents-join-their-parent-preset.md
2026-08-10-child-agents-join-their-parent-preset.md: 4534004ad54df69822872b9595a29443fc3a990b
2026-08-10-child-agents-join-their-parent-preset.zh.md: bdf9928bea4b75e2915c8adf5c15f8a01c6583e4

View File

@@ -0,0 +1,59 @@
# Agent Note: Child agents join their parent's preset composition
Status: implemented
English | [中文](2026-08-10-child-agents-join-their-parent-preset.zh.md)
## Problem
Tool and prompt-section visibility is inherited along `dsh-scope`'s parent chain, and an agent's scope key is minted with no parent. [Per-session agent presets](../architecture/2026-08-03-per-session-agent-presets.md) moved every model-facing row onto the agent plane and made `AgentPresets.mount()` the one thing that binds that parent link — from the api-proxy's session create, resume, and fork paths. The two in-process subagent drivers compose their children through `applyChildComposition()`, which installed only the per-child persona and tool filter, so a child's scope chain had length one and its registry view resolved the global layer alone.
That layer is now empty in any deployment with a preset roster: the web-app patch layer disables every host-plane tool row. A one-shot child therefore reached the model with zero tools, a continuable child with only the host-plane `report`, and neither carried its parent's persona, workspace context, plan-mode section, or skill catalog. The fork path had already been given the same treatment for the same reason; delegation had not.
The child's durable header compounded it. `childSessionMeta()` recorded no preset, so a cold read of a child session resolved the deployment default — a tool set the child never ran under, which is exactly what the model-visible ⟺ logged rule exists to prevent.
## Decision
`AgentPresets.composeFrom(agentCtx, parentCtx)` joins one agent to the standing composition another already runs on, and returns the preset id joined. It locates the parent's mount through `standingMountFor()` — the agent's key is parented to its preset's standing key, the same relation `serviceForAgent()` reads — and binds the child's key to that same standing key, keeping the binding under the roster's sole re-link authority. A parent that joined no preset yields no join and no error, which is the rosterless deployment: its model-facing rows sit in the host composition, where the child already resolves them through the global layer.
This is a bind, not a mount, and both differences are load-bearing. The child gets its parent's exact generation, so a composition file edited since the parent started cannot hand the child a different one than its parent's history was produced under, and a preset deleted since cannot fail a child whose parent keeps running. It is also synchronous, which is what lets the child creation windows use it — both in-process drivers compose inside a synchronous `setup`.
`applyChildComposition(childCtx, parent, composition)` takes the parent and performs the join before applying the child's own registrations. The parameter is the point: it makes composing a child without the join unrepresentable at the call sites, rather than leaving each new driver to remember a second step. `childSessionMeta()` records the joined id through `AgentPresets.composedPreset()`, read from the parent's live scope chain rather than its header, because a parent that switched preset while blank runs on the newer composition while its header still names the older one.
`dsh-subagent` reaches the roster through `ctx.get('agentPresets')` with a type-only import and an optional peer dependency — the documented opportunistic-consumption pattern it already uses for `sandboxPolicy` and `approval`.
Giving the child its parent's tools exposed a second defect the same agent-plane move introduced: `ToolRegistry` exempted SCOPED registrations from a restriction and filtered only the global layer, so once every model-facing row became an ancestor contribution, a child's `toolFilter` stopped constraining anything — and, with the global layer empty, `restrict()` rejected every name it was given as unknown, failing the child outright. The exempt set is the tools a scope registers ITSELF, not the tools that happen to live in the global layer; reading it the second way held only while those two sets coincided. `view()` now filters everything a scope inherits — the global layer and every ancestor layer — and exempts only its own. The own-layer exemption is load-bearing rather than incidental: the delegation runtime registers a child's `report` and structured-output tools into the child's own layer, and a filter naming the capabilities the child may use must not strip the machinery it answers through.
## Alternatives considered
**Re-mount the parent's preset by id in the child's setup.** Rejected on both semantics and mechanics. It re-reads the roster and re-stats the composition file, so an edit since the parent started forks the child onto a different generation, and a preset deleted since fails the child while its parent runs on. `mount()` is also asynchronous, which the synchronous creation windows cannot accept without restructuring both drivers.
**Bind the child's key to the PARENT's key rather than to the standing mount.** Rejected because it changes what a child inherits: the parent's own scope layer carries its per-agent restrictions, which would then intersect into every descendant, and a child outliving its parent would hang off a disposed agent's key. Joining the standing mount gives the child its parent's composition and nothing else.
**Extend the continuable activation setup registry to cover one-shot children.** Rejected because that registry's contribution type is synchronous `(childCtx) => () => void` with per-installation revocation, modelling deployment capabilities that come and go, while a preset join is a one-time bind with no revocation of its own. Widening it would have made the omission possible again for any driver that skipped the registry.
**Let `dsh-subagent` import `resolveSessionPreset` and mount by the resolved id.** Rejected because it makes the preset roster a hard module edge for a package that must work without one, and it lands back on the remount semantics above.
**Filter every layer on the chain, including the scope's own.** Rejected because it makes a per-child capability filter delete that child's reporting and structured-output tools, which the delegation runtime registers into the child's own layer — an `allow` naming the capabilities a child may use would leave it unable to answer at all.
**Leave the durable header alone and fix only the live join.** Rejected because the live child and the same child read cold would then disagree about which composition produced its history — the same class of defect, moved rather than fixed.
## Testing
`packages/preset/agent-presets/tests/mount.spec.ts` covers the join against real fixture compositions: the child sees its parent's tools and prompt sections, no second generation is mounted, the join survives the parent's disposal (a background child outliving its parent), the reported id matches, a parent without a preset joins nothing, and an unscoped context is refused.
`packages/core/tools/tests/scoped.spec.ts` covers the restriction rule directly: a child's filter removes a tool it inherited from an ancestor scope, the child's own registrations survive its own filter, and an ancestor's restriction still reaches every scope nested inside it.
`packages/subagent/subagent-inprocess/tests/preset-inheritance.spec.ts` asserts the model-visible result through `startInProcessRun()` on a host composition carrying no model-facing rows: the schemas in the child's own request, its parent's prompt section, the recorded header preset, a `toolFilter` applied over the inherited preset tools, and a parent that switched preset while blank — to a DIFFERENT preset, so the assertion distinguishes reading the parent's live scope chain from reading its creation header.
The assembled-transcript layer is the shipped Web composition's e2e rather than a keyless snapshot. Every runnable example this repo ships composes no preset roster, so the defect is not observable in the snapshot harness at all: a snapshot scenario would first need an example that mounts a roster AND delegates. The Web e2e boots the real `base` + `web-app` patch layers with both shipped presets, which is the assembled evidence the testing policy asks for; the Web browser lane's subagent goldens carry the visible consequence, since a child that records its preset now shows the preset badge its parent shows.
## Consequences
Delegation now costs a scope-parent bind per child and nothing else — no extra plugin instances, no roster read, no failure mode. A child's capabilities are exactly its parent's, minus whatever its own `toolFilter` removes; a per-subagent preset ("agent types") remains unbuilt and would be a new request field rather than a change to this join.
`applyChildComposition()` changed shape, so any future out-of-tree in-process driver must supply the parent. That is the intended cost: the previous signature let a caller compose a capability-less child and get no error.
A cold-resumed continuable child joins its parent's CURRENT composition rather than the one its own header records. The window is narrow — the parent must create the child, stay blank, switch preset, and only then wake it, since a resident child never re-joins and a one-shot child never resumes — and the alternative is worse: resolving the child's own recorded id would re-read the roster and hand back the preset-deleted failure mode this join exists to avoid. The child's header still records what it started under, so the divergence is observable rather than silent.
`ToolRegistry` now reads a restriction's exempt set as "what this scope registers itself" rather than "the global layer", which changes one documented behavior beyond delegation: a tool an ANCESTOR scope contributes is now subject to a descendant's filter, where before only global-layer tools were. Nothing else on the chain loses its exemption — a scope's own registrations stay outside its own filter, which is the property the delegation runtime depends on.

View File

@@ -0,0 +1,59 @@
# Agent Note: Child agents join their parent's preset composition
Status: implemented
[English](2026-08-10-child-agents-join-their-parent-preset.md) | 中文
## Problem
工具与提示段的可见性沿 `dsh-scope` 的父链继承,而 agent 的 scope key 铸造出来时没有父。[逐会话 agent preset](../architecture/2026-08-03-per-session-agent-presets.md) 把所有面向模型的行搬到了 agent 平面,并让 `AgentPresets.mount()` 成为绑定那条父链的唯一途径——调用点在 api-proxy 的会话创建、恢复与 fork 路径上。两个进程内 subagent 驱动通过 `applyChildComposition()` 组装子 agent而它只安装了逐子 agent 的 persona 与工具限制,于是子 agent 的 scope 链长度为一,其注册表视图只能解析到全局层。
在任何配置了 preset roster 的部署里那一层现在是空的web-app 补丁层禁用了全部宿主平面工具行。因此一次性子 agent 抵达模型时工具为零,可继续子 agent 只剩宿主平面的 `report`,两者都不带父方的 persona、工作区上下文、plan-mode 段与技能目录。fork 路径此前已因同一理由做过相同处理;委派没有。
子 agent 的持久化 header 让问题更进一步。`childSessionMeta()` 不记录任何 preset于是冷读一个子会话解析到的是部署默认值——一套该子 agent 从未运行过的工具集,而这正是"模型可见 ⟺ 已记录"规则要杜绝的情形。
## Decision
`AgentPresets.composeFrom(agentCtx, parentCtx)` 让一个 agent 加入另一个 agent 已在运行的常驻组装,并返回所加入的 preset id。它通过 `standingMountFor()` 定位父方的挂载——agent 的 key 认父到其 preset 的常驻 key正是 `serviceForAgent()` 读取的同一关系——再把子 agent 的 key 绑到同一个常驻 key 上,绑定句柄仍归 roster 独有的重链权威持有。未加入任何 preset 的父方不产生加入、也不报错,那就是无 roster 的部署:它面向模型的行位于宿主组装中,子 agent 已经能通过全局层解析到它们。
这是认父而非挂载,两处差别都要紧。子 agent 拿到的是父方那个确切的代际,因此父方启动后被编辑过的组装文件不可能把与父方历史所产出时不同的另一个代际交给它,此后被删除的 preset 也不可能让一个父方仍在运行的子 agent 失败。它还是同步的,这正是子 agent 创建窗口能够使用它的前提——两个进程内驱动都在同步的 `setup` 中完成组装。
`applyChildComposition(childCtx, parent, composition)` 接收父方,并在应用子 agent 自身注册之前完成加入。这个参数正是要点所在:它让"组装子 agent 却不做该加入"在各调用点无法表达,而不是把第二个步骤留给每个新驱动去记住。`childSessionMeta()` 通过 `AgentPresets.composedPreset()` 记录所加入的 id该值从父方**活着的** scope 链读取而不是从其 header 读取,因为在空白期切换过 preset 的父方运行在更新的那份组装上,而它的 header 仍写着旧的那个。
`dsh-subagent` 以类型级导入加可选 peer 依赖的方式,通过 `ctx.get('agentPresets')` 触达 roster——这正是它对 `sandboxPolicy``approval` 已在使用的、有明确文档的机会性消费模式。
把父方的工具交给子 agent 之后,暴露出同一次 agent 平面搬迁引入的第二个缺陷:`ToolRegistry` 把**作用域级**注册排除在限制之外、只过滤全局层,因此当所有面向模型的行都变成祖先贡献之后,子 agent 的 `toolFilter` 就不再约束任何东西——而且全局层为空时,`restrict()` 会把收到的每个名字都判为未知并直接让子 agent 创建失败。豁免集合应当是作用域**自己注册**的工具,而不是恰好位于全局层的工具;后一种读法只在这两个集合重合时才成立。`view()` 现在过滤作用域继承来的一切——全局层与每个祖先层——只豁免它自己那层。这条自身层豁免是承重的而非顺带的:委派运行时把子 agent 的 `report` 与结构化输出工具注册进子 agent 自己那层,而一个只点名子 agent 可用能力的过滤器绝不能把它回报所依赖的机制一并剥掉。
## Alternatives considered
**在子 agent 的 setup 里按 id 重新挂载父方的 preset。** 语义与机制两方面都不成立而被否决。它会重读 roster 并重新 stat 组装文件,因此父方启动后的一次编辑就会把子 agent 分叉到另一个代际,而此后被删除的 preset 会让子 agent 失败、父方却照常运行。`mount()` 还是异步的,同步的创建窗口无法在不重构两个驱动的前提下接受它。
**把子 agent 的 key 绑到**父方的** key 而不是常驻挂载上。** 否决,因为这改变了子 agent 继承的内容:父方自己的 scope 层携带其逐 agent 限制,那些限制会就此与每个后代求交,而活得比父方久的子 agent 会挂在一个已 dispose 的 agent key 上。加入常驻挂载给到子 agent 的是父方的组装,仅此而已。
**扩展可继续 activation setup 注册表以覆盖一次性子 agent。** 否决,因为该注册表的贡献类型是同步的 `(childCtx) => () => void` 并带有逐次安装的撤销,建模的是会来会走的部署能力,而 preset 加入是一次性认父、自身没有撤销可言。扩展它反而会让任何绕过该注册表的驱动重新具备遗漏的可能。
**让 `dsh-subagent` 导入 `resolveSessionPreset` 并按解析出的 id 挂载。** 否决,因为这会给一个必须在没有 roster 时也能工作的包引入硬模块边,而且最终仍落回上述的重新挂载语义。
**过滤链上的每一层,包括作用域自身那层。** 否决,因为那会让逐子 agent 的能力过滤器把该子 agent 的回报与结构化输出工具一并删掉——它们由委派运行时注册进子 agent 自己那层——于是一个点名"子 agent 可用哪些能力"的 `allow` 会让它彻底无法回报。
**只修活着的加入,不动持久化 header。** 否决,因为那样活着的子 agent 与冷读同一个子 agent 会对"哪份组装产出了这段历史"给出不同答案——同一类缺陷,只是被搬了个地方而不是被修掉。
## Testing
`packages/preset/agent-presets/tests/mount.spec.ts` 用真实 fixture 组装覆盖该加入:子 agent 看到父方的工具与提示段、不会挂载出第二个代际、加入在父方 dispose 后依然成立(活得比父方久的后台子 agent、上报的 id 一致、没有 preset 的父方不产生加入、以及无 scope 的上下文被拒绝。
`packages/core/tools/tests/scoped.spec.ts` 直接覆盖该限制规则:子 agent 的过滤器能移除它从祖先作用域继承来的工具、子 agent 自身的注册在自己的过滤器下存活、祖先的限制仍作用于其内嵌套的每个作用域。
`packages/subagent/subagent-inprocess/tests/preset-inheritance.spec.ts` 在一个不含任何面向模型行的宿主组装上,通过 `startInProcessRun()` 断言模型可见的结果:子 agent 自身请求中的 schema、父方的提示段、记录下来的 header preset、施加在继承来的 preset 工具之上的 `toolFilter`,以及在空白期切换过 preset 的父方——切换到**另一个** preset这样断言才能区分"读父方活 scope 链"与"读父方创建 header"。
组装记录这一层用的是真实 shipped Web 组装的 e2e而不是无密钥快照。本仓库所有可运行 example 都不组装 preset roster因此该缺陷在快照 harness 里根本不可观察:要做快照场景,得先有一个既挂载 roster 又发起委派的 example。Web e2e 启动的是真实的 `base` + `web-app` 补丁层与两个 shipped preset这正是测试政策要求的组装证据Web 浏览器 lane 的 subagent golden 承载了可见后果——记录了 preset 的子 agent 现在会显示与其父方相同的 preset 徽标。
## Consequences
委派现在的成本是每个子 agent 一次 scope 认父,再无其他——没有额外的插件实例、没有 roster 读取、没有新的失败模式。子 agent 的能力恰好等于父方的能力,减去它自己的 `toolFilter` 所移除的部分;逐 subagent 的 preset"agent 类型")仍未构建,那会是一个新的请求字段,而不是对这次加入的改动。
`applyChildComposition()` 的形态变了,因此将来任何仓库外的进程内驱动都必须提供父方。这是刻意付出的代价:此前的签名允许调用方组装出一个毫无能力的子 agent 而不报任何错。
冷恢复的可继续子 agent 加入的是父方**当前**的组装,而不是它自己 header 所记录的那份。窗口很窄——父方必须先建子、保持空白、切换 preset之后才唤醒它驻留中的子 agent 不会重新加入,一次性子 agent 也不会恢复——而替代方案更糟:按子 agent 自己记录的 id 解析会重读 roster把这次认父刻意规避掉的"preset 已删除"失败模式又请回来。子 agent 的 header 仍记录它启动时的那份,因此这处分歧是可观察的而非静默的。
`ToolRegistry` 现在把限制的豁免集合读作"该作用域自己注册的东西"而不是"全局层",这在委派之外改变了一处既有行为:**祖先**作用域贡献的工具现在会受后代过滤器约束,而此前只有全局层的工具会。链上其余部分的豁免不变——作用域自身的注册仍在自己的过滤器之外,这正是委派运行时所依赖的性质。

View File

@@ -1,6 +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-30-static-repository-plugin-format.md
2026-07-30-static-repository-plugin-format.md: c66ee111eb0cac9e0d6c54581855ffc18efc8611
2026-07-30-static-repository-plugin-format.zh.md: c85aaf44d96098eb1ccb45456cce1a15e7408fc5
# 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: 002cad0827e969b322997821dc978db85e2955f3
2026-08-10-minimal-preset-owns-rl-composition.zh.md: e957b57395c68b336695bdae07ea15a54ca1ea4e

View File

@@ -0,0 +1,39 @@
# Agent Note: The minimal preset owns the complete RL agent composition
Status: implemented
English | [中文](2026-08-10-minimal-preset-owns-rl-composition.zh.md)
## Problem
The Web surface offered two owners for the Claude SWE-compatible RL agent: a process-wide `core-web.cordis.yml` patch and the per-session `minimal` preset. Once [agent presets](../architecture/2026-08-03-per-session-agent-presets.md) became the agent-composition boundary, the preset's scoped `deployment:persona` shadowed the overlay's corrected global persona with stale coding-agent text. The overlay test mounted no preset, while the preset test booted without the overlay, so neither exercised the composition users selected.
The split also hid other drift. The preset mounted one-shot Bash rather than the [persistent Bash](../feature/2026-07-29-persistent-bash-str-replace-editor.md) used by the RL harness and omitted the RL compaction policy. Keeping both owners makes every future prompt, tool, and policy change a cross-product.
## Decision
The shipped Web `minimal` preset is the sole Web owner of the RL agent composition. It declares an entry-local PTY registry and local backend, persistent `bash` with the RL environment description and 300-second timeout, `str_replace_editor`, and an entry-local compaction backend. Tool presentation remains a deployment choice. The compaction policy keeps the RL threshold, absolute retention, generation cap, and retry count; model capacity comes from routed adapter metadata because `contextWindow` is no longer a compact-basic config field. The editor accepts no `requireAbsolutePath` setting because absolute paths are its unconditional contract.
The preset persona is exactly `You are a helpful software engineer assistant.` and sets `complete: true`. A complete `PromptSection` participates in ordinary assembly so tools, contexts, variables, and cooperative listeners still resolve; after the `system-prompt/assemble` waterfall, the prompt registry restores a detached copy of that section as the sole system-prompt section. Multiple effective complete sections reject assembly. This final registry constraint prevents harness identity, Web orientation, tool guidance, or an assembly listener from appending prompt text.
The process-wide `core-web.cordis.yml` patch is absent. Browser UI, workspace attachment, persistence, filesystem, subprocess, sandbox, permission, model routing, and other cross-session services remain host-owned. Selecting `minimal` changes one agent's model-facing composition without changing other sessions in the Web process.
## Verification
System-prompt and persona package tests prove final complete-section enforcement, including waterfall mutation and duplicate rejection. The shipped-preset composition test asserts the exact prompt, Bash description, absolute editor schema, and two-tool catalog under the default native presentation. The keyless Web replay sends a real request through a `minimal` agent while global identity, Web surface text, and a test section are registered, then executes two persistent Bash calls to prove environment and cwd state survive and executes the editor through an absolute path.
The standalone [`minimal.cordis.yml`](../../../../examples/jsonrpc-agent/minimal.cordis.yml) mirrors the same prompt, tools, timeouts, and compaction policy for the bundled JSON-RPC runtime. Its keyless SDK replay asserts the assembled system prompt and two-tool catalog, executes persistent Bash across calls, and exercises the editor; the Python SDK tutorial provides the runnable entry point.
## Alternatives considered
**Keep `core-web.cordis.yml` as a compatibility patch.** Rejected because a process patch and a session preset are two independent owners for one agent contract; precedence makes either one capable of silently undoing the other.
**Disable every known prompt contributor in the preset.** Rejected because host rows are process-wide and new contributors would reopen the prompt. A final complete-section constraint expresses the negative guarantee at the registry that assembles the prompt.
**Filter sections only with a prepended waterfall listener.** Rejected because another prepended wrapper can run outside it and append after the filter. Enforcement after the complete waterfall has stable final authority.
**Mount PTY services on the Web host.** Rejected because only the minimal agent consumes them. An entry-local `pty` realm gives the services the same lifetime and scope as their sole consumer without publishing a process-global service from a preset.
## Consequences
The RL prompt is fixed rather than environment-overridable. The Web preset and standalone JSON-RPC example state the same contract for their respective launch surfaces. The model sees only persistent `bash` and `str_replace_editor`; shell state is per agent and disappears with that agent. The preset pays for its own PTY and compaction service instances, while other presets pay nothing for them. The local persistent-shell backend requires the supported POSIX terminal substrate, so this preset is not a Windows agent surface.

View File

@@ -0,0 +1,39 @@
# Agent Note: minimal preset 拥有完整的 RL agent 组合
Status: implemented
[English](2026-08-10-minimal-preset-owns-rl-composition.md) | 中文
## 问题
Web surface 同时由两个位置定义与 Claude SWE 兼容的 RL agent智能体进程级 `core-web.cordis.yml` patch以及逐会话的 `minimal` preset。[agent preset](../architecture/2026-08-03-per-session-agent-presets.md) 成为 agent 组合边界后preset 中带作用域的 `deployment:persona` 会用陈旧的 coding-agent 文本遮蔽 overlay 修正过的全局 persona。overlay 测试没有挂载 preset而 preset 测试启动时没有 overlay因此两者都没有覆盖用户实际选择的组合。
这种拆分还掩盖了其他偏差。preset 挂载了一次性 Bash而不是 RL harness 使用的[持久 Bash](../feature/2026-07-29-persistent-bash-str-replace-editor.md),并且遗漏了 RL 压缩compaction策略。保留两个所有者会使今后每次修改提示词、工具或策略时都必须验证二者的交叉组合。
## 决策
随附的 Web `minimal` preset 是 RL agent 组合在 Web 中的唯一所有者。它声明 entry 本地的 PTY 注册表与本地后端、带 RL 环境描述且超时为 300 秒的持久 `bash``str_replace_editor`,以及 entry 本地的压缩后端。工具呈现仍由部署选择。压缩策略保留 RL 的阈值、绝对保留量、生成上限和重试次数;模型容量来自经路由选定的适配器元数据,因为 `contextWindow` 已不再是 compact-basic 的配置字段。编辑器不接受 `requireAbsolutePath` 设置,因为要求绝对路径是它的无条件约定。
preset persona 恰好是 `You are a helpful software engineer assistant.`,并设置 `complete: true`。complete `PromptSection` 参与常规组装,因此工具、上下文、变量和协作式监听器仍会解析;`system-prompt/assemble` waterfall瀑布式事件结束后提示词注册表会将该段落的独立副本恢复为唯一的系统提示词段落。存在多个有效 complete 段时,组装会被拒绝。这项最终注册表约束可防止 harness 身份、Web 定位、工具引导或组装监听器追加提示词文本。
进程级 `core-web.cordis.yml` patch 不再存在。浏览器 UI、workspace 附加、持久化、文件系统、子进程、沙箱、权限、模型路由及其他跨会话服务仍由宿主持有。选择 `minimal` 只会改变一个 agent 面向模型的组合,不会改变 Web 进程中的其他会话。
## 验证
系统提示词与 persona 包测试证明了 complete 段的最终约束,包括 waterfall 修改与重复项拒绝。交付 preset 组合测试在默认原生呈现下断言精确的提示词、Bash 描述、要求绝对路径的编辑器 schema 和双工具目录。无密钥 Web 回放通过 `minimal` agent 发送一个真实请求同时注册全局身份、Web surface 文本和一个测试段落;随后执行两次持久 Bash 调用,证明环境与 cwd 状态能够保留,并通过绝对路径执行编辑器。
独立的 [`minimal.cordis.yml`](../../../../examples/jsonrpc-agent/minimal.cordis.yml) 为内置 JSON-RPC 运行时复现相同的提示词、工具、超时和压缩策略。其无密钥 SDK 回放会断言组装后的系统提示词与双工具目录,跨调用执行持久 Bash并使用编辑器Python SDK 教程提供可运行的入口。
## 考虑过的替代方案
**将 `core-web.cordis.yml` 保留为兼容 patch。** 被拒绝,因为进程 patch 与会话 preset 是同一 agent 约定的两个独立所有者;优先级会使任意一方都能静默撤销另一方的配置。
**在 preset 中禁用每个已知的提示词贡献方。** 被拒绝,因为宿主行属于整个进程,新的贡献方也会重新开放提示词。由组装提示词的注册表实施最终 complete 段约束,才能表达这项否定保证。
**仅使用前置 waterfall 监听器筛选段落。** 被拒绝,因为另一个前置包装层可以在该监听器外执行,并在筛选后追加内容。在整个 waterfall 结束后实施约束,才能稳定拥有最终决定权。
**在 Web 宿主上挂载 PTY 服务。** 被拒绝,因为只有 minimal agent 消费这些服务。entry 本地的 `pty` realm 与唯一消费方具有相同的生命周期和作用域,无需由 preset 发布进程级全局服务。
## 后果
RL 提示词固定不变不能通过环境覆盖。Web preset 与独立 JSON-RPC 示例分别在各自的启动界面声明相同的约定。模型只看到持久 `bash``str_replace_editor`shell 状态按 agent 隔离,并随该 agent 一并消失。preset 为自身的 PTY 与压缩服务实例承担开销,其他 preset 无需承担。持久 shell 的本地后端需要受支持的 POSIX 终端基础环境,因此该 preset 不适用于 Windows agent surface。

View File

@@ -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-10-subagent-empty-terminal-message-output.md
2026-08-10-subagent-empty-terminal-message-output.md: 693013f6810005ce02b08bd82f1f6a18511c40fb
2026-08-10-subagent-empty-terminal-message-output.zh.md: 64d61af21f838ef3f515db8af116cbdd74e96179

View File

@@ -0,0 +1,31 @@
# Agent Note: One selection rule keeps subagent output past an empty terminal message
Status: implemented
English | [中文](2026-08-10-subagent-empty-terminal-message-output.zh.md)
## Problem
The agent loop appends an empty-content `assistant/message` when a `max-tokens` step assembled only tool-call blocks because `BlockAssembler.blocks()` drops truncated tool calls; the message records usage only. Three consumers selected the child's output independently and treated that usage record as output. The in-process driver's `readResult` and the continuable Activation's `subagent/end` capture selected the last `assistant/message` without filtering, while the SDK backend's observer let any `assistant/message` take precedence over accumulated text. In a multi-step turn cut off at max-tokens, the final empty message caused the real partial answer to be omitted from `SubagentResult.output`, the tool result, telemetry, and `subagent/end.lastAssistantMessage`. The in-process driver also lacked a streamed-text fallback, so a cancelled child whose only text existed in `assistant/chunk` events reported `[]`.
## Decision
`dsh-subagent` owns one canonical selection rule in `src/assistant-output.ts`: select the last non-empty assistant message; without one, select the accumulated `text-delta` stream; ignore empty-content messages. The incremental `AssistantOutputFold` implements the rule through `push(event)` for session-event transports, `pushText(text)` for chunk-only transports, and `collect()` for selection. `finalAssistantOutput(events)` applies it to a complete event suffix for the in-process `readResult` and Activation capture. The SDK backend folds notification events; the ACP backend exposes no complete assistant messages and folds raw chunk text. `SubagentResult.output` defines the result contract, and `subagent/end.lastAssistantMessage` uses the same rule. When a child produces neither form of output, the lifecycle field is absent rather than an empty array for both one-shot and continuable runs. A `max-tokens` or `aborted` result retains its actual stop reason.
The foreground delegation tool uses the same selection. A non-`completed` result remains an `isError` tool result, but its message appends the child's partial text after the stop-reason headline so the parent model receives both the failure and available output.
## Verification
The keyless SDK backend test uses `FAKE_EMPTY_MESSAGE` to emit a usage-only terminal message. The `subagent-max-tokens-partial` ACP snapshot records a child that streams text and a tool call, ends at a tool-only max-tokens step with an empty usage message in its durable log, and returns the partial text through the parent's errored tool result. Unit coverage checks empty terminal messages, cancellation, message ordering, textless non-empty messages, and exclusion of tool-result content.
## Alternatives considered
**Fix each consumer in place without a shared helper.** Rejected: three independent selections had diverged, while observers of one run must agree on its output.
**Stop the loop from appending the empty message.** Rejected: the message records usage and preserves the step in the durable log ("model-visible ⟺ logged"); changing session events to address output selection would affect every replay and projection consumer.
**Treat empty-content messages as an error.** Rejected: the streamed text is the child's real partial answer, and the stop reason already tells the consumer the turn was cut short.
## Consequences
Multi-step children cut off at max-tokens report their earlier text; cancelled in-process children retain text streamed before the abort; one-shot and continuable `subagent/end` events agree with `SubagentResult.output`. A message whose content is non-empty but textless, such as reasoning-only content, is selected instead of streamed text because the rule tests content length rather than text presence. A non-empty message is also selected instead of text streamed after it: a child cancelled while streaming a later step reports its earlier complete message, while the stop reason records the truncation.

View File

@@ -0,0 +1,31 @@
# Agent Note: 用同一条选取规则在空终止消息后保留子代理输出
Status: implemented
[English](2026-08-10-subagent-empty-terminal-message-output.md) | 中文
## 问题
`max-tokens` 步骤只组装了工具调用块时agent loop智能体循环会追加一条空内容的 `assistant/message`,因为 `BlockAssembler.blocks()` 会丢弃被截断的工具调用;这条消息仅记录 usage。三个消费方独立选取子 agent 的输出,并把这条 usage 记录当成输出。进程内驱动的 `readResult` 与 continuable Activation 的 `subagent/end` capture 不加过滤地选取最后一条 `assistant/message`SDK 后端的观察器则让任何 `assistant/message` 优先于累积的文本。在被 max-tokens 截断的多步轮次中,最后那条空消息导致 `SubagentResult.output`、工具结果、遥测与 `subagent/end.lastAssistantMessage` 都漏掉真实的部分回答。进程内驱动也没有流式文本兜底,因此被取消的子 agent 若其唯一文本只存在于 `assistant/chunk` 事件中,也会报告 `[]`
## 决策
`dsh-subagent``src/assistant-output.ts` 中拥有唯一的规范选取规则:选取最后一条非空 assistant 消息;没有时选取累积的 `text-delta` 流;忽略空内容消息。增量的 `AssistantOutputFold` 通过 `push(event)` 处理会话事件传输,通过 `pushText(text)` 处理仅分片传输,并通过 `collect()` 完成选取。`finalAssistantOutput(events)` 把规则应用于完整的事件后缀,供进程内 `readResult` 与 Activation capture 使用。SDK 后端折叠通知事件ACP 后端不暴露完整的 assistant 消息,而是折叠原始分片文本。`SubagentResult.output` 定义结果约定,`subagent/end.lastAssistantMessage` 使用同一规则。子 agent 不产生这两种输出中的任何一种时,一次性与 continuable 运行的生命周期字段都会缺省,而不是空数组。`max-tokens``aborted` 结果保留实际的终止原因。
前台委派工具使用同一选取规则。非 `completed` 的结果仍是 `isError` 工具结果,但其消息会在终止原因标题之后附上子 agent 的部分文本,让父模型同时接收失败信息与已有输出。
## 验证
无密钥 SDK 后端测试使用 `FAKE_EMPTY_MESSAGE` 发出一条仅记录 usage 的终止消息。`subagent-max-tokens-partial` ACP 快照记录一个子 agent它流式输出文本与一次工具调用结束于仅含工具调用的 max-tokens 步骤,持久化日志中含一条空的 usage 消息,并通过父侧的错误工具结果返回部分文本。单元覆盖检查空终止消息、取消、消息顺序、不含文本的非空消息,以及排除工具结果内容。
## 考虑过的替代方案
**各消费方就地修复、不抽共享辅助函数。** 之所以否决:三处独立选取已发生分歧,而同一次运行的观察方必须对其输出达成一致。
**让 loop 不再追加空消息。** 之所以否决:这条消息记录 usage并在持久化日志中保留该步骤"model-visible ⟺ logged");为处理输出选取而改动会话事件,会影响所有 replay 与 projection 消费方。
**把空内容消息视为错误。** 之所以否决:流式文本才是子代理真实的部分回答,且终止原因已经告诉消费方轮次被截断。
## 后果
被 max-tokens 截断的多步子 agent 会报告其更早的文本;被取消的进程内子 agent 保留中止前已流式的文本;一次性与 continuable 的 `subagent/end` 事件同 `SubagentResult.output` 一致。内容非空但不含文本的消息(例如仅含 reasoning 的内容)仍然优先于流式文本,因为规则检查内容长度,而不是文本是否存在。非空消息同样优先于其后才流式出的文本:子 agent 在流式输出后续步骤时被取消,报告的是更早那条完整消息,终止原因则记录该截断。

View File

@@ -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-06-24-workspace-context.md
2026-06-24-workspace-context.md: e7a3724847b9dc8cfad11e87b2c96a3ef442bcba
2026-06-24-workspace-context.zh.md: 39c3f52da10b7299301d10bd8b78330cbae8e19c
2026-06-24-workspace-context.md: 4398fbb1606e764dde9fad8a8f3c325f5e0fead4
2026-06-24-workspace-context.zh.md: 72ae077be08e95ce582584e66945c2187255abdd

View File

@@ -52,7 +52,7 @@ Every workspace context event stores versioned metadata with `{ action, scope, p
At reconciliation time the plugin scans workspace-sourced `user/message` events and derives the latest state for each visible scope. Successful nested touches aggregate under their parent execution token, including when a later composite result is blocked; the top-level result transfers them either to the open session step or directly to a per-agent projection queue. A `step/end` releases its staged touches only after that boundary is durable, and the next `agent/pre-step` waits for the serialized projections. Each projection composes against visible history plus the current inbox and replaces the single pending workspace context instead of accumulating intermediate renderings.
An unchanged path and digest is suppressed. A logged removal is a tombstone, so a reappearing candidate becomes a new `set`. Resume works from persisted metadata: a compatible visible baseline supplies comparison state rather than causing another complete baseline to be appended. If compaction removes an instruction event from the visible surface, that state no longer suppresses a later load, matching the fact that the model can no longer see it. Only changes actually included under the byte budget enter metadata or pending state, so an omitted file remains eligible on a later touch.
An unchanged path and digest is suppressed. A logged removal is a tombstone, so a reappearing candidate becomes a new `set`. Resume works from persisted metadata: a compatible visible baseline supplies comparison state rather than causing another complete baseline to be appended. If compaction removes an instruction event from the visible surface, that state no longer suppresses a later load, matching the fact that the model can no longer see it. A change enters metadata or pending state only when its file-specific section retains at least one content byte, or when the original content is genuinely empty. Partial truncation commits the full-content digest once any byte survives; zero-content truncation remains eligible on a later touch. A baseline may retain budget diagnostics with no committed changes. A dynamic batch with no committed change is withheld entirely and retried on a later touch. An omitted file remains eligible on a later touch.
The initial baseline's typed changes are comparison state only while its event remains in the visible session surface. Resume retains a compatible baseline and reconciles current baseline and visible dynamic scopes, so changes made while the agent was offline append transitions before the first resumed request. When compaction shadows the baseline event, the next entering pre-step composes the complete current baseline and records it in the same request; a successful filesystem touch can instead re-add an unchanged baseline scope or append baseline edits or removals as dynamic messages. Neither path rewrites the original event. The in-memory scope marker and provider-version cache only select and accelerate probes, so neither can suppress context the model no longer sees.

View File

@@ -52,7 +52,7 @@ shell 命令不会触发发现。本地 bash 调用会启动全新的 shell
协调时,插件扫描带工作区来源的 `user/message` 事件,并派生每个可见作用域的最新状态。即使后续复合结果被拦截,成功的嵌套 touch 也会聚合到父级执行 token 下;顶层结果会将它们交给打开的会话步骤,或直接交给逐 agent 投影队列。`step/end` 只会在自身边界持久化后释放其暂存的 touch下一次 `agent/pre-step` 会等待串行投影完成。每次投影都会根据可见历史和当前 inbox 进行组合,并替换唯一一条待处理工作区上下文,而不会累积中间渲染结果。
路径和 digest 均未变化时会被抑制。日志中的移除操作是一条墓碑记录,因此重新出现的候选项会成为新的 `set`。恢复操作从持久化元数据继续工作:兼容的可见基线会提供比较状态,而不会导致再次追加完整基线。如果压缩从可见表面移除某条指令事件,该状态不再抑制后续加载,这与模型已经无法看见它的事实一致。只有真正纳入字节预算的变更才会进入元数据或待处理状态,因此被省略的文件在之后的触碰中仍有资格加载。
路径和 digest 均未变化时会被抑制。日志中的移除操作是一条墓碑记录,因此重新出现的候选项会成为新的 `set`。恢复操作从持久化元数据继续工作:兼容的可见基线会提供比较状态,而不会导致再次追加完整基线。如果压缩从可见表面移除某条指令事件,该状态不再抑制后续加载,这与模型已经无法看见它的事实一致。变更只有在对应文件专属段落保留至少一个内容字节,或原始内容确实为空时,才会进入元数据或待处理状态。只要任一内容字节保留下来,部分截断就会提交完整内容 digest截断到零内容仍可在后续触碰中处理。基线可以保留字节预算诊断而不提交任何变更。动态批次若没有可提交变更则整批不注入并在后续触碰中重试。被省略的文件仍可在之后的触碰中加载。
只有当初始基线事件仍在可见会话表层中时,其类型化变更才用作比较状态。恢复会保留兼容的基线,并对账当前基线和可见的动态 scope因此 agent 离线期间的变更会在第一次恢复请求前追加为转换。当压缩遮蔽基线事件时,下一次进入步骤的 pre-step 会组合完整的当前基线,并在同一请求中记录它;也可以改由一次成功的文件系统触碰重新添加未变化的基线 scope或把基线编辑或移除操作追加为动态消息。两条路径都不会重写原始事件。内存中的 scope 标记和提供方版本 cache 只用于选择探测对象并加速探测,因此二者都不能抑制模型已无法看见的上下文。

View File

@@ -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: 7c830d93f19a40ab193cfebabca854882ab68d62
2026-07-06-approval-seam.zh.md: 9dedfddadc23b0da44b28e8750508653ee20bb83
2026-07-06-approval-seam.md: 8aa9986139dae77e08c166b72545bfa688a389e0
2026-07-06-approval-seam.zh.md: ef4ccf5fd2b54888a648737866ff6f5fe1678882

View File

@@ -123,7 +123,7 @@ Costs and accepted limits:
- **Who decides whether a call asks in the first place?** Policy producers: a hook returning `permissionDecision: ask`, any `tools/pre-execute` listener, or the sandbox escalation gate. The seam and the bridge only route and answer; neither injects its own judgment about what deserves a prompt.
- **What happens when the user dismisses the prompt, or the turn aborts mid-ask?** Dismissal maps to `cancelled` with its own deny text. An already-aborted signal settles `cancelled` without dispatching; an abort during the ask discards the late answer. When both audit appends commit, either path records one pair, never two.
- **What if the client answers with an option the harness never offered?** Any selection other than the offered `allow_once` maps to `rejected` — an unknown optionId from a non-conforming client can never grant.
- **How do subagents' approvals route?** An agent no answerer owns delegates through the whole waterfall and fails closed — in-process subagents are deliberately unanswerable. A `'never'` parent seeds that override into each in-process child's log ([decision](2026-07-25-subagent-policy-inheritance.md)), so the child is told up front instead of asking into the empty waterfall. `subagent-acp`'s child-side auto-answer is separate; routing a child's asks to the parent controller is deferred (§ Deferred).
- **How do subagents' approvals route?** They do not: delegation pins every in-process child to `'never'` ([approvals-pinned decision](2026-08-10-subagent-approval-pinned-never.md)), so each child ask resolves `rejected` before any answerer and the child is told up front through its runtime context. `subagent-acp`'s child-side auto-answer is separate; routing a child's asks to the parent controller is deferred (§ Deferred).
- **What does `policy: 'never'` actually change at runtime?** The service resolves every ask for that session to `rejected` before dispatching any answerer (in-service, so no registration order can bypass it); the next atomic runtime-context snapshot states the policy; each successful auto-rejection records the audit pair.
- **What happens across a hot reload, or when an answerer unloads mid-session?** Answerers dispose with their owning fiber, so the next ask degrades to `unavailable` instead of hanging on a dead channel; remounting re-registers the answerer with no catch-up state.
- **Where does a client get approval context?** The request carries the exact `callId` and the asker's human-readable `reason`; channel adapters may correlate richer tool-call state without duplicating arguments in the approval seam.

View File

@@ -123,7 +123,7 @@ ACP 桥只应答其会话映射所拥有的精确 agent 对象。它携带既有
- **谁决定一次调用是否需要 ask** 策略生产者:返回 `permissionDecision: ask` 的钩子、任何 `tools/pre-execute` 监听器、或沙箱升级门禁。seam 和桥只负责路由和应答;二者都不注入自己对「什么值得弹出提示」的判断。
- **用户关闭提示或轮次在 ask 进行中中止时会发生什么?** 关闭映射为 `cancelled` 并携带自己的拒绝文本。已中止的 signal 直接结算为 `cancelled` 而不派发ask 进行中的中止丢弃迟到的应答。当两个审计追加都提交时,任一路径都记录恰好一对事件,绝不会两对。
- **如果客户端以 harness 从未提供的选项应答呢?** 除已提供的 `allow_once` 之外的任何选项都映射为 `rejected`——来自不合规客户端的未知 optionId 永远不能授权。
- **subagent 的审批如何路由?** 没有应答者拥有的 agent 穿过整个 waterfall 委派并失败关闭——进程内 subagent 被刻意设计为不可应答。`'never'` 父级会把该覆盖项预置到每个进程内子 agent 的日志中([决策](2026-07-25-subagent-policy-inheritance.md)),因此子 agent 一开始就会得知,而不是向空的 waterfall 发出 ask`subagent-acp` 的子侧自动应答是独立的;将子 agent 的 ask 路由到父控制器已延后(§ 延后)。
- **subagent 的审批如何路由?** 不路由:委派会把每个进程内子 agent 钉定为 `'never'`[审批钉定决策](2026-08-10-subagent-approval-pinned-never.md)),因此子 agent 的每次 ask 都在任何应答者之前解析为 `rejected`,子 agent 则通过其运行时上下文一开始就会得知`subagent-acp` 的子侧自动应答是独立的;将子 agent 的 ask 路由到父控制器已延后(§ 延后)。
- **`policy: 'never'` 在运行时实际改变了什么?** 服务在派发任何应答者之前,将该会话的每次 ask 解析为 `rejected`(在服务内部,因此没有注册顺序能绕过它);下一份原子化的运行时上下文快照会声明该策略;每次成功的自动拒绝都会记录审计对。
- **热重载或应答者在会话中途卸载时会发生什么?** 应答者随其拥有的 fiber 一起 dispose因此下一次 ask 降级为 `unavailable` 而非挂在死通道上;重新挂载会重新注册应答者,无需追赶状态。
- **客户端从哪里获得审批上下文?** 请求携带精确的 `callId` 和发起方的人类可读 `reason`;通道适配器可自行关联更丰富的工具调用状态,而无需在审批 seam 中重复携带参数。

View File

@@ -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: 5fc2fb07fcd0b00bf72c818d3806b298312cdc31
2026-07-08-self-referential-cordis-toolset.zh.md: 8f34c97d94cad9b79a0e823406c07cdcfb38793f
2026-07-08-self-referential-cordis-toolset.md: 0d78e0adff487edae00c2422acc1ef8941e7636a
2026-07-08-self-referential-cordis-toolset.zh.md: 665387a9fb24bf0e6fc04fdfb1ced88b932742ad

View File

@@ -40,7 +40,7 @@ The boundary normalizes unambiguous JSON-Schema forms into `ParameterSchemaSpec`
Every temporary Plugin is a child of one internal `cordis-dynamic` group beneath the tool plugin, so ordinary fiber disposal handles toolset reload and unload. `cordis_mount` awaits settlement; startup failure disposes the fiber before returning an error. A settled pending Plugin remains visible with its missing injections. `cordis_unmount` awaits the Plugin fiber's disposal.
Temporary Plugins exist only in process memory. They create no Plugin file, install no package, change no `cordis.yml` or personal/project configuration, do not survive restart, and have no automatic save, promote, or install path. Keeping an experiment means asking the Agent to implement a normal local, project, or repository Plugin through the regular development workflow.
Temporary Plugins exist only in process memory. They create no Plugin file, install no package, change no `cordis.yml` or personal/project configuration, do not survive restart, and have no automatic save, promote, or install path. Keeping an experiment means asking the Agent to implement a normal project Plugin or installable profile bundle through the regular development workflow.
### Cross-mount composition via provide/inject

View File

@@ -40,7 +40,7 @@ vm 隔离了意外的全局污染,上下文门面隐藏了框架内部细节
每个临时 Plugin 都是工具插件下方内部 `cordis-dynamic` 分组的子节点,因此普通的 fiber 释放即可处理工具集重载和卸载。`cordis_mount` 会等待 settlement启动失败时在返回错误前释放 fiber。已 settle 但处于 pending 状态的 Plugin 仍然可见,并列出其缺失的注入。`cordis_unmount` 等待 Plugin fiber 的释放完成。
临时 Plugin 只存在于进程内存中。它不会创建 Plugin 文件、安装 package、修改 `cordis.yml` 或个人/项目配置、跨重启存续,也不存在自动保存、转正式或安装路径。若要保留实验结果,应让 Agent 通过常规开发流程实现普通的本地、项目或仓库 Plugin
临时 Plugin 只存在于进程内存中。它不会创建 Plugin 文件、安装 package、修改 `cordis.yml` 或个人/项目配置、跨重启存续,也不存在自动保存、转正式或安装路径。若要保留实验结果,应让 Agent 通过常规开发流程实现普通的项目 Plugin 或可安装的 profile 组合包
### 通过 provide/inject 实现跨挂载组合

View File

@@ -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-20-dsh-cli-personal-config.md
2026-07-20-dsh-cli-personal-config.md: f5207c5ffbd963b9b7c4a7166fa9f17a460707a9
2026-07-20-dsh-cli-personal-config.zh.md: 24478a4b4fd5878032bf80f5b30ab9e2008da785
2026-07-20-dsh-cli-personal-config.md: 02883c89f27e51d6091d4d65167ebdd6a96f6f51
2026-07-20-dsh-cli-personal-config.zh.md: 6e56e892cf682ea514b036750e44dbb06e944a80

View File

@@ -19,7 +19,7 @@ Two coupled pieces, aligned with the `apps/` assembly tier proposed by the `dsh
**Personal config (`dsh-app-boot`).** The personal overlay lives in the Harness home — `$DSH_HOME`, else `~/.dsh` — resolved by the shared [`resolveDshHome`](../architecture/2026-07-24-single-harness-home-resolver.md) (`@deepseek-ai/dsh-paths`), the same single root skills and AGENTS.md resolve against. The dsh TUI, Web, and headless surfaces consume its two optional files; the demo bins boot their committed trees verbatim:
- `.env` — loaded after the invoking directory's `.env`; `process.loadEnvFile` never overrides, so precedence is ambient > project `.env` > personal `.env`.
- `config.yaml` — a top-level YAML array of `@cordisjs/plugin-include` `PatchOptions`, parsed with the include's own `!!js` dialect (`loadPersonalPatches`) and passed to `boot()`, which forwards it as the root include's `patches`. Patch semantics match the shipped surface overlays: an id-targeted patch replaces the named entry's whole `config`, `insert` appends entries, and an unmatched id is a silent no-op. The [repository Plugin integration](2026-07-30-config-only-repository-plugins.md) uses one shipped row to make an exact GitHub source list a config-only choice.
- `config.yaml` — a top-level YAML array of `@cordisjs/plugin-include` `PatchOptions`, parsed with the include's own `!!js` dialect (`loadPersonalPatches`) and passed to `boot()`, which forwards it as the root include's `patches`. Patch semantics match the shipped surface overlays: an id-targeted patch replaces the named entry's whole `config`, `insert` appends entries, and an unmatched id is a silent no-op. External packages are installed as [profile bundles](../simplification/2026-08-09-remove-repository-plugin.md); this personal layer configures the Loader rows those bundles contribute.
- A missing file means no overlay; a present-but-unreadable, unparsable, or non-array file throws at boot (misconfiguration fails loud, never a silent skip).
The PTY smoke's launcher isolates `$DSH_HOME` to a per-test directory, exactly as it already isolates `DSH_AGENTS_HOME`, so a developer's real personal overlay cannot leak into fixtures; only the dsh CLI reads personal config, so no other test launcher needed changes.
@@ -40,7 +40,7 @@ The TUI and Web register the exact personal path through Cordis HMR after boot.
## Consequences
- `dsh` from any directory (and `pnpm run demo:tui`) can apply personal providers, models, repository Plugins, and other Loader entries with no checkout edit; verified end-to-end against a personal Anthropic proxy with Opus 4.8, including a bash tool round trip.
- `dsh` from any directory (and `pnpm run demo:tui`) can apply personal providers, models, installed bundle entries, and other Loader entries with no checkout edit; verified end-to-end against a personal Anthropic proxy with Opus 4.8, including a bash tool round trip.
- Because an id-targeted patch replaces the whole `config`, a personal override restates the base fields it keeps and can drift when the base entry changes shape; the loader's entry-not-found/name-mismatch warnings and [`dsh --dump-config`](../../../../apps/cli/README.md#profiles) (which prints the composed tree those patches produce) are the diagnostics.
- Personal patches resolve ids against the booted file's own tree, so nested-include overlays (Code Mode) are not personalized; live-run parity for those leaves is deferred.
- `dsh-app-boot` depends on `js-yaml` and imports the include's `!!js` YAML dialect (`entryListSchema`) directly, and, like `apps/cli`, depends on `@deepseek-ai/dsh-paths` for `resolveDshHome`.

View File

@@ -19,7 +19,7 @@ Status: implemented
**个人配置(`dsh-app-boot`)。** 个人 overlay 存放在 Harness home——`$DSH_HOME`,否则 `~/.dsh`——由共享的 [`resolveDshHome`](../architecture/2026-07-24-single-harness-home-resolver.md)`@deepseek-ai/dsh-paths`)解析,与 skills、AGENTS.md 解析所依据的单一根目录相同。dsh 的 TUI、Web 和无头界面使用其中两个可选文件;各示例 bin 仍然逐字节按已提交的配置树启动:
- `.env`——在调用目录的 `.env` 之后加载;`process.loadEnvFile` 从不覆盖已有值,因此优先级为环境变量 > 项目 `.env` > 个人 `.env`
- `config.yaml`——顶层 YAML 数组,元素为 `@cordisjs/plugin-include``PatchOptions`,用 include 自己的 `!!js` 方言解析(`loadPersonalPatches`)并传给 `boot()`,由它作为根 include 的 `patches` 转发。补丁语义与交付的 surface overlay 一致:按 id 定位的补丁替换该配置项的整个 `config``insert` 追加配置项,未匹配的 id 静默不执行任何操作。[仓库插件集成](2026-07-30-config-only-repository-plugins.md)通过一个已交付配置项,使精确 GitHub 源列表成为纯配置选择
- `config.yaml`——顶层 YAML 数组,元素为 `@cordisjs/plugin-include``PatchOptions`,用 include 自己的 `!!js` 方言解析(`loadPersonalPatches`)并传给 `boot()`,由它作为根 include 的 `patches` 转发。补丁语义与交付的 surface overlay 一致:按 id 定位的补丁替换该配置项的整个 `config``insert` 追加配置项,未匹配的 id 静默不执行任何操作。外部包作为 [profile 组合包](../simplification/2026-08-09-remove-repository-plugin.md)安装;这个个人层负责配置这些组合包提供的 Loader 配置项
- 文件缺失即无 overlay文件存在但不可读、不可解析或非数组则在启动时抛出配置错误响亮失败绝不静默跳过
PTY 冒烟测试的启动器把 `$DSH_HOME` 隔离到每个测试自己的目录,与它已有的 `DSH_AGENTS_HOME` 隔离方式完全一致,开发者真实的个人 overlay 不可能泄漏进 fixture只有 dsh CLI 读取个人配置,因此其他测试启动器无需改动。
@@ -40,7 +40,7 @@ TUI 和 Web 启动后通过 Cordis HMR热模块替换注册确切的个人
## Consequences
- 在任意目录运行 `dsh`(以及 `pnpm run demo:tui`),无需修改 checkout即可应用个人提供方、模型、仓库插件和其他 Loader 配置项;已针对个人 Anthropic 代理与 Opus 4.8 端到端验证,包括一次 bash 工具往返。
- 在任意目录运行 `dsh`(以及 `pnpm run demo:tui`),无需修改 checkout即可应用个人提供方、模型、已安装组合包的配置项和其他 Loader 配置项;已针对个人 Anthropic 代理与 Opus 4.8 端到端验证,包括一次 bash 工具往返。
- 由于按 id 定位的补丁替换整个 `config`,个人覆盖必须复述它保留的基础字段,并可能随基础配置项形态变化而漂移;诊断手段是 loader 的「配置项未找到/名称不匹配」警告和 [`dsh --dump-config`](../../../../apps/cli/README.md#profiles)(打印这些补丁合成出的配置树)。
- 个人补丁只在被启动文件自身的树里解析 id因此嵌套 include 的 overlayCode Mode不会被个性化这些叶子的实际运行等价性暂缓。
- `dsh-app-boot` 依赖 `js-yaml`,并直接导入 include 的 `!!js` YAML 方言(`entryListSchema`);与 `apps/cli` 一样依赖 `@deepseek-ai/dsh-paths` 以获取 `resolveDshHome`

View File

@@ -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-24-web-session-model-selector.md
2026-07-24-web-session-model-selector.md: e6a96ac62f69a3bd312f61cc920caa259d2dc5b0
2026-07-24-web-session-model-selector.zh.md: 5a0245359d69ac6e59a20dc3276b9411c4b25e23
2026-07-24-web-session-model-selector.md: 3f7dde359842a82d03218dc71ca0e3617ae6dbdf
2026-07-24-web-session-model-selector.zh.md: 474476966da8d4628578dc3e13f74f643bd6eaed

Some files were not shown because too many files have changed in this diff Show More