Merge branch 'master' into worktree/python-sdk-model-visible-assertions
This commit is contained in:
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-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 @@ ACP(Agent 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 是能力 seam(Service 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 是能力 seam(Service 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 seam(Service 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 seam(Service 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-14-provider-routed-llm-adapters.md
|
||||
2026-07-14-provider-routed-llm-adapters.md: e1eaf52f21481a7c65e85effb7607b16f9b0ffdd
|
||||
2026-07-14-provider-routed-llm-adapters.zh.md: 4e620408dabffc8368293635613afb5778b6e822
|
||||
2026-07-14-provider-routed-llm-adapters.md: 78c8d6788006c503b532ff2bbddd30342415f0a4
|
||||
2026-07-14-provider-routed-llm-adapters.zh.md: 5e73cab5f1f2b1296c9a486d1c833e95bb5674a0
|
||||
|
||||
@@ -40,9 +40,9 @@ pi-ai's common stream options do not expose stop sequences. `dsh-llm-pi-ai` reje
|
||||
|
||||
Assistant messages carry the request's `provider` and `model`, plus an optional JSON-serializable adapter replay state. A successful `assistant/message` session event records those fields and `deriveMessages()` returns them with the assistant message. User, system, context, and tool-result messages carry no assistant route fields. The provider/model fields are authoritative loop data; an adapter owns only its opaque replay-state payload.
|
||||
|
||||
A terminal successful `finish` chunk may carry replay state, and `BlockAssembler` retains it alongside usage and finish reason. The loop attaches that state to the assembled assistant message's model source without exposing a response-rewrite hook. Error and aborted responses do not produce a normal assistant message and therefore do not enter future model history.
|
||||
A terminal successful `finish` chunk may carry replay state as a `ReplayEnvelope`: opaque response-level metadata plus optional per-block entries aligned with the emitted block sequence. `BlockAssembler` makes one keep/drop decision for content and metadata — when max-token assembly drops a tool call, the envelope loses the entry at the same position — so the state the loop attaches to the assembled assistant message's model source always describes the stored blocks, per the [max-token replay-state alignment decision](../bug-fix/2026-08-15-max-token-replay-state-alignment.md). The loop exposes no response-rewrite hook. Error and aborted responses do not produce a normal assistant message and therefore do not enter future model history.
|
||||
|
||||
The pi-ai replay state is a versioned, minimal projection of its successful `AssistantMessage`: source API/provider/model, response id/model, stop reason, and index-aligned text, thinking, and tool-call signatures. It does not duplicate text or tool arguments already carried by Harness content blocks, and it omits diagnostics, timestamps, usage, and errors. On a later request, `LlmRuntime` gives replay state to the target adapter only when the historical provider and target provider are currently owned by the same adapter instance. That adapter combines the logged Harness content with replay state when it can restore the historical response, and owns any required cross-model or cross-provider conversion. An adapter receiving replay state with an unknown version or mismatched block shape fails explicitly; a different adapter receives only provider-neutral content plus provider/model fields.
|
||||
The pi-ai replay state fills that envelope with a versioned, minimal projection of its successful `AssistantMessage`: a response half (source API/provider/model, response id/model, stop reason) and per-block text, thinking, and tool-call signatures. It does not duplicate text or tool arguments already carried by Harness content blocks, and it omits diagnostics, timestamps, usage, and errors. On a later request, `LlmRuntime` gives replay state to the target adapter only when the historical provider and target provider are currently owned by the same adapter instance. That adapter combines the logged Harness content with replay state when it can restore the historical response, and owns any required cross-model or cross-provider conversion. Durable content stays authoritative: an adapter receiving replay state it cannot use — an unknown kind or version, malformed metadata, or a block shape that no longer matches the content — degrades that message to provider-neutral conversion with a diagnostic; a different adapter receives only provider-neutral content plus provider/model fields.
|
||||
|
||||
This state is model-visible replay input and therefore follows the existing [reconstructable-request rule](2026-07-05-reconstructable-requests.md): it is present in both the terminal `finish` chunk and the assembled `assistant/message` model source that drives derivation. Resume and fork preserve it verbatim. Compaction that shadows the assistant message also removes its replay state from the active surface; the summary is ordinary provider-neutral content.
|
||||
|
||||
|
||||
@@ -40,9 +40,9 @@ pi-ai 的通用流选项不支持停止序列。若 Harness `stop` 选项已定
|
||||
|
||||
助手消息携带请求的 `provider` 和 `model`,以及可选的 JSON 可序列化适配器回放状态。成功的 `assistant/message` 会话事件记录这些字段,`deriveMessages()` 返回助手消息时也会包含它们。用户、系统、上下文与工具结果消息不携带助手路由字段。提供方/模型字段是 agent loop 的权威数据;适配器仅拥有其不透明回放状态 payload。
|
||||
|
||||
成功的终止 `finish` 分片可以携带回放状态,`BlockAssembler` 会将其与 token 用量和结束原因一起保留。agent loop 会把该状态附加到已组装助手消息的模型来源中,但不公开响应改写钩子。错误或中止响应不会生成正常助手消息,因此不会进入后续模型历史。
|
||||
成功的终止 `finish` 分片可以以 `ReplayEnvelope` 形式携带回放状态:不透明的响应级元数据,加上与发射块序列对齐的可选逐块条目。`BlockAssembler` 对内容与元数据只做一次保留/丢弃决定——max-token 组装丢弃工具调用时,数据同一位置的条目一并丢弃——因此 agent loop 附加到已组装助手消息模型来源中的状态始终描述存储的块,见 [max-token 回放状态对齐决定](../bug-fix/2026-08-15-max-token-replay-state-alignment.md)。agent loop 不公开响应改写钩子。错误或中止响应不会生成正常助手消息,因此不会进入后续模型历史。
|
||||
|
||||
pi-ai 回放状态是其成功 `AssistantMessage` 的带版本最小投影,包含源 API/提供方/模型、响应 ID/模型、停止原因,以及按索引对齐的文本签名、thinking 签名和工具调用签名。它不会重复 Harness 内容块中已有的文本或工具参数,也不包含诊断信息、时间戳、用量或错误。后续请求中,只有历史提供方和目标提供方当前归同一个适配器实例所有时,`LlmRuntime` 才会把回放状态交给目标适配器。适配器在能够恢复历史响应时,将 Harness 记录的内容与回放状态组合,并负责所需的跨模型或跨提供方转换。适配器收到未知版本或块形状不匹配的回放状态时会显式失败;其他适配器只能收到提供方无关的内容以及提供方/模型字段。
|
||||
pi-ai 回放状态用其成功 `AssistantMessage` 的带版本最小投影填充该结构:一个响应半区(源 API/提供方/模型、响应 ID/模型、停止原因),以及逐块的文本签名、thinking 签名和工具调用签名。它不会重复 Harness 内容块中已有的文本或工具参数,也不包含诊断信息、时间戳、用量或错误。后续请求中,只有历史提供方和目标提供方当前归同一个适配器实例所有时,`LlmRuntime` 才会把回放状态交给目标适配器。适配器在能够恢复历史响应时,将 Harness 记录的内容与回放状态组合,并负责所需的跨模型或跨提供方转换。持久化内容保持权威:适配器收到无法使用的回放状态——未知 kind 或版本、格式错误的元数据、或与内容不再匹配的块结构——会把该消息降级为提供方无关转换并带出诊断;其他适配器只能收到提供方无关的内容以及提供方/模型字段。
|
||||
|
||||
该状态属于模型可见的回放输入,因此遵循现有的[请求可重建规则](2026-07-05-reconstructable-requests.md):它同时存在于终止 `finish` 分片和驱动派生的已组装 `assistant/message` 模型来源中。恢复和 fork 会原样保留该状态。压缩(compaction)遮蔽助手消息时,也会从活动 surface 中移除其回放状态;摘要属于普通的提供方无关内容。
|
||||
|
||||
|
||||
@@ -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-30-config-plane-boundaries.md
|
||||
2026-07-30-config-plane-boundaries.md: 2dfb527749e8dea54e037abc602af1611c3f9794
|
||||
2026-07-30-config-plane-boundaries.zh.md: 8b1ec3206237b7c2deafb743b62e697b3c452ffc
|
||||
2026-07-30-config-plane-boundaries.md: aab0d7a80c2732b33637ec721a9ab7c1b50fce3f
|
||||
2026-07-30-config-plane-boundaries.zh.md: fb069e6dd9f276c7cc0069695b388ef37a081ab4
|
||||
|
||||
@@ -6,6 +6,8 @@ English | [中文](2026-07-30-config-plane-boundaries.zh.md)
|
||||
|
||||
> Scope: boundary hardening of the [web configuration plane](2026-07-30-web-config-plane.md) — which namespaces reach the wire, which callers reach them, and how an editor holding a partial, possibly stale view writes without destroying what it cannot see.
|
||||
|
||||
> The caller boundary, the redaction, and the revision fencing remain current. Restricting which namespaces reach the wire to the configurable-provider directory is superseded by the [plugin-owned settings surface](2026-08-12-plugin-owned-settings-surface.md), which serves every registered namespace.
|
||||
|
||||
## Problem
|
||||
|
||||
The plane worked and was reachable by more callers, and with more authority, than its design claimed.
|
||||
|
||||
@@ -6,6 +6,8 @@ Status: implemented
|
||||
|
||||
> 范围:对 [Web 配置面](2026-07-30-web-config-plane.md)的边界加固——哪些 namespace 能抵达协议、哪些调用方能抵达它们,以及一个只持有局部且可能陈旧的视图的编辑器该如何写入,才不会毁掉它看不见的东西。
|
||||
|
||||
> 调用方边界、脱敏与 revision 设栅依然有效。把「哪些 namespace 能抵达协议」限制为可配置提供方目录这一条,已被[由插件自己拥有的设置表层](2026-08-12-plugin-owned-settings-surface.md)取代——后者服务每一个已注册的 namespace。
|
||||
|
||||
## 问题
|
||||
|
||||
这个面能用,但能触达它的调用方、以及它们所拥有的权限,都比设计声称的更多。
|
||||
|
||||
@@ -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 发生改动后的代际隔离。无密钥 ACP(Agent Client Protocol)快照固定单个产品与两个产品同时启用时的模型可见工具 schema,提供方测试则另行证明原生可执行文件解析、失败、取消和进程树完全停稳。
|
||||
base bundle 测试证明生产 `dsh-base` 既不包含产品提供方依赖,也不包含提供方配置行。Web 组装显式挂载两个可选提供方,并覆盖不暴露任何工具、仅暴露 Codex、仅暴露 Claude 和同时暴露两者这四种工具集合,也覆盖自行创作的 preset 发生改动后的代际隔离。由包负责的 Loader 组装证明 Codex-only 与双提供方按需启用路径会注册选中的提供方,而不会启动产品进程。无密钥 ACP(Agent 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 可选载荷的安装成本。
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-11-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
|
||||
@@ -0,0 +1,386 @@
|
||||
# Agent Note: Repository naming contract and pre-release rename ledger
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-08-11-repository-naming-contract-and-rename-ledger.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
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 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.
|
||||
|
||||
The last pre-release window made repository-wide renames cheap. Keeping weak names would have turned accidental vocabulary into a compatibility contract.
|
||||
|
||||
## Decision
|
||||
|
||||
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 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.
|
||||
|
||||
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. 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.
|
||||
|
||||
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. They carry the current factual names. The three partially superseded decisions link back to this decision.
|
||||
|
||||
### Name the role that exists
|
||||
|
||||
Use a common, concrete noun. Name the stable responsibility, not the first implementation, the current folder, or a possible future expansion. Do not add a word that carries no information. Do not shorten a name by deleting the word that distinguishes its scope.
|
||||
|
||||
An interface package names the capability. An implementation package adds the mechanism, protocol, environment, or vendor that distinguishes that implementation. Use `local` only when same-host execution is part of the contract. Do not use it for a provider that happens to read local-looking paths through replaceable `ctx.fs` or starts work through replaceable `ctx.subprocess`.
|
||||
|
||||
Use a singular `ctx` key for one engine, runtime, policy, controller, resolver, store, or current configuration. Use a plural key for a registry or a service that owns multiple named members. The class role and key number must agree. A plural key does not by itself make an object a registry; its operations and ownership do. Do not reuse one Cordis `Context` key for incompatible host and client declarations. TypeScript declaration merging sees both faces even when they use separate runtime contexts. Add the role suffix when the natural plural already belongs to another face.
|
||||
|
||||
Use `Service` only when no sharper role is honest. `GoalService` and `SessionTitleService` are valid retained names because each owns a domain service whose work is not accurately reduced to storage, registration, or one execution mechanism.
|
||||
|
||||
### Role words are contracts
|
||||
|
||||
| Word | Use it when | Do not use it when |
|
||||
|---|---|---|
|
||||
| `Controller` | The object accepts commands or user intent and changes one existing domain or presentation state. It coordinates a bounded state transition. | The object executes arbitrary work, owns a provider fleet, or only converts values for display. |
|
||||
| `Store` | The object owns one data set and mainly provides create, read, update, delete, snapshot, or subscription operations for that data. | It validates a state machine, arbitrates authority, dispatches work, owns provider precedence, or coordinates several domains. A map inside a class does not make the class a store. |
|
||||
| `Directory` | The object exposes entries for discovery or selection. Its consumer asks what choices exist and reads their metadata. | Producers register arbitrary implementations into it, or callers execute work through it. A directory can be backed by a registry, but the two faces are not the same. |
|
||||
| `Presenter` | The object is a pure conversion from domain values or tool arguments to render intent. It has no I/O, subscription, mutation, or lifecycle ownership. | It reads services, changes state, or controls when work runs. Those jobs belong to a controller or runtime. |
|
||||
| `Registry` | The object owns a dynamic set of named registrations. It defines lookup, duplicate or precedence rules, registration lifetime, and disposal. | The main caller contract is dispatch, execution, cancellation, policy enforcement, or orchestration. A runtime can contain a registry as an internal part. |
|
||||
| `Runtime` | The object runs live work. It owns dispatch, cancellation, provider coordination, or operation lifecycle across calls. | The object only stores records, returns a catalog, resolves one value, or holds configuration. `Runtime` is not a generic replacement for `Service`. |
|
||||
| `Resolver` | The object computes or locates one answer from supplied inputs, usually without owning the answer's lifecycle. | It owns a mutable collection or a long-running execution lifecycle. |
|
||||
| `Binder` | The object attaches one declared interface to the caller's context or lifecycle and returns the bound value. | It owns the bound value as a collection, controls its domain state, or merely converts data. |
|
||||
| `Engine` | The object implements a domain algorithm or stateful execution model, such as workflow, compaction, or query evaluation. | It only selects a provider or forwards a request across a protocol boundary. |
|
||||
| `Policy` | The object decides what is allowed, selected, limited, or observed. | It performs the mechanism that the decision permits. Keep policy and executor names separate. |
|
||||
| `Executor` | The object runs an explicit request or resolved specification in one capability. | It owns a broad application lifecycle or a catalog of providers. |
|
||||
| `Gateway` | The object adapts a process, network, RPC, or API boundary and translates between the two sides. | It only registers same-process services or stores metadata. |
|
||||
| `Provider` | The object supplies one implementation of a capability definition. Add a mechanism or vendor qualifier when more than one provider can exist. | It is the capability definition, the registry of providers, or the consumer-facing runtime. |
|
||||
| `Backend` | The object implements a replaceable lower-level persistence, transport, or execution backend behind a defined interface. | It is a user-facing service or only a returned reference to one live object. |
|
||||
| `Handle` | The value is a reference to one live resource and controls or observes that resource. | The object creates and manages the whole resource pool. Do not use `Owner` or the vague `Resource` when `Handle` or a sharper manager role fits. |
|
||||
| `Config` | The object owns one resolved configuration value or one tightly bounded configuration record and its update contract. | It stores a general collection, executes work, or exposes unrelated settings. |
|
||||
| `Service` | The object owns a cohesive domain service whose authority cannot be stated honestly as one of the sharper roles above. | The name is used only because the class extends Cordis `Service`, or because choosing the real role takes more thought. |
|
||||
|
||||
The practical tests are direct. If callers mainly call `register()` and receive a disposer, use `Registry`. If callers mainly call `run()`, `dispatch()`, `cancel()`, or `execute()`, use `Runtime`, `Engine`, or `Executor`. If callers mainly browse choices, use `Directory`. If an object mainly binds one specification to caller-owned context and lifetime, use `Binder`. If the object only maps domain data to UI data, use `Presenter`. If it also changes state, it is not a presenter.
|
||||
|
||||
### Use qualifiers that add information
|
||||
|
||||
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. 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.
|
||||
|
||||
Do not remove an intentional vendor qualifier to avoid repetition. `dsh-subagent-dsh-sdk` names the DeepSeek Harness SDK provider and avoids confusion with another SDK. Its private class becomes `SdkSubagentProvider` because the class also needs to say what it provides.
|
||||
|
||||
### Put the rule in project documentation
|
||||
|
||||
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 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
|
||||
|
||||
| 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. |
|
||||
|
||||
Keep `@deepseek-ai/dsh-sdk-client`, `@deepseek-ai/dsh-sdk-protocol`, and `deepseek-harness-sdk-runtime`. Exclude `@deepseek-ai/create-sdk`, `@deepseek-ai/dsh-scripts`, `@deepseek-ai/dsh-helper`, and `@deepseek-ai/dsh-telemetry`; the separate removal decision deletes them and their support graph.
|
||||
|
||||
### Shell and terminal
|
||||
|
||||
| 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. |
|
||||
| Dialect-neutral `BashExecutor`, `BashExecRequest`, `BashExecSpec`, `BashProcess`, `BashRunResult`, `BashSandboxInfo`, `BashProcessRead`, and `BashProcessStatus` names | Corresponding `Shell*` names | These types cross both Bash and PowerShell implementations. Leaf types that describe Bash syntax or behavior keep `Bash`. |
|
||||
| `BASH_SETTINGS_NAMESPACE`, settings namespace `bash` | `SHELL_SETTINGS_NAMESPACE`, settings namespace `shell` | Both shell providers register this capability-owned settings section. The constant and durable namespace must use the capability name. |
|
||||
| `@deepseek-ai/dsh-bash-env`, `ctx.bashEnv`, `BashEnvRegistry` | `@deepseek-ai/dsh-shell-env`, `ctx.shellEnv`, `ShellEnvRegistry` | The environment registry is shared by Bash and PowerShell tools. |
|
||||
| `docs/subsystems/bash.md` | `docs/subsystems/shell.md` | The subsystem page documents the dialect-neutral capability. |
|
||||
| `packages/pty/` | `packages/terminal/` | The package family owns persistent terminal sessions. Raw PTY allocation remains in the subprocess layer. |
|
||||
| `@deepseek-ai/dsh-pty`, `ctx.pty`, `PtyService` | `@deepseek-ai/dsh-terminal`, `ctx.terminals`, `TerminalSessionService` | Callers manage multiple named terminal sessions. They do not allocate raw PTYs through this service. |
|
||||
| Public high-level `Pty*` session and backend names | `Terminal*` names | The public abstraction is a terminal session. Keep low-level `SubprocessTerminal*` names because they already name the substrate. |
|
||||
| `@deepseek-ai/dsh-pty-local`, `LocalPtyBackend` | `@deepseek-ai/dsh-terminal-bash`, `BashTerminalBackend` | The provider depends on Bash prompt and shell behavior. `local` hides the actual dialect. |
|
||||
| `@deepseek-ai/dsh-tool-pty` | `@deepseek-ai/dsh-tool-terminal` | The model-facing tools are already `terminal_*`; the package should use the same product noun. |
|
||||
| `tool-bash-persistent` in the former PTY family | `shell/tool-bash-persistent/` | The tool is a Bash tool and belongs with shell tools. Keep its npm name: `persistent` distinguishes it from one-shot `bash`, while `bash-terminal` would blur the product tool with the terminal-session family. |
|
||||
| `docs/subsystems/pty.md` | `docs/subsystems/terminal.md` | The page documents terminal sessions, not raw PTY allocation. |
|
||||
|
||||
Keep the Bash- and PowerShell-specific leaf packages, plugin ids, types, and tools. Their dialect names are accurate.
|
||||
|
||||
### Language server and jobs
|
||||
|
||||
| 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. |
|
||||
| `@deepseek-ai/dsh-tasks`, `ctx.tasks`, `TaskService` | `@deepseek-ai/dsh-jobs`, `ctx.jobs`, `JobRegistry` | The service registers, owns, observes, waits for, and cancels multiple background jobs. It is a registry, not a general task service. |
|
||||
| Public `TaskId`, `TaskKindMap`, `TaskStart`, `TaskHooks`, `TaskOutcome`, `TaskSnapshot`, `TaskRead`, and `TaskDoneListener` names | Corresponding `Job*` names | These types belong to the renamed job domain. `JobId` is shorter and clearer than `BackgroundTaskId` or `BgTaskId`. |
|
||||
| `@deepseek-ai/dsh-tasks-local`, `LocalTaskService` | `@deepseek-ai/dsh-jobs-local`, `LocalJobRegistry` | This is the process-local provider of the job registry. Here `local` is meaningful because the jobs and callbacks live in one process. |
|
||||
| `@deepseek-ai/dsh-tool-tasks` | `@deepseek-ai/dsh-tool-jobs` | The consumer controls the job registry and should use the same domain noun. |
|
||||
| `ToolTasks`, `toolTasks`, `ToolTasksConfigSchema`, `PublicTaskSnapshot`, `publicTask`, `validateTaskId` | Corresponding `*Jobs`, `*Job*`, and `validateJobId` names | Imports, forwarded config, public tool values, and helpers are part of the same job domain. Keeping `Task` after the package rename would create a second vocabulary for one feature. |
|
||||
| `task_output`, `task_list`, `task_kill` | `job_output`, `job_list`, `job_kill` | These model tools act on jobs, not user tasks. `run_in_background` returns a `JobId`. |
|
||||
| `@deepseek-ai/dsh-client-ui-task`, `client/ui-task/` | `@deepseek-ai/dsh-client-ui-jobs`, `client/ui-jobs/` | The client package presents the background-job collection. It is not one user task. |
|
||||
| `TaskView`, wire frame `session/tasks`, `tasksBySession` | `JobView`, wire frame `session/jobs`, `jobsBySession` | The browser contract and its mirror expose the same job domain as the registry and tools. |
|
||||
| `docs/subsystems/tasks.md` | `docs/subsystems/jobs.md` | The subsystem page must use the public job vocabulary. |
|
||||
|
||||
Keep the base LSP package, `ctx.lsp`, LSP protocol types, and the LSP tool. The seam deliberately exposes language-server semantics; only its provider qualifier is wrong.
|
||||
|
||||
### Input triggers, tool presentation, permission presets, and user questions
|
||||
|
||||
| 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`. |
|
||||
| `@deepseek-ai/dsh-agent-tool-mode`, plugin `tool-mode` | `@deepseek-ai/dsh-agent-tool-presentation`, plugin `tool-presentation` | The plugin changes how tools are presented to the model. It does not change execution behavior. Keep local `Config.mode` and `ToolPresentationMode`. |
|
||||
| `packages/interaction/permission/` | `packages/interaction/permission-presets/` | The package owns named combinations of sandbox and approval settings, not permission enforcement. |
|
||||
| `@deepseek-ai/dsh-permission`, `ctx.permission`, `PermissionService` | `@deepseek-ai/dsh-permission-presets`, `ctx.permissionPresets`, `PermissionPresetService` | The service selects and persists presets. Sandbox and approval services enforce the result. |
|
||||
| `@deepseek-ai/dsh-client-ui-permission` | `@deepseek-ai/dsh-client-ui-permission-presets` | The UI edits and selects permission presets. |
|
||||
| `docs/subsystems/permission.md` | `docs/subsystems/permission-presets.md` | The page documents preset selection, not permission enforcement. |
|
||||
| `@deepseek-ai/dsh-user-interaction`, `user-interaction/` | `@deepseek-ai/dsh-user-questions`, `user-questions/` | The seam supports question batches and answers only. Approval, commands, and directory picking are separate interaction seams. |
|
||||
| `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. Removal of the `both` tool-presentation mode remains deferred to a separate proposal; this rename does not remove behavior.
|
||||
|
||||
### Typert, API gateway, and tools
|
||||
|
||||
| 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. |
|
||||
| `bindTypeRTGateway`, `typertGateway` binding | `bindTypertRemote`, `typertRemote` | These bindings expose Typert Remote services, not the concrete API gateway service. |
|
||||
| Public `TypeRT*` and camel-case `typeRT*` identifiers | `Typert*` and `typert*` | `Typert` is the one canonical product spelling. |
|
||||
| Protocol interface `TypeRTService` | `TypertRegistryContract` | The protocol-owned interface is the dependency-inverted face implemented by the existing concrete `TypertRegistry`. A distinct suffix prevents an import and declaration collision. |
|
||||
| `ToolRegistry` | `ToolRuntime` | The class owns presentation, approval and guard policy, dispatch, cancellation, validation, finalization, and observation. Registration is only one internal part. |
|
||||
| `ToolRegistryScheduler`, `TOOL_REGISTRY_SCHEDULER` | `ToolRuntimeScheduler`, `TOOL_RUNTIME_SCHEDULER` | The scheduler controls runtime dispatch, not registration. |
|
||||
|
||||
Keep `@deepseek-ai/dsh-tools` and `ctx.tools`. Keep `@deepseek-ai/dsh-api-gateway`, its `gateway/` folder, `ctx.typertGateway`, and `TypertGatewayService`; that service is a real API gateway. Its internal `TypeRT*` identifiers still follow the `Typert*` spelling rule.
|
||||
|
||||
### Workspace instructions, telemetry, identity, and launch environment
|
||||
|
||||
| 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. |
|
||||
| Plugin and durable source names `workspace-context` and `workspace-instructions` | `agent-instructions` | The recorded source is a specific class of agent instructions. `AgentInstruction*` replaces public `WorkspaceInstruction*` names. This term does not include system, developer, or user messages. |
|
||||
| `ctx.telemetry`, abstract `Telemetry` | `ctx.sessionTelemetry`, `SessionTelemetryBackend` | The service captures session-ledger telemetry and hands it to a reporting backend. It is not a repository-wide metrics or tracing service. |
|
||||
| `TelemetryBackend` | `SessionTelemetrySink` | This lower layer receives emitted records. `Sink` distinguishes it from the coordinating backend service. |
|
||||
| `TelemetryCoordinator`, `TelemetryRecord`, `TelemetrySeverity`, `TelemetrySharingStatus`, and `TelemetryCapture` | Corresponding `SessionTelemetry*` names | These public types belong only to session telemetry. |
|
||||
| `telemetry/record` | `session-telemetry/record` | The event name must state its owning domain. |
|
||||
| `TelemetryOtel`, `TelemetryMode`, plugin `telemetry-otel` | `OpenTelemetrySessionBackend`, `SessionTelemetryMode`, plugin `session-telemetry-otel` | The provider name states both the OpenTelemetry mechanism and session scope. Keep the package names `dsh-session-telemetry` and `dsh-session-telemetry-otel`. |
|
||||
| `docs/subsystems/telemetry.md` | `docs/subsystems/session-telemetry.md` | The page documents session telemetry, not repository-wide observability. |
|
||||
| `session/user-id/`, `@deepseek-ai/dsh-user-id` | `identity/anonymous-user-id/`, `@deepseek-ai/dsh-anonymous-user-id` | The value is a random correlation id shared by telemetry, feedback, and DeepSeek requests. It is neither a Session concern nor an authenticated user identity. |
|
||||
| `USER_ID_FILE_NAME`, `.userid`, feedback label `User` | `ANONYMOUS_USER_ID_FILE_NAME`, `.anonymous-user-id`, feedback label `Anonymous user` | The file and UI must not imply account identity. Keep the existing `AnonymousUserId` functions and the standard OTel attribute `user.id`. |
|
||||
| `util/environment/`, `@deepseek-ai/dsh-environment` | `util/launch-environment/`, `@deepseek-ai/dsh-launch-environment` | The package captures one immutable layered snapshot at launch. It is not a general environment API. |
|
||||
| Public `Environment*`, `createEnvironmentSnapshot`, `environmentOf`, `DSH_ENVIRONMENT_KEY` | `LaunchEnvironment*`, `createLaunchEnvironmentSnapshot`, `launchEnvironmentOf`, `DSH_LAUNCH_ENVIRONMENT_KEY` | The names state the snapshot's lifetime and purpose. |
|
||||
| `ctx.launcherEnvironment` | `ctx.launchEnvironment` | The value describes the application launch, not only a launcher component. Keep source labels `process`, `project-env`, and `user-env`. |
|
||||
|
||||
### Schedule, workflow, goals, and compaction
|
||||
|
||||
| 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*`. |
|
||||
| `WorkflowService`, `ctx.workflows` | `WorkflowEngine`, `ctx.workflowEngine` | One engine parses and executes workflow programs. The plural key wrongly suggests a registry. Keep `@deepseek-ai/dsh-workflow` and workflow events and tools. |
|
||||
| `@deepseek-ai/dsh-workflow-workerthread`, `WorkerWorkflowEngine` | `@deepseek-ai/dsh-workflow-worker-thread`, `WorkerThreadWorkflowEngine` | `worker thread` is the precise Node mechanism and the repository spelling uses the full words. |
|
||||
| `@deepseek-ai/dsh-goal-session`, `goal/goal-session/` | `@deepseek-ai/dsh-goal-round-driver`, `goal/goal-round-driver/` | The plugin drives same-session Goal Rounds. It neither stores goals nor defines sessions. Keep `GoalService`, goal source, events, and contracts. |
|
||||
| `packages/compact/` | `packages/compaction/` | The group is a noun-domain family. `compact` remains the user command verb. |
|
||||
| `@deepseek-ai/dsh-compact`, `ctx.compact`, `CompactService` | `@deepseek-ai/dsh-compaction`, `ctx.compaction`, `CompactionEngine` | The object runs the compaction algorithm and lifecycle. It is an engine, not a generic service. |
|
||||
| `compact/*` events and public domain prefixes | `compaction/*` | Events and domain types use the noun. Keep verb-shaped operations such as `compactNow`, `compactRegion`, and `compactIfNeeded`. |
|
||||
| `@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 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
|
||||
|
||||
| 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. |
|
||||
| Abstract `Credentials` | `CredentialProvider` | The class resolves credential references. Keep package names, keys, and events. |
|
||||
| `CredentialsLocal` | `LocalCredentialProvider` | This provider reads the host process and `.env` state, so local execution is part of its contract. |
|
||||
| `ClientModuleHostService`, `ctx.clientModuleHost` | `ClientModuleRegistry`, `ctx.clientModules` | The service owns multiple registered client modules. Keep the package and the browser `ClientModuleLoader`. |
|
||||
| `AgentDefaultModelService` | `AgentDefaultModelConfig` | The object stores one default model selection. It does not run a service or general registry. Keep its package, key, settings namespace, and type. |
|
||||
| `SessionReferenceService`, `ctx.sessionReferences` | `SessionReferenceResolver`, `ctx.sessionReferenceResolver` | It resolves one session reference from a URI or input. It does not own a reference collection. |
|
||||
| `SessionQueryService`, `SessionQuerySqlite` | `SessionQueryEngine`, `SqliteSessionQueryEngine` | The classes execute a query model and its SQLite implementation. Keep package names, key, and tool. |
|
||||
| `@deepseek-ai/dsh-session-export`, `session-export/`, Loader id `session-export`, `ctx.sessionExport` | `@deepseek-ai/dsh-session-log-export`, `session-log-export/`, Loader id `session-log-download`, `ctx.sessionLogDownload` | The npm package names the Session-log export because npm rejects `download` in package names. The Loader id and browser API retain `download` because they describe the browser side effect. |
|
||||
| `SessionExportDownloadController`, other `SessionExport*` browser types, `useSessionExport`, `SessionExportHeader` | `SessionLogDownloadController`, corresponding `SessionLogDownload*` types, `useSessionLogDownload`, `SessionLogDownloadHeaderAction` | The controller owns preflight, duplicate-request collapse, modal state, and browser save. `ExportDownload` repeats the action, and the component contributes one Header action rather than the Header. |
|
||||
| `CommandService` in the host command package | `CommandRuntime` | The object registers and executes host commands across live calls. Keep its package, key, types, and events. |
|
||||
| `TokenMeterService` | `TokenMeter` | The object measures token use. `Service` adds no scope. |
|
||||
| `LlmService` | `LlmRuntime` | The object selects providers and runs live model requests. Keep the package, key, adapters, and events. |
|
||||
|
||||
### Host web server, session data, and code execution
|
||||
|
||||
| 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. |
|
||||
| `SessionPersistenceJsonl` | `JsonlSessionPersistence` | Put the implementation qualifier first and keep the capability role intact. |
|
||||
| `SessionPersistenceSqlite` | `SqliteSessionPersistence` | Use the same provider naming order as JSONL. |
|
||||
| `@deepseek-ai/dsh-session-title-first-message-llm`, cadence `first-message` | `@deepseek-ai/dsh-session-title-first-prompt-llm`, cadence `first-prompt` | The trigger is the first user prompt, not any message in the session log. |
|
||||
| `@deepseek-ai/dsh-session-title-all-messages-llm`, cadence `all-user-messages` | `@deepseek-ai/dsh-session-title-all-prompts-llm`, cadence `all-prompts` | The backend refreshes from user prompts. `all messages` wrongly includes assistant and tool events. |
|
||||
| `@deepseek-ai/dsh-code-runtime-worker`, `WorkerCodeRuntime` | `@deepseek-ai/dsh-code-runtime-worker-thread`, `WorkerThreadCodeRuntime` | The implementation uses a Node worker thread. `worker` alone is too broad. |
|
||||
| `SubprocessService` | `SubprocessRuntime` | The service owns live child-process execution and lifecycle. Keep its package and key. |
|
||||
| `LocalSubprocessService` | `LocalSubprocessRuntime` | The provider runs same-host processes and process trees. |
|
||||
| `E2BSubprocessService` | `E2BSubprocessRuntime` | The provider runs subprocesses in the E2B runtime. |
|
||||
|
||||
Keep the complete session projection family and `SessionProjection*` vocabulary. A projection is a maintained read model; `Reducer` would name only its fold operation and would understate caching and lookup. Keep `SessionTitleService`, checkpoint policy, persistence package names, time context, and tmux context.
|
||||
|
||||
### Filesystem, skill, subagent, and web providers
|
||||
|
||||
| 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. |
|
||||
| `SkillService` | `SkillRegistry` | The service registers providers and resolves skills from their catalogs. |
|
||||
| `@deepseek-ai/dsh-skill-local`, `LocalSkillProvider`, provider id `local` | `@deepseek-ai/dsh-skill-filesystem`, `FileSystemSkillProvider`, provider id `filesystem` | The provider discovers skill files through `ctx.fs`, which can be local or remote. The mechanism is filesystem access, not locality. |
|
||||
| `SubagentService` | `SubagentRuntime` | The service selects providers and owns live spawn, resume, follow-up, cancellation, and settlement behavior. |
|
||||
| `@deepseek-ai/dsh-subagent-spawn`, `SpawnProvider` | `@deepseek-ai/dsh-subagent-spawn-in-process`, `SpawnInProcessProvider` | This provider starts a child agent in the current process. The configured provider id remains `spawn`. |
|
||||
| `@deepseek-ai/dsh-subagent-fork`, `ForkProvider` | `@deepseek-ai/dsh-subagent-fork-in-process`, `ForkInProcessProvider` | This provider forks an agent in the current process. The configured provider id remains `fork`. |
|
||||
| `@deepseek-ai/dsh-subagent-inprocess`, `subagent-inprocess/` | `@deepseek-ai/dsh-subagent-in-process-driver`, `subagent-in-process-driver/` | The package contains common in-process driving logic, not a third provider. |
|
||||
| Private `SdkProvider` in `dsh-subagent-dsh-sdk` | `SdkSubagentProvider` | The repeated package qualifier is intentional, and the class must say that it provides subagents through the SDK. |
|
||||
| `WebService`, `WebServiceConfig` | `WebRuntime`, `WebRuntimeConfig` | The object selects providers and runs live search and fetch operations. Keep the package, key, provider packages, and model tool. |
|
||||
| `@deepseek-ai/dsh-web-fetch-local`, `LocalFetchProvider`, `LocalFetchLimits`, provider id `local-http` | `@deepseek-ai/dsh-web-fetch-http`, `HttpFetchProvider`, `HttpFetchLimits`, provider id `http` | This provider performs direct HTTP fetches. `local` says where code happens to run, not which mechanism it provides. |
|
||||
|
||||
Keep `@deepseek-ai/dsh-subagent-dsh-sdk`, its provider id `dsh-sdk`, external ACP, Codex, and Claude Code provider families, the subagent tool package names, the main filesystem package and backends, filesystem tools and events, and the skill badge and tool packages.
|
||||
|
||||
### Hooks, guards, plan mode, extensions, and diagnostics
|
||||
|
||||
| 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. |
|
||||
| `@deepseek-ai/dsh-timeout-policy` | `@deepseek-ai/dsh-tool-call-timeout-policy` | The full `tool-call` qualifier names what the policy limits without calling the plugin a model-facing tool. Keep its `guard/timeout-policy/` directory and plugin id `timeout-policy`; the `packages/*/tool-*` catalog convention still applies only to packages that register tools. |
|
||||
| `PlanModeService` | `PlanModeController` | The object controls transitions into and out of plan mode. It is not a general execution runtime. |
|
||||
| `packages/self-modification/` | `packages/extensions/` | The group contains repository plugin inspection and mounting tools. `extensions` states the stable package role without asserting that the agent modifies itself. Keep the package names `tool-cordis` and repository-plugin names. |
|
||||
| `packages/support/` | `packages/test-support/` | The group is test-only infrastructure. Its path must say so. |
|
||||
| `invariants/` in the former support family | `runtime-diagnostics/invariants/` | Invariants can run in production diagnostics even though shipped presets omit them. They are not test support. |
|
||||
| `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. This decision renames the controller class, not the product feature.
|
||||
|
||||
### Utilities, E2B, host, bundles, examples, and applications
|
||||
|
||||
| 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. |
|
||||
| `E2BSandboxService` | `E2BRuntime` | The class creates, reuses, and disposes the E2B execution environment used by filesystem and subprocess adapters. It is broader than one sandbox handle and narrower than a generic owner. Keep `@deepseek-ai/dsh-e2b`, `ctx.e2b`, and the `e2b/` group. |
|
||||
| `@deepseek-ai/dsh-frontend-static` | `@deepseek-ai/dsh-host-frontend-static` | The package is the Host plugin that serves the frontend assets. The prefix distinguishes it from frontend application code. |
|
||||
| `PluginInventoryService` | `PluginInventoryGateway` | The class is a Remote-only adapter from the live Loader tree to the `pluginInventory/list` RPC. It owns no same-process service, cache, history, or mutation path. `Gateway` states the role that exists. |
|
||||
| `@deepseek-ai/dsh-jsonrpc-demo` | `@deepseek-ai/dsh-sdk-jsonrpc-demo` | The example demonstrates the runtime SDK over JSON-RPC. It belongs to the one SDK meaning. |
|
||||
| `@deepseek-ai/dsh-frontend` | `@deepseek-ai/dsh-web-frontend` | The application is the web frontend. Keep its physical `apps/web/` folder. |
|
||||
|
||||
Keep atomic-write, brand, native-command, timeout utility, directory-picker, `dsh-base`, `dsh-web-app`, app boot, CLI names, and the `headless` package, bundle, and example identity. `headless` is the intended product essence and may later support more than one-shot execution.
|
||||
|
||||
### Client runtime and UI
|
||||
|
||||
| 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. |
|
||||
| `WorkspacesService` | `WorkspaceRuntime` | The client object coordinates live workspace selection and operations. Existing `ctx` keys stay where the ledger does not name a key change. |
|
||||
| `WorkspaceGroupBy`, `WorkspaceOrderBy`, `workspaceExpansion`, `setWorkspaceExpanded`, `expandedProjects`, `projectLabel`, `recentSessionOrder`, `recentSessionUpdatedAt`, `syncRecentSessions`, `setRecentSessionOrder`, `retainWorkspaceKeys`, `workspaceKey` | `SessionGroupBy`, `SessionOrderBy`, `groupExpansion`, `setGroupExpanded`, `expandedGroups`, `workspaceLabel`, `sessionOrderByAccount`, `sessionUpdatedAtByAccount`, `syncSessionOrderAccount`, `setSessionOrder`, `retainAccountKeys`, `accountKey` | These are Session-list viewing names. Their accounts include real Workspaces, Ungrouped, and the flat list. `Workspace`, `project`, and `recent` therefore state the wrong subject or mechanism. Keep `WorkspaceViewState`; the store still belongs to the Workspace browser. |
|
||||
| `LocaleService` | `LocaleRuntime` | The object coordinates locale definitions, selection, persistence, and change publication. |
|
||||
| `ThemeService` | `ThemeRuntime` | The object coordinates themes, preference resolution, system sensing, and change publication. |
|
||||
| `LayoutService` | `LayoutController` | The object controls the current UI layout state. |
|
||||
| `@deepseek-ai/dsh-client-ui-model` | `@deepseek-ai/dsh-client-ui-model-selection` | The package controls the model selection for a session. The singular `model` name is too broad. |
|
||||
| `ModelService`, `ctx.models` | `ModelDirectoryResolver`, `ctx.modelDirectories` | Its only public operation, `directoryFor(sessionId)`, resolves and retains one directory per live session. It has no registration API, so `Registry` would be false. Each `ModelDirectory` remains the consumer-facing catalog of selectable models. |
|
||||
| `SettingsScopeService` | `SettingsScopeBinder` | Its sole operation binds one namespace specification to the caller's transport and lifecycle and returns a `SettingsScopeController`. Keep `ctx.settingsScope`; it names the singular binding capability, not a collection of scopes. |
|
||||
| `@deepseek-ai/dsh-client-ui-models` | `@deepseek-ai/dsh-client-ui-settings-models` | This package owns the Models settings panel. Keep `ModelsSettingsStore`; it holds one settings view model with data operations and subscriptions and is a real store. |
|
||||
| `@deepseek-ai/dsh-client-ui-plugin-config`, `client/ui-plugin-config/` | `@deepseek-ai/dsh-client-ui-settings-plugins`, `client/ui-settings-plugins/` | This package owns the Plugins settings section, not a general plugin-configuration system. The target joins the `ui-settings-*` family and uses the section's plural product name. |
|
||||
| `PluginConfigSection`, `PluginConfigSectionProps`, `PluginConfigSectionInjected`, `PluginSettingsTabRow`, `PluginConfigKey`, `settings.pluginConfig` | `PluginsSettingsSection`, `PluginsSettingsSectionProps`, `PluginsSettingsSectionInjected`, `PluginsSettingsTabEntry`, `PluginsSettingsLocaleKey`, `settings.plugins` | The section owns the Plugins settings presentation and tab ledger. The metadata value is one slot entry, not a rendered row. Each card still edits one plugin's configuration. |
|
||||
| `@deepseek-ai/dsh-client-ui-plugins`, `client/ui-plugins/`, Loader id `ui-plugins`, `client-ui-plugins-invariant` | `@deepseek-ai/dsh-client-ui-settings-plugin-inventory`, `client/ui-settings-plugin-inventory/`, Loader id `ui-settings-plugin-inventory`, `client-ui-settings-plugin-inventory-invariant` | This later package owns the read-only Plugin Inventory tab in the Plugins settings section. `ui-plugins` is too broad and does not distinguish the inventory from editable plugin settings. |
|
||||
| `PluginSettingsSection`, `PluginSettingsSectionProps`, `PluginSettingsSectionInjected`, `PluginsKey`, `settings.plugins` in the former `ui-plugins` package | `PluginInventorySettingsTab`, `PluginInventorySettingsTabProps`, `PluginInventorySettingsTabInjected`, `PluginInventoryLocaleKey`, `settings.pluginInventory` | The component is now a tab contribution, not a settings section. The other names state the inventory subject and avoid colliding with `PluginsSettingsSection` and its `settings.plugins` locale namespace. Keep the shared `settings.plugins.tab` slot name; both tabs contribute to the Plugins section through that slot. |
|
||||
| `@deepseek-ai/dsh-client-ui-feedback`, `client/ui-feedback/`, Loader id `ui-feedback`, `client-ui-feedback-invariant` | `@deepseek-ai/dsh-client-ui-message-feedback`, `client/ui-message-feedback/`, Loader id `ui-message-feedback`, `client-ui-message-feedback-invariant` | This package presents ratings and notes for assistant messages through the `messageFeedback` Remote. The old name also appears to cover command feedback and any later feedback UI. It does not. |
|
||||
| `FeedbackController`, `FeedbackStatus`, `FeedbackView`, `FeedbackActionResult`, `FeedbackInjected`, `FeedbackActionProps`, `FeedbackActions`, `FeedbackKey` in the former `ui-feedback` package | `MessageFeedbackController`, `MessageFeedbackStatus`, `MessageFeedbackView`, `MessageFeedbackActionResult`, `MessageFeedbackInjected`, `MessageFeedbackActionProps`, `MessageFeedbackActions`, `MessageFeedbackKey` | These are exported Client names. The `Message` qualifier prevents them from claiming every feedback domain. Keep `Controller`: the object accepts rating and note actions and coordinates one Session's load, mutation, conflict, reconnect, and disposal state. |
|
||||
| `agent-loop-store.ts`, `bash-store.ts`, `web-search-store.ts` | `agent-loop-card-controller.ts`, `bash-card-controller.ts`, `web-search-card-controller.ts` | Each module exports a card controller. A private `SnapshotStore` field does not make the module a store. |
|
||||
| `card-store.ts` | `card-form.ts` | The module owns the staged form, field conversion, and form actions. The snapshot stores it returns are presentation adapters, not the module's main role. |
|
||||
| `@deepseek-ai/dsh-client-ui-question` | `@deepseek-ai/dsh-client-ui-user-questions` | The UI presents the user-question seam, not an arbitrary question domain. |
|
||||
| `@deepseek-ai/dsh-client-ui-command`, `ui-command/` | `@deepseek-ai/dsh-client-ui-commands`, `ui-commands/` | The package presents and runs a collection of commands. |
|
||||
| `@deepseek-ai/dsh-client-ui-directory-picker`, `client/ui-directory-picker/`, Loader id `ui-directory-picker`, `client-ui-directory-picker-invariant` | `@deepseek-ai/dsh-client-ui-directory-picker-browse`, `client/ui-directory-picker-browse/`, Loader id `ui-directory-picker-browse`, `client-ui-directory-picker-browse-invariant` | The Client packages now contain separate `browse` and `native` directory-picker presentations. The unqualified package is the browse implementation, not their shared definition. The target matches the Host backend family and changes no boundary. |
|
||||
| Client `ctx.command`, `CommandService`, `CommandServiceContract` | `ctx.commandUi`, `CommandUiRuntime`, `CommandUiContract` | The host already owns `ctx.commands`. The client service is the UI runtime for command discovery and execution. Existing `CommandUiSpec` fixes the `Ui` casing. |
|
||||
| `ConversationService` | `ConversationController` | The object controls the active conversation state and user actions. |
|
||||
| `InputService` | `SessionInputResolver` | The interface resolves the input facade for one session scope. It is neither a global input registry nor an execution service. Keep `InputHub` as the concrete hub and `ctx.conversation.input` as the published face. |
|
||||
|
||||
Use `Ui`, not `UI`, inside PascalCase identifiers. Keep the remaining client package names unless this ledger names them. Keep the deprecated client connection and Host `ApiProxy` vocabulary for now; the API plane will replace them, and a rename would add churn to a surface scheduled for removal.
|
||||
|
||||
## Explicit non-renames
|
||||
|
||||
The following debated names stay unchanged because the current scope is accurate or a rename would create a false concept:
|
||||
|
||||
- Keep the complete sandbox family and `ctx.sandbox`. Do not introduce `processSandbox`.
|
||||
- Keep `@deepseek-ai/dsh-api-gateway`, `ctx.typertGateway`, and `TypertGatewayService`.
|
||||
- Keep session projection names. A projection is not only a reducer function.
|
||||
- Keep `@deepseek-ai/dsh-session-stats`, `sessionStats`, and `SessionStatsProjection`. They accurately name whole-session statistics and the maintained read model that carries them.
|
||||
- Keep `GoalService`; it owns the goal state machine, authority, compare-and-set behavior, events, and remote operations. It is not just a store.
|
||||
- Keep `SessionTitleService`; its role is a domain service shared by title providers.
|
||||
- Keep `PermissionPresetSettingsController` even though it is long. Every word limits the role.
|
||||
- Keep `ModelsSettingsStore`; its main contract is one settings data model with store operations.
|
||||
- Keep `InputHub`; it is the concrete hub that backs `SessionInputResolver`.
|
||||
- Keep `dsh-subagent-dsh-sdk` and provider id `dsh-sdk`; the repeated qualifier prevents ambiguity.
|
||||
- Keep `headless`; the product identity is accurate even if the runtime later supports more than one-shot use.
|
||||
- Keep deprecated Host `ApiProxy` and client connection names until the API replacement removes them.
|
||||
- Keep `Web` for the Host server and the provider-neutral web capability. Use `HTTP` only for the direct fetch provider.
|
||||
- Keep `E2B`, not `E2B sandbox`, as the package and context name.
|
||||
- Keep MCP, Todo, app boot, base bundle, web-app bundle, and CLI names. Keep the directory-picker capability and Host backend names; only the unqualified Client `browse` presentation is renamed.
|
||||
- Keep `@deepseek-ai/dsh-client-ui-directory-picker-native`; its suffix names the native-chooser presentation beside the renamed `-browse` variant. Keep `SURFACE_PACKAGES`; within the directory-picker auto selector it is the package map for the Client presentation half, contrasted with `BACKEND_PACKAGES`.
|
||||
- Keep `@deepseek-ai/dsh-host-plugin-inventory`, `ctx.pluginInventory`, the `pluginInventory/list` Remote, and the `PluginInventory*` payload types. They accurately name the Host-owned read-only inventory; only the adapter class and the overly broad Client presentation names change.
|
||||
- Keep `ConfigurablePluginsTab`. It is the tab that renders plugins with editable configuration; it does not own the complete Plugins settings section.
|
||||
- Keep the shared `settings.plugins.tab` slot. It belongs to the Plugins settings section. The inventory package changes its own locale namespace to `settings.pluginInventory`; it does not create a separate tab slot.
|
||||
- Keep the `@deepseek-ai/dsh-message-feedback` capability, `messageFeedback` Remote, assistant-action entry id `feedback`, hook key `feedback`, and locale namespace `feedback`. Their surrounding interfaces already limit them to message feedback or to the local assistant-message slot. Only the broad Client package and exported UI names change.
|
||||
- Keep `RemoteFailure`, `RemoteResult`, and `SessionRemotes`. The first two are Typert carrier-result values, while the last is the set of Remote namespaces used by the Client Session cluster. None is a store, controller, registry, or runtime.
|
||||
- Keep the `/export` human command, `/api/session.export` Host route, `DownloadsApi`, and its `sessionLog` operation. The command names the user action, the Host route exports the archive, and the API groups direct HTTP downloads. The renamed Client controller owns the separate browser-download step.
|
||||
- Keep `.client` and `.host` in test filenames. They identify the compiler face each test enters and do not claim a product role.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Keep the current names and add a glossary.** Rejected. A glossary cannot make `BashExecutor` truthful when PowerShell implements it, or make `ToolRegistry` disclose that it enforces and executes tools. The identifier must carry the useful distinction.
|
||||
|
||||
**Prefix every npm package with its group.** Rejected. Flat npm names do not need a copy of the directory tree. A mechanical prefix adds length without explaining the package role.
|
||||
|
||||
**Call the whole repository an SDK.** Rejected. The project is an agent harness. SDK is the supported JSON-RPC client/server stack used by Python and TypeScript clients. Two meanings make package names and product prose ambiguous.
|
||||
|
||||
**Use `Service` for every Cordis service class.** Rejected. Cordis inheritance is an implementation fact. The class name must tell callers whether the object registers, stores, resolves, controls, or runs work.
|
||||
|
||||
**Use `Runtime` as the standard replacement for `Service`.** Rejected. `Runtime` is correct only when the object owns live execution or lifecycle. Registries, stores, directories, controllers, resolvers, engines, and configuration objects keep their sharper roles.
|
||||
|
||||
**Prefer the shortest possible name.** Rejected. Short is useful only after scope is clear. `PermissionPresetSettingsController` keeps `Preset`; `JobId` is short because `Job` already carries the domain; `BgTaskId` is short but cryptic.
|
||||
|
||||
**Use broad names for possible future features.** Rejected. Name the stable current role. A future boundary change can rename the object again before release or use a new proposal after release. Vague names charge every current reader for an unbuilt future.
|
||||
|
||||
**Rename `dsh-compact-basic` to `dsh-compaction-llm`.** Rejected. `LLM` adds no distinction in the current backend family. `basic` is less ambitious and does not claim an algorithm that does not exist.
|
||||
|
||||
**Rename session projections to reducers.** Rejected. Reduction is how a projection is built. The package also owns the read-model value, cache, and lookup contract.
|
||||
|
||||
**Rename the persistent Bash tool to `bash-terminal`.** Rejected. That name collides with the terminal-session family. Moving `tool-bash-persistent` under `shell/` fixes its home while its current name continues to distinguish it from the one-shot Bash tool.
|
||||
|
||||
**Rename or split boundaries while applying the ledger.** Rejected. Reviewers must be able to see that behavior did not change. A real boundary defect needs its own proposal, tests, and consequences.
|
||||
|
||||
**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.
|
||||
|
||||
## Verification
|
||||
|
||||
- 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.
|
||||
- `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.
|
||||
|
||||
## Consequences
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
@@ -0,0 +1,386 @@
|
||||
# Agent Note: 仓库命名约定与预发布重命名清单
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-08-11-repository-naming-contract-and-rename-ledger.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
仓库的发展速度曾超过部分名称的演进速度。一些包名描述的是最初的实现,而非所提供的能力。若干类即使实际承担注册表、运行时、引擎、控制器或解析器的职责,名称仍使用 `Service`。部分 `ctx` 键以单数命名注册表,却以复数命名单个引擎。还有一些提供方明明通过可替换的文件系统或子进程服务工作,可以在另一执行环境中运行,名称却使用 `local`。
|
||||
|
||||
这些名称并非无关紧要。名称会告诉贡献者一项职责从哪里开始、到哪里结束。`Store` 表示数据访问。`Registry` 表示注册与查找。`Runtime` 表示实时执行和生命周期。如果同一个词同时表示这三者,调用方就必须阅读实现,才能判断哪个对象拥有策略、工作或状态。
|
||||
|
||||
仓库还曾在两种含义下使用 `SDK`。受支持的 Python 和 TypeScript 客户端使用 JSON-RPC SDK 协议。项目整体是 DeepSeek Harness,而不是 SDK 项目。已移除的 SDK 项目工具链使宽泛的含义失去依据,但文案和名称仍保留了部分旧用法。
|
||||
|
||||
首次发布带标签版本之前的最后一个窗口,使仓库级重命名仍可低成本完成。若继续保留含义不清的名称,偶然形成的词汇就会变成兼容性约定。
|
||||
|
||||
## 决策
|
||||
|
||||
仓库使用本清单中的全部当前名称。本决策只更改名称;包职责、服务边界、行为、默认值和数据模型保持不变。如果某个名称暴露出不合理的边界,需要另写一份 proposed Agent Note,专门提议边界变更。
|
||||
|
||||
每个已重命名系列只有一套词汇。清单点名某一接口时,其目录、NPM 包名、导入、Cordis 插件名称、`ctx` 键、公开类型、直接耦合的事件或工具标识符、配置、测试、fixture(测试前置数据)、示例、生成的参考资料以及当前文档都使用当前名称。仓库不保留别名、兼容包、重复的服务键、双重事件名称或回退解析器,并拒绝旧名称。
|
||||
|
||||
同一系列不会公开两套词汇。
|
||||
|
||||
### `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,已移除的项目生成器、启动器、辅助工具和启动器遥测包继续保持不存在。
|
||||
|
||||
本决策部分取代三项现行决策。它替换[包重新分组决策](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/` 归属保持不变。
|
||||
|
||||
如果其他已实现说明中的包、路径或类型被重命名,而其边界和理由保持不变,则本决策不会取代这些说明。这些说明使用已实现的事实名称。三项被部分取代的决策都链接回本决策。
|
||||
|
||||
### 按实际职责命名
|
||||
|
||||
使用常见且具体的名词。名称应描述稳定职责,而不是最初的实现、当前目录或未来可能出现的扩展。不得添加不传递任何信息的词。不得为了缩短名称而删除用于限定作用域的词。
|
||||
|
||||
接口包以能力命名。实现包增加机制、协议、环境或供应商限定词,以区分不同实现。只有同主机执行属于约定时,才能使用 `local`。如果提供方只是通过可替换的 `ctx.fs` 读取看似本地的路径,或通过可替换的 `ctx.subprocess` 启动工作,就不得使用该词。
|
||||
|
||||
如果对象是单个引擎、运行时、策略、控制器、解析器、存储或当前配置,使用单数 `ctx` 键。如果对象是注册表,或服务拥有多个具名成员,使用复数键。类的职责和键的单复数必须一致。复数键本身不能证明对象是注册表;应由其操作和所有权决定。不得让不兼容的 host 与 client 声明复用同一个 Cordis `Context` 键。即使二者使用独立的运行时上下文,TypeScript 声明合并仍会同时看到两种类型。如果自然复数已经属于另一个端面,就增加职责后缀。
|
||||
|
||||
仅当没有更精确的职责词能够如实描述对象时,才使用 `Service`。`GoalService` 和 `SessionTitleService` 是保留的有效名称,因为它们各自拥有领域服务,其工作无法准确归约为存储、注册或单一执行机制。
|
||||
|
||||
### 职责词即约定
|
||||
|
||||
| 词 | 适用场景 | 不适用场景 |
|
||||
|---|---|---|
|
||||
| `Controller` | 对象接受命令或用户意图,并更改一项已有的领域状态或呈现状态。它协调有界的状态转换。 | 对象执行任意工作、管理一组提供方,或仅将值转换为显示形式。 |
|
||||
| `Store` | 对象拥有一组数据,主要对这些数据提供创建、读取、更新、删除、快照或订阅操作。 | 对象验证状态机、行使裁决权、分派工作、决定提供方优先级,或协调多个领域。类内部存在映射并不会让该类成为存储。 |
|
||||
| `Directory` | 对象公开条目,供发现或选择。消费方会查询有哪些选项,并读取其元数据。 | 生产方可向其中注册任意实现,或调用方通过它执行工作。目录可以由注册表支撑,但两者的对外职责并不相同。 |
|
||||
| `Presenter` | 对象只负责将领域值或工具参数转换为渲染意图。它不拥有 I/O、订阅、变更或生命周期。 | 对象读取服务、更改状态或控制工作运行时机。这些职责属于控制器或运行时。 |
|
||||
| `Registry` | 对象拥有一组动态的具名注册项。它定义查找规则、重复项或优先级规则、注册生命周期和资源释放。 | 调用方的主要约定是分派、执行、取消、策略执行或编排。运行时可以在内部包含注册表。 |
|
||||
| `Runtime` | 对象运行实时工作。它跨调用拥有分派、取消、提供方协调或操作生命周期。 | 对象只存储记录、返回目录、解析单个值或保存配置。`Runtime` 不是 `Service` 的通用替代词。 |
|
||||
| `Resolver` | 对象根据所提供的输入计算或定位一个答案,通常不拥有答案的生命周期。 | 对象拥有可变集合或长时间运行的执行生命周期。 |
|
||||
| `Binder` | 对象将一个已声明接口附加到调用方的上下文或生命周期,并返回绑定后的值。 | 对象以集合形式拥有绑定值、控制其领域状态,或仅转换数据。 |
|
||||
| `Engine` | 对象实现领域算法或有状态执行模型,例如工作流、压缩或查询求值。 | 对象只选择提供方,或跨协议边界转发请求。 |
|
||||
| `Policy` | 对象决定允许、选择、限制或观察什么。 | 对象执行决策所允许的机制。策略和执行器必须分别命名。 |
|
||||
| `Executor` | 对象在一项能力内运行明确的请求或已解析的规范。 | 对象拥有宽泛的应用生命周期或提供方目录。 |
|
||||
| `Gateway` | 对象适配进程、网络、RPC 或 API 边界,并在两侧之间转换。 | 对象只注册同进程服务或存储元数据。 |
|
||||
| `Provider` | 对象为一项能力定义提供一种实现。如果可以存在多个提供方,应增加机制或供应商限定词。 | 对象是能力定义、提供方注册表或面向消费方的运行时。 |
|
||||
| `Backend` | 对象在已定义接口之后,实现可替换的底层持久化、传输或执行后端。 | 对象是面向用户的服务,或只是对某个实时对象返回的引用。 |
|
||||
| `Handle` | 该值是对一个实时资源的引用,并控制或观察该资源。 | 对象创建并管理整个资源池。不得使用 `Owner` 或含义模糊的 `Resource`;如果 `Handle` 或更精确的管理职责合适,就应采用后者。 |
|
||||
| `Config` | 对象拥有一个已解析的配置值,或一份边界严格受限的配置记录及其更新约定。 | 对象存储通用集合、执行工作或公开不相关的设置。 |
|
||||
| `Service` | 对象拥有一项职责内聚的领域服务,且以上更精确的职责词都无法如实描述其职责范围。 | 仅因为类继承自 Cordis `Service` 而使用该名称,或因为确定真正的职责需要进一步思考。 |
|
||||
|
||||
实用判断方式很直接。如果调用方主要调用 `register()` 并收到资源释放函数,应使用 `Registry`。如果调用方主要调用 `run()`、`dispatch()`、`cancel()` 或 `execute()`,应使用 `Runtime`、`Engine` 或 `Executor`。如果调用方主要浏览选项,应使用 `Directory`。如果对象主要将一份规范绑定到调用方拥有的上下文和生命周期,应使用 `Binder`。如果对象只将领域数据映射为 UI 数据,应使用 `Presenter`。如果它还会更改状态,就不是呈现器。
|
||||
|
||||
### 使用能够补充信息的限定词
|
||||
|
||||
如果协议或方言名称能够区分实现,就应保留。实现依赖相应机制时,保留 `Bash`、`Pwsh`、`JSON-RPC`、`SQLite`、`JSONL`、`OpenTelemetry`、`Claude Code` 和 `E2B`。每个当前后端都已使用 LLM(大语言模型)seam 时,不要在压缩后端名称中加入 `LLM`;在出现更具体的算法名称之前,`basic` 才是如实且中性的名称。
|
||||
|
||||
不得虚构 `process sandbox` 概念。当前 `sandbox` 系列已经准确命名其产品职责。本决策不改变该职责。
|
||||
|
||||
PascalCase 标识符中的首字母缩略词使用首字母大写格式:`Ui`、`Llm`、`JsonRpc` 和 `ApiProxy`。在文案和适用的包名中使用惯例规定的全大写形式:UI、LLM、JSON-RPC 和 API。`Typert` 是标识符和文案中的唯一准确产品拼写;不得写成 `TypeRT`、`TypeRt`,也不得对 `Typert` 作其他内部拆分。
|
||||
|
||||
不得为了避免重复而删除有意保留的供应商限定词。`dsh-subagent-dsh-sdk` 表示 DeepSeek Harness SDK 提供方,可避免与其他 SDK 混淆。其私有类改名为 `SdkSubagentProvider`,因为类名还需要说明它提供什么。
|
||||
|
||||
### 将规则写入项目文档
|
||||
|
||||
配对的包创建指南 `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 服务器。 |
|
||||
|
||||
保留 `@deepseek-ai/dsh-sdk-client`、`@deepseek-ai/dsh-sdk-protocol` 和 `deepseek-harness-sdk-runtime`。排除 `@deepseek-ai/create-sdk`、`@deepseek-ai/dsh-scripts`、`@deepseek-ai/dsh-helper` 和 `@deepseek-ai/dsh-telemetry`;单独的移除决策负责删除这些包及其支撑依赖图。
|
||||
|
||||
### 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。 |
|
||||
| 方言无关的 `BashExecutor`、`BashExecRequest`、`BashExecSpec`、`BashProcess`、`BashRunResult`、`BashSandboxInfo`、`BashProcessRead` 和 `BashProcessStatus` 名称 | 对应的 `Shell*` 名称 | 这些类型横跨 Bash 和 PowerShell 实现。描述 Bash 语法或行为的叶层类型保留 `Bash`。 |
|
||||
| `BASH_SETTINGS_NAMESPACE`,设置命名空间 `bash` | `SHELL_SETTINGS_NAMESPACE`,设置命名空间 `shell` | 两个 shell 提供方都注册这项由能力拥有的设置分区。常量和持久化命名空间必须使用能力名称。 |
|
||||
| `@deepseek-ai/dsh-bash-env`, `ctx.bashEnv`, `BashEnvRegistry` | `@deepseek-ai/dsh-shell-env`, `ctx.shellEnv`, `ShellEnvRegistry` | Bash 和 PowerShell 工具共享该环境注册表。 |
|
||||
| `docs/subsystems/bash.md` | `docs/subsystems/shell.md` | 该子系统页面记录方言无关的能力。 |
|
||||
| `packages/pty/` | `packages/terminal/` | 该包系列负责持久终端会话。原始 PTY 分配仍位于子进程层。 |
|
||||
| `@deepseek-ai/dsh-pty`, `ctx.pty`, `PtyService` | `@deepseek-ai/dsh-terminal`, `ctx.terminals`, `TerminalSessionService` | 调用方管理多个具名终端会话,而不是通过该服务分配原始 PTY。 |
|
||||
| 公开的高层 `Pty*` 会话和后端名称 | `Terminal*` 名称 | 公开抽象是终端会话。保留底层 `SubprocessTerminal*` 名称,因为它们已经说明底层机制。 |
|
||||
| `@deepseek-ai/dsh-pty-local`, `LocalPtyBackend` | `@deepseek-ai/dsh-terminal-bash`, `BashTerminalBackend` | 该提供方依赖 Bash 提示符和 shell 行为。`local` 隐藏了实际方言。 |
|
||||
| `@deepseek-ai/dsh-tool-pty` | `@deepseek-ai/dsh-tool-terminal` | 面向模型的工具已使用 `terminal_*`;包应采用相同的产品名词。 |
|
||||
| 原 PTY 系列中的 `tool-bash-persistent` | `shell/tool-bash-persistent/` | 该工具是 Bash 工具,应与 shell 工具放在一起。保留其 NPM 名称:`persistent` 将它与一次性 `bash` 区分开来,而 `bash-terminal` 会混淆产品工具与终端会话系列。 |
|
||||
| `docs/subsystems/pty.md` | `docs/subsystems/terminal.md` | 该页面记录终端会话,而不是原始 PTY 分配。 |
|
||||
|
||||
保留 Bash 和 PowerShell 专用的叶层包、插件 id、类型和工具。这些方言名称准确无误。
|
||||
|
||||
### 语言服务器与作业
|
||||
|
||||
| 旧名称 | 当前名称 | 理由 |
|
||||
|---|---|---|
|
||||
| `@deepseek-ai/dsh-lsp-local` | `@deepseek-ai/dsh-lsp-stdio` | 该提供方通过可替换的文件系统和子进程服务,以 stdio 传输 LSP。它不一定在本地运行。 |
|
||||
| `packages/tasks/` | `packages/jobs/` | 该系列负责脱离前台运行的工具作业。`jobs` 简短,并可避免与用户任务或 todo 概念冲突。 |
|
||||
| `@deepseek-ai/dsh-tasks`, `ctx.tasks`, `TaskService` | `@deepseek-ai/dsh-jobs`, `ctx.jobs`, `JobRegistry` | 该服务注册、拥有、观察、等待并取消多个后台作业。它是注册表,而不是通用任务服务。 |
|
||||
| 公开的 `TaskId`、`TaskKindMap`、`TaskStart`、`TaskHooks`、`TaskOutcome`、`TaskSnapshot`、`TaskRead` 和 `TaskDoneListener` 名称 | 对应的 `Job*` 名称 | 这些类型属于重命名后的作业领域。`JobId` 比 `BackgroundTaskId` 或 `BgTaskId` 更短、更清晰。 |
|
||||
| `@deepseek-ai/dsh-tasks-local`, `LocalTaskService` | `@deepseek-ai/dsh-jobs-local`, `LocalJobRegistry` | 这是作业注册表的进程内提供方。此处的 `local` 有明确含义,因为作业和回调都存在于同一进程。 |
|
||||
| `@deepseek-ai/dsh-tool-tasks` | `@deepseek-ai/dsh-tool-jobs` | 消费方控制作业注册表,应使用相同的领域名词。 |
|
||||
| `ToolTasks`、`toolTasks`、`ToolTasksConfigSchema`、`PublicTaskSnapshot`、`publicTask`、`validateTaskId` | 对应的 `*Jobs`、`*Job*` 与 `validateJobId` 名称 | import、转发配置、公开工具值与辅助函数都属于同一个作业领域。包重命名后继续保留 `Task`,会为同一功能制造第二套词汇。 |
|
||||
| `task_output`, `task_list`, `task_kill` | `job_output`, `job_list`, `job_kill` | 这些模型工具操作的是作业,而不是用户任务。`run_in_background` 返回 `JobId`。 |
|
||||
| `@deepseek-ai/dsh-client-ui-task`、`client/ui-task/` | `@deepseek-ai/dsh-client-ui-jobs`、`client/ui-jobs/` | 该客户端包呈现后台作业集合,而不是一项用户任务。 |
|
||||
| `TaskView`、线路帧 `session/tasks`、`tasksBySession` | `JobView`、线路帧 `session/jobs`、`jobsBySession` | 浏览器约定及其镜像应采用与注册表和工具相同的作业领域名称。 |
|
||||
| `docs/subsystems/tasks.md` | `docs/subsystems/jobs.md` | 该子系统页面必须采用公开的作业词汇。 |
|
||||
|
||||
保留基础 LSP 包、`ctx.lsp`、LSP 协议类型和 LSP 工具。该 seam 有意公开语言服务器语义;错误的只有提供方限定词。
|
||||
|
||||
### 输入触发器、工具呈现、权限预设和用户问题
|
||||
|
||||
| 旧名称 | 当前名称 | 理由 |
|
||||
|---|---|---|
|
||||
| `@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`。 |
|
||||
| `@deepseek-ai/dsh-agent-tool-mode`,插件 `tool-mode` | `@deepseek-ai/dsh-agent-tool-presentation`,插件 `tool-presentation` | 该插件改变工具向模型呈现的方式,而不改变执行行为。保留局部 `Config.mode` 和 `ToolPresentationMode`。 |
|
||||
| `packages/interaction/permission/` | `packages/interaction/permission-presets/` | 该包拥有沙箱与审批设置的具名组合,而不负责执行权限。 |
|
||||
| `@deepseek-ai/dsh-permission`, `ctx.permission`, `PermissionService` | `@deepseek-ai/dsh-permission-presets`, `ctx.permissionPresets`, `PermissionPresetService` | 该服务选择并持久化预设。沙箱和审批服务负责执行结果。 |
|
||||
| `@deepseek-ai/dsh-client-ui-permission` | `@deepseek-ai/dsh-client-ui-permission-presets` | UI 编辑和选择权限预设。 |
|
||||
| `docs/subsystems/permission.md` | `docs/subsystems/permission-presets.md` | 该页面记录预设选择,而不是权限执行。 |
|
||||
| `@deepseek-ai/dsh-user-interaction`, `user-interaction/` | `@deepseek-ai/dsh-user-questions`, `user-questions/` | 该 seam 仅支持批量问题和答案。审批、命令和目录选择属于其他交互 seam。 |
|
||||
| `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` 工具呈现模式的工作仍推迟到另一份提案;本次重命名不移除行为。
|
||||
|
||||
### Typert、API 网关与工具
|
||||
|
||||
| 旧名称 | 当前名称 | 理由 |
|
||||
|---|---|---|
|
||||
| `packages/typert/type-meta/`, `@deepseek-ai/dsh-type-meta` | `typert/protocol/`, `@deepseek-ai/dsh-typert-protocol` | 该包拥有 Typert Remote 协议、装饰器、绑定、编解码器、查找逻辑和上下文约定。它不是通用类型元数据。 |
|
||||
| 协议包中的 `GatewayService` | `TypertRemoteService` | 该基类标记要导出为 Remote 的同进程服务。它不是 API 网关。 |
|
||||
| `bindTypeRTGateway`、`typertGateway` 绑定 | `bindTypertRemote`、`typertRemote` | 这些绑定公开 Typert Remote 服务,而非具体的 API 网关服务。 |
|
||||
| 公开的 `TypeRT*` 标识符和小驼峰形式的 `typeRT*` 标识符 | `Typert*` 和 `typert*` | `Typert` 是唯一规范的产品拼写。 |
|
||||
| 协议接口 `TypeRTService` | `TypertRegistryContract` | 该协议拥有的接口是现有具体类 `TypertRegistry` 所实现的依赖倒置接口。不同的后缀可避免导入和声明冲突。 |
|
||||
| `ToolRegistry` | `ToolRuntime` | 该类拥有呈现、审批与防护策略、分派、取消、验证、终结和观察。注册只是内部组成部分。 |
|
||||
| `ToolRegistryScheduler`, `TOOL_REGISTRY_SCHEDULER` | `ToolRuntimeScheduler`, `TOOL_RUNTIME_SCHEDULER` | 调度器控制运行时分派,而不是注册。 |
|
||||
|
||||
保留 `@deepseek-ai/dsh-tools` 和 `ctx.tools`。保留 `@deepseek-ai/dsh-api-gateway`、其 `gateway/` 目录、`ctx.typertGateway` 以及 `TypertGatewayService`;该服务是真正的 API 网关。其内部的 `TypeRT*` 标识符仍应遵循 `Typert*` 拼写规则。
|
||||
|
||||
### 工作区指令、遥测、身份和启动环境
|
||||
|
||||
| 旧名称 | 当前名称 | 理由 |
|
||||
|---|---|---|
|
||||
| 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` 文件。它并非通用工作区上下文。 |
|
||||
| 插件名称和持久来源名称 `workspace-context` 与 `workspace-instructions` | `agent-instructions` | 记录的来源是一类具体的 agent 指令。以 `AgentInstruction*` 替换公开的 `WorkspaceInstruction*` 名称。该术语不包括系统消息、开发者消息或用户消息。 |
|
||||
| `ctx.telemetry`、抽象类 `Telemetry` | `ctx.sessionTelemetry`、`SessionTelemetryBackend` | 该服务捕获会话账本遥测,并交给报告后端。它不是仓库级指标或追踪服务。 |
|
||||
| `TelemetryBackend` | `SessionTelemetrySink` | 该底层接收已发出的记录。`Sink` 用于将它与协调型后端服务区分开。 |
|
||||
| `TelemetryCoordinator`、`TelemetryRecord`、`TelemetrySeverity`、`TelemetrySharingStatus` 和 `TelemetryCapture` | 对应的 `SessionTelemetry*` 名称 | 这些公开类型只属于会话遥测。 |
|
||||
| `telemetry/record` | `session-telemetry/record` | 事件名称必须说明所属领域。 |
|
||||
| `TelemetryOtel`、`TelemetryMode`,插件 `telemetry-otel` | `OpenTelemetrySessionBackend`、`SessionTelemetryMode`,插件 `session-telemetry-otel` | 提供方名称同时说明 OpenTelemetry 机制和会话作用域。保留包名 `dsh-session-telemetry` 和 `dsh-session-telemetry-otel`。 |
|
||||
| `docs/subsystems/telemetry.md` | `docs/subsystems/session-telemetry.md` | 该页面记录会话遥测,而不是仓库级可观测性。 |
|
||||
| `session/user-id/`, `@deepseek-ai/dsh-user-id` | `identity/anonymous-user-id/`, `@deepseek-ai/dsh-anonymous-user-id` | 该值是遥测、反馈和 DeepSeek 请求共用的随机关联 id。它既不属于 Session 领域,也不是经过身份验证的用户身份。 |
|
||||
| `USER_ID_FILE_NAME`、`.userid`,反馈标签 `User` | `ANONYMOUS_USER_ID_FILE_NAME`、`.anonymous-user-id`,反馈标签 `Anonymous user` | 文件和 UI 不得暗示账户身份。保留现有 `AnonymousUserId` 函数和标准 OTel 属性 `user.id`。 |
|
||||
| `util/environment/`, `@deepseek-ai/dsh-environment` | `util/launch-environment/`, `@deepseek-ai/dsh-launch-environment` | 该包在启动时捕获一份不可变的分层快照。它不是通用环境 API。 |
|
||||
| 公开的 `Environment*`、`createEnvironmentSnapshot`、`environmentOf`、`DSH_ENVIRONMENT_KEY` | `LaunchEnvironment*`、`createLaunchEnvironmentSnapshot`、`launchEnvironmentOf`、`DSH_LAUNCH_ENVIRONMENT_KEY` | 这些名称说明快照的生命周期和用途。 |
|
||||
| `ctx.launcherEnvironment` | `ctx.launchEnvironment` | 该值描述应用启动,而不只描述启动器组件。保留来源标签 `process`、`project-env` 和 `user-env`。 |
|
||||
|
||||
### 日程、工作流、目标与压缩
|
||||
|
||||
| 旧名称 | 当前名称 | 理由 |
|
||||
|---|---|---|
|
||||
| `@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*`。 |
|
||||
| `WorkflowService`, `ctx.workflows` | `WorkflowEngine`, `ctx.workflowEngine` | 一个引擎负责解析并执行工作流程序。复数键错误地暗示这是注册表。保留 `@deepseek-ai/dsh-workflow` 以及工作流事件和工具。 |
|
||||
| `@deepseek-ai/dsh-workflow-workerthread`, `WorkerWorkflowEngine` | `@deepseek-ai/dsh-workflow-worker-thread`, `WorkerThreadWorkflowEngine` | `worker thread` 是准确的 Node 机制,仓库拼写要求使用完整单词。 |
|
||||
| `@deepseek-ai/dsh-goal-session`, `goal/goal-session/` | `@deepseek-ai/dsh-goal-round-driver`, `goal/goal-round-driver/` | 该插件驱动同一会话内的 Goal Rounds。它既不存储目标,也不定义会话。保留 `GoalService`、目标来源、事件和约定。 |
|
||||
| `packages/compact/` | `packages/compaction/` | 该组是以名词命名的领域系列。`compact` 仍作为面向用户的命令动词。 |
|
||||
| `@deepseek-ai/dsh-compact`, `ctx.compact`, `CompactService` | `@deepseek-ai/dsh-compaction`, `ctx.compaction`, `CompactionEngine` | 该对象运行压缩(compaction)算法和生命周期。它是引擎,而不是通用服务。 |
|
||||
| `compact/*` 事件和公开领域前缀 | `compaction/*` | 事件和领域类型使用名词形式。保留动词形式的操作,例如 `compactNow`、`compactRegion` 和 `compactIfNeeded`。 |
|
||||
| `@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`、命令包,以及相互独立的压缩定义包和提供方包。合并这些包的提议仍被否决。本次重命名只改变词汇,不改变该包边界。
|
||||
|
||||
### 设置、凭据、客户端模块和较小的核心职责
|
||||
|
||||
| 旧名称 | 当前名称 | 理由 |
|
||||
|---|---|---|
|
||||
| 抽象类 `Settings` | `SettingsProvider` | 该类通过可替换能力提供设置。保留包、键和事件。 |
|
||||
| `@deepseek-ai/dsh-settings-local`, `SettingsLocal` | `@deepseek-ai/dsh-settings-file`, `FileSettingsProvider` | 该实现通过文件系统 seam 以文件为后端。`file` 说明机制,`local` 则不能。 |
|
||||
| 抽象类 `Credentials` | `CredentialProvider` | 该类解析凭据引用。保留包名、键和事件。 |
|
||||
| `CredentialsLocal` | `LocalCredentialProvider` | 该提供方读取宿主进程和 `.env` 状态,因此本地执行属于其约定。 |
|
||||
| `ClientModuleHostService`, `ctx.clientModuleHost` | `ClientModuleRegistry`, `ctx.clientModules` | 该服务拥有多个已注册的客户端模块。保留包和浏览器端的 `ClientModuleLoader`。 |
|
||||
| `AgentDefaultModelService` | `AgentDefaultModelConfig` | 该对象存储一项默认模型选择。它不运行服务,也不是通用注册表。保留其包、键、设置命名空间和类型。 |
|
||||
| `SessionReferenceService`, `ctx.sessionReferences` | `SessionReferenceResolver`, `ctx.sessionReferenceResolver` | 它从 URI 或输入解析一个会话引用,并不拥有引用集合。 |
|
||||
| `SessionQueryService`, `SessionQuerySqlite` | `SessionQueryEngine`, `SqliteSessionQueryEngine` | 这些类执行查询模型及其 SQLite 实现。保留包名、键和工具。 |
|
||||
| `@deepseek-ai/dsh-session-export`, `session-export/`, Loader id `session-export`, `ctx.sessionExport` | `@deepseek-ai/dsh-session-log-export`, `session-log-export/`, Loader id `session-log-download`, `ctx.sessionLogDownload` | npm 包名使用 Session 日志导出语义,因为 npm 禁止包名包含 `download`。Loader id 与浏览器 API 保留 `download`,因为它们描述浏览器副作用。 |
|
||||
| `SessionExportDownloadController`, 其他 `SessionExport*` 浏览器类型、`useSessionExport`、`SessionExportHeader` | `SessionLogDownloadController`, 对应的 `SessionLogDownload*` 类型、`useSessionLogDownload`、`SessionLogDownloadHeaderAction` | 该 controller 拥有预检、重复请求合并、弹窗状态和浏览器保存。`ExportDownload` 重复表达同一动作,该组件贡献的是一个 Header action,不是整个 Header。 |
|
||||
| 宿主命令包中的 `CommandService` | `CommandRuntime` | 该对象跨实时调用注册并执行宿主命令。保留其包、键、类型和事件。 |
|
||||
| `TokenMeterService` | `TokenMeter` | 该对象测量 token 用量。`Service` 没有补充作用域信息。 |
|
||||
| `LlmService` | `LlmRuntime` | 该对象选择提供方并运行实时模型请求。保留包、键、适配器和事件。 |
|
||||
|
||||
### 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` | 子系统标签必须与服务采用相同作用域。 |
|
||||
| `SessionPersistenceJsonl` | `JsonlSessionPersistence` | 将实现限定词放在前面,同时完整保留能力职责。 |
|
||||
| `SessionPersistenceSqlite` | `SqliteSessionPersistence` | 采用与 JSONL 相同的提供方命名顺序。 |
|
||||
| `@deepseek-ai/dsh-session-title-first-message-llm`,触发周期 `first-message` | `@deepseek-ai/dsh-session-title-first-prompt-llm`,触发周期 `first-prompt` | 触发条件是第一条用户提示词,而不是会话日志中的任意消息。 |
|
||||
| `@deepseek-ai/dsh-session-title-all-messages-llm`,触发周期 `all-user-messages` | `@deepseek-ai/dsh-session-title-all-prompts-llm`,触发周期 `all-prompts` | 后端根据用户提示词刷新。`all messages` 会错误地包含助手消息和工具事件。 |
|
||||
| `@deepseek-ai/dsh-code-runtime-worker`, `WorkerCodeRuntime` | `@deepseek-ai/dsh-code-runtime-worker-thread`, `WorkerThreadCodeRuntime` | 该实现使用 Node 工作线程。单独的 `worker` 作用域过宽。 |
|
||||
| `SubprocessService` | `SubprocessRuntime` | 该服务拥有实时子进程的执行和生命周期。保留其包和键。 |
|
||||
| `LocalSubprocessService` | `LocalSubprocessRuntime` | 该提供方运行同主机进程和进程树。 |
|
||||
| `E2BSubprocessService` | `E2BSubprocessRuntime` | 该提供方在 E2B 运行时中运行子进程。 |
|
||||
|
||||
保留完整的会话投影系列和 `SessionProjection*` 词汇。投影是持续维护的读取模型;`Reducer` 只说明其折叠操作,会淡化缓存和查找职责。保留 `SessionTitleService`、检查点策略、持久化包名、时间上下文和 tmux 上下文。
|
||||
|
||||
### 文件系统、skill、subagent 和 Web 提供方
|
||||
|
||||
| 旧名称 | 当前名称 | 理由 |
|
||||
|---|---|---|
|
||||
| `@deepseek-ai/dsh-fs-policy` | `@deepseek-ai/dsh-fs-observation-policy` | 该包定义哪些文件系统观察可以授权后续操作。它不是完整的文件系统策略或沙箱策略。 |
|
||||
| `FsPolicyExec` | `FsObservationActor` | 该值表示策略所关联的观察与操作的执行主体。它本身不执行策略。 |
|
||||
| `SkillService` | `SkillRegistry` | 该服务注册提供方,并从其目录解析 skill(技能)。 |
|
||||
| `@deepseek-ai/dsh-skill-local`、`LocalSkillProvider`,提供方 id `local` | `@deepseek-ai/dsh-skill-filesystem`、`FileSystemSkillProvider`,提供方 id `filesystem` | 该提供方通过可位于本地或远端的 `ctx.fs` 发现 skill 文件。其机制是文件系统访问,而不是本地性。 |
|
||||
| `SubagentService` | `SubagentRuntime` | 该服务选择提供方,并拥有实时 spawn、恢复、跟进、取消和结算行为。 |
|
||||
| `@deepseek-ai/dsh-subagent-spawn`, `SpawnProvider` | `@deepseek-ai/dsh-subagent-spawn-in-process`, `SpawnInProcessProvider` | 该提供方在当前进程内启动子 agent。配置的提供方 id 仍为 `spawn`。 |
|
||||
| `@deepseek-ai/dsh-subagent-fork`, `ForkProvider` | `@deepseek-ai/dsh-subagent-fork-in-process`, `ForkInProcessProvider` | 该提供方在当前进程内 fork 一个 agent。配置的提供方 id 仍为 `fork`。 |
|
||||
| `@deepseek-ai/dsh-subagent-inprocess`, `subagent-inprocess/` | `@deepseek-ai/dsh-subagent-in-process-driver`, `subagent-in-process-driver/` | 该包包含通用的进程内驱动逻辑,而不是第三个提供方。 |
|
||||
| 私有的 `SdkProvider`,位于 `dsh-subagent-dsh-sdk` 中 | `SdkSubagentProvider` | 重复的包限定词是有意保留的,类名还必须说明它通过 SDK 提供 subagent。 |
|
||||
| `WebService`, `WebServiceConfig` | `WebRuntime`, `WebRuntimeConfig` | 该对象选择提供方并运行实时搜索和抓取操作。保留包、键、提供方包和模型工具。 |
|
||||
| `@deepseek-ai/dsh-web-fetch-local`、`LocalFetchProvider`、`LocalFetchLimits`,提供方 id `local-http` | `@deepseek-ai/dsh-web-fetch-http`、`HttpFetchProvider`、`HttpFetchLimits`,提供方 id `http` | 该提供方执行直接 HTTP 抓取。`local` 只说明代码恰好在哪里运行,并未说明它提供哪种机制。 |
|
||||
|
||||
保留 `@deepseek-ai/dsh-subagent-dsh-sdk`、其提供方 id `dsh-sdk`、外部 ACP(Agent Client Protocol)、Codex 和 Claude Code 提供方系列、subagent 工具包名、主文件系统包和后端、文件系统工具和事件,以及 skill 徽章和工具包。
|
||||
|
||||
### 钩子、防护、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` | 该插件向模型添加提醒,并不阻止工具调用,也不执行防护决策。 |
|
||||
| `@deepseek-ai/dsh-timeout-policy` | `@deepseek-ai/dsh-tool-call-timeout-policy` | 完整的 `tool-call` 限定词说明该策略限制的对象,而不会把插件称为面向模型的工具。保留其 `guard/timeout-policy/` 目录和插件 id `timeout-policy`;`packages/*/tool-*` 目录约定仍只适用于注册工具的包。 |
|
||||
| `PlanModeService` | `PlanModeController` | 该对象控制进入和退出计划模式的状态转换,而不是通用执行运行时。 |
|
||||
| `packages/self-modification/` | `packages/extensions/` | 该组包含仓库插件检查和挂载工具。`extensions` 说明稳定的包职责,但不声称 agent 会修改自身。保留包名 `tool-cordis` 和仓库插件名称。 |
|
||||
| `packages/support/` | `packages/test-support/` | 该组仅包含测试基础设施,其路径必须明确说明这一点。 |
|
||||
| 原 support 系列中的 `invariants/` | `runtime-diagnostics/invariants/` | 尽管交付预设未包含不变量检查,它们仍可在生产诊断中运行,因此不属于测试支持。 |
|
||||
| `InvariantService` | `InvariantRegistry` | 该对象拥有已注册的不变量检查。保留 `@deepseek-ai/dsh-invariants` 和 `ctx.invariants`。 |
|
||||
| `packages/client/test-runtime/` | `packages/test-support/client-runtime/` | 该包是客户端测试基础设施。如果现有 NPM 名称已经说明这一约定,则予以保留。 |
|
||||
|
||||
保留 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` | 该策略保留命令和工具输出,而不是通用数据保留框架。 |
|
||||
| `E2BSandboxService` | `E2BRuntime` | 该类创建、复用和释放文件系统与子进程适配器所使用的 E2B 执行环境。它比单个沙箱句柄的职责更广,又比通用所有者更具体。保留 `@deepseek-ai/dsh-e2b`、`ctx.e2b` 和 `e2b/` 组。 |
|
||||
| `@deepseek-ai/dsh-frontend-static` | `@deepseek-ai/dsh-host-frontend-static` | 该包是提供前端资源的 Host 插件。此前缀可将它与前端应用代码区分开。 |
|
||||
| `PluginInventoryService` | `PluginInventoryGateway` | 该类只负责把实时 Loader 树适配到 `pluginInventory/list` RPC。它不拥有同进程服务、缓存、历史或修改路径。`Gateway` 准确说明现有角色。 |
|
||||
| `@deepseek-ai/dsh-jsonrpc-demo` | `@deepseek-ai/dsh-sdk-jsonrpc-demo` | 该示例演示通过 JSON-RPC 使用运行时 SDK,属于 SDK 的唯一含义。 |
|
||||
| `@deepseek-ai/dsh-frontend` | `@deepseek-ai/dsh-web-frontend` | 该应用是 Web 前端。保留其物理目录 `apps/web/`。 |
|
||||
|
||||
保留 atomic-write、brand、native-command、timeout 实用工具、目录选择器、`dsh-base`、`dsh-web-app`、应用启动、CLI(命令行界面)名称,以及 `headless` 包、组合包和示例身份。`headless` 是预期的产品本质,未来也可以支持不止一次性执行。
|
||||
|
||||
### 客户端运行时与 UI
|
||||
|
||||
| 旧名称 | 当前名称 | 理由 |
|
||||
|---|---|---|
|
||||
| `SlotsService` | `SlotRegistry` | 该对象拥有具名 slot 声明和注册项。 |
|
||||
| `SessionsService` | `SessionRuntime` | 该对象拥有实时客户端会话协调职责,而不是被动的会话列表。 |
|
||||
| `WorkspacesService` | `WorkspaceRuntime` | 该客户端对象协调实时工作区选择和操作。如果清单未点名更改某个现有 `ctx` 键,则该键保持不变。 |
|
||||
| `WorkspaceGroupBy`、`WorkspaceOrderBy`、`workspaceExpansion`、`setWorkspaceExpanded`、`expandedProjects`、`projectLabel`、`recentSessionOrder`、`recentSessionUpdatedAt`、`syncRecentSessions`、`setRecentSessionOrder`、`retainWorkspaceKeys`、`workspaceKey` | `SessionGroupBy`、`SessionOrderBy`、`groupExpansion`、`setGroupExpanded`、`expandedGroups`、`workspaceLabel`、`sessionOrderByAccount`、`sessionUpdatedAtByAccount`、`syncSessionOrderAccount`、`setSessionOrder`、`retainAccountKeys`、`accountKey` | 这些名称描述的是会话列表查看状态。其 account 包括真实工作区、未分组项和平铺列表。因此,`Workspace`、`project` 和 `recent` 指向了错误的对象或机制。保留 `WorkspaceViewState`;该存储仍属于工作区浏览器。 |
|
||||
| `LocaleService` | `LocaleRuntime` | 该对象协调区域设置定义、选择、持久化和变更发布。 |
|
||||
| `ThemeService` | `ThemeRuntime` | 该对象协调主题、偏好解析、系统感知和变更发布。 |
|
||||
| `LayoutService` | `LayoutController` | 该对象控制当前 UI 布局状态。 |
|
||||
| `@deepseek-ai/dsh-client-ui-model` | `@deepseek-ai/dsh-client-ui-model-selection` | 该包控制会话的模型选择。单数 `model` 名称作用域过宽。 |
|
||||
| `ModelService`, `ctx.models` | `ModelDirectoryResolver`, `ctx.modelDirectories` | 它唯一的公开操作 `directoryFor(sessionId)` 为每个实时会话解析并保留一个目录。它没有注册 API,因此使用 `Registry` 并不准确。每个 `ModelDirectory` 仍是面向消费方的可选模型目录。 |
|
||||
| `SettingsScopeService` | `SettingsScopeBinder` | 它唯一的操作把一份命名空间规范绑定到调用方的传输层和生命周期,并返回 `SettingsScopeController`。保留 `ctx.settingsScope`;它命名的是单一绑定能力,而不是 scope 集合。 |
|
||||
| `@deepseek-ai/dsh-client-ui-models` | `@deepseek-ai/dsh-client-ui-settings-models` | 该包拥有 Models 设置面板。保留 `ModelsSettingsStore`;它保存一个具有数据操作和订阅能力的设置视图模型,确实是存储。 |
|
||||
| `@deepseek-ai/dsh-client-ui-plugin-config`、`client/ui-plugin-config/` | `@deepseek-ai/dsh-client-ui-settings-plugins`、`client/ui-settings-plugins/` | 该包拥有 Plugins 设置分区,而不是通用的插件配置系统。目标名称归入 `ui-settings-*` 系列,并采用该分区的复数产品名。 |
|
||||
| `PluginConfigSection`、`PluginConfigSectionProps`、`PluginConfigSectionInjected`、`PluginSettingsTabRow`、`PluginConfigKey`、`settings.pluginConfig` | `PluginsSettingsSection`、`PluginsSettingsSectionProps`、`PluginsSettingsSectionInjected`、`PluginsSettingsTabEntry`、`PluginsSettingsLocaleKey`、`settings.plugins` | 该分区拥有 Plugins 设置呈现和 tab 清单。元数据值表示一项 slot entry,而不是一条渲染行。每张卡片仍编辑一个插件的配置。 |
|
||||
| `@deepseek-ai/dsh-client-ui-plugins`、`client/ui-plugins/`、Loader id `ui-plugins`、`client-ui-plugins-invariant` | `@deepseek-ai/dsh-client-ui-settings-plugin-inventory`、`client/ui-settings-plugin-inventory/`、Loader id `ui-settings-plugin-inventory`、`client-ui-settings-plugin-inventory-invariant` | 这个后来加入的包拥有 Plugins 设置分区中的只读 Plugin Inventory tab。`ui-plugins` 作用域过宽,也无法将该清单与可编辑插件设置区分开。 |
|
||||
| 原 `ui-plugins` 包中的 `PluginSettingsSection`、`PluginSettingsSectionProps`、`PluginSettingsSectionInjected`、`PluginsKey`、`settings.plugins` | `PluginInventorySettingsTab`、`PluginInventorySettingsTabProps`、`PluginInventorySettingsTabInjected`、`PluginInventoryLocaleKey`、`settings.pluginInventory` | 该组件现在贡献一个 tab,而不是设置分区。其余名称明确说明清单主题,并避免与 `PluginsSettingsSection` 及其 `settings.plugins` 区域设置命名空间冲突。保留共享的 `settings.plugins.tab` slot 名;两个 tab 都通过该 slot 向 Plugins 分区贡献内容。 |
|
||||
| `@deepseek-ai/dsh-client-ui-feedback`、`client/ui-feedback/`、Loader id `ui-feedback`、`client-ui-feedback-invariant` | `@deepseek-ai/dsh-client-ui-message-feedback`、`client/ui-message-feedback/`、Loader id `ui-message-feedback`、`client-ui-message-feedback-invariant` | 这个包通过 `messageFeedback` Remote 展示 assistant 消息的评分和说明。旧名称看起来还涵盖 command feedback 和以后可能出现的其他反馈界面,但实际并非如此。 |
|
||||
| 原 `ui-feedback` 包中的 `FeedbackController`、`FeedbackStatus`、`FeedbackView`、`FeedbackActionResult`、`FeedbackInjected`、`FeedbackActionProps`、`FeedbackActions`、`FeedbackKey` | `MessageFeedbackController`、`MessageFeedbackStatus`、`MessageFeedbackView`、`MessageFeedbackActionResult`、`MessageFeedbackInjected`、`MessageFeedbackActionProps`、`MessageFeedbackActions`、`MessageFeedbackKey` | 这些名称会从 Client 包导出。增加 `Message` 限定词,避免它们声称代表所有反馈领域。保留 `Controller`:该对象接受评分和说明操作,并协调一个 Session 的加载、修改、冲突、重连和释放状态。 |
|
||||
| `agent-loop-store.ts`、`bash-store.ts`、`web-search-store.ts` | `agent-loop-card-controller.ts`、`bash-card-controller.ts`、`web-search-card-controller.ts` | 每个模块都导出一个卡片控制器。私有 `SnapshotStore` 字段不会让模块成为存储。 |
|
||||
| `card-store.ts` | `card-form.ts` | 该模块拥有暂存表单、字段转换和表单操作。它返回的快照存储是呈现适配器,而不是模块的主要职责。 |
|
||||
| `@deepseek-ai/dsh-client-ui-question` | `@deepseek-ai/dsh-client-ui-user-questions` | UI 呈现用户问题 seam,而不是任意问题领域。 |
|
||||
| `@deepseek-ai/dsh-client-ui-command`, `ui-command/` | `@deepseek-ai/dsh-client-ui-commands`, `ui-commands/` | 该包呈现并运行一组命令。 |
|
||||
| `@deepseek-ai/dsh-client-ui-directory-picker`、`client/ui-directory-picker/`、Loader id `ui-directory-picker`、`client-ui-directory-picker-invariant` | `@deepseek-ai/dsh-client-ui-directory-picker-browse`、`client/ui-directory-picker-browse/`、Loader id `ui-directory-picker-browse`、`client-ui-directory-picker-browse-invariant` | 客户端包现已拆成 `browse` 和 `native` 两种目录选择器呈现。未加限定词的包实际只是 browse 实现,并非两者的共同定义。目标名称与 Host 后端系列一致,不改变边界。 |
|
||||
| 客户端 `ctx.command`、`CommandService`、`CommandServiceContract` | `ctx.commandUi`、`CommandUiRuntime`、`CommandUiContract` | Host 已拥有 `ctx.commands`。该客户端服务是命令发现和执行的 UI 运行时。现有 `CommandUiSpec` 确立了 `Ui` 大小写格式。 |
|
||||
| `ConversationService` | `ConversationController` | 该对象控制当前对话状态和用户操作。 |
|
||||
| `InputService` | `SessionInputResolver` | 该接口为一个会话作用域解析输入外观。它既不是全局输入注册表,也不是执行服务。保留 `InputHub` 作为具体中枢,并保留 `ctx.conversation.input` 作为对外接口。 |
|
||||
|
||||
PascalCase 标识符内部使用 `Ui`,不要使用 `UI`。除非清单明确要求重命名,否则保留其余客户端包名。暂时保留已弃用的客户端连接和 Host `ApiProxy` 词汇;API 平面将替换它们,而在计划移除的表面上重命名只会增加改动量。
|
||||
|
||||
## 明确保留的名称
|
||||
|
||||
以下经过讨论的名称保持不变,因为当前作用域准确,或重命名会制造虚假概念:
|
||||
|
||||
- 保留完整的 sandbox 系列和 `ctx.sandbox`。不得引入 `processSandbox`。
|
||||
- 保留 `@deepseek-ai/dsh-api-gateway`、`ctx.typertGateway` 和 `TypertGatewayService`。
|
||||
- 保留会话投影名称。投影并不只是归约函数。
|
||||
- 保留 `@deepseek-ai/dsh-session-stats`、`sessionStats` 和 `SessionStatsProjection`。这些名称准确表示全会话统计数据及承载它们的持续维护读模型。
|
||||
- 保留 `GoalService`;它拥有目标状态机、裁决权、比较并设置行为、事件和远程操作,不只是存储。
|
||||
- 保留 `SessionTitleService`;它的职责是由多个标题提供方共享的领域服务。
|
||||
- 保留 `PermissionPresetSettingsController`,即使它很长。每个词都在限定其职责。
|
||||
- 保留 `ModelsSettingsStore`;其主要约定是一个具有存储操作的设置数据模型。
|
||||
- 保留 `InputHub`;它是支撑 `SessionInputResolver` 的具体中枢。
|
||||
- 保留 `dsh-subagent-dsh-sdk` 和提供方 id `dsh-sdk`;重复的限定词可避免歧义。
|
||||
- 保留 `headless`;即使运行时以后支持不止一次性使用,该产品身份仍然准确。
|
||||
- 保留已弃用的 Host `ApiProxy` 和客户端连接名称,直至 API 替代方案将其移除。
|
||||
- Host 服务器和提供方无关的 Web 能力都保留 `Web`。仅直接抓取提供方使用 `HTTP`。
|
||||
- 保留 `E2B` 作为包名和上下文名称,不改为 `E2B sandbox`。
|
||||
- 保留 MCP、Todo、应用启动、基础组合包、web-app 组合包和 CLI 名称。保留目录选择器能力和 Host 后端名称;只重命名未加限定词的 Client `browse` 呈现。
|
||||
- 保留 `@deepseek-ai/dsh-client-ui-directory-picker-native`;其后缀说明它是在重命名后的 `-browse` 变体旁使用原生选择器的呈现。保留 `SURFACE_PACKAGES`;在目录选择器自动选择器中,它是客户端呈现端面的包映射,并与 `BACKEND_PACKAGES` 对照。
|
||||
- 保留 `@deepseek-ai/dsh-host-plugin-inventory`、`ctx.pluginInventory`、`pluginInventory/list` Remote 以及 `PluginInventory*` 载荷类型。它们准确命名由 Host 拥有的只读清单;只有适配器类和作用域过宽的客户端呈现名称需要修改。
|
||||
- 保留 `ConfigurablePluginsTab`。该 tab 渲染具有可编辑配置的插件,不拥有完整的 Plugins 设置分区。
|
||||
- 保留共享的 `settings.plugins.tab` slot。它属于 Plugins 设置分区。清单包只把自己的 locale namespace 改为 `settings.pluginInventory`,不会创建独立的 tab slot。
|
||||
- 保留 `@deepseek-ai/dsh-message-feedback` 能力、`messageFeedback` Remote、assistant-action entry id `feedback`、hook key `feedback` 和 locale namespace `feedback`。它们所在的接口已经把作用域限定为消息反馈或本地 assistant-message slot。只修改作用域过宽的 Client 包名和导出的 UI 名称。
|
||||
- 保留 `RemoteFailure`、`RemoteResult` 和 `SessionRemotes`。前两者是 Typert 载体结果值,后者是客户端 Session 集群使用的一组 Remote 命名空间。它们都不是 store、controller、registry 或 runtime。
|
||||
- 保留用户命令 `/export`、Host 路由 `/api/session.export`、`DownloadsApi` 及其 `sessionLog` 操作。命令说明用户动作,Host 路由导出归档,API 则归类直接 HTTP 下载。重命名的 Client controller 拥有独立的浏览器下载步骤。
|
||||
- 测试文件名保留 `.client` 和 `.host`。它们标识测试进入的编译端面,不声称产品职责。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
**保留现有名称并添加词汇表。**不予采纳。词汇表无法让由 PowerShell 实现的 `BashExecutor` 名副其实,也无法让 `ToolRegistry` 表明它会执行并强制实施工具策略。标识符本身必须承载有用的区别。
|
||||
|
||||
**为每个 NPM 包添加所属组前缀。**不予采纳。扁平的 NPM 名称不需要复刻目录树。机械添加前缀只会增加长度,无法解释包的职责。
|
||||
|
||||
**将整个仓库称为 SDK。**不予采纳。该项目是 agent harness(智能体框架)。SDK 是 Python 和 TypeScript 客户端使用的、受支持的 JSON-RPC 客户端/服务器栈。一词两义会使包名和产品文案产生歧义。
|
||||
|
||||
**所有 Cordis 服务类都使用 `Service`。**不予采纳。Cordis 继承只是实现事实。类名必须告诉调用方该对象负责注册、存储、解析、控制还是运行工作。
|
||||
|
||||
**统一使用 `Runtime` 替换 `Service`。**不予采纳。只有对象拥有实时执行或生命周期时,`Runtime` 才正确。注册表、存储、目录、控制器、解析器、引擎和配置对象都应保留更精确的职责名。
|
||||
|
||||
**优先使用最短的名称。**不予采纳。只有作用域明确之后,简短才有价值。`PermissionPresetSettingsController` 保留 `Preset`;`JobId` 简短,是因为 `Job` 已经表明领域;`BgTaskId` 虽短,却晦涩难懂。
|
||||
|
||||
**为未来可能出现的功能使用宽泛名称。**不予采纳。应按稳定的当前职责命名。未来若要改变边界,可以在发布前再次重命名对象,或在发布后另写提案。含义模糊的名称会让每位当前读者为尚未构建的未来付出理解成本。
|
||||
|
||||
**将 `dsh-compact-basic` 重命名为 `dsh-compaction-llm`。**不予采纳。`LLM` 没有在当前后端系列中增加区别。`basic` 意图更克制,也不会声称存在一个实际并不存在的算法。
|
||||
|
||||
**将会话投影重命名为归约器。**不予采纳。归约只是构建投影的方式。该包还拥有读取模型值、缓存和查找约定。
|
||||
|
||||
**将持久 Bash 工具重命名为 `bash-terminal`。**不予采纳。该名称与终端会话系列冲突。将 `tool-bash-persistent` 移到 `shell/` 可以纠正其归属位置,同时现有名称仍能将其与一次性 Bash 工具区分开。
|
||||
|
||||
**应用清单时一并重命名或拆分边界。**不予采纳。评审人必须能够确认行为没有改变。真正的边界缺陷需要独立提案、测试和后果分析。
|
||||
|
||||
**为旧名称保留别名。**不予采纳。没有已发布的消费方需要这些别名。别名会保留两套词汇,使首次发布携带一项从未有用户需要的迁移。
|
||||
|
||||
## 验证
|
||||
|
||||
- 清单中的每项映射都出现在仓库中。每个系列只有一套公开词汇;同一个 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 项目工具链继续保持不存在。
|
||||
- `pnpm run check:ci` 覆盖源代码平面的类型检查、构建、包卫生检查、生成参考资料检查、受影响的快照、翻译配对、`doc-sync` 和 lint。发布形态的 Python 运行时冒烟测试和必需 CI 覆盖打包运行时与平台路径。
|
||||
|
||||
## 后果
|
||||
|
||||
仓库为每个重命名系列保留一套词汇。清单点名的旧磁盘名称、协议值、工具名称和配置项不再工作。能够识别陈旧配置的所属解析器会明确报错,而不是同时接受两种形式。
|
||||
|
||||
一些名称更长。额外增加的词只有在防止误述权限或机制时才有意义。如果名称中的词不能全部限定职责,长名称仍然错误。
|
||||
|
||||
职责后缀不能替代对行为的检查。包创建指南保留本决策中的直接判断方式:检查调用方执行什么操作、对象拥有什么生命周期,以及对象控制什么失败或策略。
|
||||
|
||||
基于旧路径和旧符号的分支需要解决冲突。这是发布前移除旧词汇且不保留兼容别名的一次性成本。
|
||||
@@ -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-12-plugin-owned-settings-surface.md
|
||||
2026-08-12-plugin-owned-settings-surface.md: 3137cfe81ef3cb78a940f085c559ab4a7b62cce3
|
||||
2026-08-12-plugin-owned-settings-surface.zh.md: 8dd5e5ccebf1cfb80b55a615f6049dd391943bd7
|
||||
@@ -0,0 +1,61 @@
|
||||
# Agent Note: Plugin-owned settings surface
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-08-12-plugin-owned-settings-surface.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
A plugin that registered a settings namespace could not reach the browser configuration page, and both gates that stopped it lived in this repository.
|
||||
|
||||
`packages/host/apiproxy` held two hardcoded namespace lists. `settings.describe` filtered its answer through them and every write checked them first, so a namespace outside them answered `settings-not-exposed` even when its owner had registered it. Adding a plugin to the configuration page therefore meant editing a package the plugin author does not own.
|
||||
|
||||
The plugin configuration section rendered an unordered list of whatever cards were registered into `settings.plugin.item`. A card carried an opaque `id`, never the namespace it edited, so the section could not tell which served namespaces already had a home. That left every question about "who renders this namespace" unanswerable from the ledger the section could see.
|
||||
|
||||
Together the two meant a user-authored plugin was configurable only by hand-editing `settings.yaml`. The [web plugin configuration note](../feature/2026-08-10-web-plugin-configuration.md) recorded the allowlist as deliberate, and the [config-plane boundaries note](2026-07-30-config-plane-boundaries.md) tied web-configurability to membership in the configurable-provider directory. Both conclusions blocked exactly the plugin authors the general seam was built for.
|
||||
|
||||
## Decision
|
||||
|
||||
**Registering is exposing.** The api-proxy serves every namespace `ctx.settings.describe()` returns and gates no write. `WEB_SETTINGS_NAMESPACES`, `PRODUCT_SETTINGS_NAMESPACES`, the union with `ctx.llm.listConfigurableProviders()`, and the `settings-not-exposed` error code are gone. A name no registration answers — unknown, or malformed and therefore unable to address one — folds into the seam's own `settings-rejected`, so the proxy contributes no boundary and no vocabulary of its own.
|
||||
|
||||
**The settings seam is untouched.** Which client may read a namespace, and which page renders it, are facts about consumers; a Service Definition that carried either would let one Consumer dictate its contract. `SettingsRegisterOptions` gains nothing.
|
||||
|
||||
**`settings.plugin.item` is keyed on the settings namespace.** The slot moved from `list` to `keyed`, the key being the namespace the card edits, following the `tool.call.toolview` precedent where each tool plugin registers its renderer under the tool name. A card declares `key`, not `id`/`order`. The slot is declared by the Plugins section's `configurable` tab, which owns the card list.
|
||||
|
||||
**The tab drives dispatch from the served namespaces.** It reads `settings.describe` once, subscribes to the settings-document invalidation and to connection resets, and dispatches one key per served namespace. What renders is the intersection of two ledgers — namespaces a live Host plugin registered, and cards registered under those keys — computed in the tab's controller from the slot ledger (`ctx.slots.entries`, `ctx.slots.subscribe`) and the wire answer.
|
||||
|
||||
Keying makes absence the signal, and that is what removes the bookkeeping the previous shape needed. A namespace another surface owns (`ui-theme`, `permission`, `llm-*`, `agent-presets`) has no card under its key, so it renders nothing without declaring anything anywhere. A card whose namespace this deployment does not serve is never dispatched, which also fixes the old empty-state defect: the tab counted registered cards, including ones rendering nothing, so a deployment exposing none showed an empty list instead of its empty line.
|
||||
|
||||
**Nothing renders a form it was not given.** The tab supplies no fallback card. A plugin's browser half owns its card completely — chrome, controls, and copy — which is what the slot's `fallback` option would have replaced with a schema-reverse-rendered form.
|
||||
|
||||
## What the allowlist protected
|
||||
|
||||
The gate did keep one thing off the wire, and this note states it plainly because the decision has to survive the accurate version: a registered namespace the list did not name never had its resolved, `base`, or `user` values reach the browser at all. The plugin inventory page is not a substitute — `PluginInventoryEntry` carries `entryId`, `moduleName`, `enabled`, and `fiberPhase`, and its "configuration" row renders an enabled/disabled tag, never a stored value.
|
||||
|
||||
What the gate was not is the boundary its position suggested. Every `settings.*` method sits in `PRIVILEGED_METHODS` (`packages/client/connection`), so a non-loopback or cross-origin request is refused with 403 before reaching this code; `role('secret')` fields are structurally stripped from every layer of every response; and the document the plane edits is the user's own `settings.yaml`, which the same settings page offers to open. The writes it did not block were also the consequential ones: `permission` (which can widen the approval preset) and `agent-presets` (which decides what a session mounts) were both already served.
|
||||
|
||||
So the exposure this change actually adds, in this repository, is one namespace: `agent-default-model`, whose two fields name a provider and a model and which no browser half renders. A future namespace whose values genuinely must not cross the wire is answered per field by `role('secret')` — finer than a namespace switch, and already enforced.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**A declaration on `settings.register()`** (`client: { surface: 'plugin-config' | 'custom', title, description }`), which the removed `WEB_SETTINGS_NAMESPACES` comment named as the intended direction. It keeps registration from crossing the transport by default and lets a plugin author self-serve in one line. Rejected because `surface` is browser-page vocabulary and `title`/`description` are presentation: a Service Definition carrying them is a seam shaped by one Consumer. Its fail-closed property is also worth less than it reads — see what the allowlist protected, above.
|
||||
|
||||
**A separate exposure catalog**, a registry of its own that plugins join beside their settings registration, generalizing `ctx.llm.registerConfigurableProviders()`. Rejected because it makes one fact require two registrations that can drift: registering a namespace and forgetting the catalog entry produces a section nothing can edit, with no gate able to see the mistake.
|
||||
|
||||
**A deny-list `Config` field on the api-proxy**, so a deployment could withhold a namespace. Rejected for having no consumer: every currently registered namespace is one a user may edit, and a genuinely sensitive field is answered per-field by `role('secret')`, which is the finer instrument. A namespace-wide switch invented ahead of its first use is the speculative option the package rules forbid.
|
||||
|
||||
**A schema-driven generic card as the slot's `fallback`**, so a plugin with no browser half still got a form from `schema.toJSON()` (schemastery already carries `description`, `role`, `min`/`max`/`step` and serializes them). Rejected because client plugins load at runtime from mounted Loader entries, so a plugin author can ship a real card, and a reverse-rendered form was already judged worse than a hand-written one for the Models page. The `fallback` option remains available without a contract change if that judgment changes.
|
||||
|
||||
**A client-side claim registry**, where each surface owning a namespace declares it so a generic card knows what is already covered. Rejected with the generic card: keyed dispatch already makes an unclaimed key render nothing, so the registry would restate what the slot ledger says.
|
||||
|
||||
**Keeping the list slot and adding a namespace field to its options.** Rejected because the section would still enumerate entries rather than namespaces, keeping the empty-state defect and leaving a card for an uncomposed plugin to suppress itself.
|
||||
|
||||
## Consequences
|
||||
|
||||
A plugin distributed outside this repository is configurable from the settings page with no change here: it registers its namespace on the Host and its card under that key in the browser, and the section pairs the two. Cards now appear in card registration order rather than by hand-assigned `order`. That is stable for the cards this package registers, which install from one generator, and **not** stable across plugins: apply order between packages is unconstrained (`packages/client/AGENTS.md`), so several external cards can still reorder between boots. Ordering them needs an explicit key the section can sort on, which the keyed registration does not carry today.
|
||||
|
||||
Deferred, and larger than this change: the redactor returns a `role('secret')` reachable only through a union, intersection, or transform verbatim (its own `TODO(settings-wire-redaction)`), and `schema.toJSON()` carries a secret's default. That gap predates this change, but serving every registered namespace widens its blast radius from schemas audited in this repository to any third-party schema, so the wire should refuse a namespace it cannot prove it can redact. Also deferred: an assembled-composition test of the headline capability — an overlay-mounted fixture plugin whose Host half registers a namespace and whose `dsh.client` half registers a card, asserted end-to-end. The current coverage proves each half separately; the shipped cards' unchanged output cannot prove the new path.
|
||||
|
||||
The wire read the section adds is one `settings.describe` beside the per-scope reads the cards already make. Its invalidation is imprecise in one direction: the wire announces document commits and connection resets, not registrations, so a namespace registered after the section's read joins on the next commit or reconnect.
|
||||
|
||||
Two frictions remain for an author outside this repository, both recorded in the section's README. The browser half must be a `dsh.client` package built in the client module system's lazy-CJS factory format, and the `clientBundle` preset that emits it lives in `packages/client/tsdown.client.ts` rather than a published package. The bundle-purity gate forbids importing this package's card chrome or staged-form model as values, so such a card reimplements staging and revision fencing. Sharing them would mean either publishing the preset or declaring a child slot inside the card so the section supplies the chrome; neither is built.
|
||||
@@ -0,0 +1,61 @@
|
||||
# Agent Note: 由插件自己拥有的设置表层
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-08-12-plugin-owned-settings-surface.md) | 中文
|
||||
|
||||
## Problem
|
||||
|
||||
注册了 settings 命名空间的插件到不了浏览器配置页,而拦住它的两道门都在本仓库里。
|
||||
|
||||
`packages/host/apiproxy` 持有两份硬编码的命名空间清单。`settings.describe` 用它们过滤答复,每次写入也先对照它们,因此清单之外的命名空间即便其拥有方已注册,也只会得到 `settings-not-exposed`。于是把一个插件加进配置页,意味着要改一个插件作者并不拥有的包。
|
||||
|
||||
插件配置分区渲染的是注册进 `settings.plugin.item` 的卡片列表,无序。卡片携带的是不透明的 `id`,从不是它所编辑的命名空间,因此分区无从判断哪些被服务的命名空间已经有了归属。凡是"这个命名空间由谁渲染"的问题,都无法从分区看得见的账本里得到答案。
|
||||
|
||||
两者相加,用户自己写的插件就只能靠手改 `settings.yaml` 来配置。[web 插件配置 note](../feature/2026-08-10-web-plugin-configuration.md) 把白名单记为刻意为之,[配置面边界 note](2026-07-30-config-plane-boundaries.md) 则把「可在 Web 上配置」绑定到可配置提供方目录的成员资格。这两条结论恰恰挡住了那个通用 seam 本来要服务的插件作者。
|
||||
|
||||
## Decision
|
||||
|
||||
**注册即暴露。** api-proxy 服务 `ctx.settings.describe()` 返回的每一个命名空间,写入不设门禁。`WEB_SETTINGS_NAMESPACES`、`PRODUCT_SETTINGS_NAMESPACES`、与 `ctx.llm.listConfigurableProviders()` 的并集,以及 `settings-not-exposed` 错误码,全部删除。没有任何注册应答的名字——未知的,或格式非法因而根本无法寻址到注册的——都折叠为 seam 自己的 `settings-rejected`,于是代理既不贡献边界,也不贡献自己的词汇。
|
||||
|
||||
**settings seam 不动。** 哪个客户端可以读某个命名空间、哪个页面渲染它,都是关于 Consumer 的事实;Service Definition 只要携带其中之一,就等于让一个 Consumer 决定它的契约。`SettingsRegisterOptions` 一个字段都没加。
|
||||
|
||||
**`settings.plugin.item` 以 settings 命名空间为键。** 该 slot 从 `list` 改为 `keyed`,键就是卡片所编辑的命名空间,沿用 `tool.call.toolview` 的先例——每个工具插件把自己的渲染器注册在工具名这个键上。卡片声明 `key`,不再声明 `id`/`order`。该 slot 由「插件」分区的 `configurable` 标签页声明,卡片列表归它所有。
|
||||
|
||||
**标签页以被服务的命名空间驱动派发。** 它读取一次 `settings.describe`,订阅 settings 文档失效通知与连接重置,并为每个被服务的命名空间派发一个键。渲染出来的是两份账本的交集——存活 Host 插件注册的命名空间,以及注册在这些键上的卡片——由标签页的 controller 从 slot 账本(`ctx.slots.entries`、`ctx.slots.subscribe`)与协议答复算出。
|
||||
|
||||
以命名空间为键,让「缺席」本身成为信号,而这正是它消掉旧形态所需簿记的原因。归别的界面所有的命名空间(`ui-theme`、`permission`、`llm-*`、`agent-presets`)在其键上没有卡片,于是什么都不渲染,且无需在任何地方声明任何东西。命名空间未被本部署服务的卡片根本不会被派发,这同时修掉了旧的空态缺陷:标签页数的是已注册卡片,其中包含那些什么都不渲染的,因此一个都不暴露的部署看到的是空列表,而不是它那行空态文案。
|
||||
|
||||
**不渲染任何未被交给它的表单。** 标签页不提供兜底卡片。插件的浏览器半侧完整拥有自己的卡片——外观、控件与文案——而这正是 slot 的 `fallback` 选项会用一份 schema 反向渲染的表单取代掉的东西。
|
||||
|
||||
## 白名单实际护住了什么
|
||||
|
||||
这道门确实挡住了一样东西,本 note 如实写出,因为这个决策必须在准确版本下也站得住:不在名单上的已注册命名空间,其 resolved、`base` 与 `user` 值根本不会抵达浏览器。插件清单页不能替代它——`PluginInventoryEntry` 携带的是 `entryId`、`moduleName`、`enabled` 与 `fiberPhase`,它那一行「configuration」渲染的是启用/停用标签,从不是任何已存值。
|
||||
|
||||
这道门不是的,是它所处位置暗示的那种边界。每个 `settings.*` 方法都在 `PRIVILEGED_METHODS` 里(`packages/client/connection`),非回环或跨源请求在到达这段代码之前就以 403 被拒;`role('secret')` 字段在每种响应的每一层都被结构性剥离;而这个面所编辑的文档,本就是用户自己的 `settings.yaml`,同一个设置页还提供了打开它的入口。它没有挡住的写入,恰恰是有分量的那些:`permission`(能放宽审批预设)与 `agent-presets`(决定一个会话挂载什么)本来就已被服务。
|
||||
|
||||
因此本次改动在本仓库实际新增的暴露面是一个命名空间:`agent-default-model`——它的两个字段指明一个提供方与一个模型,且没有任何浏览器半侧渲染它。将来若某个命名空间的值确实不该跨越协议,由 `role('secret')` 逐字段作答:比整命名空间开关更精细,而且已经在执行。
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**在 `settings.register()` 上加声明**(`client: { surface: 'plugin-config' | 'custom', title, description }`),这也是被删掉的 `WEB_SETTINGS_NAMESPACES` 注释所点名的既定方向。它让注册默认不跨越传输边界,并让插件作者一行代码自助。否决的原因是 `surface` 是浏览器页面的词汇,而 `title`/`description` 属于呈现:Service Definition 一旦携带它们,就成了被单个 Consumer 塑形的 seam。它那条 fail-closed 性质的价值也不如读起来那么高——见上文「白名单实际护住了什么」。
|
||||
|
||||
**另设一份暴露目录**,插件在注册 settings 之外再加入这份自有注册表,即把 `ctx.llm.registerConfigurableProviders()` 一般化。否决的原因是它把一件事实拆成两处可能脱节的注册:注册了命名空间却忘了目录条目,产出的是一个谁都编辑不了的分节,而没有任何门禁看得见这个错误。
|
||||
|
||||
**给 api-proxy 加一个 deny-list `Config` 字段**,让部署方能扣下某个命名空间。因为没有消费者而否决:当前每一个已注册的命名空间都是用户可以编辑的,而真正敏感的字段由 `role('secret')` 逐字段作答,那是更精细的工具。在第一个用例出现之前就发明出来的整命名空间开关,正是包规则所禁止的投机选项。
|
||||
|
||||
**把 schema 驱动的通用卡片作为该 slot 的 `fallback`**,让没有浏览器半侧的插件也能从 `schema.toJSON()` 得到一份表单(schemastery 本就携带 `description`、`role`、`min`/`max`/`step` 并将其序列化)。否决的原因是客户端插件按已挂载的 Loader entries 在运行时加载,插件作者完全可以交付一张真正的卡片;而反向渲染的表单在模型页那次已被判定不如手写。若这个判断日后改变,`fallback` 选项无需改动契约即可启用。
|
||||
|
||||
**客户端认领注册表**,让每个拥有某命名空间的界面声明它,好让通用卡片知道哪些已经有人管。与通用卡片一并否决:keyed 派发本就让无人认领的键什么都不渲染,这份注册表只会把 slot 账本已经说过的话再说一遍。
|
||||
|
||||
**保留 list slot,只给它的 options 加一个命名空间字段。** 否决的原因是分区枚举的仍是 entry 而非命名空间,空态缺陷照旧,未组装插件的卡片也仍需自我抑制。
|
||||
|
||||
## Consequences
|
||||
|
||||
在本仓库之外分发的插件无需改动这里即可从设置页配置:它在 Host 上注册自己的命名空间、在浏览器里把卡片注册在该键上,由分区把两者配对。卡片现在按卡片注册顺序出现,而不再依赖手工指定的 `order`。对本包注册的这几张卡它是稳定的——它们从同一个 generator 安装;对**跨插件**的卡片它并不稳定:包与包之间的 apply 顺序是无约束的(`packages/client/AGENTS.md`),因此多个外部卡片仍可能在不同次启动之间重排。要为它们定序,需要一个 section 可排序的显式键,而 keyed 注册今天并不携带。
|
||||
|
||||
以下延后,且都大于本次改动:脱敏器对只能经由 union、intersection 或 transform 抵达的 `role('secret')` 原样返回(其自身的 `TODO(settings-wire-redaction)`),而 `schema.toJSON()` 会携带 secret 的默认值。该缺口早于本次改动,但服务每一个已注册命名空间,把它的影响面从本仓库内经审计的 schema 扩大到任意第三方 schema,因此协议应当拒绝服务它无法证明可安全脱敏的命名空间。同样延后的还有:对本次头号能力的组装态测试——用 overlay 挂载一个 fixture 插件(Host 半注册命名空间、`dsh.client` 半注册卡片)并在端到端断言。当前覆盖分别证明了两个半侧;已发卡片输出未变这一点,证明不了新路径。
|
||||
|
||||
分区新增的协议读取是一次 `settings.describe`,与卡片各自已有的 per-scope 读取并列。它的失效通知在一个方向上不精确:协议通告的是文档提交与连接重置,而非注册行为,因此在分区读取之后才被注册的命名空间,要等下一次提交或重连才会加入。
|
||||
|
||||
对仓库之外的作者仍留有两处摩擦,均记在该分区的 README 里。浏览器半侧必须是按客户端模块系统的 lazy-CJS factory 格式构建的 `dsh.client` 包,而产出它的 `clientBundle` 预设位于 `packages/client/tsdown.client.ts`,并非已发布的包。bundle 纯净度门禁禁止以值的形式导入本包的卡片外观与暂存表单模型,因此这样的卡片要重新实现暂存与 revision 设栅。要共享它们,要么发布该预设,要么在卡片内部声明一层子 slot 让分区提供外观;两者都尚未构建。
|
||||
@@ -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-07-31-composer-text-layers-share-one-scrollport.md
|
||||
2026-07-31-composer-text-layers-share-one-scrollport.md: 6097779529f86e6d994296ae396f108c63f01abc
|
||||
2026-07-31-composer-text-layers-share-one-scrollport.zh.md: 753d67d538d0c17512444639d60b7b5c8ff80e9a
|
||||
2026-07-31-composer-text-layers-share-one-scrollport.md: d01231f706a7d3850ce1b3770ef351cb7e211384
|
||||
2026-07-31-composer-text-layers-share-one-scrollport.zh.md: 3ce8cc05dc57b05bb8c0bd903a0b64b12d4a6963
|
||||
|
||||
@@ -24,6 +24,8 @@ One scrolling box, holding both layers.
|
||||
|
||||
The browser then applies one offset to both layers, in the same frame, on the same compositor. The caret is bound to its glyphs by construction rather than by upkeep: there is no code to run, no event to wait for, and no state that can be one frame stale. The wheel-chaining handler stays, retargeted from the textarea to the scrollport, and remains the only listener on the box.
|
||||
|
||||
Safari's native text control has one engine exception: deleting across a soft-wrap threshold can retain the former line layout after the mirror shrinks. The [Safari soft-wrap recovery](2026-08-13-safari-textarea-soft-wrap-reflow.md) restores the zero-overflow invariant before paint without changing the one-scrollport design.
|
||||
|
||||
Two things the previous mechanism needed are gone with it:
|
||||
|
||||
**The backdrop's trailing-line sentinel.** It existed to keep the two boxes' scroll extents equal — a textarea reserves a line box for the caret after a final newline while `white-space: pre-wrap` collapses a text node's trailing newline, so a draft ending in a newline made the backdrop one line shorter and clamped the mirrored offset a line above the caret. With one scrollport the backdrop's own extent decides nothing: the mirror div sizes the stack for both layers, both start at the same top, and a layer whose content ends earlier simply paints nothing on the last line. The shape is worth keeping in mind rather than the mechanism: it is the one that measured 628 against 652 when the two boxes had to agree on a height.
|
||||
|
||||
@@ -24,6 +24,8 @@ composer 的文本由两层叠放绘制(见 [InputBar](../../../../packages/cl
|
||||
|
||||
于是浏览器在同一帧、同一个合成器上,把同一个偏移施加给两层。光标与字形的绑定来自结构本身,而不是来自持续维护:没有代码要跑,没有事件要等,也没有任何状态可能落后一帧。滚轮接力处理器保留,只是从 textarea 改挂到滚动容器上,并且仍是这个盒子上唯一的监听。
|
||||
|
||||
Safari 的原生文本控件存在一个引擎例外:跨过软换行阈值的删除可能在镜像层收缩后仍保留原先的行布局。[Safari 软换行恢复](2026-08-13-safari-textarea-soft-wrap-reflow.md)会在绘制前恢复零溢出不变量,而不改变单滚动容器设计。
|
||||
|
||||
上一版机制所需要的两样东西随它一起消失:
|
||||
|
||||
**backdrop 的尾行哨兵。** 它的存在只是为了让两个盒子的滚动范围相等——textarea 会在末尾换行之后为光标保留一个行盒,而 `white-space: pre-wrap` 会折叠文本节点的尾随换行,因此以换行结尾的草稿会让 backdrop 少一行,把镜像偏移钳制在光标上方一行。改为单一滚动容器后,backdrop 自身的范围不再决定任何事:镜像层为两层统一定高,两层顶端对齐,内容更早结束的那一层只是在最后一行什么都不画。值得记住的是这类草稿形状而不是那套机制:正是它在「两个盒子必须就高度达成一致」的时代量出了 628 对 652。
|
||||
|
||||
@@ -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(测试前置数据)承载的是这次改动所消除的那个差值,而不仅仅是它的缺席。
|
||||
|
||||
@@ -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-04-large-history-pagination-call-stack.md
|
||||
2026-08-04-large-history-pagination-call-stack.md: 28c22121123a227c507c506683ae727d238d98bd
|
||||
2026-08-04-large-history-pagination-call-stack.zh.md: 57dde9bdc0a4aa52e1af024eb606bf9258430fa7
|
||||
@@ -0,0 +1,27 @@
|
||||
# Agent Note: Large history provenance is scanned without argument expansion
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-08-04-large-history-pagination-call-stack.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
A finalized assistant message can reference hundreds of thousands of streamed chunks through `sourceEventSeqs`. History pagination found the message group's first event with `Math.min(event.seq, ...sourceEventSeqs)`, so a valid session could exceed the JavaScript engine's function-argument limit and make `session.history` fail with HTTP 500.
|
||||
|
||||
## Decision
|
||||
|
||||
Pagination scans `sourceEventSeqs` and updates the earliest sequence number one element at a time. The algorithm remains linear in the provenance size and preserves the existing page boundary: a page starts before all recorded sources of its oldest included message.
|
||||
|
||||
A regression test rejects multi-argument minimum calls and verifies that every provenance event remains on the page with its finalized message. This exercises the failure mechanism without making the default test suite allocate a production-sized chunk stream.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
- **Raise the JavaScript stack or argument limit** — rejected: the limit is engine- and deployment-dependent, and array expansion still makes valid history depend on an unrelated runtime ceiling.
|
||||
- **Truncate `sourceEventSeqs` during pagination** — rejected: this could cut a page inside a message and violate replay grouping.
|
||||
- **Cap streamed chunk count at the provider boundary** — rejected: providers may legitimately emit long streams, and pagination must handle every valid session representation.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Large provenance arrays no longer make history pagination throw solely because of their length.
|
||||
- Pagination semantics and wire responses are unchanged.
|
||||
- This does not bound the byte size of a history page or the browser cost of replaying it; those performance concerns remain separate from the server-side call-stack failure.
|
||||
@@ -0,0 +1,27 @@
|
||||
# Agent Note: 大规模历史记录的溯源信息通过扫描处理,不做参数展开
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-08-04-large-history-pagination-call-stack.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
一条已定稿的 assistant 消息可以通过 `sourceEventSeqs` 引用数十万个流式分片。历史记录分页使用 `Math.min(event.seq, ...sourceEventSeqs)` 查找消息组的首个事件,因此,有效会话可能超出 JavaScript 引擎的函数参数数量上限,导致 `session.history` 以 HTTP 500 失败。
|
||||
|
||||
## 决策
|
||||
|
||||
分页逻辑逐项扫描 `sourceEventSeqs`,每次使用一个元素更新最早的序号。该算法的复杂度相对溯源信息规模仍为线性,并保留现有的页面边界:页面起点位于其所含最早消息的所有已记录来源之前。
|
||||
|
||||
回归测试会拒绝以多个参数调用取最小值的做法,并验证每个溯源事件都会与其已定稿消息保留在同一页中。这既覆盖了故障机制,也避免默认测试套件分配生产规模的分片流。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
- **提高 JavaScript 栈或参数上限**:不予采纳,因为该上限取决于引擎和部署环境,而且数组展开仍会让有效历史记录受制于无关的运行时上限。
|
||||
- **在分页时截断 `sourceEventSeqs`**:不予采纳,因为这可能会从消息中间切分页面,破坏回放分组。
|
||||
- **在提供方边界限制流式分片数量**:不予采纳,因为提供方可能会合理地产生长流,而分页必须处理每一种有效的会话表示。
|
||||
|
||||
## 后果
|
||||
|
||||
- 大型溯源数组不再仅因长度而使历史记录分页抛出异常。
|
||||
- 分页语义与协议响应保持不变。
|
||||
- 本决策不限制历史记录页面的字节大小,也不限制浏览器回放该页面的开销;这两项性能问题仍与服务端调用栈故障分开处理。
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-08-10-minimal-preset-owns-rl-composition.md
|
||||
2026-08-10-minimal-preset-owns-rl-composition.md: d83083de74f1d3203822d82e4e04560819412b48
|
||||
2026-08-10-minimal-preset-owns-rl-composition.zh.md: 36aeea0d8af6e371eddd6f09e36bb48a0348959e
|
||||
2026-08-10-minimal-preset-owns-rl-composition.md: 65d24f9a03eedffac34f0c0141a8e2f643a48b7b
|
||||
2026-08-10-minimal-preset-owns-rl-composition.zh.md: 72eaa0e5bba46743abd336e64316c886fa58631a
|
||||
|
||||
@@ -14,13 +14,13 @@ The split also hid other drift. The preset mounted one-shot Bash rather than the
|
||||
|
||||
The shipped Web `minimal` preset is the sole Web owner of the RL agent composition. It declares an entry-local PTY registry and local backend, persistent `bash` with the RL environment description and 300-second timeout, and `str_replace_editor`. Tool presentation remains a deployment choice. The later [bare two-tool runtime decision](../feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.md) supersedes this note's original compaction and filesystem-provider choices: the current preset mounts an entry-local `fs-local` provider and no compaction backend. The editor accepts no `requireAbsolutePath` setting because absolute paths are its unconditional contract.
|
||||
|
||||
The preset persona is exactly `You are a helpful software engineer assistant.` and sets `complete: true`. A complete `PromptSection` participates in ordinary assembly so tools, contexts, variables, and cooperative listeners still resolve; after the `system-prompt/assemble` waterfall, the prompt registry restores a detached copy of that section as the sole system-prompt section. Multiple effective complete sections reject assembly. This final registry constraint prevents harness identity, Web orientation, tool guidance, or an assembly listener from appending prompt text.
|
||||
The preset persona is exactly `You are a helpful software engineer assistant.`, sets `complete: true`, and suppresses runtime context for its agent scope. A complete `PromptSection` participates in ordinary assembly so tools, variables, and cooperative listeners still resolve; after the `system-prompt/assemble` waterfall, the prompt registry restores a detached copy of that section as the sole system-prompt section and discards every dynamic context contribution. Multiple effective complete sections reject assembly. These final registry constraints prevent harness identity, Web orientation, tool guidance, an assembly listener, sandbox policy, approval policy, delegation, or another dynamic context provider from adding model input.
|
||||
|
||||
The process-wide `core-web.cordis.yml` patch is absent. Browser UI, workspace attachment, persistence, subprocess, sandbox, permission, model routing, and other cross-session services remain host-owned. Selecting `minimal` changes one agent's model-facing composition and shadows the host filesystem provider only for that agent, without changing other sessions in the Web process.
|
||||
|
||||
## Verification
|
||||
|
||||
System-prompt and persona package tests prove final complete-section enforcement, including waterfall mutation and duplicate rejection. The shipped-preset composition test asserts the exact prompt, Bash description, absolute editor schema, and two-tool catalog under the default native presentation. The keyless Web replay sends a real request through a `minimal` agent while global identity, Web-orientation text, and a test section are registered, asserts the entry-local filesystem is bare and compaction is absent, then executes two persistent Bash calls to prove environment and cwd state survive and executes the editor through an absolute path.
|
||||
System-prompt and persona package tests prove final complete-section and runtime-context suppression, including waterfall mutation and duplicate rejection. The shipped-preset composition test asserts the exact prompt, Bash description, absolute editor schema, and two-tool catalog under the default native presentation. The keyless Web replay sends a real request through a `minimal` agent while global identity, Web-orientation text, dynamic policy contexts, and a test section are registered, asserts that no runtime-context snapshot exists, the entry-local filesystem is bare, and compaction is absent, then executes two persistent Bash calls to prove environment and cwd state survive and executes the editor through an absolute path.
|
||||
|
||||
The standalone [`minimal.cordis.yml`](../../../../examples/jsonrpc-agent/minimal.cordis.yml) is the complete two-tool composition for the bundled JSON-RPC runtime. The [bare two-tool runtime decision](../feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.md) owns its launch-specific environment configuration, bare filesystem, and absence of compaction. Its keyless SDK replay asserts the assembled system prompt and two-tool catalog, executes persistent Bash across calls, and exercises the editor; the Python SDK tutorial provides the runnable entry point.
|
||||
|
||||
|
||||
@@ -14,13 +14,13 @@ Status: implemented
|
||||
|
||||
随附的 Web `minimal` preset 是 RL agent 组合在 Web 中的唯一所有者。它声明 entry 本地的 PTY 注册表与本地后端、带 RL 环境描述且超时为 300 秒的持久 `bash`,以及 `str_replace_editor`。工具呈现仍由部署选择。后续的[裸双工具运行时决策](../feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.md)取代了本记录最初的压缩与文件系统提供方选择:当前 preset 挂载 entry 本地的 `fs-local` 提供方,不挂载压缩后端。编辑器不接受 `requireAbsolutePath` 设置,因为要求绝对路径是它的无条件约定。
|
||||
|
||||
preset persona 恰好是 `You are a helpful software engineer assistant.`,并设置 `complete: true`。complete `PromptSection` 参与常规组装,因此工具、上下文、变量和协作式监听器仍会解析;`system-prompt/assemble` waterfall(瀑布式事件)结束后,提示词注册表会将该段落的独立副本恢复为唯一的系统提示词段落。存在多个有效 complete 段时,组装会被拒绝。这项最终注册表约束可防止 harness 身份、Web 定位、工具引导或组装监听器追加提示词文本。
|
||||
preset persona 恰好是 `You are a helpful software engineer assistant.`,它设置 `complete: true`,并为其 agent 作用域抑制 runtime context。complete `PromptSection` 参与常规组装,因此工具、变量和协作式监听器仍会解析;`system-prompt/assemble` waterfall(瀑布式事件)结束后,提示词注册表会将该段落的独立副本恢复为唯一的系统提示词段落,并丢弃每个动态上下文贡献。存在多个有效 complete 段时,组装会被拒绝。这些最终注册表约束可防止 harness 身份、Web 定位、工具引导、组装监听器、沙箱策略、批准策略、委派或其他动态上下文提供方添加模型输入。
|
||||
|
||||
进程级 `core-web.cordis.yml` patch 不再存在。浏览器 UI、workspace 附加、持久化、子进程、沙箱、权限、模型路由及其他跨会话服务仍由宿主持有。选择 `minimal` 会改变一个 agent 面向模型的组合,并且仅为该 agent 遮蔽宿主文件系统提供方,不会改变 Web 进程中的其他会话。
|
||||
|
||||
## 验证
|
||||
|
||||
系统提示词与 persona 包测试证明了 complete 段的最终约束,包括 waterfall 修改与重复项拒绝。交付 preset 组合测试在默认原生呈现下断言精确的提示词、Bash 描述、要求绝对路径的编辑器 schema 和双工具目录。无密钥 Web 回放通过 `minimal` agent 发送一个真实请求,同时注册全局身份、Web 定位文本和一个测试段落;它断言 entry 本地文件系统是裸后端且压缩不存在,随后执行两次持久 Bash 调用,证明环境与 cwd 状态能够保留,并通过绝对路径执行编辑器。
|
||||
系统提示词与 persona 包测试证明了 complete 段最终约束与 runtime-context 抑制,包括 waterfall 修改与重复项拒绝。交付 preset 组合测试在默认原生呈现下断言精确的提示词、Bash 描述、要求绝对路径的编辑器 schema 和双工具目录。无密钥 Web 回放通过 `minimal` agent 发送一个真实请求,同时注册全局身份、Web 定位文本、动态策略上下文和一个测试段落;它断言不存在 runtime-context 快照、entry 本地文件系统是裸后端且压缩不存在,随后执行两次持久 Bash 调用,证明环境与 cwd 状态能够保留,并通过绝对路径执行编辑器。
|
||||
|
||||
独立的 [`minimal.cordis.yml`](../../../../examples/jsonrpc-agent/minimal.cordis.yml) 是内置 JSON-RPC 运行时的完整双工具组合。[裸双工具运行时决策](../feature/2026-08-11-minimal-profiles-bare-two-tool-runtime.md)说明其启动方式专属的环境配置、裸文件系统和无压缩选择。其无密钥 SDK 回放会断言组装后的系统提示词与双工具目录,跨调用执行持久 Bash,并使用编辑器;Python SDK 教程提供可运行的入口。
|
||||
|
||||
|
||||
@@ -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-12-fix-pwsh-terminal-overlay-dup.md
|
||||
2026-08-12-fix-pwsh-terminal-overlay-dup.md: 7a214ea093faf49141c66bfc2cbb065fbeb29825
|
||||
2026-08-12-fix-pwsh-terminal-overlay-dup.zh.md: 6d38ca58ad97e5ad04992e54a7938c9fea300863
|
||||
@@ -0,0 +1,55 @@
|
||||
# Agent Note: fix the pwsh terminal overlay duplicate-loader collision
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-08-12-fix-pwsh-terminal-overlay-dup.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
`apps/web/tests/pwsh-terminal.e2e.ts` fails on every platform with `TypeError: duplicate loader entry id: tool-pwsh`, thrown from `vendor/loader/src/config/group.ts:64` while applying the web composition. The failing seed lane boots the full shipped bundle plus a test overlay, so the E2E never reaches its rendering assertion and every `check:ci:snapshot`/`test:web` run reports a red web test even though the feature under test is unrelated to the change under review.
|
||||
|
||||
The web E2E scaffold applies an `extraOverlayPath` after the shipped Web surface and base patches. `pwsh-terminal.overlay.yml` used an `insert` block to add a `tool-pwsh` row:
|
||||
|
||||
```yaml
|
||||
- insert:
|
||||
- id: pwsh-local
|
||||
name: '@deepseek-ai/dsh-pwsh-local'
|
||||
- id: tool-pwsh
|
||||
name: '@deepseek-ai/dsh-tool-pwsh'
|
||||
```
|
||||
|
||||
`insert` is correct only while `tool-pwsh` is absent from the composition. The id exists because `86b6979bdc` (refactor(bundle): fold the Windows shell platform layer into the base rows) moved both shell stacks into the base bundle with inverted platform gates — `packages/bundle/base/cordis.patch.yml` declares `tool-pwsh` with `disabled: !!js process.platform !== 'win32'`, so the row is present in the composition on every platform. Later, `42fc7c5ffb` (refactor(preset): gate tool-pwsh by platform alongside tool-bash) added a web-app patch row that disables `tool-pwsh` for surfaces that use presets; a patch row cannot introduce an id, so it is not the source of the collision. The overlay's `insert` delivers a second row with the same id in the same loader group, and the loader rejects the pair at boot.
|
||||
|
||||
## Decision
|
||||
|
||||
Replace the overlay's `insert` of `tool-pwsh` with a top-level id-targeted override:
|
||||
|
||||
```yaml
|
||||
- id: tool-pwsh
|
||||
name: '@deepseek-ai/dsh-tool-pwsh'
|
||||
disabled: false
|
||||
```
|
||||
|
||||
The effective `tool-pwsh` state is a three-layer stack: the base row gates `disabled` on `process.platform !== 'win32'`, the web-app overlay sets `disabled: true` unconditionally for preset surfaces, and this lane's override clears it back to `disabled: false` regardless of platform. An `id`-targeted top-level override replaces the composed row; only an `insert` would collide.
|
||||
|
||||
The lane also now disables `pwsh-sandbox` by id, symmetric with the existing `bash-sandbox` disable: the base gates `pwsh-sandbox` with `disabled: !!js process.platform !== 'win32'`, so on Windows it would otherwise mount beside the inserted `pwsh-local` and both would register the same executor service. Disabling it keeps `pwsh-local` the lone executor on every platform.
|
||||
|
||||
The overlay header comment was updated to describe the full selection and the `tool-pwsh` inline comment now names the base row as the source of the id.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Keep the `insert` and change the web composition instead.** Rejected, because the shipped web composition should keep the host `tool-pwsh` row disabled for every surface that uses presets; the overlay is the lane that deliberately needs it, so the by-id enable belongs there. The base row itself cannot be removed either: it is the platform-gated shell-stack declaration shared by every bundle.
|
||||
|
||||
**Enable `tool-pwsh` in the `insert` block.** Not possible: an `insert` of an id that already exists is the very duplicate being fixed. The row must be targeted by id, which is the top-level override form, not `insert`.
|
||||
|
||||
**Patch `tool-pwsh` by id without setting `disabled: false`.** Insufficient: the web-app overlay sets `disabled: true` unconditionally, and the base row's platform gate only applies where the web-app override is absent, so an override that only restates `name` leaves the row disabled and the lane renders no terminal card. The `disabled: false` is required.
|
||||
|
||||
**Only disable `bash-sandbox` and rely on the platform gate to keep `pwsh-sandbox` off.** Rejected: that holds on POSIX but breaks on Windows, where the base row leaves `pwsh-sandbox` enabled and it would collide with the inserted `pwsh-local` on the shared executor service. The lane's `pwsh-sandbox` disable keeps one executor on every platform.
|
||||
|
||||
## Verification
|
||||
|
||||
Reverting the fix (restoring the `insert` of `tool-pwsh`) reproduces the exact `duplicate loader entry id: tool-pwsh` boot failure, confirming the override is load-bearing. With the fix in place `pwsh-terminal.e2e.ts` passes 2/2 on the same head — this exercises the POSIX seam, where the seeded pwsh call renders through the enabled `tool-pwsh` and the inserted `pwsh-local`. The seed lane requires a usable `pwsh`, so it skips on hosts without one; a `pwsh` binary is present on this machine and the test ran. The Windows path (base `pwsh-sandbox` mounted beside the inserted `pwsh-local`) is not exercised by any CI lane, whose `test:web` runs only on Linux; the overlay disables `pwsh-sandbox` to keep that path composable if it ever runs on a Windows dev machine.
|
||||
|
||||
## Consequences
|
||||
|
||||
The web E2E seed lane that exercises PowerShell boot now composes instead of colliding, so `check:ci:snapshot` and `test:web` stop failing on the duplicate independently of the change under test. The pattern is general: a `--patch`/`extraOverlayPath` overlay must probe whether a row already exists in the bundle it augments before choosing `insert` over an id-targeted override; `insert` of an id that the base or shipped Web surface already declares is a boot-time duplicate.
|
||||
@@ -0,0 +1,55 @@
|
||||
# Agent Note:修复 pwsh 终端 overlay 的重复 loader 冲突
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-08-12-fix-pwsh-terminal-overlay-dup.md) | 中文
|
||||
|
||||
## Problem
|
||||
|
||||
`apps/web/tests/pwsh-terminal.e2e.ts` 在所有平台上都以 `TypeError: duplicate loader entry id: tool-pwsh` 失败,由 `vendor/loader/src/config/group.ts:64` 在应用 web 组合时抛出。该失败的 seed 通道会启动完整发布的 bundle 加一个测试 overlay,因此 E2E 永远到不了渲染断言,导致 `check:ci:snapshot` 与 `test:web` 每次运行都报一个红的 web 测试,即便被测功能与评审中的改动无关。
|
||||
|
||||
web E2E scaffold 在已发布的 Web 表面与 base patches 之后应用 `extraOverlayPath`。`pwsh-terminal.overlay.yml` 用 `insert` 块新增 `tool-pwsh` 行:
|
||||
|
||||
```yaml
|
||||
- insert:
|
||||
- id: pwsh-local
|
||||
name: '@deepseek-ai/dsh-pwsh-local'
|
||||
- id: tool-pwsh
|
||||
name: '@deepseek-ai/dsh-tool-pwsh'
|
||||
```
|
||||
|
||||
`insert` 仅在组合中不存在 `tool-pwsh` 时才正确。该 id 存在是因为 `86b6979bdc`(refactor(bundle): fold the Windows shell platform layer into the base rows)把两套 shell 栈以互逆的平台门移进了 base bundle —— `packages/bundle/base/cordis.patch.yml` 声明 `tool-pwsh` 且 `disabled: !!js process.platform !== 'win32'`,于是该行在每个平台都存在于组合中。随后 `42fc7c5ffb`(refactor(preset): gate tool-pwsh by platform alongside tool-bash)往 web-app patch 里加了一行对使用 preset 的表面禁用 `tool-pwsh` 的行;patch 不能引入 id,因此它不是冲突来源。overlay 的 `insert` 于是在同一个 loader 组里再送一个同 id 的行,loader 在启动时拒绝这对重复。
|
||||
|
||||
## Decision
|
||||
|
||||
把 overlay 对 `tool-pwsh` 的 `insert` 替换成顶层按 id override:
|
||||
|
||||
```yaml
|
||||
- id: tool-pwsh
|
||||
name: '@deepseek-ai/dsh-tool-pwsh'
|
||||
disabled: false
|
||||
```
|
||||
|
||||
有效的 `tool-pwsh` 状态是三层栈:base 行把 `disabled` 门在 `process.platform !== 'win32'` 上,web-app overlay 对 preset 表面无条件设 `disabled: true`,本通道的 override 无论平台都把 `disabled: false` 还回去。`id` 定位的顶层 override 替换组合后的行;只有 `insert` 才会相撞。
|
||||
|
||||
该通道现在也按 id 禁用 `pwsh-sandbox`,与既有的 `bash-sandbox` 禁用对称:base 以 `disabled: !!js process.platform !== 'win32'` 门住 `pwsh-sandbox`,因此在 Windows 上它本会与插入的 `pwsh-local` 并存,两者会注册同一个 executor 服务。禁用它让 `pwsh-local` 在每个平台上都是唯一 executor。
|
||||
|
||||
overlay 头部注释已更新为完整描述选择,`tool-pwsh` 行内注释现在把 base 行标为该 id 的来源。
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**保留 `insert`、改 web 组合。** 拒绝。已发布的 web 组合应在所有使用 preset 的表面上保持 host `tool-pwsh` 行禁用;overlay 才是那条刻意需要该行的通道,因此按 id 启用应放在那里。base 行本身也不能移除:它是所有 bundle 共享的平台门 shell 栈声明。
|
||||
|
||||
**在 `insert` 块里启用 `tool-pwsh`。** 不可行。对已存在的 id 做 `insert` 正是这里要修的重复。该行必须按 id 定位,即顶层 override 形式,而非 `insert`。
|
||||
|
||||
**只按 id 改 `tool-pwsh` 而不设 `disabled: false`。** 不充分。web-app 无条件设 `disabled: true`,base 行的平台门只在 web-app override 缺失处生效,因此只重申 `name` 的 override 会让行保持禁用,通道渲染不出终端卡。`disabled: false` 是必需的。
|
||||
|
||||
**只禁用 `bash-sandbox`、依赖平台门让 `pwsh-sandbox` 保持关闭。** 拒绝。在 POSIX 上成立,但在 Windows 上会失败:base 行让 `pwsh-sandbox` 启用,它会与插入的 `pwsh-local` 在共享 executor 服务上相撞。本通道禁用 `pwsh-sandbox` 让每个平台只有唯一 executor。
|
||||
|
||||
## Verification
|
||||
|
||||
把修复还原(恢复对 `tool-pwsh` 的 `insert`)即复现同样的 `duplicate loader entry id: tool-pwsh` 启动失败,证实 override 是有效的。修复后同一 head 上 `pwsh-terminal.e2e.ts` 2/2 通过 —— 这作用于 POSIX seam,播种的 pwsh 调用经启用的 `tool-pwsh` 与插入的 `pwsh-local` 渲染出来。该 seed 通道需要可用的 `pwsh`,无此二进制的主机会跳过;本机有 `pwsh`,测试实际跑过。Windows 路径(base `pwsh-sandbox` 与插入的 `pwsh-local` 并存)没有任何 CI lane 覆盖,其 `test:web` 只在 Linux 上跑;overlay 禁用 `pwsh-sandbox` 让该路径在真到 Windows 开发机运行时可组合。
|
||||
|
||||
## Consequences
|
||||
|
||||
用于执行 PowerShell 启动的 web E2E seed 通道现在能组合而非相撞,因此 `check:ci:snapshot` 与 `test:web` 不再与被测改动无关地在该 duplicate 上失败。该模式具有通用性:`--patch`/`extraOverlayPath` overlay 在决定用 `insert` 还是按 id override 之前,必须探测目标 bundle 是否已存在该行;对已由 base 或已发布 Web 表面声明的 id 做 `insert`,是启动期重复。
|
||||
@@ -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-resolve-store-pwsh-aliases.md
|
||||
2026-08-12-resolve-store-pwsh-aliases.md: 20fe58e15e75462dc0a9ba76c7a1a94939f8a004
|
||||
2026-08-12-resolve-store-pwsh-aliases.zh.md: bbfa4616127a9dbdb6609fe2973283663de55b31
|
||||
@@ -0,0 +1,23 @@
|
||||
# Agent Note: Resolve Microsoft Store pwsh aliases
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-08-12-resolve-store-pwsh-aliases.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
`resolvePwshPath` documented that Microsoft Store installs resolve through PATH, but its existence probe was `existsSync`, which stats a candidate and therefore follows reparse points. The Store's `%LOCALAPPDATA%\Microsoft\WindowsApps\pwsh.exe` is an app execution alias whose target directory ACL refuses stat (EACCES), so `existsSync` missed it and resolution silently fell through to Windows PowerShell 5.1 on hosts whose only PowerShell 7 is a Store install.
|
||||
|
||||
## Decision
|
||||
|
||||
`candidateExists` accepts a candidate that stats as a file or that lstat sees as a link-shaped reparse point, and `resolvePwshPath` uses it. Spawning the alias path works because CreateProcess resolves app execution aliases. A dangling link-shaped candidate is accepted so a broken pwsh fails loudly at spawn instead of silently downgrading to 5.1.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Probe the WindowsApps package directory directly.** The Store package path is versioned and ACL-hidden; hard-coding it duplicates packaging knowledge that PATH plus the alias already owns.
|
||||
|
||||
**Keep the 5.1 fallback for stat failures.** Rejected: it silently runs a different shell than the one installed, which is the defect this note fixes.
|
||||
|
||||
## Consequences
|
||||
|
||||
Store-installed PowerShell 7 now resolves ahead of the 5.1 fallback on Windows; real-file candidates and non-Windows behavior are unchanged. The dangling-symlink unit test pins the stat/lstat split on every platform.
|
||||
@@ -0,0 +1,23 @@
|
||||
# Agent Note: 解析 Microsoft Store 的 pwsh 别名
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-08-12-resolve-store-pwsh-aliases.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
`resolvePwshPath` 声称 Store 安装经 PATH 解析,但它的存在性探测用的是 `existsSync`,会对候选做 stat、从而跟随重解析点。Store 的 `%LOCALAPPDATA%\Microsoft\WindowsApps\pwsh.exe` 是 app execution alias,其目标目录的 ACL 拒绝 stat(EACCES),于是 `existsSync` 看不到它,解析静默落到 Windows PowerShell 5.1——在这类「唯一的 PowerShell 7 是 Store 安装」的机器上就用了错误的 shell。
|
||||
|
||||
## 决策
|
||||
|
||||
`candidateExists` 接受「stat 为文件」或「lstat 为链接形态重解析点」的候选,`resolvePwshPath` 改用它。spawn 别名路径可以工作,因为 CreateProcess 会解析 app execution alias。悬空的链接形态候选同样被接受,让损坏的 pwsh 在 spawn 时响亮失败,而不是静默降级到 5.1。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
**直接探测 WindowsApps 包目录。** Store 包路径带版本且被 ACL 隐藏;硬编码它只是重复了 PATH 加别名已经拥有的打包知识。
|
||||
|
||||
**对 stat 失败继续走 5.1 回退。** 否决:它静默运行了一个并非所装的 shell,这正是本 note 修复的缺陷。
|
||||
|
||||
## 后果
|
||||
|
||||
Windows 上 Store 安装的 PowerShell 7 现在先于 5.1 回退被解析;普通文件候选和非 Windows 平台行为不变。悬空 symlink 单元测试在全部平台上钉住 stat/lstat 的分裂行为。
|
||||
@@ -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-unlink-fixture-junctions-before-delete.md
|
||||
2026-08-12-unlink-fixture-junctions-before-delete.md: 4514a33d728866b817f4e9c1393f16c07976ed45
|
||||
2026-08-12-unlink-fixture-junctions-before-delete.zh.md: 3c212c052ab0303ccb8d31d2b310a365a1d8cc99
|
||||
@@ -0,0 +1,23 @@
|
||||
# Agent Note: Unlink fixture junctions before recursive deletion
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-08-12-unlink-fixture-junctions-before-delete.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
The install-lefthook and translation-pairing fixtures junction the repository's real `scripts/`, `node_modules`, and tsx package directories into fixture trees so installer probes resolve through them. Windows recursive deletion can treat a junction (a MOUNT_POINT reparse point) as a directory and follow it into its target; Git's `worktree remove` did exactly that and deleted the repository's tracked `scripts/` and tsx package (the incident's instrumentation pinned the deletion to that step). A fixture cleanup that trusts its deleter therefore deletes the repository's own sources instead of the fixture.
|
||||
|
||||
## Decision
|
||||
|
||||
`scripts/test-fixture-cleanup.ts` owns junction-safe fixture teardown: `unlinkFixtureLinks` walks a tree and unlinks every reparse point before `removeFixtureSafely` removes the now link-free tree (with Windows async-handle retries). Every affected `afterEach` and the pre-`worktree remove` hook call it. The general rule lives in `docs/defensive-patterns.md`: remove link-shaped paths with unlink, reserve recursive `rmSync` for known real directories.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Trust recursive deletion alone.** Rejected: whether a given deleter follows junctions is tool- and version-dependent, and one path through `git worktree remove` already destroyed tracked files; no cleanup may bet the repository on that behavior.
|
||||
|
||||
**Copy instead of junctioning the real directories.** Rejected: the fixtures exist to probe the real installer paths through their real contents, so copies would stop exercising the boundary under test.
|
||||
|
||||
## Consequences
|
||||
|
||||
Fixture teardown can no longer reach repository sources through junctions. The extra walk is one lstat/unlink pass over small fixture trees. The data-destroying defect now has its durable why beside the defensive-patterns rule, and the helper is the shared teardown path for future junction fixtures.
|
||||
@@ -0,0 +1,23 @@
|
||||
# Agent Note: 递归删除前先解链 fixture junction
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-08-12-unlink-fixture-junctions-before-delete.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
install-lefthook 与 translation-pairing 的 fixture 把仓库真实的 `scripts/`、`node_modules` 和 tsx 包目录用 junction 链进 fixture 树,让 installer 探测能穿透解析。Windows 的递归删除可能把 junction(MOUNT_POINT 重解析点)当作目录并跟随进其目标;Git 的 `worktree remove` 正是这样删掉了仓库被跟踪的 `scripts/` 和 tsx 包(事故的插桩把删除定位到这一步)。因此,信任删除器的 fixture 清理删掉的是仓库自己的源码,而不是 fixture。
|
||||
|
||||
## 决策
|
||||
|
||||
`scripts/test-fixture-cleanup.ts` 拥有 junction 安全的 fixture 拆除:`unlinkFixtureLinks` 先遍历并解链所有重解析点,`removeFixtureSafely` 再删除已无链接的树(带 Windows 异步句柄重试)。所有受影响的 `afterEach` 和 `worktree remove` 前的钩子都调用它。通用规则记录在 `docs/defensive-patterns.md`:链接形态的路径用 unlink 删除,递归 `rmSync` 只留给确知为真实目录的路径。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
**只信任递归删除。** 否决:特定删除器是否跟随 junction 随工具和版本而异,而 `git worktree remove` 这一条路径已经摧毁过被跟踪文件;任何清理都不该拿仓库去赌这个行为。
|
||||
|
||||
**复制而不是 junction 真实目录。** 否决:fixture 的意义就是用真实内容探测真实 installer 路径,复制品会失去被测边界。
|
||||
|
||||
## 后果
|
||||
|
||||
fixture 拆除不再能穿过 junction 触及仓库源码。额外开销只是对小型 fixture 树的一趟 lstat/unlink。这个摧毁数据的缺陷现在在 defensive-patterns 规则旁有了持久化的原因,helper 也是未来所有 junction 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/bug-fix/2026-08-12-unlink-stale-profile-fallback-links.md
|
||||
2026-08-12-unlink-stale-profile-fallback-links.md: 32959eb1b83bcc290d1daa4e4a020a2721be489b
|
||||
2026-08-12-unlink-stale-profile-fallback-links.zh.md: 1f4da12748c9b57c12bf41a740001d5df770beb6
|
||||
@@ -0,0 +1,25 @@
|
||||
# Agent Note: Unlink stale profile fallback links instead of rmSync
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-08-12-unlink-stale-profile-fallback-links.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
`healProfilesModuleFallback` re-points `$DSH_HOME/profiles/node_modules` entries when an installation moves, and Windows hosts keep those entries as junctions. `ensureSymlink` deleted a stale entry with `rmSync(link)`, but Node treats a junction as a directory for removal: without `recursive`, `rmSync` throws `ERR_FS_EISDIR`, so every launch from a moved installation or a second worktree crashed before booting. The `replaces a wrong symlink` unit test reproduces that crash on Windows at the exact removal call.
|
||||
|
||||
## Decision
|
||||
|
||||
`ensureSymlink` removes a stale link with `unlinkSync(link)`. `unlink` deletes the reparse point or symlink itself on every platform and never descends into the target, which preserves the function's fail-loud guarantee that a real directory is never deleted. The [profile-plugin-bundles decision](../architecture/2026-08-05-profile-plugin-bundles.md) keeps owning the fallback's two-anchor resolution; this note owns only the removal primitive.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**`rmSync(link, { recursive: true })`.** On Node 24 this deletes the junction without following its target, but `recursive` would silently delete a real directory that replaced the link between the `lstat` guard and the removal, weakening the fail-loud contract that motivates the guard.
|
||||
|
||||
**`rmdirSync(link)`.** Removes a junction on Windows as well, but it reads as directory removal for a link, and `unlinkSync` is the repository's existing junction-cleanup idiom.
|
||||
|
||||
**Delete and recreate every entry unconditionally.** Correct but churns unchanged links on every launch and widens the concurrent-heal race window.
|
||||
|
||||
## Consequences
|
||||
|
||||
Windows launches heal moved or second-checkout installations instead of crashing with `ERR_FS_EISDIR`; POSIX behavior is unchanged because `unlinkSync` also unlinks plain symlinks. The existing `replaces a wrong symlink` test now passes on Windows where it previously reproduced the crash. Two concurrent healers deleting the same stale link still surface the second deletion as `ENOENT`, unchanged from the previous `rmSync` implementation.
|
||||
@@ -0,0 +1,25 @@
|
||||
# Agent Note: 用 unlink 删除过期的 profile 回退链接而非 rmSync
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-08-12-unlink-stale-profile-fallback-links.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
`healProfilesModuleFallback` 在安装位置迁移时会把 `$DSH_HOME/profiles/node_modules` 中的条目重新指向新目标,而 Windows 主机上这些条目是 junction。`ensureSymlink` 原先用 `rmSync(link)` 删除过期条目,但 Node 在删除时把 junction 当作目录处理:不带 `recursive` 的 `rmSync` 会抛 `ERR_FS_EISDIR`,于是从迁移后的安装或第二个 worktree 启动时,每次都会在应用引导前崩溃。`replaces a wrong symlink` 单元测试在 Windows 上正好在该删除调用处复现了这一崩溃。
|
||||
|
||||
## 决策
|
||||
|
||||
`ensureSymlink` 改用 `unlinkSync(link)` 删除过期链接。`unlink` 在所有平台上都只删除重解析点或符号链接本身、绝不进入目标目录,从而保住该函数“真实目录永远不会被删除”的大声失败保证。[profile-plugin-bundles 决策](../architecture/2026-08-05-profile-plugin-bundles.md)继续拥有回退目录的双锚点解析;本 note 只拥有“用哪个删除原语”这一决定。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
**`rmSync(link, { recursive: true })`。** Node 24 上它只删 junction、不跟随目标,但 `recursive` 会在 `lstat` 守卫与删除之间链接被替换成真实目录时静默删除该目录,削弱守卫存在所依据的大声失败契约。
|
||||
|
||||
**`rmdirSync(link)`。** Windows 上同样能删 junction,但它读起来像“删目录”,而 `unlinkSync` 才是仓库现有的 junction 清理惯例。
|
||||
|
||||
**无条件删除并重建所有条目。** 正确,但每次启动都翻动未变化的链接,并扩大并发修复的竞态窗口。
|
||||
|
||||
## 后果
|
||||
|
||||
Windows 启动现在可以修复迁移后的安装或第二个 checkout,而不是以 `ERR_FS_EISDIR` 崩溃;POSIX 行为不变,因为 `unlinkSync` 同样能 unlink 普通符号链接。现有的 `replaces a wrong symlink` 测试在 Windows 上从复现崩溃变为通过。两个并发 healer 删除同一过期链接时,第二次删除仍会以 `ENOENT` 浮现,与原先的 `rmSync` 实现一致。
|
||||
@@ -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` 这一行选项,录自真实装配的应用。
|
||||
@@ -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-safari-textarea-soft-wrap-reflow.md
|
||||
2026-08-13-safari-textarea-soft-wrap-reflow.md: fb264a8e6fbe24369584f2427bbb0c462b450ecf
|
||||
2026-08-13-safari-textarea-soft-wrap-reflow.zh.md: 7f55a5260e825059e1f9a08db03f13e19484d14e
|
||||
@@ -0,0 +1,47 @@
|
||||
# Agent Note: Safari textarea soft-wrap shrink recovery
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-08-13-safari-textarea-soft-wrap-reflow.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
The composer keeps the caret and selection in a transparent native textarea while the backdrop paints visible glyphs and the hidden mirror determines the full draft height. The [single-scrollport decision](2026-07-31-composer-text-layers-share-one-scrollport.md) therefore depends on the textarea owning no scrollable overflow: after every draft commit, its `scrollHeight` and `clientHeight` are equal and its `scrollTop` is zero.
|
||||
|
||||
Safari 26.5.2 can retain the textarea's former native line layout when Backspace moves a draft across a soft-wrap threshold at the same time that React updates the mirror. In the reproduced two-line-to-one-line transition, the mirror, backdrop, grow stack, and textarea box all become 28px high, but the textarea still reports `scrollHeight=52` and `scrollTop=20`. The caret remains in the stale native line while the backdrop correctly paints one line.
|
||||
|
||||
The `color` declaration is not a layout input. Changing its inline style changes the computed color but leaves the stale `52/28/20` state intact. Editing the stylesheet rule happens to trigger broader rule invalidation and clears the state to `28/28/0`, which explains why Web Inspector makes the declaration appear causal.
|
||||
|
||||
## Decision
|
||||
|
||||
`InputBar` detects Safari once from the Apple vendor and the `Version/... Safari/...` user-agent form, while rejecting known alternate iOS browser tokens such as `CriOS`, `FxiOS`, `EdgiOS`, and `OPiOS`. A browser shell indistinguishable through these identity fields still has to violate the textarea overflow invariant before the recovery mutates layout.
|
||||
|
||||
The native textarea change handler records whether an edit shortens the controlled draft. After that draft commits, a layout effect returns without reading geometry unless both the cached Safari identity and the native-shrink signal are present. It then checks the single-scrollport invariant: equal `scrollHeight` and `clientHeight` are settled and trigger no forced layout. A mismatch first changes the textarea's real height by one pixel, forces layout, restores the owned height, and forces layout again. This rebuilds Safari's native text-control layout without changing the value, selection, IME state, or undo transaction.
|
||||
|
||||
The temporary native overflow can leave the draft scrollport's auto height at the former line count even after the textarea is correct. The recovery therefore repeats the one-pixel invalidation on `[data-input-scroll]` after repairing the textarea. Both elements return to their owned styles before paint; the settled one-line state is `scrollHeight=clientHeight=28`, `scrollTop=0`, and a 28px scrollport.
|
||||
|
||||
## Verification
|
||||
|
||||
Component tests synthesize Safari's stale metrics, assert the textarea-then-scrollport invalidation order, preserve selection, and prove that a growing native draft reads no geometry. Browser-identity tests cover desktop and mobile Safari, desktop Chromium, Chrome, Edge, and Opera on iOS, and an Apple web view.
|
||||
|
||||
The assembled package is also exercised in Safari 26.5.2 through the native 51-character-to-50-character Backspace path. Playwright WebKit 26.5 settles correctly without the workaround in both the assembled app and a reduced page, so the repository's Chromium browser lane cannot reproduce this Safari application defect; the focused component test pins the engine state until an automatable Safari lane exists.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Change `color` or use `-webkit-text-fill-color`.** Rejected because inline color changes and transparent text fill leave the stale native geometry unchanged. Stylesheet-rule editing works only because its invalidation scope is broader than the declaration's paint semantics.
|
||||
|
||||
**Set `scrollTop=0`.** Rejected because it moves the stale native content without rebuilding its two-line `scrollHeight`; the caret can become clipped instead of aligned.
|
||||
|
||||
**Rewrite the textarea value.** Clearing and restoring the value rebuilds Safari's text control, but it mutates the editing state that owns IME composition and selection. The height invalidation leaves the value untouched.
|
||||
|
||||
**Use `field-sizing: content`.** Rejected because Safari reproduces the stale two-line intrinsic height after the same deletion, and the composer still needs the mirror as the caret ruler and backdrop metric peer.
|
||||
|
||||
**Invalidate only the textarea or only the scrollport.** Rejected because the textarea-only recovery clears `52/28/20` but can leave the scrollport at 52px, while the scrollport-only recovery leaves the textarea's native overflow untouched. The ordered pair is the smallest complete recovery.
|
||||
|
||||
**Check geometry after every Safari draft commit.** Rejected because reading `scrollHeight` or `clientHeight` after React changes the mirror can synchronously lay out even a healthy growing draft. A native shortening signal limits the invariant read to edits that can produce the observed shrink defect.
|
||||
|
||||
**Run the recovery in every browser.** Rejected because Chromium, Playwright WebKit, and Firefox maintain the invariant without forced layouts. The Safari identity and observed mismatch jointly bound the synchronous work.
|
||||
|
||||
## Consequences
|
||||
|
||||
Non-Safari browsers, programmatic draft updates, and native edits that do not shorten the draft perform no geometry read. A native Safari shortening reads the overflow invariant and pays the four forced layouts only when the textarea violates it. The exceptional path accepts rare local work before paint to preserve caret alignment, native editing semantics, and the single scrolling box. An equivalent stale state caused only by resize or sidebar width changes has not been observed and is outside this recovery trigger. The browser test gap remains explicit: real Safari evidence owns the engine defect, while deterministic component coverage owns the recovery and its browser gate.
|
||||
@@ -0,0 +1,47 @@
|
||||
# Agent Note: Safari textarea 软换行收缩恢复
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-08-13-safari-textarea-soft-wrap-reflow.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
composer 把光标与选区留在透明的原生 textarea 中,由 backdrop 绘制可见字形,并由隐藏的镜像层决定完整草稿高度。因此,[单滚动容器决策](2026-07-31-composer-text-layers-share-one-scrollport.md)依赖 textarea 不持有可滚动溢出:每次草稿提交后,它的 `scrollHeight` 与 `clientHeight` 相等,`scrollTop` 为零。
|
||||
|
||||
当 Backspace 让草稿跨过软换行阈值,同时 React 更新镜像层时,Safari 26.5.2 可能保留 textarea 原先的原生行布局。在复现出的两行变一行转换中,镜像层、backdrop、自增高栈和 textarea 盒都变为 28px 高,但 textarea 仍报告 `scrollHeight=52` 与 `scrollTop=20`。光标留在陈旧的原生行中,而 backdrop 已正确绘制为一行。
|
||||
|
||||
`color` 声明不是布局输入。修改 inline style 会改变计算后的颜色,却让陈旧的 `52/28/20` 状态保持不变。编辑样式表规则会碰巧触发范围更广的规则失效,并把状态清为 `28/28/0`,这正是 Web Inspector 让该声明显得像成因的原因。
|
||||
|
||||
## 决策
|
||||
|
||||
`InputBar` 通过 Apple vendor 与 `Version/... Safari/...` 形式的 user agent 一次性识别 Safari,同时排除 `CriOS`、`FxiOS`、`EdgiOS`、`OPiOS` 等已知的 iOS 其他浏览器 token。仅凭这些 identity 字段无法区分的浏览器壳仍必须先违反 textarea 溢出不变量,恢复逻辑才会修改布局。
|
||||
|
||||
原生 textarea change handler 会记录本次编辑是否缩短受控草稿。草稿提交后,除非同时存在已缓存的 Safari identity 与原生缩短信号,否则 layout effect 会在读取几何前直接返回。随后它才检查单滚动容器不变量:`scrollHeight` 与 `clientHeight` 相等即为稳定态,不会触发强制布局。出现差异时,逻辑先把 textarea 的实际高度改变一个像素,强制布局,再恢复其自有高度并再次强制布局。这样无需改变值、选区、输入法组合状态或撤销事务,即可重建 Safari 的原生文本控件布局。
|
||||
|
||||
即使 textarea 已正确恢复,临时的原生溢出仍可能让草稿滚动容器的 auto 高度停在原行数。因此,恢复逻辑会在修复 textarea 后,对 `[data-input-scroll]` 重复一次单像素失效。两个元素都会在绘制前恢复各自拥有的样式;稳定的一行状态为 `scrollHeight=clientHeight=28`、`scrollTop=0`,滚动容器高度为 28px。
|
||||
|
||||
## 验证
|
||||
|
||||
组件测试会合成 Safari 的陈旧度量,断言先 textarea 后滚动容器的失效顺序,保留选区,并证明原生草稿增长不会读取几何。浏览器 identity 测试覆盖桌面与移动 Safari、桌面 Chromium、iOS Chrome/Edge/Opera 和 Apple web view。
|
||||
|
||||
组装后的包还会在 Safari 26.5.2 中通过原生的 51 字符到 50 字符 Backspace 路径验证。Playwright WebKit 26.5 在组装应用与最小化页面中都无需本绕法即可正确稳定,因此仓库的 Chromium 浏览器泳道无法复现这个 Safari 应用缺陷;在可自动化的 Safari 泳道出现之前,由聚焦组件测试固定该引擎状态。
|
||||
|
||||
## 备选方案
|
||||
|
||||
**修改 `color` 或使用 `-webkit-text-fill-color`。** 被否决,因为 inline color 修改与透明 text fill 都不会改变陈旧的原生几何。编辑样式表规则之所以有效,只是因为其失效范围比该声明的绘制语义更广。
|
||||
|
||||
**设置 `scrollTop=0`。** 被否决,因为这只会移动陈旧的原生内容,不会重建其两行 `scrollHeight`;光标可能从错位变为被裁剪。
|
||||
|
||||
**重写 textarea 的值。** 清空再恢复值能够重建 Safari 文本控件,但会改动拥有输入法组合与选区的编辑状态。高度失效不会触碰值。
|
||||
|
||||
**使用 `field-sizing: content`。** 被否决,因为相同删除后 Safari 的两行固有高度仍会陈旧,并且 composer 仍需要镜像层充当光标标尺与 backdrop 的度量对端。
|
||||
|
||||
**只让 textarea 或滚动容器失效。** 被否决,因为只恢复 textarea 虽能清除 `52/28/20`,却可能把滚动容器留在 52px;只恢复滚动容器则不会改变 textarea 的原生溢出。这个有序二元操作是最小的完整恢复。
|
||||
|
||||
**每次 Safari 草稿提交后都检查几何。** 被否决,因为 React 改变镜像层后读取 `scrollHeight` 或 `clientHeight`,即使草稿健康增长也可能同步执行布局。原生缩短信号把不变量读取限制在可能产生已观测收缩缺陷的编辑中。
|
||||
|
||||
**在所有浏览器中运行恢复逻辑。** 被否决,因为 Chromium、Playwright WebKit 与 Firefox 无需强制布局即可维持该不变量。Safari identity 与已观测到的差异共同限定同步工作范围。
|
||||
|
||||
## 影响
|
||||
|
||||
非 Safari 浏览器、程序化草稿更新,以及不会缩短草稿的原生编辑都不会读取几何。Safari 的原生缩短会读取溢出不变量,并且仅在 textarea 违反不变量时承担四次强制布局。例外路径以绘制前的罕见局部工作换取光标对齐、原生编辑语义与单一滚动盒。尚未观测到仅由 resize 或侧栏宽度变化引发的同类陈旧状态,本恢复触发器也不覆盖它。浏览器测试缺口保持显式:真实 Safari 证据负责引擎缺陷,确定性的组件覆盖负责恢复逻辑与浏览器门控。
|
||||
@@ -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-15-max-token-replay-state-alignment.md
|
||||
2026-08-15-max-token-replay-state-alignment.md: 256a64403a08377cf35ba645175698678eaa7f8b
|
||||
2026-08-15-max-token-replay-state-alignment.zh.md: a24f3e194dca100d2ea0faf659b1868c605c26c1
|
||||
@@ -0,0 +1,33 @@
|
||||
# Agent Note: Replay state aligns with assembled content by construction
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-08-15-max-token-replay-state-alignment.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
pi-ai recorded one opaque replay blob per response, projected from the provider's native message, while `BlockAssembler.blocks()` separately dropped tool calls from a `max-tokens` response because a truncated call is unsafe to execute. The durable assistant message therefore stored transformed content next to metadata describing the untransformed native block list. The next request failed during history reconstruction with `INVALID_REPLAY_STATE: block count does not match assistant content`, and because the mismatch was already on disk, every later request on that session failed the same way — the session was permanently stuck. The root cause is structural: two representations of one response were snapshotted at different pipeline points, with their index alignment enforced only by a read-time hard error.
|
||||
|
||||
## Decision
|
||||
|
||||
Two changes, one per side of the durable boundary.
|
||||
|
||||
**Write side — one keep/drop decision.** The finish chunk's `replayState` becomes a typed `ReplayEnvelope`: an opaque `response` half plus optional opaque per-block entries aligned with the emitted block sequence. `BlockAssembler` computes its keep/drop decision once and applies it to blocks and envelope entries together, so any transformation assembly performs — today's max-token tool-call drop or a future one — prunes the matching metadata by construction. Retained blocks keep their entries, so a truncated response keeps signatures for the reasoning and text it kept. An envelope whose entries do not match the emitted block count is discarded whole (a misemitting adapter must not publish misattributed metadata). pi-ai splits its former flat state into a version-2 response half and per-block signature entries.
|
||||
|
||||
**Read side — durable content is authoritative.** `toPiAssistant` treats replay state as fidelity metadata, not as a load-bearing input: any state the reading build cannot use — another adapter's kind, another version (including the flat version-1 form already on disk), malformed metadata, or a block shape that no longer matches the content — degrades that one message to the existing foreign provider-neutral conversion and reports the `INVALID_REPLAY_STATE` diagnostic through the plugin's `onReplayDegrade` hook (a logger warning). The request proceeds. This is what lets sessions poisoned before this change continue instead of erroring forever, and it bounds every future divergence source to a fidelity loss on one message.
|
||||
|
||||
## Verification
|
||||
|
||||
Assembler unit tests prove pruning, misalignment discard, and pass-through for untransformed and per-block-free envelopes. pi-ai unit tests prove the version-2 envelope round-trip and that every formerly-throwing invalid-state case now degrades to foreign conversion with the diagnostic. An agent-loop regression drives a truncated text-plus-tool-call response through persistence and shows the follow-up request carrying the pruned envelope. Keyless real-composition tests boot `dsh-llm-pi-ai` through the Loader and prove a native continuation without `tool_calls` after truncation, and a successful continuation over a legacy flat-state message whose block count no longer matches. The authored keyless snapshot scenario `max-tokens-continue` pins the assembled application's durable log — truncated turn, pruned envelope on the stored message, continued turn — through the real ACP subprocess path.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Suppress the whole replay state when assembly drops a tool call.** Works for today's one transformation, but re-derives the drop condition beside `blocks()` (the two drift silently), discards valid signatures for the retained blocks, and leaves read-time divergence — legacy sessions on disk foremost — a hard error.
|
||||
|
||||
**Keep the state and relax pi-ai's block-count validation to attach what fits.** Rejected: index-aligned signatures attached to a different block list would present false native history to the provider. Degrading attaches nothing.
|
||||
|
||||
**Teach each adapter to rewrite its state after assembly.** Rejected as an adapter obligation with an opaque blob; the envelope moves exactly the needed structure — and nothing else — into shared vocabulary, and the assembler's single decision does the rewrite mechanically.
|
||||
|
||||
## Consequences
|
||||
|
||||
Continuing after a max-token response that included a tool call works, retains the kept blocks' native signatures, and replays as a native pi-ai message. Sessions recorded before this change replay their affected assistant messages as provider-neutral content (with a diagnostic) instead of failing the turn; on-disk `replayState` values changed shape under the pre-release no-compatibility stance, with the old flat form handled by the same degrade path. This supersedes the read-time hard-error rule in the [provider-routed adapter decision](../architecture/2026-07-14-provider-routed-llm-adapters.md) for unusable state; validation itself is unchanged and still precedes any native reconstruction.
|
||||
@@ -0,0 +1,33 @@
|
||||
# Agent Note: 回放状态与组装内容按构造对齐
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-08-15-max-token-replay-state-alignment.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
pi-ai 为每个响应记录一个从提供方原生消息投影而来的不透明回放数据,而 `BlockAssembler.blocks()` 会另行从 `max-tokens` 响应中丢弃工具调用,因为被截断的调用不能安全执行。持久化的 assistant 消息因此把变换后的内容与描述未变换原生块清单的元数据存在一起。下一个请求在历史重建阶段以 `INVALID_REPLAY_STATE: block count does not match assistant content` 失败;由于不一致已经落盘,该会话之后的每个请求都以同样方式失败——会话被永久卡死。根因是结构性的:同一响应的两种表示在流水线的不同位置各自拍摄快照,其索引对齐只靠读取时的硬错误来维持。
|
||||
|
||||
## 决定
|
||||
|
||||
两处改动,各覆盖持久化边界的一侧。
|
||||
|
||||
**写侧——一次保留/丢弃决定。** finish 分片的 `replayState` 变为有类型的 `ReplayEnvelope`:一个不透明的 `response` 半区,加上与发射块序列对齐的可选不透明逐块条目。`BlockAssembler` 只计算一次保留/丢弃决定,并把它同时应用于块和逐块条目,因此组装执行的任何变换——今天的 max-token 工具调用丢弃或未来的其他变换——都按构造裁剪掉对应元数据。保留的块保留其条目,所以被截断的响应仍为其保留的推理(reasoning)与文本保有签名。条目数与发射块数不一致的数据整体丢弃(发射不当的适配器不得发布归属错误的元数据)。pi-ai 把原先的平铺状态拆为版本 2 的 response 半区和逐块签名条目。
|
||||
|
||||
**读侧——持久化内容是权威记录。** `toPiAssistant` 把回放状态当作保真度元数据,而非承重输入:读取方无法使用的任何状态——其他适配器的 kind、其他版本(包括已落盘的平铺版本 1 形式)、格式错误的元数据、或与内容不再匹配的块结构——都把这一条消息降级为既有的外来提供方无关转换,并通过插件的 `onReplayDegrade` 钩子(logger 警告)上报 `INVALID_REPLAY_STATE` 诊断。请求继续执行。正是这一点让本次改动之前已被毒化的会话得以继续而不是永远报错,也把未来一切分叉源约束为单条消息的保真度损失。
|
||||
|
||||
## 验证
|
||||
|
||||
组装器单元测试证明裁剪、错位丢弃、以及未变换与无逐块条目数据的透传。pi-ai 单元测试证明版本 2 数据的往返,以及先前每个抛错的无效状态用例现在都降级为外来转换并带出诊断。agent loop 回归用例驱动一个被截断的文本加工具调用响应穿过持久化,并证明后续请求携带裁剪后的数据。无密钥真实组合测试通过 loader 启动 `dsh-llm-pi-ai`,证明截断后不带 `tool_calls` 的原生续聊,以及在块数不再匹配的旧平铺状态消息之上成功续聊。手工编写的无密钥快照场景 `max-tokens-continue` 通过真实 ACP 子进程路径钉住组装应用的持久化日志——截断轮次、存储消息上裁剪后的数据、以及继续的轮次。
|
||||
|
||||
## 已考虑的替代方案
|
||||
|
||||
**组装丢弃工具调用时抑制整个回放状态。** 对今天唯一的变换有效,但在 `blocks()` 旁边重新推导丢弃条件(两处会无声漂移),丢掉保留块的有效签名,并让读取时的分叉——首当其冲是已落盘的旧会话——仍然是硬错误。
|
||||
|
||||
**保留状态并放宽 pi-ai 的块数校验、能贴多少贴多少。** 否决:索引对齐的签名贴到不同的块清单上,会向提供方呈现虚假的原生历史。降级则什么都不贴。
|
||||
|
||||
**让每个适配器在组装后改写自己的状态。** 否决:这把义务压给持有不透明数据的适配器;信封只把恰好需要的结构——不多一分——纳入共享词汇,组装器的单一决定即可机械完成改写。
|
||||
|
||||
## 影响
|
||||
|
||||
包含工具调用的 max-token 响应之后的续聊可以工作,保留块保有原生签名,并作为原生 pi-ai 消息回放。本次改动之前记录的会话,其受影响的 assistant 消息作为提供方无关内容回放(带诊断)而不是让轮次失败;`replayState` 落盘形状在预发布无兼容承诺立场下发生变化,旧平铺形式由同一降级路径处理。对不可用状态而言,这取代了[提供方路由适配器决定](../architecture/2026-07-14-provider-routed-llm-adapters.md)中读取时硬错误的规则;校验本身不变,仍先于任何原生重建执行。
|
||||
@@ -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-15-persistent-bash-keeps-controlled-prompt.md
|
||||
2026-08-15-persistent-bash-keeps-controlled-prompt.md: 9ee71f2adc0d473c5490dbe2b29ce55c8377e275
|
||||
2026-08-15-persistent-bash-keeps-controlled-prompt.zh.md: 48b2cc323e745a184cefc9603d461fe8fb27e15e
|
||||
@@ -0,0 +1,35 @@
|
||||
# Agent Note: Persistent bash keeps the backend's controlled prompt
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-08-15-persistent-bash-keeps-controlled-prompt.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
`dsh-tool-bash-persistent` initialized its shell with `stty -echo; PS1='__DSH_PERSISTENT_BASH_PROMPT__ '`, overwriting the `PS1` that `dsh-terminal-bash` sets in the spawn environment. The backend's prompt readiness requires the printable tail after the OSC `133;D` marker to exactly equal the controlled prompt ([design](../feature/2026-07-16-persistent-pty-sessions.md)), so after initialization no send could ever settle through it. `PROMPT_COMMAND` survived the override, so the marker kept arriving and every send paid the silence tier plus handoff grace — 3.5 s per tool call under production defaults, 7.2 s for the first call because the initialization send degraded too, and an extra 3.5 s tail after every long command. macOS has no exact stdin-wait tier, and on Linux the exact probe cannot observe a sub-poll-interval command leaving its stdin wait, so the degradation applied to effectively every call. Package tests masked it by configuring `idleSilenceMs: 100`.
|
||||
|
||||
The override existed to give the tool a known prompt for two consumers: a viewport-suffix fallback that detected "shell at a prompt without the end marker", and cosmetic stripping of prompt text from partial output.
|
||||
|
||||
## Decision
|
||||
|
||||
The backend owns its prompt protocol and repairs it itself: the controlled `PROMPT_COMMAND` re-asserts `PS1` after printing the marker, so any in-shell prompt override — this tool's former initialization, a model command, a sourced script — lasts zero prompts. This also protects providers that cannot report foreground state, where the exact prompt text is the only readiness evidence.
|
||||
|
||||
The tool stops overwriting `PS1` (initialization is `stty -echo` alone) and replaces its viewport-suffix fallback with the seam's existing signal: a send that settles as `stdin_read` without the end marker in scrollback returns the captured partial output. The private prompt constant and its stripping are deleted; partial output may now end with the backend's own prompt text, which the tool cannot and should not know.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Fix only the tool, leaving `PROMPT_COMMAND` unchanged.** Rejected because the seam would stay silently fragile: any later consumer or model command that touches `PS1` reintroduces the 3.5 s degradation with no failing signal, and providers without foreground inspection lose their only readiness factor.
|
||||
|
||||
**Import the controlled prompt into the tool.** Rejected because the prompt is one provider's protocol constant; a Consumer matching it would couple the tool to `dsh-terminal-bash` specifically, and any other mounted backend would break it again.
|
||||
|
||||
**Drop the prompt-text factor from backend readiness.** Rejected because for providers whose `inspectForeground` reports nothing, marker-plus-text is the defense against command output that embeds the raw OSC marker sequence; weakening it trades a fast path for a false-settle risk.
|
||||
|
||||
**Widen `handoffGraceMs`/`idleSilenceMs` tuning instead.** Rejected because no silence value fixes a dead fast path; it only rebalances how much every call overpays.
|
||||
|
||||
## Consequences
|
||||
|
||||
Measured on darwin with production defaults: raw sends settle in ~86 ms with the controlled prompt intact versus ~3540 ms after an override; tool calls drop from 7180/3560/3566 ms to 355/88/91 ms for spawn+init+echo, echo, and pwd.
|
||||
|
||||
The `stdin_read` fallback is behavior, not only cosmetics: after `exec`, an interrupt, or an interactive foreground child whose stdin wait the provider proves (the Linux exact tier), the call now returns captured partial output instead of spinning to the command deadline. Where no provider proves the wait (macOS), an interactive child still runs to `timeoutMs` — recorded as a known limitation in the tool README. Partial output can carry the backend's trailing prompt; complete marker-delimited output is byte-identical to before, which the keyless jsonrpc-agent snapshots confirm.
|
||||
|
||||
The loader-composition suite now sets `idleSilenceMs` above the send bound, so silence can settle nothing and every case fails if prompt readiness regresses; a real-PTY case overrides `PS1` in-shell and requires the next send to settle as `stdin_read` with the healed prompt. The self-repair cannot survive a command that overwrites `PROMPT_COMMAND` itself; the silence tier remains the bound there, unchanged from the prior design.
|
||||
@@ -0,0 +1,35 @@
|
||||
# Agent Note: 持久 bash 保留后端的受控提示符
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-08-15-persistent-bash-keeps-controlled-prompt.md) | 中文
|
||||
|
||||
## Problem
|
||||
|
||||
`dsh-tool-bash-persistent` 用 `stty -echo; PS1='__DSH_PERSISTENT_BASH_PROMPT__ '` 初始化其 shell,覆盖了 `dsh-terminal-bash` 在 spawn 环境中设定的 `PS1`。后端的提示符就绪检测要求 OSC `133;D` 标记之后的可打印尾部与受控提示符完全相等([设计](../feature/2026-07-16-persistent-pty-sessions.md)),因此初始化之后任何 send 都无法经由该路径结算。`PROMPT_COMMAND` 未被覆盖,标记仍持续到达,于是每次 send 都要支付静默层加交接宽限——生产默认值下每次工具调用 3.5 秒;首次调用 7.2 秒,因为初始化 send 同样退化;每条长命令结束后还要多等 3.5 秒。macOS 没有精确 stdin 等待层,而 Linux 的精确探测无法观察到在一个轮询周期内完成的命令脱离其 stdin 等待,因此退化实际覆盖了几乎每次调用。包测试把 `idleSilenceMs` 配成 100 毫秒,掩盖了该问题。
|
||||
|
||||
这个覆盖存在的目的是给工具一个已知提示符,服务两个消费点:用视口后缀检测「shell 已回到提示符但没有结束标记」的回退判定,以及从部分输出中剥离提示符文本的美化。
|
||||
|
||||
## Decision
|
||||
|
||||
后端拥有自己的提示符协议并自行修复:受控 `PROMPT_COMMAND` 在打印标记后重新设定 `PS1`,因此任何 shell 内的提示符覆盖——本工具从前的初始化、模型命令、被 source 的脚本——都存活不到下一个提示符。这同时保护了无法报告前台状态的提供方:在那里,确切的提示符文本是唯一的就绪证据。
|
||||
|
||||
工具不再覆盖 `PS1`(初始化只剩 `stty -echo`),并用 seam 已有的信号替换其视口后缀回退:一次以 `stdin_read` 结算而 scrollback 中没有结束标记的 send,返回已捕获的部分输出。私有提示符常量及其剥离逻辑删除;部分输出现在可能以后端自己的提示符文本结尾,工具无法也不应知道该文本。
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**只改工具,不动 `PROMPT_COMMAND`。** 被拒绝:seam 仍然静默脆弱——之后任何触碰 `PS1` 的消费方或模型命令都会在没有失败信号的情况下重新引入 3.5 秒退化,且无前台检查的提供方失去唯一的就绪因子。
|
||||
|
||||
**把受控提示符导入工具。** 被拒绝:提示符是单个提供方的协议常量;Consumer 匹配它就把工具与 `dsh-terminal-bash` 具体耦合,换任何其他后端都会再次损坏。
|
||||
|
||||
**从后端就绪检测中去掉提示符文本因子。** 被拒绝:对 `inspectForeground` 无法报告任何信息的提供方而言,标记加文本是对抗「命令输出中嵌入原始 OSC 标记序列」的防御;削弱它是拿误结算风险换快速路径。
|
||||
|
||||
**改为调大 `handoffGraceMs`/`idleSilenceMs`。** 被拒绝:任何静默值都修不好已死的快速路径,只是重新分配每次调用多付多少。
|
||||
|
||||
## Consequences
|
||||
|
||||
darwin 上以生产默认值实测:受控提示符完好时裸 send 约 86 毫秒结算,覆盖后约 3540 毫秒;工具调用从 7180/3560/3566 毫秒(spawn+init+echo、echo、pwd)降至 355/88/91 毫秒。
|
||||
|
||||
`stdin_read` 回退是行为而不只是美化:在 `exec`、中断,或提供方能证明其 stdin 等待的交互式前台子进程(Linux 精确层)之后,调用现在返回已捕获的部分输出,而不是空转到命令期限。没有提供方证明该等待时(macOS),交互式子进程仍会运行到 `timeoutMs`——已记入工具 README 的已知限制。部分输出可能带有后端的尾部提示符;由标记界定的完整输出与之前逐字节相同,无密钥 jsonrpc-agent 快照确认了这一点。
|
||||
|
||||
loader 组合套件现在把 `idleSilenceMs` 设在 send 上限之上,静默无法结算任何 send,提示符就绪一旦回归,每个用例都会失败;一个真实 PTY 用例在 shell 内覆盖 `PS1`,并要求下一次 send 以 `stdin_read` 结算且提示符已修复。自我修复无法在 `PROMPT_COMMAND` 本身被覆盖的命令后存活;那里静默层仍是边界,与先前设计一致。
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user