merge: bring master into feat/pwsh-persistent-pty

This commit is contained in:
Huanqi Cao
2026-08-15 11:14:45 +08:00
818 changed files with 7336 additions and 69077 deletions
@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-08-12-collapsed-sidebar-shared-entry-motion.md
2026-08-12-collapsed-sidebar-shared-entry-motion.md: c5bc18973db693cf9ba60800fbcf7720593dbad5
2026-08-12-collapsed-sidebar-shared-entry-motion.zh.md: b87ef6322d548061cc2e29c0e8f9896098c09041
@@ -0,0 +1,33 @@
# Agent Note: Collapsed sidebar upper controls share one entry motion
Status: implemented
Archived: 2026-08-12
English | [中文](2026-08-12-collapsed-sidebar-shared-entry-motion.zh.md)
## Problem
The collapsed sidebar rail renders four upper controls owned by two packages: the shell owns the toggle and New Session, while the workspace region owns add and search. Their opacity timing matched, but their geometry did not. Right-aligned controls moved with the narrowing column while left-aligned controls stayed fixed, so add appeared slower than search even under the same fade.
The bottom settings control has a different role. It is pinned to the rail foot and must not join the upper controls' horizontal entry.
## Decision
At the rail settle point, the four upper 36px controls start from one left-anchored layout and share one `150ms` animation from `translateX(49px)` to their final 10px inset. The shell applies the translation to its toggle and New Session seats and once to the workspace region, so add and search inherit the same path without nested transforms. Opacity uses the same animation timeline.
The settings seat uses a separate opacity-only keyframe with the same duration and easing. A page that starts collapsed renders the rail without an entry animation, and reduced-motion mode disables both keyframes.
## Alternatives considered
**Keep every rail control fixed at its final inset.** This removes the mismatch, but it also removes the requested horizontal entry from the four upper controls.
**Animate each workspace button independently.** This would duplicate shell timing inside `ui-workspace` and could apply both a region and child transform. Translating the registered region once keeps animation ownership in the sidebar shell.
**Translate the settings control with the upper controls.** Rejected because settings is a bottom-pinned foot action, not part of the upper control sequence.
## Consequences
- Toggle, New Session, add, and search follow the same horizontal coordinates throughout collapse.
- Settings fades at its final horizontal coordinate.
- Static collapsed renders retain their final geometry without startup motion.
- Style tests pin the shared animation assignments, translation distance, base anchors, and settings exception.
@@ -0,0 +1,33 @@
# Agent Note: 收起侧栏的上方控件共用同一进入动画
Status: implemented
Archived: 2026-08-12
[English](2026-08-12-collapsed-sidebar-shared-entry-motion.md) | 中文
## Problem
收起侧栏轨道的四个上方控件由两个包渲染:外壳持有侧栏切换与新建会话,Workspace 区域持有添加和搜索。它们的透明度时序相同,但几何行为不同。右对齐控件会随栏变窄而移动,左对齐控件则保持不动,因此添加即使使用相同淡入,视觉上仍比搜索慢。
底部设置控件承担不同角色。它固定在轨道页脚,不能参与上方控件的横向进入。
## Decision
轨道落位时,四个 36px 上方控件从同一个左对齐布局开始,共用一段 `150ms` 动画,从 `translateX(49px)` 移动到最终 10px 内边距。外壳把位移分别应用于侧栏切换、新建会话,并只对 Workspace 区域应用一次,因此添加与搜索会继承同一路径,不产生嵌套变换。透明度使用同一条动画时间线。
设置控件使用时长与缓动相同、但只改变透明度的独立关键帧。页面初始即为收起状态时不会播放进入动画;减少动态效果模式会禁用两段关键帧。
## Alternatives considered
**把每个轨道控件固定在最终内边距。** 这能消除不一致,但也会移除四个上方控件所需的横向进入效果。
**分别为每个 Workspace 按钮添加动画。** 这会在 `ui-workspace` 中重复外壳时序,还可能同时应用区域与子控件变换。只移动一次已注册区域,可以让动画继续由侧栏外壳持有。
**让设置控件随上方控件一起移动。** 不予采纳,因为设置是固定在底部的页脚操作,不属于上方控件序列。
## Consequences
- 侧栏切换、新建会话、添加与搜索在整个收起过程中使用相同横坐标。
- 设置在最终横坐标上淡入。
- 静态收起渲染保持最终几何,不播放启动动画。
- 样式测试固定共用动画分配、位移距离、基础锚点与设置例外。
+3
View File
@@ -97,6 +97,9 @@
"bug-fix/2026-08-10-web-favicon-dark-mode.i18n.yaml": "sha256:859c4399f9a017a68ba89552fdafa05e73c0599d94cee9551c84ea5b749a14f3",
"bug-fix/2026-08-10-web-favicon-dark-mode.md": "sha256:4d17e247abd76ae3aed5fb4e075fd66a2838292f89f7021c82a79fe37ed905e6",
"bug-fix/2026-08-10-web-favicon-dark-mode.zh.md": "sha256:7bbff8a3b7061c127afcc75cd2a8043b02a999b78c0180edd8f7e4807fcfe71d",
"bug-fix/2026-08-12-collapsed-sidebar-shared-entry-motion.i18n.yaml": "sha256:3ce4f6e39e173fc304bf64deca9c95bcddc1dbb492e065ca8c267a7a40788588",
"bug-fix/2026-08-12-collapsed-sidebar-shared-entry-motion.md": "sha256:7b169aa4543edfc965de5a8b7b9e60aa9d9d5218693cd0b57908e2d482280723",
"bug-fix/2026-08-12-collapsed-sidebar-shared-entry-motion.zh.md": "sha256:88db36c698800bf55c3c7531d6f92665576d978c29c15ff7d74215fb93376cb1",
"feature/2026-06-14-acp-agent-client-protocol.i18n.yaml": "sha256:006795baa43ae962a8d125cc0f1e9f134bc2ee9fb758b6e7669e3fa0126e1918",
"feature/2026-06-14-acp-agent-client-protocol.md": "sha256:6828c0af74bb3fb96206ca6b21c0e56a000b50e4744aad4bc2c05092f3a5a31b",
"feature/2026-06-14-acp-agent-client-protocol.zh.md": "sha256:ba104e841a1fb84edbd3b6c8119d50445b7785255a7a8d13bb9ac8a2cb4d2e69",
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-06-13-capability-seams.md
2026-06-13-capability-seams.md: efb000631c6dfe91ab648b102a14a27c30d790b4
2026-06-13-capability-seams.zh.md: bf6571dfa5c3a90f856480bc22c6560ed65b3e77
2026-06-13-capability-seams.md: 2a166278ea454895177fa12b58f5493276f19cd1
2026-06-13-capability-seams.zh.md: 0874af5826960ab9e718eb07b00c12b446edfd78
@@ -1,4 +1,4 @@
# Agent Note: Capability seams — Service Definition / Service provider / Consumer roles
# Agent Note: Capability seams — Service Definition / Service Provider / Consumer roles
Status: implemented
@@ -15,16 +15,18 @@ This is distinct from "who provides vs. needs a capability at runtime", which Co
A swappable capability has **three roles**:
1. **Service Definition** — the Cordis `Service` and vocabulary types owning `ctx.<key>` and depending only on the vocabulary the contract needs (e.g. `dsh-shell`: `ShellExecutor`, `ShellRunResult`, `ShellProcess`). A definition may be an abstract class or a concrete registry service; it is never a TypeScript `interface`.
2. **Service provider** — a plugin that supplies or registers an implementation (e.g. `dsh-bash-local`: subprocesses, process-group kills, spill-file truncation). Sandboxed and remote providers are sibling packages implementing or registering against the same Service Definition.
2. **Service Provider** — a plugin that supplies or registers an implementation (e.g. `dsh-bash-local`: subprocesses, process-group kills, spill-file truncation). Sandboxed and remote providers are sibling packages implementing or registering against the same Service Definition.
3. **Consumer** — what the model and plugins program against (e.g. `dsh-tool-bash`: the `bash` schema, with background handles registered into the generic job runtime). Consumers inject the service key and never import provider-specific types.
Service providers and Consumers then evolve independently: a sandboxed executor replaces `dsh-bash-local` without touching a tool schema.
The role names use title case: **Service Definition**, **Service Provider**, and **Consumer**. Generic uses of `provider` and `consumer` remain lowercase.
Roles normally use separate packages when they evolve independently, but the split is not mandatory when the roles are genuinely one concern: the LLM seam folds Service Definition and Consumer into `dsh-llm` (the Consumer is the loop itself, not a swappable schema surface) with adapters as Service provider packages. Don't split preemptively — a capability with one conceivable provider and one Consumer stays one package until a second appears.
Service Providers and Consumers then evolve independently: a sandboxed executor replaces `dsh-bash-local` without touching a tool schema.
Roles normally use separate packages when they evolve independently, but the split is not mandatory when the roles are genuinely one concern: the LLM seam folds Service Definition and Consumer into `dsh-llm` (the Consumer is the loop itself, not a swappable schema surface) with adapters as Service Provider packages. Don't split preemptively — a capability with one conceivable provider and one Consumer stays one package until a second appears.
## Terminology: "seam" names the trio, not the interface
A **seam** is the whole capability — the three roles together: a **Service Definition** (the Cordis `Service` that owns `ctx.<key>` and the vocabulary), one or more **Service providers**, and one or more **Consumers**. `packages/shell` is the canonical example — `dsh-shell` / `dsh-bash-local`+`dsh-bash-sandbox` / `dsh-tool-bash`. A package may own multiple roles, but one role alone is not the seam. The term "seam" is reserved for this complete capability; name a constituent by its role, class, service, contract, or extension point. The [glossary](../../../../docs/glossary.md#capability-seam) is the canonical entry.
A **seam** is the whole capability — the three roles together: a **Service Definition** (the Cordis `Service` that owns `ctx.<key>` and the vocabulary), one or more **Service Providers**, and one or more **Consumers**. `packages/shell` is the canonical example — `dsh-shell` / `dsh-bash-local`+`dsh-bash-sandbox` / `dsh-tool-bash`. A package may own multiple roles, but one role alone is not the seam. The term "seam" is reserved for this complete capability; name a constituent by its role, class, service, contract, or extension point. The [glossary](../../../../docs/glossary.md#capability-seam) is the canonical entry.
## Alternatives considered
@@ -33,4 +35,4 @@ A **seam** is the whole capability — the three roles together: a **Service Def
## Consequences
Separating roles adds packages and boilerplate (`package.json`, `tsconfig`, README, and injection wiring). In return, Service providers and Consumers ship and version independently, and a new backend never risks the model-facing contract. [AGENTS.md](../../../../AGENTS.md) and [architecture.md](../../../../docs/architecture.md) carry the rule; the bash trio is the reference template. This Agent Note records why independently changing roles normally split while genuinely shared concerns may remain folded.
Separating roles adds packages and boilerplate (`package.json`, `tsconfig`, README, and injection wiring). In return, Service Providers and Consumers ship and version independently, and a new backend never risks the model-facing contract. [AGENTS.md](../../../../AGENTS.md) and [architecture.md](../../../../docs/architecture.md) carry the rule; the bash trio is the reference template. This Agent Note records why independently changing roles normally split while genuinely shared concerns may remain folded.
@@ -1,4 +1,4 @@
# Agent Note: 能力 seam——Service Definition / Service provider / Consumer 角色
# Agent Note: 能力 seam——Service Definition / Service Provider / Consumer 角色
Status: implemented
@@ -15,22 +15,24 @@ harness 具有可替换的能力:当前是 bash 执行,未来会有沙箱化
一项可替换的能力包含**三个角色**:
1. **Service Definition**——拥有 `ctx.<key>` 的 Cordis `Service` 和词汇类型,仅依赖约定所需的词汇(例如 `dsh-shell``ShellExecutor``ShellRunResult``ShellProcess`)。Service Definition 可以是抽象类,也可以是具体的注册表服务;绝不是 TypeScript `interface`
2. **Service provider**——提供或注册实现的插件(例如 `dsh-bash-local`:子进程、进程组 kill、spill 文件截断)。沙箱化和远程 Service provider 是依据同一 Service Definition 实现或注册的兄弟包。
3. **Consumer**——模型和插件编程所面向的内容(例如 `dsh-tool-bash``bash` schema,后台句柄注册到通用任务运行时)。Consumer 注入服务键,从不导入 Service provider 特有的类型。
2. **Service Provider**——提供或注册实现的插件(例如 `dsh-bash-local`:子进程、进程组 kill、spill 文件截断)。沙箱化和远程 Service Provider 是依据同一 Service Definition 实现或注册的兄弟包。
3. **Consumer**——模型和插件编程所面向的内容(例如 `dsh-tool-bash``bash` schema,后台句柄注册到通用任务运行时)。Consumer 注入服务键,从不导入 Service Provider 特有的类型。
Service provider 与 Consumer 由此独立演进:沙箱化执行器替换 `dsh-bash-local` 时无需触碰任何工具 schema
角色名使用标题式大小写:**Service Definition**、**Service Provider** 和 **Consumer**。泛指的 `provider``consumer` 仍使用小写
当角色独立演进时,通常使用不同的包;但当各角色确实属于同一个关注点时,并非必须拆分:LLM(大语言模型) seam 将 Service Definition 和 Consumer 合并为 `dsh-llm`Consumer 是 agent loop(智能体循环)本身,而非可替换的 schema 接口),适配器作为 Service provider 包。不要预防性地拆分——如果一项能力只有一种可设想的 Service provider 和一个 Consumer,就保持为一个包,直到出现第二个
Service Provider 与 Consumer 由此独立演进:沙箱化执行器替换 `dsh-bash-local` 时无需触碰任何工具 schema
当角色独立演进时,通常使用不同的包;但当各角色确实属于同一个关注点时,并非必须拆分:LLM(大语言模型) seam 将 Service Definition 和 Consumer 合并为 `dsh-llm`Consumer 是 agent loop(智能体循环)本身,而非可替换的 schema 接口),适配器作为 Service Provider 包。不要预防性地拆分——如果一项能力只有一种可设想的 Service Provider 和一个 Consumer,就保持为一个包,直到出现第二个。
## 术语:seam 指三者组合,而非接口
一个 **seam** 是完整的能力——三个角色合在一起:**Service Definition**(拥有 `ctx.<key>` 和词汇的 Cordis `Service`)、一个或多个 **Service provider**,以及一个或多个 **Consumer**`packages/shell` 是规范范例——`dsh-shell` / `dsh-bash-local`+`dsh-bash-sandbox` / `dsh-tool-bash`。一个包可以承担多个角色,但单个角色本身不是 seam。「seam」一词严格保留给这种完整能力;命名其中一个组成部分时,应使用其角色、类、服务、约定或扩展点。[术语表](../../../../docs/glossary.md#capability-seam)是规范条目。
一个 **seam** 是完整的能力——三个角色合在一起:**Service Definition**(拥有 `ctx.<key>` 和词汇的 Cordis `Service`)、一个或多个 **Service Provider**,以及一个或多个 **Consumer**`packages/shell` 是规范范例——`dsh-shell` / `dsh-bash-local`+`dsh-bash-sandbox` / `dsh-tool-bash`。一个包可以承担多个角色,但单个角色本身不是 seam。「seam」一词严格保留给这种完整能力;命名其中一个组成部分时,应使用其角色、类、服务、约定或扩展点。[术语表](../../../../docs/glossary.md#capability-seam)是规范条目。
## 曾考虑的替代方案
- **始终合并各角色**:否决。因为它会重新耦合独立变化的 Service Definition、Service provider 和 Consumer。
- **始终合并各角色**:否决。因为它会重新耦合独立变化的 Service Definition、Service Provider 和 Consumer。
- **`@cordisjs/plugin-capability`**:这是完全不同的维度。它是一个权限/能力*安全*服务(具名权限加继承,通过 `ctx.capability.test` 针对会话检测这些权限),是延后的权限/沙箱工作(`tools/pre-execute` deny/ask 门)的候选方案,不是替换实现的机制。混淆这两个「能力」概念正是本 Agent Note 所指出的陷阱。
## 后果
分离角色会增加包和样板代码(`package.json``tsconfig`、README 和注入接线)。换来的是:Service provider 与 Consumer 独立发布和版本管理,新后端永远不会波及面向模型的约定。[AGENTS.md](../../../../AGENTS.md) 和 [architecture.md](../../../../docs/architecture.md) 载有这项规则;bash 三件套是参考模板。本 Agent Note 记录为什么独立变化的角色通常需要拆分,而确实共享的关注点可以保持合并。
分离角色会增加包和样板代码(`package.json``tsconfig`、README 和注入接线)。换来的是:Service Provider 与 Consumer 独立发布和版本管理,新后端永远不会波及面向模型的约定。[AGENTS.md](../../../../AGENTS.md) 和 [architecture.md](../../../../docs/architecture.md) 载有这项规则;bash 三件套是参考模板。本 Agent Note 记录为什么独立变化的角色通常需要拆分,而确实共享的关注点可以保持合并。
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-06-17-filesystem-capability-seam.md
2026-06-17-filesystem-capability-seam.md: 953ef0eaafe8f654bf55c3e5c4208560243c3d0b
2026-06-17-filesystem-capability-seam.zh.md: 1073226594acbe8c1c596bb4b2dcc959b20c0769
2026-06-17-filesystem-capability-seam.md: 6265aebf5e7ffdd4ec4dc0083aa55adb34ee78a1
2026-06-17-filesystem-capability-seam.zh.md: 024421ed59fa1e6c29bcc12ab51f69ac19a0c231
@@ -53,7 +53,7 @@ The filesystem seam uses the same dependency direction as the bash trio:
`@deepseek-ai/dsh-tool-fs` depends on `@deepseek-ai/dsh-fs`, `@deepseek-ai/dsh-tools`, `@deepseek-ai/dsh-system-prompt`, and `cordis`. It registers model-facing tools and prompt sections. It must not import `node:fs`, `node:path`, or `@deepseek-ai/dsh-fs-local`; filesystem execution always goes through `ctx.fs`. If the implementation needs concrete agent or session helper types, those dependencies belong in `tool-fs`; they must not leak back into `dsh-fs`.
The root `tool-fs` plugin registers the full filesystem tool suite (`read`, `write`, and `edit`) by composing the per-tool registration helpers. It injects `fs` and never imports a Service provider package.
The root `tool-fs` plugin registers the full filesystem tool suite (`read`, `write`, and `edit`) by composing the per-tool registration helpers. It injects `fs` and never imports a Service Provider package.
## `ctx.fs` contract
@@ -139,7 +139,7 @@ The defensive-pattern classes this repo has been bitten by are pinned directly:
## Alternatives considered
- **Model-facing tools directly over `node:fs`** — the tool package would own execution policy, path resolution, atomic writes, text decoding, and edit semantics at once, coupling the three independently-changing concerns the Problem names and churning schemas on any backend swap.
- **One combined `dsh-fs-tools` package** — the pre-seam shape; rejected for the same Service Definition / Service provider / Consumer split as bash, and the combined name never became public API.
- **One combined `dsh-fs-tools` package** — the pre-seam shape; rejected for the same Service Definition / Service Provider / Consumer split as bash, and the combined name never became public API.
- **Observed-state on `ctx.fs`** — the shape this Agent Note first landed; superseded by [the split-fs-seam Agent Note](../simplification/2026-06-26-fsspec-style-fs-seam.md) and [the event-gate Agent Note](2026-06-26-file-context-as-event-gate.md): a sandboxed/remote backend must not inherit model-facing observation policy, so the provider keeps only the version token and the optional version-guarded mutation.
## Consequences
@@ -53,7 +53,7 @@ Consumer 包仅依赖 Service Definition 包,从不依赖 `dsh-fs-local`。需
`@deepseek-ai/dsh-tool-fs` 依赖 `@deepseek-ai/dsh-fs``@deepseek-ai/dsh-tools``@deepseek-ai/dsh-system-prompt``cordis`。它注册面向模型的工具和提示词段落。它禁止导入 `node:fs``node:path``@deepseek-ai/dsh-fs-local`;文件系统执行始终通过 `ctx.fs`。如果实现需要具体的 agent(智能体)或会话辅助类型,这些依赖属于 `tool-fs`;它们禁止回漏到 `dsh-fs` 中。
`tool-fs` 插件通过组合各工具的注册辅助函数来注册完整的文件系统工具套件(`read``write``edit`)。它注入 `fs`,从不导入 Service provider 包。
`tool-fs` 插件通过组合各工具的注册辅助函数来注册完整的文件系统工具套件(`read``write``edit`)。它注入 `fs`,从不导入 Service Provider 包。
## `ctx.fs` 约定
@@ -139,7 +139,7 @@ Consumer 包仅依赖 Service Definition 包,从不依赖 `dsh-fs-local`。需
## 曾考虑的替代方案
- **面向模型的工具直接基于 `node:fs`**:工具包将同时承担执行策略、路径解析、原子写入、文本解码和编辑语义,耦合问题部分所列的三个独立变化的关注点,且任何后端替换都会搅动 schema。
- **单一合并包 `dsh-fs-tools`**seam 之前的形态;以与 bash 相同的 Service Definition / Service provider / Consumer 拆分理由否决,且合并名称从未成为公开 API。
- **单一合并包 `dsh-fs-tools`**seam 之前的形态;以与 bash 相同的 Service Definition / Service Provider / Consumer 拆分理由否决,且合并名称从未成为公开 API。
- **观测状态放在 `ctx.fs` 上**:本 Agent Note 最初落地的形态;被 [拆分文件系统 seam Agent Note](../simplification/2026-06-26-fsspec-style-fs-seam.md) 和 [事件门控 Agent Note](2026-06-26-file-context-as-event-gate.md) 取代:沙箱/远程后端不应继承面向模型的观测策略,因此提供方只保留版本令牌和可选的版本守护变更。
## 后果
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-06-18-agent-lifecycle-and-ownership-contracts.md
2026-06-18-agent-lifecycle-and-ownership-contracts.md: f0f9f90b15dee9155cfb1e8c772503642ff34be3
2026-06-18-agent-lifecycle-and-ownership-contracts.zh.md: a5f6ca10dbe7c71281cccdaf83fff95139ad2d79
2026-06-18-agent-lifecycle-and-ownership-contracts.md: 9bc558bfce75892b0ebdb80e9f8735d440cabaf4
2026-06-18-agent-lifecycle-and-ownership-contracts.zh.md: 65e0018c3f2ba56043898050c8e0b49929f8e7e7
@@ -37,7 +37,7 @@ These invariants hold and are pinned by tests:
## Session owner tokens are unique among live agents
The bash owner-token comparison relies on the shared `Agent.id`/`SessionId` being unique among live agents. Concurrent same-ID operations may both prepare privately, but publication enters the session and agent in order; `SessionStore.enter()` rejects a duplicate live session id, and every losing transaction rolls its private state back. A programmatic caller therefore cannot publish two live agents with one session token. The access *policy* (token comparison) stays in `tool-bash` (the Consumer); the bash capability keeps `owner` opaque and never interprets it — the correct Service Definition / Service provider / Consumer split.
The bash owner-token comparison relies on the shared `Agent.id`/`SessionId` being unique among live agents. Concurrent same-ID operations may both prepare privately, but publication enters the session and agent in order; `SessionStore.enter()` rejects a duplicate live session id, and every losing transaction rolls its private state back. A programmatic caller therefore cannot publish two live agents with one session token. The access *policy* (token comparison) stays in `tool-bash` (the Consumer); the bash capability keeps `owner` opaque and never interprets it — the correct Service Definition / Service Provider / Consumer split.
## Alternatives considered
@@ -37,7 +37,7 @@ ACPAgent Client Protocol)与 tool-bash 的若干限制是同一个所有权
## 会话所有者令牌在存活 agent 中唯一
bash 所有者 token 比较依赖共享的 `Agent.id`/`SessionId` 在存活 agent 中唯一。并发的同 ID 操作可以都私下准备,但发布时会依次登记会话和 agent;`SessionStore.enter()` 拒绝重复的存活会话 id,每个失败事务都回滚自己的私有状态。因此程序化调用方无法发布两个共享同一会话 token 的存活 agent。访问*策略*token 比较)留在 Consumer `tool-bash`bash 能力只存储不透明的 `owner` 字符串且从不解释它——这是正确的 Service Definition / Service provider / Consumer 拆分。
bash 所有者 token 比较依赖共享的 `Agent.id`/`SessionId` 在存活 agent 中唯一。并发的同 ID 操作可以都私下准备,但发布时会依次登记会话和 agent;`SessionStore.enter()` 拒绝重复的存活会话 id,每个失败事务都回滚自己的私有状态。因此程序化调用方无法发布两个共享同一会话 token 的存活 agent。访问*策略*token 比较)留在 Consumer `tool-bash`bash 能力只存储不透明的 `owner` 字符串且从不解释它——这是正确的 Service Definition / Service Provider / Consumer 拆分。
## 曾考虑的替代方案
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-06-20-branded-ids.md
2026-06-20-branded-ids.md: 6bf68c9b6b6dea8317992bcb6f694a8ee141f048
2026-06-20-branded-ids.zh.md: 11a78df609888fdf917246940c12463373ff2de3
2026-06-20-branded-ids.md: 29b258b21240c92e74051339f0939a8e70933099
2026-06-20-branded-ids.zh.md: 288125764d4b279c79b7469080bb0c9f2efdc709
@@ -46,7 +46,7 @@ export function OwnerToken(id: string): OwnerToken {
### Why not typing `owner` as `SessionId`?
The obvious shortcut is to type `owner` as `SessionId` directly — it always *is* one. We reject that. The bash executor seam is a capability seam (Service Definition `dsh-shell`, Service provider `dsh-bash-local`, Consumer `dsh-tool-bash`) and its owner token is *documented as deliberately opaque*: the executor "never interprets it (no access policy lives in the seam — that is the consumer's job)" (`packages/shell/shell/src/types.ts`). Typing the Service Definition's field as `SessionId` would import `dsh-session`'s vocabulary into a package that must not know what an owner token *means* — it would couple a generic execution backend to the session model and contradict the opaque-token design. A sandboxed or remote executor that replaces `dsh-bash-local` should not inherit a session dependency. The distinct `OwnerToken` brand keeps the seam decoupled: `dsh-shell` knows only "an owner is some opaque branded token," and the `dsh-tool-bash` consumer — which already decides the access policy — is the single boundary that casts its `SessionId` into an `OwnerToken`. The brand still delivers the safety win (you cannot pass a `BashTaskId` or a raw string where an owner is expected) without the coupling.
The obvious shortcut is to type `owner` as `SessionId` directly — it always *is* one. We reject that. The bash executor seam is a capability seam (Service Definition `dsh-shell`, Service Provider `dsh-bash-local`, Consumer `dsh-tool-bash`) and its owner token is *documented as deliberately opaque*: the executor "never interprets it (no access policy lives in the seam — that is the consumer's job)" (`packages/shell/shell/src/types.ts`). Typing the Service Definition's field as `SessionId` would import `dsh-session`'s vocabulary into a package that must not know what an owner token *means* — it would couple a generic execution backend to the session model and contradict the opaque-token design. A sandboxed or remote executor that replaces `dsh-bash-local` should not inherit a session dependency. The distinct `OwnerToken` brand keeps the seam decoupled: `dsh-shell` knows only "an owner is some opaque branded token," and the `dsh-tool-bash` consumer — which already decides the access policy — is the single boundary that casts its `SessionId` into an `OwnerToken`. The brand still delivers the safety win (you cannot pass a `BashTaskId` or a raw string where an owner is expected) without the coupling.
## Out of scope / possible extensions
@@ -64,6 +64,6 @@ The landed invariants: `BashTaskId` and `OwnerToken` are defined in `dsh-shell`
## Consequences
- **Mechanical churn across two surfaces.** Propagating brands touches the bash seam (Service Definition + Service provider + Consumer) and the ACP session-id surface plus the persistence coordinator. The churn is broad but low-severity: a missed site is a compile error, not a silent bug. The change is observably type-only — no snapshot or e2e behavioral diff. It sits next to the [unified agent/session identity decision](../simplification/2026-06-20-unify-agent-and-session-id.md) because both touch the session-id / owner-token boundary; `OwnerToken` stays distinct from the unified id for the decoupling reason above.
- **Mechanical churn across two surfaces.** Propagating brands touches the bash seam (Service Definition + Service Provider + Consumer) and the ACP session-id surface plus the persistence coordinator. The churn is broad but low-severity: a missed site is a compile error, not a silent bug. The change is observably type-only — no snapshot or e2e behavioral diff. It sits next to the [unified agent/session identity decision](../simplification/2026-06-20-unify-agent-and-session-id.md) because both touch the session-id / owner-token boundary; `OwnerToken` stays distinct from the unified id for the decoupling reason above.
- **Brands do not validate.** A brand is a confusability guard, not a correctness proof: a *wrong* session id that is still a well-formed string passes the type checker exactly as before. This decision does not close that gap (see Out of scope) — it only stops the *category* error of passing the wrong *kind* of id.
- **The "where to stop" line stays a judgment call.** Branding `BashTaskId` but not `ToolName`, `OwnerToken` but not `ModelId`, is a taste call about which strings "could plausibly be confused." Reasonable reviewers may want more or fewer; the policy in `brand.ts` is the tie-breaker, and this decision errs toward the ids that are model-facing or used for access control.
@@ -46,7 +46,7 @@ export function OwnerToken(id: string): OwnerToken {
### 为什么不把 `owner` 类型标注为 `SessionId`
显而易见的捷径是直接把 `owner` 类型标注为 `SessionId`——它确实*总是*一个会话 id。我们否决这个方案。bash 执行器 seam 是能力 seamService Definition `dsh-shell`、Service provider `dsh-bash-local`、Consumer `dsh-tool-bash`),其 owner token 被*明确记录为刻意不透明*:执行器「从不解释它(seam 中没有访问策略——那是消费方的职责)」(`packages/shell/shell/src/types.ts`)。把 Service Definition 的字段类型标注为 `SessionId`,会把 `dsh-session` 的词汇引入一个不应知道 owner token *含义*的包——这会让通用执行后端耦合会话模型,并违背不透明 token 的设计。取代 `dsh-bash-local` 的沙箱化执行器或远程执行器不应继承会话依赖。独立的 `OwnerToken` brand 使 seam 保持解耦:`dsh-shell` 只知道「owner 是某种带 brand 的不透明 token」,而已经决定访问策略的 `dsh-tool-bash` 消费方,是把其 `SessionId` cast 为 `OwnerToken` 的唯一边界。该 brand 仍带来安全收益(不能把 `BashTaskId` 或裸 string 传到 owner 位置),且不引入耦合。
显而易见的捷径是直接把 `owner` 类型标注为 `SessionId`——它确实*总是*一个会话 id。我们否决这个方案。bash 执行器 seam 是能力 seamService Definition `dsh-shell`、Service Provider `dsh-bash-local`、Consumer `dsh-tool-bash`),其 owner token 被*明确记录为刻意不透明*:执行器「从不解释它(seam 中没有访问策略——那是消费方的职责)」(`packages/shell/shell/src/types.ts`)。把 Service Definition 的字段类型标注为 `SessionId`,会把 `dsh-session` 的词汇引入一个不应知道 owner token *含义*的包——这会让通用执行后端耦合会话模型,并违背不透明 token 的设计。取代 `dsh-bash-local` 的沙箱化执行器或远程执行器不应继承会话依赖。独立的 `OwnerToken` brand 使 seam 保持解耦:`dsh-shell` 只知道「owner 是某种带 brand 的不透明 token」,而已经决定访问策略的 `dsh-tool-bash` 消费方,是把其 `SessionId` cast 为 `OwnerToken` 的唯一边界。该 brand 仍带来安全收益(不能把 `BashTaskId` 或裸 string 传到 owner 位置),且不引入耦合。
## 不在范围内 / 可能的扩展
@@ -64,6 +64,6 @@ export function OwnerToken(id: string): OwnerToken {
## 后果
- **两个接口面的机械性改动。** 传播 brand 涉及 bash seamService Definition + Service provider + Consumer)以及 ACP 会话 id 接口和持久化协调器。改动面广但严重度低:遗漏的位置是编译错误而非静默 bug。从可观察行为看,这是一项纯类型变更——无快照或 e2e 行为差异。它与[统一 agent/会话标识决策](../simplification/2026-06-20-unify-agent-and-session-id.md)相邻,因为二者都触及会话 id / owner-token 边界;`OwnerToken` 出于上述解耦理由仍与统一后的 id 保持独立。
- **两个接口面的机械性改动。** 传播 brand 涉及 bash seamService Definition + Service Provider + Consumer)以及 ACP 会话 id 接口和持久化协调器。改动面广但严重度低:遗漏的位置是编译错误而非静默 bug。从可观察行为看,这是一项纯类型变更——无快照或 e2e 行为差异。它与[统一 agent/会话标识决策](../simplification/2026-06-20-unify-agent-and-session-id.md)相邻,因为二者都触及会话 id / owner-token 边界;`OwnerToken` 出于上述解耦理由仍与统一后的 id 保持独立。
- **Brand 不做校验。** Brand 是混淆防护,不是正确性证明:一个*错误的*会话 id 只要仍是格式正确的 string,就和以前一样能通过类型检查器。本决策不关闭这个缺口(见「不在范围内」)——它只阻止这类*类别*错误:传入错误*种类*的 id。
- **「在哪里停下」仍是判断题。** 为 `BashTaskId` 加 brand 但不为 `ToolName` 加,为 `OwnerToken` 加但不为 `ModelId` 加,是对哪些 string「可能被混淆」的品味判断。合理的评审者可能想要更多或更少;`brand.ts` 中的策略是裁决依据,本决策倾向于面向模型或用于访问控制的 id。
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.md
2026-06-20-generic-long-running-tool-runtime.md: 7db43323dd83f8e99698a7163412c9b33a607dfb
2026-06-20-generic-long-running-tool-runtime.zh.md: fe12ef264d294224fcfaf503ba958fa90c782347
2026-06-20-generic-long-running-tool-runtime.zh.md: efd306cc8d3b82749635dde302af45235c27b431
@@ -19,13 +19,13 @@ Status: implemented
长时间运行工具是生产方。`dsh-tool-bash``ShellProcess` 适配为增量输出与进程取消;`dsh-tool-subagent` 将子运行适配为最终输出与子运行释放。bash 与 subagent 能力 seam 保持独立,不依赖会话或任务注册表。
`JobRegistry``@deepseek-ai/dsh-jobs` 中的 Service Definition;进程内 Service provider 是 `@deepseek-ai/dsh-jobs-local` 中的 `LocalJobRegistry`(该拆分记录在[任务注册表约定 Agent Note](2026-07-26-job-registry-seam.md)中)。
`JobRegistry``@deepseek-ai/dsh-jobs` 中的 Service Definition;进程内 Service Provider 是 `@deepseek-ai/dsh-jobs-local` 中的 `LocalJobRegistry`(该拆分记录在[任务注册表约定 Agent Note](2026-07-26-job-registry-seam.md)中)。
## 运行时约定
字面类型见[任务子系统页面](../../../../docs/subsystems/jobs.md)。生产方调用 `ctx.jobs.start()`,传入 kind、label、可选的所属 `Agent`、可选的正数 `outputLimitBytes` 与一个 `run()` 函数。运行时会在调用 `run()` 前完成所有可能失败的预检工作,并且只调用一次。`run()` 返回钩子后,注册过程不会再执行可能失败的步骤而直接提交;生产方无法启动没有可收集 job id 的工作。
进程内 Service provider 还拥有有界准入,其理由记录在[有界后台任务准入决策](../bug-fix/2026-08-11-bounded-background-job-admission.md)中。它的 `maxConcurrentJobsPerOwner` 配置必须是正的安全整数,默认值为 `10``start()``running``stopping` 记录派生每个确切 `Agent` 对象的活动数量,而全部无 owner 任务共享一个服务级桶。容量拒绝发生在 `run()` 与 id 分配之前,处于 stopping 的任务只有在生产方 `done` 结算时才释放名额。Service provider 不排队或抢占任务,也不保留第二份可变计数。
进程内 Service Provider 还拥有有界准入,其理由记录在[有界后台任务准入决策](../bug-fix/2026-08-11-bounded-background-job-admission.md)中。它的 `maxConcurrentJobsPerOwner` 配置必须是正的安全整数,默认值为 `10``start()``running``stopping` 记录派生每个确切 `Agent` 对象的活动数量,而全部无 owner 任务共享一个服务级桶。容量拒绝发生在 `run()` 与 id 分配之前,处于 stopping 的任务只有在生产方 `done` 结算时才释放名额。Service Provider 不排队或抢占任务,也不保留第二份可变计数。
`outputLimitBytes` 是生产方拥有的呈现策略,而非注册表缓冲区。注册表校验该值,并将其原样投影到 `JobSnapshot`;通用任务控制器添加自身的状态或通知元数据后,再将该上限应用于完整的面向模型输出。省略该值时保持现有控制器行为,因此运行时不会向无关的生产方类别施加隐式默认值。
@@ -57,7 +57,7 @@ job id 在运行时全局可见且可预测,因此注册表会授权每次访
`JobRegistry` 提供:
- `start(spec)`:经过预检与 Service provider 准入的原子注册。
- `start(spec)`:经过预检与 Service Provider 准入的原子注册。
- `get(id, caller?)``list(caller?)`:非消费式快照。
- `read(id, caller?)`:消费式流增量或幂等的最终结果。
- `kill(id, caller?, reason?)`:取消。
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-06-24-web-capability-seam.md
2026-06-24-web-capability-seam.md: 6e81650e2d7286fc3c9df1e740ba9ccbf30118df
2026-06-24-web-capability-seam.zh.md: b91a4470999fd66ec7178326fcfb5247ff83a2e0
2026-06-24-web-capability-seam.md: 7e7b09f19864bd2ad8ad9d69579c1d5c79600cde
2026-06-24-web-capability-seam.zh.md: d6051eec498edb640773ba367038581cebd1f654
@@ -38,7 +38,7 @@ The seam deliberately exposes no observation surface — no registry-change even
## Package topology
The three-package Service Definition / Service provider / Consumer split follows bash and filesystem, but the *interface* package is closer to the LLM seam. `LlmRuntime` (`packages/llm/llm/src/index.ts`) is a name-keyed provider registry: `registerAdapter(models, adapter)` stores adapters in a `Map`, returns a disposer, throws `DUPLICATE_ADAPTER` on duplicate keys, and throws `NO_ADAPTER` at resolution time. `ctx.web` follows that registry shape, but has two capability kinds and a richer selection policy (a configured provider id, or auto-select when exactly one usable provider is registered), so the `WebError` an execution throws can explain why a search or fetch capability cannot run.
The three-package Service Definition / Service Provider / Consumer split follows bash and filesystem, but the *interface* package is closer to the LLM seam. `LlmRuntime` (`packages/llm/llm/src/index.ts`) is a name-keyed provider registry: `registerAdapter(models, adapter)` stores adapters in a `Map`, returns a disposer, throws `DUPLICATE_ADAPTER` on duplicate keys, and throws `NO_ADAPTER` at resolution time. `ctx.web` follows that registry shape, but has two capability kinds and a richer selection policy (a configured provider id, or auto-select when exactly one usable provider is registered), so the `WebError` an execution throws can explain why a search or fetch capability cannot run.
The dependency direction mirrors bash and filesystem:
@@ -38,7 +38,7 @@ Web 访问是一个一等能力 seam,遵循[能力 seam Agent Note](2026-06-13
## 包拓扑
由三个包构成的 Service Definition / Service provider / Consumer 拆分沿用 bash 和 filesystem 的模式,但*接口*包更接近 LLM(大语言模型) seam。`LlmRuntime``packages/llm/llm/src/index.ts`)是一个按名称键控的提供方注册表:`registerAdapter(models, adapter)` 将适配器存入 `Map`、返回 disposer、对重复键抛出 `DUPLICATE_ADAPTER`、在解析时抛出 `NO_ADAPTER``ctx.web` 沿用该注册表形状,但有两种能力类别和更丰富的选择策略(配置的提供方 id,或在恰好只有一个可用提供方注册时自动选择),因此执行时抛出的 `WebError` 能解释搜索或 fetch 能力为何无法运行。
由三个包构成的 Service Definition / Service Provider / Consumer 拆分沿用 bash 和 filesystem 的模式,但*接口*包更接近 LLM(大语言模型) seam。`LlmRuntime``packages/llm/llm/src/index.ts`)是一个按名称键控的提供方注册表:`registerAdapter(models, adapter)` 将适配器存入 `Map`、返回 disposer、对重复键抛出 `DUPLICATE_ADAPTER`、在解析时抛出 `NO_ADAPTER``ctx.web` 沿用该注册表形状,但有两种能力类别和更丰富的选择策略(配置的提供方 id,或在恰好只有一个可用提供方注册时自动选择),因此执行时抛出的 `WebError` 能解释搜索或 fetch 能力为何无法运行。
依赖方向与 bash 和 filesystem 一致:
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-07-tool-call-timeout-policy.md
2026-07-07-tool-call-timeout-policy.md: 20ca57cee4cd3ae51e7bfb8af1d2a1f8c7edd743
2026-07-07-tool-call-timeout-policy.zh.md: 5b91c29f5492a36bbd023a892f0f2f8dd857ae53
2026-07-07-tool-call-timeout-policy.md: ce414e541f8e374dd48e46d68cb00121e0004247
2026-07-07-tool-call-timeout-policy.zh.md: 6fe3c979a3c4e7b7a6ed803a47af45ad32d52cce
@@ -16,7 +16,7 @@ Tool-call timeout is a policy that applies only to model-facing tool execution,
- `@deepseek-ai/dsh-timeout` remains the shared library that owns `deadline()` and `timeoutOf()`.
- `@deepseek-ai/dsh-tools` has an around-dispatch waterfall, `tools/execute`, between `tools/pre-execute` and `tools/post-execute`.
- The [repository naming contract](../../proposed/architecture/2026-08-11-repository-naming-contract-and-rename-ledger.md) names `@deepseek-ai/dsh-tool-call-timeout-policy` for the exact operation it limits. The plugin reads each tool's declared `timeoutMs` from the runtime and wraps a call that has one by deriving a new `exec.signal`.
- The [repository naming contract](2026-08-11-repository-naming-contract-and-rename-ledger.md) names `@deepseek-ai/dsh-tool-call-timeout-policy` for the exact operation it limits. The plugin reads each tool's declared `timeoutMs` from the runtime and wraps a call that has one by deriving a new `exec.signal`.
The execution pipeline is:
@@ -16,7 +16,7 @@ Status: implemented
- `@deepseek-ai/dsh-timeout` 仍是拥有 `deadline()``timeoutOf()` 的共享库。
- `@deepseek-ai/dsh-tools``tools/pre-execute``tools/post-execute` 之间有一个环绕分发的 waterfall(瀑布式事件)`tools/execute`
- [仓库命名约定](../../proposed/architecture/2026-08-11-repository-naming-contract-and-rename-ledger.md)使用 `@deepseek-ai/dsh-tool-call-timeout-policy`,准确说明该策略所限制的操作。插件从 runtime 读取每个工具声明的 `timeoutMs`,并通过派生新的 `exec.signal` 来包装有此声明的调用。
- [仓库命名约定](2026-08-11-repository-naming-contract-and-rename-ledger.md)使用 `@deepseek-ai/dsh-tool-call-timeout-policy`,准确说明该策略所限制的操作。插件从 runtime 读取每个工具声明的 `timeoutMs`,并通过派生新的 `exec.signal` 来包装有此声明的调用。
执行流水线如下:
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-24-adapter-owned-reasoning-effort-capabilities.md
2026-07-24-adapter-owned-reasoning-effort-capabilities.md: f71a15626b97090a89f071e552a12b1f89173620
2026-07-24-adapter-owned-reasoning-effort-capabilities.zh.md: 6e8c99f98107e2855aa7cb7ea759544013d0ea8a
2026-07-24-adapter-owned-reasoning-effort-capabilities.md: f59e5d351970e785e6ade32797f1c1a5dd9e2591
2026-07-24-adapter-owned-reasoning-effort-capabilities.zh.md: 6e657a90e8ef3cd400a3e252f728bb1aadd9f4f2
@@ -14,7 +14,7 @@ Reasoning strength was adapter configuration only, so a conversation could not d
`LlmCallConfig` and `GenerateOptions` carry the optional effort. The agent loop prepares the post-`agent/request` config under the active turn signal before writing `request/header`, so defaults and dynamic changes are model-visible only after becoming durable facts. The prepared call retains the exact adapter registration across asynchronous exact-model resolution, durable header logging, and dispatch; direct `LlmRuntime.stream()` calls likewise capture their final registration before awaiting resolution. A route with no registered adapter retains its proposed config so an `llm/stream` middleware can own and short-circuit it; terminal dispatch still rejects an unhandled route. A resumed loop retains the logged effort only when its initial provider/model route is unchanged; a route change discards the previous model's opaque id.
The native DeepSeek adapter advertises `off`, `high`, and `max` when deployment policy permits thinking, and defaults to the configured effort or `high`. Its adapter-owned `off` maps to `thinking.type: disabled` with no `reasoning_effort`; `high` and `max` enable thinking and carry their official wire effort. A `thinking: disabled` deployment publishes only `off` and rejects attempts to enable thinking before provider I/O. The pi-ai adapter publishes each exact model's `getSupportedThinkingLevels()` result unchanged, including `off`, preserves an absent profile default as a provider default, and leaves provider wire-value mapping inside pi-ai. Its common stream options represent `off` by omitting `reasoning`, as required by pi-ai's own API.
The native DeepSeek adapter advertises `off`, `low`, `high`, and `max` when deployment policy permits thinking, and defaults to the configured effort or `high`. Its adapter-owned `off` maps to `thinking.type: disabled` with no `reasoning_effort`; `low`, `high`, and `max` enable thinking and carry their same-named official wire effort. A `thinking: disabled` deployment publishes only `off` and rejects attempts to enable thinking before provider I/O. The pi-ai adapter publishes each exact model's `getSupportedThinkingLevels()` result unchanged, including `off`, preserves an absent profile default as a provider default, and leaves provider wire-value mapping inside pi-ai. Its common stream options represent `off` by omitting `reasoning`, as required by pi-ai's own API.
## Alternatives considered
@@ -14,7 +14,7 @@ Status: implemented
`LlmCallConfig``GenerateOptions` 携带可选的推理强度。agent loop 在活跃轮次信号的控制下准备 `agent/request` 处理完成后的配置,再写入 `request/header`,因此默认值和动态变更只有成为持久化事实后才对模型可见。准备完成的调用在异步确切模型解析、请求头持久记录和分派全程保留同一项确切的适配器注册;直接调用 `LlmRuntime.stream()` 时,也会在等待解析前捕获最终的适配器注册。没有已注册适配器的路由会保留原定配置,使 `llm/stream` 中间件可以接管并短路该请求;若仍未得到处理,最终分发会拒绝该路由。恢复后的 agent loop 仅在初始提供方/模型路由未变时保留日志中记录的推理强度;如果路由发生变化,则丢弃上一模型的不透明 ID。
当部署策略允许思考时,原生 DeepSeek 适配器声明 `off``high``max`,默认使用配置指定的推理强度,若未配置则使用 `high`。由适配器持有的 `off` 映射为 `thinking.type: disabled`,且不带 `reasoning_effort``high``max` 会启用思考并携带各自的官方协议强度值。配置为 `thinking: disabled` 的部署仅声明 `off`,并会在提供方 I/O 前拒绝启用思考的尝试。pi-ai 适配器原样发布每个确切模型的 `getSupportedThinkingLevels()` 结果,其中包括 `off`;profile 未指定默认值时保留提供方默认行为,并将提供方协议值的映射留在 pi-ai 内部。按照 pi-ai 自身 API 的要求,其通用流选项通过省略 `reasoning` 来表示 `off`
当部署策略允许思考时,原生 DeepSeek 适配器声明 `off``low``high``max`,默认使用配置指定的推理强度,若未配置则使用 `high`。由适配器持有的 `off` 映射为 `thinking.type: disabled`,且不带 `reasoning_effort``low``high``max` 会启用思考并携带各自的同名官方协议强度值。配置为 `thinking: disabled` 的部署仅声明 `off`,并会在提供方 I/O 前拒绝启用思考的尝试。pi-ai 适配器原样发布每个确切模型的 `getSupportedThinkingLevels()` 结果,其中包括 `off`;profile 未指定默认值时保留提供方默认行为,并将提供方协议值的映射留在 pi-ai 内部。按照 pi-ai 自身 API 的要求,其通用流选项通过省略 `reasoning` 来表示 `off`
## 备选方案
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-26-job-registry-seam.md
2026-07-26-job-registry-seam.md: 5a77e8e8098a135dd25abeb11c68127fd9f1b4e9
2026-07-26-job-registry-seam.zh.md: fdc0912eca0b7ba01241137020e8e028c5480e2f
2026-07-26-job-registry-seam.md: b4a8a66ef63f1a4955e2497cad2d0d0b1ef138ec
2026-07-26-job-registry-seam.zh.md: ebab92029ddb9edd4c7085eef0d8cc83e9289bd9
@@ -6,17 +6,17 @@ English | [中文](2026-07-26-job-registry-seam.zh.md)
## Problem
The [background-job runtime](2026-06-20-generic-long-running-tool-runtime.md) shipped `JobRegistry` as one concrete package: `@deepseek-ai/dsh-jobs` owned both the `ctx.jobs` contract every producer and controller programs against and the process-local provider (the in-memory store, settlement bookkeeping, owner-cleanup effects, teardown). That bundling recouples the two rates of change the repository's [capability-seam rule](2026-06-13-capability-seams.md) separates: swapping the registry's storage or lifecycle backend would churn the same package whose types and `ctx.jobs` API producers (`dsh-tool-bash`, `dsh-tool-terminal`, `dsh-tool-subagent`), the controller (`dsh-tool-jobs`), and `JobKindMap` extenders import. Every other swappable capability in the harness — bash, pty, fs, skill, subagent, web, session persistence — already carries the Service Definition / Service provider / Consumer split; the job registry was the remaining `core`-mode exception, guarded only by a `TODO(job-service-backend)` comment.
The [background-job runtime](2026-06-20-generic-long-running-tool-runtime.md) shipped `JobRegistry` as one concrete package: `@deepseek-ai/dsh-jobs` owned both the `ctx.jobs` contract every producer and controller programs against and the process-local provider (the in-memory store, settlement bookkeeping, owner-cleanup effects, teardown). That bundling recouples the two rates of change the repository's [capability-seam rule](2026-06-13-capability-seams.md) separates: swapping the registry's storage or lifecycle backend would churn the same package whose types and `ctx.jobs` API producers (`dsh-tool-bash`, `dsh-tool-terminal`, `dsh-tool-subagent`), the controller (`dsh-tool-jobs`), and `JobKindMap` extenders import. Every other swappable capability in the harness — bash, pty, fs, skill, subagent, web, session persistence — already carries the Service Definition / Service Provider / Consumer split; the job registry was the remaining `core`-mode exception, guarded only by a `TODO(job-service-backend)` comment.
## Decision
`jobs/` is now a three-package capability family in the bash-trio shape:
- **`@deepseek-ai/dsh-jobs` (Service Definition)** — the abstract `JobRegistry extends Service` owning `ctx.jobs`, the nine-method contract (`start`, `list`, `get`, `read`, `kill`, `wait`, `onJobDone`, `onJobsChanged`, `attachController`), all vocabulary types (`JobId`, `JobKindMap`, `JobStart`, `JobHooks`, `JobOutcome`, `JobSnapshot`, `JobRead`, `JobDoneListener`), and the snapshot invariant companion. The class-level JSDoc states the semantics every Service provider owes: registrations outlive producer and controller fibers, owned access is session-fenced, settlement is first-wins with contained listeners, and `start` refuses work while no attached job controller serves the spec's owner (controllers and listeners are scope-layered, so one process-wide registry answers both questions per owner).
- **`@deepseek-ai/dsh-jobs-local` (Service provider)** — `LocalJobRegistry`, the process-local registry: the in-memory store, per-kind id counters, waiter bookkeeping, `TASK_WAIT_TIMEOUT` deadline code, owner-cleanup effects, force-fail teardown, and the default-10 configurable admission policy. Admission derives `running` plus `stopping` capacity from the same records per exact owner, with one unowned bucket; it adds no public count or second state owner. The `dsh-timeout` dependency and Schemastery-owned provider config live here; the Service Definition package has no provider dependencies.
- **`@deepseek-ai/dsh-jobs` (Service Definition)** — the abstract `JobRegistry extends Service` owning `ctx.jobs`, the nine-method contract (`start`, `list`, `get`, `read`, `kill`, `wait`, `onJobDone`, `onJobsChanged`, `attachController`), all vocabulary types (`JobId`, `JobKindMap`, `JobStart`, `JobHooks`, `JobOutcome`, `JobSnapshot`, `JobRead`, `JobDoneListener`), and the snapshot invariant companion. The class-level JSDoc states the semantics every Service Provider owes: registrations outlive producer and controller fibers, owned access is session-fenced, settlement is first-wins with contained listeners, and `start` refuses work while no attached job controller serves the spec's owner (controllers and listeners are scope-layered, so one process-wide registry answers both questions per owner).
- **`@deepseek-ai/dsh-jobs-local` (Service Provider)** — `LocalJobRegistry`, the process-local registry: the in-memory store, per-kind id counters, waiter bookkeeping, `TASK_WAIT_TIMEOUT` deadline code, owner-cleanup effects, force-fail teardown, and the default-10 configurable admission policy. Admission derives `running` plus `stopping` capacity from the same records per exact owner, with one unowned bucket; it adds no public count or second state owner. The `dsh-timeout` dependency and Schemastery-owned provider config live here; the Service Definition package has no provider dependencies.
- **`@deepseek-ai/dsh-tool-jobs` (Consumer)** — unchanged; it injects `'jobs'` and never imports provider types.
Compositions load `dsh-jobs-local` where they previously loaded `dsh-jobs` (the CLI cordis.yml row, `agent-spine-demo`, test harnesses, the tool-catalog generator boot). Producer misconfiguration diagnostics ("background jobs unavailable: load …") name `dsh-jobs` — the Service Definition package that declares the absent `ctx.jobs` service — and the Service Definition package's own APIs (its README and the direct-mount fence) point at Service providers, so the producer message stays correct when another backend becomes the recommended default. Producers, `JobKindMap` declaration merges, and the controller keep importing `@deepseek-ai/dsh-jobs` only.
Compositions load `dsh-jobs-local` where they previously loaded `dsh-jobs` (the CLI cordis.yml row, `agent-spine-demo`, test harnesses, the tool-catalog generator boot). Producer misconfiguration diagnostics ("background jobs unavailable: load …") name `dsh-jobs` — the Service Definition package that declares the absent `ctx.jobs` service — and the Service Definition package's own APIs (its README and the direct-mount fence) point at Service Providers, so the producer message stays correct when another backend becomes the recommended default. Producers, `JobKindMap` declaration merges, and the controller keep importing `@deepseek-ai/dsh-jobs` only.
The seam keeps the in-process contract semantics unchanged: `JobStart.run()` still passes callbacks and exact `Agent` objects, so a durable or cross-process backend still has design work to do before it can satisfy this Service Definition (identity, restart, ownership, observation). The split moves that future work out of every Consumer's dependency graph; it does not pre-design the backend.
@@ -30,6 +30,6 @@ The seam keeps the in-process contract semantics unchanged: `JobStart.run()` sti
## Consequences
Bought: the job registry now matches the repository-wide seam shape; a durable, remote, or instrumented registry is a sibling Service provider implementing nine abstract methods, and no producer, controller, or `JobKindMap` extender changes when one lands. The Service Definition README states the contract; the provider README owns the lifecycle bookkeeping facts. The registry behavior suite (owner cleanup, settlement, waits, teardown) lives with `dsh-jobs-local`; the Service Definition package keeps a stub-subclass test pinning registration under `ctx.jobs` and single-service duplication behavior, plus the probe-based invariant suite.
Bought: the job registry now matches the repository-wide seam shape; a durable, remote, or instrumented registry is a sibling Service Provider implementing nine abstract methods, and no producer, controller, or `JobKindMap` extender changes when one lands. The Service Definition README states the contract; the provider README owns the lifecycle bookkeeping facts. The registry behavior suite (owner cleanup, settlement, waits, teardown) lives with `dsh-jobs-local`; the Service Definition package keeps a stub-subclass test pinning registration under `ctx.jobs` and single-service duplication behavior, plus the probe-based invariant suite.
Cost: one more package (manifest, tsconfig, README, invariant companion), and compositions must name the Service provider package. `abstract` erases at runtime and this package name used to be the mountable registry, so the Service Definition constructor fails loudly when mounted directly — a stale composition row gets "load a Service provider such as @deepseek-ai/dsh-jobs-local" at load time instead of a half-registered `ctx.jobs` failing far from the misconfiguration.
Cost: one more package (manifest, tsconfig, README, invariant companion), and compositions must name the Service Provider package. `abstract` erases at runtime and this package name used to be the mountable registry, so the Service Definition constructor fails loudly when mounted directly — a stale composition row gets "load a Service Provider such as @deepseek-ai/dsh-jobs-local" at load time instead of a half-registered `ctx.jobs` failing far from the misconfiguration.
@@ -6,30 +6,30 @@ Status: implemented
## 问题
[后台任务运行时](2026-06-20-generic-long-running-tool-runtime.md)交付时把 `JobRegistry` 做成了单个具体包:`@deepseek-ai/dsh-jobs` 既拥有每个生产方和控制器面向编程的 `ctx.jobs` 约定,也拥有进程内 Service provider(内存存储、结算簿记、所有者清理 effect、拆除)。这种捆绑重新耦合了仓库[能力 seam 规则](2026-06-13-capability-seams.md)本要分离的两种变化速率:一旦替换注册表的存储或生命周期后端,被搅动的就是同一个包,而生产方(`dsh-tool-bash``dsh-tool-terminal``dsh-tool-subagent`)、控制器(`dsh-tool-jobs`)和 `JobKindMap` 扩展方正是从这个包导入类型与 `ctx.jobs` API。harness 中其余每项可替换能力——bash、pty、fs、skill(技能)、subagent、web、会话持久化——都已具备 Service Definition / Service provider / Consumer 三分;任务注册表曾是仅剩的 `core` 模式例外,仅由一条 `TODO(job-service-backend)` 注释把守。
[后台任务运行时](2026-06-20-generic-long-running-tool-runtime.md)交付时把 `JobRegistry` 做成了单个具体包:`@deepseek-ai/dsh-jobs` 既拥有每个生产方和控制器面向编程的 `ctx.jobs` 约定,也拥有进程内 Service Provider(内存存储、结算簿记、所有者清理 effect、拆除)。这种捆绑重新耦合了仓库[能力 seam 规则](2026-06-13-capability-seams.md)本要分离的两种变化速率:一旦替换注册表的存储或生命周期后端,被搅动的就是同一个包,而生产方(`dsh-tool-bash``dsh-tool-terminal``dsh-tool-subagent`)、控制器(`dsh-tool-jobs`)和 `JobKindMap` 扩展方正是从这个包导入类型与 `ctx.jobs` API。harness 中其余每项可替换能力——bash、pty、fs、skill(技能)、subagent、web、会话持久化——都已具备 Service Definition / Service Provider / Consumer 三分;任务注册表曾是仅剩的 `core` 模式例外,仅由一条 `TODO(job-service-backend)` 注释把守。
## 决策
`jobs/` 如今是一个 bash 三件套形态的三包能力家族:
- **`@deepseek-ai/dsh-jobs`Service Definition**——抽象的 `JobRegistry extends Service`,拥有 `ctx.jobs`、九个方法的约定(`start``list``get``read``kill``wait``onJobDone``onJobsChanged``attachController`)、全部词汇类型(`JobId``JobKindMap``JobStart``JobHooks``JobOutcome``JobSnapshot``JobRead``JobDoneListener`),以及快照不变式配套插件。类级 JSDoc 陈述了每个 Service provider 都必须兑现的语义:注册的存续期长于生产方与控制器的 fiber,有所有者的访问以会话为界,结算遵循首次结果优先且监听器错误被隔离,并且当没有任何已附加的任务控制器服务于 spec 的所有者时 `start` 拒绝启动工作(控制器与监听器按 scope 分层,因此一个进程级注册表能逐所有者地回答这两个问题)。
- **`@deepseek-ai/dsh-jobs-local`Service provider**——`LocalJobRegistry`,即进程内注册表:内存存储、按 kind 划分的 id 计数器、等待方簿记、`TASK_WAIT_TIMEOUT` deadline 代码、所有者清理 effect、强制失败的拆除,以及默认值为 10 且可配置的准入策略。准入从同一组记录中按确切 owner 派生 `running``stopping` 容量,并为无 owner 任务使用一个共享桶;它不新增公开计数或第二个状态 owner。`dsh-timeout` 依赖与由 Schemastery 管理的 Service provider 配置都位于此包;Service Definition 包不含任何提供方依赖。
- **`@deepseek-ai/dsh-jobs`Service Definition**——抽象的 `JobRegistry extends Service`,拥有 `ctx.jobs`、九个方法的约定(`start``list``get``read``kill``wait``onJobDone``onJobsChanged``attachController`)、全部词汇类型(`JobId``JobKindMap``JobStart``JobHooks``JobOutcome``JobSnapshot``JobRead``JobDoneListener`),以及快照不变式配套插件。类级 JSDoc 陈述了每个 Service Provider 都必须兑现的语义:注册的存续期长于生产方与控制器的 fiber,有所有者的访问以会话为界,结算遵循首次结果优先且监听器错误被隔离,并且当没有任何已附加的任务控制器服务于 spec 的所有者时 `start` 拒绝启动工作(控制器与监听器按 scope 分层,因此一个进程级注册表能逐所有者地回答这两个问题)。
- **`@deepseek-ai/dsh-jobs-local`Service Provider**——`LocalJobRegistry`,即进程内注册表:内存存储、按 kind 划分的 id 计数器、等待方簿记、`TASK_WAIT_TIMEOUT` deadline 代码、所有者清理 effect、强制失败的拆除,以及默认值为 10 且可配置的准入策略。准入从同一组记录中按确切 owner 派生 `running``stopping` 容量,并为无 owner 任务使用一个共享桶;它不新增公开计数或第二个状态 owner。`dsh-timeout` 依赖与由 Schemastery 管理的 Service Provider 配置都位于此包;Service Definition 包不含任何提供方依赖。
- **`@deepseek-ai/dsh-tool-jobs`(Consumer)**——保持不变;它注入 `'jobs'`,从不导入提供方类型。
各组合在原先加载 `dsh-jobs` 的位置改为加载 `dsh-jobs-local`CLI(命令行界面)的 cordis.yml 配置项、`agent-spine-demo`、各测试 harness,以及工具目录生成器的启动流程。生产方的配置错误诊断信息(「background jobs unavailable: load …」)点名 `dsh-jobs`——即声明缺失的 `ctx.jobs` 服务的 Service Definition 包;Service Definition 包自身的 API(其 README 与直接挂载防线)会指向各 Service provider,因此当另一个后端日后成为推荐默认时,生产方的消息依旧正确。生产方、`JobKindMap` 声明合并和控制器仍然只导入 `@deepseek-ai/dsh-jobs`
各组合在原先加载 `dsh-jobs` 的位置改为加载 `dsh-jobs-local`CLI(命令行界面)的 cordis.yml 配置项、`agent-spine-demo`、各测试 harness,以及工具目录生成器的启动流程。生产方的配置错误诊断信息(「background jobs unavailable: load …」)点名 `dsh-jobs`——即声明缺失的 `ctx.jobs` 服务的 Service Definition 包;Service Definition 包自身的 API(其 README 与直接挂载防线)会指向各 Service Provider,因此当另一个后端日后成为推荐默认时,生产方的消息依旧正确。生产方、`JobKindMap` 声明合并和控制器仍然只导入 `@deepseek-ai/dsh-jobs`
该 seam 保持进程内约定语义不变:`JobStart.run()` 仍然传入回调和确切的 `Agent` 对象,因此持久化或跨进程后端在能满足此 Service Definition 之前仍有设计工作要做(身份、重启、所有权、观察)。这次拆分把该项未来工作移出了每个 Consumer 的依赖图;它并不预先设计后端。
## 曾考虑的替代方案
**在第二个后端出现之前保持具体服务(维持现状)。**这正是运行时 Agent Note 当初的立场:在第二个 Service provider 出现前抽取 Service Definition,可能固化错误的边界。该方案落选,因为这条边界已不再是臆测:九个服务方法及其语义自引入以来在每一次生产方集成中都保持稳定,它们正是 `dsh-tool-jobs` 与各生产方已经面向编程的那套接口,而且仓库约定默认将可替换能力拆成三个包。剩余风险(持久化后端可能需要变更约定)不因这次拆分而改变:无论拆分与否,这类变更都会落在 Service Definition 包里;而若维持现状,它们今天还会连带搅动每个 Consumer 的提供方依赖。
**在第二个后端出现之前保持具体服务(维持现状)。**这正是运行时 Agent Note 当初的立场:在第二个 Service Provider 出现前抽取 Service Definition,可能固化错误的边界。该方案落选,因为这条边界已不再是臆测:九个服务方法及其语义自引入以来在每一次生产方集成中都保持稳定,它们正是 `dsh-tool-jobs` 与各生产方已经面向编程的那套接口,而且仓库约定默认将可替换能力拆成三个包。剩余风险(持久化后端可能需要变更约定)不因这次拆分而改变:无论拆分与否,这类变更都会落在 Service Definition 包里;而若维持现状,它们今天还会连带搅动每个 Consumer 的提供方依赖。
**在单个包内仅抽取 Service Definition(在具体类旁导出一个抽象类)。**否决,因为它在运作层面并未分离任何东西:Consumer 依然依赖携带 Service provider 及其依赖项的那个包,而替换后端若不把本地 Service provider 纳入自身依赖图,就仍然无法发布。在这里,包边界才是独立演进的单位。
**在单个包内仅抽取 Service Definition(在具体类旁导出一个抽象类)。**否决,因为它在运作层面并未分离任何东西:Consumer 依然依赖携带 Service Provider 及其依赖项的那个包,而替换后端若不把本地 Service Provider 纳入自身依赖图,就仍然无法发布。在这里,包边界才是独立演进的单位。
**拆出 `types.ts` 但让服务保持具体。**基于同样的理由否决:类型并不是完整能力,`ctx.jobs` Service Definition 及其方法约定才是。生产方需要的是服务键和语义,而不只是类型形状。
## 后果
换来的是:任务注册表如今与全仓库通行的 seam 形态一致;持久化、远程或带插桩的注册表将是一个实现九个抽象方法的同级 Service provider,这样的注册表落地时,任何生产方、控制器或 `JobKindMap` 扩展方都无需改动。Service Definition 的 README 陈述约定;生命周期簿记方面的事实归 Service provider 的 README 所有。注册表行为测试套件(所有者清理、结算、等待、拆除)随 `dsh-jobs-local` 存放;Service Definition 包保留一个桩子类(stub subclass)测试,固定 `ctx.jobs` 下的注册行为与单一服务的重复注册行为,外加基于探针的不变式测试套件。
换来的是:任务注册表如今与全仓库通行的 seam 形态一致;持久化、远程或带插桩的注册表将是一个实现九个抽象方法的同级 Service Provider,这样的注册表落地时,任何生产方、控制器或 `JobKindMap` 扩展方都无需改动。Service Definition 的 README 陈述约定;生命周期簿记方面的事实归 Service Provider 的 README 所有。注册表行为测试套件(所有者清理、结算、等待、拆除)随 `dsh-jobs-local` 存放;Service Definition 包保留一个桩子类(stub subclass)测试,固定 `ctx.jobs` 下的注册行为与单一服务的重复注册行为,外加基于探针的不变式测试套件。
代价是:多出一个包,即多一份 manifest(元数据清单)、tsconfig、README 与不变式配套插件;同时各组合必须点名 Service provider 包。`abstract` 在运行时会被擦除,而这个包名过去正是可挂载的具体注册表,因此直接挂载 Service Definition 时,其构造函数会明确报错——一条陈旧的组合配置行会在加载时得到「load a Service provider such as @deepseek-ai/dsh-jobs-local」,而不是一个未完整注册的 `ctx.jobs` 在远离错误配置处才失败。
代价是:多出一个包,即多一份 manifest(元数据清单)、tsconfig、README 与不变式配套插件;同时各组合必须点名 Service Provider 包。`abstract` 在运行时会被擦除,而这个包名过去正是可挂载的具体注册表,因此直接挂载 Service Definition 时,其构造函数会明确报错——一条陈旧的组合配置行会在加载时得到「load a Service Provider such as @deepseek-ai/dsh-jobs-local」,而不是一个未完整注册的 `ctx.jobs` 在远离错误配置处才失败。
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-26-subprocess-seam.md
2026-07-26-subprocess-seam.md: f738c5be517d7f915cf5cb0f42506339259c62a2
2026-07-26-subprocess-seam.zh.md: 078648d1d676f2d5889df4ec5793de687713cbb9
2026-07-26-subprocess-seam.md: aa7a989a0321760c9bc278df8356a57f4dd0d459
2026-07-26-subprocess-seam.zh.md: 22de6774b866b9b7a0248dc7b06587f7f3235fdc
@@ -13,7 +13,7 @@ English | [中文](2026-07-26-subprocess-seam.zh.md)
A new `subprocess/` capability family owns "run and manage a process"; the bash family keeps "run a bash command" and consumes it:
- **`@deepseek-ai/dsh-subprocess` (Service Definition)** — the abstract `SubprocessRuntime` owning `ctx.subprocess`: executable lookup, fully explicit ordinary spawns, and the terminal primitive added by the [portable execution-world decision](2026-07-28-portable-execution-world-consumers.md). Each stdio stream independently selects `'pipe'`, `'inherit'`, or bounded collection `{ maxBytes, spill? }`; stdin selects `'ignore'`, `'pipe'`, or `{ data }`. `SubprocessOutcome` carries exit facts with deliberately no timeout/cancel classification, while collected output remains on the handle after settlement. The Service Definition also owns process and terminal handles, the shared scrub, and `DSH_ENV_PREFIX`/`DshEnvironment`/`CollectedOutput`; `argv` is never shell-interpreted.
- **`@deepseek-ai/dsh-subprocess-local` (Service provider)** — `LocalSubprocessRuntime` over the former `run.ts` plumbing (`spawn.ts`) plus `node-pty`: detached groups, bounded collection and private spill files, executable lookup, foreground/session inspection, and disposal that terminates and joins every managed process. `terminate()` owns TERM→grace→KILL for the tree, `waitForExit()` observes tree liveness, and injected `taskkill /T` covers Windows. Ordinary and terminal spawns apply the Service Definition's case-insensitive `KEY`/`PASSWORD`/`SECRET`/`TOKEN` scrub before explicit env. The provider has no config; every limit arrives on the spec, while Bash and PTY presentation environment overrides stay in their Consumers.
- **`@deepseek-ai/dsh-subprocess-local` (Service Provider)** — `LocalSubprocessRuntime` over the former `run.ts` plumbing (`spawn.ts`) plus `node-pty`: detached groups, bounded collection and private spill files, executable lookup, foreground/session inspection, and disposal that terminates and joins every managed process. `terminate()` owns TERM→grace→KILL for the tree, `waitForExit()` observes tree liveness, and injected `taskkill /T` covers Windows. Ordinary and terminal spawns apply the Service Definition's case-insensitive `KEY`/`PASSWORD`/`SECRET`/`TOKEN` scrub before explicit env. The provider has no config; every limit arrives on the spec, while Bash and PTY presentation environment overrides stay in their Consumers.
- **`dsh-bash-local` (Consumer)** — `inject: ['subprocess']`; maps each resolved `ShellExecSpec` onto a `SubprocessSpawnSpec` (`['bash', '-c', command]`), keeps its config, `resolve()` defaulting, fused-deadline `timedOut`/`aborted` classification, the `[stderr]`-marked background read merge with its consuming cursor, and the `onProcessDone` subclass hook. `dsh-bash-sandbox` is unchanged apart from redeclaring the inherited inject; it still wraps at the command-string level and re-enters the inherited spawn path.
- **`dsh-shell` (Service Definition)** — re-exports the moved vocabulary from `dsh-subprocess`, so no bash Consumer changes an import; `ShellExecRequest`/`ShellExecSpec`/`ShellProcess` and the sandbox facts remain bash-owned.
@@ -13,7 +13,7 @@ Status: implemented
新的 `subprocess/` 能力家族拥有「运行并管理一个进程」;bash 家族保留「运行一条 bash 命令」,并成为前者的消费方:
- **`@deepseek-ai/dsh-subprocess`Service Definition**——拥有 `ctx.subprocess` 的抽象 `SubprocessRuntime`:可执行文件查找、完全显式的普通 spawn,以及[可移植执行环境决策](2026-07-28-portable-execution-world-consumers.md)新增的终端原语。每条 stdio 流独立选择 `'pipe'``'inherit'` 或有界收集 `{ maxBytes, spill? }`stdin 选择 `'ignore'``'pipe'``{ data }``SubprocessOutcome` 只承载刻意不含超时/取消分类的退出事实,收集输出在结算后仍留在句柄上。该 Service Definition 还拥有进程与终端句柄、共享凭据清除,以及 `DSH_ENV_PREFIX`/`DshEnvironment`/`CollectedOutput``argv` 绝不经过 shell 解释。
- **`@deepseek-ai/dsh-subprocess-local`Service provider**——`LocalSubprocessRuntime` 构建在原 `run.ts` 管道(现为 `spawn.ts`)与 `node-pty` 之上:detached 进程组、有界收集与私有 spill 文件、可执行文件查找、前台/会话检查,以及终止每个受管进程并等待其退出的 dispose。`terminate()` 拥有面向进程树的 TERM→宽限→KILL,`waitForExit()` 观察进程树存活性,可注入的 `taskkill /T` 覆盖 Windows。普通与终端 spawn 都先应用 Service Definition 对 `KEY`/`PASSWORD`/`SECRET`/`TOKEN` 不区分大小写的清除,再合并显式 env。该 Service provider 没有配置;每项限制都随 spec 到达,Bash 与 PTY 的呈现环境覆盖仍归各自 Consumer 所有。
- **`@deepseek-ai/dsh-subprocess-local`Service Provider**——`LocalSubprocessRuntime` 构建在原 `run.ts` 管道(现为 `spawn.ts`)与 `node-pty` 之上:detached 进程组、有界收集与私有 spill 文件、可执行文件查找、前台/会话检查,以及终止每个受管进程并等待其退出的 dispose。`terminate()` 拥有面向进程树的 TERM→宽限→KILL,`waitForExit()` 观察进程树存活性,可注入的 `taskkill /T` 覆盖 Windows。普通与终端 spawn 都先应用 Service Definition 对 `KEY`/`PASSWORD`/`SECRET`/`TOKEN` 不区分大小写的清除,再合并显式 env。该 Service Provider 没有配置;每项限制都随 spec 到达,Bash 与 PTY 的呈现环境覆盖仍归各自 Consumer 所有。
- **`dsh-bash-local`Consumer**——`inject: ['subprocess']`;把每个解析后的 `ShellExecSpec` 映射为一个 `SubprocessSpawnSpec``['bash', '-c', command]`),并保留自身配置、`resolve()` 默认值补全、基于融合 deadline 的 `timedOut`/`aborted` 分类、带 `[stderr]` 标记的后台读取合并及其消费游标,以及 `onProcessDone` 子类钩子。`dsh-bash-sandbox` 除了重新声明继承来的 inject 之外没有变化;它仍在命令字符串层面做包装,并重新进入继承的 spawn 路径。
- **`dsh-shell`Service Definition**——把迁走的词汇从 `dsh-subprocess` 重导出,因此没有任何 bash Consumer 需要改动导入;`ShellExecRequest`/`ShellExecSpec`/`ShellProcess` 与沙箱事实仍归 bash 所有。
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-27-dispose-ladder-to-consumer.md
2026-07-27-dispose-ladder-to-consumer.md: ab2afd313448438752e15e0f89230124cb95232f
2026-07-27-dispose-ladder-to-consumer.zh.md: b181b2c3286c42d85eb91ca9ac5acf624e5c7a28
2026-07-27-dispose-ladder-to-consumer.md: 58d835864d2f6544152fdf09cf6e93380793a2ae
2026-07-27-dispose-ladder-to-consumer.zh.md: dc7413e85fa6e52de14f51fd175c7b37b43029a4
@@ -14,10 +14,10 @@ The ladder moves to its one consumer. `dsh-subagent-acp` owns `disposeAcpChild(c
## Alternatives considered
**Keep the ladder on the handle as a convenience.** Rejected: a Service Definition method every Service provider must implement is not a convenience, it is contract surface — and this one encodes one consumer's cooperation shape (stdin-EOF-first) as if it were process vocabulary. The seam's own README already had to caveat that children quiescing on other signals need "their own tier-1", which is the admission that the ladder is policy.
**Keep the ladder on the handle as a convenience.** Rejected: a Service Definition method every Service Provider must implement is not a convenience, it is contract surface — and this one encodes one consumer's cooperation shape (stdin-EOF-first) as if it were process vocabulary. The seam's own README already had to caveat that children quiescing on other signals need "their own tier-1", which is the admission that the ladder is policy.
**Move the ladder to a shared helper package.** Rejected: one consumer. A second out-of-process backend with the same stdin-EOF cooperation shape can lift `disposeAcpChild` to shared code when it exists; extracting now would recreate `dsh-subagent-subprocess`, the single-purpose library this change deleted.
## Consequences
Bought: the Service Definition is one method and one type smaller; Service providers owe four verbs and no teardown policy; the cooperative EOF window lives beside the ACP config field that tunes it, while the subprocess owner alone owns the termination window and final join. Cost: a future backend wanting EOF-first teardown writes ~20 lines against the verbs (or lifts the ACP helper); the ladder's tier tests live in the ACP suite, and the Service Definition suite pins the verbs the ladder composes (bounded `waitForExit` false before escalation and an unbounded whole-tree join after it) instead of the composed policy.
Bought: the Service Definition is one method and one type smaller; Service Providers owe four verbs and no teardown policy; the cooperative EOF window lives beside the ACP config field that tunes it, while the subprocess owner alone owns the termination window and final join. Cost: a future backend wanting EOF-first teardown writes ~20 lines against the verbs (or lifts the ACP helper); the ladder's tier tests live in the ACP suite, and the Service Definition suite pins the verbs the ladder composes (bounded `waitForExit` false before escalation and an unbounded whole-tree join after it) instead of the composed policy.
@@ -14,10 +14,10 @@ Status: implemented
## 曾考虑的替代方案
**把阶梯作为便利方法留在句柄上。**否决:一个每个 Service provider 都必须实现的 Service Definition 方法不是便利,而是约定的一部分——而这一个把某一消费方的协作模式(stdin EOF 打头)当作进程词汇来编码。seam 自己的 README 早已不得不加注「依赖其他信号才能完全停稳的子进程需要自己的第一阶」,这本身就是承认该阶梯是策略。
**把阶梯作为便利方法留在句柄上。**否决:一个每个 Service Provider 都必须实现的 Service Definition 方法不是便利,而是约定的一部分——而这一个把某一消费方的协作模式(stdin EOF 打头)当作进程词汇来编码。seam 自己的 README 早已不得不加注「依赖其他信号才能完全停稳的子进程需要自己的第一阶」,这本身就是承认该阶梯是策略。
**把阶梯移到共享辅助包。**否决:只有一个消费方。当第二个具有相同 stdin EOF 协作模式的进程外后端出现时,可以再把 `disposeAcpChild` 提升为共享代码;现在抽取只会重造 `dsh-subagent-subprocess`——本次变更删掉的那个单一用途库。
## 后果
买到的:Service Definition 少了一个方法和一个类型;Service provider 只欠四个动词,不欠拆卸策略;协作式 EOF 时间窗与调节它的 ACP 配置字段住在一起,而终止时间窗与最终的整树退出等待仅由子进程责任方拥有。代价:未来想要 EOF 打头拆卸的后端需针对这些动词写约 20 行(或直接搬 ACP 的辅助函数);阶梯的层级测试位于 ACP 套件,Service Definition 套件转而钉住阶梯所组合的动词(升级前有界 `waitForExit` 返回假,升级后无界等待整棵进程树退出),而非组合后的策略。
买到的:Service Definition 少了一个方法和一个类型;Service Provider 只欠四个动词,不欠拆卸策略;协作式 EOF 时间窗与调节它的 ACP 配置字段住在一起,而终止时间窗与最终的整树退出等待仅由子进程责任方拥有。代价:未来想要 EOF 打头拆卸的后端需针对这些动词写约 20 行(或直接搬 ACP 的辅助函数);阶梯的层级测试位于 ACP 套件,Service Definition 套件转而钉住阶梯所组合的动词(升级前有界 `waitForExit` 返回假,升级后无界等待整棵进程树退出),而非组合后的策略。
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-29-package-regrouping.md
2026-07-29-package-regrouping.md: 35042086503167d01edc1b3bfafdef92ba4781c5
2026-07-29-package-regrouping.zh.md: 2dfd7ba85484008138f799ff60c406807a9ecf7f
2026-07-29-package-regrouping.md: 52a1fa28e4826daa7b0bb84a37be3c094fddb68c
2026-07-29-package-regrouping.zh.md: bef55544664f6e6daf391b33ef7224c00507a908
@@ -17,7 +17,7 @@ The north star for the regrouping: **closely clustered packages share a group.**
## Decision
Five regrouping decisions remain current; every other group keeps its prior boundary and contents (the dependency analysis confirmed the capability families — `shell/`, `terminal/`, `code-runtime/`, `sandbox/`, `subprocess/`, `fs/`, `lsp/`, `web/`, `skill/`, and the rest — were already drawn correctly). The original sixth decision collected the SDK project initializer, launcher tooling, and runtime JSON-RPC packages under `scaffold/`; [removing that unreleased toolchain](../simplification/2026-08-11-remove-sdk-project-toolchain.md) deleted the project tooling and moved the surviving runtime trio to `sdk/`. The later [repository naming contract](../../proposed/architecture/2026-08-11-repository-naming-contract-and-rename-ledger.md) owns the `shell/`, `terminal/`, and `extensions/` group names and the two package names that this decision deferred.
Five regrouping decisions remain current; every other group keeps its prior boundary and contents (the dependency analysis confirmed the capability families — `shell/`, `terminal/`, `code-runtime/`, `sandbox/`, `subprocess/`, `fs/`, `lsp/`, `web/`, `skill/`, and the rest — were already drawn correctly). The original sixth decision collected the SDK project initializer, launcher tooling, and runtime JSON-RPC packages under `scaffold/`; [removing that unreleased toolchain](../simplification/2026-08-11-remove-sdk-project-toolchain.md) deleted the project tooling and moved the surviving runtime trio to `sdk/`. The later [repository naming contract](2026-08-11-repository-naming-contract-and-rename-ledger.md) owns the `shell/`, `terminal/`, and `extensions/` group names and the two package names that this decision deferred.
| Group | Members (folder names) | From |
|---|---|---|
@@ -37,7 +37,7 @@ Five regrouping decisions remain current; every other group keeps its prior boun
## Later naming decisions
The [repository naming contract](../../proposed/architecture/2026-08-11-repository-naming-contract-and-rename-ledger.md) resolves the two names that this move deliberately deferred. `@deepseek-ai/dsh-sdk-jsonrpc-server` names the JSON-RPC server half of the runtime SDK protocol. `@deepseek-ai/dsh-tool-call-timeout-policy` names the exact operation limited by the policy while keeping its `guard/timeout-policy/` home. Their release-blocking `FIXME` markers are removed with those renames.
The [repository naming contract](2026-08-11-repository-naming-contract-and-rename-ledger.md) resolves the two names that this move deliberately deferred. `@deepseek-ai/dsh-sdk-jsonrpc-server` names the JSON-RPC server half of the runtime SDK protocol. `@deepseek-ai/dsh-tool-call-timeout-policy` names the exact operation limited by the policy while keeping its `guard/timeout-policy/` home. Their release-blocking `FIXME` markers are removed with those renames.
## What the move touched
@@ -17,7 +17,7 @@ Status: implemented
## 决策
五项重组决策仍然有效;其余每个组都保持先前的边界与内容不变(依赖分析确认各能力家族——`shell/``terminal/``code-runtime/``sandbox/``subprocess/``fs/``lsp/``web/``skill/` 及其余——本来就划得正确)。原本的第六项决策把 SDK 项目初始化器、启动器工具与运行时 JSON-RPC 包汇集到 `scaffold/`[移除这套未发布工具链](../simplification/2026-08-11-remove-sdk-project-toolchain.md)的决策删除了项目工具,并将存留的运行时三包移到 `sdk/`。后续的[仓库命名约定](../../proposed/architecture/2026-08-11-repository-naming-contract-and-rename-ledger.md)负责 `shell/``terminal/``extensions/` 组名,以及本决策曾推迟的两个包名。
五项重组决策仍然有效;其余每个组都保持先前的边界与内容不变(依赖分析确认各能力家族——`shell/``terminal/``code-runtime/``sandbox/``subprocess/``fs/``lsp/``web/``skill/` 及其余——本来就划得正确)。原本的第六项决策把 SDK 项目初始化器、启动器工具与运行时 JSON-RPC 包汇集到 `scaffold/`[移除这套未发布工具链](../simplification/2026-08-11-remove-sdk-project-toolchain.md)的决策删除了项目工具,并将存留的运行时三包移到 `sdk/`。后续的[仓库命名约定](2026-08-11-repository-naming-contract-and-rename-ledger.md)负责 `shell/``terminal/``extensions/` 组名,以及本决策曾推迟的两个包名。
| 组 | 成员(目录名) | 来源 |
|---|---|---|
@@ -37,7 +37,7 @@ Status: implemented
## 后续命名决策
[仓库命名约定](../../proposed/architecture/2026-08-11-repository-naming-contract-and-rename-ledger.md)解决了本次移动有意推迟的两个名称。`@deepseek-ai/dsh-sdk-jsonrpc-server` 表示运行时 SDK 协议的 JSON-RPC 服务器一侧。`@deepseek-ai/dsh-tool-call-timeout-policy` 准确表示策略所限制的操作,同时保留其 `guard/timeout-policy/` 归属。这些重命名会一并移除阻塞发布的 `FIXME` 标记。
[仓库命名约定](2026-08-11-repository-naming-contract-and-rename-ledger.md)解决了本次移动有意推迟的两个名称。`@deepseek-ai/dsh-sdk-jsonrpc-server` 表示运行时 SDK 协议的 JSON-RPC 服务器一侧。`@deepseek-ai/dsh-tool-call-timeout-policy` 准确表示策略所限制的操作,同时保留其 `guard/timeout-policy/` 归属。这些重命名会一并移除阻塞发布的 `FIXME` 标记。
## 移动触及了什么
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-31-code-runtime-portable-identifier-seam.md
2026-07-31-code-runtime-portable-identifier-seam.md: 7692660c5577a9d647e8c55c8e808c1293d03e4f
2026-07-31-code-runtime-portable-identifier-seam.zh.md: 668fb46a80737d0cbf0ea79e820f1baeb17c4a5c
2026-07-31-code-runtime-portable-identifier-seam.md: 6d75f0b0872a5d4b92403f597f5551276e2d7c9f
2026-07-31-code-runtime-portable-identifier-seam.zh.md: 68515d7a4d9d26d6c510654255d36b615afc2b7d
@@ -8,11 +8,11 @@ English | [中文](2026-07-31-code-runtime-portable-identifier-seam.zh.md)
The code-runtime seam promises that a binding-namespace list valid on one backend is valid on every backend, so a Code Mode consumer can hand the same bindings to any registered runtime without knowing its language. The first backend, `dsh-code-runtime-worker-thread`, privately owned the identifier rules that enforce part of that promise: an `IDENTIFIER` regex that allowed the JS-only `$`, a `RESERVED_WORDS` set holding only ECMAScript keywords, and a `RESERVED_ERROR_PROPERTIES` set of three JS `Error` slots. Those rules described the worker's own language, not the seam's portability contract.
A second backend written against a different language (CPython) would either re-declare its own rules — letting `lambda` pass the worker and fail Python, or `$tools` pass the worker and fail every non-JS backend — or import the worker's, inverting the dependency so a Service provider reached into a sibling Service provider. Neither keeps the portability promise real: it would hold only for the backend a caller happened to test against.
A second backend written against a different language (CPython) would either re-declare its own rules — letting `lambda` pass the worker and fail Python, or `$tools` pass the worker and fail every non-JS backend — or import the worker's, inverting the dependency so a Service Provider reached into a sibling Service Provider. Neither keeps the portability promise real: it would hold only for the backend a caller happened to test against.
## Decision
The Service Definition package (`@deepseek-ai/dsh-code-runtime`) exports the portable-identifier exclusion contract as four named constants, and every Service provider imports them rather than re-declaring:
The Service Definition package (`@deepseek-ai/dsh-code-runtime`) exports the portable-identifier exclusion contract as four named constants, and every Service Provider imports them rather than re-declaring:
- `PORTABLE_RESERVED_WORDS` — the union of ECMAScript and Python reserved words. A namespace global or error-class name matching any is refused on all backends, so `lambda` is refused even though it is a legal JS parameter name. Adding a language widens this union, which is a deliberate breaking review of existing binding names.
- `RESERVED_BINDING_GLOBALS` — globals some backend owns in the program's namespace: `console` (the worker's log capture), `__dsh_main__`/`__builtins__`/`__name__` (the Python bootstrap's wrapper and seeded module globals), and `__debug__` (not a seeded slot but a CPython compile-time constant that rejects assignment, so an injected global under that name is unreachable — the same portability split by a different mechanism). Refused everywhere so a namespace list cannot pick a name that works on one backend and collides on another.
@@ -21,7 +21,7 @@ The Service Definition package (`@deepseek-ai/dsh-code-runtime`) exports the por
The Service Definition also narrows the portable identifier subset to `[A-Za-z_][A-Za-z0-9_]*` (documented on `CodeBindingNamespace.global` and `CodeBindingErrorClass`), dropping the JS-only `$`. The worker consumes the shared constants directly under their exported names — `PORTABLE_RESERVED_WORDS` for both binding-global and error-class names, `RESERVED_BINDING_GLOBALS` for backend-owned slots, `RESERVED_ERROR_MEMBERS` plus `DUNDER_MEMBER` for error members — with no local re-alias; its `IDENTIFIER` regex loses `$`.
The constants live in the Service Definition even though the worker is the only shipped backend: the whole point is that the contract is language-agnostic and owned above any single language. A Service provider that violated it would be the bug, and the shared set is where a reviewer looks to see what "portable" means.
The constants live in the Service Definition even though the worker is the only shipped backend: the whole point is that the contract is language-agnostic and owned above any single language. A Service Provider that violated it would be the bug, and the shared set is where a reviewer looks to see what "portable" means.
## Scope
@@ -33,7 +33,7 @@ This decision delivers only the Service Definition extension and the worker's ad
**Each backend declares its own exclusions.** Rejected: it makes the portability promise per-backend. A binding list the caller tested on the worker could be refused by Python, which is exactly the split the seam exists to prevent.
**The Python backend imports the worker's constants.** Rejected: it inverts the dependency — the seam's Service providers would reach into a sibling implementation for a contract neither owns. The contract belongs above both, at the seam.
**The Python backend imports the worker's constants.** Rejected: it inverts the dependency — the seam's Service Providers would reach into a sibling implementation for a contract neither owns. The contract belongs above both, at the seam.
**Keep `$` in the portable identifier subset.** Rejected: `$` is JS-only spelling. Allowing it would let `$tools` pass the worker and fail every non-JS backend, breaking portability for a purely cosmetic gain.
@@ -8,11 +8,11 @@ Status: implemented
code-runtime seam 承诺:在一个后端上有效的绑定命名空间列表,在每个后端上都有效,因此 Code Mode 消费方可以把同一组绑定交给任何已注册的运行时,而不必知道它的语言。首个后端 `dsh-code-runtime-worker-thread` 私自拥有了执行这项承诺一部分的标识符规则:一个允许 JS 专有 `$``IDENTIFIER` 正则、一个只含 ECMAScript 关键字的 `RESERVED_WORDS` 集合,以及一个含三个 JS `Error` 槽位的 `RESERVED_ERROR_PROPERTIES` 集合。这些规则描述的是 worker 自身的语言,而非 seam 的可移植性约定。
一个针对不同语言(CPython)编写的第二后端,要么重新声明自己的规则——让 `lambda` 通过 worker 却在 Python 上失败,或让 `$tools` 通过 worker 却在每个非 JS 后端上失败——要么导入 worker 的规则,从而反转依赖,使一个 Service provider 伸手进入另一个兄弟 Service provider。二者都无法让可移植承诺成真:它只对调用方恰好测试过的那个后端成立。
一个针对不同语言(CPython)编写的第二后端,要么重新声明自己的规则——让 `lambda` 通过 worker 却在 Python 上失败,或让 `$tools` 通过 worker 却在每个非 JS 后端上失败——要么导入 worker 的规则,从而反转依赖,使一个 Service Provider 伸手进入另一个兄弟 Service Provider。二者都无法让可移植承诺成真:它只对调用方恰好测试过的那个后端成立。
## Decision
Service Definition 包(`@deepseek-ai/dsh-code-runtime`)以四个具名常量导出可移植标识符排除约定,每个 Service provider 导入它们而非重新声明:
Service Definition 包(`@deepseek-ai/dsh-code-runtime`)以四个具名常量导出可移植标识符排除约定,每个 Service Provider 导入它们而非重新声明:
- `PORTABLE_RESERVED_WORDS`——ECMAScript 与 Python 保留字的联集。任何命名空间 global 或 error-class 名称匹配其中之一,都在所有后端上被拒绝,因此 `lambda` 即便是合法的 JS 参数名也被拒绝。新增一门语言即扩宽此联集,这是对现有绑定名称的一次有意的破坏性复审。
- `RESERVED_BINDING_GLOBALS`——某个后端在程序命名空间中拥有的 global:`console`worker 的日志捕获)、`__dsh_main__`/`__builtins__`/`__name__`Python bootstrap 的包装器与预置模块 global),以及 `__debug__`(不是 seed 的槽位,而是 CPython 编译期常量,赋值会被拒,故以该名注入的 global 不可达——同一种可移植性分裂,只是机制不同)。在所有后端上被拒绝,使命名空间列表无法选到一个在某后端能用、在另一后端冲突的名称。
@@ -21,7 +21,7 @@ Service Definition 包(`@deepseek-ai/dsh-code-runtime`)以四个具名常量
Service Definition 同时把可移植标识符子集收窄为 `[A-Za-z_][A-Za-z0-9_]*`(记录在 `CodeBindingNamespace.global``CodeBindingErrorClass` 上),去掉 JS 专有的 `$`。worker 直接以这些常量的导出名称消费它们——binding-global 与 error-class 名称用 `PORTABLE_RESERVED_WORDS`、后端拥有槽位用 `RESERVED_BINDING_GLOBALS`、error member 用 `RESERVED_ERROR_MEMBERS``DUNDER_MEMBER`——不再本地起别名;其 `IDENTIFIER` 正则去掉 `$`
尽管 worker 是唯一已交付的后端,这些常量仍置于 Service Definition:要点正是该约定与语言无关,且由高于任何单一语言的层级拥有。违反它的 Service provider 才是 bug,而共享集合正是复审者查看「可移植」含义的地方。
尽管 worker 是唯一已交付的后端,这些常量仍置于 Service Definition:要点正是该约定与语言无关,且由高于任何单一语言的层级拥有。违反它的 Service Provider 才是 bug,而共享集合正是复审者查看「可移植」含义的地方。
## Scope
@@ -33,7 +33,7 @@ Service Definition 同时把可移植标识符子集收窄为 `[A-Za-z_][A-Za-z0
**每个后端声明自己的排除集。** 拒绝:这让可移植承诺变成逐后端成立。调用方在 worker 上测过的绑定列表可能被 Python 拒绝,而这正是 seam 存在要防止的分裂。
**Python 后端导入 worker 的常量。** 拒绝:这反转依赖——seam 的 Service provider 会为一个二者都不拥有的约定伸手进入兄弟实现。约定属于二者之上,即 seam。
**Python 后端导入 worker 的常量。** 拒绝:这反转依赖——seam 的 Service Provider 会为一个二者都不拥有的约定伸手进入兄弟实现。约定属于二者之上,即 seam。
**在可移植标识符子集中保留 `$`。** 拒绝:`$` 是 JS 专有拼写。允许它会让 `$tools` 通过 worker 却在每个非 JS 后端上失败,为纯粹表面的好处破坏可移植性。
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-10-product-subagent-providers-in-shared-host.md
2026-08-10-product-subagent-providers-in-shared-host.md: 33b6eb6cf7a6c19e9ea71cdb7dc8881e8052ef24
2026-08-10-product-subagent-providers-in-shared-host.zh.md: fd78c7a3fee4e4ee30d27d87c752e1a23576fd85
2026-08-10-product-subagent-providers-in-shared-host.md: a747d641ae112d114912958c289fe00b592e6ea5
2026-08-10-product-subagent-providers-in-shared-host.zh.md: fef69e8a2d18135cbc9d5f0d80134fa5701bbbd0
@@ -12,21 +12,21 @@ The placement decision must preserve two independent facts. Loading a provider m
## Decision
Every shipped Profile loads the fixed `codex` and `claude-code` providers once through the base bundle's host plane. Loading either plugin only registers a dormant backend; the corresponding Codex or Claude process starts on the first actual delegation call. Agent Presets independently contribute ordinary `dsh-tool-subagent` rows for `subagent_codex` and `subagent_claude_code`, so a preset can expose neither tool, either one, or both without changing the provider registry.
Product providers remain process-scoped host-plane registrations. The [production-install exclusion decision](../simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.md) supersedes only this note's former base-bundle installation choice: production `dsh-base` neither depends on nor mounts them. A Profile that opts in installs the selected provider package and mounts it once on the host plane. Loading either plugin only registers a dormant backend; the corresponding Codex or Claude process starts on the first actual delegation call. Agent Presets independently contribute ordinary `dsh-tool-subagent` rows for `subagent_codex` and `subagent_claude_code`, so a preset can expose neither tool, either one, or both without changing the provider registry.
This decision supersedes only the opt-in composition placement recorded by the provider-contract note. That note continues to own each product protocol, result mapping, cancellation, process-tree lifecycle, and evidence tiers. The [Agent Preset architecture](2026-08-03-per-session-agent-presets.md) continues to own the Host/Agent split, preset authoring, and the rule that edits affect only newly composed sessions.
This note continues to own why a mounted product provider belongs on the host plane while its model-facing tool belongs to an Agent Preset. The production-install exclusion decision owns which Profiles install those optional packages. The provider-contract note continues to own each product protocol, result mapping, cancellation, process-tree lifecycle, and evidence tiers. The [Agent Preset architecture](2026-08-03-per-session-agent-presets.md) continues to own the Host/Agent split, preset authoring, and the rule that edits affect only newly composed sessions.
The providers use products already selected by the host environment. Codex starts `codex` from `PATH`; Claude Code resolves `claude` through the shared subprocess execution world and passes the exact path to the official SDK. Profile loading does not install a product, create product state, probe a version, test authentication, or add product-specific settings. Missing commands and product failures remain local to the attempted delegation.
The current base dependency closure still includes the Claude Agent SDK's optional platform CLI payload even though production resolves the host `claude`. Removing that unused payload belongs to the separate product installation-closure follow-up; this placement decision neither installs it dynamically nor treats it as the production executable.
Only a Profile that selects the Claude Code provider carries the Claude Agent SDK's optional platform CLI payload. Production still resolves the host `claude`; the SDK payload remains provider-package installation cost rather than the production executable.
## Verification
The base Loader test proves both provider names register exactly once and no product process starts during Profile boot. Real Agent Preset composition covers none, Codex-only, Claude-only, and both tool sets, including generation isolation after an authored preset changes. Keyless ACP snapshots pin the model-visible tool schemas for one and both products, while provider tests separately prove native executable resolution, failure, cancellation, and process-tree quiescence.
The base bundle test proves production `dsh-base` contains neither product provider dependency nor provider row. The Web composition explicitly mounts both optional providers and covers none, Codex-only, Claude-only, and both tool sets, including generation isolation after an authored preset changes. Package-owned Loader compositions prove the Codex-only and dual-provider opt-in paths register the selected providers without starting a product process. Keyless ACP snapshots pin the model-visible tool schemas for one and both products, while provider tests separately prove native executable resolution, failure, cancellation, and process-tree quiescence.
## Alternatives considered
**Keep product providers opt-in at the Profile layer.** This preserves a smaller default dependency closure, but a copied or agent-authored Preset row is not usable unless the person also discovers and edits a second composition layer. It leaves the general Preset entry incomplete for these otherwise ordinary tools.
**Keep product providers opt-in at the Profile layer.** This preserves a smaller default dependency closure but requires the user to edit both a Profile and a Preset. The production-install exclusion decision accepts that installation trade-off; this note retains the requirement that any selected provider is mounted once on the host plane rather than inside the preset.
**Store global or per-Profile product enable switches.** A process switch competes with the Preset as owner of model-visible tools and cannot express two sessions using different combinations. Availability and authentication are deployment facts, not another persisted product state.
@@ -36,6 +36,6 @@ The base Loader test proves both provider names register exactly once and no pro
## Consequences
A user manages both products through the same Agent Preset authoring path as other plugins, and each new session receives exactly the tools its chosen preset contributes. Every Profile carries two dormant provider registrations, so unused products consume package and module-loading footprint but no product process, login, model call, or product home.
A user installs each selected product provider in a Profile and exposes its tool through the same Agent Preset authoring path as other plugins. Each new session receives exactly the tools its chosen preset contributes. Profiles that do not select a product provider carry no corresponding package or module-loading footprint; loading a selected provider still starts no product process, login, model call, or product home.
The Host registry remains the single provider authority and each Preset remains the single model-tool authority. The trade-off is the current Claude SDK optional-payload installation cost, which stays explicitly deferred rather than being hidden behind another enable state or installer lifecycle.
The Host registry remains the single provider authority and each Preset remains the single model-tool authority. The trade-off is a two-layer opt-in: the Profile owns installation and host-plane registration, while the Preset owns per-agent exposure. Selecting the Claude provider also accepts its current SDK optional-payload installation cost.
@@ -12,21 +12,21 @@ Status: implemented
## 决策
每个随发行版交付的 Profile 都会通过 base 组合包的宿主平面,把固定的 `codex``claude-code` 提供方各加载一次。加载任一插件只会注册一个休眠后端;对应的 Codex 或 Claude 进程直到第一次实际委派调用时才启动。Agent Preset 分别通过普通的 `dsh-tool-subagent` 行贡献 `subagent_codex``subagent_claude_code`,因此一个 preset 可以不暴露任何工具、只暴露其中一个或同时暴露两者,而无需更改提供方注册表。
产品提供方仍是进程级的 host plane(宿主平面)注册。[生产安装排除决策](../simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.md)只取代本说明原先由 base bundle 安装提供方的选择:生产 `dsh-base` 既不依赖也不挂载它们。选择产品集成的 Profile 会安装目标提供方包,并在 host plane 挂载一次。加载任一插件只会注册一个休眠后端;对应的 Codex 或 Claude 进程直到第一次实际委派调用时才启动。Agent Preset 分别通过普通的 `dsh-tool-subagent` 行贡献 `subagent_codex``subagent_claude_code`,因此一个 preset 可以不暴露任何工具、只暴露其中一个或同时暴露两者,而无需更改提供方注册表。
决策仅取代提供方约定说明所记录的、原先由用户选择启用的组装位置。该说明仍负责每个产品的协议、结果映射、取消、进程树生命周期与证据层级。[Agent Preset 架构](2026-08-03-per-session-agent-presets.md)仍负责宿主与 agent 的划分、preset 创作,以及改动只影响新组装会话的规则。
说明继续负责解释为什么已经挂载的产品提供方属于 host plane,而面向模型的工具属于 Agent Preset。生产安装排除决策负责哪些 Profile 安装这些可选包。提供方约定说明继续负责每个产品的协议、结果映射、取消、进程树生命周期与证据层级。[Agent Preset 架构](2026-08-03-per-session-agent-presets.md)仍负责宿主与 agent 的划分、preset 创作,以及改动只影响新组装会话的规则。
这些提供方使用宿主环境已经选定的产品。Codex 启动 `codex`,该命令从 `PATH` 解析;Claude Code 通过共享的子进程执行世界解析 `claude`,并把确切路径交给官方 SDK。加载 Profile 不会安装产品、创建产品状态、探测版本、测试身份验证,也不会新增产品专属设置。命令缺失和产品故障仍局限于发生问题的那次委派。
当前 base 依赖闭包仍包含 Claude Agent SDK 的可选平台 CLI(命令行界面)载荷,尽管生产环境解析的是宿主提供的 `claude`。移除这份未使用载荷属于独立的产品安装闭包后续项;本归属决策既不会动态安装它,也不会将它当作生产可执行文件。
只有选择 Claude Code 提供方的 Profile 才会携带 Claude Agent SDK 的可选平台 CLI(命令行界面)载荷生产环境解析宿主提供的 `claude`;这份 SDK 载荷是提供方包的安装成本,而不是生产可执行文件。
## 验证
base Loader 测试证明两个提供方名称都恰好注册一次,而且 Profile 启动期间不会启动产品进程。真实 Agent Preset 组装覆盖不暴露任何工具、仅暴露 Codex、仅暴露 Claude 和同时暴露两者这四种工具集合,也覆盖自行创作的 preset 发生改动后的代际隔离。无密钥 ACPAgent Client Protocol)快照固定单个产品与两个产品同时启用时的模型可见工具 schema,提供方测试则另行证明原生可执行文件解析、失败、取消和进程树完全停稳。
base bundle 测试证明生产 `dsh-base` 既不包含产品提供方依赖,也不包含提供方配置行。Web 组装显式挂载两个可选提供方,并覆盖不暴露任何工具、仅暴露 Codex、仅暴露 Claude 和同时暴露两者这四种工具集合,也覆盖自行创作的 preset 发生改动后的代际隔离。由包负责的 Loader 组装证明 Codex-only 与双提供方按需启用路径会注册选中的提供方,而不会启动产品进程。无密钥 ACPAgent Client Protocol)快照固定单个产品与两个产品同时启用时的模型可见工具 schema,提供方测试则另行证明原生可执行文件解析、失败、取消和进程树完全停稳。
## 考虑过的替代方案
**将产品提供方保留为 Profile 层的按需启用项。** 这样可缩小默认依赖闭包,但复制或由 agent 创作的 Preset 行无法直接使用,除非用户还发现并编辑第二个组装层。对于这些本来与其他工具无异的工具,通用 Preset 入口仍不完整
**将产品提供方保留为 Profile 层的按需启用项。** 这样可缩小默认依赖闭包,但要求用户同时编辑 Profile 与 Preset。生产安装排除决策接受这项安装取舍;本说明保留的要求是,任何被选中的提供方都在 host plane 挂载一次,而不是放入 preset
**存储全局或按 Profile 配置的产品启用开关。** 进程级开关会与 Preset 争夺模型可见工具的责任归属,也无法表示两个会话使用不同组合。可用性与身份验证属于部署事实,并非另一份需要持久化的产品状态。
@@ -36,6 +36,6 @@ base Loader 测试证明两个提供方名称都恰好注册一次,而且 Prof
## 后果
用户通过与其他插件相同的 Agent Preset 创作路径管理两个产品,每个新会话只会获得其所选 preset 所贡献的工具。每个 Profile 都携带两个休眠的提供方注册,因此未使用的产品会产生包和模块加载开销,但不会启动产品进程、登录、调用模型或创建产品主目录。
用户在 Profile 中安装每个被选中的产品提供方,再通过与其他插件相同的 Agent Preset 创作路径暴露它的工具。每个新会话只会获得其所选 preset 所贡献的工具。没有选择产品提供方的 Profile 不承担对应包或模块加载开销;加载已选择的提供方仍不会启动产品进程、登录、调用模型或创建产品主目录。
宿主注册表仍是提供方的唯一权威,每个 Preset 仍是模型工具的唯一权威。代价是当前 Claude SDK 可选载荷的安装成本继续被明确延期处理,而不会隐藏在另一种启用状态或安装程序生命周期之后
宿主注册表仍是提供方的唯一权威,每个 Preset 仍是模型工具的唯一权威。代价是两层按需启用:Profile 负责安装与 host plane 注册,Preset 负责按 agent 暴露。选择 Claude 提供方还会接受当前 SDK 可选载荷的安装成本
@@ -1,6 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/proposed/architecture/2026-08-11-repository-naming-contract-and-rename-ledger.md
2026-08-11-repository-naming-contract-and-rename-ledger.md: ad5a2061c40011b3f8c32ecbb62a0b545f82f9f6
2026-08-11-repository-naming-contract-and-rename-ledger.zh.md: 8f1b3505f0cc16a6e3e6429b5924cabda231f00f
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-11-repository-naming-contract-and-rename-ledger.md
2026-08-11-repository-naming-contract-and-rename-ledger.md: 895a202256504690b0451e6c09f3fc4ea8e7f4db
2026-08-11-repository-naming-contract-and-rename-ledger.zh.md: 897d06d95b3a05fb53d36146fe1ac1f0e1330a7a
@@ -1,34 +1,34 @@
# Agent Note: Repository naming contract and pre-release rename ledger
Status: proposed
Status: implemented
English | [中文](2026-08-11-repository-naming-contract-and-rename-ledger.zh.md)
## Problem
The repository has grown faster than some names. Several package names still describe the first implementation instead of the capability. Several classes use `Service` even when they are registries, runtimes, engines, controllers, or resolvers. Some `ctx` keys are singular for registries and plural for one engine. Some provider names say `local` when they use replaceable filesystem or subprocess services and can run in another execution world.
The repository had grown faster than some names. Several package names described the first implementation instead of the capability. Several classes used `Service` even when they were registries, runtimes, engines, controllers, or resolvers. Some `ctx` keys were singular for registries and plural for one engine. Some provider names said `local` even though they used replaceable filesystem or subprocess services and could run in another execution world.
These names are not harmless. A name tells a contributor where a responsibility starts and stops. `Store` suggests data access. `Registry` suggests registrations and lookup. `Runtime` suggests live execution and lifecycle. When one word is used for all three, callers cannot tell which object owns policy, work, or state without reading the implementation.
The repository also uses `SDK` in two meanings. The supported Python and TypeScript clients use the JSON-RPC SDK protocol. The project as a whole is DeepSeek Harness, not an SDK project. The removed SDK project toolchain made the broad meaning obsolete, but current prose and names still preserve parts of it.
The repository also used `SDK` in two meanings. The supported Python and TypeScript clients use the JSON-RPC SDK protocol. The project as a whole is DeepSeek Harness, not an SDK project. The removed SDK project toolchain made the broad meaning obsolete, but prose and names preserved parts of it.
This is the last pre-release point at which repository-wide renames are cheap. Keeping weak names would turn accidental vocabulary into a compatibility contract.
The last pre-release window made repository-wide renames cheap. Keeping weak names would have turned accidental vocabulary into a compatibility contract.
## Proposal
## Decision
Apply the rename ledger in this note before the first tagged release. Change names only. Do not change package responsibilities, service boundaries, behavior, defaults, or data models in a rename change. If a name exposes a bad boundary, write a separate proposed Agent Note for that boundary change.
The repository uses every current name in this ledger. This decision changes names only; package responsibilities, service boundaries, behavior, defaults, and data models stay the same. A name that exposes a bad boundary requires a separate proposed Agent Note for that boundary change.
Each accepted family rename is atomic. Update its directory, npm package name, imports, Cordis plugin name, `ctx` key, public types, directly coupled event or tool identifiers, configuration, tests, fixtures, examples, generated references, and current documentation where the ledger names those surfaces. Do not leave an alias, compatibility package, duplicate service key, dual event name, or fallback parser. The repository is pre-release and rejects the old name.
Each renamed family has one vocabulary. Its directory, npm package name, imports, Cordis plugin name, `ctx` key, public types, directly coupled event or tool identifiers, configuration, tests, fixtures, examples, generated references, and current documentation use the current name where the ledger names those interfaces. No alias, compatibility package, duplicate service key, dual event name, or fallback parser remains. The repository rejects the old name.
Implementation can use more than one PR to keep review practical. One PR must not leave one family with two public vocabularies. The final state must satisfy the complete ledger.
No family exposes two public vocabularies.
### Use `SDK` for one thing
`SDK` means the JSON-RPC-based client/server protocol used by the supported Python and TypeScript SDKs. Keep `@deepseek-ai/dsh-sdk-client`, `@deepseek-ai/dsh-sdk-protocol`, and the wire identity `deepseek-harness-sdk-runtime`. Rename the JSON-RPC server into that family. Do not call DeepSeek Harness itself an SDK, and do not restore the removed project generator, launcher, helper, or launcher telemetry packages.
`SDK` means the JSON-RPC-based client/server protocol used by the supported Python and TypeScript SDKs. The repository keeps `@deepseek-ai/dsh-sdk-client`, `@deepseek-ai/dsh-sdk-protocol`, and the wire identity `deepseek-harness-sdk-runtime`; the JSON-RPC server belongs to the same family. DeepSeek Harness itself is not an SDK, and the removed project generator, launcher, helper, and launcher telemetry packages stay absent.
If accepted, this proposal will partially supersede three active decisions. It replaces the retained `bash/`, `pty/`, and `self-modification/` group names and both deferred package targets in the [package-regrouping decision](../../implemented/architecture/2026-07-29-package-regrouping.md). It replaces only the repository-wide SDK claim in the [SDK project toolchain removal](../../implemented/simplification/2026-08-11-remove-sdk-project-toolchain.md), which remains the owner of the deletion and the surviving runtime SDK. It replaces only the package-name rationale in the [tool-call timeout policy](../../implemented/architecture/2026-07-07-tool-call-timeout-policy.md); the timeout mechanism and its `guard/timeout-policy/` home remain unchanged.
This decision partially supersedes three active decisions. It replaces the retained `bash/`, `pty/`, and `self-modification/` group names and both deferred package targets in the [package-regrouping decision](2026-07-29-package-regrouping.md). It replaces only the repository-wide SDK claim in the [SDK project toolchain removal](../simplification/2026-08-11-remove-sdk-project-toolchain.md), which remains the owner of the deletion and the surviving runtime SDK. It replaces only the package-name rationale in the [tool-call timeout policy](2026-07-07-tool-call-timeout-policy.md); the timeout mechanism and its `guard/timeout-policy/` home remain unchanged.
Other implemented notes that use a renamed package, path, or type are not superseded when their boundary and rationale remain intact. The implementation updates those factual names. It adds reciprocal links only where this proposal changes a decision; a proposal does not make current implemented notes describe unshipped names.
Other implemented notes that use a renamed package, path, or type are not superseded when their boundary and rationale remain intact. They carry the current factual names. The three partially superseded decisions link back to this decision.
### Name the role that exists
@@ -68,7 +68,7 @@ The practical tests are direct. If callers mainly call `register()` and receive
Keep a protocol or dialect name when it distinguishes implementations. Keep `Bash`, `Pwsh`, `JSON-RPC`, `SQLite`, `JSONL`, `OpenTelemetry`, `Claude Code`, and `E2B` where the implementation depends on that mechanism. Do not put `LLM` into a compaction backend name when every current backend already uses the LLM seam; `basic` is the honest neutral name until a more specific algorithm name exists.
Do not invent a `process sandbox` concept. The current `sandbox` family already names its product responsibility, and the accepted sweep does not change that boundary.
Do not invent a `process sandbox` concept. The current `sandbox` family already names its product responsibility. This decision does not change that responsibility.
Use title case for initialisms inside PascalCase identifiers: `Ui`, `Llm`, `JsonRpc`, and `ApiProxy`. Use the conventional uppercase form in prose and package names where applicable: UI, LLM, JSON-RPC, and API. `Typert` is the exact product spelling in identifiers and prose; do not write `TypeRT`, `TypeRt`, or `Typert` with another internal split.
@@ -76,15 +76,15 @@ Do not remove an intentional vendor qualifier to avoid repetition. `dsh-subagent
### Put the rule in project documentation
When the rename implementation lands, add the full role-word contract to the paired package-creation guide at `docs/cookbook/adding-a-package.md`. Add a short linked rule to `packages/AGENTS.md`. Update the terminology table and the root project description so `SDK` and `Typert` have one meaning. The Agent Note owns the rationale and rejected alternatives; the guide owns the rule contributors follow.
The paired package-creation guide at `docs/cookbook/adding-a-package.md` contains the full role-word contract, and `packages/AGENTS.md` links to it. The terminology table and root project description give `SDK` and `Typert` one meaning. This Agent Note owns the rationale and rejected alternatives; the guide owns the rule contributors follow.
## Rename ledger
The tables list public and repository-wide names. Private local variables follow the new vocabulary when they refer to the same role. A retained low-level or product-visible name is stated where a broad replacement would be wrong.
The tables record public and repository-wide renames. The `Current` column holds the current name. Private local variables use the same vocabulary when they refer to the same role. A retained low-level or product-visible name is stated where a broad replacement would be wrong.
### Runtime SDK
| Current | Proposed | Reason |
| Former | Current | Reason |
|---|---|---|
| `@deepseek-ai/dsh-jsonrpc` | `@deepseek-ai/dsh-sdk-jsonrpc-server` | It is the server half of the SDK protocol. `jsonrpc` alone names an encoding; `sdk-jsonrpc-server` gives the family, mechanism, and role. |
| `HarnessSdkServer` | `HarnessSdkJsonRpcServer` | The class is one JSON-RPC server implementation, not every possible SDK server. |
@@ -93,7 +93,7 @@ Keep `@deepseek-ai/dsh-sdk-client`, `@deepseek-ai/dsh-sdk-protocol`, and `deepse
### Shell and terminal
| Current | Proposed | Reason |
| Former | Current | Reason |
|---|---|---|
| `packages/bash/` | `packages/shell/` | The group contains the dialect-neutral executor seam, Bash and PowerShell implementations, environment support, and shell tools. |
| `@deepseek-ai/dsh-bash`, `ctx.bash` | `@deepseek-ai/dsh-shell`, `ctx.shell` | PowerShell already implements this seam. The capability is shell execution, not Bash. |
@@ -113,7 +113,7 @@ Keep the Bash- and PowerShell-specific leaf packages, plugin ids, types, and too
### Language server and jobs
| Current | Proposed | Reason |
| Former | Current | Reason |
|---|---|---|
| `@deepseek-ai/dsh-lsp-local` | `@deepseek-ai/dsh-lsp-stdio` | The provider speaks LSP over stdio through replaceable filesystem and subprocess services. It is not necessarily local. |
| `packages/tasks/` | `packages/jobs/` | The family owns detached tool jobs. `jobs` is short and avoids collision with user task or todo concepts. |
@@ -131,7 +131,7 @@ Keep the base LSP package, `ctx.lsp`, LSP protocol types, and the LSP tool. The
### Input triggers, tool presentation, permission presets, and user questions
| Current | Proposed | Reason |
| Former | Current | Reason |
|---|---|---|
| `@deepseek-ai/dsh-client-ui-slash`, `ui-slash/` | `@deepseek-ai/dsh-client-ui-input-trigger`, `ui-input-trigger/` | The client handles `/`, `@`, keyboard arbitration, candidate menus, and programmatic launch. It is not only slash commands. |
| `ctx.slash`, `SlashService`, `SlashController`, `SlashSource` | `ctx.inputTriggers`, `InputTriggerService`, `InputTriggerController`, `InputTriggerSource` | The names cover every supported trigger and keep the existing service, controller, and source roles. Coupled locale and public type names follow `InputTrigger`. |
@@ -144,11 +144,11 @@ Keep the base LSP package, `ctx.lsp`, LSP protocol types, and the LSP tool. The
| `ctx.userInteraction`, `UserInteractionService`, `UserInteractionProvider`, `UserInteractionError` | `ctx.userQuestions`, `UserQuestionService`, `UserQuestionProvider`, `UserQuestionError` | These names state the one supported interaction form. Keep `AskUserQuestion*`, the `ask_user_question` tool, and `@deepseek-ai/dsh-tool-ask-user`. |
| `docs/subsystems/user-interaction.md` | `docs/subsystems/user-questions.md` | The page documents questions and answers only. |
Keep `/permission`, the `permissions` projection, the `permission` settings namespace, and `permission/preset`; they are accurate product or durable vocabulary. Keep the full `PermissionPresetSettingsController` name. Dropping `Preset` would remove the word that limits its authority. Plan a separate proposal to remove the `both` tool-presentation mode; this rename does not remove behavior.
Keep `/permission`, the `permissions` projection, the `permission` settings namespace, and `permission/preset`; they are accurate product or durable vocabulary. Keep the full `PermissionPresetSettingsController` name. Dropping `Preset` would remove the word that limits its authority. Removal of the `both` tool-presentation mode remains deferred to a separate proposal; this rename does not remove behavior.
### Typert, API gateway, and tools
| Current | Proposed | Reason |
| Former | Current | Reason |
|---|---|---|
| `packages/typert/type-meta/`, `@deepseek-ai/dsh-type-meta` | `typert/protocol/`, `@deepseek-ai/dsh-typert-protocol` | The package owns the Typert Remote protocol, decorators, bindings, codecs, lookups, and context contracts. It is not generic type metadata. |
| `GatewayService` in the protocol package | `TypertRemoteService` | The base class marks a same-process service for Remote export. It is not the API gateway. |
@@ -162,7 +162,7 @@ Keep `@deepseek-ai/dsh-tools` and `ctx.tools`. Keep `@deepseek-ai/dsh-api-gatewa
### Workspace instructions, telemetry, identity, and launch environment
| Current | Proposed | Reason |
| Former | Current | Reason |
|---|---|---|
| Host `ctx.workspace` | Host `ctx.workspaceRegistry` | `WorkspaceRegistry` owns multiple workspaces, but Client `ctx.workspaces` already has an incompatible type. Both declarations merge into the same Cordis `Context` interface at compile time even though their runtime contexts are separate. The role suffix states the host service and avoids that collision. Keep `@deepseek-ai/dsh-workspace`, `WorkspaceRegistry`, `Workspace`, and `workspace.*` wire names. |
| `@deepseek-ai/dsh-workspace-context`, `context/workspace-context/` | `@deepseek-ai/dsh-agent-instructions`, `context/agent-instructions/` | The package loads hierarchical `AGENTS.md` and `CLAUDE.md` files for the agent. It is not general workspace context. |
@@ -181,7 +181,7 @@ Keep `@deepseek-ai/dsh-tools` and `ctx.tools`. Keep `@deepseek-ai/dsh-api-gatewa
### Schedule, workflow, goals, and compaction
| Current | Proposed | Reason |
| Former | Current | Reason |
|---|---|---|
| `@deepseek-ai/dsh-tool-schedule`, `schedule/tool-schedule/`, plugin `tool-schedule` | `@deepseek-ai/dsh-schedule`, `schedule/schedule/`, plugin `schedule` | The package owns the durable Schedule domain, persistence barriers, management tools, timers, follow-ups, and runtime lifecycle. `tool-` describes only one part. |
| `ScheduleOwner` | `ScheduleRuntime` | The per-agent object runs live timers, durable projection, dispatch, idle waits, and disposal. `Owner` does not state that execution role. Coupled private `owner*` names follow `runtime*`. |
@@ -194,11 +194,11 @@ Keep `@deepseek-ai/dsh-tools` and `ctx.tools`. Keep `@deepseek-ai/dsh-api-gatewa
| `@deepseek-ai/dsh-compact-basic`, `BasicCompactService`, public `BasicCompact*` | `@deepseek-ai/dsh-compaction-basic`, `BasicCompactionEngine`, corresponding `BasicCompaction*` | `basic` is plain but honest. `compaction-llm` adds no information because LLM use is already part of the current implementation family. |
| `@deepseek-ai/dsh-compact-tool-result-prune`, `ToolResultPruneService`, `ctx.toolResultPrune` | `@deepseek-ai/dsh-compaction-tool-result-pruner`, `ToolResultPruner`, `ctx.toolResultPruner` | The plugin is an actor that prunes tool results. The noun `pruner` names that role. |
Keep `/compact`, the command package, and the rejected decision to preserve separate compaction definition and provider packages. The rename changes vocabulary, not that package boundary.
Keep `/compact`, the command package, and the separate compaction definition and provider packages. Merging those packages remains rejected. The rename changes vocabulary, not that package boundary.
### Settings, credentials, client modules, and small core roles
| Current | Proposed | Reason |
| Former | Current | Reason |
|---|---|---|
| Abstract `Settings` | `SettingsProvider` | The class supplies settings through a replaceable capability. Keep the package, key, and events. |
| `@deepseek-ai/dsh-settings-local`, `SettingsLocal` | `@deepseek-ai/dsh-settings-file`, `FileSettingsProvider` | The implementation is file-backed through the filesystem seam. `file` states the mechanism; `local` does not. |
@@ -216,7 +216,7 @@ Keep `/compact`, the command package, and the rejected decision to preserve sepa
### Host web server, session data, and code execution
| Current | Proposed | Reason |
| Former | Current | Reason |
|---|---|---|
| `HttpServerService`, `ctx.httpServer` | `WebServer`, `ctx.webServer` | The server owns HTTP routes and WebSocket upgrade routes. `Web` leaves room for both; `Http` is too narrow here. Keep `packages/host/webserver`, `@deepseek-ai/dsh-host-webserver`, `WebRoute`, and `WebUpgradeRoute`. |
| Documentation subsystem label `http-server` | `web-server` | The subsystem must use the same scope as the service. |
@@ -233,7 +233,7 @@ Keep the complete session projection family and `SessionProjection*` vocabulary.
### Filesystem, skill, subagent, and web providers
| Current | Proposed | Reason |
| Former | Current | Reason |
|---|---|---|
| `@deepseek-ai/dsh-fs-policy` | `@deepseek-ai/dsh-fs-observation-policy` | The package defines which filesystem observations authorize later effects. It is not the complete filesystem or sandbox policy. |
| `FsPolicyExec` | `FsObservationActor` | The value names the actor whose observations and effects the policy relates. It does not execute the policy itself. |
@@ -251,7 +251,7 @@ Keep `@deepseek-ai/dsh-subagent-dsh-sdk`, its provider id `dsh-sdk`, external AC
### Hooks, guards, plan mode, extensions, and diagnostics
| Current | Proposed | Reason |
| Former | Current | Reason |
|---|---|---|
| `@deepseek-ai/dsh-hooks-claude`, `ClaudeHookConfig`, `parseClaudeConfig`, dialect `claude` | `@deepseek-ai/dsh-hooks-claude-code`, `ClaudeCodeHookConfig`, `parseClaudeCodeConfig`, dialect `claude-code` | The hook bridge targets Claude Code, not every Anthropic or Claude product. |
| `@deepseek-ai/dsh-repeat-tool-guard`, plugin/source `repeat-tool-guard` | `@deepseek-ai/dsh-repeat-tool-reminder`, plugin/source `repeat-tool-reminder` | The plugin adds a model reminder. It does not block or enforce a guard decision. |
@@ -263,11 +263,11 @@ Keep `@deepseek-ai/dsh-subagent-dsh-sdk`, its provider id `dsh-sdk`, external AC
| `InvariantService` | `InvariantRegistry` | The object owns registered invariant checks. Keep `@deepseek-ai/dsh-invariants` and `ctx.invariants`. |
| `packages/client/test-runtime/` | `packages/test-support/client-runtime/` | The package is client test infrastructure. Keep its npm name if it already states that contract. |
Keep MCP, Todo, and the Plan Mode package, key, events, and tool names. The accepted change concerns the controller class, not the product feature.
Keep MCP, Todo, and the Plan Mode package, key, events, and tool names. This decision renames the controller class, not the product feature.
### Utilities, E2B, host, bundles, examples, and applications
| Current | Proposed | Reason |
| Former | Current | Reason |
|---|---|---|
| `util/paths/`, `@deepseek-ai/dsh-paths` | `util/home-paths/`, `@deepseek-ai/dsh-home-paths` | The helpers resolve paths under the Harness home. They are not a general path library. Keep the individual function names when they already state the returned path. |
| `util/retention/`, `@deepseek-ai/dsh-retention` | `util/output-retention/`, `@deepseek-ai/dsh-output-retention` | The policy retains command and tool output. It is not a general data-retention framework. |
@@ -281,7 +281,7 @@ Keep atomic-write, brand, native-command, timeout utility, directory-picker, `ds
### Client runtime and UI
| Current | Proposed | Reason |
| Former | Current | Reason |
|---|---|---|
| `SlotsService` | `SlotRegistry` | The object owns named slot declarations and registrations. |
| `SessionsService` | `SessionRuntime` | The object owns live client session coordination, not a passive session list. |
@@ -365,26 +365,22 @@ The following debated names stay unchanged because the current scope is accurate
**Keep aliases for old names.** Rejected. No released consumer needs them. Aliases would preserve two vocabularies and make the first release carry a migration that never had a user.
## Acceptance criteria
## Verification
- Every mapping in the ledger is applied, or this proposal is amended before implementation to explain a changed decision.
- Each family has one public vocabulary. No compatibility package, re-export alias, duplicate `ctx` key within one Cordis context, dual plugin id, dual event id, old tool alias, or fallback parser remains.
- The change is rename-only. Runtime behavior, package boundaries, defaults, policy, durable semantics, and model behavior stay equivalent except where an identifier is itself visible.
- Package directories, npm names, imports, manifests, TypeScript references and paths, Cordis config, plugin ids, service keys, events, tools, RPC names, persisted names named by the ledger, fixtures, snapshots, examples, generated catalogs, and current prose agree with the new vocabulary.
- Current implemented Agent Notes are updated with factual name and path changes when the implementation lands. The package-regrouping note records the new group inventory and package targets, the SDK removal note does not call the repository an SDK, and the timeout-policy note records the new package-name rationale. Notes whose architectural decision remains current are not rewritten into new decisions.
- Every mapping in the ledger appears in the repository. Each family has one public vocabulary; no compatibility package, re-export alias, duplicate `ctx` key within one Cordis context, dual plugin id, dual event id, old tool alias, or fallback parser remains.
- Runtime behavior, package boundaries, defaults, policy, durable semantics, and model behavior remain equivalent except where an identifier is itself visible.
- Package directories, npm names, imports, manifests, TypeScript references and paths, Cordis config, plugin ids, service keys, events, tools, RPC names, persisted names named by the ledger, fixtures, snapshots, examples, generated catalogs, and current prose use the current vocabulary.
- Current implemented Agent Notes carry the factual name and path changes. The package-regrouping note records the group inventory and package targets, the SDK removal note reserves `SDK` for the runtime protocol, and the timeout-policy note records the package-name rationale.
- The paired package-creation guide contains the role-word contract, `packages/AGENTS.md` links to it, the terminology table records the chosen words and `Typert` spelling, and root project prose calls the product DeepSeek Harness rather than DeepSeek Harness SDK.
- The removed SDK project toolchain stays absent.
- Focused tests cover each renamed family; source-plane typecheck, build, package hygiene, generated-reference gates, snapshots affected by visible identifiers, translation pairing, `doc-sync`, and lint pass on the complete implementation.
- The proposed note moves to `implemented/` only after the full ledger and documentation contract are true. Its implementation form describes the final state, not a migration checklist.
- `pnpm run check:ci` covers source-plane typecheck, build, package hygiene, generated-reference checks, affected snapshots, translation pairing, `doc-sync`, and lint. Release-shaped Python runtime smokes and required CI cover packaged-runtime and platform paths.
## Risks
## Consequences
The sweep changes many imports, paths, configuration strings, generated references, and model-visible names. A missed string can compile but fail at load or replay time. Implementation must use both type-directed edits and exact old-name searches across source, config, tests, fixtures, docs, and generated inputs.
The repository has one vocabulary for each renamed family. Old on-disk names, wire values, tool names, and configuration entries named in the ledger do not work. An owning parser that can identify stale configuration fails clearly instead of accepting both forms.
Concurrent work will conflict with moved paths and renamed symbols. This is temporary pre-release cost. Splitting the implementation by coherent family can reduce review and merge conflicts, but each family must remain atomic.
Some names are longer. The extra word is intentional when it prevents a false claim about authority or mechanism. A long name remains wrong when every word does not constrain the role.
Some names will become longer. The extra word is intentional when it prevents a false claim about authority or mechanism. Long names are still a failure when every word does not constrain the role.
Role suffixes do not replace inspection of behavior. The package guide keeps the direct tests from this decision: inspect what callers do, what lifetime the object owns, and what failure or policy it controls.
The role words can be applied mechanically if reviewers check suffixes without checking behavior. The guide must keep the direct tests in this note: inspect what callers do, what lifetime the object owns, and what failure or policy it controls.
Old on-disk names, wire values, tool names, and configuration entries named in the ledger will stop working. This is accepted before release. The implementation must fail clearly on stale configuration where the owning parser can identify it; it must not silently accept both forms.
Branches based on the former paths and symbols require conflict repair. This is a one-time pre-release cost of removing the old vocabulary without compatibility aliases.
@@ -1,34 +1,34 @@
# Agent Note: 仓库命名约定与预发布重命名清单
Status: proposed
Status: implemented
[English](2026-08-11-repository-naming-contract-and-rename-ledger.md) | 中文
## 问题
仓库的发展速度超过部分名称的演进速度。一些包名仍在描述最初的实现,而非所提供的能力。若干类即使实际承担注册表、运行时、引擎、控制器或解析器的职责,名称仍使用 `Service`。部分 `ctx` 键以单数命名注册表,却以复数命名单个引擎。还有一些提供方明明通过可替换的文件系统或子进程服务工作,可以在另一执行环境中运行,名称却使用 `local`
仓库的发展速度超过部分名称的演进速度。一些包名描述的是最初的实现,而非所提供的能力。若干类即使实际承担注册表、运行时、引擎、控制器或解析器的职责,名称仍使用 `Service`。部分 `ctx` 键以单数命名注册表,却以复数命名单个引擎。还有一些提供方明明通过可替换的文件系统或子进程服务工作,可以在另一执行环境中运行,名称却使用 `local`
这些名称并非无关紧要。名称会告诉贡献者一项职责从哪里开始、到哪里结束。`Store` 表示数据访问。`Registry` 表示注册与查找。`Runtime` 表示实时执行和生命周期。如果同一个词同时表示这三者,调用方就必须阅读实现,才能判断哪个对象拥有策略、工作或状态。
仓库还在两种含义下使用 `SDK`。受支持的 Python 和 TypeScript 客户端使用 JSON-RPC SDK 协议。项目整体是 DeepSeek Harness,而不是 SDK 项目。已移除的 SDK 项目工具链使宽泛的含义失去依据,但现有文案和名称仍保留了部分旧用法。
仓库还在两种含义下使用 `SDK`。受支持的 Python 和 TypeScript 客户端使用 JSON-RPC SDK 协议。项目整体是 DeepSeek Harness,而不是 SDK 项目。已移除的 SDK 项目工具链使宽泛的含义失去依据,但文案和名称仍保留了部分旧用法。
这是首次发布带标签版本之前最后一个能够低成本完成仓库级重命名的时点。保留含义不清的名称,会让偶然形成的词汇变成兼容性约定。
首次发布带标签版本之前最后一个窗口,使仓库级重命名仍可低成本完成。若继续保留含义不清的名称,偶然形成的词汇就会变成兼容性约定。
## 提案
## 决策
在首次发布带标签版本之前,应用本说明中的重命名清单。只更改名称,不得在重命名变更中改变包职责、服务边界、行为、默认值数据模型。如果某个名称暴露出不合理的边界,另写一份 proposed Agent Note,专门提议边界变更。
仓库使用本清单中的全部当前名称。本决策只更改名称;包职责、服务边界、行为、默认值数据模型保持不变。如果某个名称暴露出不合理的边界,需要另写一份 proposed Agent Note,专门提议边界变更。
组获准的系列重命名都必须原子完成。清单点名某一表面时,应一并更新其目录、NPM 包名、导入、Cordis 插件名称、`ctx` 键、公开类型、直接耦合的事件或工具标识符、配置、测试、fixture(测试前置数据)、示例、生成的参考资料以及当前文档。不得留下别名、兼容包、重复的服务键、双重事件名称或回退解析器。仓库尚未发布,旧名称应被拒绝
个已重命名系列只有一套词汇。清单点名某一接口时,其目录、NPM 包名、导入、Cordis 插件名称、`ctx` 键、公开类型、直接耦合的事件或工具标识符、配置、测试、fixture(测试前置数据)、示例、生成的参考资料以及当前文档都使用当前名称。仓库不保留别名、兼容包、重复的服务键、双重事件名称或回退解析器,并拒绝旧名称
实现可以拆分为多个 PR(Pull Request),以保持评审可行。任何一个 PR 都不得让同一系列同时存在两套公开词汇。最终状态必须满足完整清单
同一系列不会公开两套词汇
### `SDK` 只表示一件事
`SDK` 表示受支持的 Python 和 TypeScript SDK 所使用、基于 JSON-RPC 的客户端/服务器协议。保留 `@deepseek-ai/dsh-sdk-client``@deepseek-ai/dsh-sdk-protocol` 和协议身份 `deepseek-harness-sdk-runtime`。将 JSON-RPC 服务器重命名并纳入该系列。不得将 DeepSeek Harness 本身称为 SDK也不得恢复已移除的项目生成器、启动器、辅助工具启动器遥测包。
`SDK` 表示受支持的 Python 和 TypeScript SDK 所使用、基于 JSON-RPC 的客户端/服务器协议。仓库保留 `@deepseek-ai/dsh-sdk-client``@deepseek-ai/dsh-sdk-protocol` 和协议身份 `deepseek-harness-sdk-runtime`JSON-RPC 服务器属于同一系列。DeepSeek Harness 本身不是 SDK,已移除的项目生成器、启动器、辅助工具启动器遥测包继续保持不存在
如果本提案获准,它将部分取代三项现行决策。它替换[包重新分组决策](../../implemented/architecture/2026-07-29-package-regrouping.md)中保留的 `bash/``pty/``self-modification/` 组名,以及两项暂定包名。它只替换[移除 SDK 项目工具链](../../implemented/simplification/2026-08-11-remove-sdk-project-toolchain.md)中将整个仓库称为 SDK 的说法;后者仍负责说明删除范围和保留的运行时 SDK。它只替换[工具调用超时策略](../../implemented/architecture/2026-07-07-tool-call-timeout-policy.md)中的包名理由;超时机制及其 `guard/timeout-policy/` 归属保持不变。
本决策部分取代三项现行决策。它替换[包重新分组决策](2026-07-29-package-regrouping.md)中保留的 `bash/``pty/``self-modification/` 组名,以及两项暂定包名。它只替换[移除 SDK 项目工具链](../simplification/2026-08-11-remove-sdk-project-toolchain.md)中将整个仓库称为 SDK 的说法;后者仍负责说明删除范围和保留的运行时 SDK。它只替换[工具调用超时策略](2026-07-07-tool-call-timeout-policy.md)中的包名理由;超时机制及其 `guard/timeout-policy/` 归属保持不变。
如果其他已实现说明中的包、路径或类型被重命名,而其边界和理由保持不变,则本提案不会取代这些说明。实现阶段只更新其中的事实名称。只有本提案改变既有决策时才添加双向链接;尚未获准的提案不会让现行的已实现说明提前描述未交付名称
如果其他已实现说明中的包、路径或类型被重命名,而其边界和理由保持不变,则本决策不会取代这些说明。这些说明使用已实现的事实名称。三项被部分取代的决策都链接回本决策
### 按实际职责命名
@@ -68,7 +68,7 @@ Status: proposed
如果协议或方言名称能够区分实现,就应保留。实现依赖相应机制时,保留 `Bash``Pwsh``JSON-RPC``SQLite``JSONL``OpenTelemetry``Claude Code``E2B`。每个当前后端都已使用 LLM(大语言模型)seam 时,不要在压缩后端名称中加入 `LLM`;在出现更具体的算法名称之前,`basic` 才是如实且中性的名称。
不得虚构 `process sandbox` 概念。当前 `sandbox` 系列已经准确命名其产品职责,获准的整体重命名不会改变该边界
不得虚构 `process sandbox` 概念。当前 `sandbox` 系列已经准确命名其产品职责。本决策不改变该职责
PascalCase 标识符中的首字母缩略词使用首字母大写格式:`Ui``Llm``JsonRpc``ApiProxy`。在文案和适用的包名中使用惯例规定的全大写形式:UI、LLM、JSON-RPC 和 API。`Typert` 是标识符和文案中的唯一准确产品拼写;不得写成 `TypeRT``TypeRt`,也不得对 `Typert` 作其他内部拆分。
@@ -76,15 +76,15 @@ PascalCase 标识符中的首字母缩略词使用首字母大写格式:`Ui`
### 将规则写入项目文档
重命名实现合入时,应将完整的职责词约定加入配对的包创建指南 `docs/cookbook/adding-a-package.md`。在 `packages/AGENTS.md` 中添加一条简短规则并链接到该指南。更新术语表和根项目说明使 `SDK``Typert` 各自只有一种含义。Agent Note 负责记录理由和被否决的替代方案;指南负责记录贡献者应遵循的规则。
配对的包创建指南 `docs/cookbook/adding-a-package.md` 包含完整的职责词约定,`packages/AGENTS.md` 链接到该约定。术语表和根项目说明使 `SDK``Typert` 各自只有一种含义。Agent Note 负责记录理由和被否决的替代方案;指南负责记录贡献者应遵循的规则。
## 重命名清单
以下表格列出公开名称和仓库级名称。引用相同职责的私有局部变量也应采用新词汇。若宽泛替换并不正确,清单会明确指出保留的底层名称或产品可见名称。
以下表格记录公开名称和仓库级名称的变更。`当前名称` 栏记录当前名称。引用相同职责的私有局部变量也使用相同词汇。若宽泛替换并不正确,清单会明确指出保留的底层名称或产品可见名称。
### 运行时 SDK
| 当前名称 | 提议名称 | 理由 |
| 名称 | 当前名称 | 理由 |
|---|---|---|
| `@deepseek-ai/dsh-jsonrpc` | `@deepseek-ai/dsh-sdk-jsonrpc-server` | 它是 SDK 协议的服务器端。单独使用 `jsonrpc` 只说明编码;`sdk-jsonrpc-server` 则同时说明所属系列、机制和职责。 |
| `HarnessSdkServer` | `HarnessSdkJsonRpcServer` | 该类是 JSON-RPC 服务器的一种实现,并不代表所有可能的 SDK 服务器。 |
@@ -93,7 +93,7 @@ PascalCase 标识符中的首字母缩略词使用首字母大写格式:`Ui`
### Shell 与终端
| 当前名称 | 提议名称 | 理由 |
| 名称 | 当前名称 | 理由 |
|---|---|---|
| `packages/bash/` | `packages/shell/` | 该组包含方言无关的执行器 seam、Bash 和 PowerShell 实现、环境支持以及 shell 工具。 |
| `@deepseek-ai/dsh-bash`, `ctx.bash` | `@deepseek-ai/dsh-shell`, `ctx.shell` | PowerShell 已经实现该 seam。此项能力是 shell 执行,而不是 Bash。 |
@@ -113,7 +113,7 @@ PascalCase 标识符中的首字母缩略词使用首字母大写格式:`Ui`
### 语言服务器与作业
| 当前名称 | 提议名称 | 理由 |
| 名称 | 当前名称 | 理由 |
|---|---|---|
| `@deepseek-ai/dsh-lsp-local` | `@deepseek-ai/dsh-lsp-stdio` | 该提供方通过可替换的文件系统和子进程服务,以 stdio 传输 LSP。它不一定在本地运行。 |
| `packages/tasks/` | `packages/jobs/` | 该系列负责脱离前台运行的工具作业。`jobs` 简短,并可避免与用户任务或 todo 概念冲突。 |
@@ -131,7 +131,7 @@ PascalCase 标识符中的首字母缩略词使用首字母大写格式:`Ui`
### 输入触发器、工具呈现、权限预设和用户问题
| 当前名称 | 提议名称 | 理由 |
| 名称 | 当前名称 | 理由 |
|---|---|---|
| `@deepseek-ai/dsh-client-ui-slash`, `ui-slash/` | `@deepseek-ai/dsh-client-ui-input-trigger`, `ui-input-trigger/` | 客户端处理 `/``@`、键盘仲裁、候选菜单和程序化启动,并非只处理斜杠命令。 |
| `ctx.slash``SlashService``SlashController``SlashSource` | `ctx.inputTriggers``InputTriggerService``InputTriggerController``InputTriggerSource` | 这些名称覆盖所有受支持的触发器,并保留现有的服务、控制器和来源职责。耦合的区域设置和公开类型名称也改用 `InputTrigger`。 |
@@ -144,11 +144,11 @@ PascalCase 标识符中的首字母缩略词使用首字母大写格式:`Ui`
| `ctx.userInteraction`, `UserInteractionService`, `UserInteractionProvider`, `UserInteractionError` | `ctx.userQuestions`, `UserQuestionService`, `UserQuestionProvider`, `UserQuestionError` | 这些名称说明唯一受支持的交互形式。保留 `AskUserQuestion*``ask_user_question` 工具和 `@deepseek-ai/dsh-tool-ask-user`。 |
| `docs/subsystems/user-interaction.md` | `docs/subsystems/user-questions.md` | 该页面只记录问题和答案。 |
保留 `/permission``permissions` 投影、`permission` 设置命名空间和 `permission/preset`;它们都是准确的产品词汇或持久化词汇。保留完整名称 `PermissionPresetSettingsController`。删除 `Preset` 会去掉限定其权限的词。另行制定提案以移除 `both` 工具呈现模式;本次重命名不移除行为。
保留 `/permission``permissions` 投影、`permission` 设置命名空间和 `permission/preset`;它们都是准确的产品词汇或持久化词汇。保留完整名称 `PermissionPresetSettingsController`。删除 `Preset` 会去掉限定其权限的词。移除 `both` 工具呈现模式的工作仍推迟到另一份提案;本次重命名不移除行为。
### Typert、API 网关与工具
| 当前名称 | 提议名称 | 理由 |
| 名称 | 当前名称 | 理由 |
|---|---|---|
| `packages/typert/type-meta/`, `@deepseek-ai/dsh-type-meta` | `typert/protocol/`, `@deepseek-ai/dsh-typert-protocol` | 该包拥有 Typert Remote 协议、装饰器、绑定、编解码器、查找逻辑和上下文约定。它不是通用类型元数据。 |
| 协议包中的 `GatewayService` | `TypertRemoteService` | 该基类标记要导出为 Remote 的同进程服务。它不是 API 网关。 |
@@ -162,7 +162,7 @@ PascalCase 标识符中的首字母缩略词使用首字母大写格式:`Ui`
### 工作区指令、遥测、身份和启动环境
| 当前名称 | 提议名称 | 理由 |
| 名称 | 当前名称 | 理由 |
|---|---|---|
| Host `ctx.workspace` | Host `ctx.workspaceRegistry` | `WorkspaceRegistry` 拥有多个工作区,但 Client `ctx.workspaces` 已经使用不兼容的类型。即使二者运行时上下文独立,两份声明仍会在编译时合并进同一个 Cordis `Context` 接口。职责后缀明确指出 host 服务,并避免该冲突。保留 `@deepseek-ai/dsh-workspace``WorkspaceRegistry``Workspace``workspace.*` 协议名称。 |
| `@deepseek-ai/dsh-workspace-context`, `context/workspace-context/` | `@deepseek-ai/dsh-agent-instructions`, `context/agent-instructions/` | 该包为 agent(智能体)加载分层的 `AGENTS.md``CLAUDE.md` 文件。它并非通用工作区上下文。 |
@@ -181,7 +181,7 @@ PascalCase 标识符中的首字母缩略词使用首字母大写格式:`Ui`
### 日程、工作流、目标与压缩
| 当前名称 | 提议名称 | 理由 |
| 名称 | 当前名称 | 理由 |
|---|---|---|
| `@deepseek-ai/dsh-tool-schedule``schedule/tool-schedule/`、插件 `tool-schedule` | `@deepseek-ai/dsh-schedule``schedule/schedule/`、插件 `schedule` | 该包拥有持久 Schedule 领域、持久化屏障、管理工具、定时器、后续轮次和运行时生命周期。`tool-` 只描述其中一部分。 |
| `ScheduleOwner` | `ScheduleRuntime` | 该逐 agent 对象运行实时定时器、持久化投影、分派、空闲等待和资源释放。`Owner` 没有说明这一执行职责。耦合的私有 `owner*` 名称也改用 `runtime*`。 |
@@ -194,11 +194,11 @@ PascalCase 标识符中的首字母缩略词使用首字母大写格式:`Ui`
| `@deepseek-ai/dsh-compact-basic``BasicCompactService`、公开的 `BasicCompact*` | `@deepseek-ai/dsh-compaction-basic``BasicCompactionEngine`、对应的 `BasicCompaction*` | `basic` 朴素但准确。`compaction-llm` 没有增加信息,因为当前实现系列已使用 LLM。 |
| `@deepseek-ai/dsh-compact-tool-result-prune`, `ToolResultPruneService`, `ctx.toolResultPrune` | `@deepseek-ai/dsh-compaction-tool-result-pruner`, `ToolResultPruner`, `ctx.toolResultPruner` | 该插件是剪除工具结果的执行主体。名词 `pruner` 说明了这一职责。 |
保留 `/compact`、命令包,以及维持压缩定义包提供方包分离的既有决策。本次重命名只改变词汇,不改变该包边界。
保留 `/compact`、命令包,以及相互独立的压缩定义包提供方包。合并这些包的提议仍被否决。本次重命名只改变词汇,不改变该包边界。
### 设置、凭据、客户端模块和较小的核心职责
| 当前名称 | 提议名称 | 理由 |
| 名称 | 当前名称 | 理由 |
|---|---|---|
| 抽象类 `Settings` | `SettingsProvider` | 该类通过可替换能力提供设置。保留包、键和事件。 |
| `@deepseek-ai/dsh-settings-local`, `SettingsLocal` | `@deepseek-ai/dsh-settings-file`, `FileSettingsProvider` | 该实现通过文件系统 seam 以文件为后端。`file` 说明机制,`local` 则不能。 |
@@ -216,7 +216,7 @@ PascalCase 标识符中的首字母缩略词使用首字母大写格式:`Ui`
### Host Web 服务器、会话数据与代码执行
| 当前名称 | 提议名称 | 理由 |
| 名称 | 当前名称 | 理由 |
|---|---|---|
| `HttpServerService`, `ctx.httpServer` | `WebServer`, `ctx.webServer` | 该服务器拥有 HTTP 路由和 WebSocket 升级路由。`Web` 可以同时涵盖两者;此处的 `Http` 作用域过窄。保留 `packages/host/webserver``@deepseek-ai/dsh-host-webserver``WebRoute``WebUpgradeRoute`。 |
| 文档子系统标签 `http-server` | `web-server` | 子系统标签必须与服务采用相同作用域。 |
@@ -233,7 +233,7 @@ PascalCase 标识符中的首字母缩略词使用首字母大写格式:`Ui`
### 文件系统、skill、subagent 和 Web 提供方
| 当前名称 | 提议名称 | 理由 |
| 名称 | 当前名称 | 理由 |
|---|---|---|
| `@deepseek-ai/dsh-fs-policy` | `@deepseek-ai/dsh-fs-observation-policy` | 该包定义哪些文件系统观察可以授权后续操作。它不是完整的文件系统策略或沙箱策略。 |
| `FsPolicyExec` | `FsObservationActor` | 该值表示策略所关联的观察与操作的执行主体。它本身不执行策略。 |
@@ -251,7 +251,7 @@ PascalCase 标识符中的首字母缩略词使用首字母大写格式:`Ui`
### 钩子、防护、Plan Mode、扩展与诊断
| 当前名称 | 提议名称 | 理由 |
| 名称 | 当前名称 | 理由 |
|---|---|---|
| `@deepseek-ai/dsh-hooks-claude``ClaudeHookConfig``parseClaudeConfig`,方言 `claude` | `@deepseek-ai/dsh-hooks-claude-code``ClaudeCodeHookConfig``parseClaudeCodeConfig`,方言 `claude-code` | 该钩子桥接面向 Claude Code,而非所有 Anthropic 或 Claude 产品。 |
| `@deepseek-ai/dsh-repeat-tool-guard`,插件/来源 `repeat-tool-guard` | `@deepseek-ai/dsh-repeat-tool-reminder`,插件/来源 `repeat-tool-reminder` | 该插件向模型添加提醒,并不阻止工具调用,也不执行防护决策。 |
@@ -263,11 +263,11 @@ PascalCase 标识符中的首字母缩略词使用首字母大写格式:`Ui`
| `InvariantService` | `InvariantRegistry` | 该对象拥有已注册的不变量检查。保留 `@deepseek-ai/dsh-invariants``ctx.invariants`。 |
| `packages/client/test-runtime/` | `packages/test-support/client-runtime/` | 该包是客户端测试基础设施。如果现有 NPM 名称已经说明这一约定,则予以保留。 |
保留 MCP、Todo、Plan Mode 包、键、事件和工具名称。获准的变更针对控制器类,而不是产品功能。
保留 MCP、Todo、Plan Mode 包、键、事件和工具名称。本决策重命名控制器类,而不是产品功能。
### 实用工具、E2B、Host、组合包、示例与应用
| 当前名称 | 提议名称 | 理由 |
| 名称 | 当前名称 | 理由 |
|---|---|---|
| `util/paths/`, `@deepseek-ai/dsh-paths` | `util/home-paths/`, `@deepseek-ai/dsh-home-paths` | 这些辅助函数解析 Harness 主目录下的路径,并非通用路径库。已准确说明返回路径的函数名保持不变。 |
| `util/retention/`, `@deepseek-ai/dsh-retention` | `util/output-retention/`, `@deepseek-ai/dsh-output-retention` | 该策略保留命令和工具输出,而不是通用数据保留框架。 |
@@ -281,7 +281,7 @@ PascalCase 标识符中的首字母缩略词使用首字母大写格式:`Ui`
### 客户端运行时与 UI
| 当前名称 | 提议名称 | 理由 |
| 名称 | 当前名称 | 理由 |
|---|---|---|
| `SlotsService` | `SlotRegistry` | 该对象拥有具名 slot 声明和注册项。 |
| `SessionsService` | `SessionRuntime` | 该对象拥有实时客户端会话协调职责,而不是被动的会话列表。 |
@@ -365,26 +365,22 @@ PascalCase 标识符内部使用 `Ui`,不要使用 `UI`。除非清单明确
**为旧名称保留别名。**不予采纳。没有已发布的消费方需要这些别名。别名会保留两套词汇,使首次发布携带一项从未有用户需要的迁移。
## 验收标准
## 验
- 应用清单中的每项映射;如果决策发生变化,则必须在实施前修订本提案并说明原因
- 每个系列只有一套公开词汇。同一个 Cordis 上下文中不得留下兼容包、重新导出别名、重复的 `ctx` 键、双重插件 id、双重事件 id、旧工具别名或回退解析器
- 变更只能重命名。运行时行为、包边界、默认值、策略、持久化语义和模型行为必须保持等价,只有标识符本身可见时除外
- 包目录、NPM 名称、导入、manifest(元数据清单)、TypeScript 引用和路径、Cordis 配置、插件 id、服务键、事件、工具、RPC 名称、清单点名的持久化名称、fixture、快照、示例、生成的目录和当前文案都采用新词汇
- 实现合入时,以事实性名称和路径变更更新当前处于 implemented 状态的 Agent Note。包重新分组说明应记录新的分组清单和包名目标,SDK 移除说明不得再将仓库称为 SDK,超时策略说明应记录新的包名理由。架构决策仍然有效的说明,不得被重写为新的决策
- 配对的包创建指南应包含职责词约定,`packages/AGENTS.md` 应链接到该约定,术语表应记录选定用词和 `Typert` 拼写,根项目文案应将产品称为 DeepSeek Harness,而不是 DeepSeek Harness SDK。
- 清单中的每项映射都出现在仓库中。每个系列只有一套公开词汇;同一个 Cordis 上下文中没有兼容包、重新导出别名、重复的 `ctx` 键、双重插件 id、双重事件 id、旧工具别名或回退解析器
- 运行时行为、包边界、默认值、策略、持久化语义和模型行为保持等价,只有标识符本身可见时除外
- 包目录、NPM 名称、导入、manifest(元数据清单)、TypeScript 引用和路径、Cordis 配置、插件 id、服务键、事件、工具、RPC 名称、清单点名的持久化名称、fixture、快照、示例、生成的目录和当前文案都使用已实现词汇
- 当前处于 implemented 状态的 Agent Note 使用事实名称和路径。包重新分组说明记录分组清单和包名目标,SDK 移除说明将 `SDK` 限定为运行时协议,超时策略说明记录包名理由
- 配对的包创建指南包含职责词约定,`packages/AGENTS.md` 链接到该约定,术语表记录选定用词和 `Typert` 拼写,根项目文案将产品称为 DeepSeek Harness,而不是 DeepSeek Harness SDK
- 已移除的 SDK 项目工具链继续保持不存在。
- 聚焦测试覆盖每个重命名系列;在完整实现上,源代码平面的类型检查、构建、包卫生检查、生成参考资料门禁、受可见标识符影响的快照、翻译配对、`doc-sync` 和 lint 均应通过
- 只有完整清单和文档约定全部成为事实后,才能将该 proposed 说明移入 `implemented/`。其实现版应描述最终状态,而不是迁移检查清单。
- `pnpm run check:ci` 覆盖源代码平面的类型检查、构建、包卫生检查、生成参考资料检查、受影响的快照、翻译配对、`doc-sync` 和 lint。发布形态的 Python 运行时冒烟测试和必需 CI 覆盖打包运行时与平台路径
## 风险
## 后果
这轮整体重命名会更改大量导入、路径、配置字符串、生成的参考资料和模型可见名称。遗漏的字符串可能通过编译,却在加载或回放时失败。实现时必须结合类型驱动的修改,并在源代码、配置、测试、fixture、文档和生成输入中精确搜索旧名称
仓库为每个重命名系列保留一套词汇。清单点名的旧磁盘名称、协议值、工具名称和配置项不再工作。能够识别陈旧配置的所属解析器会明确报错,而不是同时接受两种形式
并行工作会与路径移动和符号重命名产生冲突。这是发布前的临时成本。按职责一致的系列拆分实现,可以降低评审成本和合并冲突,但每个系列都必须保持原子性
一些名称更长。额外增加的词只有在防止误述权限或机制时才有意义。如果名称中的词不能全部限定职责,长名称仍然错误
一些名称会变得更长。额外增加的词只有在防止误述权限或机制时才有意义。如果名称中的词不能全部限定职责,长名称仍然是失败的命名
职责后缀不能替代对行为的检查。包创建指南保留本决策中的直接判断方式:检查调用方执行什么操作、对象拥有什么生命周期,以及对象控制什么失败或策略
如果评审人只检查后缀而不检查行为,职责词就会被机械套用。指南必须保留本说明中的直接判断方式:检查调用方执行什么操作、对象拥有什么生命周期,以及对象控制什么失败或策略
清单点名的旧磁盘名称、协议值、工具名称和配置项将停止工作。发布前接受这一后果。如果负责解析的解析器能够识别陈旧配置,实现必须明确报错;不得静默接受两种形式。
基于旧路径和旧符号的分支需要解决冲突。这是发布前移除旧词汇且不保留兼容别名的一次性成本
@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-13-session-content-search-opt-in.md
2026-08-13-session-content-search-opt-in.md: 2d14b2def00186c2957dc03ae712fbcb63d2c820
2026-08-13-session-content-search-opt-in.zh.md: dbde561c82cebdbf51ad31b1cd22c908c8d0f327
@@ -0,0 +1,32 @@
# Agent Note: Session content search ships opt-in through openAt never
Status: implemented
English | [中文](2026-08-13-session-content-search-opt-in.zh.md)
## Problem
The shipped bundles mounted the SQLite session-query provider with the full-text index live (`openAt: first-search`), so every default deployment carried a derived FTS index and the Web sidebar offered content search. Whether a deployment wants that index — its node:sqlite import, per-search source reconciliation, and derived storage — is a deployment choice, and the product default is to ship without it; the model-facing search tools were already opt-in and unmounted (the [not-shipped-default decision](../feature/2026-08-02-session-search-not-shipped-default.md)).
Turning the capability off by unmounting the plugin row is not viable. `ApiProxyService` declares `sessionQuery` as a required injection, so without the provider the whole host API gateway stays unloaded and the Web GUI never boots. Session-log export traces subagent descendants through `ctx.sessionQuery.traceSession`, and a subagent fork resolves its Workspace through the same lineage trace — both would need optional-service guards plus a replacement lineage source, roughly tripling the change surface while losing exact reads everywhere.
## Decision
Content search is enforced off at the provider. `openAt: 'never'` is a third opening phase on `@deepseek-ai/dsh-session-query-sqlite`: `searchSessions` and `searchEvents` fail with the typed `SESSION_QUERY_SEARCH_DISABLED` code before any request normalization, node:sqlite is never imported or opened, and no source observation or reconciliation runs. Every inherited `ctx.sessionQuery` exact read, filter, and trace keeps working, so session export, fork Workspace inheritance, and title reads are unaffected.
`SESSION_QUERY_SEARCH_DISABLED` joins the closed `SessionQueryErrorCode` taxonomy, and the `tool-session-query` service boundary maps it to the model-safe message `session search is disabled in this deployment`.
The base bundle sets `openAt: never` on the `session-query-sqlite` row and the web bundle's restatement keeps it; enabling content search is a one-line `openAt` override (`first-search` or `startup`) in a later patch layer, typically with a durable `path`. The host `session.search` endpoint reports the provider failure through its existing error path, and the Web sidebar keeps its designed degradation: local title/workspace matching plus the content-search-unavailable notice. The CLI compat spec pins the shipped `openAt: never` rows, while the web e2e scaffold keeps content search enabled — its seeded-session scenarios navigate by content search, and those runs are the assembled coverage for the opt-in path.
## Alternatives considered
- **Unmount the plugin row** (`disabled: true` in the base patch): rejected — the api-gateway's required `sessionQuery` injection keeps the whole host API unloaded, and making that injection optional forces guards plus a header-walk lineage fallback in session export and fork resolution.
- **Disable at the consumers** (the host `session.search` endpoint or the sidebar): rejected — enforcement belongs to the operation that makes the decision; opt-in model tools or any other consumer would still reach the index.
- **A separate boolean beside `openAt`**: rejected — the opening phase already owns when SQLite starts; `never` extends the same axis instead of adding a second knob that can contradict it.
## Consequences
- Default deployments run no derived index: no node:sqlite import or experimental-SQLite startup warning, no reconciliation work, no derived database on disk. Sidebar search matches session titles and workspace names only.
- Search failures under the default are typed and stable rather than incidental, so callers distinguish a deployment choice from an index fault (`SESSION_QUERY_INDEX_FAILED`).
- Re-enabling content search is per-deployment configuration, not a code change, and restores the full FTS behavior unchanged.
- Compositions that mount the search tools without overriding `openAt` get the model-safe disabled message on every search call; enabling the tools implies enabling the index.
@@ -0,0 +1,32 @@
# Agent Note: 会话内容搜索通过 openAt never 以 opt-in 方式交付
Status: implemented
[English](2026-08-13-session-content-search-opt-in.md) | 中文
## 问题
交付的 bundle 之前以启用状态挂载 SQLite 会话查询提供方的全文索引(`openAt: first-search`),因此每个默认部署都携带一个派生 FTS 索引,Web 侧边栏提供内容搜索。一个部署是否需要该索引——它的 node:sqlite 导入、每次搜索的来源对账和派生存储——是部署自身的选择,产品默认不携带它交付;面向模型的搜索工具此前已经是 opt-in 且未挂载(见[非默认交付决策](../feature/2026-08-02-session-search-not-shipped-default.md))。
通过卸载插件行来关闭该能力不可行。`ApiProxyService``sessionQuery` 声明为必需注入,没有该提供方时整个宿主 API 网关保持未加载,Web GUI 无法启动。会话日志导出通过 `ctx.sessionQuery.traceSession` 追踪子代理后代,子代理分叉也通过同一血缘追踪解析其 Workspace——两者都需要可选服务守卫加一个替代血缘来源,改动面大约扩大三倍,同时使精确读取在所有地方消失。
## 决策
内容搜索在提供方处强制关闭。`openAt: 'never'``@deepseek-ai/dsh-session-query-sqlite` 的第三个打开阶段:`searchSessions``searchEvents` 在任何请求规范化之前就以类型化的 `SESSION_QUERY_SEARCH_DISABLED` 代码失败,node:sqlite 绝不会被导入或打开,也不运行任何来源观察或对账。`ctx.sessionQuery` 上继承的全部精确读取、过滤和跟踪保持可用,因此会话导出、分叉的 Workspace 继承和标题读取不受影响。
`SESSION_QUERY_SEARCH_DISABLED` 加入封闭的 `SessionQueryErrorCode` 分类,`tool-session-query` 的服务边界将它映射为模型安全消息 `session search is disabled in this deployment`
base bundle 在 `session-query-sqlite` 行上设置 `openAt: never`,web bundle 的重述保持该值;启用内容搜索只需在后续 patch 层用一行覆盖 `openAt``first-search``startup`),通常同时配一个持久 `path`。宿主 `session.search` 端点沿现有错误路径报告提供方失败,Web 侧边栏保持其既有降级:本地标题/工作区匹配加内容搜索不可用提示。CLI 兼容性测试固定交付的 `openAt: never` 行,而 Web e2e 脚手架保持内容搜索启用——其种子会话场景通过内容搜索导航,这些运行也是 opt-in 路径的装配级覆盖。
## 曾考虑的替代方案
- **卸载插件行**(在 base patch 中 `disabled: true`)——否决:api-gateway 的必需 `sessionQuery` 注入会使整个宿主 API 保持未加载,而把该注入改为可选需要守卫加上会话导出与分叉解析中的 header 遍历血缘回退。
- **在消费方处关闭**(宿主 `session.search` 端点或侧边栏)——否决:强制应由做出决定的操作执行;opt-in 的模型工具或任何其他消费方仍会触达索引。
- **在 `openAt` 旁增加独立布尔开关**——否决:打开阶段已经拥有"SQLite 何时启动"这一轴;`never` 延伸同一根轴,而不是增加一个可能与之矛盾的第二个旋钮。
## 结果
- 默认部署不运行任何派生索引:没有 node:sqlite 导入或实验性 SQLite 启动警告,没有对账工作,磁盘上没有派生数据库。侧边栏搜索只匹配会话标题和工作区名称。
- 默认状态下的搜索失败是类型化且稳定的,调用方可以把部署选择与索引故障(`SESSION_QUERY_INDEX_FAILED`)区分开。
- 重新启用内容搜索是逐部署配置而非代码改动,并原样恢复完整的 FTS 行为。
- 挂载搜索工具但未覆盖 `openAt` 的组合,每次搜索调用都会得到模型安全的已禁用消息;启用工具意味着同时启用索引。
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-08-04-composer-tab-gutter-reservation.md
2026-08-04-composer-tab-gutter-reservation.md: 3b28c35c1f11676e41cabde76d1b0d16c688f034
2026-08-04-composer-tab-gutter-reservation.zh.md: c357dd06c52a834c18d2e8a25246d4ad003db548
2026-08-04-composer-tab-gutter-reservation.md: 8bd9fb2d86982d82b44a82c55b7303fcd9a5bf4d
2026-08-04-composer-tab-gutter-reservation.zh.md: 4b70aeb1d3777384907c345968971fcf75b3e74d
@@ -14,13 +14,11 @@ So for as long as the transcript overflowed — the ordinary state of any sessio
## Decision
`.scrollBody` declares `scrollbar-gutter: stable` unconditionally, and the overlay branch declares the same box a scroll container on both axes — `overflow-x: hidden; overflow-y: auto` — instead of `overflow: hidden`.
`.scrollBody` declares `scrollbar-gutter: stable` for the Chat state, and the overlay branch overrides it with `scrollbar-gutter: auto` while staying a scroll container on both axes — `overflow-x: hidden; overflow-y: auto`. The reservation is Chat's alone: it holds the seat's content box at the same width whether or not the transcript overflows, so the card never jumps as a growing transcript starts to scroll, nor between the hero phase and the first scrolling turn. The overlay branch reserves nothing — the view owns its own scrollers, so a gutter there would only narrow the view's content — and its seat compensates for the bar instead ([the seat-width compensation](2026-08-12-composer-overlay-seat-width-compensation.md)).
The two halves are one change. The reservation is what makes both states measure against the same width; declaring the overlay branch a scroll container is what makes the reservation reach it. `stable` rather than `auto` because `auto` reserves only while the box actually overflows, and the difference between overflowing and not is precisely the difference between the two tabs — an `auto` gutter would state the bug rather than fix it.
`stable` rather than `auto` because `auto` reserves only while the box actually overflows, and the difference between overflowing and not is precisely the difference between Chat's two phases — an `auto` gutter would state the bug rather than fix it.
The overlay state is a scroll container that nothing scrolls: the view fills it (`flex: 1 1 0` with its own clip) and the seat is out of flow, so no gesture and no clipping behavior changes. What changes is which declarations the engine honours. WebKit applies `scrollbar-gutter` to an `overflow-y: auto` box and ignores it on a hidden one — measured on this app's own composer layers and recorded in [the composer scrollport note](2026-07-31-composer-text-layers-share-one-scrollport.md) — so a reservation left on a hidden box would hold in Chromium and silently not in Safari.
The horizontal axis is declared rather than left to compute: a box that scrolls on one axis computes `visible` on the other to `auto`, and would grow a horizontal scrollbar of its own the first time a view's content reached past the column.
The reservation lives on an `overflow-y: auto` box, and that form is load-bearing: WebKit applies `scrollbar-gutter` to an `overflow-y: auto` box and ignores it on a hidden one — measured on this app's own composer layers and recorded in [the composer scrollport note](2026-07-31-composer-text-layers-share-one-scrollport.md) — so a reservation on a hidden box would hold in Chromium and silently not in Safari. The overlay branch keeps its `overflow-y: auto` form too, as a clipping box nothing scrolls out of: a single-axis scroller computes the other axis to `auto`, so the horizontal axis is declared `hidden` rather than left to compute, and would otherwise grow a horizontal scrollbar of its own the first time a view's content reached past the column.
The reservation is worth what it costs only because the bar takes layout space here at all, which is not the browser's default behavior but this client's: `::-webkit-scrollbar` carries a width in ui-theme's sheet ([themed scrollbars](2026-07-28-themed-scrollbars-and-reserved-gutter.md)), and the sidebar's session list already reserves its own gutter for the same reason.
@@ -37,7 +35,7 @@ The reservation is worth what it costs only because the bar takes layout space h
## Consequences
- Chat's content column is permanently 8px narrower — in the hero phase and while the transcript is short as well, where no bar is drawn. That is the trade: one card position at every content height, instead of the widest possible column.
- The fix covers three transitions with one declaration, because all three are the same difference: Chat ↔ Trajectory, short ↔ scrolling transcript within Chat, and hero ↔ first scrolling turn.
- The card holds one position across three transitions, by two mechanisms: the reservation keeps Chat's seat at one width across its own phases (short ↔ scrolling transcript, hero ↔ first scrolling turn), and the overlay seat's compensation matches it on the Chat ↔ Trajectory transition ([the seat-width compensation](2026-08-12-composer-overlay-seat-width-compensation.md)).
- The overlay state is now a scroll container. Nothing in it can overflow today; a future view that let its content exceed the column would scroll this box instead of clipping, and would need its own clip the way the Trajectory view already has one.
- The committed golden records the reserved band, so a change to the sheet's `::-webkit-scrollbar` width — the value that decides how wide the reservation is — arrives as a reviewable diff in this scenario as well as in the sidebar's.
@@ -45,6 +43,6 @@ The reservation is worth what it costs only because the bar takes layout space h
`apps/web/tests/composer-tab-geometry.e2e.ts` measures the input card's rectangle in both tabs, at a viewport where the card sits at its width cap and one where it shrinks with the column, and asserts the two rectangles are the same rectangle. Only a real engine reports this: jsdom gives every element a zero-sized box and no scrollbar, so a unit spec could assert the declarations exist but not that the two states land in the same place. For the same reason no CSS-text spec accompanies it — it would restate the declarations without adding a fact the browser lane does not already establish.
The scenario launches chromium without Playwright's default `--hide-scrollbars`, which is load-bearing: under that argument a bar consumes no layout width, both tabs agree before this change as much as after it, and every comparison in the file holds vacuously. Measured, the pre-fix cascade leaves both bands at 0 under the argument, and at 8 and 0 with it dropped.
The scenario launches chromium without Playwright's default `--hide-scrollbars`, which is load-bearing: under that argument a bar consumes no layout width, so the tabs agree with and without the compensation and every comparison in the file holds vacuously. Measured, both bands sit at 0 under the argument and at 8 and 0 with it dropped.
The pre-fix cascade is then applied in the page — `scrollbar-gutter: auto` on the scroller, `overflow: hidden` on the overlay branch — and the same two tabs measured through it, which is what separates a card that does not move from a tab switch that never reached the layout. It reproduces the reported symptom as a number: 4px on each edge, half the 8px band. The golden records that control beside the fixed state, so the fixture carries the difference the change removes rather than only its absence.
The uncompensated cascade is then applied in the page — the overlay seat's `right` compensation dropped to 0 via `!important`, Chat's reservation untouched — and the same two tabs measured through it, which is what separates a card that does not move from a tab switch that never reached the layout. It reproduces the reported symptom as a number: 4px on each edge, half the 8px band. The golden records that control beside the fixed state, so the fixture carries the difference the change removes rather than only its absence.
@@ -14,13 +14,11 @@ composer 座位在组件树中只有一个节点、一个位置,但它究竟
## 决策
`.scrollBody` 无条件声明 `scrollbar-gutter: stable`overlay 分支则把同一个盒子在两个轴向上都声明为滚动容器——`overflow-x: hidden; overflow-y: auto`——而不再是 `overflow: hidden`
`.scrollBody` 为 Chat 状态声明 `scrollbar-gutter: stable`覆盖分支则将其覆盖为 `scrollbar-gutter: auto`,同时保持为双轴滚动容器——`overflow-x: hidden; overflow-y: auto`。这条预留只属于 Chat:它让座位的内容盒在 transcript 是否溢出时都保持同一宽度,因此卡片不会在 transcript 增长到开始滚动的那一刻跳动,也不会在 hero 态与第一个可滚动轮次之间跳动。覆盖分支不预留任何槽位——视图自己滚动,槽位只会白白收窄视图内容——它的座位改为补偿滚动条宽度([座位宽度补偿](2026-08-12-composer-overlay-seat-width-compensation.md)
这两半是同一处改动。预留使两种状态依附于同一个宽度;把 overlay 分支声明为滚动容器,才使这条预留真正抵达它。`stable` 而非 `auto`,是因为 `auto` 只在盒子确实溢出时才预留,而「溢出与否」恰恰就是两个标签页之间的那点差别——`auto` 的写法只是把缺陷重述一遍,并不能修掉它。
`stable` 而非 `auto`,是因为 `auto` 只在盒子确实溢出时才预留,而「溢出与否」恰恰就是 Chat 两种相位之间的那点差别——`auto` 的写法只是把缺陷重述一遍,并不能修掉它。
overlay 状态是一个没有任何东西会去滚动它的滚动容器:视图把它填满(`flex: 1 1 0`,且自带裁剪),座位不在常规流中,因此没有任何手势与裁剪行为发生变化。变化的是引擎会认哪些声明。WebKit 对 `overflow-y: auto` 的盒子应用 `scrollbar-gutter`,对 hidden 的盒子则忽略它——这是在本应用 composer 自身的图层上实测所得,并记录于 [composer 滚动视口记录](2026-07-31-composer-text-layers-share-one-scrollport.md)——所以把预留留在一个 hidden 盒子上,会在 Chromium 上成立,在 Safari 上悄无声息地不成立。
横向轴是显式声明的,而不是交给推导:单轴滚动的盒子会把另一轴的 `visible` 计算为 `auto`,于是只要某个视图的内容第一次伸出列外,它就会长出自己的横向滚动条。
这条预留位于 `overflow-y: auto` 的盒子上,而这个形式是承重的:WebKit 对 `overflow-y: auto` 的盒子应用 `scrollbar-gutter`,对 hidden 的盒子则忽略它——这是在本应用 composer 自身的图层上实测所得,并记录于 [composer 滚动视口记录](2026-07-31-composer-text-layers-share-one-scrollport.md)——所以把预留放在 hidden 盒子上,会在 Chromium 上成立,在 Safari 上悄无声息地不成立。覆盖分支同样保留 `overflow-y: auto` 的形式,作为没有任何内容会滚出去的裁剪盒:单轴滚动的盒子会把另一轴的 `visible` 计算为 `auto`,因此横向轴显式声明为 `hidden` 而不是交给推导,否则某个视图的内容第一次伸出列外时,它就会长出自己的横向滚动条。
这条预留之所以值回它的代价,前提是滚动条在这里确实占布局空间——这并非浏览器的默认行为,而是本客户端的选择:ui-theme 的样式表给 `::-webkit-scrollbar` 声明了宽度([滚动条主题化](2026-07-28-themed-scrollbars-and-reserved-gutter.md)),侧边栏的会话列表也正是出于同一原因预留了自己的滚动条槽。
@@ -37,7 +35,7 @@ overlay 状态是一个没有任何东西会去滚动它的滚动容器:视图
## 后果
- Chat 的内容列永久变窄 8px——hero 态与 transcript 尚短、根本不绘制滚动条时同样如此。这就是这笔交易:以最宽的列换取卡片在任何内容高度下都只有一个位置。
- 一条声明覆盖三种切换,因为这三者本就是同一个差异:Chat ↔ Trajectory、Chat 内部 transcript 较短 ↔ transcript 可滚动,以及 hero ↔ 第一个可滚动轮次
- 卡片在三种切换下保持同一位置,由两种机制达成:预留让 Chat 的座位在自身各相位间保持同一宽度(transcript 较短 ↔ 可滚动、hero ↔ 第一个可滚动轮次),Chat ↔ Trajectory 的切换则由覆盖座位的补偿来对齐([座位宽度补偿](2026-08-12-composer-overlay-seat-width-compensation.md)
- overlay 状态现在是一个滚动容器。今天其中没有任何内容会溢出;将来若有视图允许自身内容超出会话列,这个盒子会滚动而不是裁剪,那个视图就需要像 Trajectory 视图那样自带裁剪。
- 提交的 golden 记录了预留条带,因此样式表中 `::-webkit-scrollbar` 宽度的变化——决定这条预留有多宽的那个值——会在本场景中与在侧边栏场景中一样,以可评审的 diff 形式出现。
@@ -45,6 +43,6 @@ overlay 状态是一个没有任何东西会去滚动它的滚动容器:视图
`apps/web/tests/composer-tab-geometry.e2e.ts` 在两个标签页下测量输入卡片的矩形,分别取卡片处于宽度上限的视口与卡片随列收缩的视口,并断言这两个矩形是同一个矩形。只有真实引擎能报告这件事:jsdom 给每个元素的盒子尺寸都是零,也没有滚动条,因此单元测试只能断言那些声明存在,无法断言两种状态落在同一位置。出于同一原因,本次没有附带读取 CSS 文本的单元测试——它只会把声明复述一遍,并不会补上浏览器车道尚未确立的事实。
该场景启动 chromium 时去掉了 Playwright 默认的 `--hide-scrollbars`,这一点是承重的:带上该参数时滚动条不占任何布局宽度,两个标签页在改动前后同样一致,文件中的每一处比较都会空洞地通过。实测:带上该参数时,改动前的层叠让两条预留带的宽度都是 0;去掉它则分别是 8 与 0。
该场景启动 chromium 时去掉了 Playwright 默认的 `--hide-scrollbars`,这一点是承重的:带上该参数时滚动条不占任何布局宽度,因此两个标签页在有补偿与无补偿时同样一致,文件中的每一处比较都会空洞地通过。实测:带上该参数时两条预留带的宽度都是 0;去掉它则分别是 8 与 0。
随后,改动前的层叠会被注入页面——滚动容器上 `scrollbar-gutter: auto`overlay 分支上 `overflow: hidden`——并在其下测量同样的两个标签页,这正是把「卡片确实没动」与「标签页切换根本没到达布局」区分开的那一步。它把上报的症状复现为一个数字:每条边 4px,恰是 8px 带宽的一半。golden 把这份对照与修复后的状态并排记录,因此 fixture(测试前置数据)承载的是这次改动所消除的那个差值,而不仅仅是它的缺席。
随后,未补偿的级联会被注入页面——通过 `!important` 把覆盖座位的 `right` 补偿降为 0,Chat 的预留保持不变——并在其下测量同样的两个标签页,这正是把「卡片确实没动」与「标签页切换根本没到达布局」区分开的那一步。它把上报的症状复现为一个数字:每条边 4px,恰是 8px 带宽的一半。golden 把这份对照与修复后的状态并排记录,因此 fixture(测试前置数据)承载的是这次改动所消除的那个差值,而不仅仅是它的缺席。
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-08-11-bounded-background-job-admission.md
2026-08-11-bounded-background-job-admission.md: 8fa5bec07647947b730c436284da71b83deedb48
2026-08-11-bounded-background-job-admission.zh.md: d827e07aacc7bc933474577214d94bab9fe5f5c3
2026-08-11-bounded-background-job-admission.zh.md: 79e759041cbf9d6e12b444f47e9d9ef0aa218532
@@ -12,7 +12,7 @@ Status: implemented
## 决策
`LocalJobRegistry` 拥有 `maxConcurrentJobsPerOwner` 配置字段。它只接受正的安全整数,默认值为 `10`,并通过 Service provider 的 Cordis schema、typed `agent-spine-demo` 组合包与 ACP 应用配置提供。组合包只传输该值;其含义归进程内 Service provider 所有。
`LocalJobRegistry` 拥有 `maxConcurrentJobsPerOwner` 配置字段。它只接受正的安全整数,默认值为 `10`,并通过 Service Provider 的 Cordis schema、typed `agent-spine-demo` 组合包与 ACP 应用配置提供。组合包只传输该值;其含义归进程内 Service Provider 所有。
[通用任务运行时决策](../architecture/2026-06-20-generic-long-running-tool-runtime.md)拥有共享 Task 生命周期与控制 API;本记录只拥有进程内准入策略。
@@ -34,7 +34,7 @@ owner 与服务释放保留现有顺序:请求取消,在生产方释放资
## 验证
任务 Service provider 测试覆盖默认与显式上限、生产方执行前拒绝、id 计数器不变、`stopping` 占位、每种终态释放、确切 owner 隔离、同会话替代对象、共享无 owner 桶、非法配置、owner 清理和服务拆除。spine 与 ACP 组合测试固定 typed 转发。一条 keyless ACP 回放以 1 为上限启动真实 Loader 组合,启动一个真实后台 Bash 进程,观察第二次启动返回可操作错误,按返回的 job id 停止第一个任务,并验证被拒绝生产方的标记文件从未生成。
任务 Service Provider 测试覆盖默认与显式上限、生产方执行前拒绝、id 计数器不变、`stopping` 占位、每种终态释放、确切 owner 隔离、同会话替代对象、共享无 owner 桶、非法配置、owner 清理和服务拆除。spine 与 ACP 组合测试固定 typed 转发。一条 keyless ACP 回放以 1 为上限启动真实 Loader 组合,启动一个真实后台 Bash 进程,观察第二次启动返回可操作错误,按返回的 job id 停止第一个任务,并验证被拒绝生产方的标记文件从未生成。
## 曾考虑的替代方案
@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-08-12-composer-overlay-seat-width-compensation.md
2026-08-12-composer-overlay-seat-width-compensation.md: 0ec4d1272ac1adab5b724dccf44f567e15cc3368
2026-08-12-composer-overlay-seat-width-compensation.zh.md: 2f66771240fe62e15c481342d55a82dd09cfb864
@@ -0,0 +1,39 @@
# Agent Note: The overlay composer seat compensates for the bar instead of reserving a gutter
Status: implemented
English | [中文](2026-08-12-composer-overlay-seat-width-compensation.zh.md)
## Problem
The [composer-tab gutter reservation](2026-08-04-composer-tab-gutter-reservation.md) made the column's scroller reserve a scrollbar gutter unconditionally, so the composer seat measured the same width in Chat and in a view with a composer overlay. The cost was paid by every overlay view: the view's content column ended 8px short of the column's right edge, because the scroller reserved a gutter for a bar it never draws — the trajectory ledger owns its own scrollers and the outer box never scrolls.
The trajectory table made that cost visible: its full-width row divider lines stopped 8px short of the pane edge, leaving a strip of whitespace at the right of every line and of the whole content column.
## Decision
The reservation now belongs to Chat alone. The overlay branch declares `scrollbar-gutter: auto`, so the view's content spans the full column; the overlay composer seat (absolutely positioned against the padding box) gives back the bar's width with `right: var(--dsh-scrollbar-width)`, so the input card still measures the same width as Chat's seat and does not move between tabs.
The compensation value is not a literal: ui-theme's scrollbar.css defines `--dsh-scrollbar-width` (8px on the WebKit path) beside the `::-webkit-scrollbar` rule it mirrors, and the seat reads that variable. The scrollbar-styles spec pairs the variable with the mirrored rule and with the compensation consumer, so a change to the sheet's bar width without the variable — or to the variable without the consumer — fails the gate, not just review.
## Alternatives considered
**Keep the unconditional reservation and shrink every overlay view.** The pre-fix behavior. It keeps one declaration for both tabs but taxes every overlay view with an 8px content column, which the trajectory ledger surfaced as visible whitespace. Rejected because the overlay views own their scrolling; they should not pay for Chat's bar.
**Reserve on the overlay branch too and let the view bleed into the gutter.** More moving parts for the same result: the gutter would still exist on a box that never scrolls, and the view would have to break out of the content box to reclaim its width.
**Accept the 4px card shift.** Dropping the reservation without compensating the seat would move the input card on every tab switch, which is exactly the symptom the earlier note fixed. Rejected: the card position is a deliberate cross-tab invariant.
**Inset the overlay seat by the bar's width.** The [gutter-reservation note](2026-08-04-composer-tab-gutter-reservation.md) rejected exactly this, and this note adopts it; what changed is the rejection's premise. The number was the engine's, not ours — the WebKit path draws the sheet's 8px bar while the Firefox path draws whatever `scrollbar-width: thin` resolves to — so a hardcoded inset would line the two states up in Chromium and drift elsewhere. The overlay branch reserved an engine-resolved gutter of its own back then, so an inset had to match that width exactly. Today the overlay branch reserves nothing, so the compensation is the overlay side's only mechanism, and the literal half of the rejection is answered by making the 8px a variable that mirrors the `::-webkit-scrollbar` rule in the same diff. The Firefox half remains: Chat reserves the engine-resolved width while the compensation stays fixed, and the residual drift where the two differ is recorded as an accepted cost in Consequences.
## Consequences
- Chat keeps its reserved gutter and its stable card position; nothing changes on that tab.
- Overlay views (trajectory) span the full column; the trajectory ledger's divider lines reach the pane edge.
- The input card still holds one horizontal position across the Chat and Trajectory tabs, now by two mechanisms instead of one: Chat reserves, the overlay seat compensates.
- Chat reserves the engine-resolved width while the overlay seat compensates a fixed 8px. Where the two differ — the Firefox path resolves `scrollbar-width: thin` per platform, and the e2e runs only on Chromium — the card drifts by half the difference on tab switch. Accepted residual cost, recorded here rather than asserted away: no measurement of the Firefox thin width on the target platforms exists in this change.
- `--dsh-scrollbar-width` becomes a public ui-theme variable read outside ui-theme; the scrollbar-styles spec pairs it with the mirrored `::-webkit-scrollbar` width rule and with the compensation consumer, closing the indirection-gate gap the variable would otherwise leave.
## Testing
`apps/web/tests/composer-tab-geometry.e2e.ts` still asserts the card holds its position across tabs and now also asserts the split: Chat's scroller keeps `scrollbar-gutter: stable` and a nonzero band, while the overlay branch resolves `auto` with a zero band. The control cascade changed with the mechanism: it now drops the seat's `right` compensation (instead of dropping a gutter Chat never had on that branch) and measures the same 4px shift, proving the equal rectangles are not a tab switch that never reached layout. The committed golden records both states.
@@ -0,0 +1,39 @@
# Agent Note: 覆盖视图的 composer 座位改为补偿滚动条宽度,不再预留滚动条槽
Status: implemented
[English](2026-08-12-composer-overlay-seat-width-compensation.md) | 中文
## 问题
[composer 标签页滚动条槽预留](2026-08-04-composer-tab-gutter-reservation.md) 让会话列滚动容器无条件预留一条滚动条槽,使 composer 座位在 Chat 与带 composer 覆盖的视图中测得相同宽度。代价由每个覆盖视图承担:视图内容列比列右边缘窄 8px,因为滚动容器为一条它从不绘制的滚动条预留了槽——trajectory 台账由视图内部自己的滚动容器滚动,外层盒子从不滚动。
trajectory 表格让这个代价显形:整行分隔线在面板右边缘前 8px 处停止,每条线右侧以及整个内容列右侧都留下一条空白带。
## 决策
预留现在只属于 Chat。覆盖分支声明 `scrollbar-gutter: auto`,视图内容占满整列;覆盖分支的 composer 座位(相对 padding box 绝对定位)用 `right: var(--dsh-scrollbar-width)` 让出滚动条宽度,使输入卡仍与 Chat 座位测得相同宽度,切换标签页时不移动。
补偿值不是字面量:ui-theme 的 scrollbar.css 在它镜像的 `::-webkit-scrollbar` 规则旁定义 `--dsh-scrollbar-width`(WebKit 路径 8px),座位读取该变量。scrollbar-styles 规格把该变量与其镜像规则、以及补偿消费者配对检查,因此样式表滚动条宽度一变却不同步变量——或变量一变却不同步消费者——都会让门禁失败,而不只是评审时发现。
## 备选方案
**保留无条件预留,压缩每个覆盖视图。** 修复前行为。两个标签页一条声明,但每个覆盖视图都要付出 8px 内容列,trajectory 台账将其显现为可见空白。已拒绝:覆盖视图自己滚动,不应为 Chat 的滚动条买单。
**覆盖分支也预留,并让视图渗入滚动条槽。** 同样结果下更多活动部件:从不滚动的盒子上仍存在滚动条槽,视图还得突破内容盒才能取回宽度。
**接受 4px 卡片位移。** 去掉预留却不补偿座位,会在每次切换标签页时移动输入卡——正是前一份 note 修复的症状。已拒绝:卡片位置是刻意保持的跨标签页不变量。
**把 overlay 座位按滚动条宽度内缩。** [滚动条槽预留 note](2026-08-04-composer-tab-gutter-reservation.md) 当初否决的正是这个方案,本 note 采纳了它;变的是否决的前提。这个数字属于引擎而不属于我们——WebKit 路径绘制样式表里的 8px 滚动条,Firefox 路径绘制 `scrollbar-width: thin` 解析出的宽度——因此硬编码的内缩会让两种状态在 Chromium 上对齐、在别处继续漂移。当初 overlay 分支自己预留的是引擎解析出的槽宽,内缩必须精确匹配那个宽度。如今 overlay 分支不预留任何槽位,补偿成为覆盖侧唯一的机制;否决的字面量那一半,通过把 8px 变成与 `::-webkit-scrollbar` 规则同处一个 diff 的变量来回应。Firefox 那一半仍然存在:Chat 预留引擎解析宽度,补偿保持固定 8px,两者不等之处的残余漂移作为接受的代价记录在后果中。
## 后果
- Chat 保留滚动条槽与稳定的卡片位置;该标签页无任何变化。
- 覆盖视图(trajectory)占满整列;trajectory 台账的分隔线到达面板右边缘。
- 输入卡在 Chat 与 Trajectory 标签页间仍保持同一水平位置,现在由两种机制而非一种达成:Chat 预留,覆盖座位补偿。
- Chat 预留引擎解析宽度,覆盖座位补偿固定的 8px。两者不等之处——Firefox 路径按平台解析 `scrollbar-width: thin`,而 e2e 只在 Chromium 上运行——卡片在切换标签页时会漂移半个差值。这是接受的残余代价,如实记录于此而不断言消除:本次改动并未提供目标平台 Firefox thin 宽度的实测。
- `--dsh-scrollbar-width` 成为 ui-theme 对外、且被 ui-theme 之外读取的变量;scrollbar-styles 规格把它与镜像的 `::-webkit-scrollbar` 宽度规则、以及补偿消费者配对检查,补上了该变量本会留下的间接层门禁缺口。
## 测试
`apps/web/tests/composer-tab-geometry.e2e.ts` 仍断言输入卡在标签页间保持位置,并新增断言拆分:Chat 滚动容器保持 `scrollbar-gutter: stable` 与非零槽宽,覆盖分支解析为 `auto` 且槽宽为零。控制级联随机制改变:现在移除座位的 `right` 补偿(而非移除该分支上 Chat 从未有过的槽),测得同样的 4px 位移,证明相等的矩形并非从未到达布局的标签页切换。提交的 golden 记录两种状态。
@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-08-13-bounded-cold-blank-verification.md
2026-08-13-bounded-cold-blank-verification.md: bf8d167d742001ce65b3e96713a9603adb19603e
2026-08-13-bounded-cold-blank-verification.zh.md: 7cfef77a02308a8e75281877f8a774b41bb9559d
@@ -0,0 +1,37 @@
# Agent Note: Bound cold blank-session verification
Status: implemented
English | [中文](2026-08-13-bounded-cold-blank-verification.zh.md)
## Problem
The Web session tree hides blank Sessions and reuses the selected blank entry as New Session. Attached Sessions can derive blankness from their in-memory event log, but `session.list` normally avoids loading every cold log. Treating every materialized cold Session as non-blank exposes empty Sessions left by older versions. Treating a projection-cache `blank: true` as current can instead hide a real conversation after the log advances and the fail-soft cache remains stale.
The same cold list used the JSONL artifact mtime for `updatedAt`. Opening a Session appends `session/end-seed`, so a pickup with no human prompt refreshed mtime and promoted that Session above recently used conversations.
## Decision
`dsh-host-apiproxy` registers `sessionListMetadata`, a projection containing `blank` and `lastPromptAt`. The attached summary folds the same functions directly over the live log. `blank` changes only from true to false on `turn/start`; `lastPromptAt` changes only on a `user/message` whose source kind is `user`.
A cold summary trusts cached `blank: false`, because a checkpoint prefix containing `turn/start` remains non-blank. Cached `blank: true` and a cache miss do not prove the current log is blank. When persistence exposes a physical artifact through `locate()` and its observed size is at most the `coldBlankProbeMaxBytes` eligibility threshold (default 1 KiB per Session), the gateway calls `readFrom(id, 0)` and folds exact list metadata from the stored prefix. Files above the threshold, backends without a location, vanished artifacts, and failed reads all produce `blank: false`, keeping the Session visible.
`updatedAt` is the later of `createdAt` and `lastPromptAt`. An eligible artifact read supplies exact `lastPromptAt` at no additional I/O cost; other cache misses or stale checkpoints order the Session too old rather than promoting it from an unrelated file write. After each asynchronous cold read, the gateway checks the live store again and replaces the cold result with an attached summary when another request resumed that Session meanwhile.
## Alternatives considered
**Trust cached `blank: true`.** Rejected because the projection cache deliberately permits a persisted log to advance beyond its checkpoint. A crash or fail-soft write failure after the first `turn/start` would hide a real conversation and could make the client reuse it as New Session.
**Read every cold log.** Rejected because list latency and I/O would scale with total stored conversation bytes. The physical-size eligibility check targets small historical artifacts that can be checked cheaply and degrades larger unknowns toward visibility. It intentionally does not add a persistence operation solely to make the threshold atomic with the read: concurrent growth may increase one probe's read cost, but the additional events can only preserve visibility or change a blank result to non-blank.
**Store blankness and recency in an authoritative persistence index.** Deferred because JSONL has an immutable first line and would require a second durable artifact with ordered updates, while SQLite would require a schema field. The broader exact-index design remains in the [last-activity proposal](../../proposed/architecture/2026-07-29-durable-last-activity-index.md).
**Continue ordering JSONL by mtime.** Rejected because mtime records every artifact write, including pickup boundaries, rather than the latest human prompt. Its error direction promotes untouched Sessions to the front.
## Consequences
Existing small blank JSONL artifacts are hidden without depending on projection-cache availability, and a stale cache cannot hide a stored `turn/start`. A cold list may read each artifact whose observed physical size is within the configured threshold when its cache does not already prove non-blank. The default threshold compares compressed bytes for the shipped Zstandard JSONL backend.
Blank artifacts above the threshold and blank Sessions on location-less backends remain visible. Missing or delayed recency cache entries for artifacts that are not read fall back to `createdAt`. These are conservative degradations: the UI may show an extra empty row or order a Session too low, but it does not hide a conversation or promote one because it was merely opened.
The gateway-owned projection is an effect of the gateway fiber; unloading the gateway removes the key. Unit coverage pins exact-threshold eligibility, stale-true rejection, monotonic false reuse, exact small-log recency, live-attachment races, fallback direction, human-prompt recency, and fiber disposal. A keyless Web snapshot boots the shipped compressed JSONL composition, seeds a small cold blank artifact without a cache row, and verifies that the sidebar omits it.
@@ -0,0 +1,37 @@
# Agent Note: 有界验证冷空白会话
Status: implemented
[English](2026-08-13-bounded-cold-blank-verification.md) | 中文
## Problem
Web 会话树会隐藏空白 Session,并把当前选中的空白项复用为 New Session。已附加 Session 可以从内存事件日志派生空白状态,但 `session.list` 通常不会加载每一份冷日志。把所有已物化的冷 Session 都视为非空,会暴露旧版本留下的空 Session;反过来,把 projection cache 中的 `blank: true` 当成当前事实,则可能在日志已经前进而 fail-soft cache 仍然陈旧时隐藏真实对话。
同一份冷列表还曾用 JSONL 工件的 mtime 作为 `updatedAt`。打开 Session 会追加 `session/end-seed`,因此即使没有真人 prompt,单纯拾起也会刷新 mtime,并把该 Session 提升到最近使用的对话之前。
## Decision
`dsh-host-apiproxy` 注册 `sessionListMetadata` 投影,其中包含 `blank``lastPromptAt`。已附加摘要直接用同一组函数折叠实时日志。`blank` 只在 `turn/start` 时从 true 单调变为 false`lastPromptAt` 只在来源 kind 为 `user``user/message` 上更新。
冷摘要信任缓存的 `blank: false`,因为已包含 `turn/start` 的 checkpoint 前缀会始终保持非空。缓存的 `blank: true` 和 cache miss 都无法证明当前日志为空。当 persistence 通过 `locate()` 暴露物理工件,且其观测大小不超过 `coldBlankProbeMaxBytes` 资格阈值(默认每个 Session 1 KiB)时,网关调用 `readFrom(id, 0)`,从已存前缀折叠精确列表元数据。超过阈值的文件、不提供位置的后端、已消失的工件和读取失败都产生 `blank: false`,让 Session 保持可见。
`updatedAt``createdAt``lastPromptAt` 中较晚者。符合资格的工件读取无需额外 I/O 即可提供精确 `lastPromptAt`;其他 cache miss 或陈旧 checkpoint 只会让 Session 排得偏旧,而不会因无关的文件写入被提升。每次异步冷读取后,网关都会再次检查实时 store;若另一请求期间已恢复该 Session,则用已附加摘要替换冷结果。
## Alternatives considered
**信任缓存的 `blank: true`。** 拒绝,因为 projection cache 有意允许持久日志前进到 checkpoint 之后。首个 `turn/start` 之后若发生崩溃或 fail-soft 写入失败,真实对话就会被隐藏,客户端还可能把它复用为 New Session。
**读取每一份冷日志。** 拒绝,因为列表延迟与 I/O 会随所有已存对话的总字节数增长。物理大小资格检查只针对能够低成本核验的小型历史工件,更大的未知项则向保持可见降级。该检查有意不为“让阈值与读取原子化”单独新增 persistence 操作:并发增长可能增加一次探测的读取成本,但新增事件只会保持可见,或把空白结果改为非空。
**把空白状态与最近时间存入权威 persistence index。** 暂缓,因为 JSONL 的首行不可变,需要增加带有顺序写入要求的第二份持久工件;SQLite 则需要 schema 字段。更广泛的精确索引设计仍由[最后活动提案](../../proposed/architecture/2026-07-29-durable-last-activity-index.md)负责。
**继续按 mtime 排序 JSONL。** 拒绝,因为 mtime 记录包括拾起边界在内的每一次工件写入,而非最近真人 prompt;其错误方向会把未经操作的 Session 提升到列表开头。
## Consequences
既有的小型空白 JSONL 工件无需依赖 projection cache 是否存在即可被隐藏,陈旧 cache 也无法隐藏已存的 `turn/start`。对于 cache 尚不能证明非空,且观测物理大小在配置阈值内的每个 Session,冷列表可能读取其工件。对默认交付的 Zstandard JSONL 后端,该阈值比较压缩后的字节数。
超过阈值的空白工件,以及来自不提供位置的后端的空白 Session 会保持可见。对于未被读取的工件,缺失或延迟的最近时间 cache 会回退到 `createdAt`。这些都是保守降级:UI 可能多显示一条空记录,或把 Session 排得偏低,但不会隐藏真实对话,也不会因为单纯打开而把会话提升到前面。
网关自有投影是网关 fiber 的 effect;卸载网关会移除该 key。单元覆盖固定了临界大小资格、拒绝陈旧 true、复用单调 false、小日志精确最近时间、实时附加竞态、回退方向、真人 prompt 最近时间和 fiber 销毁。无密钥 Web snapshot 会启动发行版的压缩 JSONL 组合,在没有 cache row 的情况下播种一份小型冷空白工件,并验证侧栏不展示它。
@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-08-13-oauth-only-providers-withheld.md
2026-08-13-oauth-only-providers-withheld.md: 8c85c2900fd0af7c97a3ac8f04d3c6a2f469f8b0
2026-08-13-oauth-only-providers-withheld.zh.md: 8f05d096a5d474407585bcbd84bd47da8a1abb2e
@@ -0,0 +1,43 @@
# Agent Note: The configurable-provider directory withholds OAuth-only providers
Status: implemented
English | [中文](2026-08-13-oauth-only-providers-withheld.zh.md)
## Problem
The Models page offered `openai-codex` like any other pi-ai route, with the placeholder every pi-ai provider carries: enter a key, or leave it blank to authenticate from the environment. Configuring it that way and sending a message failed the turn with `Provider is not configured: openai-codex`, reported as the adapter's catch-all `PI_AI_ERROR`.
The posture the placeholder invited could not work on that route. pi-ai's `resolveProviderAuth` reaches an OAuth provider through one path — a credential already in the collection's `CredentialStore` — and has no ambient fallback for it, while `openai-codex` is the one installed provider declaring `auth.oauth` with no `auth.apiKey` beside it. `PiAiAdapter.current()` constructs its collection with `createModels()` and no options, so the store is pi-ai's default `InMemoryCredentialStore`: empty at every boot, and rebuilt from scratch each time a configuration change produces a new snapshot. No code here runs `Models.login()`, and pi-ai's library half does not read Codex's own `~/.codex/auth.json` either — its OAuth module is a PKCE login flow whose credential the *host* application persists, which is what the pi CLI supplies and this adapter does not.
So the page advertised, with the keyless posture its own placeholder describes, a provider that has no keyless posture — and the failure named the configuration key rather than the missing capability. The one thing that does authenticate the route is a ChatGPT OAuth token pasted into the key field, which is not what the offer describes and expires with nothing here to refresh it.
## Decision
The directory offers only what this adapter can authenticate. `catalogProviderTakesApiKey(provider)` answers whether pi-ai's installed provider for a route declares an api-key method — the one method the harness can feed, since it resolves a key through its own credential seam and hands it over as the request's `apiKey` override — and `directoryEntries()` skips the catalog routes that fail it.
OAuth support is not attempted. It needs a persistent credential store, a login flow, and a surface to run it from; none of those is a release-blocking fix, and shipping the offer without them is what produced the report.
Two boundaries keep the withholding narrow:
- **Catalog membership is unchanged.** `catalogProviderIds()` still answers what pi-ai ships, so the `declared` flag on a directory entry keeps meaning "no installed provider answers for this route" rather than "this route is not offered".
- **The profile half of the union is unconditional.** A route a settings document already names keeps its entry, so a stored `openai-codex` profile stays visible, editable, and deletable instead of being stranded in the document with nothing on the page to remove it.
Resolution is untouched. A profile naming `apiKeyEnv` on an OAuth-only route still builds a working provider — `routeAuth` adds the harness api-key method beside the catalog's OAuth, and pi-ai's Codex API derives the account id from the token itself — so a deployment that writes one into `settings.yaml` or `cordis.yml` keeps that path. Enforcing the withholding in `resolveProfiles` instead would have refused such a profile at registration, and because `validate` runs at boot as well as at write time, a document already naming a keyless OAuth route would fail the whole namespace's registration rather than one provider.
## Alternatives considered
- **Rejecting a keyless OAuth-only route in `resolveProfiles`.** This is where the repo normally enforces a decision, and the directory filter is a surface that a `cordis.yml` entry bypasses. It was refused for the boot behavior above: an existing stored profile would take down every other route in the namespace with it, which for a release trades a one-provider defect for a total one. The gap is that the offer, not the capability, is what got fixed — a deployment can still hand-write the route it can no longer add from the page.
- **Keeping the offer and correcting only the placeholder text.** The field would then have to say the provider needs a login this build cannot run, which is a card whose only honest content is that it does not work.
- **Mapping `Provider is not configured` to a named `LlmError`.** Worth doing, and reachable for reasons this change does not remove — any api-key route left blank whose provider finds nothing in the process environment produces the same message. Deferred as a separate change: it improves a diagnostic rather than removing a broken offer.
- **Reading `~/.codex/auth.json` into a pi-ai `CredentialStore`.** It makes Codex work without a login flow, and pi-ai owns the refresh. It also binds the harness to another tool's private file format for one provider, which is a decision for the OAuth work rather than a release fix.
## Consequences
`openai-codex` disappears from the provider picker and from the directory the Models page joins; every other installed provider is unaffected, including the six that offer OAuth *beside* an api-key method (`anthropic`, `github-copilot`, `kimi-coding`, `openrouter`, `radius`, `xai`), which keep their entries and their key path. A future provider that ships OAuth alone is withheld automatically rather than by name.
Two adjacent gaps remain and are recorded in the package README: a route naming no credential still resolves through the catalog provider's own discovery, which reads process environment variables only — not `~/.aws/credentials`, and not the harness credential seam — and the resulting failure is still the catch-all `PI_AI_ERROR`.
## Testing
Package tests pin both halves of the union: the withheld route is absent from `listConfigurableProviders()` while `anthropic` and `openai` stay, and a stored `openai-codex` profile still produces a full entry with `declared: false`. The existing resolution tests are unchanged and still pass, which is what shows the withholding did not narrow what a hand-written profile can serve. The `models-settings` and `onboarding-usable-provider` web e2e goldens lost exactly the `openai-codex` option line, recorded against the real assembled application.
@@ -0,0 +1,43 @@
# Agent Note:可配置提供方目录不再提供仅以 OAuth 认证的提供方
Status: implemented
[English](2026-08-13-oauth-only-providers-withheld.md) | 中文
## 问题
模型设置页把 `openai-codex` 当作普通 pi-ai 路由提供出来,配的还是每个 pi-ai 提供方共用的那句占位文案:填入 API 密钥,或留空使用环境认证。照此配置后发送消息,本轮以 `Provider is not configured: openai-codex` 失败,并被适配器归入兜底的 `PI_AI_ERROR`
占位文案所邀请的那种配置姿态在这条路由上不可能工作。pi-ai 的 `resolveProviderAuth` 抵达 OAuth 提供方只有一条路径——集合的 `CredentialStore` 里已经存着的凭据——对它没有任何 ambient 回退;而 `openai-codex` 正是已安装 catalog 中唯一只声明 `auth.oauth`、没有 `auth.apiKey` 的提供方。`PiAiAdapter.current()` 以不带参数的 `createModels()` 构造集合,于是用的是 pi-ai 默认的 `InMemoryCredentialStore`:每次启动都是空的,每次配置变更产生新快照时又重建一份。本仓库没有任何位置调用 `Models.login()`;pi-ai 库这一半也不会去读 Codex 自己的 `~/.codex/auth.json`——它的 OAuth 模块是一套 PKCE 登录流程,凭据由*宿主*应用持久化,这正是 pi CLI 提供、而本适配器没有提供的东西。
于是页面用自己占位文案所描述的「留空」姿态,提供了一个根本没有「留空」姿态的提供方——而失败信息指向的是配置键,不是缺失的能力。唯一能让这条路由完成认证的,是把一个 ChatGPT OAuth token 粘进密钥框,那既不是这个提供所描述的用法,也会过期且这里没有任何环节会去刷新它。
## 决策
目录只提供本适配器认得的东西。`catalogProviderTakesApiKey(provider)` 回答 pi-ai 为某路由安装的提供方是否声明了 api-key 方法——这是 harness 唯一能供给的方法,因为它通过自己的凭据 seam 解析密钥,再作为请求的 `apiKey` 覆盖交给 pi-ai——`directoryEntries()` 跳过不满足它的 catalog 路由。
不尝试实现 OAuth。它需要持久化凭据存储、登录流程,以及运行登录的界面;这三样都不是发布阻塞项的修复,而在它们缺席时仍把提供方摆出来,正是这次报告的成因。
两条边界把「不提供」的范围收窄:
- **catalog 成员身份不变。** `catalogProviderIds()` 仍回答 pi-ai 装了什么,因此目录条目上的 `declared` 标记仍然表示「没有已安装提供方对应这条路由」,而不是「这条路由不被提供」。
- **联合的 profile 那一半无条件保留。** settings 文档已经写过的路由保留条目,因此已存储的 `openai-codex` profile 仍然可见、可编辑、可删除,而不会滞留在文档里、页面上却没有任何入口能移除它。
resolution 未被触动。在仅 OAuth 的路由上指定 `apiKeyEnv` 的 profile 仍会构造出可用的提供方——`routeAuth` 会在 catalog 的 OAuth 旁边补上 harness 的 api-key 方法,而 pi-ai 的 Codex API 从 token 本身推导 account id——因此把它写进 `settings.yaml``cordis.yml` 的部署保留这条路径。改为在 `resolveProfiles` 里强制拒绝会在注册时就否掉这类 profile;又因为 `validate` 在启动时与写入时同样运行,一份已经写有无密钥 OAuth 路由的文档会让整个 namespace 注册失败,而不只是一个提供方失败。
## 备选方案
- **在 `resolveProfiles` 里拒绝无密钥的仅 OAuth 路由。** 这才是本仓库通常强制决策的位置,而目录过滤是一层 `cordis.yml` entry 可以绕过的表面。因上述启动行为被否决:已存储的 profile 会连带拖垮该 namespace 中其他所有路由,对一次发布而言,这是拿一个提供方的缺陷换取全体的缺陷。留下的缺口是:被修的是「提供」而不是「能力」——部署仍可手写一条页面上已经无法添加的路由。
- **保留提供,只修正占位文案。** 那么该输入框只能写「此提供方需要本构建无法运行的登录」,等于一张唯一诚实内容就是「它不能用」的卡片。
- **把 `Provider is not configured` 映射成具名 `LlmError`。** 值得做,而且触发原因本次改动并未消除——任何留空密钥、其提供方又在进程环境里找不到东西的 api-key 路由,都会产生同一句话。作为独立改动暂缓:它改进的是诊断,而不是移除一个坏掉的提供。
- **把 `~/.codex/auth.json` 读进 pi-ai 的 `CredentialStore`。** 这能让 Codex 在没有登录流程的情况下可用,刷新也由 pi-ai 负责。但它为一个提供方把 harness 绑定到另一个工具的私有文件格式上,这属于 OAuth 那项工作的决策,而不是发布期的修复。
## 影响
`openai-codex` 从提供方选择器、以及模型设置页所 join 的目录中消失;其余已安装提供方一概不受影响,包括在 api-key 方法*之外*另提供 OAuth 的那六个(`anthropic``github-copilot``kimi-coding``openrouter``radius``xai`),它们保留条目也保留密钥路径。将来若出现只带 OAuth 的提供方,会被自动排除,而不是靠列名。
两处相邻缺口仍在,并记录在包 README 中:不指定凭据的路由仍走 catalog 提供方自带的发现,而它只读进程环境变量——不读 `~/.aws/credentials`,也不读 harness 凭据 seam——且由此产生的失败仍是兜底的 `PI_AI_ERROR`
## 测试
包测试钉住联合的两半:不予提供的路由不出现在 `listConfigurableProviders()` 中,而 `anthropic``openai` 仍在;已存储的 `openai-codex` profile 仍产出完整条目且 `declared: false`。既有的 resolution 测试未改动且依然通过,这正是「不提供」没有收窄手写 profile 可服务范围的证据。`models-settings``onboarding-usable-provider` 两条 web e2e golden 恰好各少了 `openai-codex` 这一行选项,录自真实装配的应用。
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-06-15-code-mode.md
2026-06-15-code-mode.md: adabf80ab03bdae57bba84957cdacf648c16c2f6
2026-06-15-code-mode.zh.md: 24d83b9f896a73b449852d28c3e0a376fb6ace5a
2026-06-15-code-mode.md: dbf77557c9f1aa59deed443b74ca3ef83137d773
2026-06-15-code-mode.zh.md: 6f263d946cf7632a0747dcb61f3dd22105e21e0c
@@ -19,7 +19,7 @@ Tool presentation belongs to the registry that owns tool visibility: implementin
Three decisions, each elaborated in its own section below:
1. **Code Mode is a first-class presentation mode of `ToolRuntime`** (`dsh-tools`), selected by a validated `mode` config: `'native'` (the default, contributing the visible capability schemas), `'code'` (the registry contributes only its reserved `run_code` transport plus a generated SDK `.d.ts` in the system prompt), or `'both'` (native schemas and the transport + SDK). The registry constructs its canonical contribution at the source; the cooperative prompt-assembly result remains authoritative, and the logged request header records exactly that returned presentation.
2. **Code execution is a capability seam**`packages/code-runtime/` contains the Service Definition package `@deepseek-ai/dsh-code-runtime`, which owns `ctx.codeRuntime` ([capability seams](../architecture/2026-06-13-capability-seams.md); Consumer = `dsh-tools`, with core-consumes-a-seam precedent in `agent-loop``dsh-llm`). The runtime knows nothing about tools: it is handed a program and named async bindings, runs the program, and reports `{ value, logs, error? }`. Language and substrate are backend properties, so a future Python or container backend is another Service provider package, not a redesign.
2. **Code execution is a capability seam**`packages/code-runtime/` contains the Service Definition package `@deepseek-ai/dsh-code-runtime`, which owns `ctx.codeRuntime` ([capability seams](../architecture/2026-06-13-capability-seams.md); Consumer = `dsh-tools`, with core-consumes-a-seam precedent in `agent-loop``dsh-llm`). The runtime knows nothing about tools: it is handed a program and named async bindings, runs the program, and reports `{ value, logs, error? }`. Language and substrate are backend properties, so a future Python or container backend is another Service Provider package, not a redesign.
3. **The shipped implementation is `@deepseek-ai/dsh-code-runtime-worker-thread`**: one fresh Node worker thread per run, executing the model's TypeScript after type-strip, with bindings bridged over the message port, an empty environment, configurable heap/output/time caps, and hard termination. Its trust posture is bash-equivalent by design — no unsafe-acknowledgement flags — because the harness already ships `dsh-bash-local`, which executes arbitrary model-written shell commands with strictly *more* ambient authority.
This note owns Code Mode's presentation, composition, isolation, and settlement foundation. The later [typed tool-return Agent Note](2026-07-20-code-mode-typed-tool-returns.md) owns the generated output map, canonical binding values, `ToolCallError`, and the lossless outer-output boundary.
@@ -19,7 +19,7 @@ Cloudflare 的 [Code Mode](https://blog.cloudflare.com/code-mode/) 提出了一
三项决策,各自在下方独立小节中展开:
1. **Code Mode 是 `ToolRuntime``dsh-tools`)的一等呈现模式**,通过经校验的 `mode` 配置选择:`'native'`(默认,贡献可见能力 schema)、`'code'`(注册表仅贡献其保留的 `run_code` 传输通道加一份生成的 SDK `.d.ts` 到系统提示词中)或 `'both'`(原生 schema 加传输通道 + SDK)。注册表在源头构建其规范贡献;协作式提示词组装的结果仍具权威性,记录在日志中的请求头精确反映该返回的呈现。
2. **代码执行是一个能力 seam**——`packages/code-runtime/` 包含 Service Definition 包 `@deepseek-ai/dsh-code-runtime`,拥有 `ctx.codeRuntime`[能力 seam](../architecture/2026-06-13-capability-seams.md);消费方 = `dsh-tools`core 消费 seam 的先例见 `agent-loop``dsh-llm`)。运行时对工具一无所知:它接收一段程序和命名的异步绑定,执行程序,报告 `{ value, logs, error? }`。语言和基底是后端属性,因此未来的 Python 或容器后端只是另一个 Service 提供方包,而非重新设计。
2. **代码执行是一个能力 seam**——`packages/code-runtime/` 包含 Service Definition 包 `@deepseek-ai/dsh-code-runtime`,拥有 `ctx.codeRuntime`[能力 seam](../architecture/2026-06-13-capability-seams.md);消费方 = `dsh-tools`core 消费 seam 的先例见 `agent-loop``dsh-llm`)。运行时对工具一无所知:它接收一段程序和命名的异步绑定,执行程序,报告 `{ value, logs, error? }`。语言和基底是后端属性,因此未来的 Python 或容器后端只是另一个 Service Provider 包,而非重新设计。
3. **交付的实现是 `@deepseek-ai/dsh-code-runtime-worker-thread`**:每次运行 spawn 一个全新的 Node worker 线程,对模型的 TypeScript 进行 type-strip 后执行,绑定通过消息端口桥接,环境为空,堆/输出/时间上限可配置,并支持硬终止。其信任姿态在设计上等同于 bash——无需 unsafe-acknowledgement flag——因为 harness 已经交付了 `dsh-bash-local`,后者以严格*更高*的环境权限执行模型编写的任意 shell 命令。
本说明负责定义 Code Mode 的呈现、组合、隔离与结算基础。后续的[类型化工具返回值 Agent Note](2026-07-20-code-mode-typed-tool-returns.md)负责定义生成的输出映射、规范绑定值、`ToolCallError` 和无损外层输出边界。
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-06-18-compaction-capability-seam.md
2026-06-18-compaction-capability-seam.md: 77ccd615a20524fad2a1ee47130ba2837d83c663
2026-06-18-compaction-capability-seam.zh.md: a18e7593a5b654685a2e033dd2f0a721da74502e
2026-06-18-compaction-capability-seam.md: 71ea7144cd843364fd77db0dd2c8a0d1e8bab1ac
2026-06-18-compaction-capability-seam.zh.md: bd7ab06b3a012d83d3f918a3ebd5a553624780e9
@@ -14,7 +14,7 @@ Two forces shape the design. First, compaction policy and reusable token measure
## Decision
### Compaction is a capability seam with separate Service Definition and Service provider roles
### Compaction is a capability seam with separate Service Definition and Service Provider roles
Per the [capability-seams Agent Note](../architecture/2026-06-13-capability-seams.md), compaction ships as separate packages so the contract, the algorithm, and (later) the consumer API evolve independently:
@@ -14,7 +14,7 @@ Status: implemented
## 决策
### 压缩是一个能力 seamService Definition 与 Service 提供方角色分离
### 压缩是一个能力 seamService Definition 与 Service Provider 角色分离
遵循[能力 seam Agent Note](../architecture/2026-06-13-capability-seams.md),压缩以独立包发布,使约定、算法和(后续的)消费方 API 各自独立演进:
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.md
2026-06-21-subagent-capability-seam.md: 337e1f8aaf8db8b9d41bbf0ad02f7f8a9468e4fb
2026-06-21-subagent-capability-seam.zh.md: f88ad5cfba5b7f364064ad6b82fc918bdf5d7ef3
2026-06-21-subagent-capability-seam.md: fa3b4f570bfccdc849a38b3eda16c1c8dd7b1827
2026-06-21-subagent-capability-seam.zh.md: b25fe64377f98af92dbccb87f755627926975ef2
@@ -21,7 +21,7 @@ The harness has a long-deferred seam for **subagents** — an agent delegating w
### Why not the bash seam shape
The bash seam ([capability seams](../architecture/2026-06-13-capability-seams.md)) registers exactly one `ShellExecutor` per context; loading a second throws. That is correct for bash (one machine, one way to run a command) but wrong here: coexistence is the requirement. So the subagent service is a **named-provider registry** — each implementation registers under a unique name and a caller picks one by name — mirroring the **LLM adapter registry** (`LlmRuntime.registerAdapter`), not the single-service bash executor. The seam is still three-package (Service Definition / Service provider / Consumer); only the "one vs. many implementations" axis differs.
The bash seam ([capability seams](../architecture/2026-06-13-capability-seams.md)) registers exactly one `ShellExecutor` per context; loading a second throws. That is correct for bash (one machine, one way to run a command) but wrong here: coexistence is the requirement. So the subagent service is a **named-provider registry** — each implementation registers under a unique name and a caller picks one by name — mirroring the **LLM adapter registry** (`LlmRuntime.registerAdapter`), not the single-service bash executor. The seam is still three-package (Service Definition / Service Provider / Consumer); only the "one vs. many implementations" axis differs.
## Decision
@@ -21,7 +21,7 @@ harness 有一个长期搁置的 seam 用于 **subagent**:一个 agent 将工
### 为何不采用 bash seam 的形状
bash seam[能力 seam](../architecture/2026-06-13-capability-seams.md))在每个上下文中只注册恰好一个 `ShellExecutor`;加载第二个会抛异常。这对 bash 是正确的(一台机器、一种执行命令的方式),但对这里是错误的:共存才是需求。因此 subagent 服务是一个**命名提供方注册表**——每个实现以唯一名称注册,调用方按名称选择——镜像 **LLM(大语言模型)适配器注册表**`LlmRuntime.registerAdapter`),而非单服务的 bash 执行器。seam 仍然是由三类包构成的结构(Service Definition / Service provider / Consumer);只是「一个 vs. 多个实现」这个维度不同。
bash seam[能力 seam](../architecture/2026-06-13-capability-seams.md))在每个上下文中只注册恰好一个 `ShellExecutor`;加载第二个会抛异常。这对 bash 是正确的(一台机器、一种执行命令的方式),但对这里是错误的:共存才是需求。因此 subagent 服务是一个**命名提供方注册表**——每个实现以唯一名称注册,调用方按名称选择——镜像 **LLM(大语言模型)适配器注册表**`LlmRuntime.registerAdapter`),而非单服务的 bash 执行器。seam 仍然是由三类包构成的结构(Service Definition / Service Provider / Consumer);只是「一个 vs. 多个实现」这个维度不同。
## 决策
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-05-dynamic-workflows.md
2026-07-05-dynamic-workflows.md: bf7b98891d19c31be7e1ca2763f98bb3668ab659
2026-07-05-dynamic-workflows.zh.md: f3c06ae17e7cf185865d03e261e4e6bbabcabd1f
2026-07-05-dynamic-workflows.md: eff37365534cd41e46c98e20a5e763a233330647
2026-07-05-dynamic-workflows.zh.md: 15aa8326958faebb53d33153b1c1537af199a4c5
@@ -10,7 +10,7 @@ The harness can delegate ONE task to ONE child (`dsh-tool-subagent`), but work t
## Decision
A workflow capability family at `packages/workflow/` in the bash seam shape (Service Definition / Service provider / Consumer), plus the structured-output foundation it needs on the subagent seam.
A workflow capability family at `packages/workflow/` in the bash seam shape (Service Definition / Service Provider / Consumer), plus the structured-output foundation it needs on the subagent seam.
### The script contract (Claude Code-compatible)
@@ -10,7 +10,7 @@ harness 可以通过 `dsh-tool-subagent` 将一个任务委派给一个子 agent
## 决策
`packages/workflow/` 下以 bash seam 的形态(Service DefinitionService providerConsumer)提供一组工作流能力,以及它在 subagent seam 上所需的结构化输出基础。
`packages/workflow/` 下以 bash seam 的形态(Service DefinitionService ProviderConsumer)提供一组工作流能力,以及它在 subagent seam 上所需的结构化输出基础。
### 脚本约定(兼容 Claude Code
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-06-approval-seam.md
2026-07-06-approval-seam.md: 5c62a175b0a96ecc8dd3f5b09b618b8e4d5e7612
2026-07-06-approval-seam.zh.md: 091db8347e0a5bd5ee475809dde6b90d183d3112
2026-07-06-approval-seam.md: ace41ebbb94cc24c2fdd3e7ae2d3a69b28b169af
2026-07-06-approval-seam.zh.md: 3e06c3ba26dd6d85555727dc0c62cbffe0a2a1da
@@ -97,7 +97,7 @@ Snapshots record allowed and rejected sandbox escalation through `session/reques
- **An inline `tools/pre-execute` permission gate in the ACP bridge** — rejected: prompting for every bridge-owned call hardwires the asking policy into the transport, cannot serve a second asker (sandbox escalation happens after execution starts, with no pre-execute moment), and leaves hook-produced `ask` decisions without a shared mechanism.
- **The generic user-questions seam (`ctx.userQuestions`)** — rejected as the approval mechanism: the two share a skeleton (route by agent, block for a human, handle absence), but approval's contract is narrower in every dimension that matters: a closed outcome vocabulary instead of free text, a protocol-native prompt attached to a tool call instead of a generic form, mandatory fail-closed absence, and audit events. Approval therefore does not ride the shipped `packages/interaction/user-questions` / `ask_user_question` elicitation path — an elicitation form is not a permission prompt, and a free-text answer is not a closed outcome; sharing provider plumbing stays open if the two ever converge.
- **Static optional injection in `dsh-tools`** — rejected: the vendored cordis `Inject` type has no optional flag — the object form maps service names to intercept config, and a declared inject gates the fiber. `ctx.get('approval')` is the documented opportunistic-consumption pattern (the `tool-bash` owner-token lookup, the loop's persistence probe), reads presence per call, and degrades correctly across HMR without extra machinery.
- **The capability-seam three-package split** — rejected: Service Definition / Service provider / Consumer fits a seam whose Service provider is swappable (bash-local vs bash-sandbox). Here the service body is fixed mechanism and the variable part is listeners that live with their owners — splitting would manufacture a Service provider package with nothing in it ("don't split preemptively").
- **The capability-seam three-package split** — rejected: Service Definition / Service Provider / Consumer fits a seam whose Service Provider is swappable (bash-local vs bash-sandbox). Here the service body is fixed mechanism and the variable part is listeners that live with their owners — splitting would manufacture a Service Provider package with nothing in it ("don't split preemptively").
- **Offering `allow_always` now** — rejected: the protocol can express it, but honoring it means designing grant storage, scope identity, and revocation (§ Deferred). Advertising an option the harness cannot honor manufactures doomed grants.
## Consequences
@@ -97,7 +97,7 @@ ACP 桥只应答其会话映射所拥有的精确 agent 对象。它携带既有
- **在 ACP 桥中内联 `tools/pre-execute` 权限门禁**:否决。对桥拥有的每次调用都弹出提示,会将请求策略硬编码进传输层,无法服务第二个发起方(沙箱升级发生在执行开始之后,没有 pre-execute 时刻),且钩子产生的 `ask` 决策没有共享机制。
- **通用用户交互 seam`ctx.userQuestions`**:否决作为审批机制。二者骨架相似(按 agent 路由、阻塞等待人类、处理缺失),但审批的约定在每个关键维度上都更窄:封闭的结果词汇而非自由文本、附着在工具调用上的协议原生提示而非通用表单、强制的缺失时失败关闭、以及审计事件。因此审批不走已交付的 `packages/interaction/user-questions` / `ask_user_question` 信息征集路径——信息征集表单不是权限提示,自由文本应答不是封闭结果;如果二者将来趋同,共享提供方管道仍然开放。
- **`dsh-tools` 中的静态可选注入**:否决。vendor 的 Cordis `Inject` 类型没有 optional 标志——对象形式将服务名映射到拦截配置,声明的 inject 会阻塞 fiber。`ctx.get('approval')` 是文档化的机会性消费模式(`tool-bash` 的 owner-token 查找、loop 的持久化探测),按调用读取存在性,跨 HMR 正确降级,无需额外机制。
- **能力 seam 的三包拆分**:否决。Service Definition/Service provider/Consumer 适合 Service provider 可替换的 seambash-local vs bash-sandbox)。此处服务体是固定机制,可变部分是留在各自通道拥有者插件中的监听器——拆分只会制造一个空的 Service provider 包(「不要预防性拆分」)。
- **能力 seam 的三包拆分**:否决。Service Definition/Service Provider/Consumer 适合 Service Provider 可替换的 seambash-local vs bash-sandbox)。此处服务体是固定机制,可变部分是留在各自通道拥有者插件中的监听器——拆分只会制造一个空的 Service Provider 包(「不要预防性拆分」)。
- **现在就提供 `allow_always`**:否决。协议能表达它,但兑现它意味着设计授权存储、作用域标识和撤销(§ 延后)。展示 harness 无法兑现的选项只会制造注定失败的授权。
## 后果
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-06-sandbox.md
2026-07-06-sandbox.md: 76733c619e3e34a6ee49c80aaf5c3bd4d48d82f7
2026-07-06-sandbox.zh.md: 36ecbadd0589b4859389961aa52181de3f034dcc
2026-07-06-sandbox.md: 62c46c99a2283b03cf75d8823783367dd6b3473a
2026-07-06-sandbox.zh.md: 82c2e7962800c007a207f0204bf47cef01f79a36
@@ -54,7 +54,7 @@ OS subprocess confinement applies to the bash executor, including hook commands,
Policy rides each CALL, not the provider: two consumers may confine under different policies at the same instant (bash under `read-only` while a confined child agent keeps its state directory writable), and an approved escalated retry is a new call with a wider policy — inexpressible under a config-fixed provider mode.
The seam confines SAME-WORLD subprocesses only: a backend shares the host's filesystem and kernel. Containers, microVMs, and remote executors are NOT backends of this seam — they replace the Service providers for whole capability seams (`ctx.shell`, `ctx.fs`) as environment-coherent groups, because an agent whose bash runs in a container while its fs tools write the host lives in two split worlds.
The seam confines SAME-WORLD subprocesses only: a backend shares the host's filesystem and kernel. Containers, microVMs, and remote executors are NOT backends of this seam — they replace the Service Providers for whole capability seams (`ctx.shell`, `ctx.fs`) as environment-coherent groups, because an agent whose bash runs in a container while its fs tools write the host lives in two split worlds.
Left open, for the phase that needs them: whether network restriction arrives as a separate `network_mode` or merges into `sandbox_mode` once a runner enforces both, and whether `SandboxPolicy` grows extra writable-root grants now (the launcher already speaks `--rw <path>`) or only when escalation needs them.
@@ -199,7 +199,7 @@ Costs and accepted limits:
In-repo precedents this design copies or contrasts with:
- [The capability-seams Agent Note](../architecture/2026-06-13-capability-seams.md) — the Service Definition / Service provider / Consumer split and the "don't split preemptively" timing rule the second consumer satisfied.
- [The capability-seams Agent Note](../architecture/2026-06-13-capability-seams.md) — the Service Definition / Service Provider / Consumer split and the "don't split preemptively" timing rule the second consumer satisfied.
- The `dsh-shell` request/spec split ([the bash vocabulary catalog](../../../../docs/subsystems/shell.md)) — the complete `sandboxPolicy` rides its per-call carrier, and the explicit-`resolve()` defaulting convention.
- [The approval seam Agent Note](2026-07-06-approval-seam.md) — the channel escalation asks through; its answerer waterfall, audit pair, and one-package rationale are recorded there.
- [Event-sourced sessions](../architecture/2026-06-11-event-sourced-sessions.md) and [standalone log-only events](../simplification/2026-07-28-remove-synthetic-log-only-turns.md) — the log-as-store foundation the per-session modes fold over, and the explicit durability boundary the anchoring design obeys.
@@ -54,7 +54,7 @@ OS 子进程约束适用于 bash 执行器(包括钩子命令),后续还
策略随每次调用而非提供方携带:两个消费方可以在同一时刻以不同策略约束(bash 在 `read-only` 下运行,而一个受约束的子 agent 保持其状态目录可写),且经批准的升级重试是一次带有更宽策略的新调用——在配置固定的提供方模式下无法表达。
该 seam 仅约束与宿主共享文件系统和内核的子进程。容器、microVM 和远程执行器不是此 seam 的后端——它们以环境一致的组替换整个能力 seam(`ctx.shell``ctx.fs`)的 Service provider,因为一个 bash 在容器中运行而 fs 工具写主机的 agent 生活在两个割裂的世界中。
该 seam 仅约束与宿主共享文件系统和内核的子进程。容器、microVM 和远程执行器不是此 seam 的后端——它们以环境一致的组替换整个能力 seam(`ctx.shell``ctx.fs`)的 Service Provider,因为一个 bash 在容器中运行而 fs 工具写主机的 agent 生活在两个割裂的世界中。
留待需要时再决定:网络限制是作为独立的 `network_mode` 到来,还是在某个 runner 同时强制两者后合并进 `sandbox_mode`;以及 `SandboxPolicy` 是现在就增加额外的可写根授权(launcher 已支持 `--rw <path>`),还是等到升级机制需要时再加。
@@ -199,7 +199,7 @@ fs/web/todo 在进程内执行,因此它们的沙箱语义是各自能力边
本设计复制或对比的仓库内先例:
- [能力 seam Agent Note](../architecture/2026-06-13-capability-seams.md)——Service Definition/Service provider/Consumer 拆分与「不要过早拆分」的时机规则(第二个消费方满足了该规则)。
- [能力 seam Agent Note](../architecture/2026-06-13-capability-seams.md)——Service Definition/Service Provider/Consumer 拆分与「不要过早拆分」的时机规则(第二个消费方满足了该规则)。
- `dsh-shell` 的 request/spec 拆分([bash 词汇目录](../../../../docs/subsystems/shell.md))——完整的 `sandboxPolicy` 搭载其按调用载体,以及显式 `resolve()` 默认约定。
- [批准 seam Agent Note](2026-07-06-approval-seam.md)——升级请求通过的通道;其应答器 waterfall(瀑布式事件)、审计对和单包理由记录在那里。
- [事件溯源会话](../architecture/2026-06-11-event-sourced-sessions.md)与[独立纯日志事件](../simplification/2026-07-28-remove-synthetic-log-only-turns.md)——按会话模式 fold 所依赖的日志即存储基础,以及锚定设计遵守的显式持久性边界。
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-27-trajectory-inspection-ledger.md
2026-07-27-trajectory-inspection-ledger.md: 74ed1f8ec6f6efcbf77e9caec7e254cb114efbd9
2026-07-27-trajectory-inspection-ledger.zh.md: be648cb73478037979fd941753129b7c68be5085
2026-07-27-trajectory-inspection-ledger.md: 7f97bc395ed694a0a750d3b744cc9887834ef32f
2026-07-27-trajectory-inspection-ledger.zh.md: 34bb8741124c410fe06ec18bdde801caa987f4fb
@@ -21,7 +21,7 @@ Trajectory has to make prose, machine payloads, token usage, timing, and nested
- Call schemas come from the active recorded Request header. Keyless snapshot fixtures deliberately replace that catalog with the non-array `{{tools}}` token, which the durable inspection boundary treats as unavailable instead of attempting to project or fabricate schemas.
- Selecting a record or Request opens an inspector inside Trajectory. Tabs and Summary sections follow the selected entity: Markdown messages expose rendered content, source fields, provider/model fields, and hierarchy views; tools add JSON payload/result and schema views; Requests add options, usage, timing, and result navigation. Scrollable Summary regions keep their scrollbar thumbs transparent until hover or `focus-within`, while retaining the scrollbar reservation and scroll behavior. Images render as media rather than serialized data.
- Turn folding removes all rows after its first record and replaces them with a compact step/tool-call count; Assistant folding applies the same interaction to its tool-call descendants. Global controls fold or expand both levels.
- A long ledger initially positions the loaded tail at the bottom and mounts only the viewport's row window plus bounded overscan. Request-only separators join the next measurable virtual item, with a terminal separator retaining its own fixed clearance, so the virtualizer never owns a zero-height item. Semantic DOM-safe row keys and ARIA indexes expose identity independently from mount position. A tail with known older history virtualizes immediately even when its loaded projection is below the ordinary row threshold. Stable-key virtualizer anchoring preserves the visible item across prepends and appends; the manual scroll-height fallback applies only when completing pagination disables virtualization. Selection, timeline focus, folding, search, and bottom following address records by stable event or tool-call identity rather than requiring their DOM rows to exist. An explicit loading row covers records until initial positioning finishes and while an older Session page is pending.
- A long ledger initially positions the loaded tail at the bottom and mounts only the viewport's row window plus bounded overscan. Request-only separators join the next measurable virtual item, with a terminal separator retaining its own fixed clearance, so the virtualizer never owns a zero-height item. Semantic DOM-safe row keys and ARIA indexes expose identity independently from mount position. A tail with known older history virtualizes immediately even when its loaded projection is below the ordinary row threshold. Stable-key virtualizer anchoring preserves the visible item across prepends and appends; the manual scroll-height fallback applies only when completing pagination disables virtualization. Selection, timeline focus, folding, search, and bottom following address records by stable event or tool-call identity rather than requiring their DOM rows to exist. An explicit loading row covers records until initial positioning finishes. While an older Session prefix remains unloaded, an interactive first row precedes the loaded records and requests one older page; the same row becomes a disabled loading status for a pending page and disappears only when paging completes.
- The separate Waterfall tab is removed. A fixed Overview above the ledger projects every loaded record with known `startedAt` onto three semantic timing lanes using its own duration. While an older prefix remains unloaded and the viewport includes the loaded domain's start, a neutral ellipsis control covers the truncated edge and loads one earlier page without assigning unknown history a fabricated duration; hovering that control suppresses the ordinary timeline cursor. Finalized Assistant spans divide the recorded interval at the first non-empty token delta, so distinct TTFT and decoding colors retain their actual ratio; incomplete timing falls back to one Assistant color. Hovering for 500 ms exposes exact start/end, total duration, TTFT, and decoding time without relying on the browser's native tooltip delay. Dragging left or right commits an inclusive interval filter: any record whose active interval overlaps either boundary remains visible, records without known timing leave the focused ledger, and clearing the selection restores the full loaded ledger. Wheel gestures zoom the time domain. A right-button click clears the interval selection; dragging instead pans an already zoomed viewport without mutating it. The Overview keeps the full time domain while focused so the selection can be resized or cleared without losing orientation.
- Live history updates retain the ledger's bottom position only while the user is already following its tail. Scrolling upward clears that follow state, so streamed chunks and newly appended records do not interrupt inspection of earlier rows. Tail following and virtualizer measurement react to row keys and heights rather than content identity, so text-only stream frames neither discard the measurement cache nor repeat a DOM scroll write.
- Token streaming updates only the matching Trajectory Assistant Context, while publication is coalesced to at most once per animation frame. The target snapshot preserves the existing stage, layout, Request numbering, Overview, and search inputs; completed Assistant State retains assembled blocks, timing, and usage rather than every raw chunk payload, while Session keeps the raw Event window.
@@ -21,7 +21,7 @@ Status: implemented
- 调用 schema 来自当前生效且已记录的请求头。无密钥快照 fixture(测试前置数据)有意将该目录替换为非数组 token `{{tools}}`,持久化检查边界会将其视为不可用,而不是尝试投影或虚构 schema。
- 选择记录或请求后,Trajectory 内部会打开检查器,其标签页和概述区域随实体类型变化:Markdown 消息提供渲染内容、来源字段、提供方/模型字段和层级视图;工具提供 JSON 载荷/结果和 schema 视图;请求提供选项、用量、计时和结果跳转。可滚动的概述区域默认保持滚动条滑块透明,直到悬停或 `focus-within` 时才显示,同时保留滚动条预留空间和滚动行为。图片以媒体形式渲染,而不是显示为序列化数据。
- 折叠轮次时保留其第一条记录,并用紧凑的步骤数和工具调用数替换后续所有行;折叠助手时对其工具调用后代应用相同操作。全局控件会折叠或展开这两个层级。
- 长记录表初始时将已加载尾部置于底部,只挂载视口对应的行窗口及有界的额外缓冲行。仅含请求的分隔行并入下一个具备可测高度的虚拟项,末尾分隔行则保留固定留白,因此虚拟化器不会管理零高度项。可安全用于 DOM 的语义行键与 ARIA 索引使标识不依赖挂载位置。只要已知尾部之前仍有更早历史,即使当前已加载投影低于常规行数阈值,也会立即启用虚拟化。基于稳定键的虚拟化器锚定会在向前补页和尾部追加时保留当前可见项;只有分页完成导致虚拟化停用时,才使用手动滚动高度兜底。选择、时间线聚焦、折叠、搜索和末尾跟随均按稳定的事件或工具调用标识定位,不要求对应 DOM 行已存在。初始定位完成前以及更早 Session 页面仍在等待时,明确的加载行会遮住真实记录
- 长记录表初始时将已加载尾部置于底部,只挂载视口对应的行窗口及有界的额外缓冲行。仅含请求的分隔行并入下一个具备可测高度的虚拟项,末尾分隔行则保留固定留白,因此虚拟化器不会管理零高度项。可安全用于 DOM 的语义行键与 ARIA 索引使标识不依赖挂载位置。只要已知尾部之前仍有更早历史,即使当前已加载投影低于常规行数阈值,也会立即启用虚拟化。基于稳定键的虚拟化器锚定会在向前补页和尾部追加时保留当前可见项;只有分页完成导致虚拟化停用时,才使用手动滚动高度兜底。选择、时间线聚焦、折叠、搜索和末尾跟随均按稳定的事件或工具调用标识定位,不要求对应 DOM 行已存在。初始定位完成前,明确的加载行会遮住真实记录。更早 Session 前缀仍未加载时,交互式首行位于已加载记录之前,可请求一页更早历史;页面加载期间,同一行会变为禁用的加载状态,仅在分页完成时消失
- 移除独立的 waterfall(瀑布式事件)标签页。固定在记录表上方的 Overview 区域将所有 `startedAt` 已知的已加载记录按各自耗时投影到三条语义计时轨道。仍有更早前缀尚未加载且 viewport 包含已加载时间域起点时,中性的省略号控件会遮住截断边缘并加载一页更早历史,而不会为未知历史虚构耗时;悬停在该控件上会隐藏普通的时间线光标。已完成的助手时间条以首个非空 token 增量为分界,用不同颜色按真实比例表示 TTFT 与解码时间;计时不完整时退化为单一助手色。悬停 500 ms 后会显示精确起止时刻、总耗时、TTFT 和解码时间,而不依赖浏览器原生 tooltip 的延迟。向左或向右拖动会提交包含边界的区间筛选:任何活动区间与所选区间任一边界重叠的记录都会保留,计时未知的记录会从聚焦后的记录表中移除,清除选择则恢复完整的已加载记录表。滚轮手势用于缩放时间域。右键单击会清除区间选择;右键拖动则只会平移已放大的 viewport,不会改变该选区。聚焦后,Overview 区域仍保留完整时间范围,以便在不失去方位的情况下调整或清除选择。
- 实时历史更新仅在用户已经跟随记录表末尾时保留底部位置。向上滚动会清除跟随状态,因此流式分块和新追加的记录不会打断对旧记录的检查。末尾跟随与虚拟化器测量仅响应行键和高度,而非内容标识,因此仅含文本的流式帧既不会丢弃测量缓存,也不会重复执行 DOM 滚动写入。
- token 流式输出只更新命中的 Trajectory Assistant Context,发布则合并为每个 animation frame 最多一次。target snapshot 继续提供既有 stage、layout、请求编号、Overview 与搜索输入;已完成的 Assistant State 只保留组装后的 blocks、计时与 usage,不保留每条原始 chunk payload,而 Session 继续保存原始 Event 窗口。
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-27-typescript-sdk-and-sdk-subagent-backend.md
2026-07-27-typescript-sdk-and-sdk-subagent-backend.md: 8b17603e5c90941778835befd84d114a3f3cd78b
2026-07-27-typescript-sdk-and-sdk-subagent-backend.zh.md: fe5026871778bfcdcb85b71704d79e6fa678188d
2026-07-27-typescript-sdk-and-sdk-subagent-backend.md: 84314eaf5827464767666b1b9c65e105ea4e869a
2026-07-27-typescript-sdk-and-sdk-subagent-backend.zh.md: 48f292514844d59d2084731b30bcb5ba7fb563d5
@@ -10,7 +10,7 @@ The stdio JSON-RPC serving surface (`@deepseek-ai/dsh-sdk-jsonrpc-server`, the [
## Decision
Three packages, layered exactly like the existing Python stack, plus one Service provider registration:
Three packages, layered exactly like the existing Python stack, plus one Service Provider registration:
- **`@deepseek-ai/dsh-sdk-protocol`** (`packages/sdk/protocol/`) — the wire made shared and nominal. `JsonRpcLineTransport` moves here verbatim from `dsh-sdk-jsonrpc-server` (which now imports it), and `types.ts` names every payload the server speaks: `InitializeParams/Result`, `SessionPromptParams/Result`, the four notification payloads, and the `HarnessSdkRequestMap`/`HarnessSdkNotificationMap` indexes. The package root explicitly exports that complete interface and provides no source-module deep imports. The server's `notify()` call sites are typed against these named payloads, so server drift breaks compilation, not clients. One behavioral change: an error response now rejects with `JsonRpcResponseError` carrying the wire `code`/`data` (the Python client already preserved these; the old transport threw a bare `Error` with only the message).
- **`@deepseek-ai/dsh-sdk-client`** (`packages/sdk/client/`) — the TypeScript twin of `python/sdk`: `HarnessClient` (spawn, frame, fan out notifications, typed error surfaces, close-to-quiescence via the shared dispose ladder) under `DeepSeekHarness`/`HarnessSession` (lazy start, memoized `initialize`, `run()` pairing one `session/prompt` with its `session.finished`). Its package-root consumer interface explicitly exports both client layers, caller-facing types, and the protocol-owned `JsonRpcResponseError`; source modules, normalization helpers, and the notification producer stay internal. `TurnResult.events` contains only the root session's typed events, while `notifications` retains session ids across the root and descendants discovered from `subagent.started`; session-tree scoping is client-side, mirroring `client.py`. Deliberate asymmetries with Python: the launch spec is explicit `command`/`args` (no bundled-runtime resolution — that is a distribution concern with no TS consumer yet); `env` replaces rather than merges (callers own credential policy; `scrubbedParentEnv` from the subprocess seam is one import away); `TurnResult` carries the structured `reason` (Python exposes only `status`); teardown walks a private stdin-EOF → SIGTERM → SIGKILL ladder to actual exit (the client runs outside any harness context, so it cannot ride `ctx.subprocess`).
@@ -30,7 +30,7 @@ Four tiers, per [testing policy](../../../../docs/testing.md):
## Alternatives considered
**Import wire types from `dsh-sdk-jsonrpc-server` instead of extracting a protocol package.** Makes every SDK consumer (including `subagent-dsh-sdk`, which must not serve JSON-RPC) depend on the server plugin and its `dsh-agent`/`dsh-llm-deepseek` peer set, and leaves the notification payloads anonymous. The capability-seam rule (Service Definition / Service provider / Consumer as separate packages) already names this shape; the transport is genuinely two-sided.
**Import wire types from `dsh-sdk-jsonrpc-server` instead of extracting a protocol package.** Makes every SDK consumer (including `subagent-dsh-sdk`, which must not serve JSON-RPC) depend on the server plugin and its `dsh-agent`/`dsh-llm-deepseek` peer set, and leaves the notification payloads anonymous. The capability-seam rule (Service Definition / Service Provider / Consumer as separate packages) already names this shape; the transport is genuinely two-sided.
**Have `subagent-dsh-sdk` speak raw JSON-RPC without the client SDK.** Duplicates the request/notification pairing, subscription fan-out, timeout, and teardown logic the SDK exists to own; the user's ask was explicitly a backend that *uses* the SDK, and the layering earns its keep by making the backend ~200 lines of policy over a reusable client.
@@ -10,7 +10,7 @@ stdio JSON-RPC 对外服务接口(`@deepseek-ai/dsh-sdk-jsonrpc-server`,见[
## 决策
三个包,分层与既有 Python 栈完全一致,外加一个 Service provider 注册:
三个包,分层与既有 Python 栈完全一致,外加一个 Service Provider 注册:
- **`@deepseek-ai/dsh-sdk-protocol`**`packages/sdk/protocol/`)—— 把线协议做成共享且具名。`JsonRpcLineTransport``dsh-sdk-jsonrpc-server` 原样移入(后者现在导入它),`types.ts` 为服务器所说的每个载荷命名:`InitializeParams/Result``SessionPromptParams/Result`、四个通知载荷,以及 `HarnessSdkRequestMap`/`HarnessSdkNotificationMap` 索引。该包根显式导出这一完整接口,且不提供指向源模块的深层导入。服务器的 `notify()` 调用点以这些具名载荷标注类型,服务器漂移会先破坏编译而不是破坏客户端。一处行为变化:错误响应现在以携带线上 `code`/`data``JsonRpcResponseError` 拒绝(Python 客户端本就保留这些;旧传输只抛携带消息的裸 `Error`)。
- **`@deepseek-ai/dsh-sdk-client`**`packages/sdk/client/`)—— `python/sdk` 的 TypeScript 孪生:`HarnessClient`(spawn、分帧、通知扇出、有类型的错误表面、经共享 dispose(资源释放)阶梯关闭至完全停稳)之上是 `DeepSeekHarness`/`HarnessSession`(惰性启动、记忆化 `initialize``run()` 把一个 `session/prompt` 与其 `session.finished` 配对)。其包根消费方接口显式导出两层客户端、面向调用方的类型,以及协议包所拥有的 `JsonRpcResponseError`;源模块、规范化辅助函数和通知投递端都保留为内部实现。`TurnResult.events` 只包含根会话的类型化事件,而 `notifications` 则保留根会话及从 `subagent.started` 发现的后代各自的会话 id;基于 `subagent.started` 血缘边的会话树范围限定在客户端完成,镜像 `client.py`。与 Python 的刻意不对称:启动规格是显式 `command`/`args`(无捆绑运行时解析——那是尚无 TS 消费方的发行问题);`env` 整体替换而非合并(凭据策略归调用方;subprocess seam 的 `scrubbedParentEnv` 一个 import 即得);`TurnResult` 携带结构化 `reason`Python 只暴露 `status`);拆除走私有的 stdin-EOF → SIGTERM → SIGKILL 阶梯直到真正退出(客户端运行在任何 harness 上下文之外,无法搭乘 `ctx.subprocess`)。
@@ -30,7 +30,7 @@ stdio JSON-RPC 对外服务接口(`@deepseek-ai/dsh-sdk-jsonrpc-server`,见[
## 考虑过的替代方案
**从 `dsh-sdk-jsonrpc-server` 导入协议类型而不是提取协议包。** 会让每个 SDK 消费方(包括绝不能提供 JSON-RPC 服务的 `subagent-dsh-sdk`)依赖服务器插件及其 `dsh-agent`/`dsh-llm-deepseek` peer 集合,且通知载荷仍然匿名。能力 seam 规则(Service Definition/Service provider/Consumer 三个包分立)已经点名了这种形态;这个传输是货真价实的双边物。
**从 `dsh-sdk-jsonrpc-server` 导入协议类型而不是提取协议包。** 会让每个 SDK 消费方(包括绝不能提供 JSON-RPC 服务的 `subagent-dsh-sdk`)依赖服务器插件及其 `dsh-agent`/`dsh-llm-deepseek` peer 集合,且通知载荷仍然匿名。能力 seam 规则(Service Definition/Service Provider/Consumer 三个包分立)已经点名了这种形态;这个传输是货真价实的双边物。
**让 `subagent-dsh-sdk` 直说裸 JSON-RPC、绕开客户端 SDK。** 会复制 SDK 存在意义所在的请求/通知配对、订阅扇出、超时与拆除逻辑;用户的要求明确是一个*使用* SDK 的后端,分层的回报是后端成为可复用客户端之上约 200 行的纯策略。
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-30-deepseek-onboarding-credential-setup.md
2026-07-30-deepseek-onboarding-credential-setup.md: ebf5c66cffa8a7db90328dfcdd04e4134a78e3b1
2026-07-30-deepseek-onboarding-credential-setup.zh.md: 3df63c59fd23acf32c16dac8f4ca373409ab2968
2026-07-30-deepseek-onboarding-credential-setup.md: 823d10a723af70ec4ff51018b8b86198db0f5c29
2026-07-30-deepseek-onboarding-credential-setup.zh.md: 7e8d79c23c4b1489bfd818c90558f36509635486
@@ -12,9 +12,9 @@ The [web configuration plane](../architecture/2026-07-30-web-config-plane.md) ma
**One readiness projection owns both Models and onboarding facts.** `ui-settings-models` keeps a single store that joins `llm.providers({})`, redacted `settings.describe({})`, and batched `credentials.describe({refs})`. The onboarding projection selects the `deepseek-official` configurable-provider entry owned by the `llm-deepseek` namespace and empty settings path, reads the effective `apiKeyEnv`, and evaluates the matching credential descriptor. A live route with the same provider id but no matching configurable-provider declaration is adapter-absent for onboarding. A configured process-environment credential is ready and remains read-only.
**The settings shell contributes ordering and navigation, not provider policy.** `ui-settings` declares a root-scoped `settings.onboarding` list slot and mounts one ordered step at a time while the current surface is the empty Hero. The active registrant receives `complete()` and a private `openSection(id)` callback; completion transfers ownership to the next entry. `ui-settings-models` registers the DeepSeek step and its Models section through `slots.inject()`, so each contribution follows its declaration lifetime without making plugin load order a contract, and independently contributed dialogs cannot stack. The product-wide welcome step that precedes it is owned separately by [the versioned welcome decision](2026-07-30-versioned-gui-welcome-onboarding.md).
**The settings shell contributes ordering, not provider policy.** `ui-settings` declares a root-scoped `settings.onboarding` list slot and mounts one ordered step at a time while the current surface is the empty Hero. The active registrant receives `complete()` and a private `openSection(id)` callback; completion transfers ownership to the next entry. `ui-settings-models` registers the DeepSeek step, the preceding welcome notice, and its Models section through `slots.inject()`, so every contribution follows one client Cordis plugin's lifecycle and the dialogs cannot stack. Their common presentation is owned by the [shared-modal onboarding decision](2026-08-13-shared-modal-product-onboarding.md).
**The prompt routes to the one credential editor.** A mounted, active adapter with a resolved, writable, unconfigured reference presents one action that opens Settings on Models. The existing DeepSeek setup card there exclusively owns the password input, `credentials.set({ref, value})`, write failures, and post-write refresh; the onboarding overlay never holds or submits a secret. An unavailable settings or credential capability keeps its deployment diagnostic and routes to the same page, while an absent adapter remains skipped because navigation cannot mount a Cordis plugin.
**The prompt renders the existing credential editor inline.** A mounted, active adapter with a resolved, writable, unconfigured reference renders `ProviderEditor` in credential-only mode inside the shared onboarding modal. The same component owns the password input, validation, `credentials.set({ref, value})`, write failures, and post-write refresh; credential-only mode emits no provider-settings mutation. Configure later completes only this coordinator pass. An absent adapter remains skipped because the browser cannot mount a missing Cordis plugin.
**Unavailable states do not capture the product.** An absent configurable-provider entry, inactive route, failed initial join, read-only deployment, or unresolved settings or credential capability completes the step without rendering because the onboarding action cannot repair that state. The Models page remains the deployment diagnostic and retry surface. Configure later completes a missing-credential step for the current mounted coordinator pass and writes no completion fact. Settings, credential, provider-topology, and connection invalidations all refresh the shared join, so an external credential update completes an open step without a reload.
@@ -22,7 +22,7 @@ The [web configuration plane](../architecture/2026-07-30-web-config-plane.md) ma
**A separate onboarding store and readiness RPC sequence** — rejected because it would create a second client-side interpretation of provider identity, settings paths, secret sidecars, credential references, and invalidation ordering beside the Models page.
**A second API-key editor inside onboarding** — rejected because the Models page already renders its DeepSeek setup card for exactly this state. Duplicating its secret draft, write errors, and configured-state convergence would add a second security-sensitive UI without another user capability.
**A standalone API-key implementation inside onboarding** — rejected because it would duplicate the Models editor's secret draft, validation, errors, and configured-state convergence. The modal renders the existing `ProviderEditor` in a constrained mode instead.
**Writing the API key into provider settings** — rejected because a literal secret would enter the settings mutation path and whole-section replacement cannot safely reconstruct redacted values. Credential storage is already the product seam and supplies immediate invalidation.
@@ -30,4 +30,4 @@ The [web configuration plane](../architecture/2026-07-30-web-config-plane.md) ma
## Consequences
The ordered flow leads from the product notice to the shipped adapter's existing editor without restarting: a keyless browser test boots the real Web composition under an isolated harness home, acknowledges the notice, follows the DeepSeek page to Models, stores a generated key through that page into the home's `.credentials.yaml`, verifies no key reaches DOM, ARIA, or browser console output, and confirms the running page reports configured. The full keyless Web replay lane also pins that a non-configurable replay route with the same provider id does not block unrelated journeys. Pure readiness and React tests pin managed-file and process-environment credentials, missing providers and capabilities, navigation, cancellation, external invalidation, and coordinator transfer. The flow deliberately inherits the configuration plane's documented base limitations rather than adding local secret storage, redaction, or settings replacement workarounds.
The ordered flow leads from the product notice to an inline key form without restarting: a keyless browser test boots the real Web composition under an isolated harness home, acknowledges the notice, stores a generated key from the shared modal into the home's `.credentials.yaml`, verifies no key reaches DOM, ARIA, or browser console output, and confirms the ordinary Models page reports configured. The full keyless Web replay lane also pins that a non-configurable replay route with the same provider id does not block unrelated journeys. Pure readiness and React tests pin managed-file and process-environment credentials, missing providers and capabilities, cancellation, external invalidation, and coordinator transfer. The flow deliberately inherits the configuration plane's documented base limitations rather than adding local secret storage, redaction, or settings replacement workarounds.
@@ -12,9 +12,9 @@ Status: implemented
**Models 与首次使用引导共享同一个就绪状态投影。**`ui-settings-models` 维护一个 store,把 `llm.providers({})`、脱敏后的 `settings.describe({})` 和批量调用的 `credentials.describe({refs})` 联接为同一份状态。首次使用投影选取由 `llm-deepseek` namespace 与空 settings path 持有的 `deepseek-official` 可配置提供方条目,读取生效的 `apiKeyEnv`,并检查对应的凭据描述符。同 provider id 但没有匹配可配置提供方声明的存活路由,在首次使用引导中视为适配器缺失。通过进程环境提供的凭据若已配置,则判定为就绪并保持只读。
**设置外壳只贡献排序与导航,不持有提供方策略。** `ui-settings` 声明一个根作用域的 `settings.onboarding` list slot,并在当前界面为空白 Hero 时,每次只挂载一个有序步骤。当前注册方会收到 `complete()` 和私有 `openSection(id)` 回调;完成当前步骤后,所有权转交给下一项。`ui-settings-models` 通过 `slots.inject()` 注册 DeepSeek 步骤及 Models 分区,使每项贡献都跟随自身的声明生命周期,不让插件加载顺序成为约定;独立贡献的对话框也无法堆叠。排在它之前的产品级欢迎步骤由[版本化欢迎决策](2026-07-30-versioned-gui-welcome-onboarding.md)单独持有。
**设置外壳只贡献排序,不持有提供方策略。** `ui-settings` 声明一个根作用域的 `settings.onboarding` list slot,并在当前界面为空白 Hero 时,每次只挂载一个有序步骤。当前注册方会收到 `complete()` 和私有 `openSection(id)` 回调;完成当前步骤后,所有权转交给下一项。`ui-settings-models` 通过 `slots.inject()` 注册 DeepSeek 步骤、排在它之前的欢迎声明及 Models 分区,因此所有贡献都跟随同一个 client Cordis 插件的生命周期,两个弹窗也无法堆叠。它们的共用展示由[共用弹窗引导决策](2026-08-13-shared-modal-product-onboarding.md)持有。
**首次使用页面只负责跳转到唯一的凭据编辑器。**适配器已挂载且处于活跃状态,其引用可解析、可写但尚未配置时,界面会显示一个操作按钮,用于打开「设置」的 Models 分区。该分区已有的 DeepSeek 设置卡片全权负责密码输入框、`credentials.set({ref, value})`、写入失败处理和写入后刷新;首次使用页面绝不持有或提交 secret
**首次使用弹窗行内渲染既有凭据编辑器。** 适配器已挂载且处于活跃状态,其引用可解析、可写但尚未配置时,`ProviderEditor` 会以仅凭据模式渲染在共用引导弹窗中。同一个组件全权负责密码输入框、校验、`credentials.set({ref, value})`、写入失败处理和写入后刷新;仅凭据模式不会发出提供方 settings 变更。「稍后配置」只完成协调器当前这一轮。适配器缺失时仍跳过,因为浏览器不能挂载缺失的 Cordis 插件
**不可用状态不会占住产品。** 可配置提供方条目缺失、路由不活跃、初始联接失败、部署只读或设置/凭据能力无法解析时,都会直接完成而不渲染该步骤,因为首次使用引导无法修复这些状态。Models 页仍是部署诊断与重试界面。「稍后配置」只会完成协调器当前这一次缺少凭据的步骤,不写入任何完成状态。设置、凭据、提供方拓扑和连接失效事件都会刷新共享联接,因此外部凭据更新无需重新加载页面即可完成已打开的步骤。
@@ -22,7 +22,7 @@ Status: implemented
**为首次使用引导单设 store 与就绪状态 RPC 调用序列**:不予采用,因为这会在 Models 页之外,再建立一套客户端解释,用于判定提供方身份、设置路径、secret 槽位的伴随信息、凭据引用及失效事件顺序。
**在首次使用引导中增设第二个 API key 编辑器**:不予采用,因为 Models 页已为这一状态渲染 DeepSeek 设置卡片。复制其中的 secret 草稿、写入错误处理和已配置状态收敛会增加第二个安全敏感的 UI,却不会带来新的用户能力
**在首次使用引导中单独实现 API key 表单**:不予采用,因为这会复制 Models 编辑器的 secret 草稿、校验、错误和已配置状态收敛。弹窗改为以受限模式渲染既有 `ProviderEditor`
**把 API key 写入提供方设置**:不予采用,因为字面量 secret 会进入设置变更路径,而整个分节替换无法安全重建脱敏值。凭据存储已经是产品 seam,并能立即发出失效事件。
@@ -30,4 +30,4 @@ Status: implemented
## 后果
有序流程从产品声明页开始,无需重启即可引导用户前往随产品提供的适配器已有的编辑器:无密钥浏览器测试在隔离的 harness 家目录下启动真实 Web 组合,确认声明后依照 DeepSeek 页面前往 Models,通过该页面把生成的密钥存入该目录的 `.credentials.yaml`,验证密钥未进入 DOM、ARIA 或浏览器控制台输出,并确认运行中的页面报告已配置。完整的无密钥 Web 回放也固定了同 id 的不可配置回放路由不会阻塞无关流程。纯就绪状态测试与 React 测试固化了受管文件凭据与进程环境凭据、提供方与能力缺失、导航、取消、外部失效和协调器移交。该流程直接继承配置平面已记录的基础限制,不会另加局部的机密存储、脱敏或设置替换变通方案。
有序流程从产品声明页开始,无需重启即可进入行内密钥表单:无密钥浏览器测试在隔离的 harness 家目录下启动真实 Web 组合,确认声明后从共用弹窗把生成的密钥存入该目录的 `.credentials.yaml`,验证密钥未进入 DOM、ARIA 或浏览器控制台输出,并确认普通 Models 页面报告已配置。完整的无密钥 Web 回放也固定了同 id 的不可配置回放路由不会阻塞无关流程。纯就绪状态测试与 React 测试固化了受管文件凭据与进程环境凭据、提供方与能力缺失、取消、外部失效和协调器移交。该流程直接继承配置平面已记录的基础限制,不会另加局部的机密存储、脱敏或设置替换变通方案。
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-30-versioned-gui-welcome-onboarding.md
2026-07-30-versioned-gui-welcome-onboarding.md: 8897e28413df77d127a4d846f2a3e42789454490
2026-07-30-versioned-gui-welcome-onboarding.zh.md: 9f5eb99033cd62154937808f26b0e1aa40966c9f
2026-07-30-versioned-gui-welcome-onboarding.md: 9c8684c0510c50c28e8ece53fd794844c771e496
2026-07-30-versioned-gui-welcome-onboarding.zh.md: 52c35bf46e40afca1248bcbb94f38d7154f138a4
@@ -10,15 +10,13 @@ The GUI's credential onboarding begins with a DeepSeek-specific readiness check,
## Decision
**The Settings shell coordinates ordered steps.** `settings.onboarding` remains a root-scoped list, but `ui-settings` projects its entry ids and order into one coordinator and mounts only the first incomplete step. The active registrant receives `complete()` and `openSection(id)`; no later step mounts until ownership transfers. The product welcome registers at order `-100`, while `ui-settings-models` retains only the conditional DeepSeek readiness and credential-routing step at order `0`.
**The Settings shell coordinates ordered steps.** `settings.onboarding` remains a root-scoped list, but `ui-settings` projects its entry ids and order into one coordinator and mounts only the first incomplete step. The active registrant receives `complete()` and `openSection(id)`; no later step mounts until ownership transfers. `ui-settings-models` now registers the restored welcome notice at order `-100` and the conditional DeepSeek credential step at order `0`; their current shared presentation is owned by the [shared-modal onboarding decision](2026-08-13-shared-modal-product-onboarding.md).
**Ownerless product onboarding belongs to `ui-settings-general`.** `src/onboarding-copy.ts` is the single editable source for the complete notice, the Continue label, and `WELCOME_NOTICE_VERSION`; both supported GUI locales intentionally render the same Chinese owner copy. Runtime locale dictionaries derive their welcome values from that file, and tests import the same owner instead of repeating paragraph text. The notice is browser UI only: it creates no Session event and contributes no model-visible content. The notice states that session telemetry is [disabled by default](2026-08-10-telemetry-default-off.md) and names the `FEEDBACK_ONLY` and `FULL` opt-in modes.
**The product welcome step is versioned and feature-owned.** The notice was historically removed by the [first-run beta notice removal](../simplification/2026-08-13-remove-first-run-beta-notice.md) and is now restored in `ui-settings-models` with new testing-stage copy. `ui-settings-general` still seats no onboarding step; the plugin that owns both current steps also owns the copy, store, and shared modal.
**Loopback acknowledgement is durable per Harness profile.** The Host half registers a `ui-onboarding` section in the user-settings seam, stored under the active `$DSH_HOME/settings.yaml`. The connection plugin publishes whether the current page uses a loopback authority as `ctx.connection.isLoopback`; hostname classification remains internal to the connection package, and other client plugins consume the service state instead of importing its implementation. A loopback browser shows the notice unless `welcomeNoticeVersion` equals the owner constant exactly. Continue applies one path mutation with the current version and calls `complete()` only after the Host commits it; a failed write leaves the notice open, and closing the page or process writes nothing. Bumping the constant intentionally makes every profile acknowledge the revised copy once. A non-loopback browser must not call the loopback-only settings API. It presents the same notice, but explicit Continue completes the step only in the current browser process; reload or a new process presents it again.
**The durable `ui-onboarding` section owns acknowledgement.** The Host half registers it in the user-settings seam under the active `$DSH_HOME/settings.yaml`; the current welcome store reads and writes `welcomeNoticeVersion` through the existing public settings API. The connection plugin publishes whether the current page uses a loopback authority as `ctx.connection.isLoopback`; hostname classification remains internal to the connection package, and other client plugins consume the service state instead of importing its implementation. The API proxy exposes this one product namespace through a closed allowlist beside configurable-provider namespaces, without treating its changes as model-catalog invalidations.
**Concurrent loopback views converge without stale replacement.** The acknowledgement write omits `expectedRevision` deliberately: every loopback tab writes the same version to one path, so the operation is idempotent and preserves sibling fields instead of rebuilding the section. `settings/document-updated` reaches the client as an invalidation — through `host/settings-changed` then, and forwarded verbatim now ([forwarded Remote events](../architecture/2026-08-10-remote-event-delivery.md)); an already mounted loopback tab refetches and advances when another tab or an external editor commits the current version. The API proxy exposes this one product namespace through a closed allowlist beside configurable-provider namespaces, without treating its changes as model-catalog invalidations.
**Onboarding temporarily owns the viewport as one continuous stage.** A solid product surface replaces the complete application view through a body-level portal and marks the underlying app root inert; the exact required mask remains mounted behind that surface with `position:absolute`, zero left/right/bottom offsets, `top:80px`, `rgba(0, 0, 0, 0.24)`, and `backdrop-filter: blur(2px)`. Welcome and conditional credential setup render as successive pages in this stage instead of independent modals. Both pages reuse the Web UI's black `BrandWordmark`. The welcome page preserves the four authored paragraphs verbatim under the `内测声明` title; every paragraph uses one 16/28 body scale, and only the requested action clause inside the final paragraph receives a subtle 500 weight. A short staggered opacity/vertical entrance supplies pacing without blocking interaction and disappears under reduced motion. The title receives initial focus, Continue is the sole button, and no close, Escape, or mask-click path exists.
**Visible onboarding uses one shared modal contract.** Both current steps render through the same body-portaled `OnboardingModal`, and the underlying app root stays inert only while a dialog is visible. The shell renders no wrapper while a step loads its private facts. Explicit actions transfer coordinator ownership; Escape and mask clicks do not acknowledge or skip a step.
## Alternatives considered
@@ -32,4 +30,4 @@ The GUI's credential onboarding begins with a DeepSeek-specific readiness check,
## Consequences
A fresh profile always sees the welcome notice before provider-specific onboarding; an already configured credential skips only the later DeepSeek step. On loopback, reloading after Continue stays past the acknowledged version, changing the owner version presents it again, and closing before Continue leaves the next launch unchanged. On non-loopback, Continue advances the live process without a privileged settings request and reload presents the notice again. Focused store and React tests pin both persistence modes, exact-version comparison, write failure, sole-action behavior, no-dismiss paths, coordinator ordering, conditional DeepSeek transfer, and HMR cleanup. The real Chromium scenario boots the shipped Web composition with an isolated harness home, verifies the exact mask geometry and computed styles, reloads before and after acknowledgement, continues into missing-credential setup, confirms an acknowledged-version mismatch returns while the credential is configured, and checks the browser console.
A fresh profile sees the current testing-stage notice, then the conditional DeepSeek key dialog when no provider is usable. Focused store and React tests pin exact-version acknowledgement, coordinator ordering, conditional transfer, shared modal behavior, and HMR cleanup. The real Chromium scenario boots the shipped Web composition with an isolated harness home, verifies both dialogs, writes the key through the existing credential boundary, and checks that no secret reaches the DOM, ARIA, or browser console.
@@ -10,15 +10,13 @@ GUI 的凭据引导从 DeepSeek 专用的就绪状态检查开始,但内部测
## 决策
**设置外壳协调有序步骤。** `settings.onboarding` 仍是根作用域 list,但 `ui-settings` 会把其中各条目的 id 和顺序投影到一个协调器中,并且只挂载第一个未完成的步骤。当前注册方会收到 `complete()``openSection(id)`;所有权转移前,不会挂载后续步骤。产品欢迎步骤的顺序为 `-100``ui-settings-models` 则只保留顺序 `0` 的 DeepSeek 条件式就绪状态与凭据跳转步骤
**设置外壳协调有序步骤。** `settings.onboarding` 仍是根作用域 list,但 `ui-settings` 会把其中各条目的 id 和顺序投影到一个协调器中,并且只挂载第一个未完成的步骤。当前注册方会收到 `complete()``openSection(id)`;所有权转移前,不会挂载后续步骤。`ui-settings-models` 现在以顺序 `-100` 注册恢复后的欢迎声明,以顺序 `0` 注册 DeepSeek 条件式凭据步骤;两者当前的共用展示由[共用弹窗引导决策](2026-08-13-shared-modal-product-onboarding.md)持有
**不属于单一功能的产品引导由 `ui-settings-general` 持有。** `src/onboarding-copy.ts` 是完整通知、「继续」按钮文案和 `WELCOME_NOTICE_VERSION` 的唯一可编辑来源;GUI 支持的两种 locale 都有意渲染同一份中文所有者文案。运行时 locale 字典从该文件派生欢迎文案,测试也导入同一个所有者,而不重复段落文本。该通知只存在于浏览器 UI:它不会创建会话事件,也不会贡献任何模型可见内容。通知说明会话遥测[默认禁用](2026-08-10-telemetry-default-off.md),并列出 `FEEDBACK_ONLY``FULL` 两种显式启用模式
**产品欢迎步骤按版本管理并归功能插件所有。** 该声明曾由[移除首次启动内测声明](../simplification/2026-08-13-remove-first-run-beta-notice.md)历史决策移除,现在以新的测试阶段文案恢复在 `ui-settings-models` 中。`ui-settings-general` 仍不注册任何引导步骤;持有当前两个步骤的插件也持有文案、store 和共用弹窗
**loopback 确认状态按 Harness profile 持久化。** 宿主端在 user-settings seam 中注册 `ui-onboarding` 分节,并存入当前 `$DSH_HOME/settings.yaml`。connection 插件通过 `ctx.connection.isLoopback` 统一发布当前页面是否使用 loopback authorityhostname 判定留在 connection 包内,其他客户端插件只消费服务状态,而不导入其实现。除非 `welcomeNoticeVersion` 与文案所有者文件中的常量精确相等,否则 loopback 浏览器会显示通知。「继续」会以当前版本执行一次路径变更,并且仅在宿主端提交成功后调用 `complete()`;写入失败时通知保持打开,关闭页面或进程则不会写入任何内容。更新该常量会有意要求每个 profile 对修订后的文案重新确认一次。非 loopback 浏览器不能调用仅限 loopback 的 settings API;它仍显示同一通知,但显式点击「继续」只会在当前浏览器进程中完成该步骤,重新加载或新进程会再次显示通知
**持久化的 `ui-onboarding` 分节持有确认状态。** 宿主端在 user-settings seam 中注册它,存入当前 `$DSH_HOME/settings.yaml`;当前欢迎 store 通过既有公开 settings API 读写其中的 `welcomeNoticeVersion`。connection 插件通过 `ctx.connection.isLoopback` 统一发布当前页面是否使用 loopback authorityhostname 判定留在 connection 包内,其他客户端插件只消费服务状态,而不导入其实现。API Proxy 在可配置提供方 namespace 之外,通过封闭的允许列表暴露这一个产品 namespace,同时不会把它的变更视为模型目录失效事件
**并发 loopback 视图无需陈旧的整体替换即可收敛。** 确认写入有意省略 `expectedRevision`:每个 loopback 标签页都向同一路径写入相同版本,因此该操作是幂等的,并会保留同级字段,而不是重建整个分节。`settings/document-updated` 以失效通知形式到达客户端——当时经 `host/settings-changed`,现在则是原样转发([转发的 Remote 事件](../architecture/2026-08-10-remote-event-delivery.md));另一个标签页或外部编辑器提交当前版本后,已挂载的 loopback 标签页会重新拉取状态并推进。API Proxy(`@deepseek-ai/dsh-host-apiproxy`)在可配置提供方 namespace 之外,通过封闭的允许列表暴露这一个产品 namespace,同时不会把它的变更视为模型目录失效事件
**引导流程会暂时接管视口,形成一个连续阶段。** 纯色产品界面通过挂载到 `body` 的 portal 取代完整的应用视图,并将底层应用根节点标记为 inert;严格符合要求的遮罩仍挂载在该界面后方,并保留 `position:absolute`、left/right/bottom 偏移量为零、`top:80px``rgba(0, 0, 0, 0.24)``backdrop-filter: blur(2px)`。欢迎页和按条件显示的凭据设置页在这一阶段中依次呈现,而不是各自作为独立的模态窗口。两个页面都复用 Web UI 的黑色 `BrandWordmark`。欢迎页在 `内测声明` 标题下逐字保留既定的四段文案;所有段落统一采用 16/28 的正文字号与行高,只有最后一段中指定的行动语句使用较为克制的 500 字重。短暂的错落式透明度与纵向位移动画营造出节奏感,但不会阻碍交互,并会在用户启用减少动态效果时禁用。初始焦点落在标题上,「继续」是唯一按钮,且不存在关闭、Escape 或点击遮罩的退出路径。
**可见引导使用同一个弹窗契约。** 当前两个步骤都通过 body portal 的同一个 `OnboardingModal` 渲染,且只在弹窗可见期间把下层应用根节点设为 inert。步骤加载私有事实时,外壳不渲染任何包装。明确操作会移交协调器所有权;Escape 和点击遮罩都不会确认或跳过步骤
## 曾考虑的替代方案
@@ -32,4 +30,4 @@ GUI 的凭据引导从 DeepSeek 专用的就绪状态检查开始,但内部测
## 后果
全新 profile 始终会在提供方专用引导之前看到欢迎通知;凭据已经配置时,只会跳过后续 DeepSeek 步骤。在 loopback 上,点击「继续」后重新加载不会再次显示已确认版本,更改文案所有者文件中的版本值会让通知重新出现,而确认前关闭窗口不会改变下次启动。在非 loopback 上,「继续」会在不发起受保护 settings 请求的情况下推进当前进程,重新加载则再次显示通知。针对性的 store 与 React 测试固化了两种持久化模式、精确版本比较、写入失败、单一操作、不可关闭路径、协调器顺序、条件移交 DeepSeek 步骤和 HMR(热模块替换)清理行为。真实 Chromium 场景会使用隔离的 harness 家目录启动随产品提供的 Web 组合,验证遮罩的精确几何尺寸和计算样式,在确认前后分别重新加载,继续进入凭据缺失设置流程,确认凭据已配置时确认版本不匹配仍会使通知重新出现,并检查浏览器控制台。
全新 profile 会先看到当前测试阶段声明;当没有任何可用提供方时,再看到条件式 DeepSeek 密钥弹窗。定向 store 与 React 测试固定精确版本确认、协调器顺序、条件移交、共用弹窗行为与 HMR 清理。真实 Chromium 场景会隔离的 harness 家目录启动已发布 Web 组合,验证两个弹窗,通过既有凭据边界写入密钥,并检查 secret 未进入 DOM、ARIA 或浏览器控制台。
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-02-session-search-not-shipped-default.md
2026-08-02-session-search-not-shipped-default.md: c1bfd7f8e354a4480c5635619514fe782ea71d2c
2026-08-02-session-search-not-shipped-default.zh.md: 9b80c549425c26055700480dd57f1a0a7d01e4a8
2026-08-02-session-search-not-shipped-default.md: 60d1f424d5a9029a00b3f195d1f6e6baf2790d1e
2026-08-02-session-search-not-shipped-default.zh.md: 221613ae1aef93cd72734e4c328d822a88b64227
@@ -12,7 +12,7 @@ The [shipped-roster decision](2026-07-31-even-out-shipped-tool-rosters.md) made
The shipped TUI, Web, and headless surfaces do not mount `@deepseek-ai/dsh-tool-session-query`, and no shipped agent preset carries it. The consumer stays opt-in exactly as the model-facing-session-query-tools note describes: the ACP example's [`session-query.cordis.yml`](../../../../examples/acp-agent/session-query.cordis.yml) and its snapshot counterpart remain the mounted reference, and a custom composition can mount the package with the timeout and spill policies.
The `ctx.sessionQuery` service itself stays mounted. `session-query-sqlite` remains a base row — the TUI's `session-reference` consumes it for `/resume`and the Web overlay keeps patching it to an in-memory index for the browser content search. Only the model-facing consumer is removed.
The `ctx.sessionQuery` service itself stays mounted. `session-query-sqlite` remains a base row — the TUI's `session-reference` consumes it for `/resume`with its full-text index off by default (`openAt: never`; the [content-search opt-in decision](../architecture/2026-08-13-session-content-search-opt-in.md)), and the Web overlay keeps its in-memory values for deployments that enable content search. Only the model-facing consumer is removed.
## Alternatives considered
@@ -12,7 +12,7 @@ Status: implemented
交付的 TUI、Web 与无头 surface 均不挂载 `@deepseek-ai/dsh-tool-session-query`,交付的 agent preset 也都不包含它。该消费方仍保持 opt-in,与面向模型的会话查询工具决策所述完全一致:ACPAgent Client Protocol)示例的 [`session-query.cordis.yml`](../../../../examples/acp-agent/session-query.cordis.yml) 及其快照对侧文件仍是挂载参考,自定义组合也可以连同超时与 spill 策略一起挂载该包。
`ctx.sessionQuery` 服务本身保持挂载。`session-query-sqlite` 仍是 base 的一行,TUI 的 `session-reference` 消费它来实现 `/resume`Web overlay 也继续把它 patch 成内存索引,供浏览器内容搜索使用。被移除的只有面向模型的消费方。
`ctx.sessionQuery` 服务本身保持挂载。`session-query-sqlite` 仍是 base 的一行,TUI 的 `session-reference` 消费它来实现 `/resume`;其全文索引默认关闭(`openAt: never`,见[内容搜索 opt-in 决策](../architecture/2026-08-13-session-content-search-opt-in.md)),Web overlay 保留内存索引取值,供启用内容搜索的部署使用。被移除的只有面向模型的消费方。
## 曾考虑的替代方案
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.md
2026-08-04-claude-code-and-codex-subagent-backends.md: ccc96d6c998c4ab958a7eea1e502d036d16ec90d
2026-08-04-claude-code-and-codex-subagent-backends.zh.md: 5facbe79e7afccd9eccddac046cd97a60b50d25a
2026-08-04-claude-code-and-codex-subagent-backends.md: 666945c4d8039874729a7f9da34d9cf82bfd479d
2026-08-04-claude-code-and-codex-subagent-backends.zh.md: 35b8dd51f1c45200a495c68f19f055a224123263
@@ -12,24 +12,25 @@ The product integrations must not become second owners for task text, cwd, cance
## Decision
The harness publishes two sibling one-shot provider packages: `codex` and `claude-code`. This note owns their product protocols, result mapping, and process lifecycle; the [shared-profile-host placement decision](../architecture/2026-08-10-product-subagent-providers-in-shared-host.md) supersedes the original opt-in composition placement. Loading either provider starts no product process, and each tool accepts only a standalone text task; product selection and background execution are not model arguments.
The harness publishes two sibling one-shot provider packages: `codex` and `claude-code`. This note owns their product protocols, result mapping, and process lifecycle; the [production-install exclusion decision](../simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.md) owns their explicit Profile installation and host-plane placement, and the [product one-shot background decision](2026-08-12-product-subagent-one-shot-background-tasks.md) owns the model-visible scheduling choice. Loading either provider starts no product process, and each tool accepts only a standalone text task; product selection remains deployment configuration.
Both providers report `inheritsParentContext: false`, advertise no optional start capabilities, and pass the parent Session cwd without copying the parent conversation. Their documented tools disable background execution and use `maxDepth: 'provider-managed'`, leaving recursion policy with the out-of-process product instead of sending a limit the provider cannot enforce. Every call creates a fresh product process and a non-resumable product conversation. The shared subagent service continues to own request resolution, lifecycle events, result settlement, and foreground collection; the shared subprocess service owns credential scrubbing, process-tree termination, and whole-tree exit observation.
Both providers report `inheritsParentContext: false`, advertise no optional start capabilities, and pass the parent Session cwd without copying the parent conversation. Their documented tools use `backgroundMode: 'one-shot'` and `maxDepth: 'provider-managed'`: the consumer keeps foreground collection as the default and may place the same run in the generic Job runtime, while recursion policy stays with the out-of-process product. Every call creates a fresh product process and a non-resumable product conversation. `ctx.subagents` owns named-request resolution and paired lifecycle events; `dsh-tool-subagent` owns model-visible scheduling and foreground-versus-Job adaptation; `ctx.jobs` and `dsh-tool-jobs` own Job ids, state, output, controls, notices, and parent-owner cancellation; each product provider owns native result mapping, while `dsh-subprocess` owns credential scrubbing, process-tree termination, and whole-tree exit observation.
```text
fixed tool → shared subagent service → product provider → official product process
← final answer / explicit error / cancellation ← terminal product fact
→ foreground disposal → shared process-tree termination → whole-tree exit
fixed tool -> dsh-tool-subagent -> ctx.subagents -> product provider -> product process
foreground <- final product outcome
background -> ctx.jobs / dsh-tool-jobs -> Job id / state / notice / controls
both -> provider disposal -> dsh-subprocess -> whole-tree exit
```
### Ownership and lifecycle
| Phase | Shared owner | Product-specific responsibility | Observable result |
| Layer | Owner | Responsibility | Observable result |
| --- | --- | --- | --- |
| Resolve | `dsh-tool-subagent` and `ctx.subagents` | Validate the product's text-only input and derive native startup parameters | Unsupported context or malformed input fails before a run is published |
| Start | `dsh-subprocess` owns every acquired process tree | Reach the smallest native point at which the product conversation and process can both be controlled | `start()` publishes one existing `SubagentRun`, or cleans up and rejects |
| Run | The product owns its native protocol facts; the holder owns their mapping | Submit exactly one task and derive an existing shared stop reason; Codex uses `max-tokens` only for explicit context exhaustion | The parent receives only a final answer or an explicit failure |
| Dispose | The foreground consumer requests release; `dsh-subprocess` proves exit | Close the native protocol and express any best-effort native cancellation | Disposal is idempotent and returns only after the whole process tree exits |
| Delegation lifecycle | `ctx.subagents` | Resolve the named provider request and pair lifecycle events around the published `SubagentRun` | Unsupported context or malformed input fails before a run is published; start and terminal events remain paired |
| Scheduling and adaptation | `dsh-tool-subagent` | Interpret `run_in_background`, choose foreground collection or one-shot Job registration, and map the shared stop reason | Foreground returns the product outcome; background returns a Job id after registration |
| Job state and control | `ctx.jobs` and `dsh-tool-jobs` | Own Job state, output, cancellation, owner cleanup, completion notices, and model-facing controls | The exact parent can collect, list, or stop background work and receives its completion notice |
| Native run and teardown | Product provider and `dsh-subprocess` | Produce one native result, close the product protocol, request best-effort native cancellation, and prove process-tree exit | Foreground return and Job settlement both wait for idempotent disposal and whole-tree exit |
## Codex provider
@@ -59,7 +60,7 @@ The credentialed Claude Code e2e uses the official DeepSeek Claude Code contract
## Distribution and evidence
Each product owns branch-complete package tests, a required keyless real-product spec, a Loader composition e2e, and a credentialed DeepSeek e2e. The keyless product tier uses the exact official distribution under test, a non-empty fake product key, an isolated temporary workspace and product home, and a loopback fixed-answer model. Missing product requests, wrong authentication, altered task text, a non-exact answer, a skipped real product, or a surviving managed handle fails the required test. The Loader tier boots the README-shaped user configuration, verifies both fixed foreground-only tools in one context, and starts neither product process. The credentialed tier starts the same production provider and real product with a runtime-only key, requires a unique nonce from the fixed official DeepSeek service, and proves quiescence again; it self-skips only when a local operator supplied no key, while trusted CI preflights the secret.
Each product owns branch-complete package tests, a required keyless real-product spec, a Loader composition e2e, and a credentialed DeepSeek e2e. The keyless product tier uses the exact official distribution under test, a non-empty fake product key, an isolated temporary workspace and product home, and a loopback fixed-answer model. Missing product requests, wrong authentication, altered task text, a non-exact answer, a skipped real product, or a surviving managed handle fails the required test. The Loader tier boots the README-shaped explicit Profile configuration, verifies both fixed one-shot tools expose optional background scheduling alongside generic Job controls, and starts neither product process. The credentialed tier starts the same production provider and real product with a runtime-only key, requires a unique nonce from the fixed official DeepSeek service, and proves quiescence again; it self-skips only when a local operator supplied no key, while trusted CI preflights the secret.
The Codex evidence pins `@openai/codex@0.147.0` and `codex-cli 0.147.0`. Its real-product spec observes the exact Bearer key, original task, byte-exact final answer, unattended command rejection with no file side effect, local cancellation, and whole-tree exit. Production still supplies `codex` on `PATH`.
@@ -83,12 +84,12 @@ The project owner's distribution authorization is scoped to the official `@anthr
**Plugin-managed login, product home, models, settings, or permissions.** Those choices would create another authority beside each product's native configuration and enlarge a one-shot provider into account management. The providers expose only an explicit environment overlay and teardown grace; unattended interaction fails closed.
**Continuation, progress, background collection, and shared parent context.** The delivered user result is one self-contained task and one final answer. Product sessions, resume, follow-up, intermediate messages, parent transcript transfer, structured output, and background collection need separate user contracts and are not prebuilt.
**Continuation, progress, product-native background state, and shared parent context.** The provider payload remains one final answer for one self-contained task. The generic Job layer may add its id, status, notice, collection, and cancellation results, but product sessions, resume, follow-up, intermediate messages, parent transcript transfer, structured output, and provider-specific background state need separate user contracts and are not prebuilt.
## Consequences
Users delegate through two stable foreground tools backed by the official product integrations. Their Profile placement and per-Preset exposure are owned by the [shared-host placement decision](../architecture/2026-08-10-product-subagent-providers-in-shared-host.md); this note's provider lifecycle keeps native settings and behavior while shared services retain the sole ownership of task settlement and process-tree quiescence.
Users delegate through two stable one-shot tools backed by the official product integrations. Explicit Profile installation and host-plane provider placement are owned by the [production-install exclusion decision](../simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.md); per-Preset tool exposure and foreground-default optional Job scheduling are owned by the [product one-shot background decision](2026-08-12-product-subagent-one-shot-background-tasks.md). This note's provider lifecycle keeps native settings and behavior while shared services retain the sole ownership of job settlement and process-tree quiescence.
Every delegation pays for a fresh product process and independent model context, and only final text reaches the parent. Product-native configuration makes behavior depend on the deployment's installed product, account state, and workspace settings. Credentialed e2e runs also spend external API quota and depend on the official DeepSeek endpoint; deterministic protocol, failure, cancellation, and approval coverage remains in the keyless tier. The providers do not resume sessions, stream progress, accept new human interaction, roll back tool or file side effects, or impose a wall-clock timeout.
Every delegation pays for a fresh product process and independent model context. The product payload reaching the parent is final text only; background scheduling additionally exposes generic Job ids, status, completion notices, and collection or cancellation results. Product-native configuration makes behavior depend on the deployment's installed product, account state, and workspace settings. Credentialed e2e runs also spend external API quota and depend on the official DeepSeek endpoint; deterministic protocol, failure, cancellation, and approval coverage remains in the keyless tier. The providers do not resume sessions, stream progress, accept new human interaction, roll back tool or file side effects, or impose a wall-clock timeout.
Compatibility is pinned by package-level unit coverage, keyless real-product loopback tests, credentialed DeepSeek nonce tests, public Loader composition, built-package and NodeNext consumer checks, generated documentation and notices, and the repository CI matrix. A supported product or DeepSeek endpoint/model baseline change must refresh those facts; production performs no separate runtime version probe.

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