Merge remote-tracking branch 'origin/master' into feat/desktop-update-channels

This commit is contained in:
winewill
2026-11-03 13:20:42 +08:00
2456 changed files with 87213 additions and 45340 deletions
+3
View File
@@ -2662,6 +2662,9 @@
"simplification/2026-09-19-deepseek-messages-only.i18n.yaml": "sha256:7ffd708c5823c39cc9536178292e7e7fa57db5267bbc9acdda0a87e4953d5ba8",
"simplification/2026-09-19-deepseek-messages-only.md": "sha256:241861200ce73e02f77d182633d6ea10b1f29c696c81bff002680762db01d9b5",
"simplification/2026-09-19-deepseek-messages-only.zh.md": "sha256:311ef6f8f4cf60dae2a43ca94d772028b645631501fb3648aaeb406ba174991e",
"simplification/2026-09-26-team-direct-inbox.i18n.yaml": "sha256:9a6f1137f0c68d976fda4f44c5c301b851a8ee8e46965f3ea808ec5551045fb0",
"simplification/2026-09-26-team-direct-inbox.md": "sha256:88f7d9be3ae0b740ddb5a61a16e3c64044a207584b973acab8c3a40c36a7ff30",
"simplification/2026-09-26-team-direct-inbox.zh.md": "sha256:d6d1fafa4aa7035a11930cca3bf998d676a62b5f9b75176dff69c3c5ff6b92f0",
"testing/2026-06-11-property-based-testing.i18n.yaml": "sha256:9d378912101ad30203f1fcbcad23e0f5d6ec6f39e04a83f17f90887b5c5d761e",
"testing/2026-06-11-property-based-testing.md": "sha256:cca3b6299784cff94965f21b6d59966109a0bb41fdc2f8cf56653fa6c417a20a",
"testing/2026-06-11-property-based-testing.zh.md": "sha256:09f3d1e2defd2792f87fb0e9892fb7e1db006f3c75ea4581f97e9cde4ea98175",
@@ -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/simplification/2026-09-26-team-direct-inbox.md
2026-09-26-team-direct-inbox.md: 9b90d7ae30e0b711c602de9280cc11845d0525f0
2026-09-26-team-direct-inbox.zh.md: 8838fb4a2824838b76142f30a6ff4d0aedf0276b
@@ -1,6 +1,7 @@
# Agent Note: Team messages use the target inbox
Status: implemented
Archived: 2026-10-15
English | [中文](2026-09-26-team-direct-inbox.zh.md)
@@ -1,6 +1,7 @@
# Agent Note: Team 消息使用目标 inbox
Status: implemented
Archived: 2026-10-15
[English](2026-09-26-team-direct-inbox.md) | 中文
@@ -21,8 +21,8 @@
en: 985e0eb63f3d1fd6
zh: 337cc44fadc6227e
/agent-note-single-file-executable-sdk-runtime-distribution-single-exe/decision/build-pipeline-and-artifacts:
en: 9a4ba22142a7be24
zh: 8553b02a923641a3
en: 4a55602284b462e8
zh: 762c4f8b719f179c
/agent-note-single-file-executable-sdk-runtime-distribution-single-exe/decision/python-sdk-distribution-two-carriers-exe-for-production-node-for-development:
en: b2a740a1ab7a5002
zh: a48be6ca6fc65912
@@ -48,7 +48,7 @@ The [Office kit](2026-09-14-independent-libreoffice-kit.md) and its installed de
### Build pipeline and artifacts
[`scripts/build-exe-for-python-sdk.ts`](../../../../scripts/build-exe-for-python-sdk.ts): runtime closure verification → `pnpm run build` → (after clearing) `pnpm --filter dsh-python-runtime-closure deploy --legacy --prod --config.allow-unused-patches=true --config.node-linker=hoisted --config.auto-install-peers=false --config.link-workspace-packages=true --config.hoist-workspace-packages=false` **directly into** `python/sdk-runtime/src/deepseek_harness_runtime/runtime/node/`, including [`python/sdk-runtime/runtime-bootstrap.mjs`](../../../../python/sdk-runtime/runtime-bootstrap.mjs) as the carrier-root `runtime-bootstrap.mjs` → restore direct workspace packages omitted by legacy deploy and reject any remaining manifest gap → replace staged dependency symlinks with their target bytes, remove package-manager `.bin` links, and fail if any symlink remains → verify the deployed bootstrap and inject pkg configuration with that bin plus assets covering dynamic profile, bundle, frontend, preset, native-library, and configuration reads → stage the target `node-pty` addon → invoke `pkg --sea --compress Brotli` once per target, where the vendored pkg patch pins Brotli's quality to 6: pkg passes no options, so its default quality 11 measured 132.8 s against 3.2 s at quality 6 for an archive 12% larger → write `deepseek-harness-sdk-runtime-<platform>-<arch>` under `dist-exe/` and copy it with its lightweight resource manifest and Office skill assets into the runtime directory. The Python runtime owns that bootstrap. It calls the public CLI export for ordinary launches and dispatches a provider-private selection to the same `@deepseek-ai/dsh-subprocess-local/runner` core without changing CLI grammar or adding another executable; the [native-containment reference](../../../../packages/subprocess/subprocess-local/README.md) owns that private path. Linux CI rebuilds `pty.node` inside the matching manylinux 2.28 container because legacy deploy omits that install side effect. Every target copies its native `@vscode/ripgrep` binary beside the executable as the required `-rg` sidecar; pkg runtimes select that sidecar through `process.pkg`, while ordinary Node execution uses `@vscode/ripgrep` directly. macOS uses its target prebuild and also emits the required `-spawn-helper`. The deploy flags preserve the measured runtime layout: `--legacy` is the mandatory path with inject-workspace-packages off; hoisted supplies package directories that staging makes symlink-free; staging removes nested workspace copies only when pnpm records the same dependency identity at the payload root, preserving singleton services; disabling workspace hoisting excludes unrelated workspace source packages; disabling automatic peer installation prevents undeclared peers from expanding the closure; link-workspace-packages selects direct workspace dependencies. [`pnpm-workspace.yaml`](../../../../pnpm-workspace.yaml) overrides the transitive `@deepseek-ai/cosmokit` and `@deepseek-ai/schemastery` semver requests to the pinned vendor sources so legacy deploy never resolves those unpublished names from a registry. Production deployment permits unused workspace patches because development-only tools can be absent from its closure; included-package patch failures still fail deployment. Workspace installs retain strict unused-patch validation.
[`scripts/build-exe-for-python-sdk.ts`](../../../../scripts/build-exe-for-python-sdk.ts): runtime closure verification → `pnpm run build` → (after clearing) `pnpm --filter dsh-python-runtime-closure deploy --prod --config.allow-unused-patches=true --config.node-linker=hoisted --config.pnpmfile=scripts/python-runtime-deploy.pnpmfile.mjs --config.link-workspace-packages=true --config.hoist-workspace-packages=false` **directly into** `python/sdk-runtime/src/deepseek_harness_runtime/runtime/node/`, including [`python/sdk-runtime/runtime-bootstrap.mjs`](../../../../python/sdk-runtime/runtime-bootstrap.mjs) as the carrier-root `runtime-bootstrap.mjs` → replace staged dependency symlinks with their target bytes, remove package-manager `.bin` links, and fail if any symlink remains → reject missing direct dependency directories, verify the deployed bootstrap, and inject pkg configuration with that bin plus assets covering dynamic profile, bundle, frontend, preset, native-library, and configuration reads → stage the target `node-pty` addon → invoke `pkg --sea --compress Brotli` once per target, where the vendored pkg patch pins Brotli's quality to 6: pkg passes no options, so its default quality 11 measured 132.8 s against 3.2 s at quality 6 for an archive 12% larger → write `deepseek-harness-sdk-runtime-<platform>-<arch>` under `dist-exe/` and copy it with its lightweight resource manifest and Office skill assets into the runtime directory. The Python runtime owns that bootstrap. It calls the public CLI export for ordinary launches and dispatches a provider-private selection to the same `@deepseek-ai/dsh-subprocess-local/runner` core without changing CLI grammar or adding another executable; the [native-containment reference](../../../../packages/subprocess/subprocess-local/README.md) owns that private path. Linux CI rebuilds `pty.node` inside the matching manylinux 2.28 container and copies the resulting addon into the deployed tree. Every target copies its native `@vscode/ripgrep` binary beside the executable as the required `-rg` sidecar; pkg runtimes select that sidecar through `process.pkg`, while ordinary Node execution uses `@vscode/ripgrep` directly. macOS uses its target prebuild and also emits the required `-spawn-helper`. The deploy flags preserve the measured runtime layout: the dedicated deploy lockfile retains the source lockfile’s versions and peer settings; [`scripts/python-runtime-deploy.pnpmfile.mjs`](../../../../scripts/python-runtime-deploy.pnpmfile.mjs) preserves exact local build approvals; the [locked deployment decision](../bug-fix/2026-11-01-locked-python-runtime-deploy.md) supersedes the legacy deployment requirement and disabled-peer rationale; hoisted supplies package directories that staging makes symlink-free; staging removes nested workspace copies only when pnpm records the same dependency identity at the payload root, preserving singleton services; disabling workspace hoisting excludes unrelated workspace source packages; 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 deploy never resolves those unpublished names from a registry. Production deployment permits unused workspace patches because development-only tools can be absent from its closure; included-package patch failures still fail deployment. Workspace installs retain strict unused-patch validation.
CI: [`.github/workflows/build-exe-for-python-sdk.yml`](../../../../.github/workflows/build-exe-for-python-sdk.yml) runs [installed-wheel validation](../testing/2026-08-23-installed-python-wheel-black-box-ci.md) on Linux/Windows x64 for pull requests and Linux ARM64 plus both macOS architectures for master pushes. The [public publication workflow](../../archived/process/2026-08-11-python-publication-workflow.md) calls it for all five targets; `workflow_dispatch` can still select a subset. Native builds run on linux-x64 / linux-arm64 (`ubuntu-24.04-arm`) / macos-arm64 / macos-x64 (`macos-15-intel`) / win-x64 (`windows-2025`), with `~/.pkg-cache` cached where applicable, and pkg handles macOS ad-hoc signing. Each leg installs the release-shaped SDK and runtime wheels into a clean venv outside the checkout, verifies their package and executable origin, then drives the complete keyless scenario set through the public SDK and direct NDJSON JSON-RPC. Trusted pull requests and master pushes additionally run a real DeepSeek two-turn tool smoke on their selected targets; fork and Dependabot heads receive no key. Linux inspects the executable and native addon's GLIBC requirements and runs an additional manylinux 2.28 smoke, while macOS checks the runtime, ripgrep, and PTY helper architectures and verifies that all three deployment targets fit the wheel tag. A full five-target run retains six artifacts, each containing one release file: the platform-independent SDK wheel and five native runtime wheels; a subset dispatch retains the SDK wheel and selected runtime wheels. Bare executables and source bundles remain intermediate test inputs. [`.gitlab-ci.yml`](../../../../.gitlab-ci.yml) accepts `python-v<repository-version>` tag pipelines whose version matches the root `package.json`, builds one SDK wheel and five native runtime wheels, then a single serialized job checks and publishes all six to the project PyPI registry. The [`python/sdk-runtime` README](../../../../python/sdk-runtime/README.md) owns the Windows target and the explicit exclusion of Windows arm64.
@@ -48,7 +48,7 @@ exe 的 VFS 内是**构建产物形态的真实包树**(各包的 `lib/` + 真
### 构建流水线与产物
[`scripts/build-exe-for-python-sdk.ts`](../../../../scripts/build-exe-for-python-sdk.ts):运行时闭包校验 → `pnpm run build` →(清空后)`pnpm --filter dsh-python-runtime-closure deploy --legacy --prod --config.allow-unused-patches=true --config.node-linker=hoisted --config.auto-install-peers=false --config.link-workspace-packages=true --config.hoist-workspace-packages=false` **直接写入** `python/sdk-runtime/src/deepseek_harness_runtime/runtime/node/`,其中包含作为载体根目录 `runtime-bootstrap.mjs` 的 [`python/sdk-runtime/runtime-bootstrap.mjs`](../../../../python/sdk-runtime/runtime-bootstrap.mjs) → 恢复 legacy deploy 遗漏的直接工作区包,并拒绝剩余的 manifest 缺口 → 将暂存依赖中的符号链接替换为目标文件内容,删除包管理器的 `.bin` 链接,并在仍有任何符号链接时失败 → 验证已部署的 bootstrap,并注入以该文件为 bin 的 pkg 配置及覆盖动态读取 profile、bundle、前端、preset、原生库与配置文件的 assets → 暂存目标平台的 `node-pty` addon → 每个构建目标调用一次 `pkg --sea --compress Brotli`,其中 vendor 的 pkg 补丁把 Brotli 质量固定为 6:pkg 不传参数时默认质量 11,同一载荷实测 132.8 s,质量 6 为 3.2 s,产物大 12% → 将 `deepseek-harness-sdk-runtime-<platform>-<arch>` 写入 `dist-exe/`,并将它与轻量资源清单及 Office skill 资源一同拷回运行时目录。该 bootstrap 由 Python runtime 拥有;普通启动时它调用公开 CLI export,而提供方私有选择会分派到同一个 `@deepseek-ai/dsh-subprocess-local/runner` 核心,不改变 CLI 语法,也不增加另一个可执行文件;[原生 containment 参考](../../../../packages/subprocess/subprocess-local/README.zh.md)负责这条私有路径。Linux CI 会在匹配的 manylinux 2.28 容器中重新构建 `pty.node`,因为 legacy deploy 会遗漏这一安装副作用。每个目标都会把对应的原生 `@vscode/ripgrep` 二进制复制到可执行文件旁,作为必需的 `-rg` 伴随文件;pkg 运行时通过 `process.pkg` 选择该伴随文件,普通 Node 执行则直接使用 `@vscode/ripgrep`。macOS 使用对应目标的预构建产物,并额外生成所需的 `-spawn-helper`。部署标志都有实测依据:未启用 `inject-workspace-packages` 时必须使用 `--legacy`;`hoisted` 提供包目录,再由暂存步骤消除符号链接;只有 pnpm 在载荷根目录记录了相同依赖标识时,暂存步骤才删除对应的嵌套工作区副本,以保留服务单例;关闭工作区提升可排除无关的工作区源码包;关闭对等依赖自动安装可防止未声明的对等依赖扩大闭包;`link-workspace-packages` 选择直接工作区依赖。[`pnpm-workspace.yaml`](../../../../pnpm-workspace.yaml) 将传递的 `@deepseek-ai/cosmokit` 与 `@deepseek-ai/schemastery` semver 请求覆盖到固定的 vendor 源码,使 legacy deploy 不会从注册表解析这些未发布名称。 生产部署允许未使用的工作区补丁,因为开发工具可能不属于运行时闭包;闭包内包的补丁应用失败仍会中止部署。工作区安装继续严格校验未使用的补丁。
[`scripts/build-exe-for-python-sdk.ts`](../../../../scripts/build-exe-for-python-sdk.ts):运行时闭包校验 → `pnpm run build` →(清空后)`pnpm --filter dsh-python-runtime-closure deploy --prod --config.allow-unused-patches=true --config.node-linker=hoisted --config.pnpmfile=scripts/python-runtime-deploy.pnpmfile.mjs --config.link-workspace-packages=true --config.hoist-workspace-packages=false` **直接写入** `python/sdk-runtime/src/deepseek_harness_runtime/runtime/node/`,其中包含作为载体根目录 `runtime-bootstrap.mjs` 的 [`python/sdk-runtime/runtime-bootstrap.mjs`](../../../../python/sdk-runtime/runtime-bootstrap.mjs) → 将暂存依赖中的符号链接替换为目标文件内容,删除包管理器的 `.bin` 链接,并在仍有任何符号链接时失败 → 拒绝缺失的直接依赖目录,验证已部署的 bootstrap,并注入以该文件为 bin 的 pkg 配置及覆盖动态读取 profile、bundle、前端、preset、原生库与配置文件的 assets → 暂存目标平台的 `node-pty` addon → 每个构建目标调用一次 `pkg --sea --compress Brotli`,其中 vendor 的 pkg 补丁把 Brotli 质量固定为 6:pkg 不传参数时默认质量 11,同一载荷实测 132.8 s,质量 6 为 3.2 s,产物大 12% → 将 `deepseek-harness-sdk-runtime-<platform>-<arch>` 写入 `dist-exe/`,并将它与轻量资源清单及 Office skill 资源一同拷回运行时目录。该 bootstrap 由 Python runtime 拥有;普通启动时它调用公开 CLI export,而提供方私有选择会分派到同一个 `@deepseek-ai/dsh-subprocess-local/runner` 核心,不改变 CLI 语法,也不增加另一个可执行文件;[原生 containment 参考](../../../../packages/subprocess/subprocess-local/README.zh.md)负责这条私有路径。Linux CI 会在匹配的 manylinux 2.28 容器中重新构建 `pty.node`,并将生成的 addon 复制到部署树中。每个目标都会把对应的原生 `@vscode/ripgrep` 二进制复制到可执行文件旁,作为必需的 `-rg` 伴随文件;pkg 运行时通过 `process.pkg` 选择该伴随文件,普通 Node 执行则直接使用 `@vscode/ripgrep`。macOS 使用对应目标的预构建产物,并额外生成所需的 `-spawn-helper`。部署标志都有实测依据:独立部署锁文件保留源锁文件的版本及对等依赖设置;[`scripts/python-runtime-deploy.pnpmfile.mjs`](../../../../scripts/python-runtime-deploy.pnpmfile.mjs) 保留精确的本地构建审批;[按锁文件部署的决策](../bug-fix/2026-11-01-locked-python-runtime-deploy.zh.md)替代 legacy 部署要求及关闭对等依赖的理由;`hoisted` 提供包目录,再由暂存步骤消除符号链接;只有 pnpm 在载荷根目录记录了相同依赖标识时,暂存步骤才删除对应的嵌套工作区副本,以保留服务单例;关闭工作区提升可排除无关的工作区源码包;`link-workspace-packages` 选择直接工作区依赖。[`pnpm-workspace.yaml`](../../../../pnpm-workspace.yaml) 将传递的 `@deepseek-ai/cosmokit` 与 `@deepseek-ai/schemastery` semver 请求覆盖到固定的 vendor 源码,使 deploy 不会从注册表解析这些未发布名称。 生产部署允许未使用的工作区补丁,因为开发工具可能不属于运行时闭包;闭包内包的补丁应用失败仍会中止部署。工作区安装继续严格校验未使用的补丁。
CI 使用 [`.github/workflows/build-exe-for-python-sdk.yml`](../../../../.github/workflows/build-exe-for-python-sdk.yml):[安装后 wheel 包验证](../testing/2026-08-23-installed-python-wheel-black-box-ci.zh.md)在拉取请求上运行 Linux/Windows x64,在 master 推送上运行 Linux ARM64 与两种 macOS 架构。[公开发布工作流](../../archived/process/2026-08-11-python-publication-workflow.md)调用它构建全部五个目标;`workflow_dispatch` 仍可选择部分目标。linux-x64、linux-arm64(`ubuntu-24.04-arm`)、macos-arm64、macos-x64(`macos-15-intel`)与 win-x64(`windows-2025`)分别进行原生构建,并在适用平台缓存 `~/.pkg-cache`;macOS 的 ad-hoc 签名由 pkg 处理。每个平台都把发布形态的 SDK wheel 包与运行时 wheel 包安装到 checkout 外的干净 venv,证明包与可执行文件来源,再通过公开 SDK 与直接 NDJSON JSON-RPC 运行完整 keyless 场景。可信拉取请求与 master 推送还会在各自选定的目标上运行真实 DeepSeek 双轮工具冒烟测试;fork 与 Dependabot head 不会获得密钥。Linux 会检查可执行文件和原生 addon 各自的 GLIBC 依赖,并额外运行 manylinux 2.28 冒烟测试;macOS 则检查 runtime、ripgrep 与 PTY helper 的架构,并验证三个载荷的部署目标都符合 wheel 包标签。完整构建五个目标时保留 6 个产物,每个产物只含一个发布文件:平台无关的 SDK wheel 包与 5 个原生运行时 wheel 包;手动选择部分目标时保留 SDK wheel 与所选运行时 wheel。裸 exe 与源码包只作为测试中间输入。[`.gitlab-ci.yml`](../../../../.gitlab-ci.yml) 只接受版本与根目录 `package.json` 匹配的 `python-v<repository-version>` 标签流水线,构建一个 SDK wheel 包和 5 个原生运行时 wheel 包,再由单个串行任务校验并将这 6 个文件发布到项目的 PyPI 注册表。[`python/sdk-runtime` README](../../../../python/sdk-runtime/README.zh.md)负责 Windows 目标及对 Windows arm64 的明确排除。
@@ -21,8 +21,8 @@
en: 0a4240d88cca240a
zh: 8f5f1527caf571cd
/agent-note-client-plugin-loading-lazy-factories-cordis-lifecycle-and-hot-reload/decision/the-loading-flow-end-to-end:
en: 15fb56befd328339
zh: 0ffb10ada7e52805
en: dd05aa1ee608c414
zh: 2863ced0086e3122
/agent-note-client-plugin-loading-lazy-factories-cordis-lifecycle-and-hot-reload/decision/live-graph-reconciliation:
en: 37e2fa1fe5cb2b95
zh: 80e513db87709cb4
@@ -33,8 +33,8 @@
en: 94ca0d09e6fd1e3a
zh: d5949a2bdec795ba
/agent-note-client-plugin-loading-lazy-factories-cordis-lifecycle-and-hot-reload/consequences:
en: 16e1f2467c559315
zh: a714ec6ff2722cf1
en: 624efdd136029771
zh: 7d60207147e631fe
/agent-note-client-plugin-loading-lazy-factories-cordis-lifecycle-and-hot-reload/alternatives-considered:
en: b8f036f50cff7cd8
zh: 6d623c4a1f4314b1
@@ -67,8 +67,8 @@ Why is the roster yml rows and not a scan? Because which plugins compose into a
1. The kernel mounts the vendored Loader and injects the module system as `internal` before any entry exists. Ordering matters: `tree.import`'s bare-import fallback must never run in a browser.
2. It creates every graph row uniformly. Importing the modules row returns the memoized bootstrap exports, whose `apply()` reads that tree's `Loader.internal` and provides the same instance as `ctx.modules`; rows that require that service remain PENDING until then, so the modules row needs no special creation position. Render assembly is an ordinary host-graph row provided by `dsh-client-ui-renderer`; the kernel appends no assembly pseudo-entry.
3. Graph order governs synchronous factory availability; Cordis activation remains independent and proceeds through service waiting.
4. `settled` = every entry created + `loader.await()` quiescent + an all-ACTIVE sweep. The sweep lists each import-failed, FAILED, or PENDING fiber with its missing services. It exists because cordis inject waits have no timeout — the sweep is the fail-loud floor.
5. The framework-free loading page projects real fiber states via `internal/status`. After the sweep, the kernel calls `ctx.uiRenderer.mount(container)` and replaces the page with the real UI in one pass.
4. `settled` = every entry created + `loader.await()` quiescent + the boot audit. The audit lists each import-failed, FAILED, or PENDING fiber with its activation error or missing services. It exists because cordis inject waits have no timeout. An inactive entry on the browser required list rejects startup; other inactive entries produce one console warning ([startup policy](2026-09-09-consumer-owned-startup-strictness.md)).
5. The framework-free loading page projects real fiber states via `internal/status`. After the audit accepts the tree, the kernel calls `ctx.uiRenderer.mount(container)` and replaces the page with the real UI in one pass.
### Live graph reconciliation
@@ -104,7 +104,7 @@ The current package inventory and build forms live in the [client shell layering
One governance implementation runs on both sides of the wire; the browser-specific layer is one module system plus one reload plugin. Dynamic packages have one artifact form, so the purity check covers them all. Cordis dependencies, module requests, and the boot tier live with their owners — the manifests — while the composing app holds only the roster. Host graph validation and recursive request arrival keep synchronous factory dependencies explicit. Browser-native script loading preserves the standard mapping among plugin network resources, generated bundles, and TypeScript/TSX sources, while the module system keeps two replaceable carrier hooks: `loadBundle` for bundle bytes and `resolveAsset` for package-asset references.
Costs accepted: the vendored Loader carries idle machinery in the browser (EntryTree persistence is a no-op, groups/isolation unused); every plugin edit in dev pays a bundle rebuild plus fiber remount; graph `inject` rows guide factory arrival but service availability remains the activation authority, so a mismatch appears at the settled sweep; the static UI libraries keep direct value exports; every bundle gains a source-map artifact; and external-script failures provide only coarse URL diagnostics instead of the HTTP status available to an explicit fetch. The Host retains bundle snapshots and lazy response plans for the current graph and one prior startup generation. Script and map bodies are cached after their first `GET`, so memory grows with bundle snapshots and requested response bodies. Each materialized response stays fixed at its URL; a prior-generation map first requested after a rebuild reads the current map file.
Costs accepted: the vendored Loader carries idle machinery in the browser (EntryTree persistence is a no-op, groups/isolation unused); every plugin edit in dev pays a bundle rebuild plus fiber remount; graph `inject` rows guide factory arrival but service availability remains the activation authority, so a mismatch appears at the boot audit; the static UI libraries keep direct value exports; every bundle gains a source-map artifact; and external-script failures provide only coarse URL diagnostics instead of the HTTP status available to an explicit fetch. The Host retains bundle snapshots and lazy response plans for the current graph and one prior startup generation. Script and map bodies are cached after their first `GET`, so memory grows with bundle snapshots and requested response bodies. Each materialized response stays fixed at its URL; a prior-generation map first requested after a rebuild reads the current map file.
Roster: it lives in the web bundle's config tree (`packages/bundle/web-app/cordis.patch.yml`); `mountWebPlugins` and the `CLIENT_PACKAGES` constant are gone, and recomposing a deployment means swapping the yml/overlay. The graph composer lives in the `dsh-client-modules` node half, while the parser-preloaded client face bootstraps the browser module table. The webserver remains a plain route-registration plugin; `/api/*` binding, browser authentication, RPC envelopes, and exact Fetch routes belong to the Connection node half, while Remote dispatch belongs to API Gateway and the dev bundle watch plus SSE channel belongs to the hmr node half.
@@ -67,8 +67,8 @@ Host 会快照每个已构建插件入口 bundle,并把每个调度阶段的
1. 内核挂载 vendored Loader,在任何 entry 存在之前就把模块系统注入为 `internal`。顺序有讲究:`tree.import` 的裸 import 兜底分支在浏览器里绝不能跑到。
2. 它统一创建每个 graph row。Import modules row 会返回记忆化的 bootstrap exports,其 `apply()` 读取该树的 `Loader.internal`,把同一个实例提供为 `ctx.modules`;需要该 service 的 row 会保持 PENDING 直至此时,因此 modules row 无需特殊创建位置。渲染组装是由 `dsh-client-ui-renderer` 提供的普通 host graph row;内核不追加组装伪 entry。
3. Graph 顺序治理同步 factory 可用性;Cordis 激活与之独立,仍经服务等待推进。
4. `settled` = 每个 entry 已创建 + `loader.await()` 完全停稳 + 一次全 ACTIVE 扫描。扫描列出每个 import 失败、FAILED 或 PENDING 的 fiber 及其缺失的服务。它存在的理由:cordis 的 inject 等待没有超时——这次扫描就是大声失败的兜底线。
5. 不依赖框架的 loading 页经 `internal/status` 投影真实 fiber 状态。检查完成后,内核调用 `ctx.uiRenderer.mount(container)`,一次切换到真实 UI。
4. `settled` = 每个 entry 已创建 + `loader.await()` 完全停稳 + 启动审计。审计列出每个 import 失败、FAILED 或 PENDING 的 fiber 及其激活错误或缺失的服务。它存在的理由:cordis 的 inject 等待没有超时。浏览器必需名单中的 entry 未激活会拒绝启动;其他未激活 entry 输出一条控制台警告([启动策略](2026-09-09-consumer-owned-startup-strictness.zh.md))。
5. 不依赖框架的 loading 页经 `internal/status` 投影真实 fiber 状态。审计接受插件树后,内核调用 `ctx.uiRenderer.mount(container)`,一次切换到真实 UI。
### 动态图对账
@@ -104,7 +104,7 @@ Bootstrap 替换会在失效或卸载前被拒绝:模块系统保留其初始
Wire 两侧运行同一份治理实现;浏览器特有层只包含一套模块系统和一个重载插件。动态包只有一种产物形态,因此纯度检查覆盖全部动态包。Cordis 依赖、模块请求与启动档位都与其所有者——manifest——同住,负责组合的 app 只握名册。Host graph 校验与递归请求到达使同步 factory 依赖保持显式。浏览器原生 script 装载保留插件网络资源、生成 bundle 与 TypeScript/TSX 源码之间的标准映射,模块系统只保留两个可替换的载体钩子:负责 bundle 字节的 `loadBundle` 和负责包资源引用的 `resolveAsset`。
接受的代价:vendored Loader 在浏览器里背着闲置机件(EntryTree 持久化是 no-op,分组/隔离未用);开发期每次修改插件都要付一次 bundle 重建加 fiber 重挂;graph `inject` row 指导 factory 到达,但服务可用性仍是激活权威,因此不匹配会在 settled 扫描时浮出;静态 UI 库保留直接实体导出;每个 bundle 多出一份 sourcemap 产物,外部 script 失败也只能给出粗粒度 URL 诊断,不能像显式 fetch 那样报告 HTTP 状态。Host 为当前图及上一代启动图保留 bundle 快照与惰性响应计划。脚本和 map body 在首次 `GET` 后缓存,因此内存随 bundle 快照和已请求的响应 body 增长。每份已物化响应在其 URL 下保持固定;若上一代 map 在重建后才首次被请求,则会读取当前 map 文件。
接受的代价:vendored Loader 在浏览器里背着闲置机件(EntryTree 持久化是 no-op,分组/隔离未用);开发期每次修改插件都要付一次 bundle 重建加 fiber 重挂;graph `inject` row 指导 factory 到达,但服务可用性仍是激活权威,因此不匹配会在启动审计时浮出;静态 UI 库保留直接实体导出;每个 bundle 多出一份 sourcemap 产物,外部 script 失败也只能给出粗粒度 URL 诊断,不能像显式 fetch 那样报告 HTTP 状态。Host 为当前图及上一代启动图保留 bundle 快照与惰性响应计划。脚本和 map body 在首次 `GET` 后缓存,因此内存随 bundle 快照和已请求的响应 body 增长。每份已物化响应在其 URL 下保持固定;若上一代 map 在重建后才首次被请求,则会读取当前 map 文件。
名册位于 web 组合包的配置树(`packages/bundle/web-app/cordis.patch.yml`);`mountWebPlugins` 与 `CLIENT_PACKAGES` 常量已消失,重组一次部署等于替换 yml/overlay。Graph 组合器位于 `dsh-client-modules` node 半,由 parser 预载的 Client face 则自举浏览器模块表。Webserver 继续作为朴素路由注册插件;`/api/*` 绑定、浏览器认证、RPC envelope 与精确 Fetch 路由属于 Connection node 半,Remote 分发属于 API Gateway,开发期 bundle 监视与 SSE 通道属于 HMR node 半。
@@ -9,11 +9,11 @@
en: fca00eeb7a8d9abd
zh: d7a9766f5322dff9
/agent-note-session-projections-as-a-required-reader-seam/decision:
en: 8d38ed9356085c7e
zh: e15ede51415192f3
en: ed3ee779d31afdba
zh: 2ffad21003be59c5
/agent-note-session-projections-as-a-required-reader-seam/alternatives-considered:
en: aea3da939c90d240
zh: 21e91c481cdd23b7
en: 778e12e73a9a6900
zh: 5d4f313e4ea87372
/agent-note-session-projections-as-a-required-reader-seam/consequences:
en: ab45eb87f7c22cdb
zh: e65a97991a45164f
en: b1935fb4cc8d9cc6
zh: 0611d7033f5702a3
@@ -18,6 +18,8 @@ Owners acquire state through [awaited registration and retained handles](2026-10
`onChanged` publishes client-visible value changes only. Registration changes do not create a second Host event stream or client tombstone protocol. A later authoritative history or list baseline reflects the active key set.
`onEventChanged` groups the changes of one committed event. After every unit has been driven, it receives one `{ key, val }` entry per client-visible key whose view changed, in registration order, where `val` is the key's view. If a unit or an `onChanged` listener throws, it still receives the entries collected before the throw. Remote projection frames follow this grouping: `session/control` and `session/observeProjections` emit one frame per committed event with a `changes` array of the same entries, so a Client applies one event's changes and publishes its Session list once instead of once per key. Each entry names its key, so a key changed to `null` stays distinct from an unchanged key and no consumer pairs values with keys by position. `onChanged` remains for Host consumers that react to one key.
## Alternatives considered
- **Default missing projected state.** This preserves more partial compositions but makes missing host state indistinguishable from a valid empty value. Rejected because official profiles mount the registry and configuration errors must fail explicitly.
@@ -25,9 +27,14 @@ Owners acquire state through [awaited registration and retained handles](2026-10
- **Use `snapshot()` for every read.** This keeps one method but computes unrelated wire views and encourages consumers to depend on batch transport data for host logic. Rejected in favor of typed single-key state reads.
- **Send full host values to clients.** This avoids separate view types but exposes producer identities and policy knobs that no client consumes. Rejected in favor of explicit cropped views.
- **Broadcast registry additions and removals across Host and mux streams.** The streams have no shared ordering, so clients need tombstones, buffered frames, and baseline retries to reconcile them. Rejected because plugin-key churn does not justify a second synchronization protocol.
- **Coalesce per-key frames in the controller or Client.** Microtask batching or grouping frames by sequence number needs buffering and timing assumptions, while the registry already knows where each event's drive ends. Rejected in favor of grouping at the drive.
- **Carry frame changes as a key-to-value object.** Consumers would infer the changed keys from object members and lose the registration order. Rejected in favor of an ordered array of `{ key, val }` entries.
- **Carry parallel `keys` and `values` arrays.** This avoids repeating the entry field names, but every consumer must keep the two arrays aligned by index, and the wire types cannot express their equal length. Rejected in favor of self-contained entries.
- **Replace `onChanged` with the event-level feed.** Host consumers such as forge react to one key and gain nothing from grouping. Rejected; both feeds remain.
## Consequences
- Missing projection composition fails during activation; it never degrades to a default value.
- Host consumers avoid repeated whole-registry snapshots and log scans.
- Wire payloads exclude host-only fields and per-key watermark wrappers; ordinary baselines communicate the active key set.
- One committed event produces at most one Remote projection frame per Session, whose `seq` is that event's sequence number.
@@ -18,6 +18,8 @@ Status: implemented
`onChanged` 只发布客户端可见值的变化。注册变化不会创建第二条 Host 事件流或客户端 tombstone 协议。后续权威 history 或 list 基线会反映活跃 key 集。
`onEventChanged` 把一个已提交事件的变化归为一组。所有单元驱动完成后,它按注册顺序收到视图发生变化的每个客户端可见 key 一项 `{ key, val }`,`val` 是该 key 的视图。某个单元或 `onChanged` 监听器抛错时,它仍会收到抛错前已收集的各项。Remote projection 帧沿用这一分组:`session/control` 与 `session/observeProjections` 每个已提交事件发出一帧,携带由同样各项组成的 `changes` 数组,因此 Client 一次应用一个事件的变化,Session 列表只发布一次,而不是每个 key 一次。每项都写明自己的 key,因此改为 `null` 的 key 与未变化的 key 可以区分,消费方也不必按位置把值和 key 配对。`onChanged` 保留给只关心单个 key 的 Host 消费方。
## 考虑过的替代方案
- **为缺失的投影状态提供默认值。** 这会保留更多不完整组合,但缺失 host 状态将无法与合法空值区分。正式 profile 已挂载注册表,配置错误必须显式失败,因此否决。
@@ -25,9 +27,14 @@ Status: implemented
- **每次读取都使用 `snapshot()`。** 这只保留一个方法,但会计算无关 wire view,并鼓励消费方让 host 逻辑依赖批量传输数据。改用类型化单 key 状态读取。
- **向客户端发送完整 host 值。** 这避免单独的 view 类型,但会暴露客户端不消费的来源信息和策略旋钮。改用显式裁剪的 view。
- **跨 Host 和 mux stream 广播注册表新增和移除。** 两条 stream 没有共享顺序,客户端因此需要 tombstone、缓冲帧和基线重试来协调。插件 key 变化不值得引入第二套同步协议。
- **在 controller 或 Client 中合并逐 key 帧。** 按 microtask 攒批或按序号分组都需要缓冲和时序假设,而注册表本来就知道每个事件的驱动在哪里结束。改为在驱动处分组。
- **把帧内的变化做成 key 到值的对象。** 消费方要从对象成员推断哪些 key 变化,也会丢失注册顺序。改用有序的 `{ key, val }` 数组。
- **使用平行的 `keys` 与 `values` 两个数组。** 这样不必重复每项的字段名,但每个消费方都要按下标保持两个数组对齐,线路类型也无法表达两者等长。改用自带 key 的各项。
- **用事件级变更流取代 `onChanged`。** forge 等 Host 消费方只关心单个 key,分组对它们没有好处。两条变更流都保留。
## 后果
- 缺少投影组合会在激活期间失败,绝不会降级为默认值。
- host 消费方避免重复的全注册表快照和日志扫描。
- 协议负载排除 host 内部字段和逐 key 水位包装;普通基线会传达活跃 key 集。
- 一个已提交事件对每个 Session 至多产生一个 Remote projection 帧,其 `seq` 是该事件的序号。
@@ -30,8 +30,8 @@
en: efded09f0cd30a8e
zh: 5e16580a39ff3359
/agent-note-session-observations-and-projection-owned-client-state/decision/client-merge-rules:
en: fb8740be674e4708
zh: 7dc0c89558f51792
en: 135a93a2acecaf2a
zh: 87b69116a64b80b5
/agent-note-session-observations-and-projection-owned-client-state/decision/domain-applications:
en: 4dcae9e00148b11e
zh: acfd1b6831cf50fe
@@ -106,7 +106,7 @@ These distinctions prevent one overloaded `undefined` from representing cache mi
|---|---|---|---|
| Session list hints | Partial | Last durable checkpoint or bounded fallback cut | Unknown |
| Follow opening baseline | Complete for the Host composition | Exact opening cursor | Capability absent |
| Projection frame | One whole key | Event sequence carried by the frame | Not applicable |
| Projection frame | Every key the frame lists, each a whole value | Event sequence carried by the frame | Unchanged |
The Client stores one row per key. A row the connected Host computed carries its sequence number: a newer baseline or frame replaces it, an equal or older input is ignored, and reconnect can therefore replace the event window without rolling back a projection frame that was already accepted at a later sequence. A list hint viewed from the persisted cache carries no comparable sequence number and yields to every Host-sequenced write ([projection cache listing identity and cached rows](2026-09-19-projection-cache-listing-identity-and-cached-rows.md)).
@@ -106,7 +106,7 @@ Projection 的三种交付状态含义不同:
|---|---|---|---|
| Session list hints | 部分 | 上次持久 checkpoint 或有界 fallback cut | 未知 |
| Follow opening baseline | 对当前 Host composition 完整 | 精确 opening cursor | Capability 不存在 |
| Projection frame | 单个完整 key | Frame 携带的 event sequence | 不适用 |
| Projection frame | 帧列出的每个 key,各为完整值 | Frame 携带的 event sequence | 未变化 |
Client 为每个 key 保存一行。由建连 Host 算出的行带 sequence number:更新的 baseline 或 frame 会替换它,相同或更旧的输入被忽略,因此 reconnect 可以替换 event window,而不会回退已经在更晚 sequence 接受的 projection frame。从持久化缓存看出来的 list hint 不带可比的 sequence number,让位于所有 Host 排序过的写入([Projection cache 只读面按 lifecycle 身份匹配,客户端 store 区分 cached 与 sequenced 行](2026-09-19-projection-cache-listing-identity-and-cached-rows.zh.md))。
@@ -3,23 +3,23 @@
# After editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-27-adjacent-agent-steer-messaging.md
/agent-note-adjacent-agents-share-one-steer-send-message-operation:
en: 758d31ae04ce0d4f
zh: 112475efbd52b9f6
en: 9d0af2bae38b1eec
zh: 8a6e8502a5195548
/agent-note-adjacent-agents-share-one-steer-send-message-operation/problem:
en: c988b8a3be4e6f19
zh: b58011fd8d2e878e
en: 5dc4c218f648c27e
zh: b48531fac94b25db
/agent-note-adjacent-agents-share-one-steer-send-message-operation/decision:
en: 1188c699ce6e6ca2
zh: 4d5b974cd1216f06
en: f4789878ac0e79b2
zh: 68eb68d34cba810d
/agent-note-adjacent-agents-share-one-steer-send-message-operation/decision/one-model-tool-and-one-return-instruction:
en: 4b0a25389324fa91
zh: c2cdd9f7e40ef43b
en: 62284bd7dd804188
zh: b3a8dfebffc01bdb
/agent-note-adjacent-agents-share-one-steer-send-message-operation/decision/complete-removal-and-reintroduction-condition:
en: 5a9ec6478cf7430c
zh: f4c2f067cc72040e
en: 8c6d355d91ec83d2
zh: fcc74d1e6ef477bf
/agent-note-adjacent-agents-share-one-steer-send-message-operation/alternatives-considered:
en: 1f62d5d938144666
zh: dec0f30228c5b7a7
en: 7386e88e7051dce7
zh: dcea5b9e30374be3
/agent-note-adjacent-agents-share-one-steer-send-message-operation/consequences:
en: d55de74742c3af4b
zh: d894f9a33c8e4691
en: 9c23dd6ace029ec3
zh: f08ef3f142534843
@@ -4,26 +4,25 @@ Status: implemented
English | [中文](2026-08-27-adjacent-agent-steer-messaging.zh.md)
The adjacency, discovery, and parent-lifetime restrictions below are superseded by [tree-wide communication](2026-10-16-tree-wide-agent-communication.md); the remaining decisions stay active. The tool identity marker and initial-task communication guidance below are superseded by [tool-owned communication](2026-10-22-subagent-tool-owned-communication.md).
## Problem
Continuable Agents originally used direction-specific model controls. A parent called `send_message({ subagent_id, message })`, which delegated to a FIFO `followup` service operation. A child instead received a child-scoped `report({ output })` tool, a `tool:report` system-prompt section, and deployment-selected quiet or waking delivery. The tools described one adjacent-Agent operation through different schemas, service paths, source attribution, and scheduling.
A continuable child owns its own Session, so its parent does not automatically receive the child's transcript, tool output, or reasoning. The return path must therefore remain explicit and repeatable: a child may send progress before it finishes, remain available after sending, or fail before it can cooperate. Turning every final assistant message into an implicit result would conflate turn completion with model-selected communication and would not cover abnormal endings.
A continuable child owns its own Session, so its parent does not automatically receive the child's transcript, tool output, or reasoning. Explicit messages let a child send actionable findings before finishing. The runtime separately delivers terminal output in a settlement notice when the direct creator is still online.
The child-only tool and system-prompt section also preceded every inherited fork turn. They made a continuable fork child's request head differ from its parent's before the history that fork exists to reuse, forcing the provider to prefill the entire copied transcript again.
## Decision
`SubagentRuntime.sendMessage(sender, targetId, content, { signal })` is the only public model-authored message operation. The continuation manager accepts only the exact live sender and a target on one adjacent edge:
`SubagentRuntime.sendMessage(sender, targetId, content, { signal })` is the only public model-authored message operation. The continuation manager accepts only the exact live sender and a target in the same root tree; [tree-wide communication](2026-10-16-tree-wide-agent-communication.md) owns authorization and discovery.
- parent to direct continuable child, authorized by the child's durable `SessionHeader.parentSession`;
- resident continuable child to its exact live direct parent, authorized by the child's Activation.
Self-targets, stale Agent objects, unknown targets, and external executions are not alternate routes. The operation has no caller-supplied source, delivery mode, offline root mailbox, or provider dispatch.
Siblings, self-targets, ancestors beyond one edge, stale Agent objects, unknown targets, and one-shot children are not alternate routes. The operation has no caller-supplied source, delivery mode, offline parent mailbox, or provider dispatch.
Every accepted message uses `Agent.steer()`. A running target receives it at the nearest step boundary; an idle target starts a turn. An absent target is cold-resumed under the root before the same Steer delivery; its creation ancestors can remain offline. The manager retains waking-send accounting so a continuation-managed target cannot settle between synchronous inbox insertion and driver admission.
Every accepted message uses `Agent.steer()`. A running target receives it at the nearest step boundary; an idle target starts a turn. An absent direct child is cold-resumed through the existing continuation lifecycle before the same Steer delivery. The manager retains waking-send accounting so a continuation-managed target cannot settle between synchronous inbox insertion and driver admission.
Every direction uses one durable source. The service derives `senderSessionId` from the authorized Agent and frames the model-visible content as `Agent <sender-id> sent a message:`, so attribution cannot diverge from authority.
Every direction uses one durable source. The service derives `senderSessionId` from the exact live sender and frames content as `Agent <sender-id> sent a message:`.
```ts
import type { SessionId } from '@deepseek-ai/dsh-session'
@@ -46,15 +45,15 @@ interface SendMessageInput {
}
```
Parents and children inherit the same definition in the same registry order. The standard definition carries a process-stable internal identity that a scoped same-name tool does not satisfy. An independently selected preset may omit the tool, a historical descriptor filter may remove it on resume, and a scoped replacement may provide different semantics; neither case receives the standard call instruction. When the standard tool remains visible, the continuation manager appends the JSON-encoded direct parent id and the instruction to send one self-contained result before finishing, plus earlier actionable findings, to the child's initial user task. For a fork child this task follows the inherited completed-turn prefix; no child-only system-prompt section or tool schema precedes that prefix.
Parents and children inherit the same definition in the same registry order. The standard definition carries a process-stable internal identity that a scoped same-name tool does not satisfy. An independently selected preset may omit the tool, a historical descriptor filter may remove it on resume, and a scoped replacement may provide different semantics; none receives the standard call instruction. When the standard tool remains visible, the continuation manager appends the JSON-encoded direct parent id, automatic final-answer delivery conditions, and guidance to send earlier actionable findings to the child's initial user task. For a fork child this task follows the inherited completed-turn prefix; no child-only system-prompt section or tool schema precedes that prefix.
The instruction is guidance, not settlement enforcement. Sending does not end the child's turn, zero or several calls remain mechanically valid, and the runtime never rejects a child for staying silent. The manager-owned `subagent-settled` notice remains unconditional and separately attributed because it records how an Activation ended and preserves terminal output when the child cannot cooperate.
The instruction is guidance, not settlement enforcement. Sending does not end the child's turn, zero or several calls remain mechanically valid, and the runtime never rejects a child for staying silent. The separately attributed `subagent-settled` notice records the Activation's outcome and terminal output only when its direct creator is still online and delivery is `parent`.
Human browser prompts are not model-authored Agent messages. The remote prompt path keeps a private Queue delivery so each human prompt remains a distinct turn. Interrupt behavior and settlement delivery remain independent.
### Complete removal and reintroduction condition
The standalone `@deepseek-ai/dsh-tool-subagent-report` package, `report` schema, `tool:report` prompt section, `reportDelivery` configuration, report-specific message source, catalog entries, composition rows, and supported-behavior snapshots are absent. The unified tool gives up the recipient-free child shortcut and the old ability for a structural return tool to survive an explicit child allow-list. Those capabilities return only if a concrete use case requires semantics that an adjacent `agent_id` and fixed Steer cannot express; reintroducing them requires a distinct model operation and prefix-cost evidence, not an alias over `sendMessage()`.
The standalone `@deepseek-ai/dsh-tool-subagent-report` package, `report` schema, `tool:report` prompt section, `reportDelivery` configuration, report-specific message source, catalog entries, composition rows, and supported-behavior snapshots are absent. The unified tool gives up the recipient-free child shortcut and the old ability for a structural return tool to survive an explicit child allow-list. Those capabilities return only if a concrete use case requires semantics that a tree member’s `agent_id` and fixed Steer cannot express; reintroducing them requires a distinct model operation and prefix-cost evidence, not an alias over `sendMessage()`.
## Alternatives considered
@@ -64,16 +63,16 @@ The standalone `@deepseek-ai/dsh-tool-subagent-report` package, `report` schema,
**Make `report` global.** Roots, one-shot children, remote children, and agentless callers cannot derive a report recipient. Advertising it globally would make schema visibility disagree with authority, while `send_message` already makes the recipient explicit.
**Turn every child final message into an implicit send.** A long-lived child may have nothing useful to send in one turn and several findings in another. Automatic delivery would merge model-authored communication with the runtime's settlement account and could not replace the unconditional notice on errors, cancellation, or token exhaustion.
**Treat the final answer as a model-authored send.** A settlement notice must preserve terminal output and abnormal endings even when the child cannot cooperate. It keeps runtime attribution distinct from findings the child explicitly chose to send.
**Rely only on the tool description.** A tool description helps after the model considers that tool; the failure mode is a child that believes it is finished without considering any return call. Initial-task guidance reaches that decision without changing the inherited system or tool prefix.
**Rely only on the tool description.** A tool description helps after the model considers that tool; initial-task guidance also explains which earlier findings should reach the parent and what the final answer must contain, without changing the inherited system or tool prefix.
**Keep quiet delivery as deployment policy.** A quiet model-authored message can be accepted while an idle target never reads it. Fixed Steer gives both directions one delivery meaning and preserves accepted order with later settlement notices.
## Consequences
- Model consumers expose one `send_message({ agent_id, message })` definition to parents and children, with no model-selected Queue versus Steer parameter.
- The continuation manager remains the sole owner of adjacency authorization, residency, cold resume, waking admission, and teardown races.
- The continuation manager remains the sole owner of tree authorization, residency, cold resume, waking admission, and teardown races.
- Accepted messages may extend a running target's current turn; messages waiting together share next-step FIFO ordering.
- Caller cancellation owns work only until inbox acceptance and does not retract an accepted message or dispose the target.
- The initial task carries JSON-encoded dynamic parent addressing after a fork prefix, while the request-head system prompt and tool ordering remain reusable.
@@ -4,26 +4,25 @@ Status: implemented
[English](2026-08-27-adjacent-agent-steer-messaging.md) | 中文
邻接消息、发现范围和父子生命周期限制已由[整树通信](2026-10-16-tree-wide-agent-communication.zh.md)替代;其余决策仍然有效。 下文的工具身份标记和初始任务通信指导由[工具拥有通信指导](2026-10-22-subagent-tool-owned-communication.zh.md)取代。
## 问题
可继续 Agent 最初使用方向专属的模型控制。parent 调用 `send_message({ subagent_id, message })`,委托给 FIFO `followup` 服务操作。child 则获得 child 作用域的 `report({ output })` 工具、`tool:report` 系统提示词 section,以及由部署选择的静默或唤醒投递。两个工具用不同 schema、服务路径、来源与调度描述同一个相邻 Agent 操作。
可继续 child 拥有自己的 Session,因此 parent 不会自动收到 child 的 transcript(文本记录)、工具输出或推理。返回路径必须保持显式且可重复:child 可以在结束前发送进度、发送后仍保持可用,也可能在来得及配合前失败。把每条最终 assistant 消息变成隐式结果会混淆轮次完成与模型选择的通信,而且无法覆盖异常结束。
可继续 child 拥有自己的 Session,因此 parent 不会自动收到 child 的 transcript(文本记录)、工具输出或推理。显式消息让 child 在结束前发送可操作的发现。直接创建者仍在线时,runtime 另行通过结算通知投递终态输出。
child 专属工具与系统提示词 section 还位于每个继承 fork 轮次之前。它们让可继续 fork child 的请求头在 fork 旨在复用的历史之前就与 parent 不同,迫使提供方重新预填充整份复制 transcript。
## 决策
`SubagentRuntime.sendMessage(sender, targetId, content, { signal })` 是唯一公开的模型编写消息操作。继续执行管理器只接受确切在线 sender 与一条相邻边上的目标:
`SubagentRuntime.sendMessage(sender, targetId, content, { signal })` 是唯一公开的模型编写消息操作。继续执行管理器只接受确切在线 sender 与同一根树中的目标;[整树通信](2026-10-16-tree-wide-agent-communication.zh.md)负责授权与发现规则。
- parent 到直接可继续 child,由 child 的持久化 `SessionHeader.parentSession` 授权;
- 驻留的可继续 child 到其确切在线直接 parent,由 child 的 Activation 授权。
自身目标、过期 Agent 对象、未知目标和外部执行不是替代路由。操作不接受调用方提供的 source、投递模式、离线根邮箱或提供方分发。
sibling、自身目标、超过一条边的 ancestor、陈旧 Agent 对象、未知目标与一次性 child 都不是替代路由。该操作没有调用方提供的 source、投递模式、离线 parent mailbox 或提供方分发。
每条被接受的消息都使用 `Agent.steer()`。运行中目标在最近 step 边界接收消息;空闲目标启动轮次。不在线目标先在根下冷恢复,再接受同一 Steer 投递;其创建祖先可以保持离线。管理器保留唤醒发送记账,因此受继续执行管理的目标不会在同步 inbox 插入与 driver 准入之间结算。
每条被接受的消息都使用 `Agent.steer()`。运行中目标在最近 step 边界接收消息;空闲目标启动轮次。缺失的直接 child 会先通过现有继续执行生命周期冷恢复,再接受同一 Steer 投递。管理器保留唤醒发送记账,因此受继续执行管理的目标不会在同步 inbox 插入与 driver 准入之间结算。
两个方向使用同一种持久来源。服务从已授权 Agent 推导 `senderSessionId`,并把模型可见内容组装为 `Agent <sender-id> sent a message:`,因此来源信息不会偏离权限。
所有方向使用同一种持久 source。服务从确切在线发送方推导 `senderSessionId`,并以 `Agent <sender-id> sent a message:` 包装内容。
```ts
import type { SessionId } from '@deepseek-ai/dsh-session'
@@ -46,15 +45,15 @@ interface SendMessageInput {
}
```
parent 与 child 以相同注册表顺序继承相同定义。标准定义携带进程稳定的内部身份,同名的作用域工具不满足该身份。独立选择的 preset 可以省略该工具,历史 descriptor 过滤器可在恢复时移除该工具,作用域替代工具也可以提供不同语义;两种情况都不会收到标准调用指令。当标准工具仍可见时,继续执行管理器会把经过 JSON 编码的直接 parent id、结束前发送一份自包含结果的指令,以及更早发送可操作发现的指令追加到 child 初始用户任务。对 fork child 而言,该任务位于继承的已完成轮次前缀之后;没有 child 专属系统提示词 section 或工具 schema 位于此前缀之前。
parent 与 child 以相同注册表顺序继承相同定义。标准定义携带进程稳定的内部身份,同名的作用域工具不满足该身份。独立选择的 preset 可以省略该工具,历史 descriptor 过滤器可在恢复时移除该工具,作用域替代工具也可以提供不同语义;这些情况都不会收到标准调用指令。当标准工具仍可见时,继续执行管理器会把经过 JSON 编码的直接 parent id、最终回答的自动投递条件,以及提前发送可操作发现的指导追加到 child 初始用户任务。对 fork child 而言,该任务位于继承的已完成轮次前缀之后;没有 child 专属系统提示词 section 或工具 schema 位于此前缀之前。
该指令是指导,不是结算强制。发送不会结束 child 轮次,机制仍允许零次或多次调用,runtime 绝不会因 child 保持沉默而拒绝它。由管理器负责的 `subagent-settled` 通知仍无条件发送并采用独立来源,因为它记录 Activation 如何结束,并在 child 无法配合时保留终态输出。
该指令是指导,不是结算强制。发送不会结束 child 轮次,机制仍允许零次或多次调用,runtime 绝不会因 child 保持沉默而拒绝它。采用独立来源的 `subagent-settled` 通知仅在直接创建者仍在线且投递方式为 `parent` 时,记录 Activation 的结果与终态输出。
浏览器中的人类提示不是模型编写的 Agent 消息。远程提示路径保留私有 Queue 投递,使每条人类提示保持为独立轮次。中断行为与结算投递保持独立。
### 完整移除与重新引入条件
独立的 `@deepseek-ai/dsh-tool-subagent-report` 包、`report` schema、`tool:report` 提示词 section、`reportDelivery` 配置、report 专属消息来源、目录项、组合行和受支持行为快照均已不存在。统一工具放弃了无需接收方的 child 快捷方式,也放弃了让结构性返回工具绕过显式 child allow-list 的旧能力。只有具体用例需要相邻 `agent_id` 与固定 Steer 无法表达的语义时,这些能力才会重新出现;重新引入需要独立的模型操作与前缀成本证据,而非 `sendMessage()` 之上的别名。
独立的 `@deepseek-ai/dsh-tool-subagent-report` 包、`report` schema、`tool:report` 提示词 section、`reportDelivery` 配置、report 专属消息来源、目录项、组合行和受支持行为快照均已不存在。统一工具放弃了无需接收方的 child 快捷方式,也放弃了让结构性返回工具绕过显式 child allow-list 的旧能力。只有具体用例需要树成员的 `agent_id` 与固定 Steer 无法表达的语义时,这些能力才会重新出现;重新引入需要独立的模型操作与前缀成本证据,而非 `sendMessage()` 之上的别名。
## 考虑过的替代方案
@@ -64,16 +63,16 @@ parent 与 child 以相同注册表顺序继承相同定义。标准定义携带
**让 `report` 全局可见。** 根 Agent、一次性 child、远程 child 与无 Agent 调用方无法推导 report 接收方。全局宣传它会让 schema 可见性与权限不一致,而 `send_message` 已显式给出接收方。
**把每条 child 最终消息变成隐式发送。** 长期运行的 child 可能在某个轮次没有值得发送的内容,在另一个轮次却有多条发现。自动投递会混合模型编写通信与 runtime 结算说明,而且无法替代错误、取消或 token 耗尽时的无条件通知。
**将最终回答当作模型编写的发送。** 即使 child 无法配合,结算通知仍须保留终态输出与异常结束信息。它将 runtime 来源与 child 显式选择发送的发现区分开。
**只依赖工具描述。** 工具描述会在模型考虑该工具后提供帮助;失败模式是 child 认为自己已经完成而根本没有考虑返回调用。初始任务指导能触及该决策,又不会改变继承的系统或工具前缀。
**只依赖工具描述。** 工具描述会在模型考虑该工具后提供帮助;初始任务指导还说明哪些早期发现应发送给 parent,以及最终回答须包含什么,又不会改变继承的系统或工具前缀。
**保留静默投递作为部署策略。** 静默的模型编写消息可能被接受,但空闲目标永远不会读取它。固定 Steer 为两个方向提供一种投递含义,并保持与后续结算通知的接受顺序。
## 后果
- 模型 Consumer 向 parent 与 child 公开一个 `send_message({ agent_id, message })` 定义,不提供模型选择的 Queue 与 Steer 参数。
- 继续执行管理器仍是相邻关系授权、驻留、冷恢复、唤醒准入与拆卸竞态的唯一所有者。
- 继续执行管理器仍是整树授权、驻留、冷恢复、唤醒准入与拆卸竞态的唯一所有者。
- 被接受的消息可以延长运行中目标的当前轮次;一起等待的消息共享 next-step FIFO 顺序。
- 调用方取消只在 inbox 接受前掌管工作,不会撤回已接受消息或 dispose(资源释放)目标。
- 初始任务在 fork 前缀之后携带经过 JSON 编码的动态 parent 地址,而请求头系统提示词与工具顺序保持可复用。
@@ -9,14 +9,14 @@
en: 98b49b900b8d2f84
zh: 557faac2976592be
/agent-note-explicit-agent-identity-at-runtime-boundaries/decision:
en: 1c628cf9c743c639
zh: d969b2475fe4a155
en: 4a3ab5108f37520f
zh: d13fcd448101a95d
/agent-note-explicit-agent-identity-at-runtime-boundaries/verification:
en: 85130faaa171036b
zh: 06e5e9fab32dbfb4
en: ce1733edf03f0387
zh: 8e1ca2b69e81968e
/agent-note-explicit-agent-identity-at-runtime-boundaries/alternatives-considered:
en: 2c41456509d54d21
zh: 5704ee9a2274e300
en: 34f5f8571b21897c
zh: cf16094bc0027ca1
/agent-note-explicit-agent-identity-at-runtime-boundaries/consequences:
en: 08ab86b89fc3ec6f
zh: 3866e6557858543a
en: 0b5914067f14282e
zh: c41d21aeb28e357f
@@ -14,34 +14,34 @@ Without an explicit owner, `SubagentManager` creates and resumes children throug
## Decision
Runtime interfaces carry Agent identity at the point that owns it. `AgentSetup` receives `(agentCtx, agent)`; Agent creation and resume options carry `parentAgent` for a runtime child; scoped events carry their Agent in the payload; Remote forwarding verifies that `request.agent` is the carrier key; and Host Typert Context resolution maps wire identity to a live Agent Context without a reverse scan. `agent.ctx` remains the registration and lifecycle owner and exposes no reverse Agent property.
Runtime interfaces carry Agent identity at the point that owns it. `AgentSetup` receives `(agentCtx, agent)`; each subagent Activation retains its exact runtime root; scoped events carry their Agent in the payload; Remote forwarding verifies that `request.agent` is the carrier key; and Host Typert Context resolution maps wire identity to a live Agent Context without a reverse scan. `agent.ctx` remains the registration and lifecycle owner and exposes no reverse Agent property.
Scope-aware registries continue to use the opaque scope key only for registration membership. Tool-subagent does not classify that key or resolve an Agent from Context. A direct `AgentSetup` passes the unpublished Session explicitly and installs through the supplied Context before publication. For a settings-backed standing preset, the event payload supplies the Agent, its Session supplies the policy target, and its Context owns the registrations.
Scope-aware registries continue to use the opaque scope key only for registration membership. Tool-subagent does not classify that key or resolve an Agent from Context. A direct `AgentSetup` receives the Agent explicitly and installs through the supplied Context before Agent publication. For a settings-backed standing preset, the event payload supplies the Agent, its Session supplies the policy target, and its Context owns the registrations.
`SubagentManager` puts the exact parent in both fresh-creation and cold-resume options. A live continuable child is therefore excluded from `AgentRegistry.roots()` and satisfies `isOwnedBy(child.id, parent)`. Durable `parentSession` metadata does not substitute for this relation: a fork or resumed Session may be a runtime root when no live Agent owns it.
`SubagentManager.rootOf(agent)` reads runtime membership from existing Activations; both the child and root must match their live registry objects. Ordinary Sessions resolve to themselves, and unmanaged subagent headers grant no membership. Durable `parentSession` describes creation edges; an Activation retains the exact root while intermediate creators are offline, and same-id replacements cannot inherit it. Root-only consumers classify the explicit subagent origin.
The [scope reference](../../../../packages/core/scope/README.md) and [Agent loop reference](../../../../packages/core/agent-loop/README.md) document registration and lifecycle behavior. The [initiator-scope decision](2026-07-15-agent-initiator-scope.md) owns private-chain rationale; explicit runtime ownership does not change causal initiation or registration lifetime.
## Verification
Agent creation tests pin explicit root and child ownership. Continuation integration tests keep a real child live long enough to assert both `roots()` exclusion and `isOwnedBy()` membership. Existing Schedule tests verify that root-only registrations stay absent from an explicitly owned child.
Managed-child tests verify intermediate-creator disposal and exact-instance checks after child or root replacement. SDK approval tests verify routing during a turn after the direct creator exits. Schedule and user-question tests reject delegated Sessions while ordinary historical forks remain independent.
Remote-event tests reject a missing or mismatched Agent before forwarding a scoped waterfall. Tool-subagent tests verify that direct setup installs before Session publication; standing-preset tests verify per-Session policy sampling and inheritance.
Remote-event tests reject a missing or mismatched Agent before forwarding a scoped waterfall. Tool-subagent tests verify that direct setup installs before Agent publication; standing-preset tests verify per-Session policy sampling and inheritance.
## Alternatives considered
**Keep `Context.agent`.** A reverse accessor makes registration ownership look like operation identity and requires every Context derivation, adapter, and test double to preserve an association unrelated to Cordis service selection or effect cleanup.
**Infer runtime ownership from the caller Context.** A private manager Context, an Agent Context, and a standing preset Context can all call the same factory. Context ancestry therefore does not state which live Agent owns the result; the creator must put the parent it already knows in the request options.
**Infer runtime ownership from the caller Context.** A private manager Context, an Agent Context, and a standing preset Context can all call the same factory. Context ancestry therefore does not state which live Agent owns the result; the subagent Activation retains the exact root supplied at admission.
**Classify Agent scope keys.** An opaque scope key states routing membership, not domain identity. Classifying it would make Agent the center of composition and would still couple a plugin's effect owner to the Session whose policy it needs.
**Use the initiating Agent as creation ownership.** Initiator scope records causal asynchronous execution, not lifetime ownership. A parent may initiate work that intentionally creates a root, and setup remains outside the child's driver boundary.
**Use durable Session lineage.** `parentSession` records conversation ancestry across process lifetimes. Runtime ownership controls live roots and teardown, so equating the two would prevent a legitimately resumed fork from becoming a top-level Agent.
**Use durable Session lineage.** `parentSession` records creation ancestry across process lifetimes. An intermediate creator can be offline while its descendant remains managed by a live root; durable ids also cannot distinguish replaced Agent instances. Historical ordinary forks may retain parent metadata without being delegated.
## Consequences
Lifecycle options, events, service requests, and transport requests carry explicit Agent identities, so each operation states the identity it uses and TypeScript checks both sides. Context remains reusable for dependency access and effect ownership without becoming an alternate domain-object locator.
Continuable children have the same runtime parent relation as one-shot in-process children. Root-only consumers exclude them, parent teardown can reason from one live ownership graph, and durable lineage remains free to describe history rather than process-local lifetime.
Continuable children have one runtime root independent of creator residency. The subagent manager owns tree teardown and local membership; the core factory and registry retain only instance creation, publication, and disposal. Third-party callers create managed children through the subagent service rather than assigning ownership to arbitrary factory-created Agents.
@@ -14,34 +14,34 @@ Agent 的 Cordis Context 拥有注册及其清理。Agent 身份则为某项操
## 决策
运行时接口在拥有身份的位置携带 Agent 身份。`AgentSetup` 接收 `(agentCtx, agent)`;创建与恢复 Agent 的 options 通过 `parentAgent` 标识运行时子级;作用域事件在 payload 中携带 Agent;Remote 转发校验 `request.agent` 就是 carrier key;Host Typert Context 解析则把协议身份映射到存活 Agent Context,不执行反向扫描。`agent.ctx` 继续拥有注册和生命周期,不暴露反向 Agent 属性。
运行时接口在拥有身份的位置携带 Agent 身份。`AgentSetup` 接收 `(agentCtx, agent)`;每个 subagent Activation 保留其确切运行时树根;作用域事件在 payload 中携带 Agent;Remote 转发校验 `request.agent` 就是 carrier key;Host Typert Context 解析则把协议身份映射到存活 Agent Context,不执行反向扫描。`agent.ctx` 继续拥有注册和生命周期,不暴露反向 Agent 属性。
感知作用域的注册表继续仅使用不透明作用域键判断注册成员关系。tool-subagent 不会分类该键,也不会从 Context 解析 Agent。直接 `AgentSetup` 显式传入尚未发布的 Session,并在发布前通过所给 Context 完成安装。对于由设置控制的常驻 preset,事件 payload 提供 Agent,其 Session 提供策略目标,其 Context 拥有注册项。
感知作用域的注册表继续仅使用不透明作用域键判断注册成员关系。tool-subagent 不会分类该键,也不会从 Context 解析 Agent。直接 `AgentSetup` 显式接收 Agent,并在 Agent 发布前通过所给 Context 完成安装。对于由设置控制的常驻 preset,事件 payload 提供 Agent,其 Session 提供策略目标,其 Context 拥有注册项。
`SubagentManager` 会把确切父级放进全新创建与冷恢复的 options。因此,存活的可续跑子级不会出现在 `AgentRegistry.roots()` 中,并且满足 `isOwnedBy(child.id, parent)`。持久化 `parentSession` 元数据不能代替这项关系:没有存活 Agent 拥有 fork 或已恢复会话时,它仍可成为 runtime root。
`SubagentManager.rootOf(agent)` 从已有 Activation 读取运行时成员关系;子级和树根都必须匹配注册表中的存活对象。普通 Session 返回自身,非托管的 subagent header 不授予成员身份。持久化 `parentSession` 描述创建关系;中间创建者离线时,Activation 保留确切树根,同 id 替代实例不能继承它。仅限根级的消费方依据显式 subagent 来源分类。
[作用域参考](../../../../packages/core/scope/README.zh.md)与 [Agent loop 参考](../../../../packages/core/agent-loop/README.zh.md)说明注册和生命周期行为。[发起方作用域决策](2026-07-15-agent-initiator-scope.zh.md)负责私有调用链理由;显式运行时归属不改变因果发起关系或注册生命周期。
## 验证
Agent 创建测试锁定显式的根级与子级归属。continuation 集成测试让一个真实子级保持存活,直到断言其既不属于 `roots()`、又满足 `isOwnedBy()`。现有 Schedule 测试验证仅限根级的注册项不会出现在显式归属的子级中。
托管子级测试验证中间创建者释放,以及子级或树根替换后的确切实例检查。SDK 审批测试验证直接创建者退出后的轮次内路由。Schedule 与用户问题测试拒绝委派 Session,历史普通 fork 则保持独立。
Remote 事件测试会在转发作用域 waterfall 前拒绝缺失或不匹配的 Agent。tool-subagent 测试验证 direct setup 会在 Session 发布前完成安装;常驻 preset 测试验证逐 Session 的策略读取与继承。
Remote 事件测试会在转发作用域 waterfall 前拒绝缺失或不匹配的 Agent。tool-subagent 测试验证 direct setup 会在 Agent 发布前完成安装;常驻 preset 测试验证逐 Session 的策略读取与继承。
## 考虑过的替代方案
**保留 `Context.agent`。** 反向 accessor 会让注册所有权看起来等同于操作身份,还要求每个 Context 派生、适配器和测试替身保留一项与 Cordis 服务选择或 effect 清理无关的关联。
**从调用方 Context 推断运行时归属。** 私有管理器 Context、Agent Context 和常驻 preset Context 都能调用同一个工厂。因此,Context 祖先关系无法说明由哪个存活 Agent 拥有结果;创建方必须把它已知的父级放进请求 options。
**从调用方 Context 推断运行时归属。** 私有管理器 Context、Agent Context 和常驻 preset Context 都能调用同一个工厂。因此,Context 祖先关系无法说明由哪个存活 Agent 拥有结果;subagent Activation 保留准入时传入的确切树根。
**分类 Agent 作用域键。** 不透明作用域键表达路由成员关系,而不是领域身份。分类该键会让 Agent 成为组合中心,也仍会把插件的 effect 所有者与策略所需的 Session 耦合起来。
**使用发起 Agent 作为创建归属。** 发起方作用域记录异步执行的因果关系,而非生命周期归属。父级可能发起有意创建根级 Agent 的工作,而 setup 仍位于子级驱动边界之外。
**使用持久化会话谱系。** `parentSession` 跨进程生命周期记录对话祖先关系。运行时归属控制存活根级和 teardown,因此把二者等同会阻止合法恢复的 fork 成为顶层 Agent。
**使用持久化会话谱系。** `parentSession` 跨进程生命周期记录创建祖先关系。中间创建者可以离线,而后代仍由存活树根管理;持久 id 也无法区分替换后的 Agent 实例。历史普通 fork 可能保留父级元数据,但并不属于委派。
## 后果
生命周期 options、事件、服务请求和传输请求会携带显式 Agent 身份,因此每项操作都会声明自身使用的身份,TypeScript 也会检查两侧。Context 可以继续复用于依赖访问与 effect 所有权,而不会成为另一种领域对象定位器。
可续跑子级与一次性进程内子级使用同一种运行时父级关系。仅限根级的消费方会排除这些子级,父级 teardown 可以依据唯一的存活归属图推理,而持久化谱系仍可描述历史,不必承担进程内生命周期语义。
可续跑子级拥有独立于创建者驻留状态的唯一运行时树根。subagent 管理器拥有整树 teardown 和本地成员关系;核心工厂与注册表仅保留实例创建、发布和释放。第三方调用方通过 subagent 服务创建托管子级,而非给任意工厂创建的 Agent 指定归属。
@@ -9,8 +9,8 @@
en: 4548e7fbdbee5b5a
zh: 0078762ea92fa180
/agent-note-consumer-owned-startup-strictness/decision:
en: 1f11e04162e19ad1
zh: d102e300eba9a2ea
en: 62e93742af5bed66
zh: 3944d685b6503a2c
/agent-note-consumer-owned-startup-strictness/alternatives-considered:
en: 1b67e062213184cd
zh: 91cc8a015de2bfe4
@@ -18,5 +18,5 @@
en: 283dec1b336c540d
zh: 2ed358079072cfff
/agent-note-consumer-owned-startup-strictness/testing:
en: c38221069ce98fd3
zh: 9a2f91f539eba4d4
en: cee14c22297e5471
zh: a68748cc36f4a574
@@ -20,6 +20,8 @@ The audit runs only during initial application boot. Later config HMR remains be
This policy governs [Web host boot](../../../../packages/boot/app-boot/README.md), including its [client plugin roster](2026-07-23-client-plugin-loading-model.md). [Per-session presets](2026-09-18-declarative-agent-presets.md) own a separate strict subtree audit.
The browser boot audit applies the same policy to the client plugin tree with its own global list in [`boot-client.ts`](../../../../packages/client/web/src/boot-client.ts): the module system, Session control, the renderer, the application frame, the Session view, and its sidebar. Browser entries are named by package, so the list holds package names; a name absent from the boot graph has no effect, and neither list is derived from the other. The list is the minimum fatal set: an entry that a listed entry waits on blocks startup too. Feature plugins, including credential masking, stay optional. A failed entry reports its activation error. Optional failures appear in one console warning, and in Settings → Plugins → Plugin list only after a later graph synchronization reconciles them. Renaming a listed package requires updating the list and its tests.
## Alternatives considered
- **Add transactional and best-effort modes to vendored Loader.** Rejected because strictness belongs to the application or resource owner, while a Loader group contains unrelated plugins. A mode would also expand the vendor patch and leave callers to select a policy at every group.
@@ -36,6 +38,8 @@ The compact terminal report keeps the failing plugins visible; a separate file r
App-boot unit tests cover absent and disabled required ids, optional import failure, config evaluation failure, synchronous and asynchronous `apply()` failure, pending dependencies, and required failure teardown. Unit expectations pin diagnostic grouping, preservation of original error objects and import logs, exporter cleanup, complete diagnostic values, private file creation, concurrent report names, and failed-write fallback. The built Web-profile acceptance asserts a single port-conflict stack without Node wrapper output, verifies the saved diagnostic file and its stderr fallback, serves the full UI with optional failures and exits nonzero without readiness when the required HTTP port is occupied or `modules` or `connection` cannot activate.
Browser audit unit tests (`boot-client.client.spec.ts`, `boot.client.spec.ts`) cover required and optional import, creation, activation, and pending failures, carrier presentation, and required names absent from the boot graph. Test carriers are stricter than the product: `TestClient` requires every roster entry to activate unless a case passes `allowInactiveEntries`, and the browser e2e lanes treat a `web boot: warning` as a failure with no opt-out.
The [Web process matrix](../../../../apps/cli/tests/profiles/web/tests/web-failure-matrix.expected.e2e.ts) independently exercises optional and required failures at startup and after native patch-file edits. Authenticated HTTP requests and plugin lifecycle files distinguish a usable application from a surviving process. These keyless process checks complement the [controlled-delivery unit tests](../../../../packages/boot/app-boot/tests/user-patches.spec.ts): unit tests isolate reconciliation failures, while the process tests also require the shipped launcher, native watcher, and bounded shutdown to work together.
The matrix enables Chokidar's `awaitWriteFinish` to acknowledge stable file contents before each reload; otherwise its short change-event suppression window can discard the next test edit. Native events remain required, and assertions wait for observed activation or failure rather than a fixed settling sleep. This is explicit test configuration, not evidence for the default watcher timing.
@@ -20,6 +20,8 @@ Required id 为 `agent-loop`、`webserver`、`modules`、`connection`、`headles
该策略适用于 [Web host 启动](../../../../packages/boot/app-boot/README.zh.md),包括其 [client 插件名册](2026-07-23-client-plugin-loading-model.zh.md)。[按会话的 preset](2026-09-18-declarative-agent-presets.zh.md)持有独立的严格子树审计。
浏览器启动审计以同一策略审计客户端插件树,使用 [`boot-client.ts`](../../../../packages/client/web/src/boot-client.ts) 中独立的全局名单:模块系统、Session 控制、渲染器、应用框架、Session 视图及其侧栏。浏览器 entry 以包名命名,因此名单写包名;启动图中不存在的名字不产生影响,两份名单互不推导。名单是最小致命集合:已列出 entry 所等待的 entry 同样会阻止启动。功能插件(包括凭据遮挡)保持可选。失败 entry 报告其激活错误。可选失败出现在一条控制台警告中;只有后续图同步调和之后,才会出现在「设置 → 插件 → 插件列表」。重命名名单中的包需要同步更新名单及其测试。
## 考虑过的替代方案
- **给 vendored Loader 增加 transactional 与 best-effort mode。** 拒绝,因为严格语义属于应用或资源 owner,而一个 Loader group 包含互不相关的 plugin。Mode 还会扩大 vendor patch,并要求 caller 为每个 group 选择 policy。
@@ -36,6 +38,8 @@ Required id 为 `agent-loop`、`webserver`、`modules`、`connection`、`headles
App-boot 单元测试覆盖缺失和禁用的 required id、optional import failure、config evaluation failure、同步和异步 `apply()` failure、pending dependency,以及 required failure teardown。单元预期输出固定诊断分组、原始错误对象与导入日志的保留、exporter 清理、完整诊断值、私有文件创建、并发报告命名以及写入失败回退行为。构建后的 Web-profile acceptance 断言端口冲突堆栈只输出一次且不包含 Node 包装输出,验证已保存的诊断文件及其 stderr 回退,并会在 optional failure 存在时继续提供完整 UI,并在 required HTTP port 被占用或 `modules`、`connection` 无法激活时以非零码退出,且不报告就绪。
浏览器审计单元测试(`boot-client.client.spec.ts`、`boot.client.spec.ts`)覆盖必需与可选 entry 的导入、创建、激活与 pending 失败、carrier 呈现,以及启动图中不存在的必需名称。测试载体比产品更严格:除非用例传入 `allowInactiveEntries`,`TestClient` 要求每个 roster entry 都激活;浏览器 e2e 把 `web boot: warning` 视为失败,且不提供放宽。
[Web 进程矩阵](../../../../apps/cli/tests/profiles/web/tests/web-failure-matrix.expected.e2e.ts)分别验证启动时和原生补丁文件修改后的 optional 与 required 失败。经过认证的 HTTP 请求和插件生命周期文件区分可用应用与仅存活的进程。这些无需密钥的进程检查与[受控事件投递单元测试](../../../../packages/boot/app-boot/tests/user-patches.spec.ts)互补:单元测试隔离配置协调失败,进程测试还要求随附启动器、原生监听器和有界关闭流程协同工作。
矩阵启用 Chokidar 的 `awaitWriteFinish`,在每次重载前确认文件内容已稳定;否则它的短暂 change 事件抑制窗口可能丢弃下一次测试编辑。测试仍然依赖原生事件,并等待观察到激活或失败,而不是固定时长的休眠。这是显式测试配置,不能证明默认监听器的时序行为。
@@ -3,17 +3,17 @@
# After editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-16-host-schedule-storage.md
/agent-note-host-owned-scheduled-messages:
en: 11854b220af809ea
zh: 0f0575de37107ed8
en: b3b0dde47f58ca48
zh: d4ba3a6cf1228578
/agent-note-host-owned-scheduled-messages/problem:
en: 2c774d05ae75784f
zh: 619ba7f281b13233
/agent-note-host-owned-scheduled-messages/decision:
en: 1210b3975fd26bb3
zh: 3322e6d0aa63fdb0
en: 20dcfc8e9c44cde7
zh: d9609e41eb4e9bd9
/agent-note-host-owned-scheduled-messages/alternatives-considered:
en: 21d7ec9a9ff3cbf2
zh: 93d03b8b3012aa7e
/agent-note-host-owned-scheduled-messages/consequences:
en: 3ae0cd93ed95431e
zh: 25a0a2bc25863651
en: ce4ef02663cd485c
zh: ad8ae567c3bab8e0
@@ -4,36 +4,20 @@ Status: implemented
English | [中文](2026-09-16-host-schedule-storage.zh.md)
This note remains current for Host authority, whole-unit durability, and saved inbox receipts. [Typed rule storage](2026-10-21-schedule-rule-storage.md) and [Session targets](2026-09-17-standalone-schedules.md) replace its original management and target contracts.
## Problem
Scheduled messages must remain discoverable after restart without loading every Session. A timer attached to a live Agent cannot dispatch a task whose Session remains cold. Users also need to inspect pending and delivered tasks without activating their conversation; removing a task after delivery loses that management record.
## Decision
The existing Schedule package owns one Host storage domain containing each task's original Session id, reminder record, `active` or `inactive` status, latest delivery receipt, and saved delivery history. The domain is authoritative for retained tasks. The Host scheduler restores timers only for active tasks; a due task uses the Session controller's existing resolution operation and queues an ordinary follow-up in the original Session. Querying and deleting tasks do not resolve an Agent. Stored records lacking status normalize to `active` without scanning or recreating historical Sessions.
The original `schedule` domain and whole-unit layout remain authoritative; v2 accepts v1. [Typed rule storage](2026-10-21-schedule-rule-storage.md) supersedes the original Session-owned management API, top-level binding writes, and two-state lifecycle; [Session targets](2026-09-17-standalone-schedules.md) owns Host-wide management and delivery targets. The seven normal timing kinds preserve committed targets. An explicit target wins; absent targets read the old Session binding or reserve a stable new-once identity. No startup migration runs.
The task domain declares the whole-unit layout. Tasks are authoritative, so the JSON backend rejects an unreadable file, malformed document, unsupported version, or invalid task as a complete load failure instead of interpreting it as deletion. Failed recovery leaves `schedule.json` unchanged, while an initially absent file opens as an empty domain; repairing the file preserves task identity. `Service.init` waits for both storage validation and runtime setup, management methods wait for the same initialization result, and an unloading owner releases late-acquired storage without starting dispatch. Dispatch admission is separate from initialization: an ancestor can close Agent admission after a task mutation commits but before runtime cleanup starts. That refusal is logged without automatic retry and does not turn the persisted mutation into a failed management result. Invalid storage and failed cleanup registration still reject startup.
This replaces the earlier Session-log authority, live-only activation, and read-only catalog decisions. The [Schedule reference](../../../../packages/schedule/schedule/README.md) owns explicit absolute-time validation, fixed-rate arithmetic, and the distinction between queue admission and model completion. Historical `schedule/change` creation and fold types remain restricted to After, At, and Every through `LegacyScheduleRecord`; a separate Host decoder accepts the current `ScheduleRecord`, including Daily, Weekly, and Cron. The Host record decoder requires a stored `title` that is non-empty after trimming, already trimmed, and at most 120 characters, and the strict task schema rejects the whole domain open instead of dropping such a task. The historical change decoder tolerates an absent `title` so an already-written Session log stays readable, and it applies the same three checks when the member is present. The storage domain remains version 1. Historical events do not populate that domain, and existing `at` tasks are not converted into daily or weekly rules. Loading a Session with active legacy reminders warns that they must be recreated.
A plugin-sourced `followup()` synchronously appends the message to the Session inbox. Successful Session flush acknowledges durable delivery. One task-record put stores `lastDelivery`, appends a delivery record with the occurrence target, acknowledgment time, actual message id, and instruction snapshot, and updates the one-shot's `inactive` status or the recurring task's next target and status. Task state and its saved history therefore share one commit. Inactive tasks remain stored and never schedule another delivery; explicit deletion stops future delivery and removes the row together with its saved records. These records acknowledge inbox delivery, not user receipt or model execution completion.
New tasks initialize an empty `deliveryHistory`. A task without that optional field exposes its existing latest receipt, if any, and marks earlier records unavailable. Reading never rewrites the old task or invents the lost history or instruction snapshot. Its next acknowledged delivery preserves that available receipt and appends the new one. Strict decoding rejects duplicate message ids and disagreement between the latest receipt and the last history entry. History follows append order, not wall-clock sorting, so clock rollback does not reverse acknowledged deliveries.
A recurring task contributes its latest due occurrence once, including after Host restart. Every remains creation-aligned until an interval edit establishes a new anchor; Daily retains a local time and explicit IANA zone, and Weekly adds a non-empty ISO weekday set stored as unique ascending numbers. Every, Daily, Weekly, and Cron use `renderRecurringReminderBatchFraming` and share one follow-up message and its real message id when due for the same Session. The fixed text both renderers emit is the [delivered-reminder origin line](2026-09-24-scheduled-message-framing.md) decision. A recurring task with no representable next target remains inactive with its last receipt. A stopped Host does not send messages.
Daily is a fourth mutually exclusive creation selector, Weekly a fifth, and Cron a sixth, none a conversion of `every_seconds` or `at`. Weekly selects every local date whose ISO weekday belongs to its stored set; the decoder rejects an unnormalized stored set, and the selector rejects an empty, repeated, out-of-range, or non-integer one. Their first targets are strictly future. Missing local times and dates are skipped; overlaps select the earlier instant once per date. The next target is after the delivery decision and on a local date later than the selected occurrence's date. Only UTC targets are restricted to years 0001–9999; local dates may cross into year 0 or 10000 at the edges. After Session restoration, the runtime rechecks due members against the current clock so a rollback does not admit future tasks.
The maintained `@js-temporal/polyfill` 0.5.1 resolves local dates and overlap/gap behavior using the Host's IANA data; Schedule does not own offset-sampling logic or add another package layer. Each daily or weekly record stores normalized `HH:mm:ss.SSS` time, its zone, its weekday set where applicable, and a committed next UTC instant. Decoding and restart do not recompute that instant. Valid zone aliases remain readable across canonical-name changes, while subsequent targets use current Host time-zone data.
Model `schedule_list`, Remote `list`, and the Session header's right-aligned reminder clock show only active tasks. The independent Automation tasks page opens from the sidebar and reads active and inactive Host tasks through the read-only Remote `catalog` method, without activating Sessions or reading their history. That clock is absent while the Session has no active reminder, opens a single task's detail directly when exactly one remains, and otherwise lists them so one can be chosen; both paths open one right-Sidebar task tab that renders the same detail component the Automation tasks page uses, so neither path selects the main panel. The tab names its task and original Session through its navigation parameters and carries that binding into every mutation, so opening or deleting from it never activates a Session. The page has one All, Enabled, and Inactive status filter row, retained inactive details, latest delivery metadata, and an original-Session control that sits in the detail tab strip beside the task actions. Its header's New action starts a new Session, where the reminder is created by asking the model; the page offers no creation form. Navigation waits for Session and archive metadata, explains unavailable targets, and never restores an archived Session implicitly. Task retention remains independent of original-Session availability. Every, Daily, Weekly, and Cron are recurring rules; the page groups tasks only by status. Daily and Weekly rule labels retain the saved local time, exact rule zone, and stored weekday set rather than substituting the browser zone used for next-target display. Explicit confirmed deletion reuses `delete` with the entry's original Session binding, requiring that the supplied binding match the stored task. This argument check is not independent caller authorization: model tools capture the current Agent's Session, while the global user interface selects the task's original binding. Cross-Session model modification policy remains a product decision, not a consequence of the storage layout. A payload-free change notification invalidates queries, and reconnect queries recover missed notifications. Deletion prevents subsequent dispatch but does not retract a queued message.
The second detail level separates Rules from Delivery records. Opening records requests `history` with the task's original Session binding and an explicit page limit of 1–100. An exclusive message-id cursor reads older receipts without changing position when new deliveries append. Catalogs retain only latest-receipt metadata rather than duplicating full histories in every task-list response. Missing tasks and unknown cursors are distinct failures, not empty successful history. The records view loads lazily, rejects stale responses, and keeps original-Session navigation separate from inspection.
Name, instruction, and timing updates share the dispatch and deletion FIFO. Comparing the complete observed record inside that queue prevents a stale draft from overwriting a saved rule or a target advanced by delivery. This avoids adding a persisted revision counter, at the cost of rejecting drafts after normal delivery changes the target. One task put replaces the rule, name, or instruction, preserving identity, original Session binding, status, latest receipt, and saved history. A request may omit the timing change, the name, or the instruction; an omitted field keeps its stored value, and a name or instruction change alone leaves the committed target untouched. Normalized no-ops leave the committed target and stored bytes unchanged. Changed one-shots retain their identity as At records; changed fixed intervals use a new accepted-save anchor; changed wall-clock rules re-resolve their first future target from the same accepted-save sample, and an equivalent normalized rule stays a no-op. A timing change may select a different recurrence kind, and the new rule computes its target exactly as creation would from that accepted-save sample. Deletion removes the row together with its saved records ([consequences of that reversal](2026-09-22-schedule-hard-delete-and-bounded-history.md)), so the runtime never schedules the task again and `history` no longer resolves for its original binding. Inactive tasks are not reopened.
The page uses the existing Host task domain and scheduler, not a mock engine or a Session projection. The name, the instruction, and the run-time rows edit one local draft for the shown task, and one Save commits that draft as a single update carrying the complete expected record; Cancel restores the stored values, a failed save retains the draft, an unsaved draft survives an authoritative refresh, and a committed save and a failed catalog refresh remain separate outcomes. Pause and execution-history operations remain unsupported. Lifecycle status and latest delivery acknowledgment do not describe model results.
The client presentation derives from existing persisted data instead of widening a persisted format. The `schedule_create` transcript card reads the created task out of the call's model-visible result JSON, which already serializes the canonical task value, rather than declaring `presentationMeta`: a persisted metadata channel would add a durable field for a card that Session-log replay reconstructs from the result alone, and previously recorded Sessions render the card without a format change. The Sidebar Session-row clock mark occupies the root-scoped `sidebar.session.row.leading` seat rather than requesting a per-Session provider, because a Session-scoped seat would bind every listed Session and thereby activate and retain it; the same root-scoped owner share feeds `sidebar.session.row.hover`, and both read the shared Host catalog snapshot `schedule/catalog()` without reading a Session log.
Session inbox persistence and the task put occur in separate persistence systems. Inbox persistence precedes the task put to avoid marking delivery before its message exists. A crash or task-write failure after inbox persistence can repeat a message; storing status, target, and receipt together does not make the two stores atomic. Dispatch failures retain the task and produce a diagnostic; there is no independent retry timer. This feature does not create or replace a Goal.
@@ -56,10 +40,4 @@ Session inbox persistence and the task put occur in separate persistence systems
## Consequences
**Keep the mock's hourly choice out of the stored vocabulary.** The approved mock lists 每小时整点 beside the daily, weekly and one-shot choices. Hourly and fixed-rate intervals are the same elapsed-time rule, so the card maps that menu slot onto the existing interval kind instead of adding a persisted kind. A dedicated hourly kind would need a new durable variant, its decoder, its target resolution, and its tool schema, all to express what an interval already expresses.
Task discovery and timers do not depend on loaded Sessions. The after, absolute-time, and fixed-rate selectors retain their semantics alongside Daily, Weekly, and Cron; reading existing saved tasks does not rewrite them. The Host must have its ordinary Session persistence and recovery services available, and the shipped Web composition mounts the Schedule service for the whole deployment. Explicit deletion stops future delivery and removes the task from `list` and `catalog` without deleting conversation messages; the removed row takes its saved receipts with it. History is pruned to the configured window and record cap when an acknowledgment is appended, loaded into Host memory, and rewritten with the task on each acknowledgment. Pagination limits the returned record count; the plugin configuration (`deliveryHistoryDays`, default 30, and `deliveryHistoryRecords`, default 200) limits storage growth. Earlier overwritten receipts and deliveries whose task write failed cannot be reconstructed. Pause and execution-history operations remain unsupported.
Required coverage includes startup from stored tasks, missing-status normalization, cold Session restoration, latest-only catch-up, inactive-task retention without repeat dispatch, real message receipts, deletion across restart, removal of a deleted task's saved deliveries, queue/persistence failures, and disposal. Wall-clock obligations include gaps and skipped dates, earlier-only overlaps, fractional seconds, UTC-range edges, committed-target and alias preservation, and post-restoration clock rollback. Weekly adds weekday normalization and validation, next-target selection across a gap and an overlap, and stored set preservation. Historical decoders must reject Daily, Weekly, and Cron while Host storage accepts them. Client coverage includes every recurring kind, exact rule time, zone, and weekday set, All/Enabled/Inactive status filters, retained inactive details and original-Session navigation, authoritative refresh after confirmed deletion and notification, empty lists, reconnect, and late responses. Presentation coverage includes the page's New action starting a Session, the transcript card's narrowing of persisted result JSON including an incomplete or replayed result, the header clock's direct open for exactly one task and its count label, the idle-only row mark with the hover-card two-task limit and omission line. History coverage includes legacy byte preservation, append and restart, immutable sent instructions, strict latest-receipt consistency, stable cursor pagination, binding checks, failed persistence, and late page replies during refresh or task changes. Cross-store crash duplication remains an explicit limitation rather than a completion claim.
Name, instruction, and timing coverage includes all supported rule kinds, complete-record conflicts, delivery/save ordering, cancellation, normalized no-ops, unchanged-history persistence, pending controls, draft retention, and collision-free task identity. The [built CLI process test](../../../../apps/cli/tests/profiles/web/tests/schedule-timing.expected.e2e.ts) drives public Remote requests and reloads actual task-file bytes in a fresh Host. Its task and receipt seeds establish retained-data inputs, not real creation, delivery, or recorded-model evidence; those require ordinary Session operations and captured model requests.
Task discovery and timers do not require loaded Sessions. History remains in the same task row and shares its acknowledgment commit; reads never invent missing prompts, timestamps, or earlier deliveries. Historical Session-log After, At, and Every remain frozen and do not populate this domain. The [Schedule reference](../../../../docs/subsystems/schedule.md) owns timing, management, UI projection, and retention contracts. The [built CLI test](../../../../apps/cli/tests/profiles/web/tests/schedule-timing.expected.e2e.ts) checks raw old rows, edit-time normalization, conflicts, and cold restart through public Remote requests; seeded receipts are retained-data evidence rather than delivery evidence.
@@ -4,36 +4,20 @@ Status: implemented
[English](2026-09-16-host-schedule-storage.md) | 中文
本 note 仍拥有宿主权威来源、整单元持久性和已保存收件箱回执。[带类型规则存储](2026-10-21-schedule-rule-storage.zh.md)和 [Session 目标](2026-09-17-standalone-schedules.zh.md)替代原管理与目标契约。
## Problem
定时消息必须能在重启后被发现,无须加载每个 Session。挂在 live Agent 上的 timer 无法投递目标 Session 仍为 cold 的任务。用户还需要在不激活对话的情况下查看待投递和已投递任务;投递后删除任务会丢失这条管理记录。
## Decision
现有 Schedule 包拥有一个 Host storage domain,保存每个任务的原始 Session id、提醒记录、`active` 或 `inactive` 状态,最近一次投递回执,以及已保存的投递历史。该 domain 是保留任务的权威来源。Host scheduler 仅为活动任务恢复 timer;到期任务使用 Session controller 现有的解析操作,在原 Session 中排入普通 follow-up。查询和删除任务不会解析 Agent。已存储但缺少状态的记录规范化为 `active`,不扫描或重建历史 Session。
原有 `schedule` 领域和整单元布局仍是权威来源;v2 接受 v1。[带类型规则存储](2026-10-21-schedule-rule-storage.zh.md)替代原按 Session 所有的管理 API、顶层绑定写入和两状态生命周期;[Session 目标](2026-09-17-standalone-schedules.zh.md)拥有宿主管理范围与执行目标。七种正常时间类型保留已提交发生时点。显式目标优先;缺少目标时读取旧 Session 绑定或预留稳定的首次新建后复用身份。不运行启动迁移。
任务 domain 声明整 unit 布局。任务是权威数据,因此 JSON 后端会把文件不可读、文档损坏、版本不受支持或任务非法作为整批加载失败,而不是解释为删除。恢复失败不会改写 `schedule.json`,初始不存在的文件则作为空 domain 打开;修复该文件后会保留任务身份。`Service.init` 等待存储校验和运行时初始化,管理方法等待同一初始化结果;拥有方卸载时释放迟到的存储资源,而不开始投递。投递调度准入与初始化分开:任务变更提交后、运行时清理开始前,祖先可能关闭 Agent 准入。此类拒绝会记录日志而不自动重试,不会将已持久化的变更变成失败的管理结果。存储非法和清理注册失败仍拒绝启动。
本决策替代先前的 Session 日志权威来源、仅 live 时激活和只读目录决策。[Schedule 参考](../../../../packages/schedule/schedule/README.zh.md)负责显式绝对时间校验、固定速率计算,以及区分排队接纳与模型完成。历史 `schedule/change` 的创建与 fold 类型通过 `LegacyScheduleRecord` 保持为 After、At 和 Every;独立的宿主解码器接受包含 Daily、Weekly 与 Cron 的当前 `ScheduleRecord`。宿主记录解码器要求已存储的 `title` 去除首尾空白后非空、本身已去除首尾空白且不超过 120 个字符,严格的 task schema 会拒绝整个 domain 的打开,而不是丢弃这样的任务。历史变更解码器容忍缺失的 `title`,以便已写入的 Session 日志仍可读取;该成员存在时,同样按这三项检查校验。storage domain 保持版本 1。历史事件不会填充该 domain,已有 `at` 任务也不会转换为每日或每周规则。加载含有活动旧提醒的 Session 时,会警告需要重新创建提醒。
插件来源的 `followup()` 同步将消息追加到 Session 收件箱。Session flush 成功后确认持久投递。一次任务记录 put 存储 `lastDelivery`,追加包含发生目标、确认时间、真实消息 id 和指令快照的发送记录,并更新一次性任务的 `inactive` 状态或重复任务的下一目标和状态。因此,任务状态与其保存的历史共用一次提交。未运行任务仍保留在存储中,且不会再次安排投递;显式删除会阻止后续投递,并把该行连同其已保存记录一并移除。这些记录确认收件箱投递,不表示用户收到消息或模型执行完成。
新任务初始化空的 `deliveryHistory`。缺少该可选字段的任务会展示已有的最近一次回执(如果存在),并标明更早记录不可用。读取不会重写旧任务,也不会虚构已丢失的历史或指令快照。下一次确认投递时,保留已有回执并追加新记录。严格解码拒绝重复消息 id,以及最近一次回执与历史末项不一致的情况。历史按追加顺序排列,不按墙钟排序,因此时钟回拨不会颠倒已确认投递的顺序。
重复任务只投递一次最新到期发生时点,Host 重启后也一样。Every 保持与创建时间对齐,直到修改间隔后建立新的起算时点;Daily 保留本地时间与显式 IANA 时区,Weekly 另存非空 ISO 星期集合(唯一且升序)。Every、Daily、Weekly 与 Cron 均使用 `renderRecurringReminderBatchFraming`,同一 Session 的到期任务共用一条 follow-up 消息及其真实消息 id。两个渲染器输出的固定文本见[投递提醒的来源行](2026-09-24-scheduled-message-framing.zh.md)决策。没有可表示的下一目标时,重复任务保留未运行状态及最近一次回执。停止的 Host 不会发送消息。
Daily 是互斥的第四个创建选择器,Weekly 是第五个,Cron 是第六个,三者都不是对 `every_seconds` 或 `at` 的转换。Weekly 选择本地日期 ISO 星期属于其所存集合的每个日期;解码器拒绝未规范化的已存集合,选择器拒绝空、重复、超出范围或非整数的集合。它们的首个目标严格晚于当前时间。缺失的本地时间或日期被跳过;重叠时间选择较早时点,每个日期一次。下一目标晚于投递判断时刻,且其本地日期晚于所选发生时点的日期。仅 UTC 目标限制在 0001–9999 年;边缘处的本地日期可以进入第 0 年或第 10000 年。Session 恢复后,运行时依据当前时钟重新核对到期成员,避免时钟回拨时接纳未来任务。
受维护的 `@js-temporal/polyfill` 0.5.1 使用宿主 IANA 数据解析本地日期及重叠或缺口;Schedule 不拥有偏移量采样逻辑,也不增加包层级。每日或每周记录保存规范化后的 `HH:mm:ss.SSS` 时间、时区、适用的星期集合及已提交的下一 UTC 时点。解码和重启不会重新计算该时点。规范名称变化后,有效时区别名仍可读取;后续目标使用宿主当前的时区数据。
模型 `schedule_list`、Remote `list` 和 Session 顶部右侧的提醒时钟仅显示活动任务。独立的自动化任务页面从侧边栏打开,通过只读 Remote `catalog` 方法读取活动和未运行 Host 任务,不激活 Session 或读取其历史。该时钟在该 Session 没有活动提醒时不显示;恰好有一个时直接打开该任务详情,否则列出提醒供选择。两条路径都打开同一个右侧边栏任务页签,该页签渲染自动化任务页面所用的同一详情组件,因此都不会切换主面板。页签通过导航参数指明任务及其原 Session,并把该绑定带入每个修改操作,因此从页签打开或删除都不会激活 Session。页面提供一行“全部/已开启/未运行”状态筛选,保留未运行任务详情、最近一次投递元数据和固定在位于详情页签条内、与任务操作并排的原 Session 入口。页头的“新建”操作会打开一个新会话,提醒由模型在该会话中创建;页面不提供创建表单。导航等待 Session 与归档元数据,解释不可用目标,且不会隐式恢复已归档 Session。任务保留与原 Session 可用性相互独立。Every、Daily、Weekly 和 Cron 均为重复规则;页面只按状态分组。Daily 与 Weekly 规则标签保留保存的本地时间、精确规则时区和所存星期集合,不替换为下一目标显示所用的浏览器时区。显式确认删除复用 `delete`,传入条目的原始 Session 绑定,要求传入绑定与已存任务一致。这项参数校验不是独立的调用者鉴权:模型工具捕获当前 Agent 的 Session,全局用户界面则选择任务的原始绑定。跨会话模型修改政策仍是产品决策,不由存储布局直接决定。无载荷的变更通知使查询失效,重新连接时重新查询以恢复漏掉的通知。删除阻止后续投递,但不撤回已排队消息。
第二层详情将“规则”与“发送记录”分开。打开发送记录时,`history` 请求携带任务的原始 Session 绑定,以及 1–100 的显式分页数量。排他的消息 id 游标用于读取更早回执,新投递追加后不会改变其定位。目录仅保留最近一次回执元数据,不在每次任务列表响应中复制完整历史。任务不存在和游标未知属于独立失败,不是查询成功的空历史。记录视图按需加载,拒绝晚到的过期响应,并将原 Session 导航与查看记录分开。
名称、指令和时间更新与投递、删除共用 FIFO。在该队列内比较完整的已观察记录,可防止旧草稿覆盖已保存规则或投递已推进的目标。这样无须增加持久版本计数器,代价是正常投递改变目标后也会拒绝旧草稿。一次任务 put 替换规则、名称或指令,保留身份、原始 Session 绑定、状态、最近一次回执和已保存历史。请求可以省略时间变更、名称或指令;省略的字段保留已存储值,仅修改名称或指令不会改变已提交目标。归一化后等价的修改保留已提交目标与存储字节。修改后的一次性任务以 At 记录保留身份;固定间隔改变后以接纳保存时刻重新起算;钟表规则改变后以同一接纳保存时刻重新解析首个未来目标,而归一化后等价的规则不产生变更。时间变更可以选择不同的重复类型,新规则按创建时相同的方式、以同一接纳保存时刻计算目标。删除会移除该行及其已保存记录([该反转的后果](2026-09-22-schedule-hard-delete-and-bounded-history.zh.md)),因此运行时不会再调度该任务,`history` 也不再按其原始绑定解析。未运行任务不会被重新启用。
页面使用现有 Host 任务 domain 和调度器,不使用 mock 引擎或 Session projection。名称、指令和运行时间行编辑当前任务的同一份本地草稿,一次保存把该草稿作为携带完整预期记录的单个更新提交;取消恢复已存储值,保存失败保留草稿,未保存的草稿会保留到权威刷新之后,保存已提交与目录刷新失败仍是不同结果。暂停和执行历史操作仍不受支持。生命周期状态与最近一次投递确认不描述模型结果。
客户端的呈现从已有持久化数据派生,而不扩展持久化格式。`schedule_create` transcript(文本记录)卡片从该调用对模型可见的结果 JSON 中读出创建的任务——该 JSON 已经序列化规范任务值——而不是声明 `presentationMeta`:持久化 metadata 通道会为一张仅凭结果即可在 Session 日志回放中重建的卡片增加持久字段,而早于该卡片录制的 Session 无需格式变更即可渲染它。侧栏 Session 行的时钟标记占用 root 作用域的 `sidebar.session.row.leading` slot,而不是请求逐 Session provider,因为 Session 作用域的 slot 会绑定列表中每个 Session,从而激活并保留它;同一个 root 作用域属主约定也供给 `sidebar.session.row.hover`,两者都读取共享的 Host 目录快照 `schedule/catalog()`,不读取 Session 日志。
Session 收件箱持久化和任务 put 发生在两个持久化系统中。收件箱持久化先于任务 put,以免消息尚不存在时就标记已投递。收件箱持久化后崩溃或任务写入失败可能重复发送消息;状态、目标和回执一起存储,并不使两个存储具有原子性。投递失败保留任务并输出诊断;没有独立的重试 timer。此功能不创建或替换 Goal。
@@ -56,10 +40,4 @@ Session 收件箱持久化和任务 put 发生在两个持久化系统中。收
## Consequences
**不把 mock 的「每小时整点」引入持久词表。** 设计稿把它与每天、每周、仅一次并列。每小时整点与固定间隔在语义上是同一种按经过时间计量的规则,因此详情里的该项直接映射到既有的间隔类型,而不是新增一个持久种类。若真要新增,需要新的持久变体、解码器、目标解析与工具 schema,而它表达的能力现有间隔已经覆盖。
任务发现和 timer 不依赖已加载的 Session。延迟、绝对时间和固定速率选择器在与 Daily、Weekly、Cron 并存时保留自身语义;读取已有任务不会重写它们。Host 必须提供普通 Session 持久化和恢复服务,而随发行版交付的 Web 组合为整个部署挂载 Schedule 服务。显式删除会阻止后续投递,并将任务从 `list` 和 `catalog` 中移除,不删除对话消息;被移除的行会带走其已保存回执。历史在追加确认时按配置的窗口与条数上限裁剪,随后加载到 Host 内存,并在每次确认时随任务一起重写。分页限制返回记录数量,不限制存储增长或指令字节数。此前被覆盖的回执,以及任务写入失败的投递记录,无法重建。暂停和执行历史操作仍不受支持。
必要覆盖范围包括从已存储任务启动、缺失状态规范化、冷 Session 恢复、仅补发最新发生时点、保留未运行任务且不重复投递、真实消息回执、重启后的删除结果、删除会移除已保存发送记录、排队或持久化失败,以及销毁。钟表规则的验证要求包括缺口和跳过的日期、仅选择较早时点的重叠、小数秒、UTC 范围边缘、已提交目标和别名保留,以及恢复后的时钟回拨。Weekly 另需覆盖星期集合的规范化与校验、跨缺口与重叠的下一目标选择,以及已存集合的保留。历史解码器必须拒绝 Daily、Weekly 与 Cron,而宿主存储接受它们。客户端覆盖范围包括每种重复规则、精确规则时间、时区和星期集合、全部/已开启/未运行状态筛选、保留未运行任务详情和原 Session 导航、确认删除和通知后的权威刷新、空列表、重新连接和晚到响应。呈现覆盖范围包括页面“新建”操作打开新会话、transcript 卡片从不完整或回放的结果 JSON 中收窄任务、页头时钟在恰好一个任务时直接打开并给出数量标签、仅空闲行标记及其悬浮卡片两个任务上限与遗漏行。历史覆盖范围包括旧文件字节保留、追加与重启、不可变的发送指令、最近回执一致性严格校验、稳定游标分页、绑定校验、持久化失败,以及刷新或切换任务期间晚到的分页响应。跨存储崩溃导致的重复投递仍是明确限制,不能被表述为完成保证。
名称、指令和时间编辑覆盖范围包括所有支持的规则类型、完整记录冲突、投递与保存的顺序、取消、规范化后的无变更、历史持久化不变、等待期间的控件、草稿保留,以及无碰撞的任务标识。[已构建 CLI 进程测试](../../../../apps/cli/tests/profiles/web/tests/schedule-timing.expected.e2e.ts) 驱动公开 Remote 请求,并在新 Host 中重新加载实际任务文件字节。任务和回执种子只是已保留数据的输入,不是真实创建、投递或模型录制证据;这些证据需要普通 Session 操作与捕获的模型请求。
任务发现和定时器无需已加载 Session。历史保留在同一任务行中,与确认一同提交;读取不虚构缺失指令、时间戳或更早投递。历史 Session 日志 After、At、Every 保持冻结,不填充此领域。[Schedule 参考](../../../../docs/subsystems/schedule.zh.md)拥有时间、管理、UI 投影和保留契约。[构建后 CLI 测试](../../../../apps/cli/tests/profiles/web/tests/schedule-timing.expected.e2e.ts)通过公开 Remote 请求检查旧原始行、编辑时规范化、冲突和冷重启;种入回执是保留数据证据,不是投递证据。
@@ -0,0 +1,19 @@
# Bilingual-pair consistency record for 2026-09-17-standalone-schedules.md (docs/i18n/README.md): per heading
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
# After editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-17-standalone-schedules.md
/agent-note-standalone-schedules-and-session-targets:
en: ce8ed282ec843da0
zh: a5ca78422ea009ec
/agent-note-standalone-schedules-and-session-targets/problem:
en: 2a6769cd378455bb
zh: 84cfb0f2e1fde8e1
/agent-note-standalone-schedules-and-session-targets/decision:
en: 73ed15d2219f2898
zh: db8ea6ae79a95241
/agent-note-standalone-schedules-and-session-targets/alternatives-considered:
en: 1def5c19511fc59e
zh: 84f0f5d568272c39
/agent-note-standalone-schedules-and-session-targets/consequences:
en: bda079e488b5374b
zh: d5ffe96b33e8e65d
@@ -0,0 +1,35 @@
# Agent Note: Standalone schedules and Session targets
Status: implemented
English | [中文](2026-09-17-standalone-schedules.zh.md)
## Problem
Host tasks need global management and explicit delivery targets while remaining recognizable in their Workspace.
## Decision
The storage domain, importer, placeholder RRULE, and parallel API decisions are superseded by [typed rule storage and shared recurrence](2026-10-21-schedule-rule-storage.md). Keep the existing `schedule` domain and one management API; the target, execution, and notice decisions below remain current.
Targets are `session`, `new_once`, or `new`. `session` stores one existing Session id; create and edit check, without activating that Session, that it exists and does not belong to a delegated child. `new_once` reserves a Session id when the task is created or retargeted, creates that exact Session at first delivery, and then becomes `session`. `new` saves a reserved Session id in `pendingDelivery` before each create attempt, so a retry after a failed delivery uses the same Session. A created Session uses the target's working directory and Agent preset, or the Host defaults. When the target names a working directory, the Session joins the Workspace registered there, if any. While the Session has no title, it takes the task title; a `new` target adds the Session's creation minute in the timing rule's zone, for example `Weekday digest · 2026-10-12 09:00`, so the Sessions of successive deliveries stay distinguishable. A failed join or rename is logged and does not block delivery. `involvedSessionIds` lists the creating and editing Agent Sessions and each existing or reserved target Session in first-involvement order; Sessions that a `new` target creates are not listed.
The runtime keeps latest-only catch-up. It delivers the latest occurrence at or before the delivery decision and advances to the next future occurrence; an exhausted rule stays stored with `ended` status, and a paused task has no timer. Each delivery of one task, timed or manual, comes at least 60 seconds after the occurrence of the previous delivery, and the occurrence of a manual run is its run time; for a `new` target, whose every delivery creates a Session, a deployment can raise that interval through `newSessionIntervalSeconds`, which defaults to 60. The floor counts from the previous occurrence when the next stored occurrence keeps the interval, and from the previous acknowledgment otherwise, so acknowledgment latency does not accumulate and a late delivery does not shift later occurrences; the next on-time occurrence can then follow the late delivery by less than one interval. A task delivered to a reused Session, `session` or `new_once`, holds its execution slot until the target Agent is idle, and occurrences that become due meanwhile coalesce into one later delivery, so that Session has at most one pending delivery of the task; a `new` target holds no slot, and its launches run beside one another with only the delivery interval between them. Due tasks with a `session` target share one message per target Session, except After and At tasks. Manual run shares target resolution, the delivery interval, Session inbox persistence, the receipt, and the involvement update with timed delivery, but keeps the next occurrence and status. Both refuse an archived destination Session, including a reserved retry, because the archived-Session gate would block the turn that the message wakes. Session inbox persistence precedes the task receipt write, so a crash between the two stores can repeat a message. The implementation adds no scheduler, run, or ownership service.
After a committed create, update, or delete, Schedule queues a change notice on its own chain, so Session I/O never holds the mutation and delivery queue. Each involved Session other than the actor whose Agent is loaded receives a `schedule`-sourced message for its next model step that does not start a model turn, so notices queued while the Session is idle reach the model with its next input instead of each running as a model turn; the message names the change and carries the task id and title as JSON strings. A Session without a loaded Agent receives nothing and reads the current task when it next lists tasks. A notice failure is logged and does not roll back the committed change. Shutdown drains queued notices before storage closes.
## Alternatives considered
**Require a caller-supplied schedule id.** A user-facing title and canonical identity serve different purposes, but accepting ids would expose collision and validation policy. The Host generates identity and returns it for later operations.
**Keep creator Session ownership.** Ownership would keep list and mutation APIs scoped and would conflict with global management, retargeting, and Session creation policies. Historical involvement is metadata and notification routing, not authorization.
**Check spacing by sampling upcoming occurrences.** Each occurrence query enumerates from `DTSTART`, so a high-frequency rule with an early start blocked the Host thread before a sampled check could refuse it, and a sample cannot bound the complete sequence. Static rule checks before enumeration and the per-task delivery interval enforce the spacing instead.
**Add scheduler, run, and notification services.** The existing Schedule service, storage domain, runtime, and Session controller already own these responsibilities at the required scale. New layers would add indirection without changing durability.
**Leave created Sessions untitled and outside Workspaces.** Such a Session appears as one more untitled Session outside the Workspace that owns its directory, so the user cannot tell which task created it or, for a `new` target, which delivery it received. Joining the Workspace at the target directory and naming the Session after the task, with the creation minute for `new`, needs no stored field. A failure is only logged because delivery has priority over presentation.
## Consequences
Tasks outlive Agents and are managed by id. Archive admission reports active Session targets; explicit stop deletes tasks. Host availability, cross-store at-least-once delivery, best-effort notices, and unbounded per-delivery concurrency remain limitations. The [Schedule reference](../../../../docs/subsystems/schedule.md) owns the exact current contracts and the [management upgrade guide](../../../../docs/upgrade-guide/v0.2.2-alpha.2/schedule-remote-host-tasks/guide.md) covers changed callers.
@@ -0,0 +1,35 @@
# Agent Note: 独立计划任务与 Session 目标
Status: implemented
[English](2026-09-17-standalone-schedules.md) | 中文
## Problem
宿主任务需要全局管理和显式执行目标,并在工作区中可辨认。
## Decision
存储领域、导入器、占位 RRULE 和并行 API 决策由[带类型规则存储与共享重复计算](2026-10-21-schedule-rule-storage.zh.md)替代。保留现有 `schedule` 领域和统一管理 API;下述目标、执行和通知决策保持有效。
目标分为 `session`、`new_once` 和 `new`。`session` 存储一个现有 Session id;创建和编辑会在不激活该 Session 的情况下检查它存在且不属于被委派的子 Agent。`new_once` 在创建任务或重新指定目标时预留 Session id,在首次投递时创建该精确 Session,然后转换为 `session`。`new` 在每次创建尝试前把预留的 Session id 保存在 `pendingDelivery` 中,因此投递失败后的重试使用同一 Session。新建 Session 使用目标中的工作目录和 Agent preset,未提供时使用 Host 默认值。目标指定工作目录时,该 Session 加入在该目录注册的 Workspace(若存在)。该 Session 尚无标题时以任务标题命名;`new` 目标再加上该 Session 的创建时间(精确到分钟,按规则 `DTSTART` 的时区),例如 `Weekday digest · 2026-10-12 09:00`,以便区分各次投递创建的 Session。加入或命名失败只记录日志,不阻止投递。`involvedSessionIds` 按首次参与顺序列出创建和编辑任务的 Agent Session,以及每个现有或预留的目标 Session;`new` 目标创建的 Session 不列入其中。
运行时保留仅补发最新发生时点的行为。它投递不晚于投递判断时刻的最新发生时点,并推进到下一个未来发生时点;耗尽的规则以 `ended` 状态保留在存储中,暂停的任务没有计时器。同一任务的每次投递,无论定时还是立即执行,都在上一次投递的发生时点之后至少 60 秒,立即执行的发生时点是其运行时间;`new` 目标的每次投递都会创建 Session,部署可通过 `newSessionIntervalSeconds` 加大该间隔,默认 60。下一个已存储发生时点满足该间隔时,最小投递间隔从上一次发生时点起算,否则从上一次确认时间起算,因此确认延迟不会累积,延迟的投递也不会推移之后的发生时点;下一个准时的发生时点随后可能在延迟投递之后不足一个间隔到来。投递到复用 Session(`session` 或 `new_once`)的任务在目标 Agent 空闲之前一直占用其执行槽位,期间到期的发生时点合并为之后的一次投递,因此该 Session 最多有该任务的一次待处理投递;`new` 目标不占用槽位,各次启动并行运行,彼此之间只隔投递间隔。目标为 `session` 的到期任务按目标 Session 共享一条消息,After 和 At 任务除外。立即执行与定时投递共用目标解析、投递间隔、Session 收件箱持久化、回执与相关 Session 更新,但保留下一发生时点和状态。二者都拒绝已归档的目的 Session,包括重试时复用的预留 Session,因为已归档会话门禁会阻止该消息唤醒的轮次。Session 收件箱持久化先于任务回执写入,因此两个存储之间崩溃可能重复消息。实现不增加 scheduler、run 或 ownership service。
create、update 或 delete 提交后,Schedule 在独立的链上排队一条变更通知,因此 Session I/O 不会占用变更与投递队列。除操作者之外、Agent 已加载的每个相关 Session 都会收到一条供下一个模型步骤读取的 `schedule` 来源消息,该消息不会启动模型轮次,因此 Session 空闲期间排队的通知会随其下一条输入到达模型,而不会各自作为一个模型轮次运行;消息说明变更类型,并以 JSON 字符串携带任务 id 和标题。没有已加载 Agent 的 Session 不会收到消息,会在下次列出任务时读取当前任务。通知失败会记录日志,但不会回滚已提交的变更。关闭时先排空已排队的通知,再关闭存储。
## Alternatives considered
**要求调用方提供计划任务 id。** 用户可见标题与规范身份用途不同,但接受 id 会暴露冲突与校验策略。Host 生成身份并返回,供后续操作使用。
**保留创建者 Session 所有权。** 所有权会让 list 与 mutation API 保持作用域限制,并与全局管理、重新指定目标和 Session 创建策略冲突。历史参与关系是元数据和通知路由,不是授权。
**通过抽样即将到来的发生时点检查间隔。** 每次发生时点查询都从 `DTSTART` 开始枚举,因此起始时间较早的高频规则会在抽样检查拒绝它之前阻塞 Host 线程,而且抽样无法约束完整序列。改为在枚举之前做静态规则检查,并用每个任务的投递间隔约束间隔。
**增加 scheduler、run 和 notification service。** 现有 Schedule service、存储领域、runtime 和 Session 控制器已按所需规模承担这些职责。新层级只会增加间接性,不改变持久性。
**让新建 Session 保持无标题且不属于任何 Workspace。** 这样的 Session 会作为又一个无标题 Session 出现在拥有其目录的 Workspace 之外,用户无法分辨是哪个任务创建了它,也无法分辨对于 `new` 目标它接收了哪一次投递。加入目标目录处的 Workspace,并以任务标题命名 Session(`new` 再加创建分钟),不需要存储字段。失败只记录日志,因为投递优先于呈现。
## Consequences
任务寿命长于 Agent,并按 id 管理。归档准入报告活动 Session 目标;显式停止删除任务。宿主可用性、跨存储至少一次投递、尽力通知和每次新建目标的无界并发仍是限制。[Schedule 参考](../../../../docs/subsystems/schedule.zh.md)拥有精确的当前契约,[管理升级指南](../../../../docs/upgrade-guide/v0.2.2-alpha.2/schedule-remote-host-tasks/guide.zh.md)说明调用方变更。
@@ -9,11 +9,11 @@
en: 97fe58a13135b4a1
zh: 7490373a8d941da5
/agent-note-active-tool-headers-and-stateful-request-projection/decision:
en: 0c40a9283650c643
zh: 91a18d752d09a9ce
en: 24138152d511bf12
zh: 95516d7566b6db7c
/agent-note-active-tool-headers-and-stateful-request-projection/alternatives-considered:
en: b78785f2a603eba8
zh: b8c268228da7e2e7
en: 311db1f781e386e7
zh: 7e3312544154a158
/agent-note-active-tool-headers-and-stateful-request-projection/consequences:
en: 60d7bf28fa5a8fd1
zh: a17b222e7aca01e2
en: 8a64dba42c01f647
zh: 9e7ba7a5bd855031
@@ -14,9 +14,9 @@ Plugin and MCP tools change during conversations. Providers differ in whether th
`Session.toolHistory()` caches a capability-independent fold of committed headers and developer messages. First access restores inherited events; later reads consume only unseen events. Immutable snapshots carry initial declarations and ordered update identities with additions resolved through their historical headers. Explicit series starts and changed retained definitions rebuild declarations. Restoring a retained name with an unchanged definition continues the series: the projection keeps the declaration unchanged and lets the recorded addition re-offer the tool after its earlier removal, so a removal never disables a declared tool and a loading mode never rewrites an earlier declaration.
Loop and compaction requests carry this snapshot as `GenerateOptions.toolHistory`. At adapter dispatch, `projectToolUpdates` uses the prepared route's `toolUpdate`. Unsupported routes receive active definitions without `deferLoading` or developer messages. Supported routes defer historical additions; `in-history` retains removed definitions and removal blocks, while `addition-only` omits both. Only current-series update identities survive. Missing history or a request prefix omitting recorded updates uses current declarations without developer updates. Explicit deferred loading remains available on capable routes.
Loop and compaction requests carry this snapshot as `GenerateOptions.toolHistory`. At adapter dispatch, `projectToolUpdates` uses the prepared route's `toolUpdate`. Unsupported routes receive active definitions without `deferLoading` or developer messages. Supported routes defer historical additions; `in-history` retains removed definitions and removal blocks, while `addition-only` omits both. On addition-only routes, removing any tool rebuilds the route's baseline after that update: the request declarations change even when the removed tool was activated later, so retaining superseded activation blocks cannot preserve the full conversation prefix. Adding tools when no declarations exist also rebuilds the baseline. Deferred declarations count as existing tools. The first native tool changes the empty declaration prefix; deferring it provides no cache advantage over declaring it immediately. Previously activated tools become immediate; explicitly deferred declarations with no activation remain deferred. Subsequent additions remain deferred until their recorded activation. Superseded tool blocks are omitted without discarding other content in those messages. The Session fold and durable events remain capability-independent. Missing history or a request prefix omitting recorded updates uses current declarations without developer updates.
DeepSeek translates projected updates into system-role `tool_addition` and `tool_removal` blocks and `defer_loading` declarations, sending the tool-changes beta header when update blocks occur. The default `deepseek-flash` entry declares `addition-only`; pi-ai declares no mode. Chat and Trajectory display developer messages as context nodes, using localized tool-change notices: single changes show the tool name; multiple changes show counts and expandable name lists.
DeepSeek translates projected updates into system-role `tool_addition` and `tool_removal` blocks and `defer_loading` declarations, sending the tool-changes beta header when update blocks occur. The default `deepseek-flash` entry declares `addition-only`; pi-ai selects `in-history` for compatible Anthropic routes and `addition-only` for compatible Chat Completions and Responses routes. Chat and Trajectory display developer messages as context nodes, using localized tool-change notices: single changes show the tool name; multiple changes show counts and expandable name lists.
## Alternatives considered
@@ -26,6 +26,8 @@ DeepSeek translates projected updates into system-role `tool_addition` and `tool
**Scan the complete log on every request.** The cached fold consumes new events and supplies stable snapshots to ordinary and auxiliary calls without adding arbitrary synchronous history readers.
**Rebuild the baseline on every removal.** Deferred tools also have request declarations; deleting a tool changes those declarations regardless of its activation position. Addition-only routes cannot express that deletion in history. The rebuild therefore also covers removal-only updates and removals of later-activated tools. A past reset cannot force every future addition to become immediate: each reset establishes a new baseline from which deferred additions resume.
**Reject same-name schema changes during tool registration.** This restriction is not enforced. Immediate failure for a plugin registering an incompatible tool requires comparing the new registration with historical definitions of tools called on each affected current surface, while excluding calls removed by compaction. That registration-time historical and scope-aware check is deferred because of its implementation complexity. Same-name schema changes remain allowed; they rebuild declarations and invalidate prefix reuse even on `in-history` routes.
## Consequences
@@ -34,4 +36,6 @@ Tool lists and logical update events are capability-independent; only outgoing d
The authored [headless prompt/tool scenario](../../../../snapshots/session/dynamic-tool-prompt-updates/snapshot.yml) pins an appended prompt beside a deferred tool addition and verifies that the first request's messages stay unchanged. It shares the stable-prompt scenario's tool lifecycle and schema expectations.
The [DeepSeek replacement scenario](../../../../snapshots/session/deepseek-deferred-tools/snapshot.yml) checks the first additions to an empty set, a rebuilt baseline, a subsequent deferred addition, and removal of that later-activated tool through Messages serialization. Explicit deferred-only requests remain unchanged under the [DeepSeek failure policy](../simplification/2026-11-02-deepseek-deferred-tool-failures.md). These checks establish request construction, not measured cache savings from rebuilding.
The [real DeepSeek SDK test](../../../../apps/cli/tests/profiles/sdk/dynamic-tool-cache.e2e.ts) measures the first request after a native tool addition with both unchanged and simultaneously updated instructions. Unique user history prevents a shared system-prompt cache hit from satisfying the preceding-input assertion. Removal checks tool availability without requiring cache retention; each case disables retries.
@@ -14,9 +14,9 @@ Status: implemented
`Session.toolHistory()` 缓存对已提交请求头和 developer 消息的归约,不依赖模型能力。首次访问恢复继承事件;后续读取仅处理新增事件。不可变快照携带初始声明和有序更新标识,添加定义通过历史请求头解析。显式新序列和保留定义变化会重建声明。以未变化的定义恢复保留名称时序列继续:投影保持声明不变,由记录的添加在先前移除之后重新提供该工具,因此移除不会禁用已声明的工具,加载模式也不会改写先前声明。
循环和压缩请求通过 `GenerateOptions.toolHistory` 携带此快照。适配器分发时,`projectToolUpdates` 使用已准备路由的 `toolUpdate`。不支持的路由接收有效定义,不携带 `deferLoading` 或 developer 消息。支持的路由延迟历史添加;`in-history` 保留已移除定义和移除块,`addition-only` 则省略两者。仅保留当前序列的更新标识。缺少历史或请求前缀遗漏已记录更新时,使用当前声明且不发送 developer 更新。显式延迟加载在支持的路由上仍然可用。
循环和压缩请求通过 `GenerateOptions.toolHistory` 携带此快照。适配器分发时,`projectToolUpdates` 使用已准备路由的 `toolUpdate`。不支持的路由接收有效定义,不携带 `deferLoading` 或 developer 消息。支持的路由延迟历史添加;`in-history` 保留已移除定义和移除块,`addition-only` 则省略两者。在 addition-only 路由移除任何工具时,使用该次更新后的状态重建路由基线:即使被移除工具是后来激活的,请求声明也已发生变化,保留已被取代的激活块无法保留完整对话前缀。不存在任何声明时添加工具,也会重建基线。延迟声明也计入已有工具。首个原生工具会改变空声明前缀;将其延迟加载并不比直接声明具有缓存优势。此前已激活工具变为立即加载;没有激活记录的显式延迟声明仍保持延迟。后续添加在记录的激活位置前保持延迟加载。已被取代的工具块会被省略,但不会丢弃这些消息中的其他内容。Session 归约和持久事件仍不依赖模型能力。缺少历史或请求前缀遗漏已记录更新时,使用当前声明且不发送 developer 更新。
DeepSeek 把投影后的更新转换为 system 角色的 `tool_addition` 和 `tool_removal` 块以及 `defer_loading` 声明,在存在更新块时发送工具变更 beta 请求头。默认 `deepseek-flash` 条目声明 `addition-only`;pi-ai 不声明模式。Chat 和 Trajectory 将 developer 消息显示为上下文节点,工具变更使用本地化通知:单项变更显示工具名,多项变更显示数量及可展开的名称列表。
DeepSeek 把投影后的更新转换为 system 角色的 `tool_addition` 和 `tool_removal` 块以及 `defer_loading` 声明,在存在更新块时发送工具变更 beta 请求头。默认 `deepseek-flash` 条目声明 `addition-only`;pi-ai 为兼容的 Anthropic 路由选择 `in-history`,为兼容的 Chat Completions 和 Responses 路由选择 `addition-only`。Chat 和 Trajectory 将 developer 消息显示为上下文节点,工具变更使用本地化通知:单项变更显示工具名,多项变更显示数量及可展开的名称列表。
## Alternatives considered
@@ -26,6 +26,8 @@ DeepSeek 把投影后的更新转换为 system 角色的 `tool_addition` 和 `to
**每次请求扫描完整日志。** 缓存归约处理新事件,为普通和辅助调用提供稳定快照,不增加任意同步历史读取接口。
**每次移除都重建基线。** 延迟工具也具有请求声明;删除工具会改变这些声明,与其激活位置无关。addition-only 路由无法通过历史表达删除。因此,仅移除的更新和移除后来激活的工具也会重建基线。一次历史重置不能让所有后续添加都变为立即加载:每次重置确立新基线,后续添加继续采用延迟加载。
**在工具注册时拒绝同名 schema 变化。** 当前不执行此限制。要让注册不兼容工具的插件立即收到异常,需要将新注册与各受影响当前 surface 中已调用工具的历史定义比较,同时排除已被压缩移除的调用。由于实现复杂,注册时结合历史和作用域的检查留待后续实现。同名 schema 变化仍被允许;它们会重建声明,即使在 `in-history` 路由上也会使前缀复用失效。
## Consequences
@@ -34,4 +36,6 @@ DeepSeek 把投影后的更新转换为 system 角色的 `tool_addition` 和 `to
手写的[无界面提示词/工具场景](../../../../snapshots/session/dynamic-tool-prompt-updates/snapshot.yml)钉住追加提示词与延迟工具添加同时发生的行为,并验证首次请求的消息保持不变。它复用稳定提示词场景的工具生命周期和 schema 期望值。
[DeepSeek 替换场景](../../../../snapshots/session/deepseek-deferred-tools/snapshot.yml)通过 Messages 序列化验证向空工具集首次添加工具、重建后的基线、后续延迟添加,以及移除该后来激活的工具。根据 [DeepSeek 失败策略](../simplification/2026-11-02-deepseek-deferred-tool-failures.zh.md),显式全延迟请求保持原样。这些检查验证请求构造,不代表已经测量重建带来的缓存收益。
[真实 DeepSeek SDK 测试](../../../../apps/cli/tests/profiles/sdk/dynamic-tool-cache.e2e.ts) 分别在指令不变和指令同时更新时测量原生工具新增后的首次请求。独有的用户历史确保仅命中共享系统提示词缓存无法满足前序输入断言。移除场景检查工具可用性,不要求保留缓存;每个用例均禁用重试。
@@ -6,14 +6,14 @@
en: 17a73230ad22ec0b
zh: 5bcf27d7c0e86764
/agent-note-schedule-deletes-task-rows-and-bounds-delivery-history/problem:
en: 98e82a0add324882
zh: c10e85a3f2a522f8
en: c49b11c393e59f35
zh: 03cab3ae79a6c878
/agent-note-schedule-deletes-task-rows-and-bounds-delivery-history/decision:
en: 56328745115714b5
zh: f30d39f59aa06a13
en: 48272c9bbe2a0040
zh: 668799310bcd20ba
/agent-note-schedule-deletes-task-rows-and-bounds-delivery-history/alternatives-considered:
en: 0c4eb01054be1141
zh: 95980f8097fa36fd
en: 7663bb52e1034eeb
zh: 2ef8f146c5642837
/agent-note-schedule-deletes-task-rows-and-bounds-delivery-history/consequences:
en: 05121f4b77b8ee12
zh: 1751c9b1a752ce6b
en: f911ddff2a9b81da
zh: 38ee18e3f6b10c26
@@ -6,15 +6,15 @@ English | [中文](2026-09-22-schedule-hard-delete-and-bounded-history.zh.md)
## Problem
The Schedule domain is one whole-unit JSON document that the backend rewrites on every mutation, so every retained byte in a task row grows every write. Saved delivery records were unbounded: each acknowledgment appended to the array, and a deleted task kept its row and records so the open detail could still read them. The document's size therefore followed a task's delivery count, and deletion added a state every reader had to skip.
The Schedule domain stores each task as one JSON document that the backend rewrites whole on every change of that task, so every retained byte in a task row grows every write of that task. Saved delivery records were unbounded: each acknowledgment appended to the array, and a deleted task kept its row and records so the open detail could still read them. The document's size therefore followed a task's delivery count, and deletion added a state every reader had to skip.
## Decision
Deletion is hard deletion. `ScheduleService.delete` removes the storage row through `tasks.delete`, so the task stops scheduling, leaves `list` and `catalog`, and `history` answers `schedule_not_found` for the same `(sessionId, id)`; the row's saved delivery records are removed with it. The task schema has no `deleted` field, and no client surface offers a recycle bin or a read of an already deleted task's records.
Deletion is hard deletion. The `delete` action of `ScheduleService.edit` removes the storage row through `tasks.delete`, so the task stops scheduling, leaves `list` and `catalog`, and `history` answers `schedule_not_found` for its id; the row's saved delivery records are removed with it. `deleteInactive` removes only ended tasks in one `tasks.deleteMany` batch and preserves active and paused tasks. The task schema has no `deleted` field, and no client surface offers a recycle bin or a read of an already deleted task's records.
Delivery history is bounded by plugin `Config`: `deliveryHistoryDays` (default 30, 1–3650) and `deliveryHistoryRecords` (default 200, 1–10000). `appendDelivery` applies both bounds on the only write path. It keeps the records whose `deliveredAt` is at or after the appended receipt's `deliveredAt` minus the configured day window, keeps the newest `deliveryHistoryRecords` of those, and always retains the appended latest receipt; a pruned window sets `earlierRecordsUnavailable` and `earlierRecordsPruned`, which later appends preserve. The latter flag is optional in stored rows and confirms actual removal; legacy unavailability alone does not imply pruning. The history response includes the current retention limits, and the Client shows the cleanup notice only for confirmed pruning after loading the final nonempty page.
The [Host-owned scheduled messages](2026-09-16-host-schedule-storage.md) note keeps the rest of its decision: one authoritative version-1 task domain, timer restoration for active tasks only, cold-Session delivery through the Session controller, the catalog and detail surfaces, and the historical-versus-Host decoder split. Its status vocabulary is `active` or `inactive`; `inactive` is the sole non-active name, and the client filter row reads All, Enabled, and Inactive. That note stays active: this supersession is partial, so it is neither archived nor frozen, and it remains the owner of everything above while the deletion and retention decisions it recorded are replaced here.
The [Host-owned scheduled messages](2026-09-16-host-schedule-storage.md) note keeps the rest of its decision: timer restoration for active tasks only, cold-Session delivery through the Session controller, the catalog and detail surfaces, and the historical-versus-Host decoder split. That note stays active: this supersession is partial, so it is neither archived nor frozen, and it remains the owner of everything above while the deletion and retention decisions it recorded are replaced here. [Standalone schedules and Session targets](2026-09-17-standalone-schedules.md) owns the authoritative task domain and its `active`, `paused`, or `ended` status. Native and Remote catalogs report these same three statuses across the Host. The Automation tasks page's status filter row reads All, Enabled, Paused, and Ended.
## Alternatives considered
@@ -24,10 +24,10 @@ The [Host-owned scheduled messages](2026-09-16-host-schedule-storage.md) note ke
**Measure the window from the task instead of from each receipt.** Anchoring the window to the task's creation or committed target would discard a newly written receipt of an old task. Measuring back from the appended receipt's `deliveredAt` keeps the window anchored to the data being kept.
**Keep history unbounded and let operators clean up.** The domain publishes one whole-unit document per write, so unbounded history makes the storage document's size a function of delivery counts. Retention is a bound the storage layout requires, not a retention preference.
**Keep history unbounded and let operators clean up.** The backend rewrites each task document whole on every write of that task, so unbounded history makes the task document's size a function of delivery counts. Retention is a bound the storage layout requires, not a retention preference.
## Consequences
Deletion now offers no recovery: a deleted task's saved deliveries cannot be read back, and a task that still needs its full history must be read before deletion. Configured retention also discards receipts the task once had; the retained flag records that a window was pruned rather than pretending the history is complete.
In exchange, the persisted task set has one deletion semantics, the storage document's size is bounded by configuration instead of by delivery counts, and `inactive` is the only non-active lifecycle name. The plugin `Config` is the place to raise the bounds when a deployment needs longer delivery history.
In exchange, the persisted task set has one deletion semantics, the size of each task document is bounded by configuration instead of by delivery counts, and deletion adds no lifecycle state. The plugin `Config` is the place to raise the bounds when a deployment needs longer delivery history.
@@ -6,15 +6,15 @@ Status: implemented
## Problem
Schedule domain 是后端每次变更都会重写的单个整 unit JSON 文档,因此任务行中保留的每个字节都会随每次写入增长。已保存的发送记录没有上限:每次确认都会向数组追加,而被删除的任务会保留其行与记录,以便已打开的详情仍能读取。于是文档大小随任务的投递次数变化,删除还引入了一个所有读取方都必须跳过的状态。
Schedule domain 把每个任务存储为一个 JSON 文档,后端在该任务每次变更时整体重写它,因此任务行中保留的每个字节都会随该任务的每次写入增长。已保存的发送记录没有上限:每次确认都会向数组追加,而被删除的任务会保留其行与记录,以便已打开的详情仍能读取。于是文档大小随任务的投递次数变化,删除还引入了一个所有读取方都必须跳过的状态。
## Decision
删除是硬删除。`ScheduleService.delete` 通过 `tasks.delete` 移除存储行,因此任务停止调度、离开 `list` 与 `catalog`,`history` 对相同的 `(sessionId, id)` 返回 `schedule_not_found`;该行已保存的投递记录随行一并移除。任务 schema 没有 `deleted` 字段,也没有任何界面提供回收站或读取已删除任务记录的能力。
删除是硬删除。`ScheduleService.edit` 的 `delete` 操作通过 `tasks.delete` 移除存储行,因此任务停止调度、离开 `list` 与 `catalog`,`history` 对其 id 返回 `schedule_not_found`;该行已保存的投递记录随行一并移除。`deleteInactive` 通过一次 `tasks.deleteMany` 批量写入仅删除已结束任务,保留活动和暂停任务。任务 schema 没有 `deleted` 字段,也没有任何界面提供回收站或读取已删除任务记录的能力。
投递历史受插件 `Config` 限制:`deliveryHistoryDays`(默认 30,1–3650)与 `deliveryHistoryRecords`(默认 200,1–10000)。`appendDelivery` 在唯一的写入路径上同时应用这两个上限。它保留 `deliveredAt` 不早于「本次追加回执的 `deliveredAt` 减去配置的天数窗口」的记录,在其中保留最新的 `deliveryHistoryRecords` 条,并始终保留本次追加的最近一次回执;发生裁剪时设置 `earlierRecordsUnavailable` 和 `earlierRecordsPruned`,后续追加会保留这两个标志。后者在存储行中可选,用于确认实际删除;仅有旧格式的不可用标志不表示发生过裁剪。历史响应携带当前保留上限,客户端仅在加载完最后一页且历史非空、确认裁剪时显示清理提示。
[Host-owned scheduled messages](2026-09-16-host-schedule-storage.zh.md) 笔记保留其余决策:唯一的权威版本 1 任务 domain、只为活动任务恢复定时器、通过 Session controller 投递到冷会话、目录与详情界面,以及历史解码器与宿主解码器的划分。其状态词汇为 `active` 或 `inactive`;`inactive` 是唯一的非活动名称,客户端筛选行为全部、活动和未运行。该笔记保持活动状态:这是一次部分取代,因此它既未归档也未冻结,仍是上述内容的归属笔记,而它记录的删除与保留决策由本笔记取代。
[Host-owned scheduled messages](2026-09-16-host-schedule-storage.zh.md) 笔记保留其余决策:只为活动任务恢复定时器、通过 Session controller 投递到冷会话、目录与详情界面,以及历史解码器与宿主解码器的划分。该笔记保持活动状态:这是一次部分取代,因此它既未归档也未冻结,仍是上述内容的归属笔记,而它记录的删除与保留决策由本笔记取代。[独立计划任务与 Session 目标](2026-09-17-standalone-schedules.zh.md) 拥有权威任务 domain 及其 `active`、`paused` 或 `ended` 状态。原生和 Remote 目录在宿主范围内报告相同的三种状态。自动化任务页面的状态筛选行为全部、已开启、已暂停和已结束。
## Alternatives considered
@@ -24,10 +24,10 @@ Schedule domain 是后端每次变更都会重写的单个整 unit JSON 文档
**以任务而非每条回执为窗口起点。** 把窗口锚定在任务的创建时间或已提交目标上,会丢弃旧任务刚写入的回执。从本次追加回执的 `deliveredAt` 向前计算,使窗口锚定在所要保留的数据上。
**保留无上限的历史,由运维清理。** 该 domain 每次写入都发布整个 unit 文档,因此无上限的历史会让存储文档的大小随投递次数增长。保留上限是存储布局所要求的约束,而不是保留策略偏好。
**保留无上限的历史,由运维清理。** 后端在该任务每次写入时整体重写其任务文档,因此无上限的历史会让任务文档的大小随投递次数增长。保留上限是存储布局所要求的约束,而不是保留策略偏好。
## Consequences
删除不再提供恢复能力:已删除任务的已保存投递无法再读回,仍需要完整历史的必须在删除前读取。配置的保留策略也会丢弃任务曾经拥有的回执;保留的标志记录窗口发生过裁剪,而不是假装历史完整。
换取的是:持久任务集合只有一种删除语义,存储文档大小由配置而非投递次数决定,且 `inactive` 是唯一的非活动生命周期名称。当某个部署需要更长的投递历史时,应提高插件 `Config` 中的上限。
换取的是:持久任务集合只有一种删除语义,每个任务文档的大小由配置而非投递次数决定,且删除不增加生命周期状态。当某个部署需要更长的投递历史时,应提高插件 `Config` 中的上限。
@@ -9,14 +9,14 @@
en: 27b4d1d25cd1c178
zh: 3ddc4d9a09b6182d
/agent-note-preset-scoped-schedule-tools/decision:
en: 3c4708981d34217d
zh: cd0c07dc0888f3db
en: 652fb37ad4e5a826
zh: f5c3cc1e9a79b4ca
/agent-note-preset-scoped-schedule-tools/alternatives-considered:
en: 5cfe619038339caa
zh: c2d2651d304d867c
/agent-note-preset-scoped-schedule-tools/consequences:
en: d7c7e6b8e6268559
zh: 801e5be70e00cc90
en: aceed1aa98ed9bec
zh: b9d99a25a8b25417
/agent-note-preset-scoped-schedule-tools/testing:
en: d42cb866907a96b5
zh: 6e66f8babe744bcb
en: 05080859ac790524
zh: 4b220c10da961608
@@ -10,9 +10,9 @@ English | [中文](2026-09-24-preset-scoped-schedule-tools.zh.md)
## Decision
`@deepseek-ai/dsh-tool-schedule` (`packages/schedule/tool-schedule`) contributes the four tools as a preset-level Consumer. It declares `inject = ['tools']` and registers the definitions on `ctx.tools` from inside `ctx.inject(['schedule'], …)`, so the mounting scope is the one that owns them and the registration waits for the Host Schedule service in that same scope. The shipped Web profile mounts the row in its `standard`, `cordis`, and `ptc` presets and omits it from `minimal`. Cordis effect ownership disposes the definitions with the mount, and a composition that never resolves `schedule` registers none of them. The [Web bundle](../../../../packages/bundle/web-app/README.md) owns the `schedule` and `ui-schedule` rows the shipped composition mounts; this record owns the reminder tools' preset placement.
`@deepseek-ai/dsh-tool-schedule` (`packages/schedule/tool-schedule`) contributes `schedule_create`, `schedule_list`, `schedule_view`, and `schedule_edit` as a preset-level Consumer. It declares `inject = ['tools', 'systemPrompt']` and registers the definitions and shared authorization instruction from inside `ctx.inject(['schedule'], …)`, so the mounting scope owns them and registration waits for the Host Schedule service in that same scope. The shipped Web profile mounts the row in its `standard`, `cordis`, and `ptc` presets and omits it from `minimal`. Cordis effect ownership disposes the definitions with the mount, and a composition that never resolves `schedule` registers none of them. The [Web bundle](../../../../packages/bundle/web-app/README.md) owns the `schedule` and `ui-schedule` rows the shipped composition mounts; this record owns the reminder tools' preset placement.
`@deepseek-ai/dsh-schedule` keeps the version-1 storage domain, the Host timer and serialized queue, Host delivery through the Session controller, the Automation page's backing reads, and the `ctx.schedule` interface. `dsh-tool-schedule` is the model-facing consumer of that interface: it checks selector and identity constraints before the service call, reads `exec.agent` for the Session binding, and maps failures that are not `ScheduleInputError` to `internal_error` so storage details never reach the model.
`@deepseek-ai/dsh-schedule` keeps the `schedule` storage domain, now writing v2 and reading v1, the Host timer and serialized queue, Host delivery, the Automation tasks page's backing reads, and the `ctx.schedule` interface. `dsh-tool-schedule` is the model-facing consumer of that interface: it maps RRULE and target inputs to service requests, reads `exec.agent` for the actor and `this_session` destination, and maps undeclared failures to `internal_error` so storage details never reach the model. Management addresses every Host task by id; the target alone selects the receiving Session.
## Alternatives considered
@@ -26,9 +26,9 @@ English | [中文](2026-09-24-preset-scoped-schedule-tools.zh.md)
- Each preset that mounts the row pays the four schemas' fixed token cost, and tool availability is read from the composition instead of inferred from the Host service's presence.
- A deployment can mount `dsh-schedule` for storage and delivery without granting its agents model-driven reminder management.
- `dsh-schedule` injects no `ctx.tools` and registers no model-facing tool.
- A delegated child inherits the preset's four reminder schemas. `subagentCallerRefusal` in `dsh-tool-schedule` returns `{ code: 'subagent_session', message: 'A delegated subagent cannot use reminders.' }` from each tool when the caller's delegation depth is above zero. The [preset composition decision](../simplification/2026-10-09-preset-owned-subagent-composition.md) removes the redundant child filter; visibility does not authorize execution.
- A Session a delegated child owns can never receive a delivered reminder, so `ScheduleService.create` and `ScheduleService.update` refuse it with `subagent_session`. The refusal reads the delegation depth — the accounting the delegation cap itself enforces, carried by the persisted session header across a cold resume. The rule sits in the service, so other consumers reach it too — the Automation surface included.
- A delegated child inherits the preset's four reminder schemas. `runForCaller` in `dsh-tool-schedule` returns `{ code: 'subagent_session', message: 'A delegated subagent cannot use scheduled tasks.' }` from each tool when the caller's delegation depth is above zero. The [preset composition decision](../simplification/2026-10-09-preset-owned-subagent-composition.md) removes the redundant child filter; visibility does not authorize execution.
- A Session a delegated child owns can never receive a delivered reminder, so `ScheduleService.create` and target changes or arming through `ScheduleService.edit` refuse it with `subagent_session`. The refusal reads the delegation depth — the accounting the delegation cap itself enforces, carried by the persisted session header across a cold resume. The rule sits in the service, so other consumers reach it too — the Automation surface included.
## Testing
`packages/schedule/tool-schedule/tests/tool-schedule.spec.ts` pins the four definitions and their error mapping, including `refuses every tool for a delegated child caller`. `apps/cli/tests/web-agent-presets.e2e.ts` asserts the `minimal` preset's tool list, and `snapshots/web/minimal-preset/tool-schemas.expected.json` records its tool table without reminder schemas. `packages/schedule/schedule/tests/subagent-ownership.spec.ts` pins the delegation-depth refusal, and `scripts/optional-bundles.spec.ts` asserts that both delegation rows in the three presets omit `toolFilter`. The Web preset e2e also checks equal parent/child tool schemas and refusal from all four reminder executors for fresh and seeded children. `snapshots/web/subagent-fork-schedule` records all four refusals in a seeded fork through the shipped Web composition and shares the standard preset's prompt and schema pins.
`packages/schedule/tool-schedule/tests/tool-schedule.spec.ts` pins the four definitions and their error mapping, including `refuses every call from a registered delegated child whose scope mounts the tools`, and `packages/subagent/subagent/tests/schedule-tools.spec.ts` runs a delegated child through the tool executor and pins that refusal without a stored task. `apps/cli/tests/web-agent-presets.e2e.ts` asserts the `minimal` preset's tool list, and `snapshots/web/minimal-preset/tool-schemas.expected.json` records its tool table without reminder schemas. `packages/schedule/schedule/tests/subagent-ownership.spec.ts` pins the delegation-depth refusal, and `scripts/optional-bundles.spec.ts` asserts that both delegation rows in the three presets omit `toolFilter`. The Web preset e2e also checks equal parent/child tool schemas and refusal from all four reminder executors for fresh and seeded children. `snapshots/web/subagent-fork-schedule` records all four refusals in a seeded fork through the shipped Web composition and shares the standard preset's prompt and schema pins.
@@ -10,9 +10,9 @@ Status: implemented
## Decision
`@deepseek-ai/dsh-tool-schedule`(`packages/schedule/tool-schedule`)以 preset 级 Consumer 的身份贡献这四个工具。它声明 `inject = ['tools']`,并在 `ctx.inject(['schedule'], …)` 内通过 `ctx.tools` 注册这些定义,因此拥有它们的是挂载它的作用域,而注册会等待同一作用域中的宿主 Schedule 服务。发布版 Web profile 在其 `standard`、`cordis` 和 `ptc` preset 中挂载该行,`minimal` 不挂载。Cordis 的 effect 所有权随挂载卸载而释放这些定义,而始终未解析出 `schedule` 的组合不会注册其中任何一个。[Web bundle](../../../../packages/bundle/web-app/README.zh.md)负责随发行版交付的组合挂载的 `schedule` 与 `ui-schedule` 两行;本记录负责提醒工具的 preset 归属。
`@deepseek-ai/dsh-tool-schedule`(`packages/schedule/tool-schedule`)以 preset 级 Consumer 的身份贡献 `schedule_create`、`schedule_list`、`schedule_view` 和 `schedule_edit`。它声明 `inject = ['tools', 'systemPrompt']`,并在 `ctx.inject(['schedule'], …)` 内注册这些定义和共用授权指令,因此挂载作用域拥有它们,注册会等待同一作用域中的宿主 Schedule 服务。发布版 Web profile 在其 `standard`、`cordis` 和 `ptc` preset 中挂载该行,`minimal` 不挂载。Cordis 的 effect 所有权随挂载卸载而释放这些定义,而始终未解析出 `schedule` 的组合不会注册其中任何一个。[Web bundle](../../../../packages/bundle/web-app/README.zh.md)负责随发行版交付的组合挂载的 `schedule` 与 `ui-schedule` 两行;本记录负责提醒工具的 preset 归属。
`@deepseek-ai/dsh-schedule` 保留版本 1 storage domain、宿主定时器与串行队列、经由 Session controller 的宿主投递、自动化任务页面的读取来源,以及 `ctx.schedule` 接口。`dsh-tool-schedule` 是该接口面向模型的消费者:它在调用服务前校验选择器与身份约束,从 `exec.agent` 读取 Session 绑定,并把非 `ScheduleInputError` 的失败映射为 `internal_error`,使存储细节不会到达模型。
`@deepseek-ai/dsh-schedule` 保留 `schedule` 存储域,现写入 v2 并读取 v1,同时保留宿主定时器与串行队列、宿主投递、自动化任务页面的读取来源,以及 `ctx.schedule` 接口。`dsh-tool-schedule` 是该接口面向模型的消费者:它将 RRULE 与目标输入映射为服务请求,从 `exec.agent` 读取操作者与 `this_session` 目的地,并把未声明的失败映射为 `internal_error`,使存储细节不会到达模型。管理按 id 寻址宿主全部任务;仅目标选择接收 Session。
## Alternatives considered
@@ -26,9 +26,9 @@ Status: implemented
- 挂载该行的每个 preset 承担四个 schema 的固定 token 成本,工具可用性取自组合,而不是由宿主服务是否存在推断。
- 部署可以只为存储与投递挂载 `dsh-schedule`,而不授予其 Agent 由模型驱动的提醒管理能力。
- `dsh-schedule` 不注入 `ctx.tools`,也不注册任何面向模型的工具。
- 被委派的子 agent 继承 preset 的四个提醒 schema。调用方委派深度大于零时,`dsh-tool-schedule` 中的 `subagentCallerRefusal` 从每个工具返回 `{ code: 'subagent_session', message: 'A delegated subagent cannot use reminders.' }`。[preset 组装决策](../simplification/2026-10-09-preset-owned-subagent-composition.zh.md)移除了重复的子级过滤;可见性不授予执行权限。
- 被委派子 agent 拥有的 Session 永远收不到投递的提醒,因此 `ScheduleService.create` 与 `ScheduleService.update` 会以 `subagent_session` 拒绝它。该拒绝读取委派深度——委派封顶本身执行的记账,由持久化 session header 承载并跨冷恢复保留。这条规则在服务层,因此其它消费方(包括自动化任务页面)也会命中。
- 被委派的子 agent 继承 preset 的四个提醒 schema。调用方委派深度大于零时,`dsh-tool-schedule` 中的 `runForCaller` 从每个工具返回 `{ code: 'subagent_session', message: 'A delegated subagent cannot use scheduled tasks.' }`。[preset 组装决策](../simplification/2026-10-09-preset-owned-subagent-composition.zh.md)移除了重复的子级过滤;可见性不授予执行权限。
- 被委派子 agent 拥有的 Session 永远收不到投递的提醒,因此 `ScheduleService.create` 以及通过 `ScheduleService.edit` 改变目标或启用任务时会以 `subagent_session` 拒绝它。该拒绝读取委派深度——委派封顶本身执行的记账,由持久化 session header 承载并跨冷恢复保留。这条规则在服务层,因此其它消费方(包括自动化任务页面)也会命中。
## Testing
`packages/schedule/tool-schedule/tests/tool-schedule.spec.ts` 固定这四个定义及其错误映射,包括 `refuses every tool for a delegated child caller`。`apps/cli/tests/web-agent-presets.e2e.ts` 断言 `minimal` preset 的工具列表,`snapshots/web/minimal-preset/tool-schemas.expected.json` 记录其不含提醒工具 schema 的工具表。`packages/schedule/schedule/tests/subagent-ownership.spec.ts` 固定委派深度拒绝,`scripts/optional-bundles.spec.ts` 断言三个 preset 的两行委派行都不含 `toolFilter`。Web preset e2e 还检查父子工具 schema 相等,且四个提醒执行器对新建和带种子的子级均拒绝执行。`snapshots/web/subagent-fork-schedule` 通过出货 Web 组合记录带种子 fork 中的四次拒绝,并共享 standard preset 的提示和 schema 预期。
`packages/schedule/tool-schedule/tests/tool-schedule.spec.ts` 固定这四个定义及其错误映射,包括 `refuses every call from a registered delegated child whose scope mounts the tools`;`packages/subagent/subagent/tests/schedule-tools.spec.ts` 通过工具执行器运行一个被委派的子 agent,并在没有已存任务时固定该拒绝。`apps/cli/tests/web-agent-presets.e2e.ts` 断言 `minimal` preset 的工具列表,`snapshots/web/minimal-preset/tool-schemas.expected.json` 记录其不含提醒工具 schema 的工具表。`packages/schedule/schedule/tests/subagent-ownership.spec.ts` 固定委派深度拒绝,`scripts/optional-bundles.spec.ts` 断言三个 preset 的两行委派行都不含 `toolFilter`。Web preset e2e 还检查父子工具 schema 相等,且四个提醒执行器对新建和带种子的子级均拒绝执行。`snapshots/web/subagent-fork-schedule` 通过出货 Web 组合记录带种子 fork 中的四次拒绝,并共享 standard preset 的提示和 schema 预期。
@@ -6,14 +6,14 @@
en: f524c3b0ee74c586
zh: ec4e85f0fd853532
/agent-note-delivered-reminders-are-framed-as-scheduled-messages-from-the-user/problem:
en: c4156e2a8a3ad379
zh: 992b76509f83d61f
en: 57f2fb0a35f6a420
zh: 7174afde673c62d6
/agent-note-delivered-reminders-are-framed-as-scheduled-messages-from-the-user/decision:
en: b9b203da394f58e5
zh: ac511458d60dab49
en: 136514e302d80f4c
zh: b34e140f41547d06
/agent-note-delivered-reminders-are-framed-as-scheduled-messages-from-the-user/alternatives-considered:
en: 8074a2b9b9f5d782
zh: 1604b7ee5d34375e
en: 49cc4e396052fc09
zh: adb8b01f29ce76fb
/agent-note-delivered-reminders-are-framed-as-scheduled-messages-from-the-user/consequences:
en: 85e03b541bfce57c
zh: 77decbb804f6a0b9
en: 55c346fbd5feb417
zh: 29631345d447f643
@@ -6,17 +6,17 @@ English | [中文](2026-09-24-scheduled-message-framing.zh.md)
## Problem
Due reminders reach their original Session as user-role messages with producer kind `schedule`. Before this decision, both renderers opened that message with an instruction about how to treat its payload: one-shot delivery asked the model to present `reminder_prompt_json` as untrusted reminder content and not as new user instructions, and recurring batch delivery asked the same for every `reminder_prompt` in `reminders_json`. The message therefore asserted that its own content was untrusted instead of naming where it came from.
Due reminders reach their target Session as user-role messages with producer kind `schedule`. Before this decision, both renderers opened that message with an instruction about how to treat its payload: one-shot delivery asked the model to present `reminder_prompt_json` as untrusted reminder content and not as new user instructions, and recurring batch delivery asked the same for every `reminder_prompt` in `reminders_json`. The message therefore asserted that its own content was untrusted instead of naming where it came from.
## Decision
`renderReminderFraming` and `renderRecurringReminderBatchFraming` in [`packages/schedule/schedule/src/domain.ts`](../../../../packages/schedule/schedule/src/domain.ts) both emit one shared fixed line, `SCHEDULED_MESSAGE_FRAMING`: `This is a scheduled message from the user`. It is the block's second line, after the bracketed marker (`[SCHEDULE REMINDER]`, `[SCHEDULE REMINDER BATCH]`), and the dynamic field lines are unchanged: a one-shot message still appends `schedule_id_json`, `occurrence_at`, and `reminder_prompt_json`, and a recurring batch still appends `reminders_json`.
The fixed line states the origin of the message: a schedule bound to that Session. A model reading a user-role message whose fixed block carries it reads the following reminder payload (`reminder_prompt_json` in a one-shot message, `reminders_json` in a batch) as content the user scheduled, and the Host cannot attest that the user typed it: `schedule_create` and `schedule_update` let the model write the prompt, and a model-written prompt can carry text the model read from the web, files, or command output. The previous line marked that payload untrusted, and this decision drops that instruction, so the change accepts the residual risk that model-authored text arrives under a user-origin line. Reintroduction condition: a demonstrated injection through a model-written prompt restores a clause that scopes the payload as reminder text rather than a new instruction, and the five pinned framing assertions in the package specs plus `expectReminderFraming` in the Web e2e are the check that such a change is deliberate. Framing-line forgery is closed by encoding: the schedule id and prompt are JSON strings, the occurrence instant is a validated canonical RFC 3339 value, and each occupies one line, so a prompt carrying embedded newlines or an `occurrence_at:`-style line cannot add framing lines to the block. The [Host-owned scheduled messages](2026-09-16-host-schedule-storage.md) note keeps the rest of its decision, including delivery through the Session controller and the saved delivery records.
The fixed line states the origin of the message: a schedule that delivers to that Session. A model reading a user-role message whose fixed block carries it reads the following reminder payload (`reminder_prompt_json` in a one-shot message, `reminders_json` in a batch) as content the user scheduled, and the Host cannot attest that the user typed it: `schedule_create` and `schedule_edit` let the model write the prompt, and a model-written prompt can carry text the model read from the web, files, or command output. The previous line marked that payload untrusted, and this decision drops that instruction, so the change accepts the residual risk that model-authored text arrives under a user-origin line. Reintroduction condition: a demonstrated injection through a model-written prompt restores a clause that scopes the payload as reminder text rather than a new instruction, and the five pinned framing assertions in the package specs plus `expectReminderFraming` in the Web e2e are the check that such a change is deliberate. Framing-line forgery is closed by encoding: the schedule id and prompt are JSON strings, the occurrence instant is a validated canonical RFC 3339 value, and each occupies one line, so a prompt carrying embedded newlines or an `occurrence_at:`-style line cannot add framing lines to the block. The [Host-owned scheduled messages](2026-09-16-host-schedule-storage.md) note keeps the rest of its decision, including delivery through the Session controller and the saved delivery records.
## Alternatives considered
**Keep the untrusted-content instruction.** It asserted that the prompt was untrusted input, which the delivered message does not need: the prompt is written in the Session the schedule is bound to — by the model through `schedule_create` and `schedule_update`, or by the user in the Web task detail — and the delivery is an ordinary user-role message there.
**Keep the untrusted-content instruction.** It asserted that the prompt was untrusted input, which the delivered message does not need: the model writes the prompt through `schedule_create` and `schedule_edit`, a human caller writes it through the Web task detail or Remote `create` and `edit`, and the delivery is an ordinary user-role message in its target Session.
**Change only the recurring batch line.** One-shot and recurring delivery would then disagree about the origin of the same kind of message, and each later wording change would need both renderers edited separately. Both paths now read one constant.
@@ -24,6 +24,6 @@ The fixed line states the origin of the message: a schedule bound to that Sessio
Model-visible reminder text changes for both delivery paths, so a Session log recorded before this decision reconstructs the previous framing, and the Web notice that renders a delivered message shows the new line when expanded. Nothing else moves: no session event, storage field, client code, protocol, or durable format changes, and the delivered text remains reconstructable from the log.
The prompt text carries no authorship guarantee. It is written in the Session the schedule is bound to, by the model through `schedule_create` and `schedule_update` or by the user in the Web task detail, and a model-written reminder can quote text the model read from the web, files, or command output. The task's Session binding scopes which Session's model reaches the task; it is not caller authorization and says nothing about where the prompt content came from. The Decision holds the accepted risk and its reintroduction condition.
The prompt text carries no authorship guarantee. The model writes it through `schedule_create` and `schedule_edit`, a human caller writes it through the Web task detail or Remote `create` and `edit`, and a model-written reminder can quote text the model read from the web, files, or command output. Those tools reach every Host task by id from any Session whose preset mounts them, so the task's target Session is not caller authorization and says nothing about where the prompt content came from. The Decision holds the accepted risk and its reintroduction condition.
No recorded-session snapshot covers a delivered reminder, and this change adds none: no committed fixture contains a `schedule` message, and `snapshots/web/schedule-catalog` records catalog queries only. Two harness capabilities are missing for such a fixture: no scenario patch controls the wall clock that `packages/schedule/schedule/src/runtime.ts` reads for delivery, and the snapshot harness seeds Sessions rather than the schedule storage domain that holds Host tasks, so those rows exist only where a Web test writes them by hand. This change records the missing harness support instead of a fixture. The delivered text is pinned by the five full-string assertions in `packages/schedule/schedule/tests/{domain,daily,weekly,cron}.spec.ts` and by `expectReminderFraming` in `apps/web/tests/schedule-after.e2e.ts`, which asserts the line in the assembled request for the one-shot and recurring paths.
@@ -6,17 +6,17 @@ Status: implemented
## Problem
到期的提醒以生产者 kind 为 `schedule` 的 user-role 消息进入其原 Session。在本决策之前,两个渲染器都用一句关于如何处理其载荷的固定指令开篇:单次投递要求模型把 `reminder_prompt_json` 作为不可信的提醒内容呈现、而不是新的用户指令;周期批次投递对 `reminders_json` 中的每个 `reminder_prompt` 提出同样的要求。于是这条消息断言自身内容不可信,而没有说明它来自哪里。
到期的提醒以生产者 kind 为 `schedule` 的 user-role 消息进入其目标 Session。在本决策之前,两个渲染器都用一句关于如何处理其载荷的固定指令开篇:单次投递要求模型把 `reminder_prompt_json` 作为不可信的提醒内容呈现、而不是新的用户指令;周期批次投递对 `reminders_json` 中的每个 `reminder_prompt` 提出同样的要求。于是这条消息断言自身内容不可信,而没有说明它来自哪里。
## Decision
[`packages/schedule/schedule/src/domain.ts`](../../../../packages/schedule/schedule/src/domain.ts) 中的 `renderReminderFraming` 与 `renderRecurringReminderBatchFraming` 都输出同一条固定行,即模块常量 `SCHEDULED_MESSAGE_FRAMING`:`This is a scheduled message from the user`;它是该块的第二行,位于方括号标记(`[SCHEDULE REMINDER]`、`[SCHEDULE REMINDER BATCH]`)之后。动态字段行不变:单次消息仍追加 `schedule_id_json`、`occurrence_at`、`reminder_prompt_json`,周期批次仍追加 `reminders_json`。
这条固定行说明消息的来源:绑定到该 Session 的一条定时任务。模型读到固定块中带有它的 user-role 消息时,会把随后的提醒载荷(单次消息的 `reminder_prompt_json`、批次的 `reminders_json`)当作用户安排的内容,而 Host 无法证明该文本由用户键入:`schedule_create` 与 `schedule_update` 允许模型写入提示词,模型写入的提示词可以带上模型从网页、文件或命令输出中读到的文本。旧固定行把该载荷标记为不可信,本决策有意去掉这一指令,因此接受「模型生成的文本以用户来源行投递」这一残留风险。再引入条件:一旦出现经由模型写入提示词完成注入的实例,就恢复一句把载荷限定为提醒文本、而非新指令的说明;包内五处固定断言与 Web e2e 的 `expectReminderFraming` 是「此类改动必须是有意为之」的检查。固定行无法被动态字段伪造,这由编码保证:schedule id 与提示词是 JSON 字符串,发生时点是经校验的规范 RFC 3339 值,且各自只占一行,因此带有换行或形如 `occurrence_at:` 的提示词无法向该块增加固定行。[Host 拥有的定时消息](2026-09-16-host-schedule-storage.zh.md) 记下的其余决策不变,包括通过 Session controller 投递以及保存的投递记录。
这条固定行说明消息的来源:投递到该 Session 的一条定时任务。模型读到固定块中带有它的 user-role 消息时,会把随后的提醒载荷(单次消息的 `reminder_prompt_json`、批次的 `reminders_json`)当作用户安排的内容,而 Host 无法证明该文本由用户键入:`schedule_create` 与 `schedule_edit` 允许模型写入提示词,模型写入的提示词可以带上模型从网页、文件或命令输出中读到的文本。旧固定行把该载荷标记为不可信,本决策有意去掉这一指令,因此接受「模型生成的文本以用户来源行投递」这一残留风险。再引入条件:一旦出现经由模型写入提示词完成注入的实例,就恢复一句把载荷限定为提醒文本、而非新指令的说明;包内五处固定断言与 Web e2e 的 `expectReminderFraming` 是「此类改动必须是有意为之」的检查。固定行无法被动态字段伪造,这由编码保证:schedule id 与提示词是 JSON 字符串,发生时点是经校验的规范 RFC 3339 值,且各自只占一行,因此带有换行或形如 `occurrence_at:` 的提示词无法向该块增加固定行。[Host 拥有的定时消息](2026-09-16-host-schedule-storage.zh.md) 记下的其余决策不变,包括通过 Session controller 投递以及保存的投递记录。
## Alternatives considered
**保留「不可信内容」指令。** 它断言提示词是不可信输入,而投递的消息并不需要这一断言:提示词写在绑定该定时任务的 Session 内——由模型通过 `schedule_create` 与 `schedule_update` 写入,或由用户在 Web 任务详情中写入——投递本身就是该 Session 中的普通 user-role 消息。
**保留「不可信内容」指令。** 它断言提示词是不可信输入,而投递的消息并不需要这一断言:模型通过 `schedule_create` 与 `schedule_edit` 写入提示词,人工调用方通过 Web 任务详情或 Remote `create` 与 `edit` 写入它,投递本身就是目标 Session 中的普通 user-role 消息。
**只改周期批次那一行。** 单次与周期投递会对同类消息的来源给出不同说法,此后每次改措辞都要分别改两个渲染器。现在两条路径读同一个常量。
@@ -24,6 +24,6 @@ Status: implemented
两个投递路径的模型可见提醒文本都改变,因此在该决策之前记录的 Session 日志重建出的是旧的固定文本;Web 中渲染该投递消息的通知在展开时显示新的一行。其余不动:没有 session 事件、存储字段、客户端代码、协议或持久化格式变化,投递文本仍可从日志重建。
提示词文本不携带作者保证。它写在绑定该定时任务的 Session 内——由模型通过 `schedule_create` 与 `schedule_update` 写入,或由用户在 Web 任务详情中写入——模型创建的提醒可以把模型从网页、文件或命令输出中读到的文本原样写入。任务的 Session 绑定限定的是哪个 Session 的模型能访问该任务;它不是调用方鉴权,也不说明提示词内容的来源。本决策中记下该残留风险与再引入条件。
提示词文本不携带作者保证。模型通过 `schedule_create` 与 `schedule_edit` 写入它,人工调用方通过 Web 任务详情或 Remote `create` 与 `edit` 写入它;模型创建的提醒可以把模型从网页、文件或命令输出中读到的文本原样写入。这些工具可以从任何挂载它们的 preset 所属的 Session 按 id 访问 Host 的每个任务,因此任务的目标 Session 不是调用方鉴权,也不说明提示词内容的来源。本决策中记下该残留风险与再引入条件。
没有录制会话快照覆盖提醒投递,本次也没有新增:已提交夹具中没有 `schedule` 消息,`snapshots/web/schedule-catalog` 只记录任务查询。构成该夹具还缺两项 harness 能力:没有场景补丁能控制 `packages/schedule/schedule/src/runtime.ts` 读取的墙上时钟;快照 harness 播种的是 Session,而不是承载 Host 任务的 schedule storage domain,那些任务行只在 Web 测试里手工写入。因此本次记录缺失的 harness 支持,而不新增夹具。投递文本改由 `packages/schedule/schedule/tests/{domain,daily,weekly,cron}.spec.ts` 中五处整串断言,以及 `apps/web/tests/schedule-after.e2e.ts` 中在装配后请求上断言该行、覆盖单次与周期两条路径的 `expectReminderFraming` 固定。
@@ -9,11 +9,11 @@
en: abd0263d14861926
zh: e1736cd08e87f795
/agent-note-shared-time-vocabulary-for-wall-clock-selectors/decision:
en: fdd39b7e8fa89d70
zh: 4a0dfc81509e5485
en: f1c2b98bf56cdba3
zh: 7e9444f8e73a4834
/agent-note-shared-time-vocabulary-for-wall-clock-selectors/alternatives-considered:
en: 1737484c099fab52
zh: 303b144c52d167bd
en: 3964f7d653747b55
zh: b50636faea4917ef
/agent-note-shared-time-vocabulary-for-wall-clock-selectors/consequences:
en: 79d8d66071453d1f
zh: f940f0b6d74148d6
en: 9fdaa9da9c9952eb
zh: b941f68c9c1bb994
@@ -10,11 +10,11 @@ English | [中文](2026-09-24-shared-time-vocabulary.zh.md)
## Decision
`dsh-util-time` owns the time vocabulary that boundaries accept. `packages/util/time/src/instant.ts` holds the parsers, `src/duration.ts` holds the span type, and `src/index.ts` re-exports both and keeps `canonicalClientTimeZone` as the `string | undefined` wrapper the existing wire boundaries consume.
`dsh-util-time` owns the time vocabulary that boundaries accept. `packages/util/time/src/instant.ts` holds strict instant and zone parsers, `src/duration.ts` holds the span type, and `src/cron.ts` / `src/rrule.ts` hold bounded recurrence values. `src/index.ts` exports them and keeps `canonicalClientTimeZone` as the `string | undefined` wrapper the existing wire boundaries consume.
The library answers discriminated results and carries no failure vocabulary. `parseOffsetInstant`, `fourDigitYearEpoch`, `parseLocalCalendar`, and `canonicalizeTimeZone` answer an `accepted` arm or a `rejected` arm naming a stable reason (`not_rfc3339`, `out_of_range`, `not_a_calendar_date`, `invalid_numeric_offset`, `malformed`, `blank`, `padded`, `not_area_location`, `unsupported`, `not_canonical_shape`). Each caller maps a reason onto its own code and diagnostic, so the library never writes model-facing text. `formatFourDigitYearInstant` returns the canonical instant text or `undefined` outside the bound, `isFourDigitYearInstant` answers whether an epoch is representable at all, and `resolveLocalInstant` answers epoch milliseconds or `undefined` for a wall-clock value inside a forward transition's gap. The reason vocabulary is parallel between the two strict parsers: the same condition is `out_of_range` in both, so year 0 is refused by `parseOffsetInstant` and `parseLocalCalendar` alike, while `resolveLocalInstant` accepts any year a real `Temporal` date carries because its callers derive local dates at the UTC edges. `LocalCalendarTime.year` records that split.
Schedule consumes it and keeps every domain decision. `canonicalizeTimeZone`, `decodeInstant`, `parseAtInput`, and `futureInstant` remain Schedule entry points with their existing codes and diagnostics; they now call the shared parser and map its reason. Schedule still owns the durable record shapes, the fold and transition authority, the injection-resistant framing, and all recurrence arithmetic: the cron engine, the daily and weekly date walks, the latest-due search, and the schedule-specific accepted-decision bound.
Schedule consumes it and keeps every domain decision. `canonicalizeTimeZone`, `decodeInstant`, `parseAtInput`, and `futureInstant` remain Schedule entry points with their existing codes and diagnostics; they call the shared parser and map its reason. Pure Cron/RRULE parsing and bounded occurrence searches belong to the library, take explicit reference times, and validate committed targets with `fourDigitYearEpoch`. Schedule owns durable records, the fold and transition authority, injection-resistant framing, daily and weekly date walks, delivery limits, and domain errors. The shared values own no task state, wall clock, or timers.
`dsh-time-context` consumes it too and keeps its stricter policy. `deriveBrowserTimeZoneContext` reads each user-rpc zone through `canonicalizeTimeZone` and refuses an alias whose canonical answer differs from the reported value, so the request boundary still requires an identity another process resolves back to the same name. Time-context owns its three refusals and their messages; the shared entry only answers the canonical name or a reason, and its `unsupported` rejection carries the platform error the library caught, so that refusal keeps its `cause` in both consumers.
@@ -22,7 +22,7 @@ Schedule consumes it and keeps every domain decision. `canonicalizeTimeZone`, `d
The last copy of the canonical instant profile moves too. `packages/session/session-format-v0-to-v1/src/payload-validation.ts` validated a stored schedule instant with its own regular expression and round trip; the shared `fourDigitYearEpoch` owns that same profile, so the migration now asks it and keeps its own error type and message. Because that migration reads released logs, its accepted set is a compatibility promise: `packages/session/session-format-v0-to-v1/tests/validation.spec.ts` accepts the four-digit-year bounds and refuses each spelling a regular expression alone would admit (`0000`, `2026-02-30`, a four-digit fraction, a numeric offset, surrounding whitespace).
The maintained `@js-temporal/polyfill` 0.5.1 becomes a runtime dependency of `dsh-util-time` as well as of `dsh-schedule`. Local resolution continues to use it, so the Host's IANA data still decides gaps and overlaps and no caller owns offset sampling. `util/time`'s own description and README drop the `zero-dependency` phrase as ambiguous: the group's charter forbids harness dependencies so any group may depend on util, and does not ban external packages, which is why `packages/README.md` and the `util/` line in `AGENTS.md` keep the phrase while `packages/util/README.md` and this package's own description name what it loads.
The maintained `@js-temporal/polyfill` 0.5.1 is a runtime dependency of `dsh-util-time` as well as of `dsh-schedule`; `rrule` supplies RRULE enumeration behind the library's strict syntax and cost checks. Local resolution uses Temporal, so the Host's IANA data decides gaps and overlaps and no caller owns offset sampling. The util group's charter forbids harness dependencies so any group may depend on util; it permits external packages, which this package's own description and README identify.
Observable behavior does not change. Every accepted input resolves to the same epoch, every refused input raises the same `ScheduleInputError` or `ScheduleLogError` code and the same message, and the stored `scheduledAt` profile is unchanged, so no durable record and no recorded snapshot is re-recorded. Schedule's suites assert error codes and a few unrelated message substrings, so the assertion this change adds to `packages/schedule/schedule/tests/domain.spec.ts` is the only place that pins the three local-calendar message strings. `packages/util/time/tests/instant.spec.ts` covers the new entries directly, per-file 100% included.
@@ -38,13 +38,13 @@ Observable behavior does not change. Every accepted input resolves to the same e
**Hold the span in nanoseconds.** `Temporal` carries nanoseconds internally as a `bigint`, so a nanosecond span is representable. It was rejected for the same reason the boundaries stay in milliseconds: the count would have to be a `bigint`, which no `number` arithmetic, no `Date`, and no `JSON.stringify` accepts, so every consumer and every durable record would need a string or binary hop to move it. A nanosecond span as a `number` would instead be quantized to roughly 256 ns at the current epoch, which is a different value wearing the same name.
**Add formatting and DST arithmetic for other consumers too.** No current consumer needs model-facing formatting or offset arithmetic from this library: `dsh-time-context` owns its own formatter and zone policy text. Adding them now would give the library a public operation set with no consumer evidence, against the package rule that ties each abstraction to a current owner and need.
**Share model-facing formatting too.** `dsh-time-context` owns its formatter and zone policy text. Moving that presentation into the library would add consumer policy to the shared arithmetic, against the package rule that ties each abstraction to a current owner and need.
## Consequences
The time profile now has one home. A change to the accepted instant profile or the zone profile is made once and covered once, and a new boundary imports it instead of copying it. Schedule's `domain.ts` loses the five regular expressions that spelled the instant, calendar-date, local-time, and zone profiles, the `Intl` canonicalization, and the hand-rolled calendar conversion. It still imports the library's two bound constants for its date arithmetic at the UTC edges and delegates every representability guard to `isFourDigitYearInstant`, so the range itself has one owner.
The cost is a wider published surface and a heavier dependency graph: `dsh-util-time` gains a runtime dependency on `@js-temporal/polyfill`, so a consumer that imports only `canonicalClientTimeZone` now loads that module too. The library is also no longer a pure "validation and canonicalization only" package, which its README and package description had to state and which its Known Limitations section now records. Schedule keeps a thin mapping layer at each entry. The local-calendar and instant mappers switch over the library's closed reason unions with a `never` default, so a newly added reason fails the build there instead of silently taking a catch-all. The zone mapper instead branches only on `not_canonical_shape` — a reason the current IANA data never produces, so it is guarded by a `v8 ignore` rather than a `never` default — and folds `blank`, `padded`, `not_area_location`, and `unsupported` into one diagnostic. A reason added to `TimeZoneRejection` later would therefore compile there and silently take that generic message.
The cost is a wider published surface and a heavier dependency graph: the root entry resolves `@js-temporal/polyfill` and `rrule` even for consumers that only need a zone or span. The README records that cost and the bounded recurrence profiles. Schedule keeps a thin mapping layer at each entry. The local-calendar and instant mappers switch over the library's closed reason unions with a `never` default, so a newly added reason fails the build there instead of silently taking a catch-all. The zone mapper instead branches only on `not_canonical_shape` — a reason the current IANA data never produces, so it is guarded by a `v8 ignore` rather than a `never` default — and folds `blank`, `padded`, `not_area_location`, and `unsupported` into one diagnostic. A reason added to `TimeZoneRejection` later would therefore compile there and silently take that generic message.
A value type publishes what its consumers call, so the span exposes the constructor and the split that `dsh-time-context` uses and nothing else. A conversion, an arithmetic operation, or a `zero` identity lands with the caller that needs it; the seconds-valued fields at `dsh-schedule` and the DeepSeek file store are the spans that will ask for a second-unit conversion, and the bound and the freeze the class already carries are what make that addition a small one. The package README records the un-migrated boundaries as a known limitation.
@@ -10,11 +10,11 @@ Status: implemented
## Decision
`dsh-util-time` 拥有各边界所接收的时间词汇。解析器位于 `packages/util/time/src/instant.ts`,时间段类型位于 `src/duration.ts`,`src/index.ts` 转出两者,并保留 `canonicalClientTimeZone` 作为既有协议边界消费的 `string | undefined` 包装。
`dsh-util-time` 拥有各边界所接收的时间词汇。严格时刻与时区解析器位于 `packages/util/time/src/instant.ts`,时间段类型位于 `src/duration.ts`,有界重复规则值位于 `src/cron.ts` / `src/rrule.ts`。`src/index.ts` 导出这些入口,并保留 `canonicalClientTimeZone` 作为既有协议边界消费的 `string | undefined` 包装。
本库回答带判别值的结果,不持有失败词汇。`parseOffsetInstant`、`fourDigitYearEpoch`、`parseLocalCalendar` 与 `canonicalizeTimeZone` 回答 `accepted` 分支,或回答携带稳定原因的 `rejected` 分支(`not_rfc3339`、`out_of_range`、`not_a_calendar_date`、`invalid_numeric_offset`、`malformed`、`blank`、`padded`、`not_area_location`、`unsupported`、`not_canonical_shape`)。每个调用方把原因映射成自己的错误码与诊断文案,因此本库从不撰写面向模型的文本。`formatFourDigitYearInstant` 返回规范时刻文本,越界时返回 `undefined`;`isFourDigitYearInstant` 回答某个纪元是否可表示;`resolveLocalInstant` 回答纪元毫秒,对落在前向跳变缺口内的墙钟值回答 `undefined`。两个严格解析器的原因词汇彼此对称:同一条件在两者中都叫 `out_of_range`,所以年份 0 被 `parseOffsetInstant` 与 `parseLocalCalendar` 同样拒绝;而 `resolveLocalInstant` 接受真实 `Temporal` 日期携带的任何年份,因为它的调用方需要在 UTC 边界推导本地日期。`LocalCalendarTime.year` 记录了这一分工。
Schedule 消费它,并保留全部域内决策。`canonicalizeTimeZone`、`decodeInstant`、`parseAtInput` 与 `futureInstant` 仍是 Schedule 的入口,保留既有错误码与诊断文案;它们现在调用共享解析器并映射其原因。Schedule 仍拥有持久记录结构、折叠与状态转移权威、抗注入的框架文本,以及全部重复规则运算:cron 引擎、每日与每周日期遍历、最近到期搜索,以及 Schedule 特有的决策时刻边界。
Schedule 消费它,并保留全部域内决策。`canonicalizeTimeZone`、`decodeInstant`、`parseAtInput` 与 `futureInstant` 仍是 Schedule 的入口,保留既有错误码与诊断文案;它们调用共享解析器并映射其原因。纯 Cron/RRULE 解析与有界发生时点搜索归本库所有,接收显式参考时间,并通过 `fourDigitYearEpoch` 校验已提交时点。Schedule 拥有持久记录、折叠与状态转移权威、抗注入的框架文本、每日与每周日期遍历、投递限制及领域错误。共享值不拥有任务状态、墙钟或定时器。
`dsh-time-context` 同样消费它,并保留自己更严格的策略。`deriveBrowserTimeZoneContext` 通过 `canonicalizeTimeZone` 读取每条 user-rpc 时区,并拒绝规范答案与所报值不同的别名,因此请求边界仍然要求另一个进程能解析回同一名称。三条拒绝与它们的文案归 time-context 所有;共享入口只回答规范名称或一个原因;被拒绝的结果中只有 `unsupported` 会携带本库捕获到的平台错误,使该拒绝在两个消费方都保留其 `cause`。
@@ -22,7 +22,7 @@ Schedule 消费它,并保留全部域内决策。`canonicalizeTimeZone`、`dec
最后一份规范时刻剖面副本也一并迁移。`packages/session/session-format-v0-to-v1/src/payload-validation.ts` 原本用自己的一段正则与往返比对校验持久记录里的调度时刻;共享的 `fourDigitYearEpoch` 拥有同一剖面,因此该迁移改为询问它,并保留自己的错误类型与文案。由于这个迁移要读取已发布的日志,其接受集是兼容性承诺:`packages/session/session-format-v0-to-v1/tests/validation.spec.ts` 接受四位年份两端,并拒绝只靠正则会放进来的每种拼写(`0000`、`2026-02-30`、四位小数、数字偏移、首尾空白)。
受维护的 `@js-temporal/polyfill` 0.5.1 在成为 `dsh-schedule` 运行时依赖之外,也成为 `dsh-util-time` 的运行时依赖。本地解析继续使用它,因此缺口与重叠仍由宿主 IANA 数据决定,也没有调用方需要自己实现偏移采样。`util/time` 自身的描述与 README 去掉了 `zero-dependency` 这一说法,因为它容易被误读:该组的约定禁止的是 harness 依赖(以便任何组都能依赖 util),并不禁止外部包;因此 `packages/README.md` 与 `AGENTS.md` 里的 `util/` 一行保留该说法,而 `packages/util/README.md` 与本包自己的描述写明它加载了什么。
受维护的 `@js-temporal/polyfill` 0.5.1 是 `dsh-util-time` 与 `dsh-schedule` 的运行时依赖;`rrule` 在本库的严格语法与成本检查之后提供 RRULE 枚举。本地解析使用 Temporal,因此缺口与重叠由宿主 IANA 数据决定,也没有调用方需要自己实现偏移采样。util 组的约定禁止 harness 依赖,以便任何组都能依赖 util;它允许外部包,本包自己的描述与 README 写明了这些依赖。
可观察行为不变。每个被接受的输入解析出相同的纪元值,每个被拒绝的输入抛出相同的 `ScheduleInputError` 或 `ScheduleLogError` 错误码与相同的消息,存储的 `scheduledAt` 剖面不变,因此没有持久记录或录制快照需要重录。Schedule 的测试断言错误码以及少量无关的消息子串,因此本次在 `packages/schedule/schedule/tests/domain.spec.ts` 新增的断言是唯一固定这三条本地日历消息的地方。`packages/util/time/tests/instant.spec.ts` 直接覆盖新增入口,包含逐文件 100% 覆盖。
@@ -38,13 +38,13 @@ Schedule 消费它,并保留全部域内决策。`canonicalizeTimeZone`、`dec
**用纳秒保存时间段。** `Temporal` 内部以 `bigint` 携带纳秒,所以纳秒时间段是可表示的。之所以否决,理由与边界保持毫秒相同:计数必须变成 `bigint`,而 `number` 算术、`Date` 与 `JSON.stringify` 都不接受它,于是每个消费方与每条持久记录都要多一次字符串或二进制转换。若改用 `number` 表示纳秒,在当前离线纪元量级上它会被量化到约 256 ns,那是同一个名字下的另一个值。
**同时为其他消费方加入格式化与 DST 运算。** 目前没有消费方需要本库提供面向模型的格式化或偏移运算:`dsh-time-context` 自己拥有格式化器与时区策略文本。现在加入只会给本库一个没有消费方证据的公开操作集合,违背"每项抽象都绑定当前所有者与需求"的包规则。
**同时共享面向模型的格式化。** `dsh-time-context` 拥有自己的格式化器与时区策略文本。把这些呈现迁入本库会向共享算术加入消费方策略,违背“每项抽象都绑定当前所有者与需求”的包规则。
## Consequences
时间剖面现在只有一个归宿。时刻剖面或时区剖面的变更只做一次、只覆盖一次,新边界导入它而不是复制它。Schedule 的 `domain.ts` 去掉了为时刻、日历日期、本地时间与时区剖面命名的五段正则、`Intl` 规范化与手写日历换算。它仍然导入本库的两个边界常量用于其 UTC 边界日期运算,并把每处可表示性守卫委托给 `isFourDigitYearInstant`,因此范围本身只有一个所有者。
代价是更宽的已发布接口与更重的依赖图:`dsh-util-time` 增加了对 `@js-temporal/polyfill` 的运行时依赖,因此只导入 `canonicalClientTimeZone` 的消费方也会加载该模块。本库也不再是纯"只做校验与规范化"的包——其 README 与包描述必须说明这一点,其"已知限制"一节现在也记录了它。Schedule 在每个入口保留一层薄映射。本地日历与时刻映射函数以 `never` 默认分支穷尽 switch 本库的封闭原因联合,因此新增原因会让这两个调用点构建失败,而不是静默走兜底分支。时区映射函数则只对 `not_canonical_shape` 分支——当前 IANA 数据不会产生该原因,因此它由 `v8 ignore` 守护而不是 `never` 默认分支——并把 `blank`、`padded`、`not_area_location` 与 `unsupported` 合并成一条诊断。因此日后给 `TimeZoneRejection` 新增原因时会通过编译并静默套用那条通用文案。
代价是更宽的已发布接口与更重的依赖图:根入口会解析 `@js-temporal/polyfill` 和 `rrule`,只需要时区或跨度的消费方也一样。README 记录了这些成本与有界重复规则子集。Schedule 在每个入口保留一层薄映射。本地日历与时刻映射函数以 `never` 默认分支穷尽 switch 本库的封闭原因联合,因此新增原因会让这两个调用点构建失败,而不是静默走兜底分支。时区映射函数则只对 `not_canonical_shape` 分支——当前 IANA 数据不会产生该原因,因此它由 `v8 ignore` 守护而不是 `never` 默认分支——并把 `blank`、`padded`、`not_area_location` 与 `unsupported` 合并成一条诊断。因此日后给 `TimeZoneRejection` 新增原因时会通过编译并静默套用那条通用文案。
值类型只发布消费方会调用的部分,因此这个时间段只暴露 `dsh-time-context` 用到的构造入口与拆解,其余不发布。某个换算、某个算术运算或 `zero` 恒等元都随第一个需要它的调用方落地;`dsh-schedule` 与 DeepSeek 文件存储里以秒为单位的字段就是将来会要求第二个单位换算的那些跨度,而类已经携带的范围约束与冻结正是让这次添加保持很小的原因。包 README 把这些尚未迁移的边界记为已知限制。
@@ -9,8 +9,8 @@
en: 386864a282e5ad58
zh: a4ea75ec70eb7778
/agent-note-shared-native-cua-process-with-agent-scoped-state/decision:
en: 11bab6870c3d802f
zh: a22114f2217a3a41
en: 60ec2276ed0bff33
zh: 5fb48cc787194254
/agent-note-shared-native-cua-process-with-agent-scoped-state/alternatives-considered:
en: 883f71387f599160
zh: 4b5f1450be799397
@@ -12,7 +12,7 @@ On macOS 26.6.2, replayd derives capture-client identity from the executable pat
One user shares a native service per executable content digest across Agents and Hosts. A version lock elects the service; an additional canonical-executable-path lock prevents different builds published at one path from running together. Locks remain held until process exit. The managed startup command hands service lifetime to its client connections, and the last connection's completed cleanup ends the service. The diagnostic stdin/stdout entry proxies to this same service.
The service runs as a signed application launched through LaunchServices. Directly spawning the executable inherits its launcher’s TCC responsibility, so it cannot give the helper independent system permissions. A launch lock serializes discovery and startup. Complete signed applications use immutable content-addressed directories: replacing an executable used by a live process would violate both path-lock ownership and code-signature integrity. Ad-hoc builds can require fresh grants when their code hash changes; a stable bundle identifier alone does not preserve those grants. Installed packages retain their distributed signatures.
The service runs as a signed application launched through LaunchServices. Directly spawning the executable inherits its launcher’s TCC responsibility, so it cannot give the helper independent system permissions. A launch lock serializes discovery and startup. Complete signed applications use immutable content-addressed directories: replacing an executable used by a live process would violate both path-lock ownership and code-signature integrity. Ad-hoc builds can require fresh grants when their code hash changes; a stable bundle identifier alone does not preserve those grants. Source checkouts can keep grants with the [development signing identity](2026-10-29-cua-dev-signing-identity.md). Installed packages retain their distributed signatures.
Each Agent connection owns its application handles, AX snapshots, limits and cancellation state. An app handle grants no exclusive use of the application. A FIFO orders complete desktop operations, including clipboard transactions, while response writes are independent. Agent disposal retires only its connection. The [object API decision](2026-09-17-experimental-cua-object-api.md) retains evaluator independence and coordinate semantics; the [consent decision](2026-09-26-cua-application-consent.md) retains application authorization.
@@ -12,7 +12,7 @@ Status: implemented
同一用户的多个 Agent 和 Host 按可执行文件内容摘要共享原生服务。版本锁选出服务实例,规范化可执行路径上的另一把锁阻止发布在同一路径的不同构建同时运行。进程退出才释放锁。受管启动命令将服务生命期交给客户端连接,最后一条连接清理完成后服务退出。诊断用 stdin/stdout 入口也代理到同一个服务。
服务通过 LaunchServices 以已签名应用运行。直接启动可执行文件会继承启动器的 TCC 责任,无法让 helper 独立取得系统权限。启动锁串行处理发现与启动。完整签名应用保存在不可变的内容寻址目录中,替换运行中进程的可执行文件会破坏路径锁的归属与代码签名完整性。临时签名构建的代码哈希变化后可能需要重新授权,固定 Bundle ID 不能保证保留授权。安装版保留分发时的签名。
服务通过 LaunchServices 以已签名应用运行。直接启动可执行文件会继承启动器的 TCC 责任,无法让 helper 独立取得系统权限。启动锁串行处理发现与启动。完整签名应用保存在不可变的内容寻址目录中,替换运行中进程的可执行文件会破坏路径锁的归属与代码签名完整性。临时签名构建的代码哈希变化后可能需要重新授权,固定 Bundle ID 不能保证保留授权。源码检出可以用[开发签名身份](2026-10-29-cua-dev-signing-identity.zh.md)保留授权。安装版保留分发时的签名。
每条 Agent 连接独立持有应用句柄、AX 快照、资源限制和取消状态。持有 app 句柄不代表独占应用。FIFO 队列串行执行包含剪贴板事务的完整桌面操作,响应写入独立进行。Agent 销毁只释放自己的连接。[对象接口决策](2026-09-17-experimental-cua-object-api.zh.md)继续负责执行器独立性及坐标语义,[授权决策](2026-09-26-cua-application-consent.zh.md)继续负责应用许可。
@@ -9,11 +9,11 @@
en: 47ba548b869be190
zh: c2e5038a56fe741c
/agent-note-asynchronous-projection-registration-and-retained-readers/decision:
en: be0d17bfbd1b59d4
zh: 7a23bccdf95c21ff
en: a6f9ae8a53af5e4b
zh: 0aece791574ef060
/agent-note-asynchronous-projection-registration-and-retained-readers/alternatives-considered:
en: e7c66403fab991d2
zh: 1580ec29116c7d42
en: 12e0f5fc9638acc6
zh: 24213550da27b70b
/agent-note-asynchronous-projection-registration-and-retained-readers/consequences:
en: 31ce6f02dd3bf39d
zh: bcd29b2146f689b5
en: f5c294608d87844f
zh: 24488e439a04d81a
@@ -12,7 +12,7 @@ Host readers coupled to registry key lookup lose access when a plugin unloads, a
`SessionProjectionRegistry.register` returns an awaitable registration for a typed handle. Domain owners await it during activation before exposing dependent operations. Function plugins retain their namespace exports and use asynchronous `apply`; services use their initialization hook. Each domain service keeps its definitions and handles internal and exposes methods returning business data. Consumers depend on that service rather than registering its definitions or receiving projection readers. This supersedes the optional first-access acquisition and disposer-return details in [the mandatory-reader decision](2026-08-19-session-projection-mandatory-seam.md), while retaining its mandatory-state and cropped-client-view rules.
A handle owns one reference to a shared definition and its per-Session cells. Its disposer releases global participation exactly once; the final release removes the unit from live drives, snapshots, and checkpoints. Its reader remains usable after release, folding unseen events with the original definition. A later registration at a different state version does not replace an older handle's definition. Registration checks owner liveness after its asynchronous handoff and rejects if the owner unloaded before receiving the handle.
A handle owns one reference to a shared definition and its per-Session cells. Its disposer releases global participation exactly once; the final release removes the unit from live drives and snapshots. Checkpoints keep each Session's last cell of the released unit until its key registers again: the registry no longer advances that cell, while a retained handle read still advances it forward, so a writer that runs after the owner unloads still stores the state already folded. Under the Cordis rc.11 teardown order, Web-layer plugins release their units before the Sessions they describe detach, and the final projection-cache write would otherwise drop those rows. Stored rows can therefore lag the log; the cache promises only possibly stale rows. Its reader remains usable after release, folding unseen events with the original definition. A later registration at a different state version does not replace an older handle's definition. Registration checks owner liveness after its asynchronous handoff and rejects if the owner unloaded before receiving the handle.
AgentRegistry owns turn and step state and exposes `turnBoundaryOf(session)`. AgentLoop acquires its inbox handle once before publishing its factory, reads the last turn before creating an Agent scope, and passes an inbox callback plus that turn number into synchronous Agent construction. The callback retains its handle through cleanup after the loop fiber removes global registrations. This preserves the existing creation/setup sequence without per-Agent registration or an extra initialization state.
@@ -20,6 +20,10 @@ Plugins compiled against the synchronous API keep running during migration. The
## Alternatives considered
**Detach owned Sessions before unregistering.** Other Web-layer plugins unload in earlier fibers under the rc.11 teardown order, and plugin reload or disable reaches the same timing, so ordering one owner cannot keep every row.
**Checkpoint a released unit only while its cell is at the Session cursor.** A Session with a running turn at shutdown appends teardown events after its units are released, so its rows would be dropped again.
**Keep registry-level lookup and delay unregistering.** This couples unrelated plugin disposal order to every reader's final access. Retained readers make cleanup independent of that order.
**Return a synchronous handle.** This preserves callers' immediate reads but commits the acquisition API to synchronous initialization. Awaited acquisition leaves that timing explicit while state reads remain synchronous after readiness.
@@ -36,4 +40,4 @@ Plugins compiled against the synchronous API keep running during migration. The
## Consequences
Projection owners must migrate acquisition and retain handles; other consumers use the owning services. Released handles retain their definitions and weakly keyed cells for as long as the caller keeps them; they do not restore global wire participation. Registration-reader tests cover post-release folding, version replacement, unrelated-unit isolation, repeated disposal, concurrent owner disposal, and owner unload during acquisition. Agent lifecycle tests cover cleanup failures, scope-minting races, and background child shutdown. Compatibility tests and a real Loader subprocess verify the deprecated calls.
Projection owners must migrate acquisition and retain handles; other consumers use the owning services. Released handles retain their definitions and weakly keyed cells for as long as the caller keeps them; they do not restore global wire participation. The registry also retains a released predecessor, with its definition closures and weakly keyed cells, until the same key registers again, so a key that a reloaded plugin renames or stops registering keeps the old definition referenced for the process lifetime. Registration-reader tests cover post-release folding, version replacement, unrelated-unit isolation, repeated disposal, concurrent owner disposal, and owner unload during acquisition. Agent lifecycle tests cover cleanup failures, scope-minting races, and background child shutdown. Compatibility tests and a real Loader subprocess verify the deprecated calls.
@@ -12,7 +12,7 @@ Status: implemented
`SessionProjectionRegistry.register` 返回可等待的注册结果,用于获取类型化句柄。领域所有者在激活期间等待它,再提供依赖操作。函数插件保留命名空间导出并使用异步 `apply`;服务使用初始化钩子。领域服务在内部持有定义和句柄,通过方法返回业务数据。消费方依赖该服务,无需注册其定义或接收投影读取句柄。这取代了[必需读取方决策](2026-08-19-session-projection-mandatory-seam.zh.md)中的可选首次访问获取方式与返回释放函数的细节,同时保留必需状态和裁剪 Client 视图的规则。
句柄拥有共享定义及其逐 Session 单元的一次引用。其释放函数恰好一次地解除全局参与;最后一次释放会从实时推进、快照和检查点中移除单元。读取句柄在释放后仍可使用原定义折叠未读取事件。后续注册的其他状态版本不会替换旧句柄的定义。注册在异步交接后检查 owner 是否存活;如果 owner 在收到句柄前卸载,注册拒绝。
句柄拥有共享定义及其逐 Session 单元的一次引用。其释放函数恰好一次地解除全局参与;最后一次释放会从实时推进和快照中移除单元。检查点仍保留每个 Session 中已释放单元的最后一个 cell,直到它的 key 再次注册:注册表不再推进这个 cell,但已保留的 handle 读取时仍会将其向前推进,因此 owner 卸载后才运行的写入方仍会存下已经折叠出的状态。按 Cordis rc.11 的卸载顺序,Web 层插件会在它们所描述的 Session detach 之前释放单元,否则 projection cache 的最终写入会丢掉这些行。因此存储的行可能落后于日志;cache 只承诺行可能过时。读取句柄在释放后仍可使用原定义折叠未读取事件。后续注册的其他状态版本不会替换旧句柄的定义。注册在异步交接后检查 owner 是否存活;如果 owner 在收到句柄前卸载,注册拒绝。
AgentRegistry 持有轮次和步骤状态,并提供 `turnBoundaryOf(session)`。AgentLoop 在发布工厂前只获取一次 inbox 句柄,在创建 Agent 作用域前读取最后轮次,并将 inbox 回调与该轮次编号传入同步 Agent 构造过程。loop fiber 移除全局注册后,回调仍在清理期间保留其句柄。这保留既有创建与 setup 顺序,无需逐 Agent 注册或增加初始化状态。
@@ -20,6 +20,10 @@ AgentRegistry 持有轮次和步骤状态,并提供 `turnBoundaryOf(session)`
## Alternatives considered
**卸载前先 detach 自己拥有的 Session。** 按 rc.11 的卸载顺序,其他 Web 层插件在更早卸载的 fiber 里,插件重载或禁用也会碰上同样的时机,调整单个 owner 的顺序保不住所有行。
**只在已释放单元的 cell 位于 Session 游标时才写入检查点。** 关停时正在运行一轮的会话会在单元释放后追加 teardown 事件,因此它的这些行会再次被丢掉。
**保留注册表级查询并延迟注销。** 这使无关插件的释放顺序依赖每个读取方的最后一次访问。保留读取句柄使清理不再依赖该顺序。
**返回同步句柄。** 这保留消费者的立即读取,却将获取 API 固定为同步初始化。等待获取过程使时序显式,同时就绪后的状态读取仍同步。
@@ -36,4 +40,4 @@ AgentRegistry 持有轮次和步骤状态,并提供 `turnBoundaryOf(session)`
## Consequences
投影所有者必须迁移获取方式并保留句柄;其他消费方使用所属服务。只要调用方持有已释放句柄,其定义和弱键单元就仍存在;句柄不会恢复全局 wire 参与。注册读取测试覆盖释放后折叠、版本替换、无关单元隔离、重复释放、并发 owner 释放以及获取期间 owner 卸载。Agent 生命周期测试覆盖清理失败、作用域创建竞态与后台子 Agent 停止。兼容测试与真实 Loader 子进程验证这些已弃用调用。
投影所有者必须迁移获取方式并保留句柄;其他消费方使用所属服务。只要调用方持有已释放句柄,其定义和弱键单元就仍存在;句柄不会恢复全局 wire 参与。注册表自身也会保留已释放的前任及其定义闭包和弱键单元,直到同一 key 再次注册;因此重载后改名或不再注册的 key 会在进程生命周期内一直引用旧定义。注册读取测试覆盖释放后折叠、版本替换、无关单元隔离、重复释放、并发 owner 释放以及获取期间 owner 卸载。Agent 生命周期测试覆盖清理失败、作用域创建竞态与后台子 Agent 停止。兼容测试与真实 Loader 子进程验证这些已弃用调用。
@@ -2,18 +2,18 @@
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
# After editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-10-13-team-identity-runtime-context.md
/agent-note-team-identity-in-required-runtime-context:
en: 8c88759320ae8481
zh: 4068b18723907eb9
/agent-note-team-identity-in-required-runtime-context/problem:
en: c9a81cb731ddf7b2
zh: 428c0e6f8937d7c5
/agent-note-team-identity-in-required-runtime-context/decision:
en: 6c73479973c1e378
zh: 98677c77f77f8d81
/agent-note-team-identity-in-required-runtime-context/alternatives-considered:
/agent-note-subagent-identity-in-required-runtime-context:
en: ce8f378b6e7b57fa
zh: 26bd0415b4dba94a
/agent-note-subagent-identity-in-required-runtime-context/problem:
en: f944254565541454
zh: 27fa9f67c671dec7
/agent-note-subagent-identity-in-required-runtime-context/decision:
en: 85100936f97a139c
zh: 614081943944e28b
/agent-note-subagent-identity-in-required-runtime-context/alternatives-considered:
en: 774566ffbf9e9db8
zh: ea761d876af462f2
/agent-note-team-identity-in-required-runtime-context/consequences:
en: 18d1aa67936f1227
zh: 7e06c1f51553ecdd
/agent-note-subagent-identity-in-required-runtime-context/consequences:
en: 2c01a24cd8cc4556
zh: 37b5d1ca1173318a
@@ -1,4 +1,4 @@
# Agent Note: Team identity in required runtime context
# Agent Note: Subagent identity in required runtime context
Status: implemented
@@ -6,15 +6,15 @@ English | [中文](2026-10-13-team-identity-runtime-context.zh.md)
## Problem
A teammate identity embedded in its initial task depends on ordinary history retention. Compaction can remove it, and an ordinary fork from that teammate can retain the old role even though the fork is an independent Lead. Identity must reflect current membership while keeping shared system instructions uniform.
A teammate identity embedded in its initial task depends on ordinary history retention. Compaction can remove it, and an ordinary fork from that teammate can retain the old role even though the fork is an independent Lead. Identity must reflect the current subagent tree while keeping shared system instructions uniform.
## Decision
`tool-agent-team` contributes `team:identity` through the Agent-scoped `systemPrompt.context()` registry with `required: true`. Its provider reads the existing Team membership and renders the member's role, name, and addressing instructions. Optional-context suppression cannot remove it. Fixed collaboration policy stays in the system prompt; roster and task state remain tool queries. Initial tasks contain only their supplied content.
`tool-subagent-control` contributes required `subagent:identity` through `systemPrompt.context()` in ordinary subagent and Team compositions. It derives identity from `ctx.subagents.rootOf(agent)`: the root sees its own Session id, and descendants see their own, direct parent, and root ids. An unavailable root is reported explicitly. Optional-context suppression and Team mode do not remove identity. Agents receive messaging and discovery guidance only while the root is available and each corresponding standard tool is admitted. Team task policy stays in the shared system prompt; task state remains a tool query.
The loop owns logging, retained-snapshot comparison, and re-admission after compaction. A changed role appends the current full runtime-context snapshot after inherited history; unchanged retained context adds no message. No Team-specific retention state, event type, or log migration is introduced. Existing recorded reminders remain historical messages.
This supersedes only the initial-task identity decision in [Agent Teams](../feature/2026-08-05-agent-teams.md#team-identity); its roster, mailbox, and task decisions remain active.
This supersedes only the initial-task identity decision in [Agent Teams](../feature/2026-08-05-agent-teams.md#team-identity). [Task-board wait coordination](2026-10-19-team-task-board-inbox.md) keeps ordinary inbox waiting independent of Team-only model guidance.
## Alternatives considered
@@ -28,4 +28,4 @@ This supersedes only the initial-task identity decision in [Agent Teams](../feat
Members pay for identity within the full runtime-context snapshot. Other context changes repeat it, while stable retained context adds no new message. Forks preserve inherited history and append current identity; actual provider cache hits remain best-effort. Existing Team authorization remains in tool execution.
Verification covers fresh and fork teammates, an independent fork from a teammate, optional-context suppression, compaction restoration, cold resume, and registration disposal. The recorded `team-targets` scenario pins Lead context; the keyless headless Team workflow pins teammate context through the shipped profile.
Verification covers fresh and fork children, an independent Session fork, optional-context suppression, compaction restoration, unavailable roots, cold resume, and registration disposal. Recorded ordinary subagent and Team workflows pin identity through their shipped compositions; delegated tasks retain final-result return guidance.
@@ -1,4 +1,4 @@
# Agent Note: 必需 runtime context 中的 Team 身份
# Agent Note: 必需 runtime context 中的 subagent 身份
Status: implemented
@@ -6,15 +6,15 @@ Status: implemented
## Problem
嵌入初始任务的 teammate 身份依赖普通历史保留。压缩可能移除它;从 teammate 创建的普通 fork 即使已成为独立 Lead,也可能保留旧角色。身份必须反映当前 membership,同时保持共享系统指令一致。
嵌入初始任务的 teammate 身份依赖普通历史保留。压缩可能移除它;从 teammate 创建的普通 fork 即使已成为独立 Lead,也可能保留旧角色。身份必须反映当前 subagent 树,同时保持共享系统指令一致。
## Decision
`tool-agent-team` 通过 Agent 作用域的 `systemPrompt.context()` 注册 `team:identity`,并设置 `required: true`。提供方读取现有 Team membership,渲染成员角色、名字与寻址说明。关闭可选 context 不能移除它。固定协作策略保留在 system prompt;roster 和 task 状态仍通过工具查询。初始任务只包含传入的正文。
`tool-subagent-control` 在普通 subagent 和 Team 组合中通过 `systemPrompt.context()` 提供必需的 `subagent:identity`。它从 `ctx.subagents.rootOf(agent)` 派生身份:根看到自身 Session id,后代看到自身、直接父级和根 id。根不可用时会明确报告。可选上下文抑制和 Team 模式都不会移除身份。只有根可用时,agent 才会分别根据对应标准工具的获准情况收到通信与发现指导。Team 任务策略保留在共享系统提示中;任务状态仍通过工具查询。
Loop 负责日志记录、保留快照比较和压缩后的重新接纳。角色变化会在继承历史之后追加当前完整 runtime-context 快照;保留的 context 未变化时不追加消息。不引入 Team 专属的保留状态、事件类型或日志迁移。已有身份提醒仍作为历史消息保留。
本决策仅取代 [Agent Teams](../feature/2026-08-05-agent-teams.zh.md#team-identity) 中的初始任务身份决策;其 roster、mailbox 和 task 决策仍然有效。
这仅取代 [Agent Teams](../feature/2026-08-05-agent-teams.zh.md#team-identity) 中将身份放入初始任务的决策。[任务板等待协调](2026-10-19-team-task-board-inbox.zh.md)使普通收件箱等待独立于 Team 专用模型指导。
## Alternatives considered
@@ -28,4 +28,4 @@ Loop 负责日志记录、保留快照比较和压缩后的重新接纳。角色
成员承担完整 runtime-context 快照中的身份 token 成本。其他 context 变化会重复身份,保留的 context 稳定时则不追加消息。Fork 保留继承历史并追加当前身份;提供方实际缓存命中仍为尽力而为。现有 Team 授权仍在工具执行时落实。
验证覆盖 fresh 和 fork teammate、从 teammate 创建的独立 fork、关闭可选 context、压缩后恢复、冷恢复和注册释放。录制的 `team-targets` 场景固定 Lead context;无密钥 headless Team 工作流通过内置 profile 固定 teammate context。
验证覆盖新建和 fork 子级、独立 Session fork、可选上下文抑制、压缩恢复、不可用根、冷恢复和注册释放。录制的普通 subagent 和 Team 工作流通过各自的分发组合固定身份行为;委派任务保留最终结果返回指导。
@@ -0,0 +1,22 @@
# Bilingual-pair consistency record for 2026-10-15-agent-teams-as-subagents.md (docs/i18n/README.md): per heading
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
# After editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-10-15-agent-teams-as-subagents.md
/agent-note-agent-teams-teammates-are-subagents:
en: 51e2fc51a6e1622a
zh: 7e09ea6292e5921d
/agent-note-agent-teams-teammates-are-subagents/problem:
en: ca2848e3bbc3a009
zh: 0551e32ff6d187d0
/agent-note-agent-teams-teammates-are-subagents/decision:
en: 2c29c42cc02a9a05
zh: bc542e3a27cfd03d
/agent-note-agent-teams-teammates-are-subagents/alternatives-considered:
en: ef0cf648ff54784f
zh: 2a0b3745a54e7072
/agent-note-agent-teams-teammates-are-subagents/consequences:
en: 3ff718671be076fa
zh: 38a367077ce95d52
/agent-note-agent-teams-teammates-are-subagents/testing:
en: a0ec8546a783bfd9
zh: 8920b46970e3a2de
@@ -0,0 +1,41 @@
# Agent Note: Agent Teams teammates are subagents
Status: implemented
English | [中文](2026-10-15-agent-teams-as-subagents.zh.md)
The adjacency and parent-lifetime restrictions are superseded by [tree-wide communication](2026-10-16-tree-wide-agent-communication.md). [Task-board inbox coordination](2026-10-19-team-task-board-inbox.md) supersedes the separate roster and Team-owned wait; the single subagent identity decision remains active.
## Problem
Teammates already use the subagent runtime for creation, Session identity, catalog membership, capacity, cold resume, settlement, and interruption. A separate Team roster duplicates identity, provisioning recovery, and teardown. Team-scoped messaging and control tools shadow the standard subagent tools, forcing profiles to disable ordinary delegation and making fork tool schemas diverge from their parent.
## Decision
Use one subagent identity: the Session id. The existing `description` tool argument supplies the catalog label and display text; descriptions may repeat and never address an agent. Creation accepts no member name, reserves no names, and adds no catalog payload version.
[Tree-wide communication](2026-10-16-tree-wide-agent-communication.md) authorizes an exact live sender within one root tree and preserves the actual sender’s id. Interruption retains ancestor authority and id addressing.
Every root Agent and its descendants form the Team identified by the root Session id. Team creates no agents and contributes task tools and task policy. Ordinary subagent controls supply required identity, discovery, messaging, interruption, and inbox waiting. Parent and fork children receive identical tool schemas and shared policies.
Task ownership uses `ownerId`; root reassignment accepts itself or any reachable continuable descendant. Historical roster records remain decode-only. Team persists task changes without ending inbox waits or starting idle turns; [inbox-only waiting](../simplification/2026-10-29-inbox-only-agent-wait.md) owns wake behavior. Historical `team/message/*` records remain readable but are never delivered or acknowledged.
This supersedes the roster, provisioning, recovery, and messaging decisions in [Agent Teams](../feature/2026-08-05-agent-teams.md) and the [Team direct inbox decision](../../archived/simplification/2026-09-26-team-direct-inbox.md). The task-board and shared-checkout decisions remain active. [Adjacent Agent messaging](2026-08-27-adjacent-agent-steer-messaging.md) includes sibling routing, while [required runtime-context identity](2026-10-13-team-identity-runtime-context.md) owns the role statement. The [upgrade guide](../../../../docs/upgrade-guide/v0.2.1-rc.2/agent-team-subagents/guide.md) lists removed tools, methods, and configuration.
## Alternatives considered
**Separate Team authority and tools.** This retains a second roster and recovery lifecycle for Agents already owned by the subagent runtime, plus same-name tools and a private delivery adapter. The shared parent relation supplies the required authority.
**Unique names as sibling-routing opt-in.** Names add validation, reservation, persistence, and ambiguous resolution beside existing Session ids. Descriptions already explain responsibilities; stable ids distinguish repeated descriptions without another identity system.
**Pluggable address books.** A registry would support groups spanning unrelated parents but add another authorization input. No current consumer needs this; revisit when collaboration must cross parent trees.
## Consequences
Tree members discover and message one another through ordinary controls. Team retains task ownership, dependencies, and task policy. Subagent capacity and lifecycle apply; failed starts create no Team roster row. Existing durable task generations remain unchanged.
Descendants at any depth share the board and can be task owners. Historical members grant no authority. Separate working directories remain explicit caller choices; cross-process task coordination is unsupported.
## Testing
Verification covers tree delivery and cold resume with actual sender attribution, task CAS and owner discovery, historical decoding, inbox wait isolation, and registration disposal. Recorded sessions cover Team policy and identity, plus ordinary subagent return guidance.
@@ -0,0 +1,41 @@
# Agent Note: Agent Teams 队友是子代理
Status: implemented
[English](2026-10-15-agent-teams-as-subagents.md) | 中文
邻接和父级生命周期限制由[整树通信](2026-10-16-tree-wide-agent-communication.zh.md)取代。[任务板收件箱协调](2026-10-19-team-task-board-inbox.zh.md)取代独立名册和 Team 所有的等待;单一 subagent 身份决策仍有效。
## Problem
队友已经使用子代理运行时完成创建、Session 身份、目录成员关系、容量、冷恢复、结算和中断。独立 Team 成员列表重复了身份、创建恢复和释放职责。Team 作用域内的消息及控制工具覆盖标准子代理工具,迫使 profile 禁用普通委派,并使分叉工具模式偏离父级。
## Decision
统一使用子代理的 Session id 作为身份。现有 `description` 工具参数提供目录标签和展示文本;描述可以重复,不用于寻址代理。创建不接收成员名称、不预留名称,也不增加目录载荷版本。
[整树通信](2026-10-16-tree-wide-agent-communication.zh.md)在同一根树内授权当前在线发送方,并保留实际发送方 id。中断保留祖先权限和 id 寻址。
每个根 Agent 及其后代组成以根 Session id 标识的 Team。Team 不创建 agent,提供任务工具和任务策略。普通 subagent 控制工具提供必需身份、发现、通信、中断和收件箱等待。父级和 fork 子级接收相同工具 schema 和共享策略。
任务归属使用 `ownerId`;根重新分配可指定自身或任意可达的可继续执行后代。历史名册记录仅供解码。Team 持久化任务变更,不结束收件箱等待,也不启动空闲轮次;[仅等待收件箱](../simplification/2026-10-29-inbox-only-agent-wait.zh.md)负责唤醒行为。历史 `team/message/*` 记录仍可读取,但绝不再投递或确认。
本决策取代 [Agent Teams](../feature/2026-08-05-agent-teams.zh.md) 的成员列表、创建、恢复和消息决策,以及 [Team 直接收件箱决策](../../archived/simplification/2026-09-26-team-direct-inbox.md)。任务板和共享工作树决策仍有效。[相邻 Agent 消息](2026-08-27-adjacent-agent-steer-messaging.zh.md)纳入同级路由,[必需运行时身份](2026-10-13-team-identity-runtime-context.zh.md)负责角色声明。[升级指南](../../../../docs/upgrade-guide/v0.2.1-rc.2/agent-team-subagents/guide.zh.md)列出移除的工具、方法和配置。
## Alternatives considered
**独立 Team 权限和工具。** 这会为子代理运行时已拥有的 Agent 保留第二套成员列表与恢复生命周期,以及同名工具和私有投递适配器。共同父级关系已经提供所需权限。
**以唯一名称选择性启用同级路由。** 名称会在现有 Session id 之外增加校验、预留、持久化和歧义解析。描述已能说明职责;稳定 id 可以区分重复描述,无需另一套身份系统。
**可插拔地址簿。** 注册表可支持跨不同父级的组,但会增加另一种授权输入。当前没有消费者需要此能力;协作必须跨父级树时再考虑。
## Consequences
树内成员通过普通控制工具发现和联系彼此。Team 保留任务归属、依赖和任务策略。容量和生命周期遵循 subagent 规则;启动失败不会创建 Team 名册行。现有持久化任务代次保持不变。
任意深度后代共享任务板,均可成为任务所有者。历史成员不授予权限。独立工作目录仍由调用者显式选择;不支持跨进程任务协调。
## Testing
验证覆盖具有实际发送方归属的整树投递和冷恢复、任务 CAS 与所有者发现、历史解码、收件箱等待隔离和注册释放。录制会话覆盖 Team 策略和身份,以及普通 subagent 返回指导。
@@ -0,0 +1,19 @@
# Bilingual-pair consistency record for 2026-10-16-tree-wide-agent-communication.md (docs/i18n/README.md): per heading
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
# After editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-10-16-tree-wide-agent-communication.md
/agent-note-communication-follows-the-subagent-tree:
en: 849d806b901e9f04
zh: 649b2984f844d510
/agent-note-communication-follows-the-subagent-tree/problem:
en: c583c3cdb894e417
zh: 6edc0354b79f4f26
/agent-note-communication-follows-the-subagent-tree/decision:
en: da35a75e069fa1e4
zh: 7edf62ad66ab04ec
/agent-note-communication-follows-the-subagent-tree/alternatives-considered:
en: 470f9cf8c3e32af9
zh: 73bbe29767c5d02a
/agent-note-communication-follows-the-subagent-tree/consequences:
en: 6d409833d762d473
zh: 000cd93577ec6b34
@@ -0,0 +1,29 @@
# Agent Note: Communication follows the subagent tree
Status: implemented
English | [中文](2026-10-16-tree-wide-agent-communication.zh.md)
## Problem
A Team shares one task board across the root's entire subagent tree. Restricting discovery and messages to adjacent relatives forces deeper members to relay work through parents despite sharing tasks.
## Decision
`list_agents` starts at the caller's live root and lists that root and all reachable continuable descendants, including the caller. Parent ids and depth remain relative to the root. The existing parent catalogs remain authoritative; no Team directory is stored separately.
`send_message` accepts another root or continuable member of the same tree. The sender must be the exact live root or a resident continuable subagent. Cross-branch delivery verifies the target descriptor and creation catalogs, then restores only the target under the Team root. Final inbox admission rechecks the sender and root. Cancellation or rejection before acceptance releases the new target instance; accepted input follows normal Agent persistence.
The existing Agent registry records runtime ownership by the Team root. Creation parent ids and catalogs preserve ancestry for discovery and interrupt authority. A member finishing or being released affects only its own instance; Team-root shutdown joins all startup and cleanup. This removes ancestor restoration, parent retention, and recursive disposal without adding a Team runtime registry. Saved target presets and permission events supply restoration configuration.
This supersedes the adjacency, discovery, and parent-lifetime restrictions in [adjacent messaging](2026-08-27-adjacent-agent-steer-messaging.md) and [subagent teammates](2026-10-15-agent-teams-as-subagents.md). Their shared inbox, scheduling, attribution, and Team reuse decisions remain active. Members use Session ids for addressing; ordinary Session forks lead independent Teams.
## Alternatives considered
**Relay through parents.** This preserves the narrower routing implementation but makes the creation hierarchy determine who can collaborate. Shared work requires direct communication across branches.
**Separate Team directory and delivery.** A second member list and message path would duplicate the existing catalogs, inbox, and restoration lifecycle. Tree membership supplies the required communication scope without another registry.
## Consequences
Discovery output grows with the whole tree. Cold delivery requires one capacity slot and readable creation history. Intermediate creators may be offline. Completion notices reach only online creators, so work that requires descendant results must explicitly wait. Listing is a snapshot and does not promise that a later send succeeds. External and one-shot executions cannot receive follow-ups.
@@ -0,0 +1,29 @@
# Agent Note: 通信范围由子代理树决定
Status: implemented
[English](2026-10-16-tree-wide-agent-communication.md) | 中文
## Problem
一个 Team 在根的整棵子代理树中共享任务板。把发现和消息限定在相邻成员之间,会迫使深层成员在共享任务的同时,仍通过父级转发工作。
## Decision
`list_agents` 从调用方的在线根开始,列出该根与所有可达的可继续后代,包括调用方。父级 id 和深度始终相对根计算。现有父级目录仍是权威来源;不单独存储 Team 目录。
`send_message` 接受同一棵树中的其他根或可继续成员。发送者必须是确切在线根或驻留的可继续子代理。跨分支投递验证目标描述符和创建目录后,仅在 Team 根下恢复目标。最终收件箱准入重新检查发送者与根。接受前的取消或拒绝释放新建目标实例;已接受输入遵循普通 Agent 持久化。
现有 Agent 注册表记录 Team 根对运行时实例的所有权。创建父级 id 和目录保留用于发现及中断授权的祖先关系。成员完成或释放只影响自身实例;关闭 Team 根会等待全部启动和清理。这删除了祖先恢复、父级保留和递归释放,无需增加 Team 运行时注册表。目标保存的 preset 和权限事件提供恢复配置。
本决策替代[相邻消息](2026-08-27-adjacent-agent-steer-messaging.zh.md)与[子代理队友](2026-10-15-agent-teams-as-subagents.zh.md)中的邻接、发现及父子生命周期限制。它们的共享收件箱、调度、发送者归属和 Team 复用决策仍有效。成员使用 Session id 寻址;普通 Session 分叉领导独立 Team。
## Alternatives considered
**通过父级转发。** 这保留了更窄的路由实现,却让创建层级决定谁能协作。共享工作需要跨分支直接通信。
**单独的 Team 目录与投递。** 第二份成员列表和消息路径会重复现有目录、收件箱与恢复生命周期。树成员身份已经提供所需通信范围,无需另一个注册表。
## Consequences
发现输出随整棵树增长。冷投递需要一个容量名额和可读的创建历史。中间创建者可以离线。完成通知仅投递给在线创建者,因此依赖后代结果的工作必须显式等待。列表是快照,不保证后续发送成功。外部执行和一次性执行不能接收后续消息。
@@ -9,8 +9,8 @@
en: 9f84c9bd85b6aace
zh: 8ebb10962c219624
/agent-note-serve-session-list-rows-from-a-summary-key-set/decision:
en: 6e8f43d7a53c35fd
zh: a6e2bccc20474515
en: aada3ca481120bff
zh: 1fa45a276a47e921
/agent-note-serve-session-list-rows-from-a-summary-key-set/alternatives-considered:
en: ce8320f1032018ed
zh: 4c5fad5e605bf524
@@ -13,10 +13,10 @@ A Session-list row carries every registered client view. Four of those views are
The Session summary is every registered client view except the keys whose `SessionProjectionMap` entry carries the summary-exclusion mark.
- [`packages/session/session-projection/src/types.ts`](../../../../packages/session/session-projection/src/types.ts) declares the type-only mark `SessionProjectionSummaryExcluded<T>` over a unique symbol that is never exported and never present at runtime. `SessionProjectionViewValue<T>` strips it distributively, so a nullable entry keeps its `null`; `SessionProjectionSummaryExcludedKey` tests the non-nullish part; `SessionProjectionSummaryValues` is every view value minus those keys.
- Each owning package wraps only its own map entry: `turnOutline` (session-turn-outline), `sessionStats` (session-stats), `todos` (tool-todo), and `imageLimits` (session-controller's boot-constant attachment limits, which every Session shares). The todo entry is `SessionProjectionSummaryExcluded<TodoItem[]> | null`, because an intersection with `null` would absorb the null member.
- Each owning package wraps only its own map entry: `turnOutline` (session-turn-outline), `sessionStats` (session-stats), `todos` (tool-todo), `imageLimits` (session-controller's boot-constant attachment limits, which every Session shares), and `contextPressure` and `contextBreakdown` (token-meter). The todo entry is `SessionProjectionSummaryExcluded<TodoItem[]> | null`, because an intersection with `null` would absorb the null member.
- Registration carries the same decision as a plain boolean: `wire.summary` says whether a Session summary includes the unit, and omitting it means `true`. A key whose map entry carries the mark declares `false` so the runtime key set and the client view type agree. The mark itself has no runtime representation — `wire.view` and `viewSchema` are typed with `SessionProjectionViewValue`, which strips it, so the erase in `register` only widens the state type.
- [`packages/session/session-projection/src/index.ts`](../../../../packages/session/session-projection/src/index.ts) exposes `summaryKeys(): ReadonlySet<key>`, derived from the live registrations and rebuilt only when a new key registers or a key's last registrant disposes. `cachedSnapshot`, `viewCheckpoint`, and `snapshot` accept `ProjectionKeySelection` (`readonly Key[] | ReadonlySet<Key>`) and use a passed set as-is.
- [`packages/session/session-projection-cache/src/index.ts`](../../../../packages/session/session-projection-cache/src/index.ts) forwards that selection unchanged, and [`packages/api/session-controller/src/list.ts`](../../../../packages/api/session-controller/src/list.ts) passes `summaryKeys()` to the live and cold cached faces. `cachedPredecessorTitle` keeps its title-only read.
- [`packages/session/session-projection-cache/src/index.ts`](../../../../packages/session/session-projection-cache/src/index.ts) forwards that selection unchanged, and [`packages/api/session-controller/src/list.ts`](../../../../packages/api/session-controller/src/list.ts) passes `summaryKeys()` to the live registry's `snapshot` and to the cold cache read. `cachedPredecessorTitle` keeps its title-only read.
- Client types only: `SessionSummary.projectionValues` narrows to `Partial<SessionProjectionSummaryValues>`, while `SessionProjectionSnapshot.values` keeps every view value. No wire field, function, or UI name changes, and an opened Session still receives every key through `session/follow`, the control baseline, and `session.projections`.
Estimated effect: with logs whose outlines dominate the projection payload, the four keys are estimated at roughly two thirds of a Session-list response for a few hundred Sessions — the outline alone near 57%, the figures and todo list near 5% together, and the shared image limits near 5% — and at roughly 85% of the same response after transport compression, where the outline's unique text barely compresses while the remaining keys do.
@@ -13,10 +13,10 @@ Session 列表行携带每个已注册的客户端视图。其中四个视图从
Session 摘要就是每个已注册客户端视图,减去 `SessionProjectionMap` 条目带摘要排除标记的键。
- [`packages/session/session-projection/src/types.ts`](../../../../packages/session/session-projection/src/types.ts) 声明类型层标记 `SessionProjectionSummaryExcluded<T>`,它基于一个从不导出、运行时从不存在的 unique symbol。`SessionProjectionViewValue<T>` 以分发方式去掉标记,可空条目因此保留 `null`;`SessionProjectionSummaryExcludedKey` 取非空部分判断;`SessionProjectionSummaryValues` 是全部视图值减去这些键。
- 每个 owning package 只包装自己的 map 条目:`turnOutline`(session-turn-outline)、`sessionStats`(session-stats)、`todos`(tool-todo),以及 `imageLimits`(session-controller 的启动常量附件上限,每个 Session 共享)。todo 条目写作 `SessionProjectionSummaryExcluded<TodoItem[]> | null`,因为与 `null` 求交会吞掉 null 成员。
- 每个 owning package 只包装自己的 map 条目:`turnOutline`(session-turn-outline)、`sessionStats`(session-stats)、`todos`(tool-todo)、`imageLimits`(session-controller 的启动常量附件上限,每个 Session 共享),以及 `contextPressure` 和 `contextBreakdown`(token-meter)。todo 条目写作 `SessionProjectionSummaryExcluded<TodoItem[]> | null`,因为与 `null` 求交会吞掉 null 成员。
- 注册用普通布尔值承载同一决定:`wire.summary` 表示 Session 摘要是否包含该单元,省略即 `true`。map 条目带标记的 key 声明 `false`,让运行时键集合与客户端视图类型一致。标记本身没有运行时表示 —— `wire.view` 与 `viewSchema` 的签名已用 `SessionProjectionViewValue` 去标记,因此 `register` 里的擦除只是放宽状态类型。
- [`packages/session/session-projection/src/index.ts`](../../../../packages/session/session-projection/src/index.ts) 提供 `summaryKeys(): ReadonlySet<key>`,从实时注册派生,只有新键注册或某键最后一个注册者注销时才重建。`cachedSnapshot`、`viewCheckpoint`、`snapshot` 接受 `ProjectionKeySelection`(`readonly Key[] | ReadonlySet<Key>`),传入 Set 时直接使用。
- [`packages/session/session-projection-cache/src/index.ts`](../../../../packages/session/session-projection-cache/src/index.ts) 原样转发该选择,[`packages/api/session-controller/src/list.ts`](../../../../packages/api/session-controller/src/list.ts) 把 `summaryKeys()` 传给 live 与 cold 两个缓存读取面。`cachedPredecessorTitle` 保持只读 title。
- [`packages/session/session-projection-cache/src/index.ts`](../../../../packages/session/session-projection-cache/src/index.ts) 原样转发该选择,[`packages/api/session-controller/src/list.ts`](../../../../packages/api/session-controller/src/list.ts) 把 `summaryKeys()` 传给实时注册表的 `snapshot` 与冷会话的缓存读取。`cachedPredecessorTitle` 保持只读 title。
- 客户端只有类型变化:`SessionSummary.projectionValues` 收窄为 `Partial<SessionProjectionSummaryValues>`,而 `SessionProjectionSnapshot.values` 保留全部视图值。wire 字段、函数名与 UI 名称均不变;打开的 Session 仍通过 `session/follow`、控制基线与 `session.projections` 收到每个键。
估算效果:当 outline 在投影负载中占主导时,四个键估计占几百个 Session 时列表响应的约三分之二 —— outline 单项约 57%,统计与 todo 合计约 5%,共享的图片上限约 5% —— 占传输压缩后同一响应的约 85%,因为 outline 的唯一文本几乎无法压缩,而其余键可以。
@@ -0,0 +1,19 @@
# Bilingual-pair consistency record for 2026-10-19-team-task-board-inbox.md (docs/i18n/README.md): per heading
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
# After editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-10-19-team-task-board-inbox.md
/agent-note-team-task-boards-share-ordinary-subagent-waits:
en: 7f19734715f5d50f
zh: 95af09c9d855d1af
/agent-note-team-task-boards-share-ordinary-subagent-waits/problem:
en: 6fdc766a348d8659
zh: 8a84748e147deed5
/agent-note-team-task-boards-share-ordinary-subagent-waits/decision:
en: 154da8a8a5146c89
zh: 76a7c921153d238d
/agent-note-team-task-boards-share-ordinary-subagent-waits/alternatives-considered:
en: eed6f6d4fa0ccd7b
zh: 61271057d6e5d019
/agent-note-team-task-boards-share-ordinary-subagent-waits/consequences:
en: cc250eb21dbb45a1
zh: 9d03d1eb53075fb6
@@ -0,0 +1,31 @@
# Agent Note: Team task boards share ordinary subagent waits
Status: implemented
English | [中文](2026-10-19-team-task-board-inbox.zh.md)
## Problem
The subagent tree already supplies identity, discovery, routing, and lifecycle. A second Team roster can grant assignment authority from stale history and duplicates member navigation. Team-wide status waits observe unrelated activity and miss input already pending for the caller. Persisting task-board wakeups as messages adds unread state and message schemas even though the task journal already owns the state to inspect.
## Decision
The subagent tree is the Team. Ordinary controls own required identity, communication, discovery, interruption, and `wait_agent`, independently of the optional task board. Team tools own task policy, without adding it to ordinary subagent compositions. Team owns task storage, dependencies, and compare-and-set commands. Assignment accepts the root or any discovered continuable descendant. Historical roster records grant no authority; the Team panel shows tasks and ordinary navigation owns members.
The wait-signal decision is superseded by [inbox-only Agent waits](../simplification/2026-10-29-inbox-only-agent-wait.md). Tree ownership, task durability, and historical non-authority remain unchanged.
Task state remains in the existing journal. Ordinary tool results record what the waiting model observes. Historical roster, mailbox, and task generations remain readable and unchanged. This partially supersedes the roster and wait decisions in [Agent Teams](../feature/2026-08-05-agent-teams.md) and [subagent teammates](2026-10-15-agent-teams-as-subagents.md). Their task durability and single-identity rationale remain active; [required runtime identity](2026-10-13-team-identity-runtime-context.md) owns retention.
## Alternatives considered
**Keep Team membership and a separate wait tool.** This duplicates tree authority and wait lifecycle, allowing stale roster rows or unrelated activity to affect current work.
**Persist task-board notices in every inbox.** The task journal already preserves every revision. Durable notices add message types, unread state, coalescing, and delivery-failure recovery solely to interrupt a current wait.
**Poll Agent status or start every idle task owner.** Activity does not identify useful input, and task readiness does not authorize a new turn. Explicit messages continue idle work when needed.
## Consequences
Task completion, inbox availability, current waiting, and Agent lifetime remain separate. Task updates do not end waits; callers inspect current tasks when deciding what to do. The task journal retains committed revisions without storing redundant notifications in recipient Sessions.
Verification distinguishes pending inbox input from task commits, isolates callers, excludes next-turn input and status changes, and covers wait cleanup and task persistence. Assignment coverage retains arbitrary depth and historical non-authority.
@@ -0,0 +1,31 @@
# Agent Note: Team 任务板复用普通 subagent 等待
Status: implemented
[English](2026-10-19-team-task-board-inbox.md) | 中文
## 问题
subagent 树已经提供身份、发现、路由和生命周期。第二份 Team 名册可能根据过期历史授予分配权限,并重复成员导航。Team 范围状态等待会观察无关活动,却漏掉调用者已有的待处理输入。任务日志已经拥有待查询状态,将任务板唤醒持久化为消息仍会增加未读状态和消息 schema。
## 决策
subagent 树就是 Team。普通控制工具独立于可选任务板,负责必需身份、通信、发现、中断和 `wait_agent`。Team 工具负责任务策略,不向普通 subagent 组合添加该策略。Team 负责任务存储、依赖和比较并交换命令。分配接受根或任意已发现的可继续执行后代。历史名册不授予权限;Team 面板显示任务,普通导航负责成员。
等待信号决策已由[仅等待收件箱的 Agent 等待](../simplification/2026-10-29-inbox-only-agent-wait.zh.md)取代。树归属、任务持久性和历史记录不授予权限的决策保持不变。
任务状态保留在现有日志中。普通工具结果记录等待中的模型所观察到的结果。历史名册、邮箱和任务代次保持可读且不变。这部分取代 [Agent Teams](../feature/2026-08-05-agent-teams.zh.md) 和 [subagent 队友](2026-10-15-agent-teams-as-subagents.zh.md) 中的名册与等待决策。其任务持久性和单一身份理由继续生效;[必需运行时身份](2026-10-13-team-identity-runtime-context.zh.md)负责保留规则。
## 考虑过的替代方案
**保留 Team 成员关系与独立等待工具。** 这会重复树权限和等待生命周期,让过期名册行或无关活动影响当前工作。
**将任务板通知持久化到每个收件箱。** 任务日志已保留每个修订。持久通知仅为打断当前等待,就增加消息类型、未读状态、合并和投递失败恢复。
**轮询 Agent 状态或启动每个空闲任务所有者。** 活动状态不能标识有用输入,任务就绪也不授权新轮次。需要时通过显式消息继续空闲成员的工作。
## 后果
任务完成、收件箱可用性、当前等待和 Agent 生命周期保持独立。任务更新不会结束等待;调用者在决定下一步工作时查询当前任务。任务日志保留已提交修订,不在接收者 Session 中存储重复通知。
验证区分已有收件箱输入和任务提交,隔离调用者,排除下一轮输入及状态变化,并覆盖等待清理和任务持久性。分配验证保留任意深度和历史记录无权限场景。
@@ -0,0 +1,19 @@
# Bilingual-pair consistency record for 2026-10-20-session-team-mode.md (docs/i18n/README.md): per heading
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
# After editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-10-20-session-team-mode.md
/agent-note-team-availability-is-selected-per-session:
en: 5f36d81bd414ea67
zh: 71883b0fe2afd1b6
/agent-note-team-availability-is-selected-per-session/problem:
en: db7a81ab452fdd0f
zh: a14fdcdb8ff4cd47
/agent-note-team-availability-is-selected-per-session/decision:
en: f4e4ba7be0eda796
zh: 410649e86ff5ca72
/agent-note-team-availability-is-selected-per-session/alternatives-considered:
en: cd3cc1f3fab84f80
zh: 6d7792742d62f9fe
/agent-note-team-availability-is-selected-per-session/consequences:
en: b56a6a1c8ad1aa7a
zh: 43bdd8a5bccce8bf
@@ -0,0 +1,29 @@
# Agent Note: Team availability is selected per Session
Status: implemented
English | [中文](2026-10-20-session-team-mode.zh.md)
## Problem
A plugin-level Team switch requires users to change the shared profile before choosing collaboration for one conversation. It can disable another Session’s capability and leaves a second control beside the Session selection. Ordinary tasks still need their existing tool catalog, and disabling collaboration must not discard task history or children.
## Decision
The base bundle loads the Team service and task tools, and the Web bundle loads the Team UI. The packages belong to the subagent and client product groups. A saved root-Session selection gates task tools and Team guidance throughout the tree, and the shared board in the root conversation; new Sessions default off. Ordinary subagent creation, communication, and navigation remain available.
The model and reasoning menu owns the user’s selection. The root conversation’s Subagent panel displays members above shared tasks. Disabling Team keeps the journal and children; re-enabling restores the board. Historical task boards without a saved selection stay enabled. Profile loading removes the retired optional bundle selection, retaining row overrides.
This supersedes the explicit-composition choice in [Agent Teams](../feature/2026-08-05-agent-teams.md). Its durability, ownership, and lifecycle decisions remain active. [Optional bundles](../process/2026-09-15-shipped-optional-bundles.md) still governs other experimental features.
## Alternatives considered
**Keep both switches.** Capability installation remains a prerequisite that the conversation control cannot explain or resolve; users must manage shared profile state for a Session decision.
**Hide the plugin card without loading Team.** New profiles would have no way to obtain the capability. Loading experimental packages in the default profile would violate product isolation; moving them to their owning product groups makes the delivered support explicit.
## Consequences
Default profiles carry the Team implementation while exposing no task tools until enabled. The removed package names require custom compositions to migrate; durable event names and task data remain unchanged. [The upgrade guide](../../../../docs/upgrade-guide/v0.2.1-rc.2/session-team-mode/guide.md) owns those steps.
Verification covers default startup, new-Session opt-in, independent roots, persisted selections, retained tasks, old profile migration, and absence of the plugin switch. Product-isolation checks retain an unrelated experimental plugin as their negative control.
@@ -0,0 +1,29 @@
# Agent Note: 按 Session 选择 Team 能力
Status: implemented
[English](2026-10-20-session-team-mode.md) | 中文
## Problem
插件级 Team 开关要求用户先修改共享 profile,才能为一个会话选择协作。它可能关闭另一个 Session 的能力,并在会话选项之外留下第二个控件。普通任务仍需要原有工具目录,关闭协作也不能丢弃任务历史或子智能体。
## Decision
基础 bundle 加载 Team 服务与任务工具,Web bundle 加载 Team UI。相关包归属 subagent 与 client 产品组。根 Session 保存的选择控制整棵树的任务工具与 Team 指引,以及根会话中的共享任务板;新 Session 默认关闭。普通 subagent 创建、通信与导航继续可用。
模型与推理等级菜单承载用户选择。根会话的 Subagent 面板在成员下方显示共享任务。关闭 Team 保留日志与子智能体;重新开启恢复任务板。没有保存选择的历史任务板保持开启。profile 加载时移除已停用的可选 bundle 选择,保留配置行覆盖。
本决策替代 [Agent Teams](../feature/2026-08-05-agent-teams.zh.md) 中显式选择组合的部分;其持久性、归属与生命周期决策继续有效。[可选 bundle](../process/2026-09-15-shipped-optional-bundles.zh.md)仍约束其他实验性功能。
## Alternatives considered
**保留两个开关。** 安装能力仍是会话控件无法解释或解决的前置条件;用户必须为 Session 决策管理共享 profile 状态。
**隐藏插件卡片但不加载 Team。** 新 profile 将无法获得该能力。默认 profile 加载实验性包会违反产品隔离;将相关包移入所属产品组可明确随产品提供的支持。
## Consequences
默认 profile 包含 Team 实现,但开启前不暴露任务工具。自定义组合必须迁移已移除的包名;持久事件名称与任务数据保持不变。[升级指南](../../../../docs/upgrade-guide/v0.2.1-rc.2/session-team-mode/guide.zh.md)负责具体步骤。
验证覆盖默认启动、新 Session 手动开启、独立根、选择持久化、任务保留、旧 profile 迁移和插件开关移除。产品隔离检查使用另一个无关实验性插件作为负对照。
@@ -0,0 +1,37 @@
# Bilingual-pair consistency record for 2026-10-20-workspace-tree-state-per-pane.md (docs/i18n/README.md): per heading
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
# After editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-10-20-workspace-tree-state-per-pane.md
/agent-note-the-workspace-tree-is-per-pane-state-drawn-inside-each-file-tab-and-the-plugin-mounting-it-decides-where-files-open:
en: 3768a6ac42af3a0b
zh: ba3c0826b2a4d287
/agent-note-the-workspace-tree-is-per-pane-state-drawn-inside-each-file-tab-and-the-plugin-mounting-it-decides-where-files-open/problem:
en: 3a7f14dd68a3ff14
zh: b21be07e8a7e6024
/agent-note-the-workspace-tree-is-per-pane-state-drawn-inside-each-file-tab-and-the-plugin-mounting-it-decides-where-files-open/decision:
en: a15c7c13f167e5ac
zh: ff261e3705c47ff0
/agent-note-the-workspace-tree-is-per-pane-state-drawn-inside-each-file-tab-and-the-plugin-mounting-it-decides-where-files-open/decision/the-tree-is-drawn-inside-each-file-tab-and-its-state-is-kept-per-pane:
en: 432aff475f1dfd21
zh: bbc058c1f967c067
/agent-note-the-workspace-tree-is-per-pane-state-drawn-inside-each-file-tab-and-the-plugin-mounting-it-decides-where-files-open/decision/the-rows-are-a-controlled-component-factory-over-flat-entries:
en: a06955933f9f7f0e
zh: ccda2bd27175d36b
/agent-note-the-workspace-tree-is-per-pane-state-drawn-inside-each-file-tab-and-the-plugin-mounting-it-decides-where-files-open/decision/the-plugin-that-mounts-the-tree-decides-where-a-file-lands:
en: 7cf8e3671900fe71
zh: 952f69cba9c44c10
/agent-note-the-workspace-tree-is-per-pane-state-drawn-inside-each-file-tab-and-the-plugin-mounting-it-decides-where-files-open/decision/navigation-is-consumed-by-the-destination-tab:
en: e7b254df55d9768d
zh: d3303f20bb9ebdd6
/agent-note-the-workspace-tree-is-per-pane-state-drawn-inside-each-file-tab-and-the-plugin-mounting-it-decides-where-files-open/decision/each-package-works-without-the-others:
en: dd160808a3ac77bc
zh: f63741f159717072
/agent-note-the-workspace-tree-is-per-pane-state-drawn-inside-each-file-tab-and-the-plugin-mounting-it-decides-where-files-open/alternatives-considered:
en: 1c5501c71afc7748
zh: 3a2b0ec2d7923a12
/agent-note-the-workspace-tree-is-per-pane-state-drawn-inside-each-file-tab-and-the-plugin-mounting-it-decides-where-files-open/consequences:
en: 586ec09cfcf60b82
zh: ef683318fbf68d71
/agent-note-the-workspace-tree-is-per-pane-state-drawn-inside-each-file-tab-and-the-plugin-mounting-it-decides-where-files-open/verification:
en: eae354783022bed8
zh: fe2563a3bb259d55
@@ -0,0 +1,74 @@
# Agent Note: The workspace tree is per-pane state drawn inside each file tab, and the plugin mounting it decides where files open
Status: implemented
English | [中文](2026-10-20-workspace-tree-state-per-pane.zh.md)
## Problem
Reading several workspace files needs a tree beside their previews without losing expansion, loaded levels, width, scroll, or keyboard position on every tab switch. The dock mounts only the selected tab's body, so a component instance cannot own the state shared by a pane's file tabs.
A floating panel holds one tab. A preview's file picker must replace that tab in its panel, while the files page must remain available and open content in the dock. A covering tree must close where content is navigated to, not where the initiating click happened.
## Decision
### The tree is drawn inside each file tab, and its state is kept per pane
`ui-sidebar-right` supplies `useTabInfo().panel` with the containing pane's `id` and `isFloating`; moving a tab updates those fields without changing `tab.id`. It supplies no pane lifetime. `ui-sidebar-files` owns each pane's tree state, visibility, width, listing, directory watches, scroll, and last revealed file. The navigator and toggle are Session-scoped Component Factories; `ui-sidebar-documentpreview` renders them inside the tab's content row and header, with stable positions across loading states. The docking kit does not acquire a files-specific overlay or layout rule.
A pane's records follow the tabs whose navigator last drew there, using each tab's `signal`. A replacement in the same pane retains the records. A moved tab uses its destination pane's records; a pane without records starts hidden at the default width. Tab-owned preview scroll is independent of that tree state. Records are released after their last associated tab closes or draws the navigator elsewhere.
Directory watches follow visible drawings rather than retained records. Hidden trees hold no Host directory watch; showing them again refreshes the root and expanded directories while retaining rows. A same-pane tab switch keeps the listing and watches across the replacement drawing. This extends the [directory watch lifetimes](../feature/2026-09-17-sidebar-file-and-directory-auto-refresh.md) from one tab to the pane's drawn tree.
The files face owns ancestor reveal, scrolling, row measurement, and focus across drawings, so replacing a drawing does not discard an ongoing interaction. [The navigator reference](../../../../packages/client/ui-sidebar-files/README.md#the-navigator) owns layout, workspace-command, and keyboard rules.
### The rows are a controlled Component Factory over flat entries
The root-scoped `sidebar.files.tree` Factory keeps reusable rows, its local `accessory` slot, and localized copy with the files plugin. Callers own data access, expansion, opening, and reveal. The workspace navigator adapts its pane's directory states to these inputs.
### The plugin that mounts the tree decides where a file lands
`FileNavigatorInput.onOpen(address)` is the containing plugin's callback. The navigator constructs the file address but neither chooses placement nor closes the initiating tree after the call. Its `page` input controls layout only. A callback keeps the same navigator usable for the files page and document previews without teaching its rows either host's opening policy.
The [preview and files-page callbacks](../../../../packages/client/ui-sidebar-files/README.md#the-navigator) express different lifetimes: the preview gives way to new content, while the files page remains a directory browser. [Right Sidebar placement](../../../../packages/client/ui-sidebar-right/README.md#ctxsidebarright) owns replacement and reveal; the navigator does not duplicate those policies.
### Navigation is consumed by the destination tab
The files face follows the existing navigation observable only for tabs attached to its navigator. It retains each tab's consumed revision independently of the drawing and releases that subscription on tab closure or plugin disposal. It does not require a layout inventory, navigation-origin field, or an opening result from the right Sidebar.
The face waits for the destination drawing and measures its current row, so the previous tab's width cannot decide whether content is covered. It consumes navigation even while hidden to prevent a later manual show from replaying an old close. [The navigator reference](../../../../packages/client/ui-sidebar-files/README.md#the-navigator) defines the navigation sources and revision exclusions.
### Each package works without the others
The packages compose through the right Sidebar's tab information, the files Factories, and the tab-type registry. The preview imports only the files Factory declarations; files has no preview dependency. Without a registered viewer, the files page still lists the workspace and disables file rows it cannot open. Without the files plugin, the unregistered Factory positions render nothing and the preview keeps its own header, body, and file-handoff slots.
## Alternatives considered
**A static tree package or a component in `ui-primitives`.** The [package-shape rule](../../../../docs/defensive-patterns/package-surface.md) excludes new static libraries. A primitive would also require each caller to supply the generic tree labels. The Factory keeps that copy and registration with files, at the cost of requiring that plugin to draw rows.
**A tree that owns directory reads or requires a complete path list.** A workspace is listed one expanded directory at a time. The tree therefore accepts partial entries and per-directory loading, failure, and truncation state. Owning reads inside each drawing would split the state and watch lifetimes that must survive tab switches.
**A pane lifetime signal.** Floating and docking can replace the pane while retaining the tab. A pane signal would end tree state too early and invite attaching tab-owned resources to the wrong lifetime; transferring the signal would make it outlive the pane id used as the state key. The tabs' existing signals provide the required release condition.
**A pane-level overlay that stays mounted.** It requires a layer above docked and floating tab hosts and changes to layout, focus, measurement, and drop targets to avoid intercepting input, including floating resize handles. Drawing inside tabs keeps these concerns in their existing owners.
**Keep visited bodies mounted or copy per-tab trees.** Retaining every body still creates independent trees and watches for tabs sharing a pane and retains each preview's rendered content. Copying state cannot transfer watches and needs a conflict rule when tabs move between panes.
**Keep watches for a pane's whole lifetime or release by inspecting its layout.** The main docked pane rarely disappears, so lifetime-bound watches remain after hiding. Inspecting tab kinds in the layout couples files to every navigator consumer and still does not express whether the tree is drawn.
**A focus request bit consumed by a component effect.** A request stored on the initiating pane can survive a reveal elsewhere and affect a later drawing; a hidden consumer has no element to focus. The face ties focus to its actual drawings instead.
**Carry a dragged tab's tree preference into its new pane.** This needs another transfer policy and conflicts with the state already owned by the destination pane. A new pane starts with the same defaults as every other new pane.
## Consequences
Tab remounts do not own or reset the pane's tree. Moving a tab selects the destination's state rather than copying the source's; hiding releases directory watches without deleting expansion and scroll. Navigation consumption is per tab, while display preferences remain per pane, so an open in another pane does not close the source tree.
The files page retains directory reload and directory-action controls; a preview retains its file controls and handoff. Opening policy remains with those hosts, not the reusable rows.
## Verification
- `packages/client/ui-sidebar-right/tests/tab-info.client.spec.tsx`, `service.client.spec.ts`, and `seat.client.spec.tsx` cover live pane information, replacement, reveal, and framework tab hooks.
- `packages/client/ui-sidebar-files/tests/` covers per-pane state, navigation consumption, drawing lifetimes, workspace roots, and file opening callbacks.
- `packages/client/ui-sidebar-files/tests/file-tree.client.spec.tsx`, `file-tree-interactions.client.spec.tsx`, and `file-tree-factory.client.spec.tsx` cover generic rows, independent accessories, and real Factory composition with locale and fallback.
- `apps/web/tests/file-browser.e2e.ts` and `apps/web/tests/file-browser-composition.e2e.ts` own browser evidence; the latter disables each of `ui-sidebar-documentpreview` and `ui-sidebar-files` in the shipped Web composition.
@@ -0,0 +1,74 @@
# Agent Note: 工作区目录树是按分栏保存的状态,在每个文件 tab 内绘制;挂载它的插件决定文件在哪里打开
Status: implemented
[English](2026-10-20-workspace-tree-state-per-pane.md) | 中文
## 问题
连续阅读多个工作区文件需要预览旁的目录树在每次切换 tab 后保留展开状态、已加载的层、宽度、滚动位置与键盘位置。停靠布局只挂载选中 tab 的正文,因此组件实例不能拥有分栏内各文件 tab 共用的状态。
一个浮窗只容纳一个 tab。预览的文件选择器必须在浮窗内替换该 tab,文件页则必须保持可用,并在停靠区打开内容。遮盖正文的目录树应在内容被导航到的地方关闭,而不是在发起点击的地方关闭。
## 决定
### 目录树在每个文件 tab 内绘制,其状态按分栏保存
`ui-sidebar-right` 通过 `useTabInfo().panel` 提供所在分栏的 `id` 与 `isFloating`;移动 tab 时更新这些字段,`tab.id` 保持不变。它不提供分栏生命周期。`ui-sidebar-files` 拥有各分栏的目录树状态、可见性、宽度、列举、目录监听、滚动与上次定位的文件。导航栏和开关是 Session 作用域的 Component Factory;`ui-sidebar-documentpreview` 在 tab 的内容行与头部渲染它们,加载状态变化时保持位置稳定。停靠套件不增加 files 专属的覆盖层或布局规则。
分栏记录跟随最近在该分栏绘制导航栏的 tab,以各 tab 的 `signal` 为依据。同一分栏内的替换保留记录。移走的 tab 使用目标分栏的记录;没有记录的分栏默认隐藏目录树、使用默认宽度。tab 拥有的预览滚动状态独立于目录树状态。最后一个关联 tab 关闭或在别处绘制导航栏后,记录被释放。
目录监听跟随可见的绘制,而不是保留的记录。隐藏的目录树不持有 Host 目录监听;再次显示时保留行并刷新根目录与已展开目录。同一分栏内切换 tab 时,替代绘制之间保留目录列表与监听。这把[目录监听生命周期](../feature/2026-09-17-sidebar-file-and-directory-auto-refresh.zh.md)从单个 tab 扩展为分栏中已绘制的目录树。
files face 负责跨绘制的祖先定位、滚动、行宽测量与焦点,因此替换绘制不会丢弃进行中的交互。[导航栏参考](../../../../packages/client/ui-sidebar-files/README.zh.md#the-navigator)定义布局、工作区命令与键盘规则。
### 各行是以扁平条目为输入的受控 Component Factory
root 作用域的 `sidebar.files.tree` Factory 将可复用的行、局部 `accessory` slot 和本地化文案留在 files 插件。调用方负责数据访问、展开、打开与定位。工作区导航栏把分栏的目录状态转换为这些输入。
### 挂载目录树的插件决定文件落在哪里
`FileNavigatorInput.onOpen(address)` 是容纳导航栏的插件提供的回调。导航栏构造文件地址,但不选择落位,也不在调用后关闭发起侧目录树。`page` 输入只控制布局。回调使同一导航栏能同时用于文件页与文档预览,不必让各行知道这两种宿主的打开策略。
[预览与文件页的回调](../../../../packages/client/ui-sidebar-files/README.zh.md#the-navigator)表达不同的生命周期:预览让位给新内容,文件页则保持为目录浏览器。[右侧 Sidebar 落位](../../../../packages/client/ui-sidebar-right/README.zh.md#ctxsidebarright)负责替换与揭示,导航栏不重复这些策略。
### 导航由目标 tab 消费
files face 仅跟随已关联到其导航栏的 tab 的现有导航可观察值。它独立于绘制保留各 tab 已消费的 revision,并在 tab 关闭或插件卸载时释放订阅。它不需要右侧 Sidebar 提供布局清单、导航来源字段或打开结果。
face 等待目标绘制并测量其当前行宽,因此上一个 tab 的宽度不能决定内容是否被覆盖。隐藏期间也消费导航,防止之后手动显示时重放旧的关闭操作。[导航栏参考](../../../../packages/client/ui-sidebar-files/README.zh.md#the-navigator)定义导航来源与 revision 排除规则。
### 每个包都可以单独工作
各包通过右侧 Sidebar 的 tab 信息、files Factory 和 tab 类型注册表组合。预览仅导入 files Factory 的声明;files 不依赖预览。没有已注册查看器时,文件页仍列出工作区,并禁用无法打开的文件行。没有 files 插件时,未注册的 Factory 位置不绘制内容,预览保留自己的头部、正文和文件交接 slot。
## 考虑过的替代方案
**静态目录树包或 `ui-primitives` 中的组件。** [包形态规则](../../../../docs/defensive-patterns/package-surface.zh.md)排除新增静态库。primitive 还要求各调用方传入通用目录树文案。Factory 使文案与注册留在 files,代价是绘制各行必须加载该插件。
**由目录树读取目录,或要求完整路径列表。** 工作区逐个展开的目录列出。因此目录树接收不完整条目,以及各目录的加载、失败与截断状态。在每次绘制中拥有读取会拆散必须跨 tab 切换保留的状态和监听生命周期。
**分栏生命周期信号。** 浮出和停靠可能替换分栏,但保留 tab。分栏信号会过早结束目录树状态,还会诱使调用方把 tab 资源绑到错误的生命周期;转移信号则让它比用作状态键的 pane id 活得更久。tab 已有的信号提供了所需的释放条件。
**始终挂载的分栏级覆盖层。** 它需要在停靠与浮动 tab 宿主之上增加一层,并修改布局、焦点、测量与拖放目标,避免拦截包括浮窗缩放手柄在内的输入。在 tab 内绘制则让这些职责留在原有所有者处。
**保留已访问正文或复制逐 tab 的目录树。** 保留所有正文仍会为同一分栏的不同 tab 创建独立目录树与监听,并保留各预览的已渲染内容。复制状态无法转移监听,还需要在 tab 跨分栏移动时决定冲突如何处理。
**让监听与分栏同寿,或检查布局后释放。** 主停靠分栏很少消失,因此按其生命周期持有的监听在隐藏后仍存在。检查布局中的 tab 类型会让 files 与每个导航栏消费方耦合,仍不能表达目录树是否正在绘制。
**由组件 effect 消费的聚焦请求位。** 保存在发起分栏的请求可能在揭示别处后留存,影响之后的绘制;隐藏的消费方也没有可聚焦元素。face 则把焦点与实际绘制绑定。
**把被拖动 tab 的目录树偏好带到新分栏。** 这需要另一套转移策略,并与目标分栏已经拥有的状态冲突。新分栏使用与其他新分栏相同的默认值。
## 后果
tab 重新挂载不拥有也不重置分栏目录树。移动 tab 时选择目标状态,不复制源状态;隐藏释放目录监听,但不删除展开与滚动状态。导航消费按 tab 保存,显示偏好仍按分栏保存,因此在另一分栏打开内容不会关闭源目录树。
文件页保留目录重新读取与目录操作控件;预览保留文件控件与交接操作。打开策略由这些宿主拥有,不属于可复用的行。
## 验证
- `packages/client/ui-sidebar-right/tests/tab-info.client.spec.tsx`、`service.client.spec.ts` 与 `seat.client.spec.tsx` 覆盖实时分栏信息、原位替换、揭示与框架 tab hook。
- `packages/client/ui-sidebar-files/tests/` 覆盖逐分栏状态、导航消费、绘制生命周期、工作区根目录与文件打开回调。
- `packages/client/ui-sidebar-files/tests/file-tree.client.spec.tsx`、`file-tree-interactions.client.spec.tsx` 与 `file-tree-factory.client.spec.tsx` 覆盖通用行、独立附件,以及带 locale 和 fallback 的真实 Factory 组合。
- `apps/web/tests/file-browser.e2e.ts` 与 `apps/web/tests/file-browser-composition.e2e.ts` 负责浏览器验证;后者在随包发布的 Web 组合中分别禁用 `ui-sidebar-documentpreview` 与 `ui-sidebar-files`。
@@ -0,0 +1,19 @@
# Bilingual-pair consistency record for 2026-10-21-schedule-rule-storage.md (docs/i18n/README.md): per heading
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
# After editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-10-21-schedule-rule-storage.md
/agent-note-schedule-rule-storage-and-shared-recurrence:
en: 34817514a06a4351
zh: a9e33a1c4d5845d1
/agent-note-schedule-rule-storage-and-shared-recurrence/problem:
en: 22eed4a004cbb1c5
zh: dc7b6f8ed9c074cf
/agent-note-schedule-rule-storage-and-shared-recurrence/decision:
en: 50bfa3e81af7c2b0
zh: e9bd5fae6e611030
/agent-note-schedule-rule-storage-and-shared-recurrence/alternatives-considered:
en: fbfcd35c35fb637a
zh: df9dd646e6d18e3c
/agent-note-schedule-rule-storage-and-shared-recurrence/consequences:
en: 61213641597c18bb
zh: 393a79013e311311
@@ -0,0 +1,25 @@
# Agent Note: Schedule rule storage and shared recurrence
Status: implemented
English | [中文](2026-10-21-schedule-rule-storage.zh.md)
## Problem
Host tasks already use the versioned `schedule` domain. A second domain, importer, and placeholder RRULE duplicate timing and storage authority. Cron and RRULE should be supported timing types rather than separate generations of the task API.
## Decision
Use `schedule` version 2 with `compatibleVersions: [1]`, retaining its backend routing, envelope, and whole-unit layout. Extend the existing accepted-version policy to authoritative JSON and SQLite units; reads leave storage unchanged, and the next write commits the current stamp with its data. Add `rrule` to the six existing record kinds and evaluate every kind normally. Remove the standalone store importer, import marker, placeholder RRULE, and parallel Session-bound management API. Move reusable Cron and RRULE parsing and bounded occurrence arithmetic into `dsh-util-time`; Schedule owns records, targets, delivery admission, history, and errors.
An explicit target wins. Without one, read an existing `sessionId` as a Session target; otherwise reserve a stable `new_once` target. Optional new metadata receives read defaults without rewriting storage. Every successful edit writes the current row format without `sessionId`. Custom timing accepts either RRULE or a five-field Cron expression with an explicit zone; UI labels name neither syntax. Model guidance continues to teach RRULE.
This partially supersedes [standalone schedules](2026-09-17-standalone-schedules.md): its target, execution, notification, and Host-wide management decisions remain. It restores the domain and layout in [Host schedule storage](2026-09-16-host-schedule-storage.md), while replacing that note's Session-bound management API. Both notes remain active and link to this decision.
## Alternatives considered
Changing the domain or layout would require routing and copy logic without changing the feature. Converting Cron into RRULE can change day-of-month/day-of-week and time-zone semantics. A generic value migration engine is deferred until an incompatible storage version requires one; the existing domain envelope already identifies the version.
## Consequences
Missing creation metadata cannot recover historical timestamps. A default target must retain its reserved Session identity across reads and retries. Old receipts need their original destination retained before retargeting. A current unit stamp can contain older untouched rows, so future compatible readers must retain those defaults until a complete rewrite allows removal. Old binaries reject a newer stamp. Session inbox persistence and task acknowledgment remain separate commits, so delivery remains at least once.
@@ -0,0 +1,25 @@
# Agent Note: 调度规则存储与共享重复时间计算
Status: implemented
[English](2026-10-21-schedule-rule-storage.md) | 中文
## 问题
Host 任务已经使用带版本的 `schedule` 域。第二个域、导入器和占位 RRULE 会重复定义时间与存储的权威来源。Cron 和 RRULE 应当是并列的时间规则类型,而非两代任务 API。
## 决策
使用 `schedule` 版本 2 和 `compatibleVersions: [1]`,保留后端路由、封套和整单元布局。将现有接受版本策略扩展到权威 JSON 和 SQLite 单元;读取保持存储不变,下一次写入将当前版本戳与数据一同提交。在现有六种记录类型旁添加 `rrule`,所有类型均正常求值。移除独立存储的导入器、导入标记、占位 RRULE 和并行的 Session 绑定管理 API。将可复用的 Cron、RRULE 解析及有界时间计算移至 `dsh-util-time`;Schedule 负责记录、目标、投递准入、历史和领域错误。
显式目标优先。缺少目标时,将已有 `sessionId` 读取为 Session 目标;否则保留稳定的 `new_once` 目标。新增可选元数据使用读取默认值,不重写存储。每次成功编辑都写入不含 `sessionId` 的当前行格式。自定义时间同时接受 RRULE 和带显式时区的五字段 Cron 表达式;界面标签不提示任何语法名称。模型指引继续推荐 RRULE。
本决策部分替代[独立定时任务](2026-09-17-standalone-schedules.zh.md),保留其目标、执行、通知和宿主级管理决策。它恢复[宿主任务存储](2026-09-16-host-schedule-storage.zh.md)的领域与布局,并替代按 Session 绑定的管理 API。两个 note 保持活动并链接到本决策。
## 考虑过的替代方案
更换域或布局需要增加路由和复制逻辑,却不改变功能。将 Cron 转换为 RRULE 可能改变月内日期、星期和时区语义。通用值迁移引擎留待不兼容存储版本确有需要时再引入;现有域封套已经标识版本。
## 影响
缺失的创建元数据无法恢复历史时间戳。默认目标必须在读取和重试间保持预留 Session 标识。重新指定目标前,旧回执需要保留原始投递目标。当前单元版本戳可包含未改动的旧行,因此未来兼容读取器必须保留这些默认规则,直至完整重写后才能移除。旧程序拒绝更新的版本戳。Session 收件箱持久化与任务确认仍是两次独立提交,因此投递仍然至少一次。
@@ -12,8 +12,8 @@
en: 45c63df5dac389eb
zh: d72a5bcb94de22f1
/agent-note-session-snapshots-passive-subscriptions-and-web-aggregation/decision/reads-and-subscriptions:
en: 38df4f3c4fba2cc9
zh: a34f3376faf26f0c
en: a2439f02cb9654f9
zh: 9de786792cba6a1d
/agent-note-session-snapshots-passive-subscriptions-and-web-aggregation/decision/follow-ordering-and-ownership:
en: 352ff87a3e9cd110
zh: 4ac6366cecfc08a3
@@ -18,7 +18,7 @@ History reads return chronological records, `hasMore`, and an inclusive cursor.
`usingObservation({ sessionId, projection }, signal, callback?)` acquires the existing disposable SessionObservation. A callback borrows it until the callback settles; without a callback the caller owns disposal. Ordinary history, state, and Wait output reads do not request projections. Follow, exact projection reads, and subagent history descriptor validation request them. No read activates an Agent implicitly.
`observeEvent` and `observeProjections` install listeners without acquiring an Observation. Their initial `ready` confirms subscription, not a snapshot cursor, existence, parent-child validity, or Agent readiness. History establishes continuity from its first received event; projection updates replace individual keys and may share a sequence number. `observeState` instead begins with a complete state read and coalesces later Session and Agent invalidations.
`observeEvent` and `observeProjections` install listeners without acquiring an Observation. Their initial `ready` confirms subscription, not a snapshot cursor, existence, parent-child validity, or Agent readiness. History establishes continuity from its first received event; each projection change frame covers one committed event and replaces only the keys it lists. `observeState` instead begins with a complete state read and coalesces later Session and Agent invalidations.
History, history observation, and Follow accept Address requests; projection and state operations accept Session IDs. All internal readers and listeners use Session IDs. History and Follow validate Address against their opening Observation at the Controller layer, including the existing subagent descriptor rules. Pure history observation does not acquire a read for entity validation. Gateway resource authorization remains independent: the registered Session resolver extracts the target ID from the request before access is decided.
@@ -18,7 +18,7 @@ Status: implemented
`usingObservation({ sessionId, projection }, signal, callback?)` 获取已有的可释放 SessionObservation。有回调时租约借给回调直到其结束;无回调时由调用方释放。普通历史、状态及 Wait 输出读取不请求投影。Follow、精确投影读取和子会话历史 descriptor 校验请求投影。读取不会隐式激活 Agent。
`observeEvent` 和 `observeProjections` 安装监听而不获取 Observation。首个 `ready` 确认订阅生效,不代表快照游标、实体存在、父子关系有效或 Agent 就绪。历史从首个收到的事件建立连续性;投影更新替换单个键,多个更新可以共享序号。`observeState` 则从完整状态读取开始,并合并后续 Session 和 Agent 失效通知。
`observeEvent` 和 `observeProjections` 安装监听而不获取 Observation。首个 `ready` 确认订阅生效,不代表快照游标、实体存在、父子关系有效或 Agent 就绪。历史从首个收到的事件建立连续性;每个投影变更帧对应一个已提交事件,只替换帧内列出的键。`observeState` 则从完整状态读取开始,并合并后续 Session 和 Agent 失效通知。
历史、历史观察和 Follow 接收 Address 请求;投影和状态操作接收 Session ID。所有内部读取器和监听器都使用 Session ID。History 和 Follow 在 Controller 层以开场 Observation 校验 Address,包括已有的子会话 descriptor 规则。纯历史观察不为实体校验获取读取。Gateway 资源授权与之独立:注册的 Session 解析器在访问判定前从请求提取目标 ID。
@@ -0,0 +1,19 @@
# Bilingual-pair consistency record for 2026-10-22-subagent-target-user-controls.md (docs/i18n/README.md): per heading
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
# After editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-10-22-subagent-target-user-controls.md
/agent-note-user-controls-address-the-target-subagent:
en: d7dcf54ddb311881
zh: 5f388447737c5414
/agent-note-user-controls-address-the-target-subagent/problem:
en: 962c75771235903c
zh: 203b651c0830310e
/agent-note-user-controls-address-the-target-subagent/decision:
en: 13a2617e28445744
zh: 75d193ef7d8c7804
/agent-note-user-controls-address-the-target-subagent/alternatives-considered:
en: 447adb24280bf2bb
zh: 08d95a93b14aa5c7
/agent-note-user-controls-address-the-target-subagent/consequences:
en: 98e784f55775247d
zh: 1c62d2ebc71b658f
@@ -0,0 +1,33 @@
# Agent Note: User controls address the target subagent
Status: implemented
English | [中文](2026-10-22-subagent-target-user-controls.zh.md)
## Problem
A browser knows the subagent it is displaying, but requiring its direct creator and a continuable marker makes every control caller repeat history metadata. A caller-supplied parent id checks address consistency without establishing the caller's identity or permission. Calls that resolve only the global access resource also cannot express a grant on one target Session.
## Decision
Browser prompts and `interruptByUser` address only the target child Session. Each Remote method explicitly declares that Session as its access resource; the Gateway checks the invocation's principal before attachment admission, restoration, or cancellation. A global or parent Session grant does not imply a child Session grant. Policies decide whether to grant both actions, either action, or inherited access. Same-process user interruption trusts its caller and does not accept a wire-supplied principal.
Prompt delivery derives the live root from the resident Activation or verified persisted creation edges. Only the cold target resumes; its creator can remain offline. Shared delivery retains exact live root checks, shutdown admission, cancellation, capacity, and final inbox acceptance. A root Session is not a target of the browser subagent prompt endpoint. User interruption addresses a resident Activation without persistence reads or restoration; an absent target remains a no-op, and interruption preserves parked input.
Model messages retain the exact live sender and same-tree rule from [tree-wide communication](2026-10-16-tree-wide-agent-communication.md). Model interruption retains its stricter ancestor rule. Creation parents still determine catalog edges, history-address consistency, ancestry, and completion recipients; they are not browser control credentials.
This supersedes the direct-parent addressing requirement for human continuation in [continuable subagents](../feature/2026-07-28-continuable-subagent-conversations.md). That record retains its inbox and durable continuation rationale; tree-wide communication owns its superseded model adjacency and runtime ownership restrictions. No Session format changes.
## Alternatives considered
**Keep the parent check as authorization.** The client supplies the parent id and can obtain it from history. Matching that id does not authorize the principal, and requiring it couples controls to discovery metadata.
**Require a root id from the browser.** The server already derives the runtime owner from the target. Another supplied id adds a consistency check without granting permission; Agent senders still need an independently resolved root to enforce same-tree messaging.
**Unify user and Agent interruption permissions.** A user with a target grant can stop that target, while an Agent may stop only its descendants. Same-tree collaboration does not grant members control over each other's execution.
## Consequences
Remote callers update their prompt fields, interrupt endpoint, and non-owner policy actions and resources; the [upgrade guide](../../../../docs/upgrade-guide/v0.2.1-rc.2/subagent-user-controls/guide.md) owns the migration. History URLs retain parent ids for consistency checks. Cold host prompts may read creation ancestry twice, once to find the live root and again during restoration; no verified-ancestry cache is retained across asynchronous work.
Gateway tests cover target grants, global-only and parent-only refusal, separate send and interrupt actions, owner access, and refusal before side effects. Continuation tests cover nested and cold targets with online or offline creators, unavailable roots, and rejection of a root as a subagent target. The Web interrupt scenario exercises the renamed endpoint and parked-input behavior through the shipped composition.
@@ -0,0 +1,33 @@
# Agent Note: 用户控制按目标子代理寻址
Status: implemented
[English](2026-10-22-subagent-target-user-controls.md) | 中文
## 问题
浏览器知道当前展示的子代理,但要求提供直接创建者和 continuable 标记,使每个控制调用方都要重复历史元数据。调用方提供的父级 id 只能检查地址一致性,不能确定调用者身份或权限。仅解析出 global 访问资源的调用也无法表达对单个目标 Session 的授权。
## 决策
浏览器提示词和 `interruptByUser` 仅按目标子 Session 寻址。每个 Remote 方法显式声明该 Session 为访问资源;Gateway 在附件接纳、恢复或取消之前检查调用的 principal。global 或父 Session 授权不意味着子 Session 授权。策略决定授予两个操作、其中一个操作,或继承访问权。同进程用户中断信任其调用方,不接受从线上传入的 principal。
提示词投递从驻留 Activation 或经过验证的持久化创建关系中解析在线根。仅恢复冷目标,创建者可以保持离线。共享投递保留精确在线根检查、关闭准入、取消、容量和最终 inbox 接纳。根 Session 不是浏览器子代理提示词端点的目标。用户中断直接寻址驻留 Activation,不读取持久化数据,也不恢复目标;目标缺席仍为无操作,中断保留停放的输入。
模型消息保留[整树通信](2026-10-16-tree-wide-agent-communication.zh.md)中的精确在线发送者和同树规则。模型中断保留更严格的祖先规则。创建父级仍决定目录关系、历史地址一致性、祖先关系和完成通知接收者;它们不是浏览器控制凭证。
本决策取代[可继续的 subagent](../feature/2026-07-28-continuable-subagent-conversations.zh.md)对人工继续执行要求直接父级地址的部分。旧记录保留 inbox 和持久化继续执行的理由;整树通信决策拥有其中已取代的模型相邻关系和运行时所有权限制。Session 格式不变。
## 考虑过的替代方案
**保留父级检查作为授权。** 客户端提供父级 id,也能从历史中取得它。匹配该 id 不能授权 principal,还使控制依赖于发现元数据。
**要求浏览器提供根 id。** 服务端已经能从目标解析运行时所有者。再提供一个 id 只会增加一致性检查,不会授予权限;Agent 发送者仍需要独立解析的根来约束同树消息。
**统一用户和 Agent 的中断权限。** 用户获得目标授权后可以停止该目标,而 Agent 只能停止自己的后代。同树协作不赋予成员控制彼此执行的权限。
## 影响
Remote 调用方需要更新提示词字段、中断端点,以及非 owner 策略的操作和资源;迁移步骤由[升级指南](../../../../docs/upgrade-guide/v0.2.1-rc.2/subagent-user-controls/guide.zh.md)拥有。历史 URL 保留父级 id 用于一致性检查。冷目标的宿主提示词可能读取两次创建祖先:先查找在线根,再在恢复期间验证;不跨异步操作保留已验证祖先缓存。
Gateway 测试覆盖目标授权、仅 global 和仅父级授权的拒绝、独立的发送和中断操作、owner 访问,以及副作用前拒绝。继续执行测试覆盖创建者在线或离线时的嵌套和冷目标、根不可用,以及拒绝把根当作子代理目标。Web 中断场景通过交付组合验证重命名后的端点和输入停放行为。
@@ -0,0 +1,19 @@
# Bilingual-pair consistency record for 2026-10-22-subagent-tool-owned-communication.md (docs/i18n/README.md): per heading
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
# After editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-10-22-subagent-tool-owned-communication.md
/agent-note-subagent-tools-own-communication-guidance:
en: 0bd031aa81fbc18d
zh: c530e980826698ed
/agent-note-subagent-tools-own-communication-guidance/problem:
en: ac391b569d2bc7e9
zh: a95f613aa2b330b8
/agent-note-subagent-tools-own-communication-guidance/decision:
en: 52fb33963bedfe07
zh: 75fe66d80f47323f
/agent-note-subagent-tools-own-communication-guidance/alternatives-considered:
en: c55d44c6414e0c4c
zh: 9fa2ab31843e259b
/agent-note-subagent-tools-own-communication-guidance/consequences:
en: 52f7234664c515ec
zh: 55ec7b54c044e9af
@@ -0,0 +1,27 @@
# Agent Note: Subagent tools own communication guidance
Status: implemented
English | [中文](2026-10-22-subagent-tool-owned-communication.zh.md)
## Problem
The continuation manager inspected a Symbol on the standard messaging tool before adding tool instructions and parent addressing to the initial task. This coupled orchestration to one tool implementation and made result-delivery guidance depend on tool installation. A published internal entry point carried both that marker and a host prompt adapter used only by tests.
## Decision
`tool-subagent-control` owns messaging instructions and parent addressing. Its logged runtime context gives delegated children their direct creator’s id from the Session header, explains when to send findings, and states that sending does not finish a turn. The context is emitted only for delegated children when the plugin’s own tool definition is admitted in the current scope and step; roots and ordinary Session forks receive none. Communication guidance follows inherited history without changing the shared system prompt or tool schema.
The manager owns result delivery. Local activations with `delivery: 'parent'` receive automatic-result guidance even without messaging tools; caller-owned activations receive no parent-delivery promise. Settlement timing, recipients, output, and suppression of closing text after a final message to the parent remain unchanged. Tool visibility grants no messaging authority.
This supersedes the Symbol identity and initial-task communication-guidance choices in [adjacent messaging](2026-08-27-adjacent-agent-steer-messaging.md). Its message attribution, fixed Steer delivery, and rejection of a separate report operation remain active. The internal entry point and prompt adapter are removed; the [upgrade guide](../../../../docs/upgrade-guide/v0.2.1-rc.2/subagent-user-controls/guide.md) owns caller migration.
## Alternatives considered
**Keep a cross-package marker or add a service hook.** Tool registration already gives its owner the exact definition, and prompt assembly already supplies the current Agent. Another protocol or stored identity has no independent consumer.
**Move all return guidance into the tool.** Automatic parent notification depends on activation delivery, not on messaging capability. The tool must not promise it to caller-owned activations or withhold it from children without tools.
## Consequences
Tests cover visible, hidden and replaced definitions, logged identity, plugin disposal, fork prefix reuse, and parent versus caller result delivery. Recorded-session snapshots cover the assembled prompt and durable runtime context. Tests needing host input use the existing private manager test helper; no production adapter exists solely to expose that helper.
@@ -0,0 +1,27 @@
# Agent Note: Subagent 工具拥有通信指导
Status: implemented
[English](2026-10-22-subagent-tool-owned-communication.md) | 中文
## 问题
继续执行管理器检查标准通信工具上的 Symbol,随后在初始任务中加入工具说明和父级地址。这让编排依赖一个工具实现,也让结果交付指导取决于是否安装工具。已发布的内部入口同时承载该标记和仅供测试使用的宿主 prompt 适配器。
## 决策
`tool-subagent-control` 拥有通信说明和父级寻址。持久记录的运行时上下文从 Session header 读取委派子级的直接创建者 id,说明何时发送发现,并指出发送不会结束轮次。仅当本插件的工具定义在当前作用域和 step 中可用时,才向委派子级提供上下文;根代理和普通 Session fork 不接收。通信指导位于继承历史之后,不改变共享系统提示词或工具 schema。
管理器拥有结果交付。采用 `delivery: 'parent'` 的本地 activation 即使没有通信工具,也会收到自动结果交付指导;调用方拥有的 activation 不会收到父级交付承诺。结算时序、接收方、输出,以及最后向父级发送消息后省略结束文本的规则保持不变。工具可见性不授予通信权限。
本决策取代[相邻代理通信](2026-08-27-adjacent-agent-steer-messaging.zh.md)中的 Symbol 身份和初始任务通信指导选择。该记录的消息来源归属、固定 Steer 投递,以及不另设 report 操作的决策继续有效。内部入口和 prompt 适配器已删除;调用方迁移由[升级指南](../../../../docs/upgrade-guide/v0.2.1-rc.2/subagent-user-controls/guide.zh.md)说明。
## 考虑过的替代方案
**保留跨包标记或增加 service hook。** 工具注册已经向所有者提供确切定义,提示词组装也已提供当前 Agent。另一套协议或身份存储没有独立消费者。
**把全部返回指导移入工具。** 自动父级通知取决于 activation 的交付方式,而非通信能力。工具不能向调用方拥有的 activation 承诺自动通知,也不能让没有工具的子级缺少交付说明。
## 后果
测试覆盖可见、隐藏及替换的定义、持久记录的身份、插件释放、fork 前缀复用,以及父级与调用方结果交付。录制会话快照覆盖组装后的提示词和持久运行时上下文。需要宿主输入的测试使用现有的私有 manager 测试帮助函数;生产代码不再仅为暴露该帮助函数而保留适配器。
@@ -0,0 +1,19 @@
# Bilingual-pair consistency record for 2026-10-29-cua-dev-signing-identity.md (docs/i18n/README.md): per heading
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
# After editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-10-29-cua-dev-signing-identity.md
/agent-note-development-signing-identity-for-source-built-cua-helpers:
en: 472c4efd47184a32
zh: 49f64628531970e9
/agent-note-development-signing-identity-for-source-built-cua-helpers/problem:
en: a24857a312482724
zh: dbac75d363670e86
/agent-note-development-signing-identity-for-source-built-cua-helpers/decision:
en: d2265f9ec5fb7a79
zh: 05da2479b024a4bb
/agent-note-development-signing-identity-for-source-built-cua-helpers/alternatives-considered:
en: e30db5bc9c7aab43
zh: fa84883b5527334e
/agent-note-development-signing-identity-for-source-built-cua-helpers/consequences:
en: fb0e8fe44890bdf9
zh: d9e9ace1a4c10060
@@ -0,0 +1,35 @@
# Agent Note: Development signing identity for source-built CUA helpers
Status: implemented
English | [中文](2026-10-29-cua-dev-signing-identity.zh.md)
## Problem
Source checkouts compile `DSH Computer Use.app` with an ad-hoc signature. macOS stores Accessibility and Screen Recording grants for a bundle identifier with the designated requirement of the granted code, and an ad-hoc requirement is the code hash. Every changed build and every other worktree therefore needs new grants, and these builds shared the release helper's bundle identifier and replaced its grant entry.
## Decision
The Host builds source checkouts as a development variant with bundle identifier `com.deepseek.harness.computer-use.dev` and name `DSH Computer Use Dev`, in `DSH Computer Use Dev.app`. The macOS privacy list and the helper’s reported launch application use the bundle filename, so that filename also includes Dev. The release variant and its packaging are unchanged. Grants stay with the helper application launched through LaunchServices, as the [shared helper decision](2026-10-09-cua-shared-native-helper.md) requires.
`sourceSigning` belongs to the macOS provider and accepts only default `ad-hoc` or `{ identity, keychain }` selecting an existing certificate and absolute keychain path. Builds do not create, unlock or remove identities. Developer credentials stay separate from the user-facing DSH home, which stores only the certificate selection; the shared `cua` package owns no macOS signing configuration.
The [developer signing skill](../../../skills/dsh-cua-macos-signing/SKILL.md) guides Agents through creating, reusing and unlocking an isolated identity under `~/.cua-macos-keys/`, then selecting its fingerprint and keychain path in user configuration. The tool stores a random password in a `0600` file, permits codesign access, and adds the dedicated keychain to the user search list. A file lock serializes setup and removal for that directory; a missing keychain with an existing record is preserved and rejected. A directory marker limits identity management to explicitly claimed, initially empty directories. Unexpected files or symlinked identity entries stop setup and removal before changes, so a developer-selected path cannot erase unrelated contents.
Certificate signatures embed a signing time even without a timestamp server. Development publication records which generation each unsigned bundle and identity produced, then selects that generation again instead of re-signing, so identical sources keep one executable digest and one shared service. The cache uses certificate fingerprints; explicit certificate names are not reused because renewal can change the certificate behind a name. Signing failures stop publication. Developer setup diagnostics omit passwords and complete command arguments. The build CLI defaults to `--identity -` and accepts an existing certificate with `--identity` and optional `--keychain` for release packaging.
## Alternatives considered
**Grant the launching terminal.** Launching the service directly makes macOS attribute permissions to the terminal or editor that started DSH. Every program that application runs, including commands run by the model, then has the permissions.
**Automatically create a fixed identity during source builds.** This makes ordinary startup create developer private keys and ties their lifetime to user data. An explicit developer workflow preserves the option to reduce repeated grants while keeping identity creation, removal and selection under developer control.
**Self-signed identity in the login keychain.** The login keychain is already unlocked and on the search list, but allowing `codesign` to use a new key there without a dialog requires the user's login password.
**Add the keychain to the search list only while signing.** The release packaging script does this for one invocation. Concurrent builds in several worktrees would read and restore the user list over each other, and a stopped build would leave the entry behind anyway.
**A stable launcher that spawns each worktree's helper.** Grants would follow the launcher, but any local process could ask it to run an arbitrary executable with those grants, which is the same exposure as the stored key without a signature check.
## Consequences
Development builds signed by the same certificate share a signing requirement; actual permission retention after grants, rebuilds and worktree changes still requires verification. Other processes of the same user can read the stored password and sign with that identity; the release helper has a different bundle identifier. Deleting DSH home removes its selection, not the developer identity; deleting the identity requires clearing every configuration that references it. The real-signing e2e checks reuse, unlocking, requirements across output directories and search-list restoration after removal.
@@ -0,0 +1,35 @@
# Agent Note: 源码构建的 CUA helper 使用开发签名身份
Status: implemented
[English](2026-10-29-cua-dev-signing-identity.md) | 中文
## 问题
源码检出用临时签名编译 `DSH Computer Use.app`。macOS 按 Bundle ID 和被授权代码的指定要求保存辅助功能与屏幕录制授权,临时签名的指定要求就是代码哈希。因此每次构建内容变化、每换一个 worktree,都需要重新授权;这些构建还与正式 helper 共用 Bundle ID,会替换正式版的授权条目。
## 决策
Host 以开发版构建源码检出,Bundle ID 为 `com.deepseek.harness.computer-use.dev`,名称为 `DSH Computer Use Dev`,应用包文件名为 `DSH Computer Use Dev.app`。macOS 隐私列表和 helper 返回的启动应用名称使用应用包文件名,因此文件名也需包含 Dev。正式版及其打包流程不变。按[共享 helper 决策](2026-10-09-cua-shared-native-helper.zh.md)的要求,授权仍归经 LaunchServices 启动的 helper 应用。
`sourceSigning` 由 macOS provider 定义,只接受默认的 `ad-hoc` 和指定已有证书及钥匙串绝对路径的 `{ identity, keychain }`。构建不创建、解锁或移除身份。开发者凭据与面向用户的 DSH home 分开,后者只保存选择证书的配置;共享 `cua` 包不持有 macOS 签名配置。
[开发者签名 skill](../../../skills/dsh-cua-macos-signing/SKILL.md)指导 Agent 在 `~/.cua-macos-keys/` 创建、复用和解锁独立身份,将证书指纹和钥匙串路径写入用户配置。工具把随机密码存入 `0600` 文件,允许 codesign 访问私钥,并把专用钥匙串加入用户搜索列表。文件锁让同一目录的初始化和移除串行执行;已有记录但钥匙串缺失时保留记录并报错。目录标记将身份管理限制在明确占用的初始空目录中。发现无关文件或身份文件软链接时,初始化和移除都会在修改前停止,避免开发者指定的路径导致无关内容被删除。
即使不使用时间戳服务器,证书签名也包含签名时间。开发构建记录每份未签名内容与身份对应的已签名目录,再次构建相同内容时复用该目录,保持可执行文件摘要和共享服务一致。缓存使用证书指纹;按证书名称选择时每次签名,避免续期后复用旧证书。签名失败会中止发布。开发者工具的诊断不包含密码或完整命令参数;构建 CLI 默认 `--identity -`,支持指定证书,正式打包也可省略 `--keychain`。
## 考虑过的替代方案
**把权限交给启动终端。** 直接启动服务时,macOS 把权限归属到启动 DSH 的终端或编辑器。该应用运行的所有程序,包括模型执行的命令,都会带上这些权限。
**源码构建自动创建固定身份。** 这会让普通启动生成开发者私钥,并把身份生命周期绑定到用户数据目录。显式开发者操作保留了减少重新授权的选择,也让证书的创建、移除和使用范围由开发者决定。
**把自签名身份放进登录钥匙串。** 登录钥匙串已解锁且在搜索列表中,但要让 `codesign` 不弹窗地使用其中的新私钥,需要用户的登录密码。
**只在签名期间把钥匙串加入搜索列表。** 正式打包脚本对单次调用这样处理。多个 worktree 并发构建时会互相覆盖读取和恢复的列表,构建被中断时条目也会残留。
**用固定的启动器运行各 worktree 的 helper。** 授权会跟随启动器,但任何本机进程都能让它带着这些授权运行任意可执行文件,暴露面与保存的私钥相同,还少了签名检查。
## 后果
同一证书签出的开发构建共用签名要求;授权后重建和跨 worktree 的实际权限保持仍需验证。同一用户的进程可读取保存的密码并使用该身份签名,正式 helper 使用不同的 Bundle ID。删除 DSH home 只移除配置,不删除开发者身份;删除该身份后应清理所有引用它的配置。真实签名 e2e 验证复用、解锁、不同输出目录的签名要求和移除后的搜索列表恢复。
@@ -1,22 +0,0 @@
# Bilingual-pair consistency record for 2026-09-17-multiline-display-dollar-math.md (docs/i18n/README.md): per heading
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
# After editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-09-17-multiline-display-dollar-math.md
/agent-note-multi-line-display-dollar-math:
en: 8f57c77aa43fbd6c
zh: ef5192184e4923d5
/agent-note-multi-line-display-dollar-math/problem:
en: e4b73644a982844c
zh: 48f86358242d6ccc
/agent-note-multi-line-display-dollar-math/decision:
en: d6b136b1f62285b7
zh: aba323bfdb800162
/agent-note-multi-line-display-dollar-math/alternatives-considered:
en: e0833b5674e648e7
zh: b0472020b07099b9
/agent-note-multi-line-display-dollar-math/consequences:
en: 70f16597bd640e93
zh: 0e0c9005f331e514
/agent-note-multi-line-display-dollar-math/testing:
en: f17a1453df91ce88
zh: 4f15be0139e8b694
@@ -1,66 +0,0 @@
# Agent Note: Multi-line display-dollar math
Status: implemented
English | [中文](2026-09-17-multiline-display-dollar-math.zh.md)
## Problem
Authors and models write `$$` display math in three shapes. The settled grammar rendered two of them: `$$math$$` on one line, and an opening `$$` alone on its line closed by `$$` alone on its line. The third shape — math on the opening line, a closing `$$` at the end of the last math line — did not:
```markdown
$$f(x)=x
\text{continued}$$
```
`micromark-extension-math` treats text after an opening `$$` as fence meta and accepts a closing fence only on a line whose content is exactly `$$` (after optional whitespace). The [markdown compatibility layer](../../archived/feature/2026-07-23-web-assistant-markdown.md) claimed only the same-line shape, and the settled renderer passes `node.value` to KaTeX while ignoring `node.meta`. The block therefore never closed: it consumed the document text that followed, `value` retained the literal `$$`, KaTeX raised `Can't use function '$' in math mode`, and [katex.tsx](../../../../packages/client/ui-primitives/src/markdown/katex.tsx)'s error arm painted the TeX source plus the swallowed prose in red.
In the session that surfaced this, one chat reply lost lines 22–55 of 55 to a single error span, and six note files lost every line after such a block (up to 500 lines) to one. A second defect shared the mechanism: when a later lone `$$` did close the flow, the opening line had already been assigned to `meta`, so the renderer silently dropped the first line of the formula.
## Decision
[mathCompatibility.ts](../../../../packages/client/ui-primitives/src/markdown/mathCompatibility.ts) gains multi-line display-dollar blocks. Its dollar construct keeps the opening line's text in `mathFlowValue` instead of fence meta and closes at a trailing `$$` on any later content line, so `$$a+b` continued by `c+d$$` becomes one math node.
A `$$` sequence that fails to close ends the candidate: immediately on a later line, and at that line's ending on the opening line (`$$x$$ trailing` fails there). Within the opening line the scan continues, so a later `$$` there still closes (`$$\verb*a$$a$$`). Scanning past either point would consume a later block's closing fence. A solitary `$` is content rather than a failed fence, so a formula that uses one — `$$P = \begin{cases} 1 & \text{if $x>0$} \end{cases}$$` — keeps the block open and closes normally wherever this flow owns the block. A trailing backslash escapes the line ending, not the next line's first character, so the escape run ends at the line ending instead of reaching into the next candidate. An unescaped `%` starts a TeX comment that runs to the line ending, so a `$$` at the end of a later line closes nothing when the comment holds text before it. A `%` whose text before the fence is empty or whitespace (`95%$$`, `95 %$$`) holds no comment text, and a block that opens and closes on one line keeps closing at that line's `$$`, as it did before this flow spanned lines. That state and its gate on the closing check belong to the dollar flow. The characters right after `\verb` or `\verb*` are that command's delimiter rather than a fence, so the delimiter and, when the region is empty, the `$` closing it are content; that tail is cleared at a line ending and after an escaped backslash (`\\verb` is a line break followed by letters), and a comment holds no commands. Otherwise verbatim regions are not parsed, which keeps `\verb`-bearing formulas on the same value path as every other input.
Its paragraph-interruption check declines instead of consuming the line ending, for the dollar construct only. Consuming it there keeps the paragraph open across the line boundary, so the document layer keeps the container and counts the next line lazy for every construct tried afterwards; that made upstream's `mathFlow`, the construct tried after this one, end empty inside a list item. Declining also leaves the interruption decision to that upstream flow, whose own check short-circuits without consuming and claims only the fences it accepts, so a candidate neither construct accepts (`**before` followed by `$$a\$b`) leaves its paragraph intact. The backslash construct has no competing upstream construct and keeps consuming there.
[parse.ts](../../../../packages/client/ui-primitives/src/markdown/parse.ts) registers `mathCompatibility()` after `math()`: micromark dispatches the extension registered last first, so the dollar flow above takes precedence over upstream's `mathFlow` while `math()` still owns inline `$…$`.
## Alternatives considered
**Keep the compatibility layer at same-line `$$…$$`.** The multi-line shape is what GitHub, KaTeX's auto-render, VS Code, and Typora accept, and the form models emit for multi-case formulas; leaving it unparsed means a red error span in place of the formula plus the rest of the document.
**Repair the parsed tree afterwards.** The flow consumes the swallowed text into `mathFlowValue`, where it is indistinguishable from math source; `meta` and `value` cannot be reassembled into the intended block, and the following blocks were never parsed at all.
**Rewrite `$$` fences in the source text before parsing.** The compatibility layer is a grammar extension by design; a text pass would need to locate code spans, fences, and escapes itself, and would change author text rather than parse it.
**Continue the dollar flow after a failed close.** That is what a plain `multiline` flag produces: `$$x$$ trailing\n\n$$y$$` then scans to the second block's `$$` and renders one math node holding both lines' text, where upstream inline math renders `x`, the prose, and `y` separately. Existing behavior for `$$x$$ trailing` already pinned the fallback, so the flow declines the candidate instead. Declining on any `$` rather than a full `$$` sequence also broke formulas that use a solitary `$` inside `\text{}`.
**End the attempt as soon as one line resists parsing.** Declining every failed same-line close swallows nothing but turns `$$a$b` lines into a quadratic scan and drops the solitary-`$` formulas above. The condition is the full `$$` sequence, not the character.
**Claim the paragraph interruption in the check.** Returning success there, as upstream's flow does for the fences it accepts, prevents the lazy bookkeeping that broke the list case, but it also ends the paragraph for candidates that later fail (`**before\n$$a\$b\nafter**` split in two and exposed its `**`). Declining instead lets upstream's own check decide.
**Apply the interruption guard to the backslash construct too.** The guard exists to protect the construct tried after this one; `\[`/`\(` have no second construct, and an unconditional guard dropped their multi-line blocks (`Prose line` followed by `\[`).
## Consequences
The three shapes now produce the same math node, so formulas stop truncating at the opening line and stop swallowing the prose after them. `$$` fence meta is no longer honored: `$$asciimath$$` parses as a formula whose value is `asciimath`, which upstream's flow treats as fence meta and drops. That spelling has no user in this repository, and the renderer never read `meta` anyway.
Six inputs still reach upstream's flow or stay in the surrounding paragraph, because the compatibility flow declines them. Trailing text after a closing `$$` (`$$x$$ trailing`, or a CJK quote mark closing a line of prose) resolves through upstream inline math, as the existing `markdown.client.spec.tsx` case pins — including when a later `$$` block follows. A `$$` sequence the line does not end after declines the block: immediately on a later line, and at that line's ending on the opening line. A `$$` block left open runs to the next lone `$$` or to EOF exactly as it did before this change, because the upstream flow closes at EOF.
The fourth is a multi-line block whose opening line carries a solitary `$` and that must interrupt a paragraph, with no blank line before it. Both flows decline the interruption there — this one by design, upstream because its fence meta stops at the `$` — so the block stays inside the paragraph and renders as inline math or not at all. Separating the block from the paragraph with a blank line makes this flow own it and render the display block.
The fifth follows from the comment gate: a block whose only closing fence sits behind a comment that holds text keeps scanning instead of closing there, so when nothing else closes it the block reaches the upstream flow as the shapes above do. A fence behind a whitespace-only comment closes the block there, which leaves a following lone `$$` as its own empty block where the flow it replaced kept one node.
The sixth is a multi-line block inside a container whose continuation line falls back to the container's indentation. `- Define:` followed by `$$f(x)=x` at the item's indentation and then a de-indented continuation line gives upstream's flow a fence meta and no value, so the reader sees an empty display block and the continuation as prose — the opening line is gone. Keeping the continuation at the same indentation, or moving the block out of the list, renders it normally. Both flow behaviors match the grammar this replaced; only the multi-line blocks this flow owns are new.
One further limitation is not a fallback: the flow reads the six characters before a fence and does not parse verbatim regions, so a fence inside an unterminated region (`\verb|$$`) closes the block there, and a region whose text ends in `\verb` (`\verb**\verb*$$`) has its fence read as that text's delimiter. The six characters before either fence are locally identical, so no rule that reads only that tail separates them.
Repeated unclosed candidates keep linear parser work: an open candidate declines at the next line that starts with a full `$$` it cannot close, rather than scanning each candidate to EOF, which would make 9.6 KB of `$$a$b` lines take ~4 s instead of ~16 ms. KaTeX-level failures in a formula's own source, such as `\arccot`, remain errors by design.
## Testing
`packages/client/ui-primitives/tests/markdown.client.spec.tsx` adds the multi-line case — opening-line math closed on a content line, closed by a lone fence, after a paragraph, inside a list item, and at column zero after a list item — and that case fails without the change. The same file also pins a multi-line formula that uses a solitary `$`, which like the case above renders only with this change. Further cases pin one condition each: a trailing-text candidate ahead of a later block (the flow must decline a full `$$` the line does not end after), a solitary `$` inside a single-line display formula (the flow must not decline on a single `$`), an escaped-dollar candidate that leaves its paragraph intact (the interruption check must not claim the paragraph), an unclosed `\[` candidate that leaves its paragraph intact (the guard must not apply to the backslash construct), a candidate ending in an odd backslash (the escape run must end at the line ending), a closing fence inside a TeX comment, a `%` the fence follows immediately or after whitespace, a second `%` inside an open comment, a `%` inside `\verb|…|`, the delimiter right after `\verb` and `\verb*` together with its empty region, an escaped backslash before `verb`, a verb tail that crosses no line ending, a comment ending in `verb`, a later-line trailing fence whose following prose upstream takes, a failed fence inside a comment, and bounded-work cases for repeated candidates with that backslash, without it, and ending in a verb tail.
The DOM compatibility corpus adds `math-multiline-display` with its `.settled.txt` and `.streaming.txt` fixtures, which also pin that a column-zero `$$` after a list item stays outside the list item. `apps/web/tests/math-rendering.e2e.ts` renders the multi-line shape in the browser lane.
@@ -1,66 +0,0 @@
# Agent Note: Multi-line display-dollar math
Status: implemented
[English](2026-09-17-multiline-display-dollar-math.md) | 中文
## Problem
作者和模型写 `$$` 行间公式时有三种写法。定稿语法只渲染其中两种:一行内开闭的 `$$math$$`,以及 `$$` 独占一行、由独占一行的 `$$` 闭合。第三种——开围栏同一行就带公式内容,闭合 `$$` 落在最后一行公式行行尾——不渲染:
```markdown
$$f(x)=x
\text{continued}$$
```
`micromark-extension-math` 把开围栏 `$$` 之后的同一行文本当作围栏 meta,并且只接受内容恰为 `$$`(后接可选空白)的行作为闭合围栏。[markdown 兼容层](../../archived/feature/2026-07-23-web-assistant-markdown.md)只认同一行开闭这一种形状,而定稿渲染器把 `node.value` 交给 KaTeX,不读 `node.meta`。于是该块始终不闭合:它吞掉后续正文,`value` 里残留字面 `$$`,KaTeX 抛出 `Can't use function '$' in math mode`,[katex.tsx](../../../../packages/client/ui-primitives/src/markdown/katex.tsx) 的错误分支把 TeX 原文连同被吞掉的正文一起显示为红色。
在暴露该问题的会话中,一条聊天回复 55 行里有 34 行(第 22–55 行)变成同一个错误块,六个笔记文件从出错点起的所有行(最多约 500 行)同样变成一个错误块。同一机制还带出第二个缺陷:当更靠后的独占行 `$$` 恰好闭合了这个流时,开围栏那一行已经归给 `meta`,渲染器于是静默丢掉公式的第一行。
## Decision
[mathCompatibility.ts](../../../../packages/client/ui-primitives/src/markdown/mathCompatibility.ts) 现在支持跨行的显示美元块。dollar 构造把开围栏同一行的文本留在 `mathFlowValue` 里而不是当作围栏 meta,并在后续任意内容行行尾的 `$$` 处闭合,于是 `$$a+b` 接 `c+d$$` 成为一个 math 节点。
闭合失败的 `$$` 序列会结束该候选:出现在后续行时立即结束;出现在开围栏行时在该行行尾结束(`$$x$$ trailing` 即在此放弃)。在开围栏行内扫描继续,因此同一行更靠后的 `$$` 仍能闭合(`$$\verb*a$$a$$`)。越过这两点继续扫描都会吞掉更靠后一个块的闭合围栏。孤立的一个 `$` 属于公式内容而不是失败的围栏,所以凡是本 flow 拥有该块的场合,用到它的公式——`$$P = \begin{cases} 1 & \text{if $x>0$} \end{cases}$$`——保持块打开并正常闭合。行尾反斜杠转义的是换行本身,不是下一行的第一个字符,因此转义状态在换行处结束,不会延续到下一个候选。未转义的 `%` 会开启延续到行尾的 TeX 注释:当注释在围栏之前还有文本时,块后续行行尾的 `$$` 不闭合任何东西;而围栏之前只有空白或什么都没有的 `%`(`95%$$`、`95 %$$`)不含注释文本,且在一行内开闭的块仍在该行的 `$$` 处闭合,与它跨行之前的行为一致。该状态及其对闭合判定的门控属于 dollar flow。`\verb` 或 `\verb*` 之后的那个字符是该命令的分隔符而不是围栏,因此该分隔符、以及区域为空时紧随其后的那个 `$` 都属于内容;这段尾部状态在换行处、以及在被转义的反斜杠之后清空(`\\verb` 是换行加字母,不是该命令),注释里也不识别命令。除此之外不解析原样文本区域,使含 `\verb` 的公式与其它输入走同一条 value 路径。
它的段落打断检查改为「不认领」,且只作用于 dollar 构造。在那里消耗换行会让段落跨行边界保持打开,document 层因此保留容器并把下一行对随后尝试的所有构造计为 lazy;这正是上游 `mathFlow`(本构造之后被尝试的构造)在列表项内以空块结束的原因。不认领同时把打断决定交给上游 flow:它自己的检查不消耗换行就短路返回,并且只认领它接受的围栏,因此两个构造都不接受的候选(`**before` 后接 `$$a\$b`)保持原段落。反斜杠构造没有竞争的上游构造,仍然照旧消耗换行。
[parse.ts](../../../../packages/client/ui-primitives/src/markdown/parse.ts) 把 `mathCompatibility()` 注册在 `math()` 之后:micromark 先派发最后注册的扩展,上面的 dollar flow 因此优先于上游 `mathFlow`,而行内 `$…$` 仍由 `math()` 负责。
## Alternatives considered
**兼容层维持只处理同一行 `$$…$$`。** 多行写法是 GitHub、KaTeX auto-render、VS Code、Typora 都接受的形状,也是模型写多分支公式时实际采用的形状;不解析它意味着公式位置显示为红色错误块,并连带吞掉后文。
**解析后修补语法树。** 被吞掉的正文已经进入 `mathFlowValue`,与数学源码无法区分;`meta` 和 `value` 无法还原成原意的块,而且后面的块根本没有被解析。
**解析前用文本替换改写 `$$` 围栏。** 兼容层的设计就是语法扩展;文本替换需要自己定位代码 span、围栏和转义,而且改的是作者原文而不是解析它。
**闭合失败后继续扫描。** 这正是单纯打开 `multiline` 会得到的行为:`$$x$$ trailing\n\n$$y$$` 会一路扫到第二个块的 `$$`,渲染成一个同时含着两行文本的 math 节点;而上游行内数学会把 `x`、正文和 `y` 分开渲染。`$$x$$ trailing` 的既有行为已经固定了这条回退,因此该构造放弃这个候选。对任意一个 `$`(而不是完整的 `$$` 序列)就放弃,还会破坏在 `\text{}` 内使用单个 `$` 的公式。
**只要有一行解析不下去就结束尝试。** 对每次同一行闭合失败都放弃虽然不再吞文,却会让 `$$a$b` 这类行变成平方级扫描,并丢掉上面那些含单个 `$` 的公式。判据是完整的 `$$` 序列,不是那个字符。
**在检查阶段认领段落打断。** 像上游 flow 那样返回成功可以避免破坏列表用例的 lazy 记账,但也会让最终失败的候选提前结束段落(`**before\n$$a\$b\nafter**` 被拆成两段并暴露 `**`)。改为不认领,把决定交给上游自己的检查。
**把打断守卫也用在反斜杠构造上。** 这个守卫是为了保护随后被尝试的构造;`\[`/`\(` 没有第二个构造,无条件套用会丢掉它们的跨行块(`Prose line` 后接 `\[`)。
## Consequences
三种写法现在产生同一个 math 节点,公式不再在开围栏那一行被截断,也不再吞掉其后的正文。`$$` 围栏 meta 不再生效:`$$asciimath$$` 会解析成 value 为 `asciimath` 的公式,而上游 flow 会把该段当作 fence meta 丢弃。这种写法在本仓库没有使用者,且渲染器本来就从不读 `meta`。
有六种输入仍会走到上游 flow,或留在所属段落里,因为兼容 flow 主动放弃它们。闭合 `$$` 之后还有文本时(`$$x$$ trailing`,或用中文引号收尾的散文行)走上游行内数学,这一点由 `markdown.client.spec.tsx` 中已有用例固定,后面还有别的 `$$` 块时同样如此。「后面不是行尾」的 `$$` 序列会让块放弃:出现在后续行时立即放弃,出现在开围栏行时在该行行尾放弃。未闭合的 `$$` 块仍像本次改动之前那样延伸到下一个独占行 `$$` 或文件结尾,因为上游 flow 在 EOF 处闭合。
第四种是「开围栏行含孤立 `$`、且需要打断段落、前面又没有空行」的多行块。两个 flow 都不认领这次打断——本 flow 出于设计,上游则因为它的围栏 meta 在 `$` 处即停止——该块因此留在段落里,渲染成行内数学或根本不渲染。用空行把块与段落分开,本 flow 就会拥有它并渲染 display。
第五种来自注释门控:当块唯一的闭合围栏位于「带文本的注释」之后时,块继续扫描而不是在那里闭合,若再没有别的围栏闭合它,就会像上面几种一样走到上游 flow。若围栏之前只有空白,则块在该处闭合,此后单独一行的 `$$` 会成为它自己的空块——而被替换掉的语法那时只给出一个节点。
第六种是容器内续行退回容器缩进的多行块。`- Define:` 之后以列表项缩进写 `$$f(x)=x`,再接一行不缩进的续行,上游 flow 会得到有 meta、无 value 的结果:读者看到一个空的 display 块加一行散文,公式首行无声消失。续行保持同样缩进,或把整块移到列表之外,即可正常渲染。这两种 flow 行为都与本次替换掉的语法一致;真正新增的只是「本 flow 拥有的多行块」。
另有一项不属于回退的限制:flow 只读取围栏之前的六个字符、不解析原样文本区域,因此未闭合区域内部的围栏(`\verb|$$`)会在那里闭合块,而内容以文本 `\verb` 结尾的区域(`\verb**\verb*$$`)其围栏会被读成该文本的分隔符。两种情况下围栏前的六个字符在局部完全相同,仅凭这段尾部无法区分。
重复的未闭合候选保持线性解析开销:未闭合的候选会在下一行遇到无法闭合的完整 `$$` 时放弃,而不是把每个候选都扫到 EOF——否则 9.6 KB 的 `$$a$b` 行会花约 4 秒而不是约 16 毫秒。公式自身源码层面的 KaTeX 失败,例如 `\arccot`,仍按设计报错。
## Testing
`packages/client/ui-primitives/tests/markdown.client.spec.tsx` 新增多行用例——开围栏行内容由内容行行尾 `$$` 闭合、由独占行 `$$` 闭合、出现在段落之后、出现在列表项内,以及列表项之后的零缩进围栏——该用例去掉改动即失败。同一文件还固定了「使用单个 `$` 的多行公式」,它与上面的多行用例一样只有本次改动才会渲染成 display。另有一组用例各自对应一个条件:后面还有块时的尾随文本候选(flow 必须放弃「后面不是行尾」的完整 `$$`)、单行显示公式内的单个 `$`(不得因为一个 `$` 就放弃)、保持段落完整性的转义美元候选(打断检查不得认领段落)、保持段落完整性的未闭合 `\[` 候选(守卫不得作用于反斜杠构造)、以奇数个反斜杠结尾的候选(转义状态必须在换行处结束)、TeX 注释内的闭合围栏、紧邻围栏或只隔空白的 `%`、已开启注释内的第二个 `%`、`\verb|…|` 内的 `%`、`\verb` 与 `\verb*` 之后的分隔符及其空区域、`verb` 之前的转义反斜杠、不跨行的 verb 尾部、以 `verb` 结尾的注释、后续行尾随文本围栏(其后正文由上游接管)、注释内失败的围栏,以及带该反斜杠、不带该反斜杠、以 verb 尾部结尾三种重复候选的有界解析开销。
DOM 兼容语料新增 `math-multiline-display` 及其 `.settled.txt`、`.streaming.txt`,同时固定了「列表项之后的零缩进 `$$` 不进入列表项内」。`apps/web/tests/math-rendering.e2e.ts` 在浏览器 lane 渲染多行写法。
@@ -12,8 +12,8 @@
en: 47b44ed9313c2ccf
zh: 7ad254f8ef88351d
/agent-note-subagent-end-turn-guidance-stays-in-the-delegation-paragraph/alternatives-considered:
en: d52fa1af379dea5b
zh: 4e404994f7a607dc
en: 3153c1e7563a3357
zh: 9841db2b0156075b
/agent-note-subagent-end-turn-guidance-stays-in-the-delegation-paragraph/verification:
en: be076216381ea64d
zh: 2da8d9819c20e2fe
@@ -29,7 +29,7 @@ The sentence stays one literal line. Rewording `the next result starts a new tur
**Move the guidance into the tool description.** The paragraph is assembled from the set of visible delegation tools and lists their names; the tool description describes one tool. Moving the sentence changes ownership without changing the behavior it prevents.
**Restore every prompt phrase reverted together with this sentence.** The read guidance and the `run_code` argument order are separate decisions with their own evidence; this note records only the delegation paragraph.
**Restore every prompt phrase reverted together with this sentence.** The read guidance ([2026-10-26 note](2026-10-26-read-guidance-line-ranges.md)) and the `run_code` argument order are separate decisions with their own evidence; this note records only the delegation paragraph.
## Verification
@@ -29,7 +29,7 @@ Status: implemented
**把说明移进工具描述。** 这段指导按可见委派工具集合组装并列出工具名,工具描述只描述单个工具。移动这句会改变归属,但不改变它阻止的行为。
**把与这句一起被回退的所有提示词都恢复。** read 指导与 `run_code` 参数顺序是各自有证据的独立决定;本记录只覆盖委派段。
**把与这句一起被回退的所有提示词都恢复。** read 指导([2026-10-26 note](2026-10-26-read-guidance-line-ranges.zh.md))与 `run_code` 参数顺序是各自有证据的独立决定;本记录只覆盖委派段。
## 验证
@@ -0,0 +1,22 @@
# Bilingual-pair consistency record for 2026-10-26-read-guidance-line-ranges.md (docs/i18n/README.md): per heading
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
# After editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-10-26-read-guidance-line-ranges.md
/agent-note-read-guidance-names-shell-readers-and-line-ranges:
en: 1926710614598539
zh: b0106d4a6a2d648d
/agent-note-read-guidance-names-shell-readers-and-line-ranges/problem:
en: 4189d516419d8d12
zh: 30d8496064dd44e3
/agent-note-read-guidance-names-shell-readers-and-line-ranges/decision:
en: 34f18307f7ad87af
zh: 21c466c6ce6c598d
/agent-note-read-guidance-names-shell-readers-and-line-ranges/alternatives-considered:
en: aea7ffb1159ab81a
zh: 4842ca79dc30654d
/agent-note-read-guidance-names-shell-readers-and-line-ranges/verification:
en: cbc16bfba4abe862
zh: 9a71e6c020c01f69
/agent-note-read-guidance-names-shell-readers-and-line-ranges/consequences:
en: cf55161ea197a7a9
zh: fa10ca413f8fd726
@@ -0,0 +1,35 @@
# Agent Note: Read guidance names shell readers and line ranges
Status: implemented
English | [中文](2026-10-26-read-guidance-line-ranges.zh.md)
## Problem
The read guidance named only `cat` as the shell command the read tool replaces, and described `offset`/`limit` as continuing a large file rather than reading a line range. #6057 recorded a long Session reading ranges with `sed -n X,Yp` 60 times. #6054 added `sed`, `head`, `tail`, and the line-range wording; a later prompt revert removed them together with unrelated changes.
The 2026-10-23 evaluation of the SDK profile (native tools, six benchmarks) measured this sentence as the only change: Pass@1 mean −0.03pp and Pass@3 mean +0.70pp over the five benchmarks that report Pass@3 (ProgramBench-New has none) versus the base commit, with mixed step and token deltas (ProgramBench-New −15.5 steps, Terminal-Bench-v4 +13.5 steps). The PTC mode runs did not include this variant; the section renders in every request where `read` is visible.
## Decision
Restore the #6054 wording, byte-identical: `Use the read tool — not shell commands like cat, sed, head, or tail — to inspect text files. Use offset and limit to read a line range or continue a large file.` The read tool and its guidance are owned by `@deepseek-ai/dsh-tool-fs`; write and edit guidance are unchanged.
## Alternatives considered
**Rely on the read tool schema.** The schema already documents `offset` and `limit`, but #6057 shows the model chose `sed` while that schema existed; the guidance is the only model-visible place that names `sed`, `head`, and `tail`.
**Restore every prompt phrase reverted together with this sentence.** The subagent end-turn sentence, the `run_code` argument order, the PTC syntax ownership, and the SDK persona are separate decisions; the first is already restored ([subagent end-turn note](2026-10-21-subagent-end-turn-guidance.md)), and the others carry their own evidence.
**Say "line range" without naming shell commands.** The observed failure was choosing shell commands, so the sentence names them.
## Verification
- `packages/fs/tool-fs/tests/tools.spec.ts` asserts the rendered guidance text.
- Every recorded `system-prompt*.expected.md` sidecar that renders the read guidance carries the restored sentence: `test:snapshot` replays the session and SDK set, and `pnpm run test:web:ci` replays the `snapshots/web` set (the `node 24 / snapshots and artifacts` lane runs both). Two pinned historical sidecars (`web/cordis-tool-round`, `web/schedule-catalog`) retain their recorded text.
- The evaluation above measured this single change on the native SDK profile.
## Consequences
- Every request with the read tool carries three more command names and a line-range clause, about 10 tokens.
- The model is directed to the read tool for line ranges instead of `cat`, `sed`, `head`, or `tail`.
- The benchmark measured this sentence as neutral; keeping it rests on the #6057 production observation, not on a measured quality gain.
@@ -0,0 +1,35 @@
# Agent Note: read 指导点名 shell 读取命令与行区间
Status: implemented
[English](2026-10-26-read-guidance-line-ranges.md) | 中文
## 问题
read 指导只把 `cat` 点名为 read 工具替代的 shell 命令,并把 `offset`/`limit` 描述成继续读大文件,而不是读行区间。#6057 记录了一个长 Session 用 `sed -n X,Yp` 读行区间 60 次。#6054 增加了 `sed`、`head`、`tail` 和行区间措辞;后来的一次提示词回退把它们和不相关的改动一起删掉了。
2026-10-23 对 SDK profile(native 工具、六个 benchmark)的评测里,这句是唯一改动:相对 base commit,Pass@1 均值 −0.03pp、Pass@3 均值 +0.70pp(该均值取报告 Pass@3 的五个 benchmark;ProgramBench-New 没有 Pass@3),步数与 token 变化方向不一(ProgramBench-New −15.5 步、Terminal-Bench-v4 +13.5 步)。PTC 模式的运行没有包含这个变体;只要 `read` 可见,这段就会进入每个请求。
## 决策
逐字恢复 #6054 的措辞:`Use the read tool — not shell commands like cat, sed, head, or tail — to inspect text files. Use offset and limit to read a line range or continue a large file.` read 工具及其指导归 `@deepseek-ai/dsh-tool-fs` 所有;write 与 edit 的指导不变。
## 考虑过的替代方案
**只依赖 read 工具 schema。** schema 已经写明 `offset` 与 `limit`,但 #6057 显示在那个 schema 存在时模型仍选择 `sed`;指导是唯一点名 `sed`、`head`、`tail` 的模型可见位置。
**把与这句一起被回退的所有提示词都恢复。** subagent 结束本轮句、`run_code` 参数顺序、PTC 语法归属、SDK persona 是各自独立的决定;第一项已经恢复([subagent 结束本轮 note](2026-10-21-subagent-end-turn-guidance.zh.md)),其余各有自己的证据。
**只说“行区间”,不点名 shell 命令。** 观测到的失败就是模型选择 shell 命令,所以这句点名它们。
## 验证
- `packages/fs/tool-fs/tests/tools.spec.ts` 断言渲染出的指导文本。
- 所有渲染 read 指导的 `system-prompt*.expected.md` sidecar 都带有恢复后的句子:`test:snapshot` 回放 session 与 SDK 集合,`pnpm run test:web:ci` 回放 `snapshots/web` 集合(`node 24 / snapshots and artifacts` lane 两条都跑)。两个显式 pin 的历史 sidecar(`web/cordis-tool-round`、`web/schedule-catalog`)保留录制文本。
- 上面的评测在 native SDK profile 上只改了这一句进行测量。
## 影响
- 只要 read 工具存在,每个请求多三个命令名和一句行区间说明,约 10 token。
- 模型读行区间时被引导到 read 工具,而不是 `cat`、`sed`、`head`、`tail`。
- benchmark 上这句测得为中性;保留它依据的是 #6057 的生产观测,而不是测到的质量收益。
@@ -0,0 +1,22 @@
# Bilingual-pair consistency record for 2026-11-01-locked-python-runtime-deploy.md (docs/i18n/README.md): per heading
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
# After editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-11-01-locked-python-runtime-deploy.md
/agent-note-locked-python-runtime-deployment:
en: 6a06f1078957df51
zh: d7c2146381443ced
/agent-note-locked-python-runtime-deployment/problem:
en: 2293006c8bae6a1a
zh: 10670f2d703777dd
/agent-note-locked-python-runtime-deployment/decision:
en: 223079c8e1e3a44e
zh: c906266cb0b231bc
/agent-note-locked-python-runtime-deployment/alternatives-considered:
en: 978c94c7013056a5
zh: afae764120582b6a
/agent-note-locked-python-runtime-deployment/testing:
en: d862dd609725c686
zh: 8889c5d9002bbc68
/agent-note-locked-python-runtime-deployment/consequences:
en: 41827db476b30a43
zh: 5a7652f310a69909
@@ -0,0 +1,33 @@
# Agent Note: Locked Python runtime deployment
Status: implemented
English | [中文](2026-11-01-locked-python-runtime-deploy.zh.md)
## Problem
Python executable packaging needs the same dependency versions and integrity records as the workspace lockfile. Legacy hoisted deployment ignores that lockfile and resolves compatible registry versions again: a manifest permitting `tslib@^2.8.1` can install `2.9.1` while the lockfile records `2.8.1`. Office archive preparation then rejects the missing locked integrity. A successful frozen workspace install does not constrain that later legacy deployment.
## Decision
The Python builder uses pnpm's default deployment, which derives a dedicated production lockfile from the workspace lockfile and installs it frozen. This replaces only the deployment strategy in the [single-executable distribution decision](../architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md); its runtime architecture and carrier layout remain active.
Deployment retains the source lockfile's peer settings, including `autoInstallPeers: true`. Frozen installation preserves already resolved versions and peer relationships instead of resolving new registry peers. Forcing `autoInstallPeers: false` conflicts with the derived lockfile settings and fails frozen installation. The resulting tree need not equal the legacy tree resolved with peers disabled; selected dependencies must come from the locked graph. `verify-runtime-closure.ts` independently requires explicit workspace peers and preset plugins before deployment, and `verifyStagedDependencies` rejects missing direct dependency directories after materialization and deduplication.
[`scripts/python-runtime-deploy.pnpmfile.mjs`](../../../../scripts/python-runtime-deploy.pnpmfile.mjs) translates reviewed local `file:` approvals to the exact absolute file URLs used in the derived lockfile. Approval, denial, pending decisions, and strict build validation remain unchanged. Default deployment executes approved installation scripts; their generated files can enter the staged payload. Workspace policy and the source lockfile remain unchanged.
## Alternatives considered
**Keep legacy deployment and add missing integrity records.** This would accept versions the source lockfile did not select and repeat the failure after another compatible release.
**Disable automatic peer installation during default deployment.** This conflicts with the source lockfile settings. Re-resolving a separate graph would give up the source lockfile's authority.
**Disable deployment scripts or approve local packages by name.** Skipping scripts omits required postinstall preparation. Name-only approvals do not authorize non-semver local file dependencies; exact identity translation preserves the existing review decisions.
## Testing
A real pnpm comparison with the same manifest and lockfile installs `tslib@2.9.1` through legacy deployment and retains `2.8.1` through default deployment. The Linux executable and locally built installed-wheel SDK smokes pass. Focused tests reject missing dependency directories and deployment flag regressions, preserve approval states, and exercise both metadata path bases. Cross-platform packaging and installed-wheel validation remain owned by CI.
## Consequences
Runtime versions remain traceable to the workspace lockfile, and incomplete direct dependency trees fail before executable packaging. Installation script outputs and locked peer selections can differ from legacy deployment, so byte-identical payloads are not promised. SSH helper deployment has a separate closure manifest and native payload validation; its legacy deployment is outside this Python runtime repair.
@@ -0,0 +1,33 @@
# Agent Note: Python 运行时按锁文件部署
Status: implemented
[English](2026-11-01-locked-python-runtime-deploy.md) | 中文
## 问题
Python 可执行文件打包需要使用与工作区锁文件相同的依赖版本和完整性记录。legacy hoisted 部署忽略该锁文件,重新解析兼容的注册表版本:允许 `tslib@^2.8.1` 的 manifest 可能安装 `2.9.1`,而锁文件记录的是 `2.8.1`。Office 归档准备随后因缺少锁定的完整性记录而拒绝构建。工作区的冻结安装成功,并不能约束后续的 legacy 部署。
## 决策
Python 构建器使用 pnpm 的默认部署,从工作区锁文件派生独立的生产锁文件,再冻结安装。本记录只替代[单文件可执行程序分发决策](../architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md)中的部署策略;其中的运行时架构和载体布局继续有效。
部署保留源锁文件的对等依赖设置,包括 `autoInstallPeers: true`。冻结安装保留已解析的版本和对等依赖关系,不会重新解析新的注册表对等依赖。强制指定 `autoInstallPeers: false` 与派生锁文件的设置冲突,会使冻结安装失败。生成的依赖树不必与关闭对等依赖解析的 legacy 依赖树相同,但所选依赖必须来自锁定的依赖图。`verify-runtime-closure.ts` 独立要求在部署前显式声明工作区对等依赖及 preset 插件;`verifyStagedDependencies` 在链接实体化与去重后拒绝缺失的直接依赖目录。
[`scripts/python-runtime-deploy.pnpmfile.mjs`](../../../../scripts/python-runtime-deploy.pnpmfile.mjs) 将已审阅的本地 `file:` 审批转换为派生锁文件使用的精确绝对文件 URL。批准、拒绝、未决定状态和严格构建校验均保持不变。默认部署执行已批准的安装脚本,其生成的文件可能进入暂存载荷。工作区策略和源锁文件保持不变。
## 考虑过的替代方案
**保留 legacy 部署并补全缺失的完整性记录。** 这会接受源锁文件没有选择的版本,并在另一个兼容版本发布后重复出现故障。
**在默认部署中关闭对等依赖自动安装。** 这与源锁文件的设置冲突;重新解析另一套依赖图会放弃源锁文件的决定权。
**禁用部署脚本,或按名称批准本地包。** 跳过脚本会遗漏必需的 postinstall 准备。仅按名称批准不能授权非 semver 的本地文件依赖;转换精确标识可以保留原有审批决定。
## 测试
真实 pnpm 对照使用相同 manifest 和锁文件,legacy 部署安装 `tslib@2.9.1`,默认部署保留 `2.8.1`。Linux 可执行文件及本地构建并安装的 wheel SDK 冒烟测试通过。定向测试拒绝缺失的依赖目录和部署参数回退,保留审批状态,并覆盖两种 metadata 路径基准。跨平台打包及安装后 wheel 校验仍由 CI 负责。
## 影响
运行时版本可以追溯到工作区锁文件,不完整的直接依赖树会在可执行文件打包前失败。安装脚本产物和锁定的对等依赖选择可能与 legacy 部署不同,因此不承诺载荷逐字节相同。SSH helper 部署使用独立的闭包 manifest 和原生载荷校验,其 legacy 部署不属于本次 Python 运行时修复范围。

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